mercury-agent 0.4.7 → 0.4.9

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 (36) hide show
  1. package/container/Dockerfile.power +1 -1
  2. package/docs/auth/dashboard.md +28 -28
  3. package/docs/container-lifecycle.md +4 -4
  4. package/examples/extensions/voice-synth/index.ts +94 -94
  5. package/examples/extensions/voice-transcribe/scripts/transcribe.py +1 -1
  6. package/package.json +1 -1
  7. package/resources/templates/mercury.example.yaml +1 -1
  8. package/src/adapters/whatsapp.ts +635 -632
  9. package/src/agent/container-runner.ts +3 -1
  10. package/src/agent/model-capabilities.ts +231 -231
  11. package/src/bridges/discord.ts +178 -178
  12. package/src/bridges/slack.ts +179 -179
  13. package/src/bridges/teams.ts +162 -162
  14. package/src/bridges/telegram.ts +579 -579
  15. package/src/cli/mercury.ts +2551 -2536
  16. package/src/cli/whatsapp-auth.ts +263 -260
  17. package/src/config.ts +316 -316
  18. package/src/core/permissions.ts +196 -196
  19. package/src/core/router.ts +191 -191
  20. package/src/core/routes/chat.ts +175 -175
  21. package/src/core/routes/dashboard.ts +2491 -2491
  22. package/src/core/routes/messages.ts +37 -37
  23. package/src/core/routes/mutes.ts +95 -95
  24. package/src/core/routes/roles.ts +135 -135
  25. package/src/core/runtime.ts +1140 -1140
  26. package/src/core/task-scheduler.ts +139 -139
  27. package/src/extensions/catalog.ts +117 -117
  28. package/src/extensions/hooks.ts +161 -161
  29. package/src/extensions/installer.ts +306 -306
  30. package/src/extensions/loader.ts +271 -271
  31. package/src/extensions/permission-guard.ts +52 -52
  32. package/src/server.ts +391 -391
  33. package/src/storage/db.ts +1625 -1625
  34. package/src/storage/pi-auth.ts +95 -95
  35. package/src/tts/azure.ts +52 -52
  36. package/src/tts/synthesize.ts +133 -133
