@jcheesepkg/nanobot 0.2.6 → 0.3.1

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.
@@ -10,10 +10,6 @@ declare abstract class BaseChannel {
10
10
  protected config: unknown;
11
11
  protected bus: MessageBus;
12
12
  protected _running: boolean;
13
- /** Chat IDs that have sent at least one message to this channel. */
14
- readonly knownChatIds: Set<string>;
15
- /** Called when a new chat ID is added (set by ChannelManager for persistence). */
16
- onNewChatId: (() => void) | null;
17
13
  constructor(config: unknown, bus: MessageBus);
18
14
  abstract start(): Promise<void>;
19
15
  abstract stop(): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"base.d.mts","names":[],"sources":["../../src/channels/base.ts"],"mappings":";;;;;;AAOA;uBAAsB,WAAA;EAAA,kBACF,IAAA;EAAA,UACR,MAAA;EAAA,UACA,GAAA,EAAK,UAAA;EAAA,UACL,QAAA;EAaQ;EAAA,SAVT,YAAA,EAAY,GAAA;EAYF;EATnB,WAAA;cAEY,MAAA,WAAiB,GAAA,EAAK,UAAA;EAAA,SAKzB,KAAA,CAAA,GAAS,OAAA;EAAA,SACT,IAAA,CAAA,GAAQ,OAAA;EAAA,SACR,IAAA,CAAK,GAAA,EAAK,eAAA,GAAkB,OAAA;EAlBnB;EAqBlB,SAAA,CAAU,QAAA;EAnBA;EAAA,UAoCM,aAAA,CAAc,MAAA;IAC5B,QAAA;IACA,MAAA;IACA,OAAA;IACA,KAAA;IACA,QAAA,GAAW,MAAA;EAAA,IACT,OAAA;EAAA,IAsBA,SAAA,CAAA;AAAA"}
