memoryblock 0.0.1 → 0.1.0-beta

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 (144) hide show
  1. package/LICENSE +21 -0
  2. package/bin/mblk.js +6 -0
  3. package/dist/bin/mblk.d.ts +3 -0
  4. package/dist/bin/mblk.d.ts.map +1 -0
  5. package/dist/bin/mblk.js +339 -0
  6. package/dist/bin/mblk.js.map +1 -0
  7. package/dist/cli/commands/create.d.ts +2 -0
  8. package/dist/cli/commands/create.d.ts.map +1 -0
  9. package/dist/cli/commands/create.js +48 -0
  10. package/dist/cli/commands/create.js.map +1 -0
  11. package/dist/cli/commands/delete.d.ts +5 -0
  12. package/dist/cli/commands/delete.d.ts.map +1 -0
  13. package/dist/cli/commands/delete.js +147 -0
  14. package/dist/cli/commands/delete.js.map +1 -0
  15. package/dist/cli/commands/init.d.ts +9 -0
  16. package/dist/cli/commands/init.d.ts.map +1 -0
  17. package/dist/cli/commands/init.js +209 -0
  18. package/dist/cli/commands/init.js.map +1 -0
  19. package/dist/cli/commands/permissions.d.ts +13 -0
  20. package/dist/cli/commands/permissions.d.ts.map +1 -0
  21. package/dist/cli/commands/permissions.js +60 -0
  22. package/dist/cli/commands/permissions.js.map +1 -0
  23. package/dist/cli/commands/plugin-settings.d.ts +6 -0
  24. package/dist/cli/commands/plugin-settings.d.ts.map +1 -0
  25. package/dist/cli/commands/plugin-settings.js +118 -0
  26. package/dist/cli/commands/plugin-settings.js.map +1 -0
  27. package/dist/cli/commands/plugins.d.ts +3 -0
  28. package/dist/cli/commands/plugins.d.ts.map +1 -0
  29. package/dist/cli/commands/plugins.js +83 -0
  30. package/dist/cli/commands/plugins.js.map +1 -0
  31. package/dist/cli/commands/reset.d.ts +8 -0
  32. package/dist/cli/commands/reset.d.ts.map +1 -0
  33. package/dist/cli/commands/reset.js +96 -0
  34. package/dist/cli/commands/reset.js.map +1 -0
  35. package/dist/cli/commands/server.d.ts +25 -0
  36. package/dist/cli/commands/server.d.ts.map +1 -0
  37. package/dist/cli/commands/server.js +295 -0
  38. package/dist/cli/commands/server.js.map +1 -0
  39. package/dist/cli/commands/service.d.ts +18 -0
  40. package/dist/cli/commands/service.d.ts.map +1 -0
  41. package/dist/cli/commands/service.js +309 -0
  42. package/dist/cli/commands/service.js.map +1 -0
  43. package/dist/cli/commands/start.d.ts +11 -0
  44. package/dist/cli/commands/start.d.ts.map +1 -0
  45. package/dist/cli/commands/start.js +801 -0
  46. package/dist/cli/commands/start.js.map +1 -0
  47. package/dist/cli/commands/status.d.ts +2 -0
  48. package/dist/cli/commands/status.d.ts.map +1 -0
  49. package/dist/cli/commands/status.js +78 -0
  50. package/dist/cli/commands/status.js.map +1 -0
  51. package/dist/cli/commands/stop.d.ts +9 -0
  52. package/dist/cli/commands/stop.d.ts.map +1 -0
  53. package/dist/cli/commands/stop.js +83 -0
  54. package/dist/cli/commands/stop.js.map +1 -0
  55. package/dist/cli/commands/web.d.ts +5 -0
  56. package/dist/cli/commands/web.d.ts.map +1 -0
  57. package/dist/cli/commands/web.js +63 -0
  58. package/dist/cli/commands/web.js.map +1 -0
  59. package/dist/cli/constants.d.ts +38 -0
  60. package/dist/cli/constants.d.ts.map +1 -0
  61. package/dist/cli/constants.js +80 -0
  62. package/dist/cli/constants.js.map +1 -0
  63. package/dist/cli/logger.d.ts +12 -0
  64. package/dist/cli/logger.d.ts.map +1 -0
  65. package/dist/cli/logger.js +40 -0
  66. package/dist/cli/logger.js.map +1 -0
  67. package/dist/engine/agent.d.ts +15 -0
  68. package/dist/engine/agent.d.ts.map +1 -0
  69. package/dist/engine/agent.js +19 -0
  70. package/dist/engine/agent.js.map +1 -0
  71. package/dist/engine/conversation-log.d.ts +35 -0
  72. package/dist/engine/conversation-log.d.ts.map +1 -0
  73. package/dist/engine/conversation-log.js +83 -0
  74. package/dist/engine/conversation-log.js.map +1 -0
  75. package/dist/engine/cost-tracker.d.ts +52 -0
  76. package/dist/engine/cost-tracker.d.ts.map +1 -0
  77. package/dist/engine/cost-tracker.js +110 -0
  78. package/dist/engine/cost-tracker.js.map +1 -0
  79. package/dist/engine/gatekeeper.d.ts +20 -0
  80. package/dist/engine/gatekeeper.d.ts.map +1 -0
  81. package/dist/engine/gatekeeper.js +43 -0
  82. package/dist/engine/gatekeeper.js.map +1 -0
  83. package/dist/engine/memory.d.ts +28 -0
  84. package/dist/engine/memory.d.ts.map +1 -0
  85. package/dist/engine/memory.js +69 -0
  86. package/dist/engine/memory.js.map +1 -0
  87. package/dist/engine/monitor.d.ts +81 -0
  88. package/dist/engine/monitor.d.ts.map +1 -0
  89. package/dist/engine/monitor.js +610 -0
  90. package/dist/engine/monitor.js.map +1 -0
  91. package/dist/engine/prompts.d.ts +31 -0
  92. package/dist/engine/prompts.d.ts.map +1 -0
  93. package/dist/engine/prompts.js +93 -0
  94. package/dist/engine/prompts.js.map +1 -0
  95. package/dist/index.d.ts +12 -0
  96. package/dist/index.d.ts.map +1 -0
  97. package/dist/index.js +15 -0
  98. package/dist/index.js.map +1 -0
  99. package/dist/schemas.d.ts +544 -0
  100. package/dist/schemas.d.ts.map +1 -0
  101. package/dist/schemas.js +111 -0
  102. package/dist/schemas.js.map +1 -0
  103. package/dist/types.d.ts +188 -0
  104. package/dist/types.d.ts.map +1 -0
  105. package/dist/types.js +2 -0
  106. package/dist/types.js.map +1 -0
  107. package/dist/utils/config.d.ts +24 -0
  108. package/dist/utils/config.d.ts.map +1 -0
  109. package/dist/utils/config.js +86 -0
  110. package/dist/utils/config.js.map +1 -0
  111. package/dist/utils/fs.d.ts +18 -0
  112. package/dist/utils/fs.d.ts.map +1 -0
  113. package/dist/utils/fs.js +65 -0
  114. package/dist/utils/fs.js.map +1 -0
  115. package/package.json +37 -8
  116. package/src/bin/mblk.ts +347 -0
  117. package/src/cli/commands/create.ts +62 -0
  118. package/src/cli/commands/delete.ts +165 -0
  119. package/src/cli/commands/init.ts +241 -0
  120. package/src/cli/commands/permissions.ts +77 -0
  121. package/src/cli/commands/plugin-settings.ts +111 -0
  122. package/src/cli/commands/plugins.ts +89 -0
  123. package/src/cli/commands/reset.ts +97 -0
  124. package/src/cli/commands/server.ts +327 -0
  125. package/src/cli/commands/service.ts +300 -0
  126. package/src/cli/commands/start.ts +843 -0
  127. package/src/cli/commands/status.ts +90 -0
  128. package/src/cli/commands/stop.ts +91 -0
  129. package/src/cli/commands/web.ts +74 -0
  130. package/src/cli/constants.ts +88 -0
  131. package/src/cli/logger.ts +48 -0
  132. package/src/engine/agent.ts +19 -0
  133. package/src/engine/conversation-log.ts +90 -0
  134. package/src/engine/cost-tracker.ts +134 -0
  135. package/src/engine/gatekeeper.ts +53 -0
  136. package/src/engine/memory.ts +87 -0
  137. package/src/engine/monitor.ts +719 -0
  138. package/src/engine/prompts.ts +102 -0
  139. package/src/index.ts +88 -0
  140. package/src/schemas.ts +126 -0
  141. package/src/types.ts +220 -0
  142. package/src/utils/config.ts +106 -0
  143. package/src/utils/fs.ts +64 -0
  144. package/tsconfig.json +10 -0
@@ -0,0 +1,241 @@
1
+ import * as p from '@clack/prompts';
2
+ import chalk from 'chalk';
3
+ import { ensureDir, writeJson } from '../../utils/fs.js';
4
+ import {
5
+ getWsRoot, getConfigPath, getAuthPath, isInitialized,
6
+ } from '../../utils/config.js';
7
+ import { GlobalConfigSchema } from '../../schemas.js';
8
+ import { log } from '../logger.js';
9
+
10
+ import { PROVIDERS, CHANNELS, PLUGINS, PROVIDER_AUTH, CHANNEL_AUTH } from '../constants.js';
11
+
12
+ // Brand accent
13
+ const ACCENT = chalk.hex('#805AD5');
14
+ const DIM = chalk.dim;
15
+
16
+ /**
17
+ * Interactive onboarding wizard.
18
+ * Multi-step setup: providers → channels → plugins → API keys → first block.
19
+ * Uses @clack/prompts for styled terminal UI.
20
+ */
21
+ export async function initCommand(options?: { nonInteractive?: boolean }): Promise<void> {
22
+ const wsDir = getWsRoot();
23
+
24
+ // Non-interactive: create defaults and exit
25
+ if (options?.nonInteractive) {
26
+ await ensureDir(wsDir);
27
+ const defaultConfig = GlobalConfigSchema.parse({
28
+ blocksDir: './blocks'
29
+ });
30
+ await writeJson(getConfigPath(), defaultConfig);
31
+ await writeJson(getAuthPath(), {
32
+ aws: { accessKeyId: '', secretAccessKey: '', region: 'us-east-1' },
33
+ anthropic: { apiKey: '' },
34
+ openai: { apiKey: '' },
35
+ gemini: { apiKey: '' },
36
+ telegram: { botToken: '', chatId: '' },
37
+ brave: { apiKey: '' }
38
+ });
39
+ log.brand('Initialized (non-interactive)');
40
+ log.success(`Workspace: ${wsDir}`);
41
+ log.success(`Config: ${getConfigPath()}`);
42
+ return;
43
+ }
44
+
45
+ const alreadyInit = await isInitialized();
46
+
47
+ // Welcome
48
+ console.log('');
49
+ log.banner();
50
+ console.log(DIM(' Deploy isolated AI workspaces on your machine.\n'));
51
+
52
+ p.intro(chalk.bold('Setup Wizard'));
53
+
54
+ if (alreadyInit) {
55
+ const proceed = await p.confirm({
56
+ message: 'Already configured globally. Re-run setup?',
57
+ initialValue: false,
58
+ });
59
+ if (p.isCancel(proceed) || !proceed) {
60
+ p.outro('Setup cancelled.');
61
+ return;
62
+ }
63
+ }
64
+
65
+ // ─── Step 1: Providers ───────────────────────────────
66
+ const selectedProviders = await p.multiselect({
67
+ message: 'Which providers do you want to use?',
68
+ options: PROVIDERS,
69
+ initialValues: ['bedrock'],
70
+ required: true,
71
+ });
72
+
73
+ if (p.isCancel(selectedProviders)) {
74
+ p.outro('Setup cancelled.');
75
+ return;
76
+ }
77
+
78
+ // ─── Step 2: Channels ───────────────────────────────
79
+ const selectedChannels = await p.multiselect({
80
+ message: 'Which channels do you want to enable?',
81
+ options: CHANNELS.map(ch => ({
82
+ ...ch,
83
+ label: (ch.value === 'cli' || ch.value === 'web') ? `${ch.label} (always on)` : ch.label,
84
+ })),
85
+ initialValues: ['cli', 'web'],
86
+ required: true,
87
+ });
88
+
89
+ if (p.isCancel(selectedChannels)) {
90
+ p.outro('Setup cancelled.');
91
+ return;
92
+ }
93
+
94
+ // ─── Step 3: Skills & Plugins ───────────────────────
95
+ p.log.info(`${chalk.green('✓')} Core skills (file ops, shell, dev) — always available`);
96
+ p.log.info(`${chalk.green('✓')} Multi-Agent Orchestration — always available`);
97
+
98
+ let selectedPlugins: symbol | string[] = [];
99
+ if (PLUGINS.length > 0) {
100
+ selectedPlugins = await p.multiselect({
101
+ message: 'Select additional skills & plugins to install:',
102
+ options: PLUGINS.map(pl => ({
103
+ value: pl.value,
104
+ label: pl.label,
105
+ hint: pl.hint,
106
+ })),
107
+ required: false,
108
+ });
109
+
110
+ if (p.isCancel(selectedPlugins)) {
111
+ p.outro('Setup cancelled.');
112
+ return;
113
+ }
114
+ }
115
+
116
+ // ─── Step 4: API Keys ───────────────────────────────
117
+ const authData: Record<string, Record<string, string>> = {};
118
+
119
+ for (const provider of (selectedProviders as string[])) {
120
+ const providerAuth = PROVIDER_AUTH[provider];
121
+ if (!providerAuth || providerAuth.fields.length === 0) continue;
122
+
123
+ const providerLabel = PROVIDERS.find(p => p.value === provider)?.label || provider;
124
+ p.log.step(`${providerLabel} credentials`);
125
+
126
+ const data: Record<string, string> = {};
127
+ for (const field of providerAuth.fields) {
128
+ const value = await p.text({
129
+ message: field.label,
130
+ placeholder: field.key === 'region' ? 'us-east-1' : '',
131
+ defaultValue: field.key === 'region' ? 'us-east-1' : undefined,
132
+ });
133
+
134
+ if (p.isCancel(value)) {
135
+ p.outro('Setup cancelled.');
136
+ return;
137
+ }
138
+
139
+ data[field.key] = (value as string) || '';
140
+ }
141
+ authData[provider === 'bedrock' ? 'aws' : provider] = data;
142
+ }
143
+
144
+ // Channel auth (only telegram has auth currently)
145
+ for (const channel of (selectedChannels as string[])) {
146
+ const fields = CHANNEL_AUTH[channel];
147
+ if (!fields) continue;
148
+
149
+ const channelLabel = CHANNELS.find(c => c.value === channel)?.label || channel;
150
+ p.log.step(`${channelLabel} credentials`);
151
+
152
+ const data: Record<string, string> = {};
153
+ for (const field of fields) {
154
+ const value = await p.text({
155
+ message: field.label,
156
+ placeholder: field.secret ? '' : 'optional',
157
+ });
158
+
159
+ if (p.isCancel(value)) {
160
+ p.outro('Setup cancelled.');
161
+ return;
162
+ }
163
+
164
+ data[field.key] = (value as string) || '';
165
+ }
166
+ authData[channel] = data;
167
+ }
168
+
169
+ // ─── Step 5: Connection Testing ─────────────────────
170
+ const s = p.spinner();
171
+ const results: Array<{ name: string; ok: boolean }> = [];
172
+
173
+ // Test Bedrock
174
+ if (authData.aws?.accessKeyId) {
175
+ s.start('Testing Bedrock connection...');
176
+ try {
177
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
178
+ // @ts-ignore — dynamic import, may not be installed
179
+ const { BedrockRuntimeClient, ConverseCommand } = await import('@aws-sdk/client-bedrock-runtime');
180
+ const client = new BedrockRuntimeClient({
181
+ region: authData.aws.region || 'us-east-1',
182
+ credentials: { accessKeyId: authData.aws.accessKeyId, secretAccessKey: authData.aws.secretAccessKey },
183
+ });
184
+ await client.send(new ConverseCommand({
185
+ modelId: 'anthropic.claude-3-haiku-20240307-v1:0',
186
+ messages: [{ role: 'user', content: [{ text: 'hi' }] }],
187
+ inferenceConfig: { maxTokens: 1 },
188
+ }));
189
+ results.push({ name: 'Bedrock', ok: true });
190
+ s.stop('Bedrock ✓');
191
+ } catch (err: any) {
192
+ // AccessDeniedException means credentials ARE valid, just model not enabled
193
+ const isAccessIssue = err?.name === 'AccessDeniedException' || err?.Code === 'AccessDeniedException';
194
+ if (isAccessIssue) {
195
+ results.push({ name: 'Bedrock', ok: true });
196
+ s.stop('Bedrock ✓ (credentials valid, enable models in AWS console)');
197
+ } else {
198
+ results.push({ name: 'Bedrock', ok: false });
199
+ s.stop('Bedrock ✗ (check credentials later)');
200
+ }
201
+ }
202
+ }
203
+
204
+ // Test Telegram
205
+ if (authData.telegram?.botToken) {
206
+ s.start('Testing Telegram bot...');
207
+ try {
208
+ const res = await fetch(`https://api.telegram.org/bot${authData.telegram.botToken}/getMe`);
209
+ const data = await res.json() as { ok: boolean };
210
+ results.push({ name: 'Telegram', ok: data.ok });
211
+ s.stop(data.ok ? 'Telegram ✓' : 'Telegram ✗');
212
+ } catch {
213
+ results.push({ name: 'Telegram', ok: false });
214
+ s.stop('Telegram ✗');
215
+ }
216
+ }
217
+
218
+ // ─── Step 6: Save Configuration ─────────────────────
219
+ await ensureDir(wsDir);
220
+
221
+ const defaultConfig = GlobalConfigSchema.parse({
222
+ blocksDir: './blocks',
223
+ });
224
+ await writeJson(getConfigPath(), defaultConfig);
225
+ await writeJson(getAuthPath(), authData);
226
+
227
+ // ─── Done ───────────────────────────────────────────
228
+ console.log('');
229
+ p.note(
230
+ [
231
+ ...results.map(r => `${r.ok ? chalk.green('✓') : chalk.yellow('○')} ${r.name}`),
232
+ ...(results.length === 0 ? [DIM('No connections configured yet')] : []),
233
+ '',
234
+ DIM(`Workspace: ${wsDir}`),
235
+ DIM(`Config: ${getConfigPath()}`),
236
+ ].join('\n'),
237
+ 'Setup Complete',
238
+ );
239
+
240
+ p.outro(`Run ${ACCENT('mblk start <name>')} to create and start your first block.`);
241
+ }
@@ -0,0 +1,77 @@
1
+ import { loadGlobalConfig, resolveBlockPath, loadBlockConfig, saveBlockConfig } from '../../utils/config.js';
2
+ import { pathExists } from '../../utils/fs.js';
3
+ import { log } from '../logger.js';
4
+ import { join } from 'node:path';
5
+
6
+ type PermissionScope = 'block' | 'workspace' | 'system';
7
+
8
+ const VALID_SCOPES: PermissionScope[] = ['block', 'workspace', 'system'];
9
+
10
+ /**
11
+ * View or update block permissions.
12
+ * Permissions are CLI-only — they cannot be changed via chat or web.
13
+ */
14
+ export async function permissionsCommand(
15
+ blockName: string,
16
+ options?: {
17
+ scope?: string;
18
+ allowShell?: boolean;
19
+ denyShell?: boolean;
20
+ allowNetwork?: boolean;
21
+ denyNetwork?: boolean;
22
+ maxTimeout?: string;
23
+ },
24
+ ): Promise<void> {
25
+ const globalConfig = await loadGlobalConfig();
26
+ const blockPath = resolveBlockPath(globalConfig, blockName);
27
+
28
+ if (!await pathExists(join(blockPath, 'config.json'))) {
29
+ log.error(`Block "${blockName}" not found.`);
30
+ process.exit(1);
31
+ }
32
+
33
+ const config = await loadBlockConfig(blockPath);
34
+ const perms = config.permissions || { scope: 'block', allowShell: false, allowNetwork: true, maxTimeout: 120_000 };
35
+
36
+ // No flags = show current permissions
37
+ const hasFlags = options?.scope || options?.allowShell || options?.denyShell
38
+ || options?.allowNetwork || options?.denyNetwork || options?.maxTimeout;
39
+
40
+ if (!hasFlags) {
41
+ log.brand(`permissions — ${blockName}\n`);
42
+ console.log(` Scope: ${perms.scope}`);
43
+ console.log(` Shell Access: ${perms.allowShell ? '✓ allowed' : '✗ denied'}`);
44
+ console.log(` Network: ${perms.allowNetwork ? '✓ allowed' : '✗ denied'}`);
45
+ console.log(` Max Timeout: ${(perms.maxTimeout / 1000).toFixed(0)}s`);
46
+ console.log('');
47
+
48
+ log.dim(' Scopes:');
49
+ log.dim(' block — read/write own block directory only (default)');
50
+ log.dim(' workspace — access the entire workspace');
51
+ log.dim(' system — unrestricted file and shell access');
52
+ console.log('');
53
+ return;
54
+ }
55
+
56
+ // Apply changes
57
+ if (options?.scope) {
58
+ if (!VALID_SCOPES.includes(options.scope as PermissionScope)) {
59
+ log.error(`Invalid scope: "${options.scope}". Use: block, workspace, or system.`);
60
+ process.exit(1);
61
+ }
62
+ perms.scope = options.scope as PermissionScope;
63
+ }
64
+
65
+ if (options?.allowShell) perms.allowShell = true;
66
+ if (options?.denyShell) perms.allowShell = false;
67
+ if (options?.allowNetwork) perms.allowNetwork = true;
68
+ if (options?.denyNetwork) perms.allowNetwork = false;
69
+ if (options?.maxTimeout) perms.maxTimeout = parseInt(options.maxTimeout, 10) * 1000;
70
+
71
+ // Save
72
+ (config as any).permissions = perms;
73
+ await saveBlockConfig(blockPath, config);
74
+
75
+ log.success(` Permissions updated for "${blockName}".`);
76
+ console.log(` Scope: ${perms.scope}, Shell: ${perms.allowShell ? 'yes' : 'no'}, Network: ${perms.allowNetwork ? 'yes' : 'no'}, Timeout: ${(perms.maxTimeout / 1000).toFixed(0)}s`);
77
+ }
@@ -0,0 +1,111 @@
1
+ import * as p from '@clack/prompts';
2
+ import { log } from '../logger.js';
3
+
4
+ /**
5
+ * mblk settings <plugin> — view/edit plugin settings via CLI.
6
+ * Auto-generates forms from the plugin's settings schema.
7
+ */
8
+ export async function pluginSettingsCommand(pluginId?: string): Promise<void> {
9
+ let installer: any;
10
+ try {
11
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
12
+ // @ts-ignore — optional runtime dependency, not needed at compile time
13
+ const mod = await import('@memoryblock/plugin-installer');
14
+ installer = new mod.PluginInstaller();
15
+ } catch {
16
+ log.error('Plugin installer not available.');
17
+ process.exit(1);
18
+ }
19
+
20
+ const plugins = await installer.listPlugins();
21
+
22
+ // No plugin specified — list all plugins with settings
23
+ if (!pluginId) {
24
+ const withSettings = plugins.filter((pl: any) => pl.settings && Object.keys(pl.settings).length > 0);
25
+
26
+ if (withSettings.length === 0) {
27
+ log.dim(' No plugins with configurable settings.');
28
+ return;
29
+ }
30
+
31
+ log.brand('plugin settings\n');
32
+ for (const pl of withSettings) {
33
+ const fields = Object.keys(pl.settings).length;
34
+ console.log(` ${pl.core ? '●' : '○'} ${pl.name} (${pl.id}) — ${fields} setting${fields > 1 ? 's' : ''}`);
35
+ }
36
+ console.log('');
37
+ log.dim(' Run `mblk settings <plugin-id>` to configure.');
38
+ return;
39
+ }
40
+
41
+ // Find the plugin
42
+ const plugin = plugins.find((pl: any) => pl.id === pluginId);
43
+ if (!plugin) {
44
+ log.error(`Plugin "${pluginId}" not found.`);
45
+ process.exit(1);
46
+ }
47
+
48
+ if (!plugin.settings || Object.keys(plugin.settings).length === 0) {
49
+ log.dim(` Plugin "${plugin.name}" has no configurable settings.`);
50
+ return;
51
+ }
52
+
53
+ // Load current values
54
+ const current = await installer.getPluginSettings(pluginId);
55
+
56
+ p.intro(`${plugin.name} Settings`);
57
+
58
+ const updates: Record<string, unknown> = {};
59
+
60
+ for (const [key, field] of Object.entries(plugin.settings) as [string, any][]) {
61
+ const currentVal = current[key];
62
+
63
+ if (field.type === 'select') {
64
+ const result = await p.select({
65
+ message: field.label,
66
+ options: (field.options || []).map((o: string) => ({
67
+ value: o,
68
+ label: o,
69
+ hint: o === currentVal ? 'current' : undefined,
70
+ })),
71
+ initialValue: currentVal || field.default,
72
+ });
73
+ if (p.isCancel(result)) { p.outro('Cancelled.'); return; }
74
+ updates[key] = result;
75
+ } else if (field.type === 'toggle') {
76
+ const result = await p.confirm({
77
+ message: field.label,
78
+ initialValue: currentVal ?? field.default ?? false,
79
+ });
80
+ if (p.isCancel(result)) { p.outro('Cancelled.'); return; }
81
+ updates[key] = result;
82
+ } else if (field.type === 'number') {
83
+ const result = await p.text({
84
+ message: `${field.label} (${field.min ?? ''}–${field.max ?? ''})`,
85
+ defaultValue: String(currentVal ?? field.default ?? ''),
86
+ placeholder: String(field.default ?? ''),
87
+ validate: (v: string | undefined) => {
88
+ const n = parseInt(v || '', 10);
89
+ if (isNaN(n)) return 'Must be a number';
90
+ if (field.min !== undefined && n < field.min) return `Min: ${field.min}`;
91
+ if (field.max !== undefined && n > field.max) return `Max: ${field.max}`;
92
+ },
93
+ });
94
+ if (p.isCancel(result)) { p.outro('Cancelled.'); return; }
95
+ updates[key] = parseInt(String(result), 10);
96
+ } else {
97
+ // text / password
98
+ const result = await p.text({
99
+ message: field.label,
100
+ defaultValue: String(currentVal ?? field.default ?? ''),
101
+ placeholder: field.placeholder || '',
102
+ });
103
+ if (p.isCancel(result)) { p.outro('Cancelled.'); return; }
104
+ updates[key] = result;
105
+ }
106
+ }
107
+
108
+ // Save
109
+ await installer.savePluginSettings(pluginId, updates);
110
+ p.outro(`Settings saved for ${plugin.name}.`);
111
+ }
@@ -0,0 +1,89 @@
1
+ import { log } from '../logger.js';
2
+
3
+ const INSTALLER_PKG = '@memoryblock/plugin-installer';
4
+
5
+ export async function addCommand(pluginId: string): Promise<void> {
6
+ let installer: any;
7
+ try {
8
+ const pkg = await import(INSTALLER_PKG);
9
+ installer = new pkg.PluginInstaller();
10
+ } catch {
11
+ log.error('Plugin installer not available. Install @memoryblock/plugin-installer.');
12
+ process.exit(1);
13
+ }
14
+
15
+ // Show available if no ID
16
+ if (!pluginId) {
17
+ const plugins = await installer.listPlugins();
18
+ log.brand('Skills & Plugins\n');
19
+
20
+ // Check installed status for each plugin
21
+ const rows: Array<{ id: string; name: string; status: string; installed: boolean }> = [];
22
+ for (const p of plugins) {
23
+ let installed = false;
24
+ try {
25
+ require.resolve(p.package, { paths: [process.cwd()] });
26
+ installed = true;
27
+ } catch { /* ignore */ }
28
+ rows.push({
29
+ id: p.id,
30
+ name: p.name,
31
+ status: p.status === 'upcoming' ? 'upcoming' : 'available',
32
+ installed,
33
+ });
34
+ }
35
+
36
+ // Table header
37
+ const colId = 16, colName = 24, colStatus = 12;
38
+ const header = ` ${'ID'.padEnd(colId)}${'Name'.padEnd(colName)}${'Status'.padEnd(colStatus)}Installed`;
39
+ const separator = ` ${'─'.repeat(colId)}${'─'.repeat(colName)}${'─'.repeat(colStatus)}${'─'.repeat(9)}`;
40
+ console.log(header);
41
+ console.log(separator);
42
+
43
+ for (const row of rows) {
44
+ const installedMark = row.installed ? ' ✓' : ' ·';
45
+ const statusLabel = row.status === 'upcoming' ? 'upcoming' : 'ready';
46
+ console.log(` ${row.id.padEnd(colId)}${row.name.padEnd(colName)}${statusLabel.padEnd(colStatus)}${installedMark}`);
47
+ }
48
+ console.log('');
49
+ return;
50
+ }
51
+
52
+ log.dim(` installing ${pluginId}...`);
53
+ const result = await installer.install(pluginId);
54
+
55
+ if (result.success) {
56
+ log.success(result.message);
57
+ if (result.plugin?.requiresAuth?.length) {
58
+ log.dim(` requires: ${result.plugin.requiresAuth.join(', ')}`);
59
+ }
60
+ } else {
61
+ log.warn(result.message);
62
+ }
63
+ }
64
+
65
+ export async function removeCommand(pluginId: string): Promise<void> {
66
+ if (!pluginId) {
67
+ log.error('Specify a plugin ID: mblk remove <id>');
68
+ process.exit(1);
69
+ }
70
+
71
+ let installer: any;
72
+ try {
73
+ const pkg = await import(INSTALLER_PKG);
74
+ installer = new pkg.PluginInstaller();
75
+ } catch {
76
+ log.error('Plugin installer not available.');
77
+ process.exit(1);
78
+ }
79
+
80
+ log.dim(` removing ${pluginId}...`);
81
+ const result = await installer.remove(pluginId);
82
+
83
+ if (result.success) {
84
+ log.success(result.message);
85
+ } else {
86
+ log.error(result.message);
87
+ process.exit(1);
88
+ }
89
+ }
@@ -0,0 +1,97 @@
1
+ import { log } from '../logger.js';
2
+ import { loadGlobalConfig, resolveBlockPath } from '../../utils/config.js';
3
+ import { promises as fsp } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { createInterface } from 'node:readline';
6
+ import chalk from 'chalk';
7
+ import { FILE_TEMPLATES } from '../../engine/prompts.js';
8
+
9
+ /**
10
+ * mblk reset <block> — Light cleanup: reset memory, pulse, costs, session
11
+ * mblk reset <block> --hard — Full wipe: also deletes logs/ (with confirmation)
12
+ */
13
+ export async function resetCommand(blockName: string, options: { hard?: boolean }): Promise<void> {
14
+ const globalConfig = await loadGlobalConfig();
15
+ const blockPath = resolveBlockPath(globalConfig, blockName);
16
+
17
+ // Verify block exists
18
+ try {
19
+ await fsp.access(blockPath);
20
+ } catch {
21
+ throw new Error(`Block "${blockName}" not found at ${blockPath}`);
22
+ }
23
+
24
+ // --hard requires confirmation
25
+ if (options.hard) {
26
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
27
+ const answer = await new Promise<string>((resolve) => {
28
+ rl.question(chalk.yellow(`\n⚠ --hard will wipe ALL data for "${blockName}" (logs, memory, session). Continue? (y/n): `), resolve);
29
+ });
30
+ rl.close();
31
+
32
+ if (answer.trim().toLowerCase() !== 'y') {
33
+ log.dim(' cancelled.');
34
+ return;
35
+ }
36
+ }
37
+
38
+ log.dim(` resetting ${blockName}${options.hard ? ' (hard)' : ''}...`);
39
+
40
+ // Light reset: memory, pulse, costs, session, chat.json, and logs directory
41
+ const filesToReset = ['memory.md', 'pulse.json', 'costs.json', 'session.json', 'chat.json'];
42
+
43
+ for (const file of filesToReset) {
44
+ const filePath = join(blockPath, file);
45
+ try {
46
+ await fsp.access(filePath);
47
+ if (file === 'memory.md') {
48
+ await fsp.writeFile(filePath, FILE_TEMPLATES.MEMORY_MD, 'utf-8');
49
+ } else if (file === 'pulse.json') {
50
+ await fsp.writeFile(filePath, JSON.stringify(FILE_TEMPLATES.PULSE_JSON, null, 4), 'utf-8');
51
+ } else if (file === 'costs.json') {
52
+ await fsp.writeFile(filePath, JSON.stringify(FILE_TEMPLATES.COSTS_JSON, null, 4), 'utf-8');
53
+ } else if (file === 'session.json' || file === 'chat.json') {
54
+ await fsp.unlink(filePath);
55
+ }
56
+ log.dim(` ✓ ${file}`);
57
+ } catch {
58
+ // Skip if file doesn't exist
59
+ }
60
+ }
61
+
62
+ // Wipe logs continuously for both soft and hard resets
63
+ const logsDir = join(blockPath, 'logs');
64
+ try {
65
+ const files = await fsp.readdir(logsDir);
66
+ for (const file of files) {
67
+ await fsp.unlink(join(logsDir, file));
68
+ }
69
+ log.dim(` ✓ logs/ wiped (${files.length} files)`);
70
+ } catch {
71
+ // Logs dir might not exist or be empty
72
+ }
73
+
74
+ // Hard reset: wipe monitor.md and monitor identity from config
75
+ if (options.hard) {
76
+ const monitorPath = join(blockPath, 'monitor.md');
77
+ try {
78
+ await fsp.writeFile(monitorPath, FILE_TEMPLATES.MONITOR_MD(blockName), 'utf-8');
79
+ log.dim(' ✓ monitor.md reset');
80
+ } catch { /* ignore */ }
81
+
82
+ // Clear monitor identity from config
83
+ const configPath = join(blockPath, 'config.json');
84
+ try {
85
+ const raw = await fsp.readFile(configPath, 'utf-8');
86
+ const config = JSON.parse(raw);
87
+ delete config.monitorName;
88
+ delete config.monitorEmoji;
89
+ await fsp.writeFile(configPath, JSON.stringify(config, null, 4), 'utf-8');
90
+ log.dim(' ✓ monitor identity cleared');
91
+ } catch { /* ignore */ }
92
+ } else {
93
+ log.dim(' ℹ monitor.md and identity preserved (use --hard to wipe)');
94
+ }
95
+
96
+ console.log(`\n${chalk.green(`✓ ${blockName} reset.`)}`);
97
+ }