@@ -1,306 +1,306 @@
1
- /**
2
- * Install/remove Mercury extensions from the host (CLI + dashboard).
3
- * Uses the same layout as `mercury add` / `mercury remove`.
4
- */
5
- import { spawnSync } from "node:child_process";
6
- import {
7
- cpSync,
8
- existsSync,
9
- mkdirSync,
10
- readFileSync,
11
- renameSync,
12
- rmSync,
13
- } from "node:fs";
14
- import { dirname, join } from "node:path";
15
- import type { Logger } from "../logger.js";
16
- import type { ExtensionCatalogEntry } from "./catalog.js";
17
- import { RESERVED_EXTENSION_NAMES } from "./reserved.js";
18
-
19
- const VALID_EXT_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
20
-
21
- function loadEnvFile(envPath: string): Record<string, string> {
22
- const content = readFileSync(envPath, "utf-8");
23
- const vars: Record<string, string> = {};
24
- for (const line of content.split("\n")) {
25
- const trimmed = line.trim();
26
- if (!trimmed || trimmed.startsWith("#")) continue;
27
- const match = trimmed.match(/^([^=]+)=(.*)$/);
28
- if (match) {
29
- vars[match[1]] = match[2];
30
- }
31
- }
32
- return vars;
33
- }
34
-
35
- /** Resolve MERCURY_DATA_DIR from project `.env` (default `.mercury`). */
36
- export function getProjectDataDir(cwd: string): string {
37
- const envPath = join(cwd, ".env");
38
- if (existsSync(envPath)) {
39
- const envVars = loadEnvFile(envPath);
40
- if (envVars.MERCURY_DATA_DIR) return envVars.MERCURY_DATA_DIR;
41
- }
42
- return ".mercury";
43
- }
44
-
45
- export function getUserExtensionsDir(cwd: string): string {
46
- return join(cwd, getProjectDataDir(cwd), "extensions");
47
- }
48
-
49
- export function getGlobalDir(cwd: string): string {
50
- const envPath = join(cwd, ".env");
51
- if (existsSync(envPath)) {
52
- const envVars = loadEnvFile(envPath);
53
- if (envVars.MERCURY_GLOBAL_DIR) return envVars.MERCURY_GLOBAL_DIR;
54
- }
55
- return join(cwd, getProjectDataDir(cwd), "global");
56
- }
57
-
58
- /** Path to `examples/extensions/<sourceDir>` inside the mercury-agent package. */
59
- export function resolveExamplesExtensionDir(
60
- packageRoot: string,
61
- sourceDir: string,
62
- ): string {
63
- return join(packageRoot, "examples", "extensions", sourceDir);
64
- }
65
-
66
- export type ExtensionInstallResult =
67
- | { ok: true }
68
- | { ok: false; error: string };
69
-
70
- function validateForInstall(
71
- destName: string,
72
- sourceDir: string,
73
- extensionsDir: string,
74
- ): string | null {
75
- if (!VALID_EXT_NAME_RE.test(destName)) {
76
- return `Invalid extension name "${destName}" (lowercase letters, digits, hyphens only).`;
77
- }
78
- if (RESERVED_EXTENSION_NAMES.has(destName)) {
79
- return `"${destName}" is a reserved built-in name.`;
80
- }
81
- if (!existsSync(join(sourceDir, "index.ts"))) {
82
- return "Extension source has no index.ts.";
83
- }
84
- if (existsSync(join(extensionsDir, destName))) {
85
- return `Extension "${destName}" is already installed. Remove it first.`;
86
- }
87
- return null;
88
- }
89
-
90
- /** Validate that `index.ts` exists and default-exports a function (for CLI doctor). */
91
- export async function checkExtensionIndexLoads(
92
- extDir: string,
93
- logicalName: string,
94
- ): Promise<string | null> {
95
- const indexPath = join(extDir, "index.ts");
96
- try {
97
- const mod = await import(indexPath);
98
- if (typeof mod.default !== "function") {
99
- return `${logicalName}/index.ts must export a default function`;
100
- }
101
- } catch (err) {
102
- const msg = err instanceof Error ? err.message : String(err);
103
- return `Failed to load ${logicalName}/index.ts: ${msg}`;
104
- }
105
- return null;
106
- }
107
-
108
- function installSkillIfPresent(
109
- extDir: string,
110
- name: string,
111
- cwd: string,
112
- ): void {
113
- const skillDir = join(extDir, "skill");
114
- if (!existsSync(join(skillDir, "SKILL.md"))) return;
115
-
116
- const globalDir = getGlobalDir(cwd);
117
- const dst = join(globalDir, "skills", name);
118
- mkdirSync(dirname(dst), { recursive: true });
119
- rmSync(dst, { recursive: true, force: true });
120
- cpSync(skillDir, dst, { recursive: true });
121
- }
122
-
123
- /**
124
- * Copy an extension from a local directory into `.mercury/extensions/<destName>/`.
125
- */
126
- export async function installExtensionFromDirectory(options: {
127
- cwd: string;
128
- sourceDir: string;
129
- destName: string;
130
- }): Promise<ExtensionInstallResult> {
131
- const { cwd, sourceDir, destName } = options;
132
- const extensionsDir = getUserExtensionsDir(cwd);
133
- mkdirSync(extensionsDir, { recursive: true });
134
-
135
- const err = validateForInstall(destName, sourceDir, extensionsDir);
136
- if (err) return { ok: false, error: err };
137
-
138
- const loadErr = await checkExtensionIndexLoads(sourceDir, destName);
139
- if (loadErr) return { ok: false, error: loadErr };
140
-
141
- const destDir = join(extensionsDir, destName);
142
- try {
143
- cpSync(sourceDir, destDir, { recursive: true });
144
-
145
- if (existsSync(join(destDir, "package.json"))) {
146
- const installResult = spawnSync("bun", ["install"], {
147
- stdio: "pipe",
148
- encoding: "utf-8",
149
- cwd: destDir,
150
- });
151
- if (installResult.status !== 0) {
152
- const stderr = installResult.stderr?.toString?.() ?? "";
153
- rmSync(destDir, { recursive: true, force: true });
154
- return {
155
- ok: false,
156
- error: `bun install failed in extension: ${stderr || "unknown error"}`,
157
- };
158
- }
159
- }
160
-
161
- installSkillIfPresent(destDir, destName, cwd);
162
- return { ok: true };
163
- } catch (e) {
164
- if (existsSync(destDir)) {
165
- rmSync(destDir, { recursive: true, force: true });
166
- }
167
- const msg = e instanceof Error ? e.message : String(e);
168
- return { ok: false, error: msg };
169
- }
170
- }
171
-
172
- export function removeInstalledExtension(options: {
173
- cwd: string;
174
- name: string;
175
- }): ExtensionInstallResult {
176
- const { cwd, name } = options;
177
- if (!VALID_EXT_NAME_RE.test(name)) {
178
- return { ok: false, error: `Invalid extension name "${name}".` };
179
- }
180
-
181
- const extensionsDir = getUserExtensionsDir(cwd);
182
- const extDir = join(extensionsDir, name);
183
-
184
- if (!existsSync(extDir)) {
185
- return { ok: false, error: `Extension "${name}" is not installed.` };
186
- }
187
-
188
- const globalDir = getGlobalDir(cwd);
189
- const skillDst = join(globalDir, "skills", name);
190
- if (existsSync(skillDst)) {
191
- rmSync(skillDst, { recursive: true, force: true });
192
- }
193
-
194
- rmSync(extDir, { recursive: true, force: true });
195
- return { ok: true };
196
- }
197
-
198
- /**
199
- * On startup, re-copy any installed catalog extension whose bundled source
200
- * differs from the installed copy. Triggered when a new image ships a patched
201
- * extension (e.g. MERCURY_BROWSER_SESSIONS fix) but running agents still have
202
- * the old installed copy on their data volume.
203
- *
204
- * Uses an atomic temp-dir swap so a failed copy never leaves an extension
205
- * partially installed or fully deleted.
206
- */
207
- export async function syncBundledCatalogExtensions(options: {
208
- packageRoot: string;
209
- extensionsDir: string;
210
- globalDir: string;
211
- catalog: ExtensionCatalogEntry[];
212
- logger: Logger;
213
- }): Promise<void> {
214
- const { packageRoot, extensionsDir, globalDir, catalog, logger } = options;
215
- if (!existsSync(extensionsDir)) return;
216
-
217
- for (const entry of catalog) {
218
- const installedDir = join(extensionsDir, entry.name);
219
- if (!existsSync(installedDir)) continue;
220
-
221
- const bundledDir = resolveExamplesExtensionDir(
222
- packageRoot,
223
- entry.sourceDir,
224
- );
225
- if (!existsSync(bundledDir)) continue;
226
-
227
- const bundledIndex = join(bundledDir, "index.ts");
228
- if (!existsSync(bundledIndex)) continue;
229
-
230
- let needsUpdate = false;
231
- try {
232
- for (const file of ["index.ts", "package.json"]) {
233
- const bundledFile = join(bundledDir, file);
234
- const installedFile = join(installedDir, file);
235
- if (!existsSync(bundledFile)) continue;
236
- const bundledContent = readFileSync(bundledFile, "utf-8");
237
- const installedContent = existsSync(installedFile)
238
- ? readFileSync(installedFile, "utf-8")
239
- : null;
240
- if (bundledContent !== installedContent) {
241
- needsUpdate = true;
242
- break;
243
- }
244
- }
245
- } catch {
246
- needsUpdate = true;
247
- }
248
-
249
- if (!needsUpdate) continue;
250
-
251
- logger.info("Bundled extension source updated — reinstalling", {
252
- name: entry.name,
253
- });
254
-
255
- // Atomic swap: copy to a temp sibling dir first, then replace.
256
- // This ensures a copy failure never leaves the extension deleted.
257
- const tmpDir = `${installedDir}.tmp`;
258
- rmSync(tmpDir, { recursive: true, force: true });
259
- try {
260
- cpSync(bundledDir, tmpDir, { recursive: true });
261
-
262
- if (existsSync(join(tmpDir, "package.json"))) {
263
- const installResult = spawnSync("bun", ["install"], {
264
- stdio: "pipe",
265
- encoding: "utf-8",
266
- cwd: tmpDir,
267
- });
268
- if (installResult.status !== 0) {
269
- // bun install failed — discard the temp dir; keep the old install.
270
- rmSync(tmpDir, { recursive: true, force: true });
271
- logger.warn(
272
- "Extension sync skipped: bun install failed in bundled source",
273
- {
274
- name: entry.name,
275
- stderr: installResult.stderr?.toString?.() ?? "",
276
- },
277
- );
278
- continue;
279
- }
280
- }
281
-
282
- // Swap: remove old, rename temp into place.
283
- rmSync(installedDir, { recursive: true, force: true });
284
- renameSync(tmpDir, installedDir);
285
-
286
- // Sync skill dir to global skills.
287
- const skillDir = join(installedDir, "skill");
288
- if (existsSync(join(skillDir, "SKILL.md"))) {
289
- const dst = join(globalDir, "skills", entry.name);
290
- mkdirSync(dirname(dst), { recursive: true });
291
- rmSync(dst, { recursive: true, force: true });
292
- cpSync(skillDir, dst, { recursive: true });
293
- }
294
-
295
- logger.info("Extension reinstalled from bundled source", {
296
- name: entry.name,
297
- });
298
- } catch (e) {
299
- rmSync(tmpDir, { recursive: true, force: true });
300
- logger.warn("Failed to reinstall extension from bundled source", {
301
- name: entry.name,
302
- error: e instanceof Error ? e.message : String(e),
303
- });
304
- }
305
- }
306
- }
1
+ /**
2
+ * Install/remove Mercury extensions from the host (CLI + dashboard).
3
+ * Uses the same layout as `mercury add` / `mercury remove`.
4
+ */
5
+ import { spawnSync } from "node:child_process";
6
+ import {
7
+ cpSync,
8
+ existsSync,
9
+ mkdirSync,
10
+ readFileSync,
11
+ renameSync,
12
+ rmSync,
13
+ } from "node:fs";
14
+ import { dirname, join } from "node:path";
15
+ import type { Logger } from "../logger.js";
16
+ import type { ExtensionCatalogEntry } from "./catalog.js";
17
+ import { RESERVED_EXTENSION_NAMES } from "./reserved.js";
18
+
19
+ const VALID_EXT_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
20
+
21
+ function loadEnvFile(envPath: string): Record<string, string> {
22
+ const content = readFileSync(envPath, "utf-8");
23
+ const vars: Record<string, string> = {};
24
+ for (const line of content.split("\n")) {
25
+ const trimmed = line.trim();
26
+ if (!trimmed || trimmed.startsWith("#")) continue;
27
+ const match = trimmed.match(/^([^=]+)=(.*)$/);
28
+ if (match) {
29
+ vars[match[1]] = match[2];
30
+ }
31
+ }
32
+ return vars;
33
+ }
34
+
35
+ /** Resolve MERCURY_DATA_DIR from project `.env` (default `.mercury`). */
36
+ export function getProjectDataDir(cwd: string): string {
37
+ const envPath = join(cwd, ".env");
38
+ if (existsSync(envPath)) {
39
+ const envVars = loadEnvFile(envPath);
40
+ if (envVars.MERCURY_DATA_DIR) return envVars.MERCURY_DATA_DIR;
41
+ }
42
+ return ".mercury";
43
+ }
44
+
45
+ export function getUserExtensionsDir(cwd: string): string {
46
+ return join(cwd, getProjectDataDir(cwd), "extensions");
47
+ }
48
+
49
+ export function getGlobalDir(cwd: string): string {
50
+ const envPath = join(cwd, ".env");
51
+ if (existsSync(envPath)) {
52
+ const envVars = loadEnvFile(envPath);
53
+ if (envVars.MERCURY_GLOBAL_DIR) return envVars.MERCURY_GLOBAL_DIR;
54
+ }
55
+ return join(cwd, getProjectDataDir(cwd), "global");
56
+ }
57
+
58
+ /** Path to `examples/extensions/<sourceDir>` inside the mercury-agent package. */
59
+ export function resolveExamplesExtensionDir(
60
+ packageRoot: string,
61
+ sourceDir: string,
62
+ ): string {
63
+ return join(packageRoot, "examples", "extensions", sourceDir);
64
+ }
65
+
66
+ export type ExtensionInstallResult =
67
+ | { ok: true }
68
+ | { ok: false; error: string };
69
+
70
+ function validateForInstall(
71
+ destName: string,
72
+ sourceDir: string,
73
+ extensionsDir: string,
74
+ ): string | null {
75
+ if (!VALID_EXT_NAME_RE.test(destName)) {
76
+ return `Invalid extension name "${destName}" (lowercase letters, digits, hyphens only).`;
77
+ }
78
+ if (RESERVED_EXTENSION_NAMES.has(destName)) {
79
+ return `"${destName}" is a reserved built-in name.`;
80
+ }
81
+ if (!existsSync(join(sourceDir, "index.ts"))) {
82
+ return "Extension source has no index.ts.";
83
+ }
84
+ if (existsSync(join(extensionsDir, destName))) {
85
+ return `Extension "${destName}" is already installed. Remove it first.`;
86
+ }
87
+ return null;
88
+ }
89
+
90
+ /** Validate that `index.ts` exists and default-exports a function (for CLI doctor). */
91
+ export async function checkExtensionIndexLoads(
92
+ extDir: string,
93
+ logicalName: string,
94
+ ): Promise<string | null> {
95
+ const indexPath = join(extDir, "index.ts");
96
+ try {
97
+ const mod = await import(indexPath);
98
+ if (typeof mod.default !== "function") {
99
+ return `${logicalName}/index.ts must export a default function`;
100
+ }
101
+ } catch (err) {
102
+ const msg = err instanceof Error ? err.message : String(err);
103
+ return `Failed to load ${logicalName}/index.ts: ${msg}`;
104
+ }
105
+ return null;
106
+ }
107
+
108
+ function installSkillIfPresent(
109
+ extDir: string,
110
+ name: string,
111
+ cwd: string,
112
+ ): void {
113
+ const skillDir = join(extDir, "skill");
114
+ if (!existsSync(join(skillDir, "SKILL.md"))) return;
115
+
116
+ const globalDir = getGlobalDir(cwd);
117
+ const dst = join(globalDir, "skills", name);
118
+ mkdirSync(dirname(dst), { recursive: true });
119
+ rmSync(dst, { recursive: true, force: true });
120
+ cpSync(skillDir, dst, { recursive: true });
121
+ }
122
+
123
+ /**
124
+ * Copy an extension from a local directory into `.mercury/extensions/<destName>/`.
125
+ */
126
+ export async function installExtensionFromDirectory(options: {
127
+ cwd: string;
128
+ sourceDir: string;
129
+ destName: string;
130
+ }): Promise<ExtensionInstallResult> {
131
+ const { cwd, sourceDir, destName } = options;
132
+ const extensionsDir = getUserExtensionsDir(cwd);
133
+ mkdirSync(extensionsDir, { recursive: true });
134
+
135
+ const err = validateForInstall(destName, sourceDir, extensionsDir);
136
+ if (err) return { ok: false, error: err };
137
+
138
+ const loadErr = await checkExtensionIndexLoads(sourceDir, destName);
139
+ if (loadErr) return { ok: false, error: loadErr };
140
+
141
+ const destDir = join(extensionsDir, destName);
142
+ try {
143
+ cpSync(sourceDir, destDir, { recursive: true });
144
+
145
+ if (existsSync(join(destDir, "package.json"))) {
146
+ const installResult = spawnSync("bun", ["install"], {
147
+ stdio: "pipe",
148
+ encoding: "utf-8",
149
+ cwd: destDir,
150
+ });
151
+ if (installResult.status !== 0) {
152
+ const stderr = installResult.stderr?.toString?.() ?? "";
153
+ rmSync(destDir, { recursive: true, force: true });
154
+ return {
155
+ ok: false,
156
+ error: `bun install failed in extension: ${stderr || "unknown error"}`,
157
+ };
158
+ }
159
+ }
160
+
161
+ installSkillIfPresent(destDir, destName, cwd);
162
+ return { ok: true };
163
+ } catch (e) {
164
+ if (existsSync(destDir)) {
165
+ rmSync(destDir, { recursive: true, force: true });
166
+ }
167
+ const msg = e instanceof Error ? e.message : String(e);
168
+ return { ok: false, error: msg };
169
+ }
170
+ }
171
+
172
+ export function removeInstalledExtension(options: {
173
+ cwd: string;
174
+ name: string;
175
+ }): ExtensionInstallResult {
176
+ const { cwd, name } = options;
177
+ if (!VALID_EXT_NAME_RE.test(name)) {
178
+ return { ok: false, error: `Invalid extension name "${name}".` };
179
+ }
180
+
181
+ const extensionsDir = getUserExtensionsDir(cwd);
182
+ const extDir = join(extensionsDir, name);
183
+
184
+ if (!existsSync(extDir)) {
185
+ return { ok: false, error: `Extension "${name}" is not installed.` };
186
+ }
187
+
188
+ const globalDir = getGlobalDir(cwd);
189
+ const skillDst = join(globalDir, "skills", name);
190
+ if (existsSync(skillDst)) {
191
+ rmSync(skillDst, { recursive: true, force: true });
192
+ }
193
+
194
+ rmSync(extDir, { recursive: true, force: true });
195
+ return { ok: true };
196
+ }
197
+
198
+ /**
199
+ * On startup, re-copy any installed catalog extension whose bundled source
200
+ * differs from the installed copy. Triggered when a new image ships a patched
201
+ * extension (e.g. MERCURY_BROWSER_SESSIONS fix) but running agents still have
202
+ * the old installed copy on their data volume.
203
+ *
204
+ * Uses an atomic temp-dir swap so a failed copy never leaves an extension
205
+ * partially installed or fully deleted.
206
+ */
207
+ export async function syncBundledCatalogExtensions(options: {
208
+ packageRoot: string;
209
+ extensionsDir: string;
210
+ globalDir: string;
211
+ catalog: ExtensionCatalogEntry[];
212
+ logger: Logger;
213
+ }): Promise<void> {
214
+ const { packageRoot, extensionsDir, globalDir, catalog, logger } = options;
215
+ if (!existsSync(extensionsDir)) return;
216
+
217
+ for (const entry of catalog) {
218
+ const installedDir = join(extensionsDir, entry.name);
219
+ if (!existsSync(installedDir)) continue;
220
+
221
+ const bundledDir = resolveExamplesExtensionDir(
222
+ packageRoot,
223
+ entry.sourceDir,
224
+ );
225
+ if (!existsSync(bundledDir)) continue;
226
+
227
+ const bundledIndex = join(bundledDir, "index.ts");
228
+ if (!existsSync(bundledIndex)) continue;
229
+
230
+ let needsUpdate = false;
231
+ try {
232
+ for (const file of ["index.ts", "package.json"]) {
233
+ const bundledFile = join(bundledDir, file);
234
+ const installedFile = join(installedDir, file);
235
+ if (!existsSync(bundledFile)) continue;
236
+ const bundledContent = readFileSync(bundledFile, "utf-8");
237
+ const installedContent = existsSync(installedFile)
238
+ ? readFileSync(installedFile, "utf-8")
239
+ : null;
240
+ if (bundledContent !== installedContent) {
241
+ needsUpdate = true;
242
+ break;
243
+ }
244
+ }
245
+ } catch {
246
+ needsUpdate = true;
247
+ }
248
+
249
+ if (!needsUpdate) continue;
250
+
251
+ logger.info("Bundled extension source updated — reinstalling", {
252
+ name: entry.name,
253
+ });
254
+
255
+ // Atomic swap: copy to a temp sibling dir first, then replace.
256
+ // This ensures a copy failure never leaves the extension deleted.
257
+ const tmpDir = `${installedDir}.tmp`;
258
+ rmSync(tmpDir, { recursive: true, force: true });
259
+ try {
260
+ cpSync(bundledDir, tmpDir, { recursive: true });
261
+
262
+ if (existsSync(join(tmpDir, "package.json"))) {
263
+ const installResult = spawnSync("bun", ["install"], {
264
+ stdio: "pipe",
265
+ encoding: "utf-8",
266
+ cwd: tmpDir,
267
+ });
268
+ if (installResult.status !== 0) {
269
+ // bun install failed — discard the temp dir; keep the old install.
270
+ rmSync(tmpDir, { recursive: true, force: true });
271
+ logger.warn(
272
+ "Extension sync skipped: bun install failed in bundled source",
273
+ {
274
+ name: entry.name,
275
+ stderr: installResult.stderr?.toString?.() ?? "",
276
+ },
277
+ );
278
+ continue;
279
+ }
280
+ }
281
+
282
+ // Swap: remove old, rename temp into place.
283
+ rmSync(installedDir, { recursive: true, force: true });
284
+ renameSync(tmpDir, installedDir);
285
+
286
+ // Sync skill dir to global skills.
287
+ const skillDir = join(installedDir, "skill");
288
+ if (existsSync(join(skillDir, "SKILL.md"))) {
289
+ const dst = join(globalDir, "skills", entry.name);
290
+ mkdirSync(dirname(dst), { recursive: true });
291
+ rmSync(dst, { recursive: true, force: true });
292
+ cpSync(skillDir, dst, { recursive: true });
293
+ }
294
+
295
+ logger.info("Extension reinstalled from bundled source", {
296
+ name: entry.name,
297
+ });
298
+ } catch (e) {
299
+ rmSync(tmpDir, { recursive: true, force: true });
300
+ logger.warn("Failed to reinstall extension from bundled source", {
301
+ name: entry.name,
302
+ error: e instanceof Error ? e.message : String(e),
303
+ });
304
+ }
305
+ }
306
+ }