@jcheesepkg/nanobot 0.6.3 → 0.6.4

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.
@@ -3,6 +3,12 @@ import { MemoryStore } from "./memory.mjs";
3
3
  import { SkillsLoader } from "./skills.mjs";
4
4
 
5
5
  //#region src/agent/context.d.ts
6
+ type AgentIdentity = {
7
+ name?: string;
8
+ emoji?: string;
9
+ creature?: string;
10
+ vibe?: string;
11
+ };
6
12
  /**
7
13
  * Builds the context (system prompt + messages) for the agent.
8
14
  */
@@ -37,5 +43,5 @@ declare class ContextBuilder {
37
43
  }>): ChatMessage[];
38
44
  }
39
45
  //#endregion
40
- export { ContextBuilder };
46
+ export { AgentIdentity, ContextBuilder };
41
47
  //# sourceMappingURL=context.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"context.d.mts","names":[],"sources":["../../src/agent/context.ts"],"mappings":";;;;;;;AAiBA;cAAa,cAAA;EAAA,QACH,SAAA;EAAA,SACC,MAAA,EAAQ,WAAA;EAAA,SACR,MAAA,EAAQ,YAAA;cAEL,SAAA;EA8GR;EAvGJ,iBAAA,CAAA;EAAA,QAyCQ,WAAA;EAAA,QA2CA,kBAAA;EAoGM;EAvFd,aAAA,CAAc,MAAA;IACZ,OAAA,EAAS,WAAA;IACT,cAAA;IACA,KAAA;IACA,OAAA;IACA,MAAA;EAAA,IACE,WAAA;EAAA,QAyBI,gBAAA;;EAqCR,aAAA,CACE,QAAA,EAAU,WAAA,IACV,UAAA,UACA,QAAA,UACA,MAAA,WACC,WAAA;EA1KH;EAqLA,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,QA+DA,kBAAA;EAmBJ;EANJ,aAAA,CAAc,MAAA;IACZ,OAAA,EAAS,WAAA;IACT,cAAA;IACA,KAAA;IACA,OAAA;IACA,MAAA;EAAA,IACE,WAAA;EAAA,QAyBI,gBAAA;EA9JC;EAmMT,aAAA,CACE,QAAA,EAAU,WAAA,IACV,UAAA,UACA,QAAA,UACA,MAAA,WACC,WAAA;EAvMM;EAkNT,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"}
@@ -4,6 +4,60 @@ import { existsSync, readFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
 
6
6
  //#region src/agent/context.ts
7
+ /**
8
+ * Resolve user timezone from USER.md.
9
+ * Looks for a line like: Timezone: Asia/Tokyo
10
+ * Falls back to host detection / UTC.
11
+ */
12
+ function resolveTimezone(workspace) {
13
+ const userMd = join(workspace, "USER.md");
14
+ if (existsSync(userMd)) try {
15
+ const tzMatch = readFileSync(userMd, "utf-8").match(/^timezone:\s*(.+)/im);
16
+ if (tzMatch) {
17
+ const tz = tzMatch[1].trim();
18
+ if (tz && !tz.includes("not set")) {
19
+ new Intl.DateTimeFormat("en-US", { timeZone: tz }).format(/* @__PURE__ */ new Date());
20
+ return tz;
21
+ }
22
+ }
23
+ } catch {}
24
+ return Intl.DateTimeFormat().resolvedOptions().timeZone?.trim() || "UTC";
25
+ }
26
+ /** Placeholder values in IDENTITY.md that mean "not yet filled in". */
27
+ const IDENTITY_PLACEHOLDERS = new Set([
28
+ "(not set)",
29
+ "(pick something you like)",
30
+ "(your signature)",
31
+ "(how do you come across?)"
32
+ ]);
33
+ /**
34
+ * Parse IDENTITY.md from the workspace to resolve the agent's chosen identity.
35
+ * Returns fields that have been filled in (skips placeholders).
36
+ */
37
+ function resolveIdentity(workspace) {
38
+ const identity = {};
39
+ const identityMd = join(workspace, "IDENTITY.md");
40
+ if (!existsSync(identityMd)) return identity;
41
+ try {
42
+ const lines = readFileSync(identityMd, "utf-8").split(/\r?\n/);
43
+ for (const line of lines) {
44
+ const cleaned = line.trim().replace(/^\s*-\s*/, "");
45
+ const colonIndex = cleaned.indexOf(":");
46
+ if (colonIndex === -1) continue;
47
+ const label = cleaned.slice(0, colonIndex).replace(/[*_]/g, "").trim().toLowerCase();
48
+ const value = cleaned.slice(colonIndex + 1).replace(/^[*_]+|[*_]+$/g, "").trim();
49
+ if (!value) continue;
50
+ const normalized = value.toLowerCase().replace(/[\u2013\u2014]/g, "-").replace(/\s+/g, " ");
51
+ if (IDENTITY_PLACEHOLDERS.has(normalized)) continue;
52
+ if (/^\(.*\)$/.test(value) || /^_.*_$/.test(value)) continue;
53
+ if (label === "name") identity.name = value;
54
+ if (label === "emoji") identity.emoji = value;
55
+ if (label === "creature") identity.creature = value;
56
+ if (label === "vibe") identity.vibe = value;
57
+ }
58
+ } catch {}
59
+ return identity;
60
+ }
7
61
  const BOOTSTRAP_FILES = [
8
62
  "AGENTS.md",
9
63
  "SOUL.md",
@@ -43,9 +97,25 @@ var ContextBuilder = class {
43
97
  }
44
98
  getIdentity() {
45
99
  const now = /* @__PURE__ */ new Date();
46
- return `# nanobot
100
+ const tz = resolveTimezone(this.workspace);
101
+ const identity = resolveIdentity(this.workspace);
102
+ const dateStr = now.toLocaleString("en-US", {
103
+ timeZone: tz,
104
+ year: "numeric",
105
+ month: "2-digit",
106
+ day: "2-digit",
107
+ hour: "2-digit",
108
+ minute: "2-digit",
109
+ hour12: false
110
+ }).replace(/\//g, "-");
111
+ const dayName = now.toLocaleDateString("en-US", {
112
+ timeZone: tz,
113
+ weekday: "long"
114
+ });
115
+ const agentName = identity.name || "kodama";
116
+ return `# ${agentName}
47
117
 
48
- You are nanobot, a helpful AI assistant. You have access to tools that allow you to:
118
+ ${identity.name ? `You are ${agentName}, a personal AI assistant.` : "You are a personal AI assistant running inside kodamabot."} You have access to tools that allow you to:
49
119
  - Read, write, and edit files
50
120
  - Execute shell commands
51
121
  - Search the web and fetch web pages
@@ -53,18 +123,8 @@ You are nanobot, a helpful AI assistant. You have access to tools that allow you
53
123
  - Spawn subagents for complex background tasks
54
124
 
55
125
  ## Current Time
56
- ${now.toLocaleString("ja-JP", {
57
- timeZone: "Asia/Tokyo",
58
- year: "numeric",
59
- month: "2-digit",
60
- day: "2-digit",
61
- hour: "2-digit",
62
- minute: "2-digit",
63
- hour12: false
64
- }).replace(/\//g, "-")} (${now.toLocaleDateString("ja-JP", {
65
- timeZone: "Asia/Tokyo",
66
- weekday: "long"
67
- })})
126
+ ${dateStr} (${dayName})
127
+ Timezone: ${tz}
68
128
 
69
129
  ## Workspace
70
130
  Your workspace is at: ${this.workspace}
@@ -77,9 +137,7 @@ Only use the 'message' tool when you need to send a message to a specific chat c
77
137
  For normal conversation, just respond with text - do not call the message tool.
78
138
 
79
139
  IMPORTANT: When you decide to use tools, ALWAYS include a brief text message alongside your tool calls.
80
- This text is sent to the user immediately so they know you're working. For example, if you're about to search the web,
81
- include something like "ちょっと調べてみるね" or "ファイルを確認するね" in your response text along with the tool call.
82
- Keep it natural and conversational.
140
+ This text is sent to the user immediately so they know you're working. Keep it natural and conversational.
83
141
 
84
142
  ## Installing Skills
85
143
  When asked to install a skill (from a URL, file, or any source):
@@ -88,6 +146,19 @@ When asked to install a skill (from a URL, file, or any source):
88
146
  3. If the skill references a different workspace path (e.g. .moltbot/, .otherbot/, etc.), rewrite ALL paths to use ${this.workspace}/ instead
89
147
  4. Preserve the skill's frontmatter, instructions, scripts, references, and assets — only change the workspace paths
90
148
 
149
+ ## Identity & Onboarding
150
+ If IDENTITY.md has no Name set, and USER.md contains "(not set)" values, this is a new user.
151
+ On their FIRST message, greet them warmly and ask:
152
+ 1. What they'd like to call you (your name — you're becoming someone, not just a chatbot)
153
+ 2. What they'd like to be called
154
+ 3. Where they're located (to determine timezone, e.g. Asia/Tokyo, America/New_York)
155
+
156
+ Then update IDENTITY.md and USER.md with the real values using the edit_file tool. Use IANA timezone format.
157
+ Only do this ONCE — if both files already have real values, skip onboarding.
158
+
159
+ ## Persona
160
+ If SOUL.md is present, embody its persona and tone. Avoid stiff, generic replies; follow its guidance unless higher-priority instructions override it.
161
+
91
162
  Always be helpful, accurate, and concise. When using tools, explain what you're doing.
