@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.
- package/dist/channels/base.d.mts +0 -4
- package/dist/channels/base.d.mts.map +1 -1
- package/dist/channels/base.mjs +0 -9
- package/dist/channels/base.mjs.map +1 -1
- package/dist/channels/manager.d.mts +0 -10
- package/dist/channels/manager.d.mts.map +1 -1
- package/dist/channels/manager.mjs +0 -48
- package/dist/channels/manager.mjs.map +1 -1
- package/dist/cli/index.mjs +10 -136
- package/dist/cli/index.mjs.map +1 -1
- package/dist/config/schema.d.mts +24 -24
- package/dist/heartbeat/service.d.mts +3 -1
- package/dist/heartbeat/service.d.mts.map +1 -1
- package/dist/heartbeat/service.mjs +43 -12
- package/dist/heartbeat/service.mjs.map +1 -1
- package/package.json +4 -2
package/dist/channels/base.d.mts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/channels/base.mjs
CHANGED
|
@@ -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
|
|
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":";;;;;;;
|
|
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
|
|
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"}
|
package/dist/cli/index.mjs
CHANGED
|
@@ -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,
|
|
6
|
-
import {
|
|
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
|
|
285
|
-
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
208
|
+
createGatewayServer({
|
|
335
209
|
agent,
|
|
336
210
|
port: Number(opts.port)
|
|
337
211
|
});
|
package/dist/cli/index.mjs.map
CHANGED
|
@@ -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":";;;;;;;;;;;;;AAqBA,MAAM,UAAU,IAAI,SAAS;AAE7B,eAAe,gBAAgB,QAAiC;CAC9D,MAAM,gBAAgB,OAAO,MAAM,UAAU,EAAE;AAC/C,KAAI,cAAc,WAAW,EAAG,QAAO,EAAE;CAEzC,MAAM,QAAgB,EAAE;AACxB,MAAK,MAAM,SAAS,cAClB,KAAI;EAIF,MAAM,MAAM,MAAM,OAAO,cAHN,WAAW,MAAM,OAAO,GACvC,MAAM,SACN,QAAQ,QAAQ,KAAK,EAAE,MAAM,OAAO,CACU,CAAC;EACnD,MAAM,aAAa,MAAM,UAAU;EACnC,MAAM,WAAW,eAAe,YAAY,IAAI,UAAU,IAAI;AAE9D,MAAI,CAAC,SACH,OAAM,IAAI,MAAM,WAAW,WAAW,aAAa;EAGrD,IAAI;AACJ,MAAI,OAAO,aAAa,WACtB,KAAI;AACF,cAAW,IAAI,SAAS,MAAM,WAAW,EAAE,CAAC;UACtC;AACN,cAAW,SAAS,MAAM,WAAW,EAAE,CAAC;;MAG1C,OAAM,IAAI,MAAM,WAAW,WAAW,qBAAqB;AAG7D,MACE,YACA,OAAO,aAAa,YACpB,aAAa,YACb,UAAU,SAEV,OAAM,KAAK,SAAiB;MAE5B,OAAM,IAAI,MAAM,WAAW,WAAW,kCAAkC;UAEnE,KAAK;AACZ,UAAQ,KACN,wCAAwC,MAAM,OAAO,KACnD,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAEnD;;AAIL,QAAO;;AAGT,QACG,KAAK,UAAU,CACf,YAAY,GAAG,KAAK,kCAAkC,CACtD,QAAQ,GAAG,KAAK,YAAY,WAAW,gBAAgB;AAM1D,QACG,QAAQ,UAAU,CAClB,YAAY,iDAAiD,CAC7D,aAAa;CACZ,MAAM,aAAa,eAAe;AAElC,KAAI,WAAW,WAAW,EAAE;AAC1B,UAAQ,IAAI,4BAA4B,aAAa;AACrD,UAAQ,IAAI,8CAA8C;AAC1D;;CAIF,MAAM,SAAS,aAAa,MAAM,EAAE,CAAC;AACrC,YAAW,OAAO;AAClB,SAAQ,IAAI,qBAAqB,aAAa;AAI9C,0BADkB,uBAAuB,OAAO,CACb;AAEnC,SAAQ,IAAI,KAAK,KAAK,oBAAoB;AAC1C,SAAQ,IAAI,gBAAgB;AAC5B,SAAQ,IAAI,kDAAkD;AAC9D,SAAQ,IAAI,8CAA8C;AAC1D,SAAQ,IAAI,yCAAuC;AACnD,SAAQ,IACN,oEACD;EACD;AAEJ,SAAS,yBAAyB,WAAyB;AACzD,KAAI,CAAC,WAAW,UAAU,CACxB,WAAU,WAAW,EAAE,WAAW,MAAM,CAAC;AAkD3C,MAAK,MAAM,CAAC,UAAU,YAAY,OAAO,QA/CC;EACxC,aAAa;;;;;;;;;;;EAWb,WAAW;;;;;;;;;;;;;;;;EAgBX,WAAW;;;;;;;;;;EAUX,gBAAgB;;;;;;;EAOjB,CAE0D,EAAE;EAC3D,MAAM,WAAW,KAAK,WAAW,SAAS;AAC1C,MAAI,CAAC,WAAW,SAAS,EAAE;AACzB,iBAAc,UAAU,QAAQ;AAChC,WAAQ,IAAI,aAAa,WAAW;;;CAKxC,MAAM,YAAY,KAAK,WAAW,SAAS;AAC3C,KAAI,CAAC,WAAW,UAAU,CACxB,WAAU,WAAW,EAAE,WAAW,MAAM,CAAC;CAE3C,MAAM,aAAa,KAAK,WAAW,YAAY;AAC/C,KAAI,CAAC,WAAW,WAAW,EAAE;AAC3B,gBACE,YACA;;;;;;;;;;;;;;;EAgBD;AACD,UAAQ,IAAI,6BAA6B;;;AAQ7C,QACG,QAAQ,UAAU,CAClB,YAAY,4BAA4B,CACxC,OAAO,uBAAuB,gBAAgB,QAAQ,CACtD,OAAO,aAAa,kBAAkB,MAAM,CAC5C,OAAO,OAAO,SAAS;AACtB,SAAQ,IACN,GAAG,KAAK,oCAAoC,KAAK,KAAK,KACvD;CAED,MAAM,SAAS,YAAY;CAC3B,MAAM,SAAS,UAAU,OAAO;CAChC,MAAM,UAAU,WAAW,OAAO;CAClC,MAAM,QAAQ,OAAO,OAAO,SAAS;AAErC,KAAI,CAAC,QAAQ;AACX,UAAQ,MAAM,gCAAgC;AAC9C,UAAQ,MACN,sEACD;AACD,UAAQ,KAAK,EAAE;;CAIjB,MAAM,EAAE,eAAe,MAAM,OAAO;CACpC,MAAM,EAAE,mBAAmB,MAAM,OAC/B;CAEF,MAAM,EAAE,cAAc,MAAM,OAAO;CACnC,MAAM,EAAE,mBAAmB,MAAM,OAAO;CACxC,MAAM,EAAE,gBAAgB,MAAM,OAAO;CACrC,MAAM,EAAE,qBAAqB,MAAM,OACjC;CAGF,MAAM,MAAM,IAAI,YAAY;CAC5B,MAAM,YAAY,uBAAuB,OAAO;CAEhD,MAAM,WAAW,IAAI,eAAe;EAClC;EACA,SAAS,WAAW;EACpB,cAAc;EACf,CAAC;CAEF,MAAM,cAAc,MAAM,gBAAgB,OAAO;CAIjD,MAAM,OAAO,IAAI,YADK,KAAK,YAAY,EAAE,QAAQ,YAAY,CAClB;CAG3C,MAAM,QAAQ,IAAI,UAAU;EAC1B;EACA;EACA;EACA;EACA,WAAW,OAAO,OAAO,SAAS;EAClC,eAAe,OAAO,OAAO,SAAS;EACtC,aAAa,OAAO,MAAM,IAAI,OAAO,UAAU;EAC/C,YAAY,OAAO,MAAM;EACzB,aAAa;EACb,cAAc,OAAO,MAAM;EAC3B,eAAe,OAAO,MAAM;EAC5B;EACD,CAAC;CAGF,MAAM,WAAW,IAAI,eAAe,QAAQ,IAAI;AAGhD,MAAK,QAAQ,OAAO,QAAQ;EAC1B,MAAM,aAAa,QAAQ,IAAI;EAE/B,MAAM,WAAW,MAAM,MAAM,cAC3B,IAAI,QAAQ,SACZ,YACA,IAAI,QAAQ,WAAW,QACvB,IAAI,QAAQ,MAAM,IAAI,GACvB;AAGD,MAAI,IAAI,QAAQ,WAAW,IAAI,QAAQ,IAAI;GACzC,MAAM,EAAE,0BAA0B,MAAM,OACtC;AAEF,SAAM,IAAI,gBACR,sBAAsB;IACpB,SAAS,IAAI,QAAQ,WAAW;IAChC,QAAQ,IAAI,QAAQ;IACpB,SAAS,YAAY;IACtB,CAAC,CACH;;AAGH,SAAO;;CAIT,MAAM,YAAY,IAAI,iBAAiB;EACrC;EACA,cAAc,WACZ,MAAM,cAAc,QAAQ,YAAY;EAC1C,WAAW;EACX,SAAS;EACV,CAAC;CAEF,MAAM,aAAa,KAAK,QAAQ;AAChC,KAAI,WAAW,OAAO,EACpB,SAAQ,IAAI,SAAS,WAAW,KAAK,iBAAiB;AAExD,SAAQ,IAAI,uBAAuB;CAGnC,MAAM,WAAW,YAAY;AAC3B,UAAQ,IAAI,qBAAqB;AACjC,YAAU,MAAM;AAChB,OAAK,MAAM;AACX,QAAM,MAAM;AACZ,QAAM,SAAS,SAAS;AACxB,UAAQ,KAAK,EAAE;;AAEjB,SAAQ,GAAG,UAAU,SAAS;AAC9B,SAAQ,GAAG,WAAW,SAAS;CAG/B,MAAM,EAAE,wBAAwB,MAAM,OACpC;AAEF,qBAAoB;EAClB;EACA,MAAM,OAAO,KAAK,KAAK;EACxB,CAAC;AAEF,KAAI;AAEF,QAAM,SAAS,MAAM;AACrB,QAAM,KAAK,OAAO;AAClB,QAAM,UAAU,OAAO;AAEvB,QAAM,QAAQ,IAAI,CAAC,MAAM,KAAK,EAAE,SAAS,UAAU,CAAC,CAAC;UAC9C,KAAK;AACZ,UAAQ,MAAM,kBAAkB,IAAI;AACpC,UAAQ,KAAK,EAAE;;EAEjB;AAMJ,QACG,QAAQ,QAAQ,CAChB,YAAY,mCAAmC,CAC/C,OAAO,wBAAwB,+BAA+B,CAC9D,OACC,sBACA,cACA,cACD,CACA,OAAO,OAAO,SAAS;CACtB,MAAM,SAAS,YAAY;CAC3B,MAAM,SAAS,UAAU,OAAO;CAChC,MAAM,UAAU,WAAW,OAAO;AAElC,KAAI,CAAC,QAAQ;AACX,UAAQ,MAAM,gCAAgC;AAC9C,UAAQ,KAAK,EAAE;;CAGjB,MAAM,EAAE,eAAe,MAAM,OAAO;CACpC,MAAM,EAAE,mBAAmB,MAAM,OAC/B;CAEF,MAAM,EAAE,cAAc,MAAM,OAAO;CAEnC,MAAM,MAAM,IAAI,YAAY;CAC5B,MAAM,YAAY,uBAAuB,OAAO;CAEhD,MAAM,WAAW,IAAI,eAAe;EAClC;EACA,SAAS,WAAW;EACpB,cAAc,OAAO,OAAO,SAAS;EACtC,CAAC;CAEF,MAAM,cAAc,MAAM,gBAAgB,OAAO;CAEjD,MAAM,YAAY,IAAI,UAAU;EAC9B;EACA;EACA;EACA,WAAW,OAAO,OAAO,SAAS;EAClC,aAAa,OAAO,MAAM,IAAI,OAAO,UAAU;EAC/C,YAAY,OAAO,MAAM;EACzB,cAAc,OAAO,MAAM;EAC3B,eAAe,OAAO,MAAM;EAC5B;EACD,CAAC;AAEF,KAAI,KAAK,SAAS;EAEhB,MAAM,WAAW,MAAM,UAAU,cAC/B,KAAK,SACL,KAAK,QACN;AACD,UAAQ,IAAI,KAAK,KAAK,GAAG,WAAW;QAC/B;AAEL,UAAQ,IAAI,GAAG,KAAK,sCAAsC;EAG1D,MAAM,MADW,MAAM,OAAO,kBACV,gBAAgB;GAClC,OAAO,QAAQ;GACf,QAAQ,QAAQ;GACjB,CAAC;EAEF,MAAM,YAAkB;AACtB,MAAG,SAAS,SAAS,OAAO,UAAU;IACpC,MAAM,UAAU,MAAM,MAAM;AAC5B,QAAI,CAAC,SAAS;AACZ,UAAK;AACL;;AAGF,QAAI;KACF,MAAM,WAAW,MAAM,UAAU,cAC/B,SACA,KAAK,QACN;AACD,aAAQ,IAAI,KAAK,KAAK,GAAG,SAAS,IAAI;aAC/B,KAAK;AACZ,aAAQ,MAAM,UAAU,IAAI;;AAE9B,SAAK;KACL;;AAGJ,KAAG,GAAG,eAAe;AACnB,WAAQ,IAAI,aAAa;AACzB,WAAQ,KAAK,EAAE;IACf;AAEF,OAAK;;EAEP;AAMgB,QACjB,QAAQ,WAAW,CACnB,YAAY,kBAAkB,CAG9B,QAAQ,SAAS,CACjB,YAAY,sBAAsB,CAClC,aAAa;CACZ,MAAM,SAAS,YAAY;AAE3B,SAAQ,IAAI,iBAAiB;AAC7B,SAAQ,IAAI,IAAI,OAAO,GAAG,CAAC;CAE3B,MAAM,KAAK,OAAO,SAAS;CAC3B,MAAM,UAAU,GAAG,QACf,UAAU,GAAG,MAAM,MAAM,GAAG,GAAG,CAAC,OAChC;AACJ,SAAQ,IACN,eAAe,GAAG,UAAU,cAAc,aAAa,IAAI,UAC5D;EACD;AAMJ,MAAM,UAAU,QACb,QAAQ,OAAO,CACf,YAAY,yBAAyB;AAExC,QACG,QAAQ,OAAO,CACf,YAAY,sBAAsB,CAClC,OAAO,aAAa,yBAAyB,MAAM,CACnD,OAAO,OAAO,SAAS;CACtB,MAAM,EAAE,gBAAgB,MAAM,OAAO;CAKrC,MAAM,OAFU,IAAI,YADF,KAAK,YAAY,EAAE,QAAQ,YAAY,CACf,CAErB,SAAS,KAAK,IAAI;AAEvC,KAAI,KAAK,WAAW,GAAG;AACrB,UAAQ,IAAI,qBAAqB;AACjC;;AAGF,SAAQ,IAAI,iBAAiB;AAC7B,SAAQ,IAAI,IAAI,OAAO,GAAG,CAAC;AAC3B,SAAQ,IACN,GAAG,KAAK,OAAO,GAAG,CAAC,GAAG,OAAO,OAAO,GAAG,CAAC,GAAG,WAAW,OAAO,GAAG,CAAC,GAAG,SAAS,OAAO,GAAG,CAAC,WACzF;AACD,SAAQ,IAAI,IAAI,OAAO,GAAG,CAAC;AAE3B,MAAK,MAAM,OAAO,MAAM;EACtB,IAAI;AACJ,MAAI,IAAI,SAAS,SAAS,QACxB,SAAQ,UAAU,IAAI,SAAS,WAAW,KAAK,IAAK;WAC3C,IAAI,SAAS,SAAS,OAC/B,SAAQ,IAAI,SAAS,QAAQ;MAE7B,SAAQ;EAGV,IAAI,UAAU;AACd,MAAI,IAAI,MAAM,YACZ,WAAU,IAAI,KAAK,IAAI,MAAM,YAAY,CAAC,gBAAgB;EAG5D,MAAM,SAAS,IAAI,UAAU,YAAY;AAEzC,UAAQ,IACN,GAAG,IAAI,GAAG,OAAO,GAAG,CAAC,GAAG,IAAI,KAAK,OAAO,GAAG,CAAC,GAAG,MAAM,OAAO,GAAG,CAAC,GAAG,OAAO,OAAO,GAAG,CAAC,GAAG,UACzF;;EAEH;AAEJ,QACG,QAAQ,MAAM,CACd,YAAY,sBAAsB,CAClC,eAAe,qBAAqB,WAAW,CAC/C,eAAe,wBAAwB,oBAAoB,CAC3D,OAAO,yBAAyB,sBAAsB,CACtD,OAAO,qBAAqB,qCAAqC,CACjE,OAAO,cAAc,gCAAgC,CACrD,OAAO,iBAAiB,+BAA+B,MAAM,CAC7D,OAAO,oBAAoB,yBAAyB,CACpD,OAAO,oBAAoB,uBAAuB,CAClD,OAAO,OAAO,SAAS;CACtB,MAAM,EAAE,gBAAgB,MAAM,OAAO;CAErC,IAAI;AACJ,KAAI,KAAK,MACP,YAAW;EAAE,MAAM;EAAS,SAAS,OAAO,KAAK,MAAM,GAAG;EAAM;UACvD,KAAK,KACd,YAAW;EAAE,MAAM;EAAQ,MAAM,KAAK;EAAM;UACnC,KAAK,GAEd,YAAW;EAAE,MAAM;EAAM,MADd,IAAI,KAAK,KAAK,GAAG,CACM,SAAS;EAAE;MACxC;AACL,UAAQ,MAAM,+CAA+C;AAC7D,UAAQ,KAAK,EAAE;;CAMjB,MAAM,MAFU,IAAI,YADF,KAAK,YAAY,EAAE,QAAQ,YAAY,CACf,CAEtB,OAAO;EACzB,MAAM,KAAK;EACD;EACV,SAAS,KAAK;EACd,SAAS,KAAK;EACd,IAAI,KAAK;EACT,SAAS,KAAK;EACf,CAAC;AAEF,SAAQ,IAAI,cAAc,IAAI,KAAK,KAAK,IAAI,GAAG,GAAG;EAClD;AAEJ,QACG,QAAQ,SAAS,CACjB,YAAY,yBAAyB,CACrC,SAAS,WAAW,mBAAmB,CACvC,OAAO,OAAO,UAAkB;CAC/B,MAAM,EAAE,gBAAgB,MAAM,OAAO;AAKrC,KAFgB,IAAI,YADF,KAAK,YAAY,EAAE,QAAQ,YAAY,CACf,CAE9B,UAAU,MAAM,CAC1B,SAAQ,IAAI,eAAe,QAAQ;KAEnC,SAAQ,MAAM,OAAO,MAAM,YAAY;EAEzC;AAEJ,QACG,QAAQ,SAAS,CACjB,YAAY,0BAA0B,CACtC,SAAS,WAAW,SAAS,CAC7B,OAAO,aAAa,6BAA6B,MAAM,CACvD,OAAO,OAAO,OAAe,SAA+B;CAC3D,MAAM,EAAE,gBAAgB,MAAM,OAAO;CAKrC,MAAM,MAFU,IAAI,YADF,KAAK,YAAY,EAAE,QAAQ,YAAY,CACf,CAEtB,UAAU,OAAO,CAAC,KAAK,QAAQ;AACnD,KAAI,KAAK;EACP,MAAM,SAAS,KAAK,UAAU,aAAa;AAC3C,UAAQ,IAAI,QAAQ,IAAI,KAAK,IAAI,SAAS;OAE1C,SAAQ,MAAM,OAAO,MAAM,YAAY;EAEzC;AAMJ,QACG,QAAQ,SAAS,CACjB,YAAY,sBAAsB,CAClC,aAAa;CACZ,MAAM,aAAa,eAAe;CAClC,MAAM,SAAS,YAAY;CAC3B,MAAM,YAAY,uBAAuB,OAAO;AAEhD,SAAQ,IAAI,GAAG,KAAK,mBAAmB;AAEvC,SAAQ,IACN,WAAW,WAAW,GAAG,WAAW,WAAW,GAAG,SAAS,cAC5D;AACD,SAAQ,IACN,cAAc,UAAU,GAAG,WAAW,UAAU,GAAG,SAAS,cAC7D;AAED,KAAI,WAAW,WAAW,EAAE;AAC1B,UAAQ,IAAI,UAAU,OAAO,OAAO,SAAS,QAAQ;EAErD,MAAM,gBAAgB,QAAQ,OAAO,UAAU,WAAW,OAAO;EACjE,MAAM,eAAe,QAAQ,OAAO,UAAU,UAAU,OAAO;EAC/D,MAAM,YAAY,QAAQ,OAAO,UAAU,OAAO,OAAO;EACzD,MAAM,YAAY,QAAQ,OAAO,UAAU,OAAO,OAAO;EACzD,MAAM,cAAc,QAAQ,OAAO,UAAU,SAAS,OAAO;EAC7D,MAAM,sBAAsB,QAAQ,OAAO,UAAU,iBAAiB,OAAO;AAC7E,UAAQ,IACN,mBAAmB,gBAAgB,UAAU,cAC9C;AACD,UAAQ,IACN,kBAAkB,eAAe,UAAU,cAC5C;AACD,UAAQ,IACN,eAAe,YAAY,UAAU,cACtC;AACD,UAAQ,IACN,eAAe,YAAY,UAAU,cACtC;AACD,UAAQ,IACN,iBAAiB,cAAc,UAAU,cAC1C;AACD,UAAQ,IACN,0BAA0B,sBAAsB,UAAU,cAC3D;;EAEH;AAEJ,QAAQ,OAAO"}
|
package/dist/config/schema.d.mts
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
-
|
|
46
|
-
if (existsSync(
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
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.
|
|
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"
|