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.
Files changed (111) hide show
  1. package/README.md +73 -115
  2. package/bin/mblk.js +68 -71
  3. package/dist/commands/create.d.ts +2 -0
  4. package/dist/commands/create.d.ts.map +1 -0
  5. package/dist/commands/create.js +48 -0
  6. package/dist/commands/create.js.map +1 -0
  7. package/dist/commands/delete.d.ts +5 -0
  8. package/dist/commands/delete.d.ts.map +1 -0
  9. package/dist/commands/delete.js +147 -0
  10. package/dist/commands/delete.js.map +1 -0
  11. package/dist/commands/init.d.ts +9 -0
  12. package/dist/commands/init.d.ts.map +1 -0
  13. package/dist/commands/init.js +209 -0
  14. package/dist/commands/init.js.map +1 -0
  15. package/dist/commands/permissions.d.ts +13 -0
  16. package/dist/commands/permissions.d.ts.map +1 -0
  17. package/dist/commands/permissions.js +60 -0
  18. package/dist/commands/permissions.js.map +1 -0
  19. package/dist/commands/plugin-settings.d.ts +6 -0
  20. package/dist/commands/plugin-settings.d.ts.map +1 -0
  21. package/dist/commands/plugin-settings.js +118 -0
  22. package/dist/commands/plugin-settings.js.map +1 -0
  23. package/dist/commands/plugins.d.ts +3 -0
  24. package/dist/commands/plugins.d.ts.map +1 -0
  25. package/dist/commands/plugins.js +83 -0
  26. package/dist/commands/plugins.js.map +1 -0
  27. package/dist/commands/reset.d.ts +8 -0
  28. package/dist/commands/reset.d.ts.map +1 -0
  29. package/dist/commands/reset.js +96 -0
  30. package/dist/commands/reset.js.map +1 -0
  31. package/dist/commands/server.d.ts +25 -0
  32. package/dist/commands/server.d.ts.map +1 -0
  33. package/dist/commands/server.js +295 -0
  34. package/dist/commands/server.js.map +1 -0
  35. package/dist/commands/service.d.ts +18 -0
  36. package/dist/commands/service.d.ts.map +1 -0
  37. package/dist/commands/service.js +309 -0
  38. package/dist/commands/service.js.map +1 -0
  39. package/dist/commands/start.d.ts +11 -0
  40. package/dist/commands/start.d.ts.map +1 -0
  41. package/dist/commands/start.js +794 -0
  42. package/dist/commands/start.js.map +1 -0
  43. package/dist/commands/status.d.ts +2 -0
  44. package/dist/commands/status.d.ts.map +1 -0
  45. package/dist/commands/status.js +78 -0
  46. package/dist/commands/status.js.map +1 -0
  47. package/dist/commands/stop.d.ts +9 -0
  48. package/dist/commands/stop.d.ts.map +1 -0
  49. package/dist/commands/stop.js +83 -0
  50. package/dist/commands/stop.js.map +1 -0
  51. package/dist/commands/web.d.ts +5 -0
  52. package/dist/commands/web.d.ts.map +1 -0
  53. package/dist/commands/web.js +63 -0
  54. package/dist/commands/web.js.map +1 -0
  55. package/dist/commands.d.ts +7 -0
  56. package/dist/commands.d.ts.map +1 -0
  57. package/dist/commands.js +7 -0
  58. package/dist/commands.js.map +1 -0
  59. package/dist/constants.d.ts +41 -0
  60. package/dist/constants.d.ts.map +1 -0
  61. package/dist/constants.js +81 -0
  62. package/dist/constants.js.map +1 -0
  63. package/dist/entry.d.ts +9 -0
  64. package/dist/entry.d.ts.map +1 -0
  65. package/dist/entry.js +345 -0
  66. package/dist/entry.js.map +1 -0
  67. package/package.json +32 -11
  68. package/dist/engine/agent.d.ts +0 -15
  69. package/dist/engine/agent.d.ts.map +0 -1
  70. package/dist/engine/agent.js +0 -19
  71. package/dist/engine/agent.js.map +0 -1
  72. package/dist/engine/conversation-log.d.ts +0 -35
  73. package/dist/engine/conversation-log.d.ts.map +0 -1
  74. package/dist/engine/conversation-log.js +0 -83
  75. package/dist/engine/conversation-log.js.map +0 -1
  76. package/dist/engine/cost-tracker.d.ts +0 -52
  77. package/dist/engine/cost-tracker.d.ts.map +0 -1
  78. package/dist/engine/cost-tracker.js +0 -110
  79. package/dist/engine/cost-tracker.js.map +0 -1
  80. package/dist/engine/gatekeeper.d.ts +0 -20
  81. package/dist/engine/gatekeeper.d.ts.map +0 -1
  82. package/dist/engine/gatekeeper.js +0 -43
  83. package/dist/engine/gatekeeper.js.map +0 -1
  84. package/dist/engine/memory.d.ts +0 -28
  85. package/dist/engine/memory.d.ts.map +0 -1
  86. package/dist/engine/memory.js +0 -69
  87. package/dist/engine/memory.js.map +0 -1
  88. package/dist/engine/monitor.d.ts +0 -81
  89. package/dist/engine/monitor.d.ts.map +0 -1
  90. package/dist/engine/monitor.js +0 -610
  91. package/dist/engine/monitor.js.map +0 -1
  92. package/dist/engine/prompts.d.ts +0 -31
  93. package/dist/engine/prompts.d.ts.map +0 -1
  94. package/dist/engine/prompts.js +0 -93
  95. package/dist/engine/prompts.js.map +0 -1
  96. package/dist/index.d.ts +0 -10
  97. package/dist/index.d.ts.map +0 -1
  98. package/dist/index.js +0 -14
  99. package/dist/index.js.map +0 -1
  100. package/dist/utils/config.d.ts +0 -24
  101. package/dist/utils/config.d.ts.map +0 -1
  102. package/dist/utils/config.js +0 -86
  103. package/dist/utils/config.js.map +0 -1
  104. package/dist/utils/fs.d.ts +0 -18
  105. package/dist/utils/fs.d.ts.map +0 -1
  106. package/dist/utils/fs.js +0 -65
  107. package/dist/utils/fs.js.map +0 -1
  108. package/dist/utils/logger.d.ts +0 -12
  109. package/dist/utils/logger.d.ts.map +0 -1
  110. package/dist/utils/logger.js +0 -40
  111. 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