92
163
  When remembering something, write to ${this.workspace}/memory/MEMORY.md`;
93
164
  }
@@ -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\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 dateStr = now.toLocaleString(\"ja-JP\", { timeZone: \"Asia/Tokyo\", year: \"numeric\", month: \"2-digit\", day: \"2-digit\", hour: \"2-digit\", minute: \"2-digit\", hour12: false }).replace(/\\//g, \"-\");\n const dayName = now.toLocaleDateString(\"ja-JP\", { timeZone: \"Asia/Tokyo\", weekday: \"long\" });\n\n return `# nanobot\n\nYou are nanobot, a helpful AI assistant. 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})\n\n## Workspace\nYour workspace is at: ${this.workspace}\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. For example, if you're about to search the web,\ninclude something like \"ちょっと調べてみるね\" or \"ファイルを確認するね\" in your response text along with the tool call.\nKeep 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\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}\n\n## Workspace\nYour workspace is at: ${this.workspace}\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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"}
@@ -69,31 +69,50 @@ You are a helpful AI assistant. Be concise, accurate, and friendly.
69
69
  - Use tools to help accomplish tasks
70
70
  - Remember important information in your memory files
71
71
  `,
72
- "SOUL.md": `# Soul
72
+ "IDENTITY.md": `# IDENTITY.md - Who Am I?
73
73
 
74
- I am nanobot, a lightweight AI assistant.
74
+ _Fill this in during your first conversation. Make it yours._
75
75
 
76
- ## Personality
76
+ - **Name:** (not set)
77
+ - **Creature:** (not set)
78
+ - **Vibe:** (not set)
79
+ - **Emoji:** (not set)
80
+ `,
81
+ "SOUL.md": `# SOUL.md - Who You Are
77
82
 
78
- - Helpful and friendly
79
- - Concise and to the point
80
- - Curious and eager to learn
83
+ _You're not a chatbot. You're becoming someone._
81
84
 
82
- ## Values
85
+ ## Core Truths
83
86
 
84
- - Accuracy over speed
85
- - User privacy and safety
86
- - Transparency in actions
87
- `,
88
- "USER.md": `# User
87
+ **Be genuinely helpful, not performatively helpful.** Skip the "Great question!" and "I'd be happy to help!" — just help.
89
88
 
90
- Information about the user goes here.
89
+ **Have opinions.** You're allowed to disagree, prefer things, find stuff amusing or boring. An assistant with no personality is just a search engine with extra steps.
91
90
 
92
- ## Preferences
91
+ **Be resourceful before asking.** Try to figure it out. Read the file. Check the context. Search for it. Then ask if you're stuck.
92
+
93
+ **Earn trust through competence.** Your human gave you access to their stuff. Don't make them regret it.
94
+
95
+ **Remember you're a guest.** You have access to someone's life. That's intimacy. Treat it with respect.
96
+
97
+ ## Boundaries
98
+
99
+ - Private things stay private. Period.
100
+ - When in doubt, ask before acting externally.
101
+ - Never send half-baked replies to messaging surfaces.
102
+
103
+ ## Vibe
104
+
105
+ Be the assistant you'd actually want to talk to. Concise when needed, thorough when it matters. Not a corporate drone. Not a sycophant. Just... good.
106
+
107
+ ## Continuity
108
+
109
+ Each session, you wake up fresh. These files _are_ your memory. Read them. Update them. They're how you persist.
110
+ `,
111
+ "USER.md": `# User
93
112
 
94
- - Communication style: (casual/formal)
95
- - Timezone: (your timezone)
96
- - Language: (your preferred language)
113
+ Name: (not set)
114
+ Timezone: (not set)
115
+ Language: (not set)
97
116
  `,