1
+ {"version":3,"file":"base.d.mts","names":[],"sources":["../../src/channels/base.ts"],"mappings":";;;;;;AAOA;uBAAsB,WAAA;EAAA,kBACF,IAAA;EAAA,UACR,MAAA;EAAA,UACA,GAAA,EAAK,UAAA;EAAA,UACL,QAAA;cAEE,MAAA,WAAiB,GAAA,EAAK,UAAA;EAAA,SAKzB,KAAA,CAAA,GAAS,OAAA;EAAA,SACT,IAAA,CAAA,GAAQ,OAAA;EAAA,SACR,IAAA,CAAK,GAAA,EAAK,eAAA,GAAkB,OAAA;EA0BjC;EAvBJ,SAAA,CAAU,QAAA;EAuBC;EAAA,UANK,aAAA,CAAc,MAAA;IAC5B,QAAA;IACA,MAAA;IACA,OAAA;IACA,KAAA;IACA,QAAA,GAAW,MAAA;EAAA,IACT,OAAA;EAAA,IAeA,SAAA,CAAA;AAAA"}
@@ -8,10 +8,6 @@ var BaseChannel = class {
8
8
  config;
9
9
  bus;
10
10
  _running = false;
11
- /** Chat IDs that have sent at least one message to this channel. */
12
- knownChatIds = /* @__PURE__ */ new Set();
13
- /** Called when a new chat ID is added (set by ChannelManager for persistence). */
14
- onNewChatId = null;
15
11
  constructor(config, bus) {
16
12
  this.config = config;
17
13
  this.bus = bus;
@@ -30,11 +26,6 @@ var BaseChannel = class {
30
26
  /** Handle an incoming message from the chat platform. */
31
27
  async handleMessage(params) {
32
28
  if (!this.isAllowed(params.senderId)) return;
33
- const chatIdStr = String(params.chatId);
34
- if (!this.knownChatIds.has(chatIdStr)) {
35
- this.knownChatIds.add(chatIdStr);
36
- this.onNewChatId?.();
37
- }
38
29
  const msg = createInboundMessage({
39
30
  channel: this.name,
40
31
  senderId: String(params.senderId),
@@ -1 +1 @@
1
- {"version":3,"file":"base.mjs","names":[],"sources":["../../src/channels/base.ts"],"sourcesContent":["import type { OutboundMessage, InboundMessage } from \"../bus/events.js\";\nimport { createInboundMessage } from \"../bus/events.js\";\nimport type { MessageBus } from \"../bus/queue.js\";\n\n/**\n * Abstract base class for chat channel implementations.\n */\nexport abstract class BaseChannel {\n abstract readonly name: string;\n protected config: unknown;\n protected bus: MessageBus;\n protected _running = false;\n\n /** Chat IDs that have sent at least one message to this channel. */\n readonly knownChatIds = new Set<string>();\n\n /** Called when a new chat ID is added (set by ChannelManager for persistence). */\n onNewChatId: (() => void) | null = null;\n\n constructor(config: unknown, bus: MessageBus) {\n this.config = config;\n this.bus = bus;\n }\n\n abstract start(): Promise<void>;\n abstract stop(): Promise<void>;\n abstract send(msg: OutboundMessage): Promise<void>;\n\n /** Check if a sender is allowed to use this bot. */\n isAllowed(senderId: string): boolean {\n const allowList = (this.config as { allowFrom?: string[] })?.allowFrom ?? [];\n if (allowList.length === 0) return true;\n\n const senderStr = String(senderId);\n if (allowList.includes(senderStr)) return true;\n\n // Check pipe-separated IDs (e.g., \"123456|username\")\n if (senderStr.includes(\"|\")) {\n for (const part of senderStr.split(\"|\")) {\n if (part && allowList.includes(part)) return true;\n }\n }\n return false;\n }\n\n /** Handle an incoming message from the chat platform. */\n protected async handleMessage(params: {\n senderId: string;\n chatId: string;\n content: string;\n media?: string[];\n metadata?: Record<string, unknown>;\n }): Promise<void> {\n if (!this.isAllowed(params.senderId)) return;\n\n // Track this chat so broadcasts (cron, etc.) can reach it later\n const chatIdStr = String(params.chatId);\n if (!this.knownChatIds.has(chatIdStr)) {\n this.knownChatIds.add(chatIdStr);\n this.onNewChatId?.();\n }\n\n const msg = createInboundMessage({\n channel: this.name,\n senderId: String(params.senderId),\n chatId: String(params.chatId),\n content: params.content,\n media: params.media ?? [],\n metadata: params.metadata ?? {},\n });\n\n await this.bus.publishInbound(msg);\n }\n\n get isRunning(): boolean {\n return this._running;\n }\n}\n"],"mappings":";;;;;;AAOA,IAAsB,cAAtB,MAAkC;CAEhC,AAAU;CACV,AAAU;CACV,AAAU,WAAW;;CAGrB,AAAS,+BAAe,IAAI,KAAa;;CAGzC,cAAmC;CAEnC,YAAY,QAAiB,KAAiB;AAC5C,OAAK,SAAS;AACd,OAAK,MAAM;;;CAQb,UAAU,UAA2B;EACnC,MAAM,YAAa,KAAK,QAAqC,aAAa,EAAE;AAC5E,MAAI,UAAU,WAAW,EAAG,QAAO;EAEnC,MAAM,YAAY,OAAO,SAAS;AAClC,MAAI,UAAU,SAAS,UAAU,CAAE,QAAO;AAG1C,MAAI,UAAU,SAAS,IAAI,EACzB;QAAK,MAAM,QAAQ,UAAU,MAAM,IAAI,CACrC,KAAI,QAAQ,UAAU,SAAS,KAAK,CAAE,QAAO;;AAGjD,SAAO;;;CAIT,MAAgB,cAAc,QAMZ;AAChB,MAAI,CAAC,KAAK,UAAU,OAAO,SAAS,CAAE;EAGtC,MAAM,YAAY,OAAO,OAAO,OAAO;AACvC,MAAI,CAAC,KAAK,aAAa,IAAI,UAAU,EAAE;AACrC,QAAK,aAAa,IAAI,UAAU;AAChC,QAAK,eAAe;;EAGtB,MAAM,MAAM,qBAAqB;GAC/B,SAAS,KAAK;GACd,UAAU,OAAO,OAAO,SAAS;GACjC,QAAQ,OAAO,OAAO,OAAO;GAC7B,SAAS,OAAO;GAChB,OAAO,OAAO,SAAS,EAAE;GACzB,UAAU,OAAO,YAAY,EAAE;GAChC,CAAC;AAEF,QAAM,KAAK,IAAI,eAAe,IAAI;;CAGpC,IAAI,YAAqB;AACvB,SAAO,KAAK"}
1
+ {"version":3,"file":"base.mjs","names":[],"sources":["../../src/channels/base.ts"],"sourcesContent":["import type { OutboundMessage, InboundMessage } from \"../bus/events.js\";\nimport { createInboundMessage } from \"../bus/events.js\";\nimport type { MessageBus } from \"../bus/queue.js\";\n\n/**\n * Abstract base class for chat channel implementations.\n */\nexport abstract class BaseChannel {\n abstract readonly name: string;\n protected config: unknown;\n protected bus: MessageBus;\n protected _running = false;\n\n constructor(config: unknown, bus: MessageBus) {\n this.config = config;\n this.bus = bus;\n }\n\n abstract start(): Promise<void>;\n abstract stop(): Promise<void>;\n abstract send(msg: OutboundMessage): Promise<void>;\n\n /** Check if a sender is allowed to use this bot. */\n isAllowed(senderId: string): boolean {\n const allowList = (this.config as { allowFrom?: string[] })?.allowFrom ?? [];\n if (allowList.length === 0) return true;\n\n const senderStr = String(senderId);\n if (allowList.includes(senderStr)) return true;\n\n // Check pipe-separated IDs (e.g., \"123456|username\")\n if (senderStr.includes(\"|\")) {\n for (const part of senderStr.split(\"|\")) {\n if (part && allowList.includes(part)) return true;\n }\n }\n return false;\n }\n\n /** Handle an incoming message from the chat platform. */\n protected async handleMessage(params: {\n senderId: string;\n chatId: string;\n content: string;\n media?: string[];\n metadata?: Record<string, unknown>;\n }): Promise<void> {\n if (!this.isAllowed(params.senderId)) return;\n\n const msg = createInboundMessage({\n channel: this.name,\n senderId: String(params.senderId),\n chatId: String(params.chatId),\n content: params.content,\n media: params.media ?? [],\n metadata: params.metadata ?? {},\n });\n\n await this.bus.publishInbound(msg);\n }\n\n get isRunning(): boolean {\n return this._running;\n }\n}\n"],"mappings":";;;;;;AAOA,IAAsB,cAAtB,MAAkC;CAEhC,AAAU;CACV,AAAU;CACV,AAAU,WAAW;CAErB,YAAY,QAAiB,KAAiB;AAC5C,OAAK,SAAS;AACd,OAAK,MAAM;;;CAQb,UAAU,UAA2B;EACnC,MAAM,YAAa,KAAK,QAAqC,aAAa,EAAE;AAC5E,MAAI,UAAU,WAAW,EAAG,QAAO;EAEnC,MAAM,YAAY,OAAO,SAAS;AAClC,MAAI,UAAU,SAAS,UAAU,CAAE,QAAO;AAG1C,MAAI,UAAU,SAAS,IAAI,EACzB;QAAK,MAAM,QAAQ,UAAU,MAAM,IAAI,CACrC,KAAI,QAAQ,UAAU,SAAS,KAAK,CAAE,QAAO;;AAGjD,SAAO;;;CAIT,MAAgB,cAAc,QAMZ;AAChB,MAAI,CAAC,KAAK,UAAU,OAAO,SAAS,CAAE;EAEtC,MAAM,MAAM,qBAAqB;GAC/B,SAAS,KAAK;GACd,UAAU,OAAO,OAAO,SAAS;GACjC,QAAQ,OAAO,OAAO,OAAO;GAC7B,SAAS,OAAO;GAChB,OAAO,OAAO,SAAS,EAAE;GACzB,UAAU,OAAO,YAAY,EAAE;GAChC,CAAC;AAEF,QAAM,KAAK,IAAI,eAAe,IAAI;;CAGpC,IAAI,YAAqB;AACvB,SAAO,KAAK"}
@@ -11,16 +11,8 @@ declare class ChannelManager {
11
11
  private bus;
12
12
  readonly channels: Map<string, BaseChannel>;
13
13
  private dispatchAbort;
14
- private knownChatsPath;
15
- private chatIdSaveTimer;
16
14
  constructor(config: Config, bus: MessageBus);
17
15
  private initChannels;
18
- /** Load persisted known chat IDs into channels. */
19
- private loadKnownChatIds;
20
- /** Save known chat IDs to disk. */
21
- private saveKnownChatIds;
22
- /** Debounced save — called when new chat IDs are discovered. */
23
- scheduleSave(): void;
24
16
  /** Initialize channel instances (non-blocking). Call before startAll(). */
25
17
  init(): Promise<void>;
26
18
  /** Start all channels and the outbound dispatcher (blocks until stopped). */
@@ -34,8 +26,6 @@ declare class ChannelManager {
34
26
  running: boolean;
35
27
  }>;
36
28
  get enabledChannels(): string[];
37
- /** Get known chat IDs for a channel (chats that have sent at least one message). */
38
- getKnownChatIds(channelName: string): string[];
39
29
  }
40
30
  //#endregion
41
31
  export { ChannelManager };
@@ -1 +1 @@
1
- {"version":3,"file":"manager.d.mts","names":[],"sources":["../../src/channels/manager.ts"],"mappings":";;;;;;;AAWA;cAAa,cAAA;EAAA,QACH,MAAA;EAAA,QACA,GAAA;EAAA,SACC,QAAA,EAAQ,GAAA,SAAA,WAAA;EAAA,QACT,aAAA;EAAA,QACA,cAAA;EAAA,QACA,eAAA;cAEI,MAAA,EAAQ,MAAA,EAAQ,GAAA,EAAK,UAAA;EAAA,QAMnB,YAAA;EAyIY;EAAA,QAtHlB,gBAAA;EA0HW;EAAA,QAzGX,gBAAA;EAjDA;EA8DR,YAAA,CAAA;EA5DS;EAqEH,IAAA,CAAA,GAAQ,OAAA;EArEG;EA2EX,QAAA,CAAA,GAAY,OAAA;EAzEV;EAmGF,OAAA,CAAA,GAAW,OAAA;EAAA,QAyBH,gBAAA;EAsBd,UAAA,CAAW,IAAA,WAAe,WAAA;EAI1B,SAAA,CAAA,GAAa,MAAA;IAAiB,OAAA;IAAkB,OAAA;EAAA;EAAA,IAQ5C,eAAA,CAAA;EAjHI;EAsHR,eAAA,CAAgB,WAAA;AAAA"}
1
+ {"version":3,"file":"manager.d.mts","names":[],"sources":["../../src/channels/manager.ts"],"mappings":";;;;;;;AAOA;cAAa,cAAA;EAAA,QACH,MAAA;EAAA,QACA,GAAA;EAAA,SACC,QAAA,EAAQ,GAAA,SAAA,WAAA;EAAA,QACT,aAAA;cAEI,MAAA,EAAQ,MAAA,EAAQ,GAAA,EAAK,UAAA;EAAA,QAKnB,YAAA;EAuBI;EALZ,IAAA,CAAA,GAAQ,OAAA;EAuEY;EAlEpB,QAAA,CAAA,GAAY,OAAA;EAsEC;EA5Cb,OAAA,CAAA,GAAW,OAAA;EAAA,QAkBH,gBAAA;EAsBd,UAAA,CAAW,IAAA,WAAe,WAAA;EAI1B,SAAA,CAAA,GAAa,MAAA;IAAiB,OAAA;IAAkB,OAAA;EAAA;EAAA,IAQ5C,eAAA,CAAA;AAAA"}
@@ -1,7 +1,3 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { dirname, join } from "node:path";
3
- import { homedir } from "node:os";
4
-
5
1
  //#region src/channels/manager.ts
6
2
  /**
7
3
  * Manages chat channels and coordinates message routing.
@@ -11,56 +7,23 @@ var ChannelManager = class {
11
7
  bus;
12
8
  channels = /* @__PURE__ */ new Map();
13
9
  dispatchAbort = null;
14
- knownChatsPath;
15
- chatIdSaveTimer = null;
16
10
  constructor(config, bus) {
17
11
  this.config = config;
18
12
  this.bus = bus;
19
- this.knownChatsPath = join(homedir(), ".nanobot", "known_chats.json");
20
13
  }
21
14
  async initChannels() {
22
15
  if (this.config.channels.telegram.enabled) try {
23
16
  const { TelegramChannel } = await import("./telegram.mjs");
24
17
  const channel = new TelegramChannel(this.config.channels.telegram, this.bus);
25
- channel.onNewChatId = () => this.scheduleSave();
26
18
  this.channels.set("telegram", channel);
27
19
  console.log("Telegram channel enabled");
28
20
  } catch (err) {
29
21
  console.warn("Telegram channel not available:", err);
30
22
  }
31
23
  }
32
- /** Load persisted known chat IDs into channels. */
33
- loadKnownChatIds() {
34
- if (!existsSync(this.knownChatsPath)) return;
35
- try {
36
- const raw = readFileSync(this.knownChatsPath, "utf-8");
37
- const data = JSON.parse(raw);
38
- for (const [channelName, chatIds] of Object.entries(data)) {
39
- const channel = this.channels.get(channelName);
40
- if (channel) for (const id of chatIds) channel.knownChatIds.add(id);
41
- }
42
- } catch {}
43
- }
44
- /** Save known chat IDs to disk. */
45
- saveKnownChatIds() {
46
- const data = {};
47
- for (const [name, channel] of this.channels) if (channel.knownChatIds.size > 0) data[name] = Array.from(channel.knownChatIds);
48
- const dir = dirname(this.knownChatsPath);
49
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
50
- writeFileSync(this.knownChatsPath, JSON.stringify(data, null, 2));
51
- }
52
- /** Debounced save — called when new chat IDs are discovered. */
53
- scheduleSave() {
54
- if (this.chatIdSaveTimer) return;
55
- this.chatIdSaveTimer = setTimeout(() => {
56
- this.chatIdSaveTimer = null;
57
- this.saveKnownChatIds();
58
- }, 5e3);
59
- }
60
24
  /** Initialize channel instances (non-blocking). Call before startAll(). */
61
25
  async init() {
62
26
  await this.initChannels();
63
- this.loadKnownChatIds();
64
27
  }
65
28
  /** Start all channels and the outbound dispatcher (blocks until stopped). */
66
29
  async startAll() {
@@ -81,11 +44,6 @@ var ChannelManager = class {
81
44
  /** Stop all channels and the dispatcher. */
82
45
  async stopAll() {
83
46
  console.log("Stopping all channels...");
84
- if (this.chatIdSaveTimer) {
85
- clearTimeout(this.chatIdSaveTimer);
86
- this.chatIdSaveTimer = null;
87
- }
88
- this.saveKnownChatIds();
89
47
  if (this.dispatchAbort) {
90
48
  this.dispatchAbort.abort();
91
49
  this.dispatchAbort = null;
@@ -124,12 +82,6 @@ var ChannelManager = class {
124
82
  get enabledChannels() {
125
83
  return Array.from(this.channels.keys());
126
84
  }
127
- /** Get known chat IDs for a channel (chats that have sent at least one message). */
128
- getKnownChatIds(channelName) {
129
- const channel = this.channels.get(channelName);
130
- if (!channel) return [];
131
- return Array.from(channel.knownChatIds);
132
- }
133
85
  };
134
86
 
135
87
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"manager.mjs","names":[],"sources":["../../src/channels/manager.ts"],"sourcesContent":["import { readFileSync, writeFileSync, existsSync, mkdirSync } from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport type { OutboundMessage } from \"../bus/events.js\";\nimport type { MessageBus } from \"../bus/queue.js\";\nimport type { BaseChannel } from \"./base.js\";\nimport type { Config } from \"../config/schema.js\";\n\n/**\n * Manages chat channels and coordinates message routing.\n */\nexport class ChannelManager {\n private config: Config;\n private bus: MessageBus;\n readonly channels = new Map<string, BaseChannel>();\n private dispatchAbort: AbortController | null = null;\n private knownChatsPath: string;\n private chatIdSaveTimer: ReturnType<typeof setTimeout> | null = null;\n\n constructor(config: Config, bus: MessageBus) {\n this.config = config;\n this.bus = bus;\n this.knownChatsPath = join(homedir(), \".nanobot\", \"known_chats.json\");\n }\n\n private async initChannels(): Promise<void> {\n // Telegram channel\n if (this.config.channels.telegram.enabled) {\n try {\n const { TelegramChannel } = await import(\"./telegram.js\");\n const channel = new TelegramChannel(\n this.config.channels.telegram,\n this.bus,\n );\n channel.onNewChatId = () => this.scheduleSave();\n this.channels.set(\"telegram\", channel);\n console.log(\"Telegram channel enabled\");\n } catch (err) {\n console.warn(\"Telegram channel not available:\", err);\n }\n }\n }\n\n /** Load persisted known chat IDs into channels. */\n private loadKnownChatIds(): void {\n if (!existsSync(this.knownChatsPath)) return;\n try {\n const raw = readFileSync(this.knownChatsPath, \"utf-8\");\n const data = JSON.parse(raw) as Record<string, string[]>;\n for (const [channelName, chatIds] of Object.entries(data)) {\n const channel = this.channels.get(channelName);\n if (channel) {\n for (const id of chatIds) channel.knownChatIds.add(id);\n }\n }\n } catch {\n // ignore corrupt file\n }\n }\n\n /** Save known chat IDs to disk. */\n private saveKnownChatIds(): void {\n const data: Record<string, string[]> = {};\n for (const [name, channel] of this.channels) {\n if (channel.knownChatIds.size > 0) {\n data[name] = Array.from(channel.knownChatIds);\n }\n }\n const dir = dirname(this.knownChatsPath);\n if (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n writeFileSync(this.knownChatsPath, JSON.stringify(data, null, 2));\n }\n\n /** Debounced save — called when new chat IDs are discovered. */\n scheduleSave(): void {\n if (this.chatIdSaveTimer) return;\n this.chatIdSaveTimer = setTimeout(() => {\n this.chatIdSaveTimer = null;\n this.saveKnownChatIds();\n }, 5000);\n }\n\n /** Initialize channel instances (non-blocking). Call before startAll(). */\n async init(): Promise<void> {\n await this.initChannels();\n this.loadKnownChatIds();\n }\n\n /** Start all channels and the outbound dispatcher (blocks until stopped). */\n async startAll(): Promise<void> {\n // Init if not already done\n if (this.channels.size === 0) {\n await this.initChannels();\n }\n\n if (this.channels.size === 0) {\n console.warn(\"No channels enabled\");\n return;\n }\n\n // Start outbound dispatcher\n this.dispatchAbort = new AbortController();\n const dispatchPromise = this.dispatchOutbound(this.dispatchAbort.signal);\n\n // Start all channels\n const channelPromises: Promise<void>[] = [];\n for (const [name, channel] of this.channels) {\n console.log(`Starting ${name} channel...`);\n channelPromises.push(channel.start());\n }\n\n await Promise.all([dispatchPromise, ...channelPromises]);\n }\n\n /** Stop all channels and the dispatcher. */\n async stopAll(): Promise<void> {\n console.log(\"Stopping all channels...\");\n\n if (this.chatIdSaveTimer) {\n clearTimeout(this.chatIdSaveTimer);\n this.chatIdSaveTimer = null;\n }\n // Final save of known chat IDs\n this.saveKnownChatIds();\n\n if (this.dispatchAbort) {\n this.dispatchAbort.abort();\n this.dispatchAbort = null;\n }\n\n for (const [name, channel] of this.channels) {\n try {\n await channel.stop();\n console.log(`Stopped ${name} channel`);\n } catch (err) {\n console.error(`Error stopping ${name}:`, err);\n }\n }\n }\n\n private async dispatchOutbound(signal: AbortSignal): Promise<void> {\n console.log(\"Outbound dispatcher started\");\n\n while (!signal.aborted) {\n try {\n const msg = await this.bus.consumeOutboundTimeout(1000);\n const channel = this.channels.get(msg.channel);\n if (channel) {\n try {\n await channel.send(msg);\n } catch (err) {\n console.error(`Error sending to ${msg.channel}:`, err);\n }\n } else {\n console.warn(`Unknown channel: ${msg.channel}`);\n }\n } catch {\n // timeout, continue\n }\n }\n }\n\n getChannel(name: string): BaseChannel | undefined {\n return this.channels.get(name);\n }\n\n getStatus(): Record<string, { enabled: boolean; running: boolean }> {\n const status: Record<string, { enabled: boolean; running: boolean }> = {};\n for (const [name, channel] of this.channels) {\n status[name] = { enabled: true, running: channel.isRunning };\n }\n return status;\n }\n\n get enabledChannels(): string[] {\n return Array.from(this.channels.keys());\n }\n\n /** Get known chat IDs for a channel (chats that have sent at least one message). */\n getKnownChatIds(channelName: string): string[] {\n const channel = this.channels.get(channelName);\n if (!channel) return [];\n return Array.from(channel.knownChatIds);\n }\n}\n\n"],"mappings":"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"}
1
+ {"version":3,"file":"manager.mjs","names":[],"sources":["../../src/channels/manager.ts"],"sourcesContent":["import type { MessageBus } from \"../bus/queue.js\";\nimport type { BaseChannel } from \"./base.js\";\nimport type { Config } from \"../config/schema.js\";\n\n/**\n * Manages chat channels and coordinates message routing.\n */\nexport class ChannelManager {\n private config: Config;\n private bus: MessageBus;\n readonly channels = new Map<string, BaseChannel>();\n private dispatchAbort: AbortController | null = null;\n\n constructor(config: Config, bus: MessageBus) {\n this.config = config;\n this.bus = bus;\n }\n\n private async initChannels(): Promise<void> {\n // Telegram channel\n if (this.config.channels.telegram.enabled) {\n try {\n const { TelegramChannel } = await import(\"./telegram.js\");\n const channel = new TelegramChannel(\n this.config.channels.telegram,\n this.bus,\n );\n this.channels.set(\"telegram\", channel);\n console.log(\"Telegram channel enabled\");\n } catch (err) {\n console.warn(\"Telegram channel not available:\", err);\n }\n }\n }\n\n /** Initialize channel instances (non-blocking). Call before startAll(). */\n async init(): Promise<void> {\n await this.initChannels();\n }\n\n /** Start all channels and the outbound dispatcher (blocks until stopped). */\n async startAll(): Promise<void> {\n // Init if not already done\n if (this.channels.size === 0) {\n await this.initChannels();\n }\n\n if (this.channels.size === 0) {\n console.warn(\"No channels enabled\");\n return;\n }\n\n // Start outbound dispatcher\n this.dispatchAbort = new AbortController();\n const dispatchPromise = this.dispatchOutbound(this.dispatchAbort.signal);\n\n // Start all channels\n const channelPromises: Promise<void>[] = [];\n for (const [name, channel] of this.channels) {\n console.log(`Starting ${name} channel...`);\n channelPromises.push(channel.start());\n }\n\n await Promise.all([dispatchPromise, ...channelPromises]);\n }\n\n /** Stop all channels and the dispatcher. */\n async stopAll(): Promise<void> {\n console.log(\"Stopping all channels...\");\n\n if (this.dispatchAbort) {\n this.dispatchAbort.abort();\n this.dispatchAbort = null;\n }\n\n for (const [name, channel] of this.channels) {\n try {\n await channel.stop();\n console.log(`Stopped ${name} channel`);\n } catch (err) {\n console.error(`Error stopping ${name}:`, err);\n }\n }\n }\n\n private async dispatchOutbound(signal: AbortSignal): Promise<void> {\n console.log(\"Outbound dispatcher started\");\n\n while (!signal.aborted) {\n try {\n const msg = await this.bus.consumeOutboundTimeout(1000);\n const channel = this.channels.get(msg.channel);\n if (channel) {\n try {\n await channel.send(msg);\n } catch (err) {\n console.error(`Error sending to ${msg.channel}:`, err);\n }\n } else {\n console.warn(`Unknown channel: ${msg.channel}`);\n }\n } catch {\n // timeout, continue\n }\n }\n }\n\n getChannel(name: string): BaseChannel | undefined {\n return this.channels.get(name);\n }\n\n getStatus(): Record<string, { enabled: boolean; running: boolean }> {\n const status: Record<string, { enabled: boolean; running: boolean }> = {};\n for (const [name, channel] of this.channels) {\n status[name] = { enabled: true, running: channel.isRunning };\n }\n return status;\n }\n\n get enabledChannels(): string[] {\n return Array.from(this.channels.keys());\n }\n}\n"],"mappings":";;;;AAOA,IAAa,iBAAb,MAA4B;CAC1B,AAAQ;CACR,AAAQ;CACR,AAAS,2BAAW,IAAI,KAA0B;CAClD,AAAQ,gBAAwC;CAEhD,YAAY,QAAgB,KAAiB;AAC3C,OAAK,SAAS;AACd,OAAK,MAAM;;CAGb,MAAc,eAA8B;AAE1C,MAAI,KAAK,OAAO,SAAS,SAAS,QAChC,KAAI;GACF,MAAM,EAAE,oBAAoB,MAAM,OAAO;GACzC,MAAM,UAAU,IAAI,gBAClB,KAAK,OAAO,SAAS,UACrB,KAAK,IACN;AACD,QAAK,SAAS,IAAI,YAAY,QAAQ;AACtC,WAAQ,IAAI,2BAA2B;WAChC,KAAK;AACZ,WAAQ,KAAK,mCAAmC,IAAI;;;;CAM1D,MAAM,OAAsB;AAC1B,QAAM,KAAK,cAAc;;;CAI3B,MAAM,WAA0B;AAE9B,MAAI,KAAK,SAAS,SAAS,EACzB,OAAM,KAAK,cAAc;AAG3B,MAAI,KAAK,SAAS,SAAS,GAAG;AAC5B,WAAQ,KAAK,sBAAsB;AACnC;;AAIF,OAAK,gBAAgB,IAAI,iBAAiB;EAC1C,MAAM,kBAAkB,KAAK,iBAAiB,KAAK,cAAc,OAAO;EAGxE,MAAM,kBAAmC,EAAE;AAC3C,OAAK,MAAM,CAAC,MAAM,YAAY,KAAK,UAAU;AAC3C,WAAQ,IAAI,YAAY,KAAK,aAAa;AAC1C,mBAAgB,KAAK,QAAQ,OAAO,CAAC;;AAGvC,QAAM,QAAQ,IAAI,CAAC,iBAAiB,GAAG,gBAAgB,CAAC;;;CAI1D,MAAM,UAAyB;AAC7B,UAAQ,IAAI,2BAA2B;AAEvC,MAAI,KAAK,eAAe;AACtB,QAAK,cAAc,OAAO;AAC1B,QAAK,gBAAgB;;AAGvB,OAAK,MAAM,CAAC,MAAM,YAAY,KAAK,SACjC,KAAI;AACF,SAAM,QAAQ,MAAM;AACpB,WAAQ,IAAI,WAAW,KAAK,UAAU;WAC/B,KAAK;AACZ,WAAQ,MAAM,kBAAkB,KAAK,IAAI,IAAI;;;CAKnD,MAAc,iBAAiB,QAAoC;AACjE,UAAQ,IAAI,8BAA8B;AAE1C,SAAO,CAAC,OAAO,QACb,KAAI;GACF,MAAM,MAAM,MAAM,KAAK,IAAI,uBAAuB,IAAK;GACvD,MAAM,UAAU,KAAK,SAAS,IAAI,IAAI,QAAQ;AAC9C,OAAI,QACF,KAAI;AACF,UAAM,QAAQ,KAAK,IAAI;YAChB,KAAK;AACZ,YAAQ,MAAM,oBAAoB,IAAI,QAAQ,IAAI,IAAI;;OAGxD,SAAQ,KAAK,oBAAoB,IAAI,UAAU;UAE3C;;CAMZ,WAAW,MAAuC;AAChD,SAAO,KAAK,SAAS,IAAI,KAAK;;CAGhC,YAAoE;EAClE,MAAM,SAAiE,EAAE;AACzE,OAAK,MAAM,CAAC,MAAM,YAAY,KAAK,SACjC,QAAO,QAAQ;GAAE,SAAS;GAAM,SAAS,QAAQ;GAAW;AAE9D,SAAO;;CAGT,IAAI,kBAA4B;AAC9B,SAAO,MAAM,KAAK,KAAK,SAAS,MAAM,CAAC"}
@@ -2,8 +2,8 @@
2
2
  import { ConfigSchema, getApiBase, getApiKey, getConfigWorkspacePath } from "../config/schema.mjs";
3
3
  import { getConfigPath, getDataDir, loadConfig, saveConfig } from "../config/loader.mjs";
4
4
  import { LOGO, VERSION } from "../index.mjs";
5
- import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
6
- import { dirname, isAbsolute, join, resolve } from "node:path";
5
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
6
+ import { isAbsolute, join, resolve } from "node:path";
7
7
  import { pathToFileURL } from "node:url";
8
8
  import { Command } from "commander";
9
9
 
@@ -131,112 +131,6 @@ This file stores important information that should persist across sessions.
131
131
  console.log(" Created memory/MEMORY.md");
132
132
  }
133
133
  }
134
- const configCmd = program.command("config").description("Manage nanobot configuration");
135
- configCmd.command("export").description("Export config, cron jobs, known chats, and workspace files to a portable JSON bundle").option("-o, --output <path>", "Write to file instead of stdout").action((opts) => {
136
- const dataDir = getDataDir();
137
- const config = loadConfig();
138
- const workspace = getConfigWorkspacePath(config);
139
- const readJsonSafe = (p) => {
140
- try {
141
- return JSON.parse(readFileSync(p, "utf-8"));
142
- } catch {
143
- return null;
144
- }
145
- };
146
- const readTextSafe = (p) => {
147
- try {
148
- return readFileSync(p, "utf-8");
149
- } catch {
150
- return null;
151
- }
152
- };
153
- const wsFiles = {};
154
- for (const rel of [
155
- "AGENTS.md",
156
- "SOUL.md",
157
- "USER.md",
158
- "TOOLS.md",
159
- "IDENTITY.md",
160
- "memory/MEMORY.md"
161
- ]) {
162
- const content = readTextSafe(join(workspace, rel));
163
- if (content !== null) wsFiles[rel] = content;
164
- }
165
- const memoryDir = join(workspace, "memory");
166
- if (existsSync(memoryDir)) try {
167
- for (const f of readdirSync(memoryDir)) if (f.endsWith(".md") && f !== "MEMORY.md") {
168
- const content = readTextSafe(join(memoryDir, f));
169
- if (content !== null) wsFiles[`memory/${f}`] = content;
170
- }
171
- } catch {}
172
- const bundle = {
173
- _nanobot: "config-bundle",
174
- version: VERSION,
175
- exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
176
- config,
177
- cronJobs: readJsonSafe(join(dataDir, "cron", "jobs.json")),
178
- knownChats: readJsonSafe(join(dataDir, "known_chats.json")),
179
- workspace: wsFiles
180
- };
181
- const json = JSON.stringify(bundle, null, 2);
182
- if (opts.output) {
183
- const outDir = dirname(opts.output);
184
- if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
185
- writeFileSync(opts.output, json);
186
- console.log(`Exported to ${opts.output}`);
187
- } else process.stdout.write(json + "\n");
188
- });
189
- configCmd.command("import").description("Import a config bundle (from file or stdin)").argument("[path]", "Path to bundle JSON file (omit to read from stdin)").option("--no-workspace", "Skip importing workspace files").option("--no-cron", "Skip importing cron jobs").action(async (path, opts) => {
190
- let raw;
191
- if (path) {
192
- if (!existsSync(path)) {
193
- console.error(`File not found: ${path}`);
194
- process.exit(1);
195
- }
196
- raw = readFileSync(path, "utf-8");
197
- } else {
198
- const chunks = [];
199
- for await (const chunk of process.stdin) chunks.push(chunk);
200
- raw = Buffer.concat(chunks).toString("utf-8");
201
- }
202
- let bundle;
203
- try {
204
- bundle = JSON.parse(raw);
205
- } catch {
206
- console.error("Error: Invalid JSON");
207
- process.exit(1);
208
- }
209
- if (bundle._nanobot !== "config-bundle") {
210
- console.error("Error: Not a nanobot config bundle");
211
- process.exit(1);
212
- }
213
- const dataDir = getDataDir();
214
- if (bundle.config) {
215
- saveConfig(ConfigSchema.parse(bundle.config));
216
- console.log(" Restored config.json");
217
- }
218
- if (opts.cron && bundle.cronJobs) {
219
- const cronDir = join(dataDir, "cron");
220
- if (!existsSync(cronDir)) mkdirSync(cronDir, { recursive: true });
221
- writeFileSync(join(cronDir, "jobs.json"), JSON.stringify(bundle.cronJobs, null, 2));
222
- console.log(" Restored cron/jobs.json");
223
- }
224
- if (bundle.knownChats) {
225
- writeFileSync(join(dataDir, "known_chats.json"), JSON.stringify(bundle.knownChats, null, 2));
226
- console.log(" Restored known_chats.json");
227
- }
228
- if (opts.workspace && bundle.workspace) {
229
- const workspace = getConfigWorkspacePath(loadConfig());
230
- for (const [rel, content] of Object.entries(bundle.workspace)) {
231
- const filePath = join(workspace, rel);
232
- const fileDir = dirname(filePath);
233
- if (!existsSync(fileDir)) mkdirSync(fileDir, { recursive: true });
234
- writeFileSync(filePath, content);
235
- console.log(` Restored ${rel}`);
236
- }
237
- }
238
- console.log(`\n${LOGO} Import complete (from ${bundle.exportedAt})`);
239
- });
240
134
  program.command("gateway").description("Start the nanobot gateway").option("-p, --port <number>", "Gateway port", "18790").option("--verbose", "Verbose output", false).action(async (opts) => {
241
135
  console.log(`${LOGO} Starting nanobot gateway on port ${opts.port}...`);
242
136
  const config = loadConfig();
@@ -278,36 +172,16 @@ program.command("gateway").description("Start the nanobot gateway").option("-p,
278
172
  customTools
279
173
  });
280
174
  const channels = new ChannelManager(config, bus);
281
- let gateway = null;
282
175
  cron.onJob = async (job) => {
283
176
  const sessionKey = `cron:${job.id}`;
284
- const session = agent.sessions.getOrCreate(sessionKey);
285
- const prevLen = session.getHistory().length;
286
- const response = await agent.processDirect(job.payload.message, sessionKey, "cron", job.id);
287
- if (gateway) {
288
- const turnMessages = session.getHistory().slice(prevLen);
289
- for (const msg of turnMessages) if (msg.role === "assistant" && msg.tool_calls && msg.tool_calls.length > 0) {
290
- for (const tc of msg.tool_calls) gateway.notifyToolCall(tc.function.name, tc.function.arguments);
291
- if (msg.content) gateway.notify("assistant", typeof msg.content === "string" ? msg.content : "", "default");
292
- } else if (msg.role === "tool") {
293
- const content = typeof msg.content === "string" ? msg.content : "";
294
- gateway.notifyToolResult(msg.name ?? "", content);
295
- } else if (msg.role === "assistant") gateway.notify("assistant", typeof msg.content === "string" ? msg.content : "", "default");
296
- }
297
- if (job.payload.deliver) {
177
+ const response = await agent.processDirect(job.payload.message, sessionKey, job.payload.channel ?? "cron", job.payload.to ?? job.id);
178
+ if (job.payload.deliver && job.payload.to) {
298
179
  const { createOutboundMessage } = await import("../bus/events.mjs");
299
- for (const channelName of channels.enabledChannels) {
300
- const chatIds = channels.getKnownChatIds(channelName);
301
- if (chatIds.length === 0) {
302
- console.log(`Cron: no known chats for ${channelName}, skipping`);
303
- continue;
304
- }
305
- for (const chatId of chatIds) await bus.publishOutbound(createOutboundMessage({
306
- channel: channelName,
307
- chatId,
308
- content: response ?? ""
309
- }));
310
- }
180
+ await bus.publishOutbound(createOutboundMessage({
181
+ channel: job.payload.channel ?? "cli",
182
+ chatId: job.payload.to,
183
+ content: response ?? ""
184
+ }));
311
185
  }
312
186
  return response;
313
187
  };
@@ -331,7 +205,7 @@ program.command("gateway").description("Start the nanobot gateway").option("-p,
331
205
  process.on("SIGINT", shutdown);
332
206
  process.on("SIGTERM", shutdown);
333
207
  const { createGatewayServer } = await import("../gateway/server.mjs");
334
- gateway = createGatewayServer({
208
+ createGatewayServer({
335
209
  agent,
336
210
  port: Number(opts.port)
337
211
  });
@@ -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, readFileSync, readdirSync, writeFileSync, mkdirSync } from \"node:fs\";\nimport { join, dirname, 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, getDataDir } from \"../config/loader.js\";\nimport {\n ConfigSchema,\n getConfigWorkspacePath,\n getApiKey,\n getApiBase,\n} from \"../config/schema.js\";\nimport type { Config } from \"../config/schema.js\";\nimport type { Tool } from \"../agent/tools/base.js\";\nimport { GatewayServer } from \"../gateway/server.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// Config Export / Import\n// ============================================================================\n\n/** Shape of the portable config bundle. */\ninterface ConfigBundle {\n _nanobot: string;\n version: string;\n exportedAt: string;\n config: unknown;\n cronJobs: unknown | null;\n knownChats: unknown | null;\n workspace: Record<string, string>;\n}\n\nconst configCmd = program\n .command(\"config\")\n .description(\"Manage nanobot configuration\");\n\nconfigCmd\n .command(\"export\")\n .description(\"Export config, cron jobs, known chats, and workspace files to a portable JSON bundle\")\n .option(\"-o, --output <path>\", \"Write to file instead of stdout\")\n .action((opts) => {\n const dataDir = getDataDir();\n const config = loadConfig();\n const workspace = getConfigWorkspacePath(config);\n\n // Read optional data files\n const readJsonSafe = (p: string): unknown | null => {\n try { return JSON.parse(readFileSync(p, \"utf-8\")); } catch { return null; }\n };\n const readTextSafe = (p: string): string | null => {\n try { return readFileSync(p, \"utf-8\"); } catch { return null; }\n };\n\n // Gather workspace files\n const wsFiles: Record<string, string> = {};\n const workspaceEntries = [\n \"AGENTS.md\", \"SOUL.md\", \"USER.md\", \"TOOLS.md\", \"IDENTITY.md\",\n \"memory/MEMORY.md\",\n ];\n for (const rel of workspaceEntries) {\n const content = readTextSafe(join(workspace, rel));\n if (content !== null) wsFiles[rel] = content;\n }\n\n // Also grab daily memory notes\n const memoryDir = join(workspace, \"memory\");\n if (existsSync(memoryDir)) {\n try {\n for (const f of readdirSync(memoryDir)) {\n if (f.endsWith(\".md\") && f !== \"MEMORY.md\") {\n const content = readTextSafe(join(memoryDir, f));\n if (content !== null) wsFiles[`memory/${f}`] = content;\n }\n }\n } catch { /* ignore */ }\n }\n\n const bundle: ConfigBundle = {\n _nanobot: \"config-bundle\",\n version: VERSION,\n exportedAt: new Date().toISOString(),\n config,\n cronJobs: readJsonSafe(join(dataDir, \"cron\", \"jobs.json\")),\n knownChats: readJsonSafe(join(dataDir, \"known_chats.json\")),\n workspace: wsFiles,\n };\n\n const json = JSON.stringify(bundle, null, 2);\n\n if (opts.output) {\n const outDir = dirname(opts.output);\n if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });\n writeFileSync(opts.output, json);\n console.log(`Exported to ${opts.output}`);\n } else {\n process.stdout.write(json + \"\\n\");\n }\n });\n\nconfigCmd\n .command(\"import\")\n .description(\"Import a config bundle (from file or stdin)\")\n .argument(\"[path]\", \"Path to bundle JSON file (omit to read from stdin)\")\n .option(\"--no-workspace\", \"Skip importing workspace files\")\n .option(\"--no-cron\", \"Skip importing cron jobs\")\n .action(async (path: string | undefined, opts: { workspace: boolean; cron: boolean }) => {\n let raw: string;\n\n if (path) {\n if (!existsSync(path)) {\n console.error(`File not found: ${path}`);\n process.exit(1);\n }\n raw = readFileSync(path, \"utf-8\");\n } else {\n // Read from stdin\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(chunk as Buffer);\n }\n raw = Buffer.concat(chunks).toString(\"utf-8\");\n }\n\n let bundle: ConfigBundle;\n try {\n bundle = JSON.parse(raw);\n } catch {\n console.error(\"Error: Invalid JSON\");\n process.exit(1);\n }\n\n if (bundle._nanobot !== \"config-bundle\") {\n console.error(\"Error: Not a nanobot config bundle\");\n process.exit(1);\n }\n\n const dataDir = getDataDir();\n\n // 1. Restore config\n if (bundle.config) {\n const config = ConfigSchema.parse(bundle.config);\n saveConfig(config);\n console.log(\" Restored config.json\");\n }\n\n // 2. Restore cron jobs\n if (opts.cron && bundle.cronJobs) {\n const cronDir = join(dataDir, \"cron\");\n if (!existsSync(cronDir)) mkdirSync(cronDir, { recursive: true });\n writeFileSync(join(cronDir, \"jobs.json\"), JSON.stringify(bundle.cronJobs, null, 2));\n console.log(\" Restored cron/jobs.json\");\n }\n\n // 3. Restore known chats\n if (bundle.knownChats) {\n writeFileSync(join(dataDir, \"known_chats.json\"), JSON.stringify(bundle.knownChats, null, 2));\n console.log(\" Restored known_chats.json\");\n }\n\n // 4. Restore workspace files\n if (opts.workspace && bundle.workspace) {\n const config = loadConfig();\n const workspace = getConfigWorkspacePath(config);\n\n for (const [rel, content] of Object.entries(bundle.workspace)) {\n const filePath = join(workspace, rel);\n const fileDir = dirname(filePath);\n if (!existsSync(fileDir)) mkdirSync(fileDir, { recursive: true });\n writeFileSync(filePath, content);\n console.log(` Restored ${rel}`);\n }\n }\n\n console.log(`\\n${LOGO} Import complete (from ${bundle.exportedAt})`);\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 apiKey = getApiKey(config);\n const apiBase = getApiBase(config);\n const model = config.agents.defaults.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.openrouter.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 const { CronService } = await import(\"../cron/service.js\");\n const { HeartbeatService } = await import(\n \"../heartbeat/service.js\"\n );\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 });\n\n const customTools = await loadCustomTools(config);\n\n // Create cron service\n const cronStorePath = join(getDataDir(), \"cron\", \"jobs.json\");\n const cron = new CronService(cronStorePath);\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 cronService: cron,\n toolsEnabled: config.tools.enabled,\n toolsDisabled: config.tools.disabled,\n customTools,\n });\n\n // Create channel manager (before cron so the callback can broadcast)\n const channels = new ChannelManager(config, bus);\n\n // Wire cron callback (gateway is assigned after server creation below)\n let gateway: GatewayServer | null = null;\n\n cron.onJob = async (job) => {\n const sessionKey = `cron:${job.id}`;\n\n // Snapshot history length so we can extract only this turn's messages\n const session = agent.sessions.getOrCreate(sessionKey);\n const prevLen = session.getHistory().length;\n\n const response = await agent.processDirect(\n job.payload.message,\n sessionKey,\n \"cron\",\n job.id,\n );\n\n // Push the full turn (tool calls + results + final answer) to the web UI via SSE\n if (gateway) {\n const turnMessages = session.getHistory().slice(prevLen);\n for (const msg of turnMessages) {\n if (msg.role === \"assistant\" && msg.tool_calls && msg.tool_calls.length > 0) {\n for (const tc of msg.tool_calls) {\n gateway.notifyToolCall(tc.function.name, tc.function.arguments);\n }\n // If the assistant message also has text content, push it\n if (msg.content) {\n gateway.notify(\"assistant\", typeof msg.content === \"string\" ? msg.content : \"\", \"default\");\n }\n } else if (msg.role === \"tool\") {\n const content = typeof msg.content === \"string\" ? msg.content : \"\";\n gateway.notifyToolResult(msg.name ?? \"\", content);\n } else if (msg.role === \"assistant\") {\n gateway.notify(\"assistant\", typeof msg.content === \"string\" ? msg.content : \"\", \"default\");\n }\n }\n }\n\n // Deliver to all configured chat channels (telegram, etc.)\n if (job.payload.deliver) {\n const { createOutboundMessage } = await import(\n \"../bus/events.js\"\n );\n\n for (const channelName of channels.enabledChannels) {\n const chatIds = channels.getKnownChatIds(channelName);\n if (chatIds.length === 0) {\n console.log(`Cron: no known chats for ${channelName}, skipping`);\n continue;\n }\n for (const chatId of chatIds) {\n await bus.publishOutbound(\n createOutboundMessage({\n channel: channelName,\n chatId,\n content: response ?? \"\",\n }),\n );\n }\n }\n }\n\n return response;\n };\n\n // Create heartbeat\n const heartbeat = new HeartbeatService({\n workspace,\n onHeartbeat: (prompt) =>\n agent.processDirect(prompt, \"heartbeat\"),\n intervalS: 30 * 60,\n enabled: true,\n });\n\n const cronStatus = cron.status();\n if (cronStatus.jobs > 0) {\n console.log(`Cron: ${cronStatus.jobs} scheduled jobs`);\n }\n console.log(\"Heartbeat: every 30m\");\n\n // Handle graceful shutdown\n const shutdown = async () => {\n console.log(\"\\nShutting down...\");\n heartbeat.stop();\n cron.stop();\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 gateway = createGatewayServer({\n agent,\n port: Number(opts.port),\n });\n\n try {\n // Initialise channels first (non-blocking) so enabledChannels is populated\n await channels.init();\n await cron.start();\n await heartbeat.start();\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 apiKey = getApiKey(config);\n const apiBase = getApiBase(config);\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: config.agents.defaults.model,\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 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\n// ============================================================================\n// Cron Commands\n// ============================================================================\n\nconst cronCmd = program\n .command(\"cron\")\n .description(\"Manage scheduled tasks\");\n\ncronCmd\n .command(\"list\")\n .description(\"List scheduled jobs\")\n .option(\"-a, --all\", \"Include disabled jobs\", false)\n .action(async (opts) => {\n const { CronService } = await import(\"../cron/service.js\");\n\n const storePath = join(getDataDir(), \"cron\", \"jobs.json\");\n const service = new CronService(storePath);\n\n const jobs = service.listJobs(opts.all);\n\n if (jobs.length === 0) {\n console.log(\"No scheduled jobs.\");\n return;\n }\n\n console.log(\"Scheduled Jobs\");\n console.log(\"─\".repeat(70));\n console.log(\n `${\"ID\".padEnd(10)} ${\"Name\".padEnd(20)} ${\"Schedule\".padEnd(18)} ${\"Status\".padEnd(10)} Next Run`,\n );\n console.log(\"─\".repeat(70));\n\n for (const job of jobs) {\n let sched: string;\n if (job.schedule.kind === \"every\") {\n sched = `every ${(job.schedule.everyMs ?? 0) / 1000}s`;\n } else if (job.schedule.kind === \"cron\") {\n sched = job.schedule.expr ?? \"\";\n } else {\n sched = \"one-time\";\n }\n\n let nextRun = \"\";\n if (job.state.nextRunAtMs) {\n nextRun = new Date(job.state.nextRunAtMs).toLocaleString();\n }\n\n const status = job.enabled ? \"enabled\" : \"disabled\";\n\n console.log(\n `${job.id.padEnd(10)} ${job.name.padEnd(20)} ${sched.padEnd(18)} ${status.padEnd(10)} ${nextRun}`,\n );\n }\n });\n\ncronCmd\n .command(\"add\")\n .description(\"Add a scheduled job\")\n .requiredOption(\"-n, --name <name>\", \"Job name\")\n .requiredOption(\"-m, --message <text>\", \"Message for agent\")\n .option(\"-e, --every <seconds>\", \"Run every N seconds\")\n .option(\"-c, --cron <expr>\", \"Cron expression (e.g. '0 9 * * *')\")\n .option(\"--at <iso>\", \"Run once at time (ISO format)\")\n .option(\"-d, --deliver\", \"Deliver response to channel\", false)\n .option(\"--to <recipient>\", \"Recipient for delivery\")\n .option(\"--channel <name>\", \"Channel for delivery\")\n .action(async (opts) => {\n const { CronService } = await import(\"../cron/service.js\");\n\n let schedule: { kind: string; everyMs?: number; expr?: string; atMs?: number };\n if (opts.every) {\n schedule = { kind: \"every\", everyMs: Number(opts.every) * 1000 };\n } else if (opts.cron) {\n schedule = { kind: \"cron\", expr: opts.cron };\n } else if (opts.at) {\n const dt = new Date(opts.at);\n schedule = { kind: \"at\", atMs: dt.getTime() };\n } else {\n console.error(\"Error: Must specify --every, --cron, or --at\");\n process.exit(1);\n }\n\n const storePath = join(getDataDir(), \"cron\", \"jobs.json\");\n const service = new CronService(storePath);\n\n const job = service.addJob({\n name: opts.name,\n schedule: schedule as import(\"../cron/types.js\").CronSchedule,\n message: opts.message,\n deliver: opts.deliver,\n to: opts.to,\n channel: opts.channel,\n });\n\n console.log(`Added job '${job.name}' (${job.id})`);\n });\n\ncronCmd\n .command(\"remove\")\n .description(\"Remove a scheduled job\")\n .argument(\"<jobId>\", \"Job ID to remove\")\n .action(async (jobId: string) => {\n const { CronService } = await import(\"../cron/service.js\");\n\n const storePath = join(getDataDir(), \"cron\", \"jobs.json\");\n const service = new CronService(storePath);\n\n if (service.removeJob(jobId)) {\n console.log(`Removed job ${jobId}`);\n } else {\n console.error(`Job ${jobId} not found`);\n }\n });\n\ncronCmd\n .command(\"enable\")\n .description(\"Enable or disable a job\")\n .argument(\"<jobId>\", \"Job ID\")\n .option(\"--disable\", \"Disable instead of enable\", false)\n .action(async (jobId: string, opts: { disable: boolean }) => {\n const { CronService } = await import(\"../cron/service.js\");\n\n const storePath = join(getDataDir(), \"cron\", \"jobs.json\");\n const service = new CronService(storePath);\n\n const job = service.enableJob(jobId, !opts.disable);\n if (job) {\n const status = opts.disable ? \"disabled\" : \"enabled\";\n console.log(`Job '${job.name}' ${status}`);\n } else {\n console.error(`Job ${jobId} not found`);\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\n const hasOpenrouter = Boolean(config.providers.openrouter.apiKey);\n const hasAnthropic = Boolean(config.providers.anthropic.apiKey);\n const hasOpenai = Boolean(config.providers.openai.apiKey);\n const hasGemini = Boolean(config.providers.gemini.apiKey);\n const hasDeepseek = Boolean(config.providers.deepseek.apiKey);\n const hasOpenaiCompatible = Boolean(config.providers.openaiCompatible.apiKey);\n console.log(\n `OpenRouter API: ${hasOpenrouter ? \"[set]\" : \"[not set]\"}`,\n );\n console.log(\n `Anthropic API: ${hasAnthropic ? \"[set]\" : \"[not set]\"}`,\n );\n console.log(\n `OpenAI API: ${hasOpenai ? \"[set]\" : \"[not set]\"}`,\n );\n console.log(\n `Gemini API: ${hasGemini ? \"[set]\" : \"[not set]\"}`,\n );\n console.log(\n `DeepSeek API: ${hasDeepseek ? \"[set]\" : \"[not set]\"}`,\n );\n console.log(\n `OpenAI Compatible API: ${hasOpenaiCompatible ? \"[set]\" : \"[not set]\"}`,\n );\n }\n });\n\nprogram.parse();\n"],"mappings":"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"}
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, getDataDir } from \"../config/loader.js\";\nimport {\n ConfigSchema,\n getConfigWorkspacePath,\n getApiKey,\n getApiBase,\n} from \"../config/schema.js\";\nimport type { Config } from \"../config/schema.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 apiKey = getApiKey(config);\n const apiBase = getApiBase(config);\n const model = config.agents.defaults.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.openrouter.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 const { CronService } = await import(\"../cron/service.js\");\n const { HeartbeatService } = await import(\n \"../heartbeat/service.js\"\n );\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 });\n\n const customTools = await loadCustomTools(config);\n\n // Create cron service\n const cronStorePath = join(getDataDir(), \"cron\", \"jobs.json\");\n const cron = new CronService(cronStorePath);\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 cronService: cron,\n toolsEnabled: config.tools.enabled,\n toolsDisabled: config.tools.disabled,\n customTools,\n });\n\n // Create channel manager (before cron so the callback can broadcast)\n const channels = new ChannelManager(config, bus);\n\n // Wire cron callback\n cron.onJob = async (job) => {\n const sessionKey = `cron:${job.id}`;\n\n const response = await agent.processDirect(\n job.payload.message,\n sessionKey,\n job.payload.channel ?? \"cron\",\n job.payload.to ?? job.id,\n );\n\n // Deliver to the channel/chat that initiated this cron job\n if (job.payload.deliver && job.payload.to) {\n const { createOutboundMessage } = await import(\n \"../bus/events.js\"\n );\n await bus.publishOutbound(\n createOutboundMessage({\n channel: job.payload.channel ?? \"cli\",\n chatId: job.payload.to,\n content: response ?? \"\",\n }),\n );\n }\n\n return response;\n };\n\n // Create heartbeat\n const heartbeat = new HeartbeatService({\n workspace,\n onHeartbeat: (prompt) =>\n agent.processDirect(prompt, \"heartbeat\"),\n intervalS: 30 * 60,\n enabled: true,\n });\n\n const cronStatus = cron.status();\n if (cronStatus.jobs > 0) {\n console.log(`Cron: ${cronStatus.jobs} scheduled jobs`);\n }\n console.log(\"Heartbeat: every 30m\");\n\n // Handle graceful shutdown\n const shutdown = async () => {\n console.log(\"\\nShutting down...\");\n heartbeat.stop();\n cron.stop();\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 port: Number(opts.port),\n });\n\n try {\n // Initialise channels first (non-blocking) so enabledChannels is populated\n await channels.init();\n await cron.start();\n await heartbeat.start();\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 apiKey = getApiKey(config);\n const apiBase = getApiBase(config);\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: config.agents.defaults.model,\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 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\n// ============================================================================\n// Cron Commands\n// ============================================================================\n\nconst cronCmd = program\n .command(\"cron\")\n .description(\"Manage scheduled tasks\");\n\ncronCmd\n .command(\"list\")\n .description(\"List scheduled jobs\")\n .option(\"-a, --all\", \"Include disabled jobs\", false)\n .action(async (opts) => {\n const { CronService } = await import(\"../cron/service.js\");\n\n const storePath = join(getDataDir(), \"cron\", \"jobs.json\");\n const service = new CronService(storePath);\n\n const jobs = service.listJobs(opts.all);\n\n if (jobs.length === 0) {\n console.log(\"No scheduled jobs.\");\n return;\n }\n\n console.log(\"Scheduled Jobs\");\n console.log(\"─\".repeat(70));\n console.log(\n `${\"ID\".padEnd(10)} ${\"Name\".padEnd(20)} ${\"Schedule\".padEnd(18)} ${\"Status\".padEnd(10)} Next Run`,\n );\n console.log(\"─\".repeat(70));\n\n for (const job of jobs) {\n let sched: string;\n if (job.schedule.kind === \"every\") {\n sched = `every ${(job.schedule.everyMs ?? 0) / 1000}s`;\n } else if (job.schedule.kind === \"cron\") {\n sched = job.schedule.expr ?? \"\";\n } else {\n sched = \"one-time\";\n }\n\n let nextRun = \"\";\n if (job.state.nextRunAtMs) {\n nextRun = new Date(job.state.nextRunAtMs).toLocaleString();\n }\n\n const status = job.enabled ? \"enabled\" : \"disabled\";\n\n console.log(\n `${job.id.padEnd(10)} ${job.name.padEnd(20)} ${sched.padEnd(18)} ${status.padEnd(10)} ${nextRun}`,\n );\n }\n });\n\ncronCmd\n .command(\"add\")\n .description(\"Add a scheduled job\")\n .requiredOption(\"-n, --name <name>\", \"Job name\")\n .requiredOption(\"-m, --message <text>\", \"Message for agent\")\n .option(\"-e, --every <seconds>\", \"Run every N seconds\")\n .option(\"-c, --cron <expr>\", \"Cron expression (e.g. '0 9 * * *')\")\n .option(\"--at <iso>\", \"Run once at time (ISO format)\")\n .option(\"-d, --deliver\", \"Deliver response to channel\", false)\n .option(\"--to <recipient>\", \"Recipient for delivery\")\n .option(\"--channel <name>\", \"Channel for delivery\")\n .action(async (opts) => {\n const { CronService } = await import(\"../cron/service.js\");\n\n let schedule: { kind: string; everyMs?: number; expr?: string; atMs?: number };\n if (opts.every) {\n schedule = { kind: \"every\", everyMs: Number(opts.every) * 1000 };\n } else if (opts.cron) {\n schedule = { kind: \"cron\", expr: opts.cron };\n } else if (opts.at) {\n const dt = new Date(opts.at);\n schedule = { kind: \"at\", atMs: dt.getTime() };\n } else {\n console.error(\"Error: Must specify --every, --cron, or --at\");\n process.exit(1);\n }\n\n const storePath = join(getDataDir(), \"cron\", \"jobs.json\");\n const service = new CronService(storePath);\n\n const job = service.addJob({\n name: opts.name,\n schedule: schedule as import(\"../cron/types.js\").CronSchedule,\n message: opts.message,\n deliver: opts.deliver,\n to: opts.to,\n channel: opts.channel,\n });\n\n console.log(`Added job '${job.name}' (${job.id})`);\n });\n\ncronCmd\n .command(\"remove\")\n .description(\"Remove a scheduled job\")\n .argument(\"<jobId>\", \"Job ID to remove\")\n .action(async (jobId: string) => {\n const { CronService } = await import(\"../cron/service.js\");\n\n const storePath = join(getDataDir(), \"cron\", \"jobs.json\");\n const service = new CronService(storePath);\n\n if (service.removeJob(jobId)) {\n console.log(`Removed job ${jobId}`);\n } else {\n console.error(`Job ${jobId} not found`);\n }\n });\n\ncronCmd\n .command(\"enable\")\n .description(\"Enable or disable a job\")\n .argument(\"<jobId>\", \"Job ID\")\n .option(\"--disable\", \"Disable instead of enable\", false)\n .action(async (jobId: string, opts: { disable: boolean }) => {\n const { CronService } = await import(\"../cron/service.js\");\n\n const storePath = join(getDataDir(), \"cron\", \"jobs.json\");\n const service = new CronService(storePath);\n\n const job = service.enableJob(jobId, !opts.disable);\n if (job) {\n const status = opts.disable ? \"disabled\" : \"enabled\";\n console.log(`Job '${job.name}' ${status}`);\n } else {\n console.error(`Job ${jobId} not found`);\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\n const hasOpenrouter = Boolean(config.providers.openrouter.apiKey);\n const hasAnthropic = Boolean(config.providers.anthropic.apiKey);\n const hasOpenai = Boolean(config.providers.openai.apiKey);\n const hasGemini = Boolean(config.providers.gemini.apiKey);\n const hasDeepseek = Boolean(config.providers.deepseek.apiKey);\n const hasOpenaiCompatible = Boolean(config.providers.openaiCompatible.apiKey);\n console.log(\n `OpenRouter API: ${hasOpenrouter ? \"[set]\" : \"[not set]\"}`,\n );\n console.log(\n `Anthropic API: ${hasAnthropic ? \"[set]\" : \"[not set]\"}`,\n );\n console.log(\n `OpenAI API: ${hasOpenai ? \"[set]\" : \"[not set]\"}`,\n );\n console.log(\n `Gemini API: ${hasGemini ? \"[set]\" : \"[not set]\"}`,\n );\n console.log(\n `DeepSeek API: ${hasDeepseek ? \"[set]\" : \"[not set]\"}`,\n );\n console.log(\n `OpenAI Compatible API: ${hasOpenaiCompatible ? \"[set]\" : \"[not set]\"}`,\n );\n }\n });\n\nprogram.parse();\n"],"mappings":"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"}
@@ -368,16 +368,16 @@ declare const ToolsConfigSchema: z.ZodObject<{
368
368
  export?: string | undefined;
369
369
  }>, "many">>;
370
370
  }, "strip", z.ZodTypeAny, {
371
+ exec: {
372
+ timeout: number;
373
+ restrictToWorkspace: boolean;
374
+ };
371
375
  web: {
372
376
  search: {
373
377
  apiKey: string;
374
378
  maxResults: number;
375
379
  };
376
380
  };
377
- exec: {
378
- timeout: number;
379
- restrictToWorkspace: boolean;
380
- };
381
381
  enabled?: string[] | undefined;
382
382
  disabled?: string[] | undefined;
383
383
  custom?: {
@@ -386,6 +386,10 @@ declare const ToolsConfigSchema: z.ZodObject<{
386
386
  export?: string | undefined;
387
387
  }[] | undefined;
388
388
  }, {
389
+ exec?: {
390
+ timeout?: number | undefined;
391
+ restrictToWorkspace?: boolean | undefined;
392
+ } | undefined;
389
393
  enabled?: string[] | undefined;
390
394
  web?: {
391
395
  search?: {
@@ -393,10 +397,6 @@ declare const ToolsConfigSchema: z.ZodObject<{
393
397
  maxResults?: number | undefined;
394
398
  } | undefined;
395
399
  } | undefined;
396
- exec?: {
397
- timeout?: number | undefined;
398
- restrictToWorkspace?: boolean | undefined;
399
- } | undefined;
400
400
  disabled?: string[] | undefined;
401
401
  custom?: {
402
402
  module: string;
@@ -663,16 +663,16 @@ declare const ConfigSchema: z.ZodObject<{
663
663
  export?: string | undefined;
664
664
  }>, "many">>;
665
665
  }, "strip", z.ZodTypeAny, {
666
+ exec: {
667
+ timeout: number;
668
+ restrictToWorkspace: boolean;
669
+ };
666
670
  web: {
667
671
  search: {
668
672
  apiKey: string;
669
673
  maxResults: number;
670
674
  };
671
675
  };
672
- exec: {
673
- timeout: number;
674
- restrictToWorkspace: boolean;
675
- };
676
676
  enabled?: string[] | undefined;
677
677
  disabled?: string[] | undefined;
678
678
  custom?: {
@@ -681,6 +681,10 @@ declare const ConfigSchema: z.ZodObject<{
681
681
  export?: string | undefined;
682
682
  }[] | undefined;
683
683
  }, {
684
+ exec?: {
685
+ timeout?: number | undefined;
686
+ restrictToWorkspace?: boolean | undefined;
687
+ } | undefined;
684
688
  enabled?: string[] | undefined;
685
689
  web?: {
686
690
  search?: {
@@ -688,10 +692,6 @@ declare const ConfigSchema: z.ZodObject<{
688
692
  maxResults?: number | undefined;
689
693
  } | undefined;
690
694
  } | undefined;
691
- exec?: {
692
- timeout?: number | undefined;
693
- restrictToWorkspace?: boolean | undefined;
694
- } | undefined;
695
695
  disabled?: string[] | undefined;
696
696
  custom?: {
697
697
  module: string;
@@ -752,16 +752,16 @@ declare const ConfigSchema: z.ZodObject<{
752
752
  port: number;
753
753
  };
754
754
  tools: {
755
+ exec: {
756
+ timeout: number;
757
+ restrictToWorkspace: boolean;
758
+ };
755
759
  web: {
756
760
  search: {
757
761
  apiKey: string;
758
762
  maxResults: number;
759
763
  };
760
764
  };
761
- exec: {
762
- timeout: number;
763
- restrictToWorkspace: boolean;
764
- };
765
765
  enabled?: string[] | undefined;
766
766
  disabled?: string[] | undefined;
767
767
  custom?: {
@@ -823,6 +823,10 @@ declare const ConfigSchema: z.ZodObject<{
823
823
  port?: number | undefined;
824
824
  } | undefined;
825
825
  tools?: {
826
+ exec?: {
827
+ timeout?: number | undefined;
828
+ restrictToWorkspace?: boolean | undefined;
829
+ } | undefined;
826
830
  enabled?: string[] | undefined;
827
831
  web?: {
828
832
  search?: {
@@ -830,10 +834,6 @@ declare const ConfigSchema: z.ZodObject<{
830
834
  maxResults?: number | undefined;
831
835
  } | undefined;
832
836
  } | undefined;
833
- exec?: {
834
- timeout?: number | undefined;
835
- restrictToWorkspace?: boolean | undefined;
836
- } | undefined;
837
837
  disabled?: string[] | undefined;
838
838
  custom?: {
839
839
  module: string;
@@ -17,7 +17,9 @@ declare class HeartbeatService {
17
17
  enabled?: boolean;
18
18
  });
19
19
  private get heartbeatFile();
20
- private readHeartbeatFile;
20
+ private readFile;
21
+ /** Read all HEARTBEAT.md files: root + skills/{name}/HEARTBEAT.md */
22
+ private readAllHeartbeatFiles;
21
23
  start(): Promise<void>;
22
24
  stop(): void;
23
25
  private scheduleNext;
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.mts","names":[],"sources":["../../src/heartbeat/service.ts"],"mappings":";KAgCK,iBAAA,IAAqB,MAAA,aAAmB,OAAA;;;;cAKhC,gBAAA;EAAA,QACH,SAAA;EAAA,QACA,WAAA;EAAA,QACA,SAAA;EAAA,QACA,OAAA;EAAA,QACA,QAAA;EAAA,QACA,WAAA;cAEI,MAAA;IACV,SAAA;IACA,WAAA,GAAc,iBAAA;IACd,SAAA;IACA,OAAA;EAAA;EAAA,YAQU,aAAA,CAAA;EAAA,QAIJ,iBAAA;EAWF,KAAA,CAAA,GAAS,OAAA;EAWf,IAAA,CAAA;EAAA,QAQQ,YAAA;EAAA,QAUM,IAAA;EAwBR,UAAA,CAAA,GAAc,OAAA;AAAA"}
1
+ {"version":3,"file":"service.d.mts","names":[],"sources":["../../src/heartbeat/service.ts"],"mappings":";KAgCK,iBAAA,IAAqB,MAAA,aAAmB,OAAA;;;;cAKhC,gBAAA;EAAA,QACH,SAAA;EAAA,QACA,WAAA;EAAA,QACA,SAAA;EAAA,QACA,OAAA;EAAA,QACA,QAAA;EAAA,QACA,WAAA;cAEI,MAAA;IACV,SAAA;IACA,WAAA,GAAc,iBAAA;IACd,SAAA;IACA,OAAA;EAAA;EAAA,YAQU,aAAA,CAAA;EAAA,QAIJ,QAAA;EAlBA;EAAA,QA4BA,qBAAA;EA6BF,KAAA,CAAA,GAAS,OAAA;EAWf,IAAA,CAAA;EAAA,QAQQ,YAAA;EAAA,QAUM,IAAA;EAkCR,UAAA,CAAA,GAAc,OAAA;AAAA"}
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync } from "node:fs";
1
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
 
4
4
  //#region src/heartbeat/service.ts
@@ -42,13 +42,37 @@ var HeartbeatService = class {
42
42
  get heartbeatFile() {
43
43
  return join(this.workspace, "HEARTBEAT.md");
44
44
  }
45
- readHeartbeatFile() {
46
- if (existsSync(this.heartbeatFile)) try {
47
- return readFileSync(this.heartbeatFile, "utf-8");
45
+ readFile(path) {
46
+ if (!existsSync(path)) return null;
47
+ try {
48
+ return readFileSync(path, "utf-8");
48
49
  } catch {
49
50
  return null;
50
51
  }
51
- return null;
52
+ }
53
+ /** Read all HEARTBEAT.md files: root + skills/{name}/HEARTBEAT.md */
54
+ readAllHeartbeatFiles() {
55
+ const results = [];
56
+ const rootContent = this.readFile(this.heartbeatFile);
57
+ if (rootContent && !isHeartbeatEmpty(rootContent)) results.push({
58
+ path: this.heartbeatFile,
59
+ label: "workspace",
60
+ content: rootContent
61
+ });
62
+ const skillsDir = join(this.workspace, "skills");
63
+ if (existsSync(skillsDir)) try {
64
+ for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
65
+ if (!entry.isDirectory()) continue;
66
+ const skillHeartbeat = join(skillsDir, entry.name, "HEARTBEAT.md");
67
+ const content = this.readFile(skillHeartbeat);
68
+ if (content && !isHeartbeatEmpty(content)) results.push({
69
+ path: skillHeartbeat,
70
+ label: `skills/${entry.name}`,
71
+ content
72
+ });
73
+ }
74
+ } catch {}
75
+ return results;
52
76
  }
53
77
  async start() {
54
78
  if (!this.enabled) {
@@ -76,13 +100,20 @@ var HeartbeatService = class {
76
100
  }, this.intervalS * 1e3);
77
101
  }
78
102
  async tick() {
79
- if (isHeartbeatEmpty(this.readHeartbeatFile())) return;
80
- console.log("Heartbeat: checking for tasks...");
81
- if (this.onHeartbeat) try {
82
- if ((await this.onHeartbeat(HEARTBEAT_PROMPT)).toUpperCase().replace(/_/g, "").includes(HEARTBEAT_OK_TOKEN.replace(/_/g, ""))) console.log("Heartbeat: OK (no action needed)");
83
- else console.log("Heartbeat: completed task");
84
- } catch (err) {
85
- console.error("Heartbeat execution failed:", err);
103
+ const heartbeats = this.readAllHeartbeatFiles();
104
+ if (heartbeats.length === 0) return;
105
+ const locations = heartbeats.map((h) => h.label).join(", ");
106
+ console.log(`Heartbeat: checking for tasks in ${locations}...`);
107
+ if (this.onHeartbeat) {
108
+ let prompt;
109
+ if (heartbeats.length === 1 && heartbeats[0].label === "workspace") prompt = HEARTBEAT_PROMPT;
110
+ else prompt = `Check the following HEARTBEAT.md files in your workspace for tasks:\n${heartbeats.map((h) => `- ${h.label}/HEARTBEAT.md`).join("\n")}\nFollow any instructions or tasks listed there.\nIf nothing needs attention, reply with just: HEARTBEAT_OK`;
111
+ try {
112
+ if ((await this.onHeartbeat(prompt)).toUpperCase().replace(/_/g, "").includes(HEARTBEAT_OK_TOKEN.replace(/_/g, ""))) console.log("Heartbeat: OK (no action needed)");
113
+ else console.log("Heartbeat: completed task");
114
+ } catch (err) {
115
+ console.error("Heartbeat execution failed:", err);
116
+ }
86
117
  }
87
118
  }
88
119
  async triggerNow() {
@@ -1 +1 @@
1
- {"version":3,"file":"service.mjs","names":[],"sources":["../../src/heartbeat/service.ts"],"sourcesContent":["import { readFileSync, existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\nconst DEFAULT_HEARTBEAT_INTERVAL_S = 30 * 60; // 30 minutes\n\nconst HEARTBEAT_PROMPT = `Read HEARTBEAT.md in your workspace (if it exists).\nFollow any instructions or tasks listed there.\nIf nothing needs attention, reply with just: HEARTBEAT_OK`;\n\nconst HEARTBEAT_OK_TOKEN = \"HEARTBEAT_OK\";\n\n/** Check if HEARTBEAT.md has no actionable content. */\nfunction isHeartbeatEmpty(content: string | null): boolean {\n if (!content) return true;\n\n const skipPatterns = new Set([\"- [ ]\", \"* [ ]\", \"- [x]\", \"* [x]\"]);\n\n for (const rawLine of content.split(\"\\n\")) {\n const line = rawLine.trim();\n if (\n !line ||\n line.startsWith(\"#\") ||\n line.startsWith(\"<!--\") ||\n skipPatterns.has(line)\n ) {\n continue;\n }\n return false; // Found actionable content\n }\n return true;\n}\n\ntype HeartbeatCallback = (prompt: string) => Promise<string>;\n\n/**\n * Periodic heartbeat service that wakes the agent to check for tasks.\n */\nexport class HeartbeatService {\n private workspace: string;\n private onHeartbeat: HeartbeatCallback | null;\n private intervalS: number;\n private enabled: boolean;\n private _running = false;\n private timerHandle: ReturnType<typeof setTimeout> | null = null;\n\n constructor(params: {\n workspace: string;\n onHeartbeat?: HeartbeatCallback;\n intervalS?: number;\n enabled?: boolean;\n }) {\n this.workspace = params.workspace;\n this.onHeartbeat = params.onHeartbeat ?? null;\n this.intervalS = params.intervalS ?? DEFAULT_HEARTBEAT_INTERVAL_S;\n this.enabled = params.enabled ?? true;\n }\n\n private get heartbeatFile(): string {\n return join(this.workspace, \"HEARTBEAT.md\");\n }\n\n private readHeartbeatFile(): string | null {\n if (existsSync(this.heartbeatFile)) {\n try {\n return readFileSync(this.heartbeatFile, \"utf-8\");\n } catch {\n return null;\n }\n }\n return null;\n }\n\n async start(): Promise<void> {\n if (!this.enabled) {\n console.log(\"Heartbeat disabled\");\n return;\n }\n\n this._running = true;\n this.scheduleNext();\n console.log(`Heartbeat started (every ${this.intervalS}s)`);\n }\n\n stop(): void {\n this._running = false;\n if (this.timerHandle) {\n clearTimeout(this.timerHandle);\n this.timerHandle = null;\n }\n }\n\n private scheduleNext(): void {\n if (!this._running) return;\n this.timerHandle = setTimeout(async () => {\n if (this._running) {\n await this.tick();\n this.scheduleNext();\n }\n }, this.intervalS * 1000);\n }\n\n private async tick(): Promise<void> {\n const content = this.readHeartbeatFile();\n\n if (isHeartbeatEmpty(content)) {\n return;\n }\n\n console.log(\"Heartbeat: checking for tasks...\");\n\n if (this.onHeartbeat) {\n try {\n const response = await this.onHeartbeat(HEARTBEAT_PROMPT);\n const normalized = response.toUpperCase().replace(/_/g, \"\");\n if (normalized.includes(HEARTBEAT_OK_TOKEN.replace(/_/g, \"\"))) {\n console.log(\"Heartbeat: OK (no action needed)\");\n } else {\n console.log(\"Heartbeat: completed task\");\n }\n } catch (err) {\n console.error(\"Heartbeat execution failed:\", err);\n }\n }\n }\n\n async triggerNow(): Promise<string | null> {\n if (this.onHeartbeat) {\n return this.onHeartbeat(HEARTBEAT_PROMPT);\n }\n return null;\n }\n}\n"],"mappings":";;;;AAGA,MAAM,+BAA+B;AAErC,MAAM,mBAAmB;;;AAIzB,MAAM,qBAAqB;;AAG3B,SAAS,iBAAiB,SAAiC;AACzD,KAAI,CAAC,QAAS,QAAO;CAErB,MAAM,eAAe,IAAI,IAAI;EAAC;EAAS;EAAS;EAAS;EAAQ,CAAC;AAElE,MAAK,MAAM,WAAW,QAAQ,MAAM,KAAK,EAAE;EACzC,MAAM,OAAO,QAAQ,MAAM;AAC3B,MACE,CAAC,QACD,KAAK,WAAW,IAAI,IACpB,KAAK,WAAW,OAAO,IACvB,aAAa,IAAI,KAAK,CAEtB;AAEF,SAAO;;AAET,QAAO;;;;;AAQT,IAAa,mBAAb,MAA8B;CAC5B,AAAQ;CACR,AAAQ;CACR,AAAQ;CACR,AAAQ;CACR,AAAQ,WAAW;CACnB,AAAQ,cAAoD;CAE5D,YAAY,QAKT;AACD,OAAK,YAAY,OAAO;AACxB,OAAK,cAAc,OAAO,eAAe;AACzC,OAAK,YAAY,OAAO,aAAa;AACrC,OAAK,UAAU,OAAO,WAAW;;CAGnC,IAAY,gBAAwB;AAClC,SAAO,KAAK,KAAK,WAAW,eAAe;;CAG7C,AAAQ,oBAAmC;AACzC,MAAI,WAAW,KAAK,cAAc,CAChC,KAAI;AACF,UAAO,aAAa,KAAK,eAAe,QAAQ;UAC1C;AACN,UAAO;;AAGX,SAAO;;CAGT,MAAM,QAAuB;AAC3B,MAAI,CAAC,KAAK,SAAS;AACjB,WAAQ,IAAI,qBAAqB;AACjC;;AAGF,OAAK,WAAW;AAChB,OAAK,cAAc;AACnB,UAAQ,IAAI,4BAA4B,KAAK,UAAU,IAAI;;CAG7D,OAAa;AACX,OAAK,WAAW;AAChB,MAAI,KAAK,aAAa;AACpB,gBAAa,KAAK,YAAY;AAC9B,QAAK,cAAc;;;CAIvB,AAAQ,eAAqB;AAC3B,MAAI,CAAC,KAAK,SAAU;AACpB,OAAK,cAAc,WAAW,YAAY;AACxC,OAAI,KAAK,UAAU;AACjB,UAAM,KAAK,MAAM;AACjB,SAAK,cAAc;;KAEpB,KAAK,YAAY,IAAK;;CAG3B,MAAc,OAAsB;AAGlC,MAAI,iBAFY,KAAK,mBAAmB,CAEX,CAC3B;AAGF,UAAQ,IAAI,mCAAmC;AAE/C,MAAI,KAAK,YACP,KAAI;AAGF,QAFiB,MAAM,KAAK,YAAY,iBAAiB,EAC7B,aAAa,CAAC,QAAQ,MAAM,GAAG,CAC5C,SAAS,mBAAmB,QAAQ,MAAM,GAAG,CAAC,CAC3D,SAAQ,IAAI,mCAAmC;OAE/C,SAAQ,IAAI,4BAA4B;WAEnC,KAAK;AACZ,WAAQ,MAAM,+BAA+B,IAAI;;;CAKvD,MAAM,aAAqC;AACzC,MAAI,KAAK,YACP,QAAO,KAAK,YAAY,iBAAiB;AAE3C,SAAO"}
1
+ {"version":3,"file":"service.mjs","names":[],"sources":["../../src/heartbeat/service.ts"],"sourcesContent":["import { readFileSync, existsSync, readdirSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\nconst DEFAULT_HEARTBEAT_INTERVAL_S = 30 * 60; // 30 minutes\n\nconst HEARTBEAT_PROMPT = `Read HEARTBEAT.md in your workspace (if it exists).\nFollow any instructions or tasks listed there.\nIf nothing needs attention, reply with just: HEARTBEAT_OK`;\n\nconst HEARTBEAT_OK_TOKEN = \"HEARTBEAT_OK\";\n\n/** Check if HEARTBEAT.md has no actionable content. */\nfunction isHeartbeatEmpty(content: string | null): boolean {\n if (!content) return true;\n\n const skipPatterns = new Set([\"- [ ]\", \"* [ ]\", \"- [x]\", \"* [x]\"]);\n\n for (const rawLine of content.split(\"\\n\")) {\n const line = rawLine.trim();\n if (\n !line ||\n line.startsWith(\"#\") ||\n line.startsWith(\"<!--\") ||\n skipPatterns.has(line)\n ) {\n continue;\n }\n return false; // Found actionable content\n }\n return true;\n}\n\ntype HeartbeatCallback = (prompt: string) => Promise<string>;\n\n/**\n * Periodic heartbeat service that wakes the agent to check for tasks.\n */\nexport class HeartbeatService {\n private workspace: string;\n private onHeartbeat: HeartbeatCallback | null;\n private intervalS: number;\n private enabled: boolean;\n private _running = false;\n private timerHandle: ReturnType<typeof setTimeout> | null = null;\n\n constructor(params: {\n workspace: string;\n onHeartbeat?: HeartbeatCallback;\n intervalS?: number;\n enabled?: boolean;\n }) {\n this.workspace = params.workspace;\n this.onHeartbeat = params.onHeartbeat ?? null;\n this.intervalS = params.intervalS ?? DEFAULT_HEARTBEAT_INTERVAL_S;\n this.enabled = params.enabled ?? true;\n }\n\n private get heartbeatFile(): string {\n return join(this.workspace, \"HEARTBEAT.md\");\n }\n\n private readFile(path: string): string | null {\n if (!existsSync(path)) return null;\n try {\n return readFileSync(path, \"utf-8\");\n } catch {\n return null;\n }\n }\n\n /** Read all HEARTBEAT.md files: root + skills/{name}/HEARTBEAT.md */\n private readAllHeartbeatFiles(): { path: string; label: string; content: string }[] {\n const results: { path: string; label: string; content: string }[] = [];\n\n // Root heartbeat\n const rootContent = this.readFile(this.heartbeatFile);\n if (rootContent && !isHeartbeatEmpty(rootContent)) {\n results.push({ path: this.heartbeatFile, label: \"workspace\", content: rootContent });\n }\n\n // Skills heartbeats\n const skillsDir = join(this.workspace, \"skills\");\n if (existsSync(skillsDir)) {\n try {\n for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {\n if (!entry.isDirectory()) continue;\n const skillHeartbeat = join(skillsDir, entry.name, \"HEARTBEAT.md\");\n const content = this.readFile(skillHeartbeat);\n if (content && !isHeartbeatEmpty(content)) {\n results.push({ path: skillHeartbeat, label: `skills/${entry.name}`, content });\n }\n }\n } catch {\n // skills dir not readable, ignore\n }\n }\n\n return results;\n }\n\n async start(): Promise<void> {\n if (!this.enabled) {\n console.log(\"Heartbeat disabled\");\n return;\n }\n\n this._running = true;\n this.scheduleNext();\n console.log(`Heartbeat started (every ${this.intervalS}s)`);\n }\n\n stop(): void {\n this._running = false;\n if (this.timerHandle) {\n clearTimeout(this.timerHandle);\n this.timerHandle = null;\n }\n }\n\n private scheduleNext(): void {\n if (!this._running) return;\n this.timerHandle = setTimeout(async () => {\n if (this._running) {\n await this.tick();\n this.scheduleNext();\n }\n }, this.intervalS * 1000);\n }\n\n private async tick(): Promise<void> {\n const heartbeats = this.readAllHeartbeatFiles();\n\n if (heartbeats.length === 0) {\n return;\n }\n\n const locations = heartbeats.map((h) => h.label).join(\", \");\n console.log(`Heartbeat: checking for tasks in ${locations}...`);\n\n if (this.onHeartbeat) {\n // Build a prompt that tells the agent which files to check\n let prompt: string;\n if (heartbeats.length === 1 && heartbeats[0].label === \"workspace\") {\n prompt = HEARTBEAT_PROMPT;\n } else {\n const fileList = heartbeats.map((h) => `- ${h.label}/HEARTBEAT.md`).join(\"\\n\");\n prompt = `Check the following HEARTBEAT.md files in your workspace for tasks:\\n${fileList}\\nFollow any instructions or tasks listed there.\\nIf nothing needs attention, reply with just: HEARTBEAT_OK`;\n }\n\n try {\n const response = await this.onHeartbeat(prompt);\n const normalized = response.toUpperCase().replace(/_/g, \"\");\n if (normalized.includes(HEARTBEAT_OK_TOKEN.replace(/_/g, \"\"))) {\n console.log(\"Heartbeat: OK (no action needed)\");\n } else {\n console.log(\"Heartbeat: completed task\");\n }\n } catch (err) {\n console.error(\"Heartbeat execution failed:\", err);\n }\n }\n }\n\n async triggerNow(): Promise<string | null> {\n if (this.onHeartbeat) {\n return this.onHeartbeat(HEARTBEAT_PROMPT);\n }\n return null;\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jcheesepkg/nanobot",
3
- "version": "0.2.6",
3
+ "version": "0.3.1",
4
4
  "description": "Lightweight AI assistant - TypeScript port",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",
@@ -18,6 +18,7 @@
18
18
  "build": "tsdown",
19
19
  "dev": "tsx src/cli/index.ts",
20
20
  "start": "node dist/cli/index.mjs",
21
+ "test": "vitest run",
21
22
  "typecheck": "tsc --noEmit"
22
23
  },
23
24
  "dependencies": {
@@ -33,7 +34,8 @@
33
34
  "@types/node": "^22.0.0",
34
35
  "tsdown": "^0.20.3",
35
36
  "tsx": "^4.19.0",
36
- "typescript": "^5.6.0"
37
+ "typescript": "^5.6.0",
38
+ "vitest": "^2.1.9"
37
39
  },
38
40
  "engines": {
39
41
  "node": ">=18.0.0"