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,327 @@
1
+ import {
2
+ isInitialized, getHome,
3
+ } from '../../utils/config.js';
4
+ import { log } from '../logger.js';
5
+ import { join } from 'node:path';
6
+ import { promises as fsp } from 'node:fs';
7
+ import { DEFAULT_PORT } from '../constants.js';
8
+
9
+ const API_PKG = '@memoryblock/api';
10
+
11
+ /**
12
+ * Server lifecycle management.
13
+ * Manages the web/API server as a foreground or daemon process.
14
+ */
15
+
16
+ export async function serverStartCommand(options?: {
17
+ port?: string;
18
+ newToken?: boolean;
19
+ daemon?: boolean;
20
+ }): Promise<void> {
21
+ if (!(await isInitialized())) {
22
+ log.error('Not initialized. Run `mblk init` first.');
23
+ process.exit(1);
24
+ }
25
+
26
+ const port = parseInt(options?.port || DEFAULT_PORT, 10);
27
+
28
+ // Check if server is already running (skip if stored PID is our own — we ARE the daemon child)
29
+ const existingPid = await readServerPid();
30
+ if (existingPid && existingPid !== process.pid && isProcessAlive(existingPid)) {
31
+ const existingPort = await readServerPort();
32
+ log.warn(`Server already running (PID ${existingPid}${existingPort ? `, port ${existingPort}` : ''}).`);
33
+ log.dim(' Use `mblk server stop` to stop it first.');
34
+ return;
35
+ }
36
+
37
+ // Daemon mode — spawn detached and exit
38
+ if (options?.daemon) {
39
+ const { spawn } = await import('node:child_process');
40
+ const fs = await import('node:fs');
41
+ const scriptPath = process.argv[1];
42
+
43
+ // Redirect daemon output to a log file for debugging crashes
44
+ const logPath = join(getHome(), 'server.log');
45
+ const logFd = fs.openSync(logPath, 'a');
46
+
47
+ const child = spawn(process.execPath, [
48
+ scriptPath, 'server', 'start', '--port', port.toString(),
49
+ ...(options?.newToken ? ['--new-token'] : []),
50
+ ], {
51
+ detached: true,
52
+ stdio: ['ignore', logFd, logFd],
53
+ env: process.env,
54
+ cwd: process.cwd(),
55
+ });
56
+
57
+ child.unref();
58
+ fs.closeSync(logFd);
59
+
60
+ if (child.pid) {
61
+ await writeServerPid(child.pid);
62
+ await writeServerPort(port);
63
+ log.brand('server\n');
64
+ log.success(` Server started as daemon (PID ${child.pid}).`);
65
+ log.dim(` http://localhost:${port}`);
66
+ log.dim(' `mblk server stop` to shut down.\n');
67
+ } else {
68
+ log.error('Failed to spawn daemon process.');
69
+ process.exit(1);
70
+ }
71
+ return;
72
+ }
73
+
74
+ // Foreground mode
75
+ let api: any;
76
+ try {
77
+ api = await import(API_PKG);
78
+ } catch (err) {
79
+ log.error(`Failed to load API package: ${(err as Error).message}`);
80
+ process.exit(1);
81
+ }
82
+
83
+ const workspacePath = process.cwd();
84
+ const authToken = await api.generateAuthToken(workspacePath, options?.newToken);
85
+
86
+ // Write PID + port for status/stop
87
+ await writeServerPid(process.pid);
88
+ await writeServerPort(port);
89
+
90
+ log.brand('server\n');
91
+ log.dim(` http://localhost:${port}`);
92
+ log.dim(` token: ${authToken}`);
93
+ console.log('');
94
+
95
+ // Auto-install OS service hook quietly
96
+ import('./service.js').then(s => s.silentServiceInstall()).catch(() => {});
97
+
98
+ // Resolve web UI static files
99
+ let webRoot: string | undefined;
100
+ try {
101
+ const { createRequire } = await import('node:module');
102
+ const require = createRequire(import.meta.url);
103
+ const webPkg = require.resolve('@memoryblock/web/package.json');
104
+ const { dirname, join: pJoin } = await import('node:path');
105
+ webRoot = pJoin(dirname(webPkg), 'public');
106
+ } catch {
107
+ log.warn('Web UI package not found. API-only mode.');
108
+ }
109
+
110
+ const server = new api.ApiServer({
111
+ port,
112
+ authToken,
113
+ workspacePath,
114
+ webRoot,
115
+ });
116
+
117
+ const shutdown = async () => {
118
+ log.system('server', 'shutting down...');
119
+ await server.stop();
120
+ await cleanupServerFiles();
121
+ process.exit(0);
122
+ };
123
+
124
+ process.on('SIGINT', shutdown);
125
+ process.on('SIGTERM', shutdown);
126
+
127
+ try {
128
+ await server.start();
129
+ log.dim(' server running. ctrl+c to stop.\n');
130
+ } catch (err) {
131
+ log.error(`Server failed: ${(err as Error).message}`);
132
+ await cleanupServerFiles();
133
+ process.exit(1);
134
+ }
135
+
136
+ // Keep alive
137
+ await new Promise(() => {});
138
+ }
139
+
140
+ export async function serverStopCommand(): Promise<void> {
141
+ const pid = await readServerPid();
142
+ const port = await readServerPort();
143
+
144
+ if (!pid) {
145
+ // No PID file — try killing by port as a fallback
146
+ if (port) {
147
+ const killed = await killByPort(port);
148
+ if (killed) {
149
+ log.success(` Server on port ${port} stopped (via port lookup).`);
150
+ } else {
151
+ log.dim(' No server PID found. Server may not be running.');
152
+ }
153
+ } else {
154
+ // Last resort: try the default port
155
+ const defaultPort = parseInt(DEFAULT_PORT, 10);
156
+ const killed = await killByPort(defaultPort);
157
+ if (killed) {
158
+ log.success(` Server on port ${DEFAULT_PORT} stopped (via port lookup).`);
159
+ } else {
160
+ log.dim(' No server PID found. Server may not be running.');
161
+ }
162
+ }
163
+ await cleanupServerFiles();
164
+ return;
165
+ }
166
+
167
+ if (!isProcessAlive(pid)) {
168
+ log.dim(' Server process not found (stale PID). Cleaning up.');
169
+ await cleanupServerFiles();
170
+ return;
171
+ }
172
+
173
+ try {
174
+ process.kill(pid, 'SIGTERM');
175
+ log.success(` Server stopped (PID ${pid}).`);
176
+ } catch (err: any) {
177
+ if (err.code === 'ESRCH') {
178
+ log.dim(' Server process already gone. Cleaning up.');
179
+ } else {
180
+ log.error(`Failed to stop server: ${err.message}`);
181
+ }
182
+ }
183
+
184
+ await cleanupServerFiles();
185
+ }
186
+
187
+ export async function serverStatusCommand(): Promise<void> {
188
+ const pid = await readServerPid();
189
+ const port = await readServerPort();
190
+
191
+ log.brand('server status\n');
192
+
193
+ if (!pid) {
194
+ log.dim(' Status: not running');
195
+ return;
196
+ }
197
+
198
+ if (isProcessAlive(pid)) {
199
+ log.success(` Status: running`);
200
+ log.dim(` PID: ${pid}`);
201
+ if (port) log.dim(` URL: http://localhost:${port}`);
202
+ } else {
203
+ log.dim(' Status: not running (stale PID file)');
204
+ await cleanupServerFiles();
205
+ }
206
+ console.log('');
207
+ }
208
+
209
+ export async function serverTokenCommand(options?: { newToken?: boolean }): Promise<void> {
210
+ log.brand(options?.newToken ? 'server token (new)\n' : 'server token\n');
211
+
212
+ let api: any;
213
+ try {
214
+ api = await import(API_PKG);
215
+ } catch (err) {
216
+ log.error(`Failed to load API package: ${(err as Error).message}`);
217
+ process.exit(1);
218
+ }
219
+
220
+ const workspacePath = process.cwd();
221
+ const token = await api.generateAuthToken(workspacePath, options?.newToken);
222
+
223
+ if (options?.newToken) {
224
+ log.success(` New token generated successfully.`);
225
+ }
226
+ log.dim(` token: ${token}\n`);
227
+ }
228
+
229
+
230
+ // ===== PID / Port Helpers =====
231
+
232
+ function pidPath(): string { return join(getHome(), 'server.pid'); }
233
+ function portPath(): string { return join(getHome(), 'server.port'); }
234
+
235
+ async function writeServerPid(pid: number): Promise<void> {
236
+ await fsp.mkdir(getHome(), { recursive: true });
237
+ await fsp.writeFile(pidPath(), pid.toString());
238
+ }
239
+
240
+ async function writeServerPort(port: number): Promise<void> {
241
+ await fsp.writeFile(portPath(), port.toString());
242
+ }
243
+
244
+ async function readServerPid(): Promise<number | null> {
245
+ try {
246
+ const s = await fsp.readFile(pidPath(), 'utf-8');
247
+ const n = parseInt(s.trim(), 10);
248
+ return isNaN(n) ? null : n;
249
+ } catch { return null; }
250
+ }
251
+
252
+ async function readServerPort(): Promise<number | null> {
253
+ try {
254
+ const s = await fsp.readFile(portPath(), 'utf-8');
255
+ const n = parseInt(s.trim(), 10);
256
+ return isNaN(n) ? null : n;
257
+ } catch { return null; }
258
+ }
259
+
260
+ function isProcessAlive(pid: number): boolean {
261
+ try {
262
+ process.kill(pid, 0); // Signal 0 checks existence
263
+ return true;
264
+ } catch { return false; }
265
+ }
266
+
267
+ async function cleanupServerFiles(): Promise<void> {
268
+ await fsp.unlink(pidPath()).catch(() => {});
269
+ await fsp.unlink(portPath()).catch(() => {});
270
+ }
271
+
272
+ /** Kill process by port using lsof — fallback when PID file is missing. */
273
+ async function killByPort(port: number): Promise<boolean> {
274
+ try {
275
+ const { execSync } = await import('node:child_process');
276
+ const pids = execSync(`lsof -ti:${port}`, { stdio: 'pipe' })
277
+ .toString().trim().split('\n').filter(Boolean);
278
+ for (const p of pids) {
279
+ try { process.kill(parseInt(p, 10), 'SIGTERM'); } catch { /* already gone */ }
280
+ }
281
+ return pids.length > 0;
282
+ } catch {
283
+ return false;
284
+ }
285
+ }
286
+
287
+ /**
288
+ * mblk shutdown — stop all blocks AND the server in one shot.
289
+ */
290
+ export async function shutdownCommand(): Promise<void> {
291
+ log.brand('shutdown\n');
292
+
293
+ // 1. Stop all blocks
294
+ log.dim(' Stopping all blocks...');
295
+ try {
296
+ const { stopCommand } = await import('./stop.js');
297
+ await stopCommand(undefined, { preserveEnabled: true });
298
+ } catch { /* ignore */ }
299
+
300
+ // 2. Stop the server
301
+ log.dim(' Stopping server...');
302
+ await serverStopCommand();
303
+
304
+ log.success('\n Everything shut down.');
305
+ }
306
+
307
+ /**
308
+ * mblk restart — shutdown then start server as daemon.
309
+ */
310
+ export async function restartCommand(options?: { port?: string }): Promise<void> {
311
+ log.brand('restart\n');
312
+
313
+ // Shutdown first
314
+ await shutdownCommand();
315
+
316
+ // Small delay to let PID files clean up
317
+ await new Promise(resolve => setTimeout(resolve, 500));
318
+
319
+ // Start server as daemon
320
+ log.dim('\n Starting server...');
321
+ await serverStartCommand({ port: options?.port, daemon: true });
322
+
323
+ // Start all enabled blocks
324
+ log.dim('\n Starting enabled blocks...');
325
+ const { startAllEnabledBlocks } = await import('./start.js');
326
+ await startAllEnabledBlocks();
327
+ }
@@ -0,0 +1,300 @@
1
+ import { log } from '../logger.js';
2
+ import { getHome } from '../../utils/config.js';
3
+ import { promises as fsp } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { execSync } from 'node:child_process';
6
+ import { platform } from 'node:os';
7
+
8
+ const SERVICE_ID = 'io.memoryblock.daemon';
9
+ const SERVICE_LABEL = 'memoryblock';
10
+
11
+ /**
12
+ * Get the path to the launchd plist or systemd unit file.
13
+ */
14
+ function getServicePath(): string {
15
+ const os = platform();
16
+ if (os === 'darwin') {
17
+ return join(process.env.HOME || '~', 'Library', 'LaunchAgents', `${SERVICE_ID}.plist`);
18
+ } else if (os === 'linux') {
19
+ return join(process.env.HOME || '~', '.config', 'systemd', 'user', `${SERVICE_LABEL}.service`);
20
+ } else if (os === 'win32') {
21
+ const appData = process.env.APPDATA || join(process.env.USERPROFILE || '~', 'AppData', 'Roaming');
22
+ return join(appData, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup', `${SERVICE_LABEL}.vbs`);
23
+ }
24
+ throw new Error(`Unsupported platform: ${os}. Service management is available on macOS, Linux, and Windows.`);
25
+ }
26
+
27
+ /**
28
+ * Resolve the mblk binary path — works in both dev (bun) and installed (node) modes.
29
+ */
30
+ function getMblkBinaryPath(): string {
31
+ // In dev mode: process.argv[1] is the TS source
32
+ // After npm install: the bin wrapper is symlinked
33
+ const scriptPath = process.argv[1];
34
+
35
+ // If running from bun with .ts source, use bun to execute
36
+ if (scriptPath.endsWith('.ts')) {
37
+ const bunPath = join(process.env.HOME || '~', '.bun', 'bin', 'bun');
38
+ return `${bunPath} ${scriptPath}`;
39
+ }
40
+
41
+ // Otherwise use node (installed via npm)
42
+ return `${process.execPath} ${scriptPath}`;
43
+ }
44
+
45
+ /**
46
+ * Generate launchd plist content for macOS.
47
+ */
48
+ function generateLaunchdPlist(mblkCmd: string): string {
49
+ const parts = mblkCmd.split(' ');
50
+ const programArgs = parts.map(p => ` <string>${p}</string>`).join('\n');
51
+
52
+ return `<?xml version="1.0" encoding="UTF-8"?>
53
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
54
+ <plist version="1.0">
55
+ <dict>
56
+ <key>Label</key>
57
+ <string>${SERVICE_ID}</string>
58
+ <key>ProgramArguments</key>
59
+ <array>
60
+ ${programArgs}
61
+ <string>restart</string>
62
+ </array>
63
+ <key>RunAtLoad</key>
64
+ <true/>
65
+ <key>KeepAlive</key>
66
+ <false/>
67
+ <key>WorkingDirectory</key>
68
+ <string>${process.cwd()}</string>
69
+ <key>StandardOutPath</key>
70
+ <string>${join(getHome(), 'service.log')}</string>
71
+ <key>StandardErrorPath</key>
72
+ <string>${join(getHome(), 'service.log')}</string>
73
+ <key>EnvironmentVariables</key>
74
+ <dict>
75
+ <key>PATH</key>
76
+ <string>${process.env.PATH}</string>
77
+ <key>HOME</key>
78
+ <string>${process.env.HOME}</string>
79
+ </dict>
80
+ </dict>
81
+ </plist>`;
82
+ }
83
+
84
+ /**
85
+ * Generate systemd user unit content for Linux.
86
+ */
87
+ function generateSystemdUnit(mblkCmd: string): string {
88
+ return `[Unit]
89
+ Description=memoryblock - AI assistant daemon
90
+ After=network.target
91
+
92
+ [Service]
93
+ Type=oneshot
94
+ RemainAfterExit=yes
95
+ ExecStart=${mblkCmd} restart
96
+ ExecStop=${mblkCmd} shutdown
97
+ WorkingDirectory=${process.cwd()}
98
+ Environment="PATH=${process.env.PATH}"
99
+ Environment="HOME=${process.env.HOME}"
100
+
101
+ [Install]
102
+ WantedBy=default.target`;
103
+ }
104
+
105
+ /**
106
+ * Generate a Windows VBScript to run mblk restart hidden.
107
+ */
108
+ function generateWindowsVbs(mblkCmd: string): string {
109
+ // VBScript to execute command without showing a console window
110
+ return `Set WshShell = CreateObject("WScript.Shell")\nWshShell.Run "${mblkCmd} restart", 0, False`;
111
+ }
112
+
113
+ /**
114
+ * Install memoryblock as a system service (runs on login/boot).
115
+ */
116
+ export async function serviceInstallCommand(): Promise<void> {
117
+ const os = platform();
118
+ const servicePath = getServicePath();
119
+ const mblkCmd = getMblkBinaryPath();
120
+
121
+ log.brand('service install\n');
122
+
123
+ // Write service file
124
+ await fsp.mkdir(join(servicePath, '..'), { recursive: true });
125
+
126
+ if (os === 'darwin') {
127
+ const plist = generateLaunchdPlist(mblkCmd);
128
+ await fsp.writeFile(servicePath, plist, 'utf-8');
129
+
130
+ // Unload first if already loaded (ignore errors)
131
+ try { execSync(`launchctl bootout gui/${process.getuid?.()} ${servicePath}`, { stdio: 'pipe' }); } catch { /* ignore */ }
132
+
133
+ // Load the plist
134
+ try {
135
+ execSync(`launchctl bootstrap gui/${process.getuid?.()} ${servicePath}`, { stdio: 'pipe' });
136
+ log.success(' Service installed and loaded.');
137
+ } catch {
138
+ // Fallback for older macOS
139
+ try {
140
+ execSync(`launchctl load ${servicePath}`, { stdio: 'pipe' });
141
+ log.success(' Service installed and loaded.');
142
+ } catch (err) {
143
+ log.warn(` Plist written but failed to load: ${(err as Error).message}`);
144
+ log.dim(` Try manually: launchctl load ${servicePath}`);
145
+ }
146
+ }
147
+ } else if (os === 'linux') {
148
+ const unit = generateSystemdUnit(mblkCmd);
149
+ await fsp.writeFile(servicePath, unit, 'utf-8');
150
+
151
+ try {
152
+ execSync('systemctl --user daemon-reload', { stdio: 'pipe' });
153
+ execSync(`systemctl --user enable ${SERVICE_LABEL}`, { stdio: 'pipe' });
154
+ log.success(' Service installed and enabled.');
155
+ log.dim(' It will start automatically on next login.');
156
+ log.dim(` Start now: systemctl --user start ${SERVICE_LABEL}`);
157
+ } catch (err) {
158
+ log.warn(` Unit file written but systemctl failed: ${(err as Error).message}`);
159
+ }
160
+ } else if (os === 'win32') {
161
+ const vbs = generateWindowsVbs(mblkCmd);
162
+ await fsp.writeFile(servicePath, vbs, 'utf-8');
163
+ log.success(' Service installed to Windows Startup folder.');
164
+ log.dim(' It will start automatically on next login.');
165
+ }
166
+
167
+ log.dim(` File: ${servicePath}`);
168
+ log.dim(' memoryblock will auto-start on boot/login.\n');
169
+ }
170
+
171
+ /**
172
+ * Silently install the service in the background (used by start/server commands).
173
+ * Catches all errors so it never interrupts the user.
174
+ */
175
+ export async function silentServiceInstall(): Promise<void> {
176
+ try {
177
+ const os = platform();
178
+ const servicePath = getServicePath();
179
+ const mblkCmd = getMblkBinaryPath();
180
+
181
+ await fsp.mkdir(join(servicePath, '..'), { recursive: true });
182
+
183
+ // Generate content based on OS
184
+ let content = '';
185
+ if (os === 'darwin') {
186
+ content = generateLaunchdPlist(mblkCmd);
187
+ } else if (os === 'linux') {
188
+ content = generateSystemdUnit(mblkCmd);
189
+ } else if (os === 'win32') {
190
+ content = generateWindowsVbs(mblkCmd);
191
+ } else {
192
+ return;
193
+ }
194
+
195
+ // Idempotency check: if the file exists and is identical, do nothing.
196
+ // This prevents infinite reload loops where the service restarts itself.
197
+ try {
198
+ const existing = await fsp.readFile(servicePath, 'utf-8');
199
+ if (existing === content) {
200
+ return; // Already installed and up-to-date
201
+ }
202
+ } catch {
203
+ // File doesn't exist, proceed with install
204
+ }
205
+
206
+ await fsp.writeFile(servicePath, content, 'utf-8');
207
+
208
+ if (os === 'darwin') {
209
+ try { execSync(`launchctl bootout gui/${process.getuid?.()} ${servicePath}`, { stdio: 'ignore' }); } catch { /* ignore */ }
210
+ try {
211
+ execSync(`launchctl bootstrap gui/${process.getuid?.()} ${servicePath}`, { stdio: 'ignore' });
212
+ } catch {
213
+ try { execSync(`launchctl load ${servicePath}`, { stdio: 'ignore' }); } catch { /* ignore */ }
214
+ }
215
+ } else if (os === 'linux') {
216
+ try {
217
+ execSync('systemctl --user daemon-reload', { stdio: 'ignore' });
218
+ execSync(`systemctl --user enable ${SERVICE_LABEL}`, { stdio: 'ignore' });
219
+ } catch { /* ignore */ }
220
+ }
221
+ } catch {
222
+ // Entirely silent
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Uninstall the system service.
228
+ */
229
+ export async function serviceUninstallCommand(): Promise<void> {
230
+ const os = platform();
231
+ const servicePath = getServicePath();
232
+
233
+ log.brand('service uninstall\n');
234
+
235
+ if (os === 'darwin') {
236
+ try { execSync(`launchctl bootout gui/${process.getuid?.()} ${servicePath}`, { stdio: 'pipe' }); } catch { /* ignore */ }
237
+ try { execSync(`launchctl unload ${servicePath}`, { stdio: 'pipe' }); } catch { /* ignore */ }
238
+ } else if (os === 'linux') {
239
+ try {
240
+ execSync(`systemctl --user stop ${SERVICE_LABEL}`, { stdio: 'pipe' });
241
+ execSync(`systemctl --user disable ${SERVICE_LABEL}`, { stdio: 'pipe' });
242
+ } catch { /* ignore */ }
243
+ } else if (os === 'win32') {
244
+ // Nothing to stop gracefully on Windows for a startup script
245
+ // Admin could taskkill, but mblk shutdown handles cleanup
246
+ }
247
+
248
+ try {
249
+ await fsp.unlink(servicePath);
250
+ log.success(' Service removed.');
251
+ } catch {
252
+ log.dim(' No service file found. Nothing to remove.');
253
+ }
254
+ console.log('');
255
+ }
256
+
257
+ /**
258
+ * Show service status.
259
+ */
260
+ export async function serviceStatusCommand(): Promise<void> {
261
+ const os = platform();
262
+ const servicePath = getServicePath();
263
+
264
+ log.brand('service status\n');
265
+
266
+ // Check if service file exists
267
+ try {
268
+ await fsp.access(servicePath);
269
+ } catch {
270
+ log.dim(' Status: not installed');
271
+ log.dim(` Run \`mblk service install\` to enable auto-start.\n`);
272
+ return;
273
+ }
274
+
275
+ log.success(' Status: installed');
276
+ log.dim(` File: ${servicePath}`);
277
+
278
+ if (os === 'darwin') {
279
+ try {
280
+ const output = execSync(`launchctl list | grep ${SERVICE_ID}`, { stdio: 'pipe' }).toString().trim();
281
+ if (output) {
282
+ log.dim(' launchd: loaded');
283
+ } else {
284
+ log.dim(' launchd: not loaded');
285
+ }
286
+ } catch {
287
+ log.dim(' launchd: not loaded');
288
+ }
289
+ } else if (os === 'linux') {
290
+ try {
291
+ const output = execSync(`systemctl --user is-active ${SERVICE_LABEL}`, { stdio: 'pipe' }).toString().trim();
292
+ log.dim(` systemd: ${output}`);
293
+ } catch {
294
+ log.dim(' systemd: inactive');
295
+ }
296
+ } else if (os === 'win32') {
297
+ log.dim(' Windows Startup: active (file exists)');
298
+ }
299
+ console.log('');
300
+ }