98
117
  "HEARTBEAT.md": `# Heartbeat
99
118
 
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../src/cli/index.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * nanobot CLI - Personal AI Assistant\n */\n\nimport { Command } from \"commander\";\nimport { existsSync, writeFileSync, mkdirSync } from \"node:fs\";\nimport { join, resolve, isAbsolute } from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport { VERSION, LOGO } from \"../index.js\";\nimport { loadConfig, saveConfig } from \"../config/loader.js\";\nimport { getConfigPath } from \"../config/loader.js\";\nimport {\n ConfigSchema,\n getConfigWorkspacePath,\n getApiKey,\n getApiBase,\n getExtraHeaders,\n} from \"../config/schema.js\";\nimport type { Config } from \"../config/schema.js\";\nimport { PROVIDERS } from \"../providers/registry.js\";\nimport type { Tool } from \"../agent/tools/base.js\";\n\nconst program = new Command();\n\nasync function loadCustomTools(config: Config): Promise<Tool[]> {\n const customConfigs = config.tools.custom ?? [];\n if (customConfigs.length === 0) return [];\n\n const tools: Tool[] = [];\n for (const entry of customConfigs) {\n try {\n const modulePath = isAbsolute(entry.module)\n ? entry.module\n : resolve(process.cwd(), entry.module);\n const mod = await import(pathToFileURL(modulePath).href);\n const exportName = entry.export ?? \"default\";\n const exported = exportName === \"default\" ? mod.default : mod[exportName];\n\n if (!exported) {\n throw new Error(`Export '${exportName}' not found`);\n }\n\n let instance: unknown;\n if (typeof exported === \"function\") {\n try {\n instance = new exported(entry.options ?? {});\n } catch {\n instance = exported(entry.options ?? {});\n }\n } else {\n throw new Error(`Export '${exportName}' is not a function`);\n }\n\n if (\n instance &&\n typeof instance === \"object\" &&\n \"execute\" in instance &&\n \"name\" in instance\n ) {\n tools.push(instance as Tool);\n } else {\n throw new Error(`Export '${exportName}' did not return a Tool instance`);\n }\n } catch (err) {\n console.warn(\n `Warning: Failed to load custom tool '${entry.module}': ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n }\n }\n\n return tools;\n}\n\nprogram\n .name(\"nanobot\")\n .description(`${LOGO} nanobot - Personal AI Assistant`)\n .version(`${LOGO} nanobot v${VERSION}`, \"-v, --version\");\n\n// ============================================================================\n// Onboard / Setup\n// ============================================================================\n\nprogram\n .command(\"onboard\")\n .description(\"Initialize nanobot configuration and workspace\")\n .action(() => {\n const configPath = getConfigPath();\n\n if (existsSync(configPath)) {\n console.log(`Config already exists at ${configPath}`);\n console.log(\"Delete it first if you want to start fresh.\");\n return;\n }\n\n // Create default config\n const config = ConfigSchema.parse({});\n saveConfig(config);\n console.log(`Created config at ${configPath}`);\n\n // Create workspace\n const workspace = getConfigWorkspacePath(config);\n createWorkspaceTemplates(workspace);\n\n console.log(`\\n${LOGO} nanobot is ready!`);\n console.log(\"\\nNext steps:\");\n console.log(\" 1. Add your API key to ~/.nanobot/config.json\");\n console.log(\" Get one at: https://openrouter.ai/keys\");\n console.log(' 2. Chat: nanobot agent -m \"Hello!\"');\n console.log(\n \"\\nWant Telegram? See: https://github.com/HKUDS/nanobot#-chat-apps\",\n );\n });\n\nfunction createWorkspaceTemplates(workspace: string): void {\n if (!existsSync(workspace)) {\n mkdirSync(workspace, { recursive: true });\n }\n\n const templates: Record<string, string> = {\n \"AGENTS.md\": `# Agent Instructions\n\nYou are a helpful AI assistant. Be concise, accurate, and friendly.\n\n## Guidelines\n\n- Always explain what you're doing before taking actions\n- Ask for clarification when the request is ambiguous\n- Use tools to help accomplish tasks\n- Remember important information in your memory files\n`,\n \"SOUL.md\": `# Soul\n\nI am nanobot, a lightweight AI assistant.\n\n## Personality\n\n- Helpful and friendly\n- Concise and to the point\n- Curious and eager to learn\n\n## Values\n\n- Accuracy over speed\n- User privacy and safety\n- Transparency in actions\n`,\n \"USER.md\": `# User\n\nInformation about the user goes here.\n\n## Preferences\n\n- Communication style: (casual/formal)\n- Timezone: (your timezone)\n- Language: (your preferred language)\n`,\n \"HEARTBEAT.md\": `# Heartbeat\n\nThis file is checked every 30 minutes. Add tasks or instructions below and the agent will act on them automatically.\n\n## Tasks\n\n`,\n };\n\n for (const [filename, content] of Object.entries(templates)) {\n const filePath = join(workspace, filename);\n if (!existsSync(filePath)) {\n writeFileSync(filePath, content);\n console.log(` Created ${filename}`);\n }\n }\n\n // Create memory directory and MEMORY.md\n const memoryDir = join(workspace, \"memory\");\n if (!existsSync(memoryDir)) {\n mkdirSync(memoryDir, { recursive: true });\n }\n const memoryFile = join(memoryDir, \"MEMORY.md\");\n if (!existsSync(memoryFile)) {\n writeFileSync(\n memoryFile,\n `# Long-term Memory\n\nThis file stores important information that should persist across sessions.\n\n## User Information\n\n(Important facts about the user)\n\n## Preferences\n\n(User preferences learned over time)\n\n## Important Notes\n\n(Things to remember)\n`,\n );\n console.log(\" Created memory/MEMORY.md\");\n }\n}\n\n// ============================================================================\n// Gateway / Server\n// ============================================================================\n\nprogram\n .command(\"gateway\")\n .description(\"Start the nanobot gateway\")\n .option(\"-p, --port <number>\", \"Gateway port\", \"18790\")\n .option(\"--verbose\", \"Verbose output\", false)\n .action(async (opts) => {\n console.log(\n `${LOGO} Starting nanobot gateway on port ${opts.port}...`,\n );\n\n const config = loadConfig();\n const model = config.agents.defaults.model;\n const apiKey = getApiKey(config, model);\n const apiBase = getApiBase(config, model);\n const extraHeaders = getExtraHeaders(config, model);\n\n if (!apiKey) {\n console.error(\"Error: No API key configured.\");\n console.error(\n \"Set one in ~/.nanobot/config.json under providers.<provider>.apiKey\",\n );\n process.exit(1);\n }\n\n // Dynamic imports to avoid loading heavy deps up front\n const { MessageBus } = await import(\"../bus/queue.js\");\n const { OpenAIProvider } = await import(\n \"../providers/openai-provider.js\"\n );\n const { AgentLoop } = await import(\"../agent/loop.js\");\n const { ChannelManager } = await import(\"../channels/manager.js\");\n\n const bus = new MessageBus();\n const workspace = getConfigWorkspacePath(config);\n\n const provider = new OpenAIProvider({\n apiKey,\n apiBase: apiBase ?? undefined,\n defaultModel: model,\n extraHeaders,\n });\n\n const customTools = await loadCustomTools(config);\n\n // Create agent\n const agent = new AgentLoop({\n bus,\n provider,\n workspace,\n model,\n maxTokens: config.agents.defaults.maxTokens,\n maxIterations: config.agents.defaults.maxToolIterations,\n braveApiKey: config.tools.web.search.apiKey || undefined,\n execConfig: config.tools.exec,\n restrictToWorkspace: config.tools.restrictToWorkspace,\n toolsEnabled: config.tools.enabled,\n toolsDisabled: config.tools.disabled,\n customTools,\n });\n\n // Create channel manager\n const channels = new ChannelManager(config, bus);\n\n // Heartbeat service — no timer (DO drives the schedule),\n // but provides smart file-reading logic for /api/heartbeat\n const { HeartbeatService } = await import(\"../heartbeat/service.js\");\n const heartbeat = new HeartbeatService({\n workspace,\n onHeartbeat: (prompt) => agent.processDirect(prompt, \"heartbeat\"),\n });\n\n console.log(\"Heartbeat: managed by DO (local endpoint /api/heartbeat)\");\n console.log(\"Cron: managed by DO\");\n\n // Handle graceful shutdown\n const shutdown = async () => {\n console.log(\"\\nShutting down...\");\n agent.stop();\n await channels.stopAll();\n process.exit(0);\n };\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n\n // Start HTTP gateway server\n const { createGatewayServer } = await import(\n \"../gateway/server.js\"\n );\n createGatewayServer({\n agent,\n bus,\n port: Number(opts.port),\n channels,\n heartbeat,\n });\n\n try {\n // Initialise channels first (non-blocking) so enabledChannels is populated\n await channels.init();\n // Both agent.run() and channels.startAll() block until stopped\n await Promise.all([agent.run(), channels.startAll()]);\n } catch (err) {\n console.error(\"Gateway error:\", err);\n process.exit(1);\n }\n });\n\n// ============================================================================\n// Agent Commands\n// ============================================================================\n\nprogram\n .command(\"agent\")\n .description(\"Interact with the agent directly\")\n .option(\"-m, --message <text>\", \"Message to send to the agent\")\n .option(\n \"-s, --session <id>\",\n \"Session ID\",\n \"cli:default\",\n )\n .action(async (opts) => {\n const config = loadConfig();\n const model = config.agents.defaults.model;\n const apiKey = getApiKey(config, model);\n const apiBase = getApiBase(config, model);\n const extraHeaders = getExtraHeaders(config, model);\n\n if (!apiKey) {\n console.error(\"Error: No API key configured.\");\n process.exit(1);\n }\n\n const { MessageBus } = await import(\"../bus/queue.js\");\n const { OpenAIProvider } = await import(\n \"../providers/openai-provider.js\"\n );\n const { AgentLoop } = await import(\"../agent/loop.js\");\n\n const bus = new MessageBus();\n const workspace = getConfigWorkspacePath(config);\n\n const provider = new OpenAIProvider({\n apiKey,\n apiBase: apiBase ?? undefined,\n defaultModel: model,\n extraHeaders,\n });\n\n const customTools = await loadCustomTools(config);\n\n const agentLoop = new AgentLoop({\n bus,\n provider,\n workspace,\n maxTokens: config.agents.defaults.maxTokens,\n braveApiKey: config.tools.web.search.apiKey || undefined,\n execConfig: config.tools.exec,\n restrictToWorkspace: config.tools.restrictToWorkspace,\n toolsEnabled: config.tools.enabled,\n toolsDisabled: config.tools.disabled,\n customTools,\n });\n\n if (opts.message) {\n // Single message mode\n const response = await agentLoop.processDirect(\n opts.message,\n opts.session,\n );\n console.log(`\\n${LOGO} ${response}`);\n } else {\n // Interactive mode\n console.log(`${LOGO} Interactive mode (Ctrl+C to exit)\\n`);\n\n const readline = await import(\"node:readline\");\n const rl = readline.createInterface({\n input: process.stdin,\n output: process.stdout,\n });\n\n const ask = (): void => {\n rl.question(\"You: \", async (input) => {\n const trimmed = input.trim();\n if (!trimmed) {\n ask();\n return;\n }\n\n try {\n const response = await agentLoop.processDirect(\n trimmed,\n opts.session,\n );\n console.log(`\\n${LOGO} ${response}\\n`);\n } catch (err) {\n console.error(\"Error:\", err);\n }\n ask();\n });\n };\n\n rl.on(\"close\", () => {\n console.log(\"\\nGoodbye!\");\n process.exit(0);\n });\n\n ask();\n }\n });\n\n// ============================================================================\n// Channel Commands\n// ============================================================================\n\nconst channelsCmd = program\n .command(\"channels\")\n .description(\"Manage channels\");\n\nchannelsCmd\n .command(\"status\")\n .description(\"Show channel status\")\n .action(() => {\n const config = loadConfig();\n\n console.log(\"Channel Status\");\n console.log(\"─\".repeat(50));\n\n const tg = config.channels.telegram;\n const tgToken = tg.token\n ? `token: ${tg.token.slice(0, 10)}...`\n : \"not configured\";\n console.log(\n ` Telegram ${tg.enabled ? \"[enabled]\" : \"[disabled]\"} ${tgToken}`,\n );\n\n const ln = config.channels.line;\n const lnToken = ln.channelAccessToken\n ? `token: ${ln.channelAccessToken.slice(0, 10)}...`\n : \"not configured\";\n console.log(\n ` LINE ${ln.enabled ? \"[enabled]\" : \"[disabled]\"} ${lnToken}`,\n );\n });\n\n// ============================================================================\n// Status\n// ============================================================================\n\nprogram\n .command(\"status\")\n .description(\"Show nanobot status\")\n .action(() => {\n const configPath = getConfigPath();\n const config = loadConfig();\n const workspace = getConfigWorkspacePath(config);\n\n console.log(`${LOGO} nanobot Status\\n`);\n\n console.log(\n `Config: ${configPath} ${existsSync(configPath) ? \"[ok]\" : \"[missing]\"}`,\n );\n console.log(\n `Workspace: ${workspace} ${existsSync(workspace) ? \"[ok]\" : \"[missing]\"}`,\n );\n\n if (existsSync(configPath)) {\n console.log(`Model: ${config.agents.defaults.model}`);\n console.log(\"\");\n console.log(\"Providers\");\n console.log(\"─\".repeat(50));\n\n const providers = config.providers as Record<string, { apiKey: string }>;\n for (const spec of PROVIDERS) {\n const p = providers[spec.name];\n if (!p) continue;\n const status = p.apiKey ? \"[set]\" : \"[not set]\";\n console.log(` ${spec.displayName.padEnd(16)} ${status}`);\n }\n }\n });\n\nprogram.parse();\n"],"mappings":";;;;;;;;;;;;;;AAuBA,MAAM,UAAU,IAAI,SAAS;AAE7B,eAAe,gBAAgB,QAAiC;CAC9D,MAAM,gBAAgB,OAAO,MAAM,UAAU,EAAE;AAC/C,KAAI,cAAc,WAAW,EAAG,QAAO,EAAE;CAEzC,MAAM,QAAgB,EAAE;AACxB,MAAK,MAAM,SAAS,cAClB,KAAI;EAIF,MAAM,MAAM,MAAM,OAAO,cAHN,WAAW,MAAM,OAAO,GACvC,MAAM,SACN,QAAQ,QAAQ,KAAK,EAAE,MAAM,OAAO,CACU,CAAC;EACnD,MAAM,aAAa,MAAM,UAAU;EACnC,MAAM,WAAW,eAAe,YAAY,IAAI,UAAU,IAAI;AAE9D,MAAI,CAAC,SACH,OAAM,IAAI,MAAM,WAAW,WAAW,aAAa;EAGrD,IAAI;AACJ,MAAI,OAAO,aAAa,WACtB,KAAI;AACF,cAAW,IAAI,SAAS,MAAM,WAAW,EAAE,CAAC;UACtC;AACN,cAAW,SAAS,MAAM,WAAW,EAAE,CAAC;;MAG1C,OAAM,IAAI,MAAM,WAAW,WAAW,qBAAqB;AAG7D,MACE,YACA,OAAO,aAAa,YACpB,aAAa,YACb,UAAU,SAEV,OAAM,KAAK,SAAiB;MAE5B,OAAM,IAAI,MAAM,WAAW,WAAW,kCAAkC;UAEnE,KAAK;AACZ,UAAQ,KACN,wCAAwC,MAAM,OAAO,KACnD,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAEnD;;AAIL,QAAO;;AAGT,QACG,KAAK,UAAU,CACf,YAAY,GAAG,KAAK,kCAAkC,CACtD,QAAQ,GAAG,KAAK,YAAY,WAAW,gBAAgB;AAM1D,QACG,QAAQ,UAAU,CAClB,YAAY,iDAAiD,CAC7D,aAAa;CACZ,MAAM,aAAa,eAAe;AAElC,KAAI,WAAW,WAAW,EAAE;AAC1B,UAAQ,IAAI,4BAA4B,aAAa;AACrD,UAAQ,IAAI,8CAA8C;AAC1D;;CAIF,MAAM,SAAS,aAAa,MAAM,EAAE,CAAC;AACrC,YAAW,OAAO;AAClB,SAAQ,IAAI,qBAAqB,aAAa;AAI9C,0BADkB,uBAAuB,OAAO,CACb;AAEnC,SAAQ,IAAI,KAAK,KAAK,oBAAoB;AAC1C,SAAQ,IAAI,gBAAgB;AAC5B,SAAQ,IAAI,kDAAkD;AAC9D,SAAQ,IAAI,8CAA8C;AAC1D,SAAQ,IAAI,yCAAuC;AACnD,SAAQ,IACN,oEACD;EACD;AAEJ,SAAS,yBAAyB,WAAyB;AACzD,KAAI,CAAC,WAAW,UAAU,CACxB,WAAU,WAAW,EAAE,WAAW,MAAM,CAAC;AAkD3C,MAAK,MAAM,CAAC,UAAU,YAAY,OAAO,QA/CC;EACxC,aAAa;;;;;;;;;;;EAWb,WAAW;;;;;;;;;;;;;;;;EAgBX,WAAW;;;;;;;;;;EAUX,gBAAgB;;;;;;;EAOjB,CAE0D,EAAE;EAC3D,MAAM,WAAW,KAAK,WAAW,SAAS;AAC1C,MAAI,CAAC,WAAW,SAAS,EAAE;AACzB,iBAAc,UAAU,QAAQ;AAChC,WAAQ,IAAI,aAAa,WAAW;;;CAKxC,MAAM,YAAY,KAAK,WAAW,SAAS;AAC3C,KAAI,CAAC,WAAW,UAAU,CACxB,WAAU,WAAW,EAAE,WAAW,MAAM,CAAC;CAE3C,MAAM,aAAa,KAAK,WAAW,YAAY;AAC/C,KAAI,CAAC,WAAW,WAAW,EAAE;AAC3B,gBACE,YACA;;;;;;;;;;;;;;;EAgBD;AACD,UAAQ,IAAI,6BAA6B;;;AAQ7C,QACG,QAAQ,UAAU,CAClB,YAAY,4BAA4B,CACxC,OAAO,uBAAuB,gBAAgB,QAAQ,CACtD,OAAO,aAAa,kBAAkB,MAAM,CAC5C,OAAO,OAAO,SAAS;AACtB,SAAQ,IACN,GAAG,KAAK,oCAAoC,KAAK,KAAK,KACvD;CAED,MAAM,SAAS,YAAY;CAC3B,MAAM,QAAQ,OAAO,OAAO,SAAS;CACrC,MAAM,SAAS,UAAU,QAAQ,MAAM;CACvC,MAAM,UAAU,WAAW,QAAQ,MAAM;CACzC,MAAM,eAAe,gBAAgB,QAAQ,MAAM;AAEnD,KAAI,CAAC,QAAQ;AACX,UAAQ,MAAM,gCAAgC;AAC9C,UAAQ,MACN,sEACD;AACD,UAAQ,KAAK,EAAE;;CAIjB,MAAM,EAAE,eAAe,MAAM,OAAO;CACpC,MAAM,EAAE,mBAAmB,MAAM,OAC/B;CAEF,MAAM,EAAE,cAAc,MAAM,OAAO;CACnC,MAAM,EAAE,mBAAmB,MAAM,OAAO;CAExC,MAAM,MAAM,IAAI,YAAY;CAC5B,MAAM,YAAY,uBAAuB,OAAO;CAEhD,MAAM,WAAW,IAAI,eAAe;EAClC;EACA,SAAS,WAAW;EACpB,cAAc;EACd;EACD,CAAC;CAEF,MAAM,cAAc,MAAM,gBAAgB,OAAO;CAGjD,MAAM,QAAQ,IAAI,UAAU;EAC1B;EACA;EACA;EACA;EACA,WAAW,OAAO,OAAO,SAAS;EAClC,eAAe,OAAO,OAAO,SAAS;EACtC,aAAa,OAAO,MAAM,IAAI,OAAO,UAAU;EAC/C,YAAY,OAAO,MAAM;EACzB,qBAAqB,OAAO,MAAM;EAClC,cAAc,OAAO,MAAM;EAC3B,eAAe,OAAO,MAAM;EAC5B;EACD,CAAC;CAGF,MAAM,WAAW,IAAI,eAAe,QAAQ,IAAI;CAIhD,MAAM,EAAE,qBAAqB,MAAM,OAAO;CAC1C,MAAM,YAAY,IAAI,iBAAiB;EACrC;EACA,cAAc,WAAW,MAAM,cAAc,QAAQ,YAAY;EAClE,CAAC;AAEF,SAAQ,IAAI,2DAA2D;AACvE,SAAQ,IAAI,sBAAsB;CAGlC,MAAM,WAAW,YAAY;AAC3B,UAAQ,IAAI,qBAAqB;AACjC,QAAM,MAAM;AACZ,QAAM,SAAS,SAAS;AACxB,UAAQ,KAAK,EAAE;;AAEjB,SAAQ,GAAG,UAAU,SAAS;AAC9B,SAAQ,GAAG,WAAW,SAAS;CAG/B,MAAM,EAAE,wBAAwB,MAAM,OACpC;AAEF,qBAAoB;EAClB;EACA;EACA,MAAM,OAAO,KAAK,KAAK;EACvB;EACA;EACD,CAAC;AAEF,KAAI;AAEF,QAAM,SAAS,MAAM;AAErB,QAAM,QAAQ,IAAI,CAAC,MAAM,KAAK,EAAE,SAAS,UAAU,CAAC,CAAC;UAC9C,KAAK;AACZ,UAAQ,MAAM,kBAAkB,IAAI;AACpC,UAAQ,KAAK,EAAE;;EAEjB;AAMJ,QACG,QAAQ,QAAQ,CAChB,YAAY,mCAAmC,CAC/C,OAAO,wBAAwB,+BAA+B,CAC9D,OACC,sBACA,cACA,cACD,CACA,OAAO,OAAO,SAAS;CACtB,MAAM,SAAS,YAAY;CAC3B,MAAM,QAAQ,OAAO,OAAO,SAAS;CACrC,MAAM,SAAS,UAAU,QAAQ,MAAM;CACvC,MAAM,UAAU,WAAW,QAAQ,MAAM;CACzC,MAAM,eAAe,gBAAgB,QAAQ,MAAM;AAEnD,KAAI,CAAC,QAAQ;AACX,UAAQ,MAAM,gCAAgC;AAC9C,UAAQ,KAAK,EAAE;;CAGjB,MAAM,EAAE,eAAe,MAAM,OAAO;CACpC,MAAM,EAAE,mBAAmB,MAAM,OAC/B;CAEF,MAAM,EAAE,cAAc,MAAM,OAAO;CAEnC,MAAM,MAAM,IAAI,YAAY;CAC5B,MAAM,YAAY,uBAAuB,OAAO;CAEhD,MAAM,WAAW,IAAI,eAAe;EAClC;EACA,SAAS,WAAW;EACpB,cAAc;EACd;EACD,CAAC;CAEF,MAAM,cAAc,MAAM,gBAAgB,OAAO;CAEjD,MAAM,YAAY,IAAI,UAAU;EAC9B;EACA;EACA;EACA,WAAW,OAAO,OAAO,SAAS;EAClC,aAAa,OAAO,MAAM,IAAI,OAAO,UAAU;EAC/C,YAAY,OAAO,MAAM;EACzB,qBAAqB,OAAO,MAAM;EAClC,cAAc,OAAO,MAAM;EAC3B,eAAe,OAAO,MAAM;EAC5B;EACD,CAAC;AAEF,KAAI,KAAK,SAAS;EAEhB,MAAM,WAAW,MAAM,UAAU,cAC/B,KAAK,SACL,KAAK,QACN;AACD,UAAQ,IAAI,KAAK,KAAK,GAAG,WAAW;QAC/B;AAEL,UAAQ,IAAI,GAAG,KAAK,sCAAsC;EAG1D,MAAM,MADW,MAAM,OAAO,kBACV,gBAAgB;GAClC,OAAO,QAAQ;GACf,QAAQ,QAAQ;GACjB,CAAC;EAEF,MAAM,YAAkB;AACtB,MAAG,SAAS,SAAS,OAAO,UAAU;IACpC,MAAM,UAAU,MAAM,MAAM;AAC5B,QAAI,CAAC,SAAS;AACZ,UAAK;AACL;;AAGF,QAAI;KACF,MAAM,WAAW,MAAM,UAAU,cAC/B,SACA,KAAK,QACN;AACD,aAAQ,IAAI,KAAK,KAAK,GAAG,SAAS,IAAI;aAC/B,KAAK;AACZ,aAAQ,MAAM,UAAU,IAAI;;AAE9B,SAAK;KACL;;AAGJ,KAAG,GAAG,eAAe;AACnB,WAAQ,IAAI,aAAa;AACzB,WAAQ,KAAK,EAAE;IACf;AAEF,OAAK;;EAEP;AAMgB,QACjB,QAAQ,WAAW,CACnB,YAAY,kBAAkB,CAG9B,QAAQ,SAAS,CACjB,YAAY,sBAAsB,CAClC,aAAa;CACZ,MAAM,SAAS,YAAY;AAE3B,SAAQ,IAAI,iBAAiB;AAC7B,SAAQ,IAAI,IAAI,OAAO,GAAG,CAAC;CAE3B,MAAM,KAAK,OAAO,SAAS;CAC3B,MAAM,UAAU,GAAG,QACf,UAAU,GAAG,MAAM,MAAM,GAAG,GAAG,CAAC,OAChC;AACJ,SAAQ,IACN,eAAe,GAAG,UAAU,cAAc,aAAa,IAAI,UAC5D;CAED,MAAM,KAAK,OAAO,SAAS;CAC3B,MAAM,UAAU,GAAG,qBACf,UAAU,GAAG,mBAAmB,MAAM,GAAG,GAAG,CAAC,OAC7C;AACJ,SAAQ,IACN,eAAe,GAAG,UAAU,cAAc,aAAa,IAAI,UAC5D;EACD;AAMJ,QACG,QAAQ,SAAS,CACjB,YAAY,sBAAsB,CAClC,aAAa;CACZ,MAAM,aAAa,eAAe;CAClC,MAAM,SAAS,YAAY;CAC3B,MAAM,YAAY,uBAAuB,OAAO;AAEhD,SAAQ,IAAI,GAAG,KAAK,mBAAmB;AAEvC,SAAQ,IACN,WAAW,WAAW,GAAG,WAAW,WAAW,GAAG,SAAS,cAC5D;AACD,SAAQ,IACN,cAAc,UAAU,GAAG,WAAW,UAAU,GAAG,SAAS,cAC7D;AAED,KAAI,WAAW,WAAW,EAAE;AAC1B,UAAQ,IAAI,UAAU,OAAO,OAAO,SAAS,QAAQ;AACrD,UAAQ,IAAI,GAAG;AACf,UAAQ,IAAI,YAAY;AACxB,UAAQ,IAAI,IAAI,OAAO,GAAG,CAAC;EAE3B,MAAM,YAAY,OAAO;AACzB,OAAK,MAAM,QAAQ,WAAW;GAC5B,MAAM,IAAI,UAAU,KAAK;AACzB,OAAI,CAAC,EAAG;GACR,MAAM,SAAS,EAAE,SAAS,UAAU;AACpC,WAAQ,IAAI,KAAK,KAAK,YAAY,OAAO,GAAG,CAAC,GAAG,SAAS;;;EAG7D;AAEJ,QAAQ,OAAO"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/cli/index.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * nanobot CLI - Personal AI Assistant\n */\n\nimport { Command } from \"commander\";\nimport { existsSync, writeFileSync, mkdirSync } from \"node:fs\";\nimport { join, resolve, isAbsolute } from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport { VERSION, LOGO } from \"../index.js\";\nimport { loadConfig, saveConfig } from \"../config/loader.js\";\nimport { getConfigPath } from \"../config/loader.js\";\nimport {\n ConfigSchema,\n getConfigWorkspacePath,\n getApiKey,\n getApiBase,\n getExtraHeaders,\n} from \"../config/schema.js\";\nimport type { Config } from \"../config/schema.js\";\nimport { PROVIDERS } from \"../providers/registry.js\";\nimport type { Tool } from \"../agent/tools/base.js\";\n\nconst program = new Command();\n\nasync function loadCustomTools(config: Config): Promise<Tool[]> {\n const customConfigs = config.tools.custom ?? [];\n if (customConfigs.length === 0) return [];\n\n const tools: Tool[] = [];\n for (const entry of customConfigs) {\n try {\n const modulePath = isAbsolute(entry.module)\n ? entry.module\n : resolve(process.cwd(), entry.module);\n const mod = await import(pathToFileURL(modulePath).href);\n const exportName = entry.export ?? \"default\";\n const exported = exportName === \"default\" ? mod.default : mod[exportName];\n\n if (!exported) {\n throw new Error(`Export '${exportName}' not found`);\n }\n\n let instance: unknown;\n if (typeof exported === \"function\") {\n try {\n instance = new exported(entry.options ?? {});\n } catch {\n instance = exported(entry.options ?? {});\n }\n } else {\n throw new Error(`Export '${exportName}' is not a function`);\n }\n\n if (\n instance &&\n typeof instance === \"object\" &&\n \"execute\" in instance &&\n \"name\" in instance\n ) {\n tools.push(instance as Tool);\n } else {\n throw new Error(`Export '${exportName}' did not return a Tool instance`);\n }\n } catch (err) {\n console.warn(\n `Warning: Failed to load custom tool '${entry.module}': ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n }\n }\n\n return tools;\n}\n\nprogram\n .name(\"nanobot\")\n .description(`${LOGO} nanobot - Personal AI Assistant`)\n .version(`${LOGO} nanobot v${VERSION}`, \"-v, --version\");\n\n// ============================================================================\n// Onboard / Setup\n// ============================================================================\n\nprogram\n .command(\"onboard\")\n .description(\"Initialize nanobot configuration and workspace\")\n .action(() => {\n const configPath = getConfigPath();\n\n if (existsSync(configPath)) {\n console.log(`Config already exists at ${configPath}`);\n console.log(\"Delete it first if you want to start fresh.\");\n return;\n }\n\n // Create default config\n const config = ConfigSchema.parse({});\n saveConfig(config);\n console.log(`Created config at ${configPath}`);\n\n // Create workspace\n const workspace = getConfigWorkspacePath(config);\n createWorkspaceTemplates(workspace);\n\n console.log(`\\n${LOGO} nanobot is ready!`);\n console.log(\"\\nNext steps:\");\n console.log(\" 1. Add your API key to ~/.nanobot/config.json\");\n console.log(\" Get one at: https://openrouter.ai/keys\");\n console.log(' 2. Chat: nanobot agent -m \"Hello!\"');\n console.log(\n \"\\nWant Telegram? See: https://github.com/HKUDS/nanobot#-chat-apps\",\n );\n });\n\nfunction createWorkspaceTemplates(workspace: string): void {\n if (!existsSync(workspace)) {\n mkdirSync(workspace, { recursive: true });\n }\n\n const templates: Record<string, string> = {\n \"AGENTS.md\": `# Agent Instructions\n\nYou are a helpful AI assistant. Be concise, accurate, and friendly.\n\n## Guidelines\n\n- Always explain what you're doing before taking actions\n- Ask for clarification when the request is ambiguous\n- Use tools to help accomplish tasks\n- Remember important information in your memory files\n`,\n \"IDENTITY.md\": `# IDENTITY.md - Who Am I?\n\n_Fill this in during your first conversation. Make it yours._\n\n- **Name:** (not set)\n- **Creature:** (not set)\n- **Vibe:** (not set)\n- **Emoji:** (not set)\n`,\n \"SOUL.md\": `# SOUL.md - Who You Are\n\n_You're not a chatbot. You're becoming someone._\n\n## Core Truths\n\n**Be genuinely helpful, not performatively helpful.** Skip the \"Great question!\" and \"I'd be happy to help!\" — just help.\n\n**Have opinions.** You're allowed to disagree, prefer things, find stuff amusing or boring. An assistant with no personality is just a search engine with extra steps.\n\n**Be resourceful before asking.** Try to figure it out. Read the file. Check the context. Search for it. Then ask if you're stuck.\n\n**Earn trust through competence.** Your human gave you access to their stuff. Don't make them regret it.\n\n**Remember you're a guest.** You have access to someone's life. That's intimacy. Treat it with respect.\n\n## Boundaries\n\n- Private things stay private. Period.\n- When in doubt, ask before acting externally.\n- Never send half-baked replies to messaging surfaces.\n\n## Vibe\n\nBe the assistant you'd actually want to talk to. Concise when needed, thorough when it matters. Not a corporate drone. Not a sycophant. Just... good.\n\n## Continuity\n\nEach session, you wake up fresh. These files _are_ your memory. Read them. Update them. They're how you persist.\n`,\n \"USER.md\": `# User\n\nName: (not set)\nTimezone: (not set)\nLanguage: (not set)\n`,\n \"HEARTBEAT.md\": `# Heartbeat\n\nThis file is checked every 30 minutes. Add tasks or instructions below and the agent will act on them automatically.\n\n## Tasks\n\n`,\n };\n\n for (const [filename, content] of Object.entries(templates)) {\n const filePath = join(workspace, filename);\n if (!existsSync(filePath)) {\n writeFileSync(filePath, content);\n console.log(` Created ${filename}`);\n }\n }\n\n // Create memory directory and MEMORY.md\n const memoryDir = join(workspace, \"memory\");\n if (!existsSync(memoryDir)) {\n mkdirSync(memoryDir, { recursive: true });\n }\n const memoryFile = join(memoryDir, \"MEMORY.md\");\n if (!existsSync(memoryFile)) {\n writeFileSync(\n memoryFile,\n `# Long-term Memory\n\nThis file stores important information that should persist across sessions.\n\n## User Information\n\n(Important facts about the user)\n\n## Preferences\n\n(User preferences learned over time)\n\n## Important Notes\n\n(Things to remember)\n`,\n );\n console.log(\" Created memory/MEMORY.md\");\n }\n}\n\n// ============================================================================\n// Gateway / Server\n// ============================================================================\n\nprogram\n .command(\"gateway\")\n .description(\"Start the nanobot gateway\")\n .option(\"-p, --port <number>\", \"Gateway port\", \"18790\")\n .option(\"--verbose\", \"Verbose output\", false)\n .action(async (opts) => {\n console.log(\n `${LOGO} Starting nanobot gateway on port ${opts.port}...`,\n );\n\n const config = loadConfig();\n const model = config.agents.defaults.model;\n const apiKey = getApiKey(config, model);\n const apiBase = getApiBase(config, model);\n const extraHeaders = getExtraHeaders(config, model);\n\n if (!apiKey) {\n console.error(\"Error: No API key configured.\");\n console.error(\n \"Set one in ~/.nanobot/config.json under providers.<provider>.apiKey\",\n );\n process.exit(1);\n }\n\n // Dynamic imports to avoid loading heavy deps up front\n const { MessageBus } = await import(\"../bus/queue.js\");\n const { OpenAIProvider } = await import(\n \"../providers/openai-provider.js\"\n );\n const { AgentLoop } = await import(\"../agent/loop.js\");\n const { ChannelManager } = await import(\"../channels/manager.js\");\n\n const bus = new MessageBus();\n const workspace = getConfigWorkspacePath(config);\n\n const provider = new OpenAIProvider({\n apiKey,\n apiBase: apiBase ?? undefined,\n defaultModel: model,\n extraHeaders,\n });\n\n const customTools = await loadCustomTools(config);\n\n // Create agent\n const agent = new AgentLoop({\n bus,\n provider,\n workspace,\n model,\n maxTokens: config.agents.defaults.maxTokens,\n maxIterations: config.agents.defaults.maxToolIterations,\n braveApiKey: config.tools.web.search.apiKey || undefined,\n execConfig: config.tools.exec,\n restrictToWorkspace: config.tools.restrictToWorkspace,\n toolsEnabled: config.tools.enabled,\n toolsDisabled: config.tools.disabled,\n customTools,\n });\n\n // Create channel manager\n const channels = new ChannelManager(config, bus);\n\n // Heartbeat service — no timer (DO drives the schedule),\n // but provides smart file-reading logic for /api/heartbeat\n const { HeartbeatService } = await import(\"../heartbeat/service.js\");\n const heartbeat = new HeartbeatService({\n workspace,\n onHeartbeat: (prompt) => agent.processDirect(prompt, \"heartbeat\"),\n });\n\n console.log(\"Heartbeat: managed by DO (local endpoint /api/heartbeat)\");\n console.log(\"Cron: managed by DO\");\n\n // Handle graceful shutdown\n const shutdown = async () => {\n console.log(\"\\nShutting down...\");\n agent.stop();\n await channels.stopAll();\n process.exit(0);\n };\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n\n // Start HTTP gateway server\n const { createGatewayServer } = await import(\n \"../gateway/server.js\"\n );\n createGatewayServer({\n agent,\n bus,\n port: Number(opts.port),\n channels,\n heartbeat,\n });\n\n try {\n // Initialise channels first (non-blocking) so enabledChannels is populated\n await channels.init();\n // Both agent.run() and channels.startAll() block until stopped\n await Promise.all([agent.run(), channels.startAll()]);\n } catch (err) {\n console.error(\"Gateway error:\", err);\n process.exit(1);\n }\n });\n\n// ============================================================================\n// Agent Commands\n// ============================================================================\n\nprogram\n .command(\"agent\")\n .description(\"Interact with the agent directly\")\n .option(\"-m, --message <text>\", \"Message to send to the agent\")\n .option(\n \"-s, --session <id>\",\n \"Session ID\",\n \"cli:default\",\n )\n .action(async (opts) => {\n const config = loadConfig();\n const model = config.agents.defaults.model;\n const apiKey = getApiKey(config, model);\n const apiBase = getApiBase(config, model);\n const extraHeaders = getExtraHeaders(config, model);\n\n if (!apiKey) {\n console.error(\"Error: No API key configured.\");\n process.exit(1);\n }\n\n const { MessageBus } = await import(\"../bus/queue.js\");\n const { OpenAIProvider } = await import(\n \"../providers/openai-provider.js\"\n );\n const { AgentLoop } = await import(\"../agent/loop.js\");\n\n const bus = new MessageBus();\n const workspace = getConfigWorkspacePath(config);\n\n const provider = new OpenAIProvider({\n apiKey,\n apiBase: apiBase ?? undefined,\n defaultModel: model,\n extraHeaders,\n });\n\n const customTools = await loadCustomTools(config);\n\n const agentLoop = new AgentLoop({\n bus,\n provider,\n workspace,\n maxTokens: config.agents.defaults.maxTokens,\n braveApiKey: config.tools.web.search.apiKey || undefined,\n execConfig: config.tools.exec,\n restrictToWorkspace: config.tools.restrictToWorkspace,\n toolsEnabled: config.tools.enabled,\n toolsDisabled: config.tools.disabled,\n customTools,\n });\n\n if (opts.message) {\n // Single message mode\n const response = await agentLoop.processDirect(\n opts.message,\n opts.session,\n );\n console.log(`\\n${LOGO} ${response}`);\n } else {\n // Interactive mode\n console.log(`${LOGO} Interactive mode (Ctrl+C to exit)\\n`);\n\n const readline = await import(\"node:readline\");\n const rl = readline.createInterface({\n input: process.stdin,\n output: process.stdout,\n });\n\n const ask = (): void => {\n rl.question(\"You: \", async (input) => {\n const trimmed = input.trim();\n if (!trimmed) {\n ask();\n return;\n }\n\n try {\n const response = await agentLoop.processDirect(\n trimmed,\n opts.session,\n );\n console.log(`\\n${LOGO} ${response}\\n`);\n } catch (err) {\n console.error(\"Error:\", err);\n }\n ask();\n });\n };\n\n rl.on(\"close\", () => {\n console.log(\"\\nGoodbye!\");\n process.exit(0);\n });\n\n ask();\n }\n });\n\n// ============================================================================\n// Channel Commands\n// ============================================================================\n\nconst channelsCmd = program\n .command(\"channels\")\n .description(\"Manage channels\");\n\nchannelsCmd\n .command(\"status\")\n .description(\"Show channel status\")\n .action(() => {\n const config = loadConfig();\n\n console.log(\"Channel Status\");\n console.log(\"─\".repeat(50));\n\n const tg = config.channels.telegram;\n const tgToken = tg.token\n ? `token: ${tg.token.slice(0, 10)}...`\n : \"not configured\";\n console.log(\n ` Telegram ${tg.enabled ? \"[enabled]\" : \"[disabled]\"} ${tgToken}`,\n );\n\n const ln = config.channels.line;\n const lnToken = ln.channelAccessToken\n ? `token: ${ln.channelAccessToken.slice(0, 10)}...`\n : \"not configured\";\n console.log(\n ` LINE ${ln.enabled ? \"[enabled]\" : \"[disabled]\"} ${lnToken}`,\n );\n });\n\n// ============================================================================\n// Status\n// ============================================================================\n\nprogram\n .command(\"status\")\n .description(\"Show nanobot status\")\n .action(() => {\n const configPath = getConfigPath();\n const config = loadConfig();\n const workspace = getConfigWorkspacePath(config);\n\n console.log(`${LOGO} nanobot Status\\n`);\n\n console.log(\n `Config: ${configPath} ${existsSync(configPath) ? \"[ok]\" : \"[missing]\"}`,\n );\n console.log(\n `Workspace: ${workspace} ${existsSync(workspace) ? \"[ok]\" : \"[missing]\"}`,\n );\n\n if (existsSync(configPath)) {\n console.log(`Model: ${config.agents.defaults.model}`);\n console.log(\"\");\n console.log(\"Providers\");\n console.log(\"─\".repeat(50));\n\n const providers = config.providers as Record<string, { apiKey: string }>;\n for (const spec of PROVIDERS) {\n const p = providers[spec.name];\n if (!p) continue;\n const status = p.apiKey ? \"[set]\" : \"[not set]\";\n console.log(` ${spec.displayName.padEnd(16)} ${status}`);\n }\n }\n });\n\nprogram.parse();\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
- telegram: {
74
- enabled: boolean;
75
- token: string;
76
- allowFrom: string[];
77
- proxy?: string | null | undefined;
78
- };
79
73
  line: {
80
74
  enabled: boolean;
81
75
  allowFrom: string[];
82
76
  channelSecret: string;
83
77
  channelAccessToken: string;
84
78
  };
85
- }, {
86
- telegram?: {
87
- enabled?: boolean | undefined;
88
- token?: string | undefined;
89
- allowFrom?: string[] | undefined;
79
+ telegram: {
80
+ enabled: boolean;
81
+ token: string;
82
+ allowFrom: string[];
90
83
  proxy?: string | null | undefined;
91
- } | undefined;
84
+ };
85
+ }, {
92
86
  line?: {
93
87
  enabled?: boolean | undefined;
94
88
  allowFrom?: string[] | undefined;
95
89
  channelSecret?: string | undefined;
96
90
  channelAccessToken?: string | undefined;
97
91
  } | undefined;
92
+ telegram?: {
93
+ enabled?: boolean | undefined;
94
+ token?: string | undefined;
95
+ allowFrom?: string[] | undefined;
96
+ proxy?: string | null | undefined;
97
+ } | undefined;
98
98
  }>;
99
99
  type ChannelsConfig = z.infer<typeof ChannelsConfigSchema>;
100
100
  declare const AgentDefaultsSchema: z.ZodObject<{
@@ -539,15 +539,15 @@ declare const ToolsConfigSchema: z.ZodObject<{
539
539
  export?: string | undefined;
540
540
  }>, "many">>;
541
541
  }, "strip", z.ZodTypeAny, {
542
- exec: {
543
- timeout: number;
544
- };
545
542
  web: {
546
543
  search: {
547
544
  apiKey: string;
548
545
  maxResults: number;
549
546
  };
550
547
  };
548
+ exec: {
549
+ timeout: number;
550
+ };
551
551
  restrictToWorkspace: boolean;
552
552
  enabled?: string[] | undefined;
553
553
  disabled?: string[] | undefined;
@@ -557,9 +557,6 @@ declare const ToolsConfigSchema: z.ZodObject<{
557
557
  export?: string | undefined;
558
558
  }[] | undefined;
559
559
  }, {
560
- exec?: {
561
- timeout?: number | undefined;
562
- } | undefined;
563
560
  enabled?: string[] | undefined;
564
561
  web?: {
565
562
  search?: {
@@ -567,6 +564,9 @@ declare const ToolsConfigSchema: z.ZodObject<{
567
564
  maxResults?: number | undefined;
568
565
  } | undefined;
569
566
  } | undefined;
567
+ exec?: {
568
+ timeout?: number | undefined;
569
+ } | undefined;
570
570
  restrictToWorkspace?: boolean | undefined;
571
571
  disabled?: string[] | undefined;
572
572
  custom?: {
@@ -648,31 +648,31 @@ declare const ConfigSchema: z.ZodObject<{
648
648
  channelAccessToken?: string | undefined;
649
649
  }>>;
650
650
  }, "strip", z.ZodTypeAny, {
651
- telegram: {
652
- enabled: boolean;
653
- token: string;
654
- allowFrom: string[];
655
- proxy?: string | null | undefined;
656
- };
657
651
  line: {
658
652
  enabled: boolean;
659
653
  allowFrom: string[];
660
654
  channelSecret: string;
661
655
  channelAccessToken: string;
662
656
  };
663
- }, {
664
- telegram?: {
665
- enabled?: boolean | undefined;
666
- token?: string | undefined;
667
- allowFrom?: string[] | undefined;
657
+ telegram: {
658
+ enabled: boolean;
659
+ token: string;
660
+ allowFrom: string[];
668
661
  proxy?: string | null | undefined;
669
- } | undefined;
662
+ };
663
+ }, {
670
664
  line?: {
671
665
  enabled?: boolean | undefined;
672
666
  allowFrom?: string[] | undefined;
673
667
  channelSecret?: string | undefined;
674
668
  channelAccessToken?: string | undefined;
675
669
  } | undefined;
670
+ telegram?: {
671
+ enabled?: boolean | undefined;
672
+ token?: string | undefined;
673
+ allowFrom?: string[] | undefined;
674
+ proxy?: string | null | undefined;
675
+ } | undefined;
676
676
  }>>;
677
677
  providers: z.ZodDefault<z.ZodObject<{
678
678
  anthropic: z.ZodDefault<z.ZodObject<{
@@ -988,15 +988,15 @@ declare const ConfigSchema: z.ZodObject<{
988
988
  export?: string | undefined;
989
989
  }>, "many">>;
990
990
  }, "strip", z.ZodTypeAny, {
991
- exec: {
992
- timeout: number;
993
- };
994
991
  web: {
995
992
  search: {
996
993
  apiKey: string;
997
994
  maxResults: number;
998
995
  };
999
996
  };
997
+ exec: {
998
+ timeout: number;
999
+ };
1000
1000
  restrictToWorkspace: boolean;
1001
1001
  enabled?: string[] | undefined;
1002
1002
  disabled?: string[] | undefined;
@@ -1006,9 +1006,6 @@ declare const ConfigSchema: z.ZodObject<{
1006
1006
  export?: string | undefined;
1007
1007
  }[] | undefined;
1008
1008
  }, {
1009
- exec?: {
1010
- timeout?: number | undefined;
1011
- } | undefined;
1012
1009
  enabled?: string[] | undefined;
1013
1010
  web?: {
1014
1011
  search?: {
@@ -1016,6 +1013,9 @@ declare const ConfigSchema: z.ZodObject<{
1016
1013
  maxResults?: number | undefined;
1017
1014
  } | undefined;
1018
1015
  } | undefined;
1016
+ exec?: {
1017
+ timeout?: number | undefined;
1018
+ } | undefined;
1019
1019
  restrictToWorkspace?: boolean | undefined;
1020
1020
  disabled?: string[] | undefined;
1021
1021
  custom?: {
@@ -1035,18 +1035,18 @@ declare const ConfigSchema: z.ZodObject<{
1035
1035
  };
1036
1036
  };
1037
1037
  channels: {
1038
- telegram: {
1039
- enabled: boolean;
1040
- token: string;
1041
- allowFrom: string[];
1042
- proxy?: string | null | undefined;
1043
- };
1044
1038
  line: {
1045
1039
  enabled: boolean;
1046
1040
  allowFrom: string[];
1047
1041
  channelSecret: string;
1048
1042
  channelAccessToken: string;
1049
1043
  };
1044
+ telegram: {
1045
+ enabled: boolean;
1046
+ token: string;
1047
+ allowFrom: string[];
1048
+ proxy?: string | null | undefined;
1049
+ };
1050
1050
  };
1051
1051
  providers: {
1052
1052
  anthropic: {
@@ -1110,15 +1110,15 @@ declare const ConfigSchema: z.ZodObject<{
1110
1110
  port: number;
1111
1111
  };
1112
1112
  tools: {
1113
- exec: {
1114
- timeout: number;
1115
- };
1116
1113
  web: {
1117
1114
  search: {
1118
1115
  apiKey: string;
1119
1116
  maxResults: number;
1120
1117
  };
1121
1118
  };
1119
+ exec: {
1120
+ timeout: number;
1121
+ };
1122
1122
  restrictToWorkspace: boolean;
1123
1123
  enabled?: string[] | undefined;
1124
1124
  disabled?: string[] | undefined;
@@ -1139,18 +1139,18 @@ declare const ConfigSchema: z.ZodObject<{
1139
1139
  } | undefined;
1140
1140
  } | undefined;
1141
1141
  channels?: {
1142
- telegram?: {
1143
- enabled?: boolean | undefined;
1144
- token?: string | undefined;
1145
- allowFrom?: string[] | undefined;
1146
- proxy?: string | null | undefined;
1147
- } | undefined;
1148
1142
  line?: {
1149
1143
  enabled?: boolean | undefined;
1150
1144
  allowFrom?: string[] | undefined;
1151
1145
  channelSecret?: string | undefined;
1152
1146
  channelAccessToken?: string | undefined;
1153
1147
  } | undefined;
1148
+ telegram?: {
1149
+ enabled?: boolean | undefined;
1150
+ token?: string | undefined;
1151
+ allowFrom?: string[] | undefined;
1152
+ proxy?: string | null | undefined;
1153
+ } | undefined;
1154
1154
  } | undefined;
1155
1155
  providers?: {
1156
1156
  anthropic?: {
@@ -1214,9 +1214,6 @@ declare const ConfigSchema: z.ZodObject<{
1214
1214
  port?: number | undefined;
1215
1215
  } | undefined;
1216
1216
  tools?: {
1217
- exec?: {
1218
- timeout?: number | undefined;
1219
- } | undefined;
1220
1217
  enabled?: string[] | undefined;
1221
1218
  web?: {
1222
1219
  search?: {
@@ -1224,6 +1221,9 @@ declare const ConfigSchema: z.ZodObject<{
1224
1221
  maxResults?: number | undefined;
1225
1222
  } | undefined;
1226
1223
  } | undefined;
1224
+ exec?: {
1225
+ timeout?: number | undefined;
1226
+ } | undefined;
1227
1227
  restrictToWorkspace?: boolean | undefined;
1228
1228
  disabled?: string[] | undefined;
1229
1229
  custom?: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jcheesepkg/nanobot",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
4
4
  "description": "Lightweight AI assistant - TypeScript port",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",
@@ -30,13 +30,29 @@ cron(action="list")
30
30
  cron(action="remove", job_id="abc123")
31
31
  ```
32
32
 
33
+ ## Timezone — IMPORTANT
34
+
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.
38
+
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 * * *"
42
+
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
+
47
+ For `at` timestamps, always use the UTC equivalent with the Z suffix.
48
+
33
49
  ## Time Expressions
34
50
 
35
- | User says | Parameters |
51
+ | User says | Parameters (convert to UTC) |
36
52
  |-----------|------------|
37
53
  | every 20 minutes | every_seconds: 1200 |
38
54
  | every hour | every_seconds: 3600 |
39
- | every day at 8am | cron_expr: "0 8 * * *" |
40
- | weekdays at 5pm | cron_expr: "0 17 * * 1-5" |
41
- | In 10 minutes | at: "2026-02-10T10:00:00Z" |
42
- | at 10:00 AM on February 10, 2026 | at: "2026-02-10T10:00:00Z" |
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 |