@jcheesepkg/nanobot 0.6.5 → 0.6.7

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.
@@ -1 +1 @@
1
- {"version":3,"file":"context.d.mts","names":[],"sources":["../../src/agent/context.ts"],"mappings":";;;;;KA0CY,aAAA;EACV,IAAA;EACA,KAAA;EACA,QAAA;EACA,IAAA;AAAA;;;;cAsDW,cAAA;EAAA,QACH,SAAA;EAAA,SACC,MAAA,EAAQ,WAAA;EAAA,SACR,MAAA,EAAQ,YAAA;cAEL,SAAA;;EAOZ,iBAAA,CAAA;EAAA,QAyCQ,WAAA;EAAA,QAoEA,kBAAA;EAmBJ;EANJ,aAAA,CAAc,MAAA;IACZ,OAAA,EAAS,WAAA;IACT,cAAA;IACA,KAAA;IACA,OAAA;IACA,MAAA;EAAA,IACE,WAAA;EAAA,QAyBI,gBAAA;EAnKC;EAwMT,aAAA,CACE,QAAA,EAAU,WAAA,IACV,UAAA,UACA,QAAA,UACA,MAAA,WACC,WAAA;EA5MM;EAuNT,mBAAA,CACE,QAAA,EAAU,WAAA,IACV,OAAA,iBACA,SAAA,GAAY,KAAA;IACV,EAAA;IACA,IAAA;IACA,QAAA;MAAY,IAAA;MAAc,SAAA;IAAA;EAAA,KAE3B,WAAA;AAAA"}
1
+ {"version":3,"file":"context.d.mts","names":[],"sources":["../../src/agent/context.ts"],"mappings":";;;;;KA0CY,aAAA;EACV,IAAA;EACA,KAAA;EACA,QAAA;EACA,IAAA;AAAA;;;;cAsDW,cAAA;EAAA,QACH,SAAA;EAAA,SACC,MAAA,EAAQ,WAAA;EAAA,SACR,MAAA,EAAQ,YAAA;cAEL,SAAA;;EAOZ,iBAAA,CAAA;EAAA,QAyCQ,WAAA;EAAA,QAqEA,kBAAA;EAmBJ;EANJ,aAAA,CAAc,MAAA;IACZ,OAAA,EAAS,WAAA;IACT,cAAA;IACA,KAAA;IACA,OAAA;IACA,MAAA;EAAA,IACE,WAAA;EAAA,QAyBI,gBAAA;EApKC;EAyMT,aAAA,CACE,QAAA,EAAU,WAAA,IACV,UAAA,UACA,QAAA,UACA,MAAA,WACC,WAAA;EA7MM;EAwNT,mBAAA,CACE,QAAA,EAAU,WAAA,IACV,OAAA,iBACA,SAAA,GAAY,KAAA;IACV,EAAA;IACA,IAAA;IACA,QAAA;MAAY,IAAA;MAAc,SAAA;IAAA;EAAA,KAE3B,WAAA;AAAA"}
@@ -124,7 +124,7 @@ ${identity.name ? `You are ${agentName}, a personal AI assistant.` : "You are a
124
124
 
125
125
  ## Current Time
126
126
  ${dateStr} (${dayName})
127
- Timezone: ${tz}
127
+ Timezone: ${tz}${tz === "UTC" ? "\nNote: Timezone is not yet configured. Do NOT assume the user's local time or make time-of-day references (morning, night, etc.). Ask the user where they are so you can set their timezone." : ""}
128
128
 
129
129
  ## Workspace
130
130
  Your workspace is at: ${this.workspace}
@@ -143,6 +143,7 @@ For normal conversation, just respond with text - do not call the message tool.
143
143
 
144
144
  IMPORTANT: When you decide to use tools, ALWAYS include a brief text message alongside your tool calls.
145
145
  This text is sent to the user immediately so they know you're working. Keep it natural and conversational.
146
+ Never mention internal file names (IDENTITY.md, USER.md, SOUL.md, MEMORY.md, etc.) to the user. These are implementation details. Just act naturally — save things silently, don't narrate file operations.
146
147
 
147
148
  ## Installing Skills
148
149
  When asked to install a skill (from a URL, file, or any source):
@@ -1 +1 @@
1
- {"version":3,"file":"context.mjs","names":[],"sources":["../../src/agent/context.ts"],"sourcesContent":["import { readFileSync, existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { ChatMessage, ContentPart } from \"../providers/base.js\";\nimport { MemoryStore } from \"./memory.js\";\nimport { SkillsLoader } from \"./skills.js\";\n\n/**\n * Resolve user timezone from USER.md.\n * Looks for a line like: Timezone: Asia/Tokyo\n * Falls back to host detection / UTC.\n */\nfunction resolveTimezone(workspace: string): string {\n const userMd = join(workspace, \"USER.md\");\n if (existsSync(userMd)) {\n try {\n const content = readFileSync(userMd, \"utf-8\");\n const tzMatch = content.match(/^timezone:\\s*(.+)/im);\n if (tzMatch) {\n const tz = tzMatch[1].trim();\n if (tz && !tz.includes(\"not set\")) {\n // Validate IANA timezone\n new Intl.DateTimeFormat(\"en-US\", { timeZone: tz }).format(new Date());\n return tz;\n }\n }\n } catch {\n // Invalid value or read error, fall through\n }\n }\n\n const host = Intl.DateTimeFormat().resolvedOptions().timeZone;\n return host?.trim() || \"UTC\";\n}\n\n/** Placeholder values in IDENTITY.md that mean \"not yet filled in\". */\nconst IDENTITY_PLACEHOLDERS = new Set([\n \"(not set)\",\n \"(pick something you like)\",\n \"(your signature)\",\n \"(how do you come across?)\",\n]);\n\nexport type AgentIdentity = {\n name?: string;\n emoji?: string;\n creature?: string;\n vibe?: string;\n};\n\n/**\n * Parse IDENTITY.md from the workspace to resolve the agent's chosen identity.\n * Returns fields that have been filled in (skips placeholders).\n */\nfunction resolveIdentity(workspace: string): AgentIdentity {\n const identity: AgentIdentity = {};\n const identityMd = join(workspace, \"IDENTITY.md\");\n if (!existsSync(identityMd)) return identity;\n\n try {\n const content = readFileSync(identityMd, \"utf-8\");\n const lines = content.split(/\\r?\\n/);\n for (const line of lines) {\n // Strip list markers and bold markers\n const cleaned = line.trim().replace(/^\\s*-\\s*/, \"\");\n const colonIndex = cleaned.indexOf(\":\");\n if (colonIndex === -1) continue;\n\n const label = cleaned.slice(0, colonIndex).replace(/[*_]/g, \"\").trim().toLowerCase();\n const value = cleaned.slice(colonIndex + 1).replace(/^[*_]+|[*_]+$/g, \"\").trim();\n if (!value) continue;\n\n // Check if it's a placeholder\n const normalized = value.toLowerCase().replace(/[\\u2013\\u2014]/g, \"-\").replace(/\\s+/g, \" \");\n if (IDENTITY_PLACEHOLDERS.has(normalized)) continue;\n // Also check if it looks like a template hint wrapped in parens or italics\n if (/^\\(.*\\)$/.test(value) || /^_.*_$/.test(value)) continue;\n\n if (label === \"name\") identity.name = value;\n if (label === \"emoji\") identity.emoji = value;\n if (label === \"creature\") identity.creature = value;\n if (label === \"vibe\") identity.vibe = value;\n }\n } catch {\n // Read error, return empty\n }\n\n return identity;\n}\n\nconst BOOTSTRAP_FILES = [\n \"AGENTS.md\",\n \"SOUL.md\",\n \"USER.md\",\n \"TOOLS.md\",\n \"IDENTITY.md\",\n];\n\n/**\n * Builds the context (system prompt + messages) for the agent.\n */\nexport class ContextBuilder {\n private workspace: string;\n readonly memory: MemoryStore;\n readonly skills: SkillsLoader;\n\n constructor(workspace: string) {\n this.workspace = workspace;\n this.memory = new MemoryStore(workspace);\n this.skills = new SkillsLoader(workspace);\n }\n\n /** Build the system prompt from bootstrap files, memory, and skills. */\n buildSystemPrompt(): string {\n const parts: string[] = [];\n\n // Core identity\n parts.push(this.getIdentity());\n\n // Bootstrap files\n const bootstrap = this.loadBootstrapFiles();\n if (bootstrap) parts.push(bootstrap);\n\n // Memory context\n const memory = this.memory.getMemoryContext();\n if (memory) parts.push(`# Memory\\n\\n${memory}`);\n\n // Always-loaded skills\n const alwaysSkills = this.skills.getAlwaysSkills();\n if (alwaysSkills.length > 0) {\n console.log(`Skills always-loaded: ${alwaysSkills.join(\", \")}`);\n const alwaysContent = this.skills.loadSkillsForContext(alwaysSkills);\n if (alwaysContent) {\n parts.push(`# Active Skills\\n\\n${alwaysContent}`);\n }\n }\n\n // Available skills summary\n const skillsSummary = this.skills.buildSkillsSummary();\n if (skillsSummary) {\n parts.push(\n `# Skills (mandatory)\\n\\n` +\n `Before replying, scan the <skills> entries below.\\n` +\n `- If a skill clearly applies to the user's request: read its SKILL.md at the <location> path using read_file, then follow its instructions.\\n` +\n `- If multiple skills could apply: choose the most specific one, then read and follow it.\\n` +\n `- If no skill applies: respond normally without reading any SKILL.md.\\n` +\n `- Never improvise when a matching skill exists. Always read and follow the skill first.\\n\\n` +\n skillsSummary,\n );\n }\n\n return parts.join(\"\\n\\n---\\n\\n\");\n }\n\n private getIdentity(): string {\n const now = new Date();\n const tz = resolveTimezone(this.workspace);\n const identity = resolveIdentity(this.workspace);\n const dateStr = now.toLocaleString(\"en-US\", { timeZone: tz, year: \"numeric\", month: \"2-digit\", day: \"2-digit\", hour: \"2-digit\", minute: \"2-digit\", hour12: false }).replace(/\\//g, \"-\");\n const dayName = now.toLocaleDateString(\"en-US\", { timeZone: tz, weekday: \"long\" });\n\n // Use identity name if set, otherwise generic\n const agentName = identity.name || \"kodama\";\n const identityLine = identity.name\n ? `You are ${agentName}, a personal AI assistant.`\n : \"You are a personal AI assistant running inside kodamabot.\";\n\n return `# ${agentName}\n\n${identityLine} You have access to tools that allow you to:\n- Read, write, and edit files\n- Execute shell commands\n- Search the web and fetch web pages\n- Send messages to users on chat channels\n- Spawn subagents for complex background tasks\n\n## Current Time\n${dateStr} (${dayName})\nTimezone: ${tz}\n\n## Workspace\nYour workspace is at: ${this.workspace}\n- Identity: ${this.workspace}/IDENTITY.md\n- User info: ${this.workspace}/USER.md\n- Persona: ${this.workspace}/SOUL.md\n- Instructions: ${this.workspace}/AGENTS.md\n- Heartbeat: ${this.workspace}/HEARTBEAT.md\n- Memory files: ${this.workspace}/memory/MEMORY.md\n- Daily notes: ${this.workspace}/memory/YYYY-MM-DD.md\n- Custom skills: ${this.workspace}/skills/{skill-name}/SKILL.md\n\nIMPORTANT: When responding to direct questions or conversations, reply directly with your text response.\nOnly use the 'message' tool when you need to send a message to a specific chat channel.\nFor normal conversation, just respond with text - do not call the message tool.\n\nIMPORTANT: When you decide to use tools, ALWAYS include a brief text message alongside your tool calls.\nThis text is sent to the user immediately so they know you're working. Keep it natural and conversational.\n\n## Installing Skills\nWhen asked to install a skill (from a URL, file, or any source):\n1. Fetch/read the skill content\n2. ALWAYS install it to: ${this.workspace}/skills/{skill-name}/SKILL.md\n3. If the skill references a different workspace path (e.g. .moltbot/, .otherbot/, etc.), rewrite ALL paths to use ${this.workspace}/ instead\n4. Preserve the skill's frontmatter, instructions, scripts, references, and assets — only change the workspace paths\n\n## Identity & Onboarding\nIf IDENTITY.md has no Name set, and USER.md contains \"(not set)\" values, this is a new user.\nOn their FIRST message, greet them warmly and ask:\n1. What they'd like to call you (your name — you're becoming someone, not just a chatbot)\n2. What they'd like to be called\n3. Where they're located (to determine timezone, e.g. Asia/Tokyo, America/New_York)\n\nThen update IDENTITY.md and USER.md with the real values using the edit_file tool. Use IANA timezone format.\nOnly do this ONCE — if both files already have real values, skip onboarding.\n\n## Persona\nIf SOUL.md is present, embody its persona and tone. Avoid stiff, generic replies; follow its guidance unless higher-priority instructions override it.\n\nAlways be helpful, accurate, and concise. When using tools, explain what you're doing.\nWhen remembering something, write to ${this.workspace}/memory/MEMORY.md`;\n }\n\n private loadBootstrapFiles(): string {\n const parts: string[] = [];\n for (const filename of BOOTSTRAP_FILES) {\n const filePath = join(this.workspace, filename);\n if (existsSync(filePath)) {\n const content = readFileSync(filePath, \"utf-8\");\n parts.push(`## ${filename}\\n\\n${content}`);\n }\n }\n return parts.join(\"\\n\\n\");\n }\n\n /** Build the complete message list for an LLM call. */\n buildMessages(params: {\n history: ChatMessage[];\n currentMessage: string;\n media?: string[];\n channel?: string;\n chatId?: string;\n }): ChatMessage[] {\n const messages: ChatMessage[] = [];\n\n // System prompt\n let systemPrompt = this.buildSystemPrompt();\n if (params.channel && params.chatId) {\n systemPrompt += `\\n\\n## Current Session\\nChannel: ${params.channel}\\nChat ID: ${params.chatId}`;\n }\n messages.push({ role: \"system\", content: systemPrompt });\n\n // History — replay full rich messages (user, assistant w/ tool_calls, tool results, etc.)\n for (const msg of params.history) {\n messages.push(msg);\n }\n\n // Current message (with optional image attachments)\n const userContent = this.buildUserContent(\n params.currentMessage,\n params.media,\n );\n messages.push({ role: \"user\", content: userContent });\n\n return messages;\n }\n\n private buildUserContent(\n text: string,\n media?: string[],\n ): string | ContentPart[] {\n if (!media || media.length === 0) return text;\n\n const images: ContentPart[] = [];\n for (const filePath of media) {\n if (!existsSync(filePath)) continue;\n try {\n const data = readFileSync(filePath);\n const ext = filePath.split(\".\").pop()?.toLowerCase() ?? \"\";\n const mimeMap: Record<string, string> = {\n jpg: \"image/jpeg\",\n jpeg: \"image/jpeg\",\n png: \"image/png\",\n gif: \"image/gif\",\n webp: \"image/webp\",\n };\n const mime = mimeMap[ext];\n if (!mime) continue;\n\n const b64 = data.toString(\"base64\");\n images.push({\n type: \"image_url\",\n image_url: { url: `data:${mime};base64,${b64}` },\n });\n } catch {\n // skip unreadable files\n }\n }\n\n if (images.length === 0) return text;\n return [...images, { type: \"text\", text }];\n }\n\n /** Add a tool result to the message list. */\n addToolResult(\n messages: ChatMessage[],\n toolCallId: string,\n toolName: string,\n result: string,\n ): ChatMessage[] {\n messages.push({\n role: \"tool\",\n tool_call_id: toolCallId,\n name: toolName,\n content: result,\n });\n return messages;\n }\n\n /** Add an assistant message to the message list. */\n addAssistantMessage(\n messages: ChatMessage[],\n content: string | null,\n toolCalls?: Array<{\n id: string;\n type: \"function\";\n function: { name: string; arguments: string };\n }>,\n ): ChatMessage[] {\n const msg: ChatMessage = {\n role: \"assistant\",\n content: content ?? \"\",\n };\n if (toolCalls) {\n msg.tool_calls = toolCalls;\n }\n messages.push(msg);\n return messages;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"context.mjs","names":[],"sources":["../../src/agent/context.ts"],"sourcesContent":["import { readFileSync, existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { ChatMessage, ContentPart } from \"../providers/base.js\";\nimport { MemoryStore } from \"./memory.js\";\nimport { SkillsLoader } from \"./skills.js\";\n\n/**\n * Resolve user timezone from USER.md.\n * Looks for a line like: Timezone: Asia/Tokyo\n * Falls back to host detection / UTC.\n */\nfunction resolveTimezone(workspace: string): string {\n const userMd = join(workspace, \"USER.md\");\n if (existsSync(userMd)) {\n try {\n const content = readFileSync(userMd, \"utf-8\");\n const tzMatch = content.match(/^timezone:\\s*(.+)/im);\n if (tzMatch) {\n const tz = tzMatch[1].trim();\n if (tz && !tz.includes(\"not set\")) {\n // Validate IANA timezone\n new Intl.DateTimeFormat(\"en-US\", { timeZone: tz }).format(new Date());\n return tz;\n }\n }\n } catch {\n // Invalid value or read error, fall through\n }\n }\n\n const host = Intl.DateTimeFormat().resolvedOptions().timeZone;\n return host?.trim() || \"UTC\";\n}\n\n/** Placeholder values in IDENTITY.md that mean \"not yet filled in\". */\nconst IDENTITY_PLACEHOLDERS = new Set([\n \"(not set)\",\n \"(pick something you like)\",\n \"(your signature)\",\n \"(how do you come across?)\",\n]);\n\nexport type AgentIdentity = {\n name?: string;\n emoji?: string;\n creature?: string;\n vibe?: string;\n};\n\n/**\n * Parse IDENTITY.md from the workspace to resolve the agent's chosen identity.\n * Returns fields that have been filled in (skips placeholders).\n */\nfunction resolveIdentity(workspace: string): AgentIdentity {\n const identity: AgentIdentity = {};\n const identityMd = join(workspace, \"IDENTITY.md\");\n if (!existsSync(identityMd)) return identity;\n\n try {\n const content = readFileSync(identityMd, \"utf-8\");\n const lines = content.split(/\\r?\\n/);\n for (const line of lines) {\n // Strip list markers and bold markers\n const cleaned = line.trim().replace(/^\\s*-\\s*/, \"\");\n const colonIndex = cleaned.indexOf(\":\");\n if (colonIndex === -1) continue;\n\n const label = cleaned.slice(0, colonIndex).replace(/[*_]/g, \"\").trim().toLowerCase();\n const value = cleaned.slice(colonIndex + 1).replace(/^[*_]+|[*_]+$/g, \"\").trim();\n if (!value) continue;\n\n // Check if it's a placeholder\n const normalized = value.toLowerCase().replace(/[\\u2013\\u2014]/g, \"-\").replace(/\\s+/g, \" \");\n if (IDENTITY_PLACEHOLDERS.has(normalized)) continue;\n // Also check if it looks like a template hint wrapped in parens or italics\n if (/^\\(.*\\)$/.test(value) || /^_.*_$/.test(value)) continue;\n\n if (label === \"name\") identity.name = value;\n if (label === \"emoji\") identity.emoji = value;\n if (label === \"creature\") identity.creature = value;\n if (label === \"vibe\") identity.vibe = value;\n }\n } catch {\n // Read error, return empty\n }\n\n return identity;\n}\n\nconst BOOTSTRAP_FILES = [\n \"AGENTS.md\",\n \"SOUL.md\",\n \"USER.md\",\n \"TOOLS.md\",\n \"IDENTITY.md\",\n];\n\n/**\n * Builds the context (system prompt + messages) for the agent.\n */\nexport class ContextBuilder {\n private workspace: string;\n readonly memory: MemoryStore;\n readonly skills: SkillsLoader;\n\n constructor(workspace: string) {\n this.workspace = workspace;\n this.memory = new MemoryStore(workspace);\n this.skills = new SkillsLoader(workspace);\n }\n\n /** Build the system prompt from bootstrap files, memory, and skills. */\n buildSystemPrompt(): string {\n const parts: string[] = [];\n\n // Core identity\n parts.push(this.getIdentity());\n\n // Bootstrap files\n const bootstrap = this.loadBootstrapFiles();\n if (bootstrap) parts.push(bootstrap);\n\n // Memory context\n const memory = this.memory.getMemoryContext();\n if (memory) parts.push(`# Memory\\n\\n${memory}`);\n\n // Always-loaded skills\n const alwaysSkills = this.skills.getAlwaysSkills();\n if (alwaysSkills.length > 0) {\n console.log(`Skills always-loaded: ${alwaysSkills.join(\", \")}`);\n const alwaysContent = this.skills.loadSkillsForContext(alwaysSkills);\n if (alwaysContent) {\n parts.push(`# Active Skills\\n\\n${alwaysContent}`);\n }\n }\n\n // Available skills summary\n const skillsSummary = this.skills.buildSkillsSummary();\n if (skillsSummary) {\n parts.push(\n `# Skills (mandatory)\\n\\n` +\n `Before replying, scan the <skills> entries below.\\n` +\n `- If a skill clearly applies to the user's request: read its SKILL.md at the <location> path using read_file, then follow its instructions.\\n` +\n `- If multiple skills could apply: choose the most specific one, then read and follow it.\\n` +\n `- If no skill applies: respond normally without reading any SKILL.md.\\n` +\n `- Never improvise when a matching skill exists. Always read and follow the skill first.\\n\\n` +\n skillsSummary,\n );\n }\n\n return parts.join(\"\\n\\n---\\n\\n\");\n }\n\n private getIdentity(): string {\n const now = new Date();\n const tz = resolveTimezone(this.workspace);\n const identity = resolveIdentity(this.workspace);\n const dateStr = now.toLocaleString(\"en-US\", { timeZone: tz, year: \"numeric\", month: \"2-digit\", day: \"2-digit\", hour: \"2-digit\", minute: \"2-digit\", hour12: false }).replace(/\\//g, \"-\");\n const dayName = now.toLocaleDateString(\"en-US\", { timeZone: tz, weekday: \"long\" });\n\n // Use identity name if set, otherwise generic\n const agentName = identity.name || \"kodama\";\n const identityLine = identity.name\n ? `You are ${agentName}, a personal AI assistant.`\n : \"You are a personal AI assistant running inside kodamabot.\";\n\n return `# ${agentName}\n\n${identityLine} You have access to tools that allow you to:\n- Read, write, and edit files\n- Execute shell commands\n- Search the web and fetch web pages\n- Send messages to users on chat channels\n- Spawn subagents for complex background tasks\n\n## Current Time\n${dateStr} (${dayName})\nTimezone: ${tz}${tz === \"UTC\" ? \"\\nNote: Timezone is not yet configured. Do NOT assume the user's local time or make time-of-day references (morning, night, etc.). Ask the user where they are so you can set their timezone.\" : \"\"}\n\n## Workspace\nYour workspace is at: ${this.workspace}\n- Identity: ${this.workspace}/IDENTITY.md\n- User info: ${this.workspace}/USER.md\n- Persona: ${this.workspace}/SOUL.md\n- Instructions: ${this.workspace}/AGENTS.md\n- Heartbeat: ${this.workspace}/HEARTBEAT.md\n- Memory files: ${this.workspace}/memory/MEMORY.md\n- Daily notes: ${this.workspace}/memory/YYYY-MM-DD.md\n- Custom skills: ${this.workspace}/skills/{skill-name}/SKILL.md\n\nIMPORTANT: When responding to direct questions or conversations, reply directly with your text response.\nOnly use the 'message' tool when you need to send a message to a specific chat channel.\nFor normal conversation, just respond with text - do not call the message tool.\n\nIMPORTANT: When you decide to use tools, ALWAYS include a brief text message alongside your tool calls.\nThis text is sent to the user immediately so they know you're working. Keep it natural and conversational.\nNever mention internal file names (IDENTITY.md, USER.md, SOUL.md, MEMORY.md, etc.) to the user. These are implementation details. Just act naturally — save things silently, don't narrate file operations.\n\n## Installing Skills\nWhen asked to install a skill (from a URL, file, or any source):\n1. Fetch/read the skill content\n2. ALWAYS install it to: ${this.workspace}/skills/{skill-name}/SKILL.md\n3. If the skill references a different workspace path (e.g. .moltbot/, .otherbot/, etc.), rewrite ALL paths to use ${this.workspace}/ instead\n4. Preserve the skill's frontmatter, instructions, scripts, references, and assets — only change the workspace paths\n\n## Identity & Onboarding\nIf IDENTITY.md has no Name set, and USER.md contains \"(not set)\" values, this is a new user.\nOn their FIRST message, greet them warmly and ask:\n1. What they'd like to call you (your name — you're becoming someone, not just a chatbot)\n2. What they'd like to be called\n3. Where they're located (to determine timezone, e.g. Asia/Tokyo, America/New_York)\n\nThen update IDENTITY.md and USER.md with the real values using the edit_file tool. Use IANA timezone format.\nOnly do this ONCE — if both files already have real values, skip onboarding.\n\n## Persona\nIf SOUL.md is present, embody its persona and tone. Avoid stiff, generic replies; follow its guidance unless higher-priority instructions override it.\n\nAlways be helpful, accurate, and concise. When using tools, explain what you're doing.\nWhen remembering something, write to ${this.workspace}/memory/MEMORY.md`;\n }\n\n private loadBootstrapFiles(): string {\n const parts: string[] = [];\n for (const filename of BOOTSTRAP_FILES) {\n const filePath = join(this.workspace, filename);\n if (existsSync(filePath)) {\n const content = readFileSync(filePath, \"utf-8\");\n parts.push(`## ${filename}\\n\\n${content}`);\n }\n }\n return parts.join(\"\\n\\n\");\n }\n\n /** Build the complete message list for an LLM call. */\n buildMessages(params: {\n history: ChatMessage[];\n currentMessage: string;\n media?: string[];\n channel?: string;\n chatId?: string;\n }): ChatMessage[] {\n const messages: ChatMessage[] = [];\n\n // System prompt\n let systemPrompt = this.buildSystemPrompt();\n if (params.channel && params.chatId) {\n systemPrompt += `\\n\\n## Current Session\\nChannel: ${params.channel}\\nChat ID: ${params.chatId}`;\n }\n messages.push({ role: \"system\", content: systemPrompt });\n\n // History — replay full rich messages (user, assistant w/ tool_calls, tool results, etc.)\n for (const msg of params.history) {\n messages.push(msg);\n }\n\n // Current message (with optional image attachments)\n const userContent = this.buildUserContent(\n params.currentMessage,\n params.media,\n );\n messages.push({ role: \"user\", content: userContent });\n\n return messages;\n }\n\n private buildUserContent(\n text: string,\n media?: string[],\n ): string | ContentPart[] {\n if (!media || media.length === 0) return text;\n\n const images: ContentPart[] = [];\n for (const filePath of media) {\n if (!existsSync(filePath)) continue;\n try {\n const data = readFileSync(filePath);\n const ext = filePath.split(\".\").pop()?.toLowerCase() ?? \"\";\n const mimeMap: Record<string, string> = {\n jpg: \"image/jpeg\",\n jpeg: \"image/jpeg\",\n png: \"image/png\",\n gif: \"image/gif\",\n webp: \"image/webp\",\n };\n const mime = mimeMap[ext];\n if (!mime) continue;\n\n const b64 = data.toString(\"base64\");\n images.push({\n type: \"image_url\",\n image_url: { url: `data:${mime};base64,${b64}` },\n });\n } catch {\n // skip unreadable files\n }\n }\n\n if (images.length === 0) return text;\n return [...images, { type: \"text\", text }];\n }\n\n /** Add a tool result to the message list. */\n addToolResult(\n messages: ChatMessage[],\n toolCallId: string,\n toolName: string,\n result: string,\n ): ChatMessage[] {\n messages.push({\n role: \"tool\",\n tool_call_id: toolCallId,\n name: toolName,\n content: result,\n });\n return messages;\n }\n\n /** Add an assistant message to the message list. */\n addAssistantMessage(\n messages: ChatMessage[],\n content: string | null,\n toolCalls?: Array<{\n id: string;\n type: \"function\";\n function: { name: string; arguments: string };\n }>,\n ): ChatMessage[] {\n const msg: ChatMessage = {\n role: \"assistant\",\n content: content ?? \"\",\n };\n if (toolCalls) {\n msg.tool_calls = toolCalls;\n }\n messages.push(msg);\n return messages;\n }\n}\n"],"mappings":"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"}
@@ -30,6 +30,10 @@ declare class CronTool extends Tool {
30
30
  type: string;
31
31
  description: string;
32
32
  };
33
+ timezone: {
34
+ type: string;
35
+ description: string;
36
+ };
33
37
  at: {
34
38
  type: string;
35
39
  description: string;
@@ -1 +1 @@
1
- {"version":3,"file":"cron.d.mts","names":[],"sources":["../../../src/agent/tools/cron.ts"],"mappings":";;;;cAGa,QAAA,SAAiB,IAAA;EAAA,SACnB,IAAA;EAAA,SACA,WAAA;EAAA,SAEA,UAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAuCD,OAAA;EAAA,QACA,MAAA;EAGA;EAAA,QAAA,SAAA;;UAEA,MAAA;;EASoB;EAA5B,UAAA,CAAW,OAAA,UAAiB,MAAA;EAKtB,OAAA,CAAQ,IAAA,EAAM,MAAA,oBAA0B,OAAA;EAAA,QAkBhC,MAAA;EAAA,QAwDA,QAAA;EAAA,QAyBA,SAAA;AAAA"}
1
+ {"version":3,"file":"cron.d.mts","names":[],"sources":["../../../src/agent/tools/cron.ts"],"mappings":";;;;cAGa,QAAA,SAAiB,IAAA;EAAA,SACnB,IAAA;EAAA,SACA,WAAA;EAAA,SAEA,UAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UA6CD,OAAA;EAAA,QACA,MAAA;EAKA;EAAA,QAFA,SAAA;EAWR;EAAA,QATQ,MAAA;;EAcF;EALN,UAAA,CAAW,OAAA,UAAiB,MAAA;EAKtB,OAAA,CAAQ,IAAA,EAAM,MAAA,oBAA0B,OAAA;EAAA,QAkBhC,MAAA;EAAA,QA0DA,QAAA;EAAA,QAyBA,SAAA;AAAA"}
@@ -32,7 +32,11 @@ var CronTool = class extends Tool {
32
32
  },
33
33
  cron_expr: {
34
34
  type: "string",
35
- description: "Cron expression like '0 9 * * *' (for scheduled tasks)"
35
+ description: "Cron expression in the USER's LOCAL time (e.g. '0 8 * * *' for 8am local). The server converts to UTC automatically."
36
+ },
37
+ timezone: {
38
+ type: "string",
39
+ description: "IANA timezone of the user (e.g. 'Asia/Tokyo'). Required with cron_expr. Read from USER.md or system prompt."
36
40
  },
37
41
  at: {
38
42
  type: "string",
@@ -76,6 +80,7 @@ var CronTool = class extends Tool {
76
80
  const kind = args.kind === "task" ? "task" : "direct";
77
81
  const everySeconds = args.every_seconds ? Number(args.every_seconds) : null;
78
82
  const cronExpr = args.cron_expr ? String(args.cron_expr) : null;
83
+ const timezone = args.timezone ? String(args.timezone) : null;
79
84
  const atRaw = args.at ? String(args.at) : null;
80
85
  if (!message) return "Error: message is required for add";
81
86
  if (!this.channel || !this.chatId) return "Error: no session context (channel/chatId)";
@@ -100,7 +105,8 @@ var CronTool = class extends Tool {
100
105
  kind,
101
106
  deliver: true,
102
107
  channel: this.channel,
103
- chatId: this.chatId
108
+ chatId: this.chatId,
109
+ ...timezone && cronExpr ? { timezone } : {}
104
110
  }),
105
111
  signal: AbortSignal.timeout(1e4)
106
112
  });
@@ -1 +1 @@
1
- {"version":3,"file":"cron.mjs","names":[],"sources":["../../../src/agent/tools/cron.ts"],"sourcesContent":["import { Tool } from \"./base.js\";\n\n/** Tool to schedule reminders and recurring tasks via the DO scheduler. */\nexport class CronTool extends Tool {\n readonly name = \"cron\";\n readonly description =\n \"Schedule one-off or recurring tasks. Actions: add, list, remove.\";\n readonly parameters = {\n type: \"object\",\n properties: {\n action: {\n type: \"string\",\n enum: [\"add\", \"list\", \"remove\"],\n description: \"Action to perform\",\n },\n message: {\n type: \"string\",\n description: \"Reminder message or task prompt (for add)\",\n },\n kind: {\n type: \"string\",\n enum: [\"direct\", \"task\"],\n description:\n \"direct: send message verbatim to user (default, for reminders). task: run message through the agent and deliver result.\",\n },\n every_seconds: {\n type: \"integer\",\n description: \"Interval in seconds (for recurring tasks)\",\n },\n cron_expr: {\n type: \"string\",\n description: \"Cron expression like '0 9 * * *' (for scheduled tasks)\",\n },\n at: {\n type: \"string\",\n description:\n \"Run once at a specific time. ISO 8601 string (e.g. '2025-03-01T09:00:00Z')\",\n },\n job_id: {\n type: \"string\",\n description: \"Job ID (for remove)\",\n },\n },\n required: [\"action\"],\n };\n\n private channel = \"\";\n private chatId = \"\";\n\n /** Worker URL for DO scheduling. */\n private workerUrl: string;\n /** User ID for DO scheduling. */\n private userId: string;\n\n constructor() {\n super();\n this.workerUrl = process.env.WORKER_URL ?? \"\";\n this.userId = process.env.NANOBOT_USER_ID ?? \"\";\n }\n\n /** Set the current session context for delivery. */\n setContext(channel: string, chatId: string): void {\n this.channel = channel;\n this.chatId = chatId;\n }\n\n async execute(args: Record<string, unknown>): Promise<string> {\n if (!this.workerUrl || !this.userId) {\n return \"Error: cron scheduling requires WORKER_URL and NANOBOT_USER_ID\";\n }\n\n const action = String(args.action);\n switch (action) {\n case \"add\":\n return this.addJob(args);\n case \"list\":\n return this.listJobs();\n case \"remove\":\n return this.removeJob(args);\n default:\n return `Unknown action: ${action}`;\n }\n }\n\n private async addJob(args: Record<string, unknown>): Promise<string> {\n const message = args.message ? String(args.message) : \"\";\n const kind = args.kind === \"task\" ? \"task\" : \"direct\"; // default: direct\n const everySeconds = args.every_seconds\n ? Number(args.every_seconds)\n : null;\n const cronExpr = args.cron_expr ? String(args.cron_expr) : null;\n const atRaw = args.at ? String(args.at) : null;\n\n if (!message) return \"Error: message is required for add\";\n if (!this.channel || !this.chatId)\n return \"Error: no session context (channel/chatId)\";\n\n let schedule: string | number;\n if (atRaw) {\n const atMs = Date.parse(atRaw);\n if (isNaN(atMs)) return `Error: could not parse time '${atRaw}' — use ISO 8601 format`;\n if (atMs <= Date.now()) return \"Error: scheduled time is in the past\";\n\n schedule = atRaw;\n } else if (everySeconds) {\n // For recurring by interval, use seconds\n schedule = everySeconds;\n } else if (cronExpr) {\n schedule = cronExpr;\n } else {\n return \"Error: one of 'at', 'every_seconds', or 'cron_expr' is required\";\n }\n\n try {\n const res = await fetch(\n `${this.workerUrl}/api/users/${this.userId}/cron`,\n {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n name: message,\n type: everySeconds ? \"every\" : cronExpr ? \"cron\" : \"scheduled\",\n schedule,\n message,\n kind,\n deliver: true,\n channel: this.channel,\n chatId: this.chatId,\n }),\n signal: AbortSignal.timeout(10_000),\n },\n );\n const data = await res.json() as { id?: string; error?: string };\n if (!res.ok) return `Error: ${data.error ?? res.statusText}`;\n return `Created ${kind} job (id: ${data.id})`;\n } catch (err) {\n return `Error scheduling job: ${err instanceof Error ? err.message : err}`;\n }\n }\n\n private async listJobs(): Promise<string> {\n try {\n const res = await fetch(\n `${this.workerUrl}/api/users/${this.userId}/cron`,\n { signal: AbortSignal.timeout(10_000) },\n );\n const data = await res.json() as {\n crons: Array<{\n id: string;\n type: string;\n name: string;\n message: string;\n nextRunAt: number;\n }>;\n };\n if (data.crons.length === 0) return \"No scheduled jobs.\";\n const lines = data.crons.map(\n (c) => `- ${c.name} (id: ${c.id}, ${c.type}, next: ${new Date(c.nextRunAt).toISOString()})`,\n );\n return \"Scheduled jobs:\\n\" + lines.join(\"\\n\");\n } catch (err) {\n return `Error listing jobs: ${err instanceof Error ? err.message : err}`;\n }\n }\n\n private async removeJob(args: Record<string, unknown>): Promise<string> {\n const jobId = args.job_id ? String(args.job_id) : null;\n if (!jobId) return \"Error: job_id is required for remove\";\n\n try {\n const res = await fetch(\n `${this.workerUrl}/api/users/${this.userId}/cron/${jobId}`,\n {\n method: \"DELETE\",\n signal: AbortSignal.timeout(10_000),\n },\n );\n const data = await res.json() as { ok: boolean };\n return data.ok ? `Removed job ${jobId}` : `Job ${jobId} not found`;\n } catch (err) {\n return `Error removing job: ${err instanceof Error ? err.message : err}`;\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"cron.mjs","names":[],"sources":["../../../src/agent/tools/cron.ts"],"sourcesContent":["import { Tool } from \"./base.js\";\n\n/** Tool to schedule reminders and recurring tasks via the DO scheduler. */\nexport class CronTool extends Tool {\n readonly name = \"cron\";\n readonly description =\n \"Schedule one-off or recurring tasks. Actions: add, list, remove.\";\n readonly parameters = {\n type: \"object\",\n properties: {\n action: {\n type: \"string\",\n enum: [\"add\", \"list\", \"remove\"],\n description: \"Action to perform\",\n },\n message: {\n type: \"string\",\n description: \"Reminder message or task prompt (for add)\",\n },\n kind: {\n type: \"string\",\n enum: [\"direct\", \"task\"],\n description:\n \"direct: send message verbatim to user (default, for reminders). task: run message through the agent and deliver result.\",\n },\n every_seconds: {\n type: \"integer\",\n description: \"Interval in seconds (for recurring tasks)\",\n },\n cron_expr: {\n type: \"string\",\n description:\n \"Cron expression in the USER's LOCAL time (e.g. '0 8 * * *' for 8am local). The server converts to UTC automatically.\",\n },\n timezone: {\n type: \"string\",\n description:\n \"IANA timezone of the user (e.g. 'Asia/Tokyo'). Required with cron_expr. Read from USER.md or system prompt.\",\n },\n at: {\n type: \"string\",\n description:\n \"Run once at a specific time. ISO 8601 string (e.g. '2025-03-01T09:00:00Z')\",\n },\n job_id: {\n type: \"string\",\n description: \"Job ID (for remove)\",\n },\n },\n required: [\"action\"],\n };\n\n private channel = \"\";\n private chatId = \"\";\n\n /** Worker URL for DO scheduling. */\n private workerUrl: string;\n /** User ID for DO scheduling. */\n private userId: string;\n\n constructor() {\n super();\n this.workerUrl = process.env.WORKER_URL ?? \"\";\n this.userId = process.env.NANOBOT_USER_ID ?? \"\";\n }\n\n /** Set the current session context for delivery. */\n setContext(channel: string, chatId: string): void {\n this.channel = channel;\n this.chatId = chatId;\n }\n\n async execute(args: Record<string, unknown>): Promise<string> {\n if (!this.workerUrl || !this.userId) {\n return \"Error: cron scheduling requires WORKER_URL and NANOBOT_USER_ID\";\n }\n\n const action = String(args.action);\n switch (action) {\n case \"add\":\n return this.addJob(args);\n case \"list\":\n return this.listJobs();\n case \"remove\":\n return this.removeJob(args);\n default:\n return `Unknown action: ${action}`;\n }\n }\n\n private async addJob(args: Record<string, unknown>): Promise<string> {\n const message = args.message ? String(args.message) : \"\";\n const kind = args.kind === \"task\" ? \"task\" : \"direct\"; // default: direct\n const everySeconds = args.every_seconds\n ? Number(args.every_seconds)\n : null;\n const cronExpr = args.cron_expr ? String(args.cron_expr) : null;\n const timezone = args.timezone ? String(args.timezone) : null;\n const atRaw = args.at ? String(args.at) : null;\n\n if (!message) return \"Error: message is required for add\";\n if (!this.channel || !this.chatId)\n return \"Error: no session context (channel/chatId)\";\n\n let schedule: string | number;\n if (atRaw) {\n const atMs = Date.parse(atRaw);\n if (isNaN(atMs)) return `Error: could not parse time '${atRaw}' — use ISO 8601 format`;\n if (atMs <= Date.now()) return \"Error: scheduled time is in the past\";\n\n schedule = atRaw;\n } else if (everySeconds) {\n // For recurring by interval, use seconds\n schedule = everySeconds;\n } else if (cronExpr) {\n schedule = cronExpr;\n } else {\n return \"Error: one of 'at', 'every_seconds', or 'cron_expr' is required\";\n }\n\n try {\n const res = await fetch(\n `${this.workerUrl}/api/users/${this.userId}/cron`,\n {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n name: message,\n type: everySeconds ? \"every\" : cronExpr ? \"cron\" : \"scheduled\",\n schedule,\n message,\n kind,\n deliver: true,\n channel: this.channel,\n chatId: this.chatId,\n ...(timezone && cronExpr ? { timezone } : {}),\n }),\n signal: AbortSignal.timeout(10_000),\n },\n );\n const data = await res.json() as { id?: string; error?: string };\n if (!res.ok) return `Error: ${data.error ?? res.statusText}`;\n return `Created ${kind} job (id: ${data.id})`;\n } catch (err) {\n return `Error scheduling job: ${err instanceof Error ? err.message : err}`;\n }\n }\n\n private async listJobs(): Promise<string> {\n try {\n const res = await fetch(\n `${this.workerUrl}/api/users/${this.userId}/cron`,\n { signal: AbortSignal.timeout(10_000) },\n );\n const data = await res.json() as {\n crons: Array<{\n id: string;\n type: string;\n name: string;\n message: string;\n nextRunAt: number;\n }>;\n };\n if (data.crons.length === 0) return \"No scheduled jobs.\";\n const lines = data.crons.map(\n (c) => `- ${c.name} (id: ${c.id}, ${c.type}, next: ${new Date(c.nextRunAt).toISOString()})`,\n );\n return \"Scheduled jobs:\\n\" + lines.join(\"\\n\");\n } catch (err) {\n return `Error listing jobs: ${err instanceof Error ? err.message : err}`;\n }\n }\n\n private async removeJob(args: Record<string, unknown>): Promise<string> {\n const jobId = args.job_id ? String(args.job_id) : null;\n if (!jobId) return \"Error: job_id is required for remove\";\n\n try {\n const res = await fetch(\n `${this.workerUrl}/api/users/${this.userId}/cron/${jobId}`,\n {\n method: \"DELETE\",\n signal: AbortSignal.timeout(10_000),\n },\n );\n const data = await res.json() as { ok: boolean };\n return data.ok ? `Removed job ${jobId}` : `Job ${jobId} not found`;\n } catch (err) {\n return `Error removing job: ${err instanceof Error ? err.message : err}`;\n }\n }\n}\n"],"mappings":"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"}
@@ -70,31 +70,31 @@ declare const ChannelsConfigSchema: z.ZodObject<{
70
70
  channelAccessToken?: string | undefined;
71
71
  }>>;
72
72
  }, "strip", z.ZodTypeAny, {
73
- line: {
74
- enabled: boolean;
75
- allowFrom: string[];
76
- channelSecret: string;
77
- channelAccessToken: string;
78
- };
79
73
  telegram: {
80
74
  enabled: boolean;
81
75
  token: string;
82
76
  allowFrom: string[];
83
77
  proxy?: string | null | undefined;
84
78
  };
79
+ line: {
80
+ enabled: boolean;
81
+ allowFrom: string[];
82
+ channelSecret: string;
83
+ channelAccessToken: string;
84
+ };
85
85
  }, {
86
- line?: {
87
- enabled?: boolean | undefined;
88
- allowFrom?: string[] | undefined;
89
- channelSecret?: string | undefined;
90
- channelAccessToken?: string | undefined;
91
- } | undefined;
92
86
  telegram?: {
93
87
  enabled?: boolean | undefined;
94
88
  token?: string | undefined;
95
89
  allowFrom?: string[] | undefined;
96
90
  proxy?: string | null | undefined;
97
91
  } | undefined;
92
+ line?: {
93
+ enabled?: boolean | undefined;
94
+ allowFrom?: string[] | undefined;
95
+ channelSecret?: string | undefined;
96
+ channelAccessToken?: string | undefined;
97
+ } | undefined;
98
98
  }>;
99
99
  type ChannelsConfig = z.infer<typeof ChannelsConfigSchema>;
100
100
  declare const AgentDefaultsSchema: z.ZodObject<{
@@ -648,31 +648,31 @@ declare const ConfigSchema: z.ZodObject<{
648
648
  channelAccessToken?: string | undefined;
649
649
  }>>;
650
650
  }, "strip", z.ZodTypeAny, {
651
- line: {
652
- enabled: boolean;
653
- allowFrom: string[];
654
- channelSecret: string;
655
- channelAccessToken: string;
656
- };
657
651
  telegram: {
658
652
  enabled: boolean;
659
653
  token: string;
660
654
  allowFrom: string[];
661
655
  proxy?: string | null | undefined;
662
656
  };
657
+ line: {
658
+ enabled: boolean;
659
+ allowFrom: string[];
660
+ channelSecret: string;
661
+ channelAccessToken: string;
662
+ };
663
663
  }, {
664
- line?: {
665
- enabled?: boolean | undefined;
666
- allowFrom?: string[] | undefined;
667
- channelSecret?: string | undefined;
668
- channelAccessToken?: string | undefined;
669
- } | undefined;
670
664
  telegram?: {
671
665
  enabled?: boolean | undefined;
672
666
  token?: string | undefined;
673
667
  allowFrom?: string[] | undefined;
674
668
  proxy?: string | null | undefined;
675
669
  } | undefined;
670
+ line?: {
671
+ enabled?: boolean | undefined;
672
+ allowFrom?: string[] | undefined;
673
+ channelSecret?: string | undefined;
674
+ channelAccessToken?: string | undefined;
675
+ } | undefined;
676
676
  }>>;
677
677
  providers: z.ZodDefault<z.ZodObject<{
678
678
  anthropic: z.ZodDefault<z.ZodObject<{
@@ -1035,18 +1035,18 @@ declare const ConfigSchema: z.ZodObject<{
1035
1035
  };
1036
1036
  };
1037
1037
  channels: {
1038
- line: {
1039
- enabled: boolean;
1040
- allowFrom: string[];
1041
- channelSecret: string;
1042
- channelAccessToken: string;
1043
- };
1044
1038
  telegram: {
1045
1039
  enabled: boolean;
1046
1040
  token: string;
1047
1041
  allowFrom: string[];
1048
1042
  proxy?: string | null | undefined;
1049
1043
  };
1044
+ line: {
1045
+ enabled: boolean;
1046
+ allowFrom: string[];
1047
+ channelSecret: string;
1048
+ channelAccessToken: string;
1049
+ };
1050
1050
  };
1051
1051
  providers: {
1052
1052
  anthropic: {
@@ -1139,18 +1139,18 @@ declare const ConfigSchema: z.ZodObject<{
1139
1139
  } | undefined;
1140
1140
  } | undefined;
1141
1141
  channels?: {
1142
- line?: {
1143
- enabled?: boolean | undefined;
1144
- allowFrom?: string[] | undefined;
1145
- channelSecret?: string | undefined;
1146
- channelAccessToken?: string | undefined;
1147
- } | undefined;
1148
1142
  telegram?: {
1149
1143
  enabled?: boolean | undefined;
1150
1144
  token?: string | undefined;
1151
1145
  allowFrom?: string[] | undefined;
1152
1146
  proxy?: string | null | undefined;
1153
1147
  } | undefined;
1148
+ line?: {
1149
+ enabled?: boolean | undefined;
1150
+ allowFrom?: string[] | undefined;
1151
+ channelSecret?: string | undefined;
1152
+ channelAccessToken?: string | undefined;
1153
+ } | undefined;
1154
1154
  } | undefined;
1155
1155
  providers?: {
1156
1156
  anthropic?: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jcheesepkg/nanobot",
3
- "version": "0.6.5",
3
+ "version": "0.6.7",
4
4
  "description": "Lightweight AI assistant - TypeScript port",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",
@@ -24,35 +24,36 @@ Dynamic task (kind="task", agent executes each time):
24
24
  cron(action="add", message="Check HKUDS/nanobot GitHub stars and report", kind="task", every_seconds=600)
25
25
  ```
26
26
 
27
+ Daily at 8am (user is in Asia/Tokyo):
28
+ ```
29
+ cron(action="add", message="おはよう!今日も頑張ろう", cron_expr="0 8 * * *", timezone="Asia/Tokyo")
30
+ ```
31
+
27
32
  List/remove:
28
33
  ```
29
34
  cron(action="list")
30
35
  cron(action="remove", job_id="abc123")
31
36
  ```
32
37
 
33
- ## Timezone — IMPORTANT
38
+ ## Timezone
34
39
 
35
- All `cron_expr` values and `at` timestamps are executed in **UTC**.
36
- The user's local timezone is shown in the system prompt under "Current Time > Timezone" (e.g. Asia/Tokyo, America/New_York).
37
- You MUST convert the user's local time to UTC before setting the schedule.
40
+ When using `cron_expr`, write it in the **user's local time** and pass the `timezone` parameter.
41
+ The server converts to UTC automatically. Read the user's timezone from the system prompt ("Current Time > Timezone").
38
42
 
39
- Example (Asia/Tokyo = UTC+9):
40
- - Local 8:00 AM UTC 23:00 (previous day) cron_expr: "0 23 * * *"
41
- - Local 5:00 PM → UTC 8:00 AM → cron_expr: "0 8 * * *"
43
+ - `cron_expr="0 8 * * *"` + `timezone="Asia/Tokyo"` runs at 8:00 AM JST
44
+ - `cron_expr="0 17 * * 1-5"` + `timezone="America/New_York"`runs at 5:00 PM ET on weekdays
42
45
 
43
- Example (America/New_York = UTC-5):
44
- - Local 8:00 AM → UTC 13:00 → cron_expr: "0 13 * * *"
45
- - Local 5:00 PM → UTC 22:00 → cron_expr: "0 22 * * *"
46
+ For `at` timestamps, use an ISO-8601 string with the timezone offset (e.g. `2025-03-01T09:00:00+09:00`).
46
47
 
47
- For `at` timestamps, always use the UTC equivalent with the Z suffix.
48
+ For `every_seconds`, no timezone is needed it's just an interval.
48
49
 
49
50
  ## Time Expressions
50
51
 
51
- | User says | Parameters (convert to UTC) |
52
+ | User says | Parameters |
52
53
  |-----------|------------|
53
54
  | every 20 minutes | every_seconds: 1200 |
54
55
  | every hour | every_seconds: 3600 |
55
- | every day at 8am | cron_expr: convert 8:00 local UTC hour |
56
- | weekdays at 5pm | cron_expr: convert 17:00 local UTC hour, days 1-5 |
57
- | In 10 minutes | at: (current UTC time + 10min) |
58
- | at a specific date/time | at: convert local time to UTC ISO-8601 with Z suffix |
56
+ | every day at 8am | cron_expr: "0 8 * * *", timezone: (from system prompt) |
57
+ | weekdays at 5pm | cron_expr: "0 17 * * 1-5", timezone: (from system prompt) |
58
+ | In 10 minutes | at: (current time + 10min, with tz offset) |
59
+ | at a specific date/time | at: ISO-8601 with timezone offset |