memoryblock 0.1.4 → 0.1.5
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/README.md +73 -115
- package/bin/mblk.js +68 -71
- package/dist/commands/create.d.ts +2 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +48 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/commands/delete.d.ts +5 -0
- package/dist/commands/delete.d.ts.map +1 -0
- package/dist/commands/delete.js +147 -0
- package/dist/commands/delete.js.map +1 -0
- package/dist/commands/init.d.ts +9 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +209 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/permissions.d.ts +13 -0
- package/dist/commands/permissions.d.ts.map +1 -0
- package/dist/commands/permissions.js +60 -0
- package/dist/commands/permissions.js.map +1 -0
- package/dist/commands/plugin-settings.d.ts +6 -0
- package/dist/commands/plugin-settings.d.ts.map +1 -0
- package/dist/commands/plugin-settings.js +118 -0
- package/dist/commands/plugin-settings.js.map +1 -0
- package/dist/commands/plugins.d.ts +3 -0
- package/dist/commands/plugins.d.ts.map +1 -0
- package/dist/commands/plugins.js +83 -0
- package/dist/commands/plugins.js.map +1 -0
- package/dist/commands/reset.d.ts +8 -0
- package/dist/commands/reset.d.ts.map +1 -0
- package/dist/commands/reset.js +96 -0
- package/dist/commands/reset.js.map +1 -0
- package/dist/commands/server.d.ts +25 -0
- package/dist/commands/server.d.ts.map +1 -0
- package/dist/commands/server.js +295 -0
- package/dist/commands/server.js.map +1 -0
- package/dist/commands/service.d.ts +18 -0
- package/dist/commands/service.d.ts.map +1 -0
- package/dist/commands/service.js +309 -0
- package/dist/commands/service.js.map +1 -0
- package/dist/commands/start.d.ts +11 -0
- package/dist/commands/start.d.ts.map +1 -0
- package/dist/commands/start.js +794 -0
- package/dist/commands/start.js.map +1 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +78 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/stop.d.ts +9 -0
- package/dist/commands/stop.d.ts.map +1 -0
- package/dist/commands/stop.js +83 -0
- package/dist/commands/stop.js.map +1 -0
- package/dist/commands/web.d.ts +5 -0
- package/dist/commands/web.d.ts.map +1 -0
- package/dist/commands/web.js +63 -0
- package/dist/commands/web.js.map +1 -0
- package/dist/commands.d.ts +7 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +7 -0
- package/dist/commands.js.map +1 -0
- package/dist/constants.d.ts +41 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +81 -0
- package/dist/constants.js.map +1 -0
- package/dist/entry.d.ts +9 -0
- package/dist/entry.d.ts.map +1 -0
- package/dist/entry.js +345 -0
- package/dist/entry.js.map +1 -0
- package/package.json +32 -11
- package/dist/engine/agent.d.ts +0 -15
- package/dist/engine/agent.d.ts.map +0 -1
- package/dist/engine/agent.js +0 -19
- package/dist/engine/agent.js.map +0 -1
- package/dist/engine/conversation-log.d.ts +0 -35
- package/dist/engine/conversation-log.d.ts.map +0 -1
- package/dist/engine/conversation-log.js +0 -83
- package/dist/engine/conversation-log.js.map +0 -1
- package/dist/engine/cost-tracker.d.ts +0 -52
- package/dist/engine/cost-tracker.d.ts.map +0 -1
- package/dist/engine/cost-tracker.js +0 -110
- package/dist/engine/cost-tracker.js.map +0 -1
- package/dist/engine/gatekeeper.d.ts +0 -20
- package/dist/engine/gatekeeper.d.ts.map +0 -1
- package/dist/engine/gatekeeper.js +0 -43
- package/dist/engine/gatekeeper.js.map +0 -1
- package/dist/engine/memory.d.ts +0 -28
- package/dist/engine/memory.d.ts.map +0 -1
- package/dist/engine/memory.js +0 -69
- package/dist/engine/memory.js.map +0 -1
- package/dist/engine/monitor.d.ts +0 -81
- package/dist/engine/monitor.d.ts.map +0 -1
- package/dist/engine/monitor.js +0 -610
- package/dist/engine/monitor.js.map +0 -1
- package/dist/engine/prompts.d.ts +0 -31
- package/dist/engine/prompts.d.ts.map +0 -1
- package/dist/engine/prompts.js +0 -93
- package/dist/engine/prompts.js.map +0 -1
- package/dist/index.d.ts +0 -10
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -14
- package/dist/index.js.map +0 -1
- package/dist/utils/config.d.ts +0 -24
- package/dist/utils/config.d.ts.map +0 -1
- package/dist/utils/config.js +0 -86
- package/dist/utils/config.js.map +0 -1
- package/dist/utils/fs.d.ts +0 -18
- package/dist/utils/fs.d.ts.map +0 -1
- package/dist/utils/fs.js +0 -65
- package/dist/utils/fs.js.map +0 -1
- package/dist/utils/logger.d.ts +0 -12
- package/dist/utils/logger.d.ts.map +0 -1
- package/dist/utils/logger.js +0 -40
- package/dist/utils/logger.js.map +0 -1
|
@@ -0,0 +1,794 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { loadGlobalConfig, loadBlockConfig, loadAuth, resolveBlockPath, isInitialized, saveBlockConfig, resolveBlocksDir, loadPulseState, savePulseState, } from '@memoryblock/core';
|
|
4
|
+
import { t } from '@memoryblock/locale';
|
|
5
|
+
import { Monitor } from '@memoryblock/core';
|
|
6
|
+
import { promises as fsp } from 'node:fs';
|
|
7
|
+
import { pathExists } from '@memoryblock/core';
|
|
8
|
+
import { log } from '@memoryblock/core';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { PROVIDERS, PLUGINS } from '../constants.js';
|
|
11
|
+
// Use variable-based dynamic imports so TypeScript doesn't try to resolve
|
|
12
|
+
// these at compile time. They are runtime-only dependencies.
|
|
13
|
+
const ADAPTERS_PKG = '@memoryblock/adapters';
|
|
14
|
+
const TOOLS_PKG = '@memoryblock/tools';
|
|
15
|
+
const CHANNELS_PKG = '@memoryblock/channels';
|
|
16
|
+
const WEB_SEARCH_PKG = '@memoryblock/plugin-web-search';
|
|
17
|
+
const DAEMON_PKG = '@memoryblock/daemon';
|
|
18
|
+
const AGENTS_PKG = '@memoryblock/plugin-agents';
|
|
19
|
+
async function setupBlockRuntimeLogs(blockConfig, blockPath, auth, options, channelType) {
|
|
20
|
+
const model = blockConfig.adapter.model.split('.').pop()?.replace(/-v\d.*$/, '') || blockConfig.adapter.model;
|
|
21
|
+
if (options?.daemon) {
|
|
22
|
+
log.dim(` ${model} · daemon · ${blockConfig.tools.sandbox ? 'sandboxed' : 'unrestricted'}`);
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
log.dim(` ${model} · ${channelType} · ${blockConfig.tools.sandbox ? 'sandboxed' : 'unrestricted'}`);
|
|
26
|
+
console.log('');
|
|
27
|
+
// Initialize adapter
|
|
28
|
+
let adapter;
|
|
29
|
+
try {
|
|
30
|
+
const adapters = await import(ADAPTERS_PKG);
|
|
31
|
+
const provider = blockConfig.adapter.provider || 'bedrock';
|
|
32
|
+
if (provider === 'openai') {
|
|
33
|
+
adapter = new adapters.OpenAIAdapter({
|
|
34
|
+
model: blockConfig.adapter.model,
|
|
35
|
+
apiKey: auth.openai?.apiKey || process.env.OPENAI_API_KEY,
|
|
36
|
+
});
|
|
37
|
+
log.dim(` ✓ openai adapter`);
|
|
38
|
+
}
|
|
39
|
+
else if (provider === 'gemini') {
|
|
40
|
+
adapter = new adapters.GeminiAdapter({
|
|
41
|
+
model: blockConfig.adapter.model,
|
|
42
|
+
apiKey: auth.gemini?.apiKey || process.env.GEMINI_API_KEY,
|
|
43
|
+
});
|
|
44
|
+
log.dim(` ✓ gemini adapter`);
|
|
45
|
+
}
|
|
46
|
+
else if (provider === 'anthropic') {
|
|
47
|
+
adapter = new adapters.AnthropicAdapter({
|
|
48
|
+
model: blockConfig.adapter.model,
|
|
49
|
+
apiKey: auth.anthropic?.apiKey || process.env.ANTHROPIC_API_KEY,
|
|
50
|
+
});
|
|
51
|
+
log.dim(` ✓ anthropic adapter`);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
// Bedrock: pass credentials directly
|
|
55
|
+
adapter = new adapters.BedrockAdapter({
|
|
56
|
+
model: blockConfig.adapter.model,
|
|
57
|
+
region: blockConfig.adapter.region || auth.aws?.region || 'us-east-1',
|
|
58
|
+
maxTokens: blockConfig.adapter.maxTokens,
|
|
59
|
+
accessKeyId: auth.aws?.accessKeyId || process.env.AWS_ACCESS_KEY_ID,
|
|
60
|
+
secretAccessKey: auth.aws?.secretAccessKey || process.env.AWS_SECRET_ACCESS_KEY,
|
|
61
|
+
});
|
|
62
|
+
log.dim(` ✓ bedrock adapter`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
throw new Error(`Failed to load adapter: ${err.message}`);
|
|
67
|
+
}
|
|
68
|
+
// Initialize tool registry
|
|
69
|
+
let registry;
|
|
70
|
+
try {
|
|
71
|
+
const tools = await import(TOOLS_PKG);
|
|
72
|
+
registry = tools.createDefaultRegistry();
|
|
73
|
+
// Try loading web search plugin
|
|
74
|
+
try {
|
|
75
|
+
const webSearch = await import(WEB_SEARCH_PKG);
|
|
76
|
+
if (webSearch.tools) {
|
|
77
|
+
for (const tool of webSearch.tools) {
|
|
78
|
+
registry.register(tool);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
log.dim(` ✓ web search plugin`);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// Not installed — that's fine, it's optional
|
|
85
|
+
}
|
|
86
|
+
// Try loading agent orchestration plugin
|
|
87
|
+
try {
|
|
88
|
+
const agentsPlugin = await import(AGENTS_PKG);
|
|
89
|
+
if (agentsPlugin.tools) {
|
|
90
|
+
for (const tool of agentsPlugin.tools) {
|
|
91
|
+
registry.register(tool);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
log.dim(` ✓ agent orchestration plugin`);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// Not installed — that's fine, it's optional
|
|
98
|
+
}
|
|
99
|
+
log.dim(` ✓ ${registry.listTools().length} tools loaded`);
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
throw new Error(`Failed to load tools: ${err.message}`);
|
|
103
|
+
}
|
|
104
|
+
// Initialize channel(s)
|
|
105
|
+
let channel;
|
|
106
|
+
try {
|
|
107
|
+
const channels = await import(CHANNELS_PKG);
|
|
108
|
+
const activeChannels = [];
|
|
109
|
+
// Only bind CLI if TTY is available, otherwise background daemons will crash
|
|
110
|
+
if (process.stdout.isTTY && !options?.daemon) {
|
|
111
|
+
activeChannels.push(new channels.CLIChannel(blockConfig.name));
|
|
112
|
+
}
|
|
113
|
+
// Add Telegram if configured
|
|
114
|
+
const telegramToken = auth.telegram?.botToken;
|
|
115
|
+
if (telegramToken) {
|
|
116
|
+
const globalConfig = await loadGlobalConfig();
|
|
117
|
+
const chatId = blockConfig.channel.telegram?.chatId || auth.telegram?.chatId || '';
|
|
118
|
+
const enableAlerts = globalConfig.channelAlerts ?? true;
|
|
119
|
+
activeChannels.push(new channels.TelegramChannel(blockConfig.name, chatId, enableAlerts));
|
|
120
|
+
}
|
|
121
|
+
// Add WebChannel when: daemon mode, explicit 'web' or 'multi' channel, or block config says web
|
|
122
|
+
if (options?.daemon || channelType === 'web' || channelType === 'multi' ||
|
|
123
|
+
blockConfig.channel.type?.includes('web')) {
|
|
124
|
+
activeChannels.push(new channels.WebChannel(blockConfig.name, blockPath));
|
|
125
|
+
}
|
|
126
|
+
// Wrap them in the MultiChannelManager
|
|
127
|
+
channel = new channels.MultiChannelManager(activeChannels);
|
|
128
|
+
const names = activeChannels.map((c) => c.name).join(', ');
|
|
129
|
+
log.dim(` ✓ bound channels: ${names}`);
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
throw new Error(`Failed to load channel: ${err.message}`);
|
|
133
|
+
}
|
|
134
|
+
console.log('');
|
|
135
|
+
return { adapter, registry, channel };
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Attach a CLI readline to a running daemon instance.
|
|
139
|
+
* Instead of starting a new Monitor, we write messages to chat.json
|
|
140
|
+
* and watch for assistant replies — piggybacking on the WebChannel.
|
|
141
|
+
*/
|
|
142
|
+
async function attachCLIToRunningBlock(blockName, blockPath) {
|
|
143
|
+
const { createInterface, moveCursor, clearLine } = await import('node:readline');
|
|
144
|
+
const { watch } = await import('node:fs');
|
|
145
|
+
const chatFile = join(blockPath, 'chat.json');
|
|
146
|
+
const THEME = {
|
|
147
|
+
brand: chalk.hex('#7C3AED'),
|
|
148
|
+
brandBg: chalk.bgHex('#7C3AED').white.bold,
|
|
149
|
+
founderBg: chalk.bgHex('#1c64c8ff').white.bold,
|
|
150
|
+
system: chalk.hex('#6B7280'),
|
|
151
|
+
dim: chalk.dim,
|
|
152
|
+
};
|
|
153
|
+
// Load block config to get monitor name
|
|
154
|
+
const blockConfig = await loadBlockConfig(blockPath);
|
|
155
|
+
const monitorLabel = blockConfig.monitorEmoji
|
|
156
|
+
? `${blockConfig.monitorEmoji} ${blockConfig.monitorName || 'Monitor'}`
|
|
157
|
+
: blockConfig.monitorName || 'Monitor';
|
|
158
|
+
console.log(THEME.system(' ╭───────────────────────────────────────────────────╮'));
|
|
159
|
+
console.log(THEME.system(' │') + ' attached to running instance '
|
|
160
|
+
+ THEME.system(' │'));
|
|
161
|
+
console.log(THEME.system(' │') + ' type a message and press enter. ctrl+c to detach. '
|
|
162
|
+
+ THEME.system('│'));
|
|
163
|
+
console.log(THEME.system(' ╰───────────────────────────────────────────────────╯'));
|
|
164
|
+
console.log('');
|
|
165
|
+
// Track which messages we've already displayed
|
|
166
|
+
let lastKnownLength = 0;
|
|
167
|
+
try {
|
|
168
|
+
const raw = await fsp.readFile(chatFile, 'utf8');
|
|
169
|
+
const msgs = JSON.parse(raw);
|
|
170
|
+
lastKnownLength = msgs.length;
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// chat.json doesn't exist yet, that's fine
|
|
174
|
+
}
|
|
175
|
+
// Watch chat.json for new assistant responses
|
|
176
|
+
let debounceTimer = null;
|
|
177
|
+
const watcher = watch(blockPath, (_, filename) => {
|
|
178
|
+
if (filename === 'chat.json') {
|
|
179
|
+
if (debounceTimer)
|
|
180
|
+
clearTimeout(debounceTimer);
|
|
181
|
+
debounceTimer = setTimeout(async () => {
|
|
182
|
+
try {
|
|
183
|
+
const raw = await fsp.readFile(chatFile, 'utf8');
|
|
184
|
+
const msgs = JSON.parse(raw);
|
|
185
|
+
// Display any new messages
|
|
186
|
+
for (let i = lastKnownLength; i < msgs.length; i++) {
|
|
187
|
+
const m = msgs[i];
|
|
188
|
+
if (m.role === 'assistant' || (m.role === 'system' && !m.processed)) {
|
|
189
|
+
console.log('');
|
|
190
|
+
console.log(`${THEME.brandBg(` ${monitorLabel} `)} ${THEME.system(blockName)}`);
|
|
191
|
+
console.log('');
|
|
192
|
+
// Simple markdown formatting for terminal
|
|
193
|
+
const formatted = (m.content || '')
|
|
194
|
+
.replace(/\*\*(.*?)\*\*/g, (_, p1) => chalk.bold(p1))
|
|
195
|
+
.replace(/`([^`]+)`/g, (_, p1) => chalk.cyan(p1))
|
|
196
|
+
.replace(/_(.*?)_/g, (_, p1) => chalk.italic(p1));
|
|
197
|
+
console.log(formatted);
|
|
198
|
+
console.log('');
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
lastKnownLength = msgs.length;
|
|
202
|
+
}
|
|
203
|
+
catch { /* ignore read errors */ }
|
|
204
|
+
}, 300);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
// Also poll as fallback (FSEvents can miss cross-process writes)
|
|
208
|
+
const pollInterval = setInterval(async () => {
|
|
209
|
+
try {
|
|
210
|
+
const raw = await fsp.readFile(chatFile, 'utf8');
|
|
211
|
+
const msgs = JSON.parse(raw);
|
|
212
|
+
if (msgs.length > lastKnownLength) {
|
|
213
|
+
for (let i = lastKnownLength; i < msgs.length; i++) {
|
|
214
|
+
const m = msgs[i];
|
|
215
|
+
if (m.role === 'assistant') {
|
|
216
|
+
console.log('');
|
|
217
|
+
console.log(`${THEME.brandBg(` ${monitorLabel} `)} ${THEME.system(blockName)}`);
|
|
218
|
+
console.log('');
|
|
219
|
+
const formatted = (m.content || '')
|
|
220
|
+
.replace(/\*\*(.*?)\*\*/g, (_, p1) => chalk.bold(p1))
|
|
221
|
+
.replace(/`([^`]+)`/g, (_, p1) => chalk.cyan(p1))
|
|
222
|
+
.replace(/_(.*?)_/g, (_, p1) => chalk.italic(p1));
|
|
223
|
+
console.log(formatted);
|
|
224
|
+
console.log('');
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
lastKnownLength = msgs.length;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
catch { /* ignore */ }
|
|
231
|
+
}, 2000);
|
|
232
|
+
// Readline for user input
|
|
233
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
234
|
+
rl.on('line', async (line) => {
|
|
235
|
+
const content = line.trim();
|
|
236
|
+
if (!content)
|
|
237
|
+
return;
|
|
238
|
+
// Style the user input
|
|
239
|
+
moveCursor(process.stdout, 0, -1);
|
|
240
|
+
clearLine(process.stdout, 0);
|
|
241
|
+
console.log(`\n${THEME.founderBg(' Founder ')} ${content}`);
|
|
242
|
+
// Write to chat.json so the daemon's WebChannel picks it up
|
|
243
|
+
try {
|
|
244
|
+
let msgs = [];
|
|
245
|
+
try {
|
|
246
|
+
const raw = await fsp.readFile(chatFile, 'utf8');
|
|
247
|
+
msgs = JSON.parse(raw);
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
msgs = [];
|
|
251
|
+
}
|
|
252
|
+
msgs.push({
|
|
253
|
+
role: 'user',
|
|
254
|
+
content,
|
|
255
|
+
timestamp: new Date().toISOString(),
|
|
256
|
+
processed: false, // WebChannel will pick this up
|
|
257
|
+
});
|
|
258
|
+
lastKnownLength = msgs.length; // Don't re-display our own message
|
|
259
|
+
await fsp.writeFile(chatFile, JSON.stringify(msgs, null, 4), 'utf8');
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
console.error(THEME.system(` Failed to send: ${err.message}`));
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
rl.on('SIGINT', () => {
|
|
266
|
+
console.log(THEME.dim('\n Detached from running instance. Daemon continues in background.\n'));
|
|
267
|
+
watcher.close();
|
|
268
|
+
clearInterval(pollInterval);
|
|
269
|
+
if (debounceTimer)
|
|
270
|
+
clearTimeout(debounceTimer);
|
|
271
|
+
rl.close();
|
|
272
|
+
process.exit(0);
|
|
273
|
+
});
|
|
274
|
+
rl.on('close', () => {
|
|
275
|
+
watcher.close();
|
|
276
|
+
clearInterval(pollInterval);
|
|
277
|
+
if (debounceTimer)
|
|
278
|
+
clearTimeout(debounceTimer);
|
|
279
|
+
});
|
|
280
|
+
// Keep process alive
|
|
281
|
+
await new Promise(() => { }); // Block forever until Ctrl+C
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Model selection per provider.
|
|
285
|
+
* For API-based providers: fetch available models dynamically.
|
|
286
|
+
* Fallback: let user enter a model ID manually.
|
|
287
|
+
*/
|
|
288
|
+
async function selectModel(provider, auth) {
|
|
289
|
+
console.log('');
|
|
290
|
+
p.intro(chalk.bold('Model Selection'));
|
|
291
|
+
p.log.info(`Let's pick a model for the ${chalk.bold(provider)} provider.`);
|
|
292
|
+
if (provider === 'openai') {
|
|
293
|
+
// Try to fetch models from OpenAI API
|
|
294
|
+
const apiKey = auth.openai?.apiKey || process.env.OPENAI_API_KEY;
|
|
295
|
+
if (apiKey) {
|
|
296
|
+
try {
|
|
297
|
+
const s = p.spinner();
|
|
298
|
+
s.start('Fetching available OpenAI models...');
|
|
299
|
+
const res = await fetch('https://api.openai.com/v1/models', {
|
|
300
|
+
headers: { 'Authorization': `Bearer ${apiKey}` },
|
|
301
|
+
});
|
|
302
|
+
const data = await res.json();
|
|
303
|
+
s.stop('Models fetched.');
|
|
304
|
+
if (data.data && data.data.length > 0) {
|
|
305
|
+
// Filter to chat-capable models
|
|
306
|
+
const chatModels = data.data
|
|
307
|
+
.filter(m => m.id.includes('gpt') || m.id.includes('o1') || m.id.includes('o3') || m.id.includes('o4'))
|
|
308
|
+
.sort((a, b) => a.id.localeCompare(b.id))
|
|
309
|
+
.slice(0, 20);
|
|
310
|
+
if (chatModels.length > 0) {
|
|
311
|
+
const selected = await p.select({
|
|
312
|
+
message: 'Select an OpenAI model:',
|
|
313
|
+
options: [
|
|
314
|
+
...chatModels.map(m => ({ value: m.id, label: m.id })),
|
|
315
|
+
{ value: '_custom', label: 'Enter custom model ID...' },
|
|
316
|
+
],
|
|
317
|
+
});
|
|
318
|
+
if (p.isCancel(selected))
|
|
319
|
+
throw new Error('Model selection cancelled.');
|
|
320
|
+
if (selected !== '_custom')
|
|
321
|
+
return selected;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
// Fall through to manual entry
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
else if (provider === 'gemini') {
|
|
331
|
+
// Try to fetch models from Gemini API
|
|
332
|
+
const apiKey = auth.gemini?.apiKey || process.env.GEMINI_API_KEY;
|
|
333
|
+
if (apiKey) {
|
|
334
|
+
try {
|
|
335
|
+
const s = p.spinner();
|
|
336
|
+
s.start('Fetching available Gemini models...');
|
|
337
|
+
const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`);
|
|
338
|
+
const data = await res.json();
|
|
339
|
+
s.stop('Models fetched.');
|
|
340
|
+
if (data.models && data.models.length > 0) {
|
|
341
|
+
const chatModels = data.models
|
|
342
|
+
.filter(m => m.supportedGenerationMethods?.includes('generateContent'))
|
|
343
|
+
.filter(m => m.name.includes('gemini'))
|
|
344
|
+
.slice(0, 20);
|
|
345
|
+
if (chatModels.length > 0) {
|
|
346
|
+
const selected = await p.select({
|
|
347
|
+
message: 'Select a Gemini model:',
|
|
348
|
+
options: [
|
|
349
|
+
...chatModels.map(m => ({
|
|
350
|
+
value: m.name.replace('models/', ''),
|
|
351
|
+
label: m.displayName,
|
|
352
|
+
hint: m.name.replace('models/', ''),
|
|
353
|
+
})),
|
|
354
|
+
{ value: '_custom', label: 'Enter custom model ID...' },
|
|
355
|
+
],
|
|
356
|
+
});
|
|
357
|
+
if (p.isCancel(selected))
|
|
358
|
+
throw new Error('Model selection cancelled.');
|
|
359
|
+
if (selected !== '_custom')
|
|
360
|
+
return selected;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
// Fall through to manual entry
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
else if (provider === 'anthropic') {
|
|
370
|
+
// Anthropic doesn't have a public model listing endpoint
|
|
371
|
+
const selected = await p.select({
|
|
372
|
+
message: 'Select an Anthropic model:',
|
|
373
|
+
options: [
|
|
374
|
+
{ value: 'claude-sonnet-4-20250514', label: 'Claude Sonnet 4', hint: 'latest' },
|
|
375
|
+
{ value: 'claude-3-5-sonnet-20241022', label: 'Claude 3.5 Sonnet', hint: 'balanced' },
|
|
376
|
+
{ value: 'claude-3-5-haiku-20241022', label: 'Claude 3.5 Haiku', hint: 'fast & affordable' },
|
|
377
|
+
{ value: '_custom', label: 'Enter custom model ID...' },
|
|
378
|
+
],
|
|
379
|
+
});
|
|
380
|
+
if (p.isCancel(selected))
|
|
381
|
+
throw new Error('Model selection cancelled.');
|
|
382
|
+
if (selected !== '_custom')
|
|
383
|
+
return selected;
|
|
384
|
+
}
|
|
385
|
+
// Bedrock, Ollama, or custom fallback
|
|
386
|
+
const hint = provider === 'bedrock'
|
|
387
|
+
? 'e.g. us.anthropic.claude-sonnet-4-5-20250929-v1:0'
|
|
388
|
+
: provider === 'ollama'
|
|
389
|
+
? 'e.g. llama3, mistral, codellama'
|
|
390
|
+
: 'Enter model ID';
|
|
391
|
+
const modelId = await p.text({
|
|
392
|
+
message: `Enter the ${provider} model ID:`,
|
|
393
|
+
placeholder: hint,
|
|
394
|
+
validate: (v) => {
|
|
395
|
+
if (!v || !v.trim())
|
|
396
|
+
return 'Model ID is required.';
|
|
397
|
+
},
|
|
398
|
+
});
|
|
399
|
+
if (p.isCancel(modelId))
|
|
400
|
+
throw new Error('Model selection cancelled.');
|
|
401
|
+
return modelId.trim();
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Find existing blocks that have completed setup (have a monitor name + model configured).
|
|
405
|
+
* These are candidates for copying settings from.
|
|
406
|
+
*/
|
|
407
|
+
async function findConfiguredBlocks(globalConfig, currentBlockName) {
|
|
408
|
+
const blocksDir = resolveBlocksDir(globalConfig);
|
|
409
|
+
const configured = [];
|
|
410
|
+
try {
|
|
411
|
+
const dirs = await fsp.readdir(blocksDir);
|
|
412
|
+
for (const d of dirs) {
|
|
413
|
+
if (d === currentBlockName || d.startsWith('_') || d.startsWith('.'))
|
|
414
|
+
continue;
|
|
415
|
+
const bPath = join(blocksDir, d);
|
|
416
|
+
const isDir = await fsp.stat(bPath).then(s => s.isDirectory()).catch(() => false);
|
|
417
|
+
if (!isDir)
|
|
418
|
+
continue;
|
|
419
|
+
try {
|
|
420
|
+
const cfgRaw = await fsp.readFile(join(bPath, 'config.json'), 'utf8');
|
|
421
|
+
const cfg = JSON.parse(cfgRaw);
|
|
422
|
+
// Only show blocks that have completed setup: model is configured and monitor has a name
|
|
423
|
+
if (cfg.adapter?.model && cfg.monitorName) {
|
|
424
|
+
configured.push({
|
|
425
|
+
name: cfg.name || d,
|
|
426
|
+
provider: cfg.adapter?.provider || 'bedrock',
|
|
427
|
+
model: cfg.adapter?.model || '',
|
|
428
|
+
monitorName: cfg.monitorName,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
catch {
|
|
433
|
+
// Skip corrupted blocks
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
catch {
|
|
438
|
+
// blocks dir doesn't exist yet
|
|
439
|
+
}
|
|
440
|
+
return configured;
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Mini-onboarding flow for new blocks that haven't been configured yet.
|
|
444
|
+
* Runs when `mblk start <block>` is called and the block has no model set.
|
|
445
|
+
*
|
|
446
|
+
* Steps:
|
|
447
|
+
* 1. Offer to copy settings from an existing configured block (if any exist)
|
|
448
|
+
* 2. Select provider
|
|
449
|
+
* 3. Select model
|
|
450
|
+
* 4. Skills & Plugins setup
|
|
451
|
+
* 5. Save config
|
|
452
|
+
*/
|
|
453
|
+
async function miniOnboarding(blockConfig, blockPath, blockName, auth, globalConfig) {
|
|
454
|
+
console.log('');
|
|
455
|
+
log.banner();
|
|
456
|
+
p.intro(chalk.bold(`Block Setup — ${blockName}`));
|
|
457
|
+
p.log.info('This block needs to be configured before it can start.');
|
|
458
|
+
// ─── Step 0: Copy from existing block? ────────────────
|
|
459
|
+
const configuredBlocks = await findConfiguredBlocks(globalConfig, blockName);
|
|
460
|
+
if (configuredBlocks.length > 0) {
|
|
461
|
+
const copyChoice = await p.select({
|
|
462
|
+
message: 'How would you like to configure this block?',
|
|
463
|
+
options: [
|
|
464
|
+
...configuredBlocks.map(b => {
|
|
465
|
+
const shortModel = b.model.split('.').pop()?.replace(/-v\d.*$/, '') || b.model;
|
|
466
|
+
return {
|
|
467
|
+
value: b.name,
|
|
468
|
+
label: `Copy from "${b.name}"`,
|
|
469
|
+
// hint: `${b.monitorName || ''} · ${b.provider} / ${b.model.split('.').pop()?.replace(/-v\d.*$/, '') || b.model}`,
|
|
470
|
+
hint: `${b.provider} · ${shortModel}`,
|
|
471
|
+
};
|
|
472
|
+
}),
|
|
473
|
+
{ value: '_fresh', label: 'Start fresh', hint: 'choose provider, model, and skills' },
|
|
474
|
+
],
|
|
475
|
+
});
|
|
476
|
+
if (p.isCancel(copyChoice))
|
|
477
|
+
throw new Error('Setup cancelled.');
|
|
478
|
+
if (copyChoice !== '_fresh') {
|
|
479
|
+
// Copy config from the selected block
|
|
480
|
+
const sourceBlockPath = join(resolveBlocksDir(globalConfig), copyChoice);
|
|
481
|
+
try {
|
|
482
|
+
const sourceCfgRaw = await fsp.readFile(join(sourceBlockPath, 'config.json'), 'utf8');
|
|
483
|
+
const sourceCfg = JSON.parse(sourceCfgRaw);
|
|
484
|
+
// Copy adapter, memory, tools, permissions — but NOT name, monitorName, monitorEmoji, or channel
|
|
485
|
+
const copied = {
|
|
486
|
+
...blockConfig,
|
|
487
|
+
adapter: { ...sourceCfg.adapter },
|
|
488
|
+
memory: { ...sourceCfg.memory },
|
|
489
|
+
tools: { ...sourceCfg.tools },
|
|
490
|
+
permissions: { ...sourceCfg.permissions },
|
|
491
|
+
goals: [...(sourceCfg.goals || [])],
|
|
492
|
+
};
|
|
493
|
+
await saveBlockConfig(blockPath, copied);
|
|
494
|
+
p.log.success(`Copied settings from "${copyChoice}" — provider: ${copied.adapter.provider}, model: ${copied.adapter.model.split('.').pop()?.replace(/-v\d.*$/, '') || copied.adapter.model}`);
|
|
495
|
+
p.outro('Block configured. Starting...');
|
|
496
|
+
return copied;
|
|
497
|
+
}
|
|
498
|
+
catch {
|
|
499
|
+
p.log.warning(`Failed to copy from "${copyChoice}". Continuing with fresh setup.`);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
// ─── Step 1: Provider Selection ───────────────────────
|
|
504
|
+
const selectedProvider = await p.select({
|
|
505
|
+
message: 'Select your LLM provider:',
|
|
506
|
+
options: PROVIDERS,
|
|
507
|
+
});
|
|
508
|
+
if (p.isCancel(selectedProvider))
|
|
509
|
+
throw new Error('Setup cancelled.');
|
|
510
|
+
const provider = selectedProvider;
|
|
511
|
+
// ─── Step 2: Model Selection ──────────────────────────
|
|
512
|
+
const model = await selectModel(provider, auth);
|
|
513
|
+
// ─── Step 3: Skills & Plugins ─────────────────────────
|
|
514
|
+
p.log.step(chalk.bold('Skills & Plugins'));
|
|
515
|
+
p.log.info(`${chalk.green('✓')} Core tools (file ops, shell, dev) — always available`);
|
|
516
|
+
p.log.info(`${chalk.green('✓')} Multi-Agent Orchestration — always available`);
|
|
517
|
+
// Use the shared PLUGINS list, filtering to non-AWS plugins for block setup
|
|
518
|
+
const skillOptions = PLUGINS.filter(p => p.value !== 'aws');
|
|
519
|
+
let selectedSkills = [];
|
|
520
|
+
if (skillOptions.length > 0) {
|
|
521
|
+
selectedSkills = await p.multiselect({
|
|
522
|
+
message: 'Enable additional skills:',
|
|
523
|
+
options: skillOptions,
|
|
524
|
+
required: false,
|
|
525
|
+
});
|
|
526
|
+
if (p.isCancel(selectedSkills))
|
|
527
|
+
throw new Error('Setup cancelled.');
|
|
528
|
+
}
|
|
529
|
+
// Check which plugins are actually installed
|
|
530
|
+
const installedSkills = [];
|
|
531
|
+
for (const skill of selectedSkills) {
|
|
532
|
+
try {
|
|
533
|
+
await import(`@memoryblock/plugin-${skill}`);
|
|
534
|
+
installedSkills.push(skill);
|
|
535
|
+
}
|
|
536
|
+
catch {
|
|
537
|
+
p.log.warning(`Plugin "${skill}" is not installed. Run \`mblk add ${skill}\` to install.`);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
// ─── Step 4: Save Config ──────────────────────────────
|
|
541
|
+
const updated = {
|
|
542
|
+
...blockConfig,
|
|
543
|
+
adapter: { ...blockConfig.adapter, provider, model },
|
|
544
|
+
};
|
|
545
|
+
await saveBlockConfig(blockPath, updated);
|
|
546
|
+
p.outro('Block configured. Starting...');
|
|
547
|
+
return updated;
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Start all enabled blocks as daemons.
|
|
551
|
+
* Skips blocks that are unconfigured (no model) or already running.
|
|
552
|
+
* Used by `mblk start` (no args) and `mblk restart`.
|
|
553
|
+
*/
|
|
554
|
+
export async function startAllEnabledBlocks() {
|
|
555
|
+
const globalConfig = await loadGlobalConfig();
|
|
556
|
+
const blocksDir = resolveBlocksDir(globalConfig);
|
|
557
|
+
if (!(await pathExists(blocksDir))) {
|
|
558
|
+
log.dim(' No blocks directory found.');
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
const entries = await fsp.readdir(blocksDir, { withFileTypes: true });
|
|
562
|
+
let started = 0;
|
|
563
|
+
for (const entry of entries) {
|
|
564
|
+
if (!entry.isDirectory() || entry.name.startsWith('_') || entry.name.startsWith('.'))
|
|
565
|
+
continue;
|
|
566
|
+
const blockPath = join(blocksDir, entry.name);
|
|
567
|
+
try {
|
|
568
|
+
const blockConfig = await loadBlockConfig(blockPath);
|
|
569
|
+
// Skip disabled blocks
|
|
570
|
+
if (blockConfig.enabled === false) {
|
|
571
|
+
log.dim(` ${entry.name}: disabled (skipped)`);
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
// Skip unconfigured blocks (no model set)
|
|
575
|
+
if (!blockConfig.adapter?.model) {
|
|
576
|
+
log.dim(` ${entry.name}: not configured (skipped)`);
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
// Skip already running blocks
|
|
580
|
+
const pulse = await loadPulseState(blockPath);
|
|
581
|
+
if (pulse.status === 'ACTIVE') {
|
|
582
|
+
const lockFile = join(blockPath, '.lock');
|
|
583
|
+
try {
|
|
584
|
+
const pidStr = await fsp.readFile(lockFile, 'utf8');
|
|
585
|
+
const pid = parseInt(pidStr.trim(), 10);
|
|
586
|
+
try {
|
|
587
|
+
process.kill(pid, 0);
|
|
588
|
+
log.dim(` ${entry.name}: already running (PID ${pid})`);
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
catch { /* stale */ }
|
|
592
|
+
}
|
|
593
|
+
catch { /* no lock file */ }
|
|
594
|
+
}
|
|
595
|
+
// Start as daemon
|
|
596
|
+
await savePulseState(blockPath, {
|
|
597
|
+
status: 'SLEEPING',
|
|
598
|
+
lastRun: new Date().toISOString(),
|
|
599
|
+
nextWakeUp: null,
|
|
600
|
+
currentTask: null,
|
|
601
|
+
error: null,
|
|
602
|
+
});
|
|
603
|
+
const daemon = await import(DAEMON_PKG);
|
|
604
|
+
const pid = await daemon.spawnDaemon(blockConfig.name, 'multi', blockPath);
|
|
605
|
+
log.success(` ${entry.name}: started (PID ${pid})`);
|
|
606
|
+
started++;
|
|
607
|
+
}
|
|
608
|
+
catch (err) {
|
|
609
|
+
log.warn(` ${entry.name}: failed to start — ${err.message}`);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
if (started === 0) {
|
|
613
|
+
log.dim(' No enabled blocks to start.');
|
|
614
|
+
}
|
|
615
|
+
else {
|
|
616
|
+
log.dim(`\n ${started} block(s) started as daemons.`);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
export async function startCommand(blockName, options) {
|
|
620
|
+
if (!(await isInitialized())) {
|
|
621
|
+
throw new Error(t.general.notInitialized);
|
|
622
|
+
}
|
|
623
|
+
if (!blockName) {
|
|
624
|
+
log.brand('Starting all blocks...\n');
|
|
625
|
+
await startAllEnabledBlocks();
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
// Auto-install OS service hook quietly
|
|
629
|
+
import('./service.js').then(s => s.silentServiceInstall()).catch(() => { });
|
|
630
|
+
const globalConfig = await loadGlobalConfig();
|
|
631
|
+
const blockPath = resolveBlockPath(globalConfig, blockName);
|
|
632
|
+
if (!(await pathExists(blockPath))) {
|
|
633
|
+
log.warn(`Block "${blockName}" does not exist.`);
|
|
634
|
+
const createIt = await p.confirm({ message: `Would you like to create it now?` });
|
|
635
|
+
if (p.isCancel(createIt) || !createIt) {
|
|
636
|
+
process.exit(0);
|
|
637
|
+
}
|
|
638
|
+
const { createCommand } = await import('./create.js');
|
|
639
|
+
await createCommand(blockName);
|
|
640
|
+
}
|
|
641
|
+
let blockConfig = await loadBlockConfig(blockPath);
|
|
642
|
+
const auth = await loadAuth();
|
|
643
|
+
const channelType = options?.channel || blockConfig.channel.type || 'cli';
|
|
644
|
+
// ─── Single Instance Check ──────────────────────────────
|
|
645
|
+
const pulse = await loadPulseState(blockPath);
|
|
646
|
+
if (pulse.status === 'ACTIVE') {
|
|
647
|
+
// Check if the lock PID is still alive (stale lock from a crash)
|
|
648
|
+
const lockFile = join(blockPath, '.lock');
|
|
649
|
+
let stale = false;
|
|
650
|
+
try {
|
|
651
|
+
const pidStr = await fsp.readFile(lockFile, 'utf8');
|
|
652
|
+
const pid = parseInt(pidStr.trim(), 10);
|
|
653
|
+
if (pid && pid !== process.pid) {
|
|
654
|
+
try {
|
|
655
|
+
process.kill(pid, 0); // signal 0 = check if process exists
|
|
656
|
+
// Process is alive — block is genuinely running
|
|
657
|
+
}
|
|
658
|
+
catch {
|
|
659
|
+
// Process is dead — stale lock
|
|
660
|
+
stale = true;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
else if (!pid || isNaN(pid)) {
|
|
664
|
+
stale = true;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
catch {
|
|
668
|
+
// No lock file — might be stale from before locks existed
|
|
669
|
+
stale = true;
|
|
670
|
+
}
|
|
671
|
+
if (!stale) {
|
|
672
|
+
// Block is genuinely running — attach CLI to the running instance
|
|
673
|
+
if (process.stdout.isTTY && !options?.daemon) {
|
|
674
|
+
log.brand(`${blockName}\n`);
|
|
675
|
+
await setupBlockRuntimeLogs(blockConfig, blockPath, auth, options, channelType);
|
|
676
|
+
log.dim(' Block is already running. Attaching CLI to existing instance...\n');
|
|
677
|
+
await attachCLIToRunningBlock(blockName, blockPath);
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
log.error(t.block.alreadyRunning(blockName));
|
|
681
|
+
log.dim(` ${t.block.singleInstanceHint}`);
|
|
682
|
+
log.dim(` ${t.block.stopHint(blockName)}\n`);
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
// Stale lock — clean it up and continue
|
|
686
|
+
log.dim(` ${t.block.staleLockRecovered}`);
|
|
687
|
+
try {
|
|
688
|
+
await fsp.unlink(join(blockPath, '.lock'));
|
|
689
|
+
}
|
|
690
|
+
catch { /* ignore */ }
|
|
691
|
+
}
|
|
692
|
+
// Write lock file with our PID — but NOT when spawning a daemon,
|
|
693
|
+
// because the daemon child process will write its own lock.
|
|
694
|
+
if (!options?.daemon) {
|
|
695
|
+
await fsp.writeFile(join(blockPath, '.lock'), String(process.pid), 'utf8');
|
|
696
|
+
}
|
|
697
|
+
// ─── Mini-Onboarding (if block has no model configured) ─────
|
|
698
|
+
if (!blockConfig.adapter.model) {
|
|
699
|
+
// Daemon / non-TTY mode cannot run interactive onboarding
|
|
700
|
+
if (!process.stdout.isTTY || options?.daemon) {
|
|
701
|
+
throw new Error(`Block "${blockName}" has no model configured. ` +
|
|
702
|
+
`Run \`mblk start ${blockName}\` in a terminal first to complete setup.`);
|
|
703
|
+
}
|
|
704
|
+
blockConfig = await miniOnboarding(blockConfig, blockPath, blockName, auth, globalConfig);
|
|
705
|
+
}
|
|
706
|
+
// Mark block as enabled (persists across reboots)
|
|
707
|
+
blockConfig.enabled = true;
|
|
708
|
+
await saveBlockConfig(blockPath, blockConfig);
|
|
709
|
+
// ─── AM I THE DAEMON CHILD? ───────────────────────────
|
|
710
|
+
if (process.env.MBLK_IS_DAEMON === '1') {
|
|
711
|
+
let shuttingDown = false;
|
|
712
|
+
let monitor = null;
|
|
713
|
+
const shutdown = async () => {
|
|
714
|
+
if (shuttingDown)
|
|
715
|
+
return;
|
|
716
|
+
shuttingDown = true;
|
|
717
|
+
if (monitor) {
|
|
718
|
+
try {
|
|
719
|
+
await monitor.stop();
|
|
720
|
+
}
|
|
721
|
+
catch { /* ignore */ }
|
|
722
|
+
}
|
|
723
|
+
try {
|
|
724
|
+
const lockFile = join(blockPath, '.lock');
|
|
725
|
+
const pidStr = await fsp.readFile(lockFile, 'utf8');
|
|
726
|
+
if (Number(pidStr.trim()) === process.pid) {
|
|
727
|
+
await fsp.unlink(lockFile);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
catch { /* ignore */ }
|
|
731
|
+
process.exit(0);
|
|
732
|
+
};
|
|
733
|
+
process.on('SIGINT', shutdown);
|
|
734
|
+
process.on('SIGTERM', shutdown);
|
|
735
|
+
process.on('uncaughtException', async (err) => {
|
|
736
|
+
if (shuttingDown)
|
|
737
|
+
return;
|
|
738
|
+
log.error(t.errors.unexpected(err.message));
|
|
739
|
+
await fsp.writeFile(join(blockPath, 'daemon-debug-error.log'), err.stack || err.message);
|
|
740
|
+
await shutdown();
|
|
741
|
+
});
|
|
742
|
+
process.on('unhandledRejection', async (reason) => {
|
|
743
|
+
if (shuttingDown)
|
|
744
|
+
return;
|
|
745
|
+
log.error(t.errors.unexpected(String(reason)));
|
|
746
|
+
const stack = reason?.stack || String(reason);
|
|
747
|
+
await fsp.writeFile(join(blockPath, 'daemon-debug-error.log'), stack);
|
|
748
|
+
await shutdown();
|
|
749
|
+
});
|
|
750
|
+
try {
|
|
751
|
+
const { adapter, registry, channel } = await setupBlockRuntimeLogs(blockConfig, blockPath, auth, options, channelType);
|
|
752
|
+
if (!adapter || !registry || !channel)
|
|
753
|
+
return;
|
|
754
|
+
// Create and start the monitor in the background
|
|
755
|
+
monitor = new Monitor({ blockPath, blockConfig, adapter, registry, channel });
|
|
756
|
+
await monitor.start();
|
|
757
|
+
}
|
|
758
|
+
catch (err) {
|
|
759
|
+
log.error(`Daemon init failed: ${err.message}`);
|
|
760
|
+
await fsp.writeFile(join(blockPath, 'daemon-debug-error.log'), err.stack || err.message);
|
|
761
|
+
process.exit(1);
|
|
762
|
+
}
|
|
763
|
+
return; // The daemon runs indefinitely here
|
|
764
|
+
}
|
|
765
|
+
// ─── I AM THE PARENT CLI ──────────────────────────────
|
|
766
|
+
try {
|
|
767
|
+
// Reset pulse so the daemon child doesn't see stale ACTIVE status
|
|
768
|
+
await savePulseState(blockPath, {
|
|
769
|
+
status: 'SLEEPING',
|
|
770
|
+
lastRun: new Date().toISOString(),
|
|
771
|
+
nextWakeUp: null,
|
|
772
|
+
currentTask: null,
|
|
773
|
+
error: null,
|
|
774
|
+
});
|
|
775
|
+
const daemon = await import(DAEMON_PKG);
|
|
776
|
+
const pid = await daemon.spawnDaemon(blockConfig.name, channelType, blockPath);
|
|
777
|
+
if (options?.daemon) {
|
|
778
|
+
log.brand(`${blockConfig.name}\n`);
|
|
779
|
+
log.success(`Daemon spawned successfully! PID: ${pid}`);
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
// Always background the daemon, then attach CLI natively
|
|
783
|
+
log.brand(`${blockConfig.name}\n`);
|
|
784
|
+
log.success(`Daemon spawned (PID ${pid}). Attaching CLI...\n`);
|
|
785
|
+
// Let daemon init chat.json and WebChannel before tailing
|
|
786
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
787
|
+
await setupBlockRuntimeLogs(blockConfig, blockPath, auth, options, channelType);
|
|
788
|
+
await attachCLIToRunningBlock(blockName, blockPath);
|
|
789
|
+
}
|
|
790
|
+
catch (err) {
|
|
791
|
+
throw new Error(`Failed to spawn daemon: ${err.message}`);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
//# sourceMappingURL=start.js.map
|