wave-agent-sdk 1.1.5 → 1.3.0

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 (210) hide show
  1. package/dist/agent.d.ts +128 -28
  2. package/dist/agent.js +201 -49
  3. package/dist/builtin/index.js +2 -0
  4. package/dist/builtin/plugins.js +11 -20
  5. package/dist/builtin/skills/settings.js +7 -20
  6. package/dist/builtin/skills/wave-daemon.d.ts +1 -0
  7. package/dist/builtin/skills/wave-daemon.js +194 -0
  8. package/dist/constants/images.d.ts +26 -0
  9. package/dist/constants/images.js +26 -0
  10. package/dist/constants/index.d.ts +16 -0
  11. package/dist/constants/index.js +16 -0
  12. package/dist/constants/memory.d.ts +26 -0
  13. package/dist/constants/memory.js +34 -0
  14. package/dist/constants/messages.d.ts +11 -0
  15. package/dist/constants/messages.js +11 -0
  16. package/dist/constants/plugins.d.ts +8 -0
  17. package/dist/constants/plugins.js +8 -0
  18. package/dist/constants/tools.d.ts +1 -0
  19. package/dist/constants/tools.js +1 -0
  20. package/dist/core/plugin.d.ts +54 -10
  21. package/dist/core/plugin.js +137 -23
  22. package/dist/core/session.d.ts +1 -1
  23. package/dist/core/session.js +1 -1
  24. package/dist/exec/catalog.d.ts +140 -0
  25. package/dist/exec/catalog.js +470 -0
  26. package/dist/exec/catalogAnnouncement.d.ts +89 -0
  27. package/dist/exec/catalogAnnouncement.js +293 -0
  28. package/dist/exec/constants.d.ts +51 -0
  29. package/dist/exec/constants.js +51 -0
  30. package/dist/exec/execRuntime.d.ts +55 -0
  31. package/dist/exec/execRuntime.js +217 -0
  32. package/dist/exec/workerSource.d.ts +28 -0
  33. package/dist/exec/workerSource.js +299 -0
  34. package/dist/host/index.d.ts +23 -0
  35. package/dist/host/index.js +23 -0
  36. package/dist/index.d.ts +7 -1
  37. package/dist/index.js +8 -1
  38. package/dist/managers/MemoryRuleManager.d.ts +6 -0
  39. package/dist/managers/MemoryRuleManager.js +12 -0
  40. package/dist/managers/aiManager.d.ts +35 -25
  41. package/dist/managers/aiManager.js +204 -202
  42. package/dist/managers/backgroundTaskManager.js +14 -0
  43. package/dist/managers/bashModeManager.d.ts +33 -0
  44. package/dist/managers/bashModeManager.js +110 -0
  45. package/dist/managers/hookManager.d.ts +18 -0
  46. package/dist/managers/hookManager.js +37 -3
  47. package/dist/managers/liveConfigManager.d.ts +33 -0
  48. package/dist/managers/liveConfigManager.js +106 -11
  49. package/dist/managers/lspManager.d.ts +9 -0
  50. package/dist/managers/lspManager.js +47 -18
  51. package/dist/managers/mcpManager.d.ts +68 -10
  52. package/dist/managers/mcpManager.js +265 -15
  53. package/dist/managers/messageManager.d.ts +60 -18
  54. package/dist/managers/messageManager.js +170 -81
  55. package/dist/managers/permissionManager.d.ts +69 -0
  56. package/dist/managers/permissionManager.js +221 -78
  57. package/dist/managers/planManager.d.ts +9 -0
  58. package/dist/managers/planManager.js +19 -1
  59. package/dist/managers/pluginManager.d.ts +46 -2
  60. package/dist/managers/pluginManager.js +117 -11
  61. package/dist/managers/pluginScopeManager.d.ts +15 -2
  62. package/dist/managers/pluginScopeManager.js +20 -1
  63. package/dist/managers/skillManager.d.ts +50 -0
  64. package/dist/managers/skillManager.js +166 -12
  65. package/dist/managers/slashCommandManager.d.ts +10 -0
  66. package/dist/managers/slashCommandManager.js +44 -31
  67. package/dist/managers/subagentManager.d.ts +15 -0
  68. package/dist/managers/subagentManager.js +81 -9
  69. package/dist/managers/toolManager.d.ts +29 -3
  70. package/dist/managers/toolManager.js +87 -13
  71. package/dist/managers/workflowManager.js +6 -0
  72. package/dist/prompts/autoMemory.d.ts +9 -0
  73. package/dist/prompts/autoMemory.js +30 -31
  74. package/dist/prompts/autoMemoryExtraction.d.ts +4 -0
  75. package/dist/prompts/autoMemoryExtraction.js +8 -111
  76. package/dist/prompts/index.d.ts +0 -1
  77. package/dist/prompts/index.js +0 -4
  78. package/dist/prompts/memoryTypes.d.ts +63 -0
  79. package/dist/prompts/memoryTypes.js +191 -0
  80. package/dist/services/GitService.d.ts +7 -0
  81. package/dist/services/GitService.js +23 -0
  82. package/dist/services/MarketplaceService.d.ts +101 -17
  83. package/dist/services/MarketplaceService.js +323 -102
  84. package/dist/services/artifactContent.d.ts +84 -0
  85. package/dist/services/artifactContent.js +204 -0
  86. package/dist/services/artifactSession.d.ts +6 -0
  87. package/dist/services/artifactSession.js +17 -0
  88. package/dist/services/autoMemoryService.js +5 -13
  89. package/dist/services/configurationService.d.ts +92 -9
  90. package/dist/services/configurationService.js +246 -64
  91. package/dist/services/contentSummarizer.d.ts +15 -0
  92. package/dist/services/contentSummarizer.js +45 -0
  93. package/dist/services/execAvailability.d.ts +9 -0
  94. package/dist/services/execAvailability.js +32 -0
  95. package/dist/services/fileWatcher.js +61 -6
  96. package/dist/services/initializationService.js +21 -17
  97. package/dist/services/interactionService.d.ts +9 -1
  98. package/dist/services/interactionService.js +28 -8
  99. package/dist/services/jsonlHandler.d.ts +98 -0
  100. package/dist/services/jsonlHandler.js +250 -12
  101. package/dist/services/memory.d.ts +17 -1
  102. package/dist/services/memory.js +44 -7
  103. package/dist/services/officialMarketplaceMirror.d.ts +85 -0
  104. package/dist/services/officialMarketplaceMirror.js +290 -0
  105. package/dist/services/pluginLoader.d.ts +12 -4
  106. package/dist/services/pluginLoader.js +38 -7
  107. package/dist/services/remoteSettingsService.js +20 -6
  108. package/dist/services/session.d.ts +74 -0
  109. package/dist/services/session.js +174 -16
  110. package/dist/services/sessionEntries.d.ts +2 -0
  111. package/dist/services/sessionEntries.js +20 -0
  112. package/dist/services/worktreeHooks.js +6 -1
  113. package/dist/stdio/index.d.ts +12 -0
  114. package/dist/stdio/index.js +12 -0
  115. package/dist/stdio/notificationRouter.d.ts +38 -0
  116. package/dist/stdio/notificationRouter.js +97 -0
  117. package/dist/stdio/rpcClient.d.ts +18 -0
  118. package/dist/stdio/rpcClient.js +10 -0
  119. package/dist/stdio/stdioAgent.d.ts +229 -0
  120. package/dist/stdio/stdioAgent.js +360 -0
  121. package/dist/tools/artifactTool.js +406 -273
  122. package/dist/tools/bashTool.js +10 -6
  123. package/dist/tools/editTool.js +6 -3
  124. package/dist/tools/execTool.d.ts +2 -0
  125. package/dist/tools/execTool.js +165 -0
  126. package/dist/tools/exitPlanMode.js +10 -2
  127. package/dist/tools/grepTool.js +7 -1
  128. package/dist/tools/readTool.js +30 -2
  129. package/dist/tools/types.d.ts +34 -8
  130. package/dist/tools/webFetchTool.js +15 -166
  131. package/dist/tools/workflowTool.js +40 -8
  132. package/dist/tools/writeTool.js +6 -3
  133. package/dist/types/agent.d.ts +24 -1
  134. package/dist/types/commands.d.ts +7 -0
  135. package/dist/types/configuration.d.ts +45 -2
  136. package/dist/types/hooks.d.ts +1 -0
  137. package/dist/types/hooks.js +19 -0
  138. package/dist/types/marketplace.d.ts +40 -2
  139. package/dist/types/mcp.d.ts +42 -0
  140. package/dist/types/messaging.d.ts +1 -8
  141. package/dist/types/permissions.d.ts +22 -0
  142. package/dist/types/permissions.js +17 -0
  143. package/dist/types/plugins.d.ts +26 -2
  144. package/dist/types/skills.d.ts +26 -0
  145. package/dist/utils/bashParser.d.ts +17 -0
  146. package/dist/utils/bashParser.js +72 -0
  147. package/dist/utils/bashStructure/bashLexer.d.ts +96 -0
  148. package/dist/utils/bashStructure/bashLexer.js +676 -0
  149. package/dist/utils/bashStructure/bashParser.d.ts +144 -0
  150. package/dist/utils/bashStructure/bashParser.js +606 -0
  151. package/dist/utils/bashStructure/bashSemantics.d.ts +70 -0
  152. package/dist/utils/bashStructure/bashSemantics.js +477 -0
  153. package/dist/utils/bashStructure/index.d.ts +26 -0
  154. package/dist/utils/bashStructure/index.js +27 -0
  155. package/dist/utils/bashStructure/types.d.ts +62 -0
  156. package/dist/utils/bashStructure/types.js +47 -0
  157. package/dist/utils/constants.d.ts +10 -0
  158. package/dist/utils/constants.js +10 -0
  159. package/dist/utils/containerSetup.js +48 -6
  160. package/dist/utils/convertMessagesForAPI.d.ts +7 -1
  161. package/dist/utils/convertMessagesForAPI.js +64 -14
  162. package/dist/utils/fileChangeReminder.d.ts +20 -0
  163. package/dist/utils/fileChangeReminder.js +153 -0
  164. package/dist/utils/fileSearch.js +4 -3
  165. package/dist/utils/fileUtils.d.ts +44 -0
  166. package/dist/utils/fileUtils.js +118 -0
  167. package/dist/utils/frontmatterYaml.d.ts +33 -0
  168. package/dist/utils/frontmatterYaml.js +192 -0
  169. package/dist/utils/imageBudget.d.ts +85 -0
  170. package/dist/utils/imageBudget.js +109 -0
  171. package/dist/utils/imageDimensions.d.ts +83 -0
  172. package/dist/utils/imageDimensions.js +232 -0
  173. package/dist/utils/imageProcessor.d.ts +66 -0
  174. package/dist/utils/imageProcessor.js +84 -0
  175. package/dist/utils/imageRewrite.d.ts +29 -0
  176. package/dist/utils/imageRewrite.js +251 -0
  177. package/dist/utils/markdownParser.d.ts +5 -1
  178. package/dist/utils/markdownParser.js +9 -51
  179. package/dist/utils/mcpInstructions.d.ts +61 -0
  180. package/dist/utils/mcpInstructions.js +126 -0
  181. package/dist/utils/mcpUtils.d.ts +7 -0
  182. package/dist/utils/mcpUtils.js +11 -2
  183. package/dist/utils/memoryAge.d.ts +32 -0
  184. package/dist/utils/memoryAge.js +47 -0
  185. package/dist/utils/memoryEntrypoint.d.ts +20 -0
  186. package/dist/utils/memoryEntrypoint.js +49 -0
  187. package/dist/utils/memoryIndex.d.ts +30 -0
  188. package/dist/utils/memoryIndex.js +76 -0
  189. package/dist/utils/messageOperations.d.ts +6 -20
  190. package/dist/utils/messageOperations.js +40 -91
  191. package/dist/utils/nestedMemory.d.ts +22 -0
  192. package/dist/utils/nestedMemory.js +61 -0
  193. package/dist/utils/npmTarball.d.ts +19 -0
  194. package/dist/utils/npmTarball.js +92 -0
  195. package/dist/utils/pluginSource.d.ts +37 -0
  196. package/dist/utils/pluginSource.js +73 -0
  197. package/dist/utils/ripgrep.d.ts +18 -4
  198. package/dist/utils/ripgrep.js +56 -4
  199. package/dist/utils/runtimeDeps.d.ts +35 -0
  200. package/dist/utils/runtimeDeps.js +426 -0
  201. package/dist/utils/skillParser.js +22 -52
  202. package/dist/utils/subagentParser.js +48 -45
  203. package/dist/utils/tokenCalculation.js +0 -8
  204. package/dist/utils/userSettings.d.ts +90 -0
  205. package/dist/utils/userSettings.js +291 -0
  206. package/dist/utils/worktreeUtils.d.ts +2 -1
  207. package/dist/utils/worktreeUtils.js +64 -34
  208. package/package.json +12 -4
  209. package/dist/managers/bangManager.d.ts +0 -26
  210. package/dist/managers/bangManager.js +0 -78
@@ -1,8 +1,12 @@
1
1
  import { promises as fs, existsSync, mkdirSync } from "fs";
2
2
  import * as path from "path";
3
3
  import * as crypto from "crypto";
4
+ import { AsyncLocalStorage } from "async_hooks";
4
5
  import { getPluginsDir } from "../utils/configPaths.js";
5
6
  import { GitService } from "./GitService.js";
7
+ import { parsePluginSource, describePluginSource, } from "../utils/pluginSource.js";
8
+ import { DEFAULT_PLUGIN_VERSION } from "../constants/plugins.js";
9
+ import { ALLOW_OFFICIAL_MARKET_GIT_FALLBACK, fetchOfficialMarketplaceFromMirror, } from "./officialMarketplaceMirror.js";
6
10
  import { ConfigurationService } from "./configurationService.js";
7
11
  import { logger, logError, logWarn } from "../utils/globalLogger.js";
8
12
  /**
@@ -16,6 +20,8 @@ import { logger, logError, logWarn } from "../utils/globalLogger.js";
16
20
  */
17
21
  export class MarketplaceService {
18
22
  constructor(workdir = process.cwd(), configurationService = new ConfigurationService()) {
23
+ /** In-flight [refreshMarketplaces] call (single-flight, spec 插件市场 场景 13). */
24
+ this.refreshInFlight = null;
19
25
  this.workdir = workdir;
20
26
  this.configurationService = configurationService;
21
27
  this.pluginsDir = getPluginsDir();
@@ -58,7 +64,6 @@ export class MarketplaceService {
58
64
  continue;
59
65
  const config = {
60
66
  source: m.source,
61
- autoUpdate: m.autoUpdate,
62
67
  };
63
68
  await this.configurationService.addMarketplaceToScope(this.workdir, "user", m.name, config);
64
69
  }
@@ -81,12 +86,11 @@ export class MarketplaceService {
81
86
  // Add to user-scope settings if not already in any scope
82
87
  const scoped = this.configurationService.getMergedMarketplaces(this.workdir);
83
88
  if (!scoped[builtin.name]) {
84
- await this.configurationService.addMarketplaceToScope(this.workdir, "user", builtin.name, { source: builtin.source, autoUpdate: builtin.autoUpdate });
89
+ await this.configurationService.addMarketplaceToScope(this.workdir, "user", builtin.name, { source: builtin.source });
85
90
  }
86
91
  // Update cache: add entry + set builtinSeeded flag
87
92
  await this.updateCacheMarketplace(builtin.name, {
88
93
  source: builtin.source,
89
- autoUpdate: builtin.autoUpdate,
90
94
  isBuiltin: true,
91
95
  declaredScope: "builtin",
92
96
  });
@@ -132,10 +136,10 @@ export class MarketplaceService {
132
136
  }
133
137
  /**
134
138
  * Acquires a file-based lock (with PID tracking for stale lock detection) and executes the provided function.
135
- * Supports re-entrancy within the same process.
139
+ * Supports re-entrancy within the same call chain (see [lockChain]).
136
140
  */
137
141
  async withLock(fn) {
138
- if (MarketplaceService.isLockedInProcess) {
142
+ if (MarketplaceService.lockChain.getStore()) {
139
143
  return await fn();
140
144
  }
141
145
  let lockFd;
@@ -168,12 +172,10 @@ export class MarketplaceService {
168
172
  throw new Error(`Failed to acquire marketplace lock after ${maxRetries} retries. If no other wave-agent process is running, please delete ${this.lockPath}`);
169
173
  }
170
174
  await fs.writeFile(this.lockPath, String(process.pid), "utf-8");
171
- MarketplaceService.isLockedInProcess = true;
172
175
  try {
173
- return await fn();
176
+ return await MarketplaceService.lockChain.run(true, fn);
174
177
  }
175
178
  finally {
176
- MarketplaceService.isLockedInProcess = false;
177
179
  await lockFd.close();
178
180
  await fs.unlink(this.lockPath).catch(() => { });
179
181
  }
@@ -282,6 +284,36 @@ export class MarketplaceService {
282
284
  await fs.writeFile(tmpPath, JSON.stringify(registry, null, 2));
283
285
  await fs.rename(tmpPath, this.installedPluginsPath);
284
286
  }
287
+ /**
288
+ * Whether an install record belongs to the given install location.
289
+ *
290
+ * Records written before scopes were tracked carry no `scope`; they stand for
291
+ * "installed once on this machine" and match any scope, so uninstalling them
292
+ * keeps working (they are that plugin's only record). They still match by
293
+ * project path, so records of two different projects stay distinguishable.
294
+ */
295
+ matchesInstallLocation(entry, location) {
296
+ if (!entry.scope) {
297
+ return (entry.projectPath === undefined ||
298
+ entry.projectPath === location.projectPath);
299
+ }
300
+ return (entry.scope === location.scope &&
301
+ entry.projectPath === location.projectPath);
302
+ }
303
+ /**
304
+ * The install location a record is keyed by. Scope-less legacy records are
305
+ * treated as user-scope when they have to be re-created (e.g. by an update).
306
+ */
307
+ locationOf(entry) {
308
+ return { scope: entry.scope ?? "user", projectPath: entry.projectPath };
309
+ }
310
+ describeInstallLocations(entries) {
311
+ return entries
312
+ .map((entry) => entry.scope
313
+ ? `${entry.scope}${entry.projectPath ? ` (${entry.projectPath})` : ""}`
314
+ : "unscoped")
315
+ .join(", ");
316
+ }
285
317
  /**
286
318
  * Loads a marketplace manifest from a local path
287
319
  */
@@ -325,7 +357,6 @@ export class MarketplaceService {
325
357
  return {
326
358
  name,
327
359
  source: config.source,
328
- autoUpdate: config.autoUpdate ?? cache?.autoUpdate,
329
360
  lastUpdated: cache?.lastUpdated,
330
361
  isBuiltin: name === MarketplaceService.BUILTIN_MARKETPLACE.name,
331
362
  declaredScope,
@@ -345,6 +376,53 @@ export class MarketplaceService {
345
376
  return "builtin";
346
377
  return null;
347
378
  }
379
+ /**
380
+ * 已注册的市场(作用域配置 + 缓存注册表合并去重),用于添加前的判重
381
+ */
382
+ async listRegisteredMarketplaces() {
383
+ const scoped = this.configurationService.getMergedMarketplaces(this.workdir);
384
+ const registered = Object.entries(scoped).map(([name, config]) => ({
385
+ name,
386
+ source: config.source,
387
+ }));
388
+ const cache = await this.getCacheRegistry();
389
+ for (const entry of cache?.marketplaces ?? []) {
390
+ if (!registered.some((m) => m.name === entry.name)) {
391
+ registered.push({ name: entry.name, source: entry.source });
392
+ }
393
+ }
394
+ return registered;
395
+ }
396
+ /**
397
+ * 添加市场前判重(spec ecosystem/plugin「设置页插件市场」场景 16):来源重复或
398
+ * 清单声明的名称与已有市场同名时不允许添加。只传 source 时仅判来源(用于克隆
399
+ * 之前,避免为一个已存在的来源白克隆一次)。
400
+ */
401
+ async assertMarketplaceAddable(source, name) {
402
+ const registered = await this.listRegisteredMarketplaces();
403
+ const sameSource = registered.find((m) => MarketplaceService.isSameMarketplaceSource(m.source, source));
404
+ if (sameSource) {
405
+ throw new Error(`该市场来源已添加:${sameSource.name}`);
406
+ }
407
+ if (name && registered.some((m) => m.name === name)) {
408
+ throw new Error(`同名市场已存在:${name}`);
409
+ }
410
+ }
411
+ /** 两个市场来源是否指向同一处(同类型比 url/repo/path,忽略 ref) */
412
+ static isSameMarketplaceSource(a, b) {
413
+ if (a.source !== b.source)
414
+ return false;
415
+ if (a.source === "directory" && b.source === "directory") {
416
+ return a.path === b.path;
417
+ }
418
+ if (a.source === "github" && b.source === "github") {
419
+ return a.repo === b.repo;
420
+ }
421
+ if (a.source === "git" && b.source === "git") {
422
+ return a.url === b.url;
423
+ }
424
+ return false;
425
+ }
348
426
  /**
349
427
  * Adds a new marketplace (local directory, GitHub repo, or Git URL)
350
428
  */
@@ -367,6 +445,7 @@ export class MarketplaceService {
367
445
  const tempSource = isFullUrl
368
446
  ? { source: "git", url: urlOrRepo, ref }
369
447
  : { source: "github", repo: urlOrRepo, ref };
448
+ await this.assertMarketplaceAddable(tempSource);
370
449
  const targetPath = this.getMarketplacePath(tempSource);
371
450
  if (!existsSync(targetPath)) {
372
451
  try {
@@ -388,12 +467,15 @@ export class MarketplaceService {
388
467
  source: isFullUrl
389
468
  ? { source: "git", url: urlOrRepo, ref }
390
469
  : { source: "github", repo: urlOrRepo, ref },
391
- autoUpdate: false,
392
470
  lastUpdated: new Date().toISOString(),
393
471
  };
394
472
  }
395
473
  else {
396
474
  const absolutePath = path.resolve(input);
475
+ await this.assertMarketplaceAddable({
476
+ source: "directory",
477
+ path: absolutePath,
478
+ });
397
479
  let manifest;
398
480
  try {
399
481
  manifest = await this.loadMarketplaceManifest(absolutePath);
@@ -404,19 +486,18 @@ export class MarketplaceService {
404
486
  marketplace = {
405
487
  name: manifest.name,
406
488
  source: { source: "directory", path: absolutePath },
407
- autoUpdate: false,
408
489
  lastUpdated: new Date().toISOString(),
409
490
  };
410
491
  }
492
+ // 名称来自市场自身清单,加载后才知道 → 同名判重放在这里(来源判重已在分支内)
493
+ await this.assertMarketplaceAddable(marketplace.source, marketplace.name);
411
494
  const config = {
412
495
  source: marketplace.source,
413
- autoUpdate: marketplace.autoUpdate,
414
496
  };
415
497
  await this.configurationService.addMarketplaceToScope(this.workdir, scope, marketplace.name, config);
416
498
  // Update cache with metadata
417
499
  await this.updateCacheMarketplace(marketplace.name, {
418
500
  source: marketplace.source,
419
- autoUpdate: marketplace.autoUpdate,
420
501
  lastUpdated: marketplace.lastUpdated,
421
502
  });
422
503
  return marketplace;
@@ -496,8 +577,18 @@ export class MarketplaceService {
496
577
  }
497
578
  /**
498
579
  * Updates a specific marketplace or all marketplaces
580
+ *
581
+ * `refreshCheckout` (default true) pulls the checkout from its upstream; pass
582
+ * false to upgrade plugins against the current checkout only — the interactive
583
+ * "批量更新插件" path does that, because opening the plugin-marketplace surface
584
+ * already refreshed the checkout (spec 插件市场 A-013).
585
+ *
586
+ * Returns the number of installed plugins whose version actually changed
587
+ * during this run (0 = everything was already up to date) — the GUI hosts
588
+ * use it to tell "已更新 N 个插件" apart from "已是最新" (spec 插件市场 场景 13).
499
589
  */
500
590
  async updateMarketplace(name, options) {
591
+ const refreshCheckout = options?.refreshCheckout ?? true;
501
592
  return this.withLock(async () => {
502
593
  const marketplaces = await this.listMarketplaces();
503
594
  const toUpdate = name
@@ -508,44 +599,76 @@ export class MarketplaceService {
508
599
  }
509
600
  const isGitAvailable = await this.gitService.isGitAvailable();
510
601
  const errors = [];
602
+ let upgradedPlugins = 0;
511
603
  for (const marketplace of toUpdate) {
512
604
  try {
513
- if (marketplace.source.source === "github" ||
514
- marketplace.source.source === "git") {
515
- if (!isGitAvailable) {
516
- logWarn(`Skipping update for Git/GitHub marketplace "${marketplace.name}" because Git is not installed.`);
517
- continue;
518
- }
519
- const targetPath = this.getMarketplacePath(marketplace.source);
520
- if (existsSync(targetPath)) {
521
- await this.gitService.pull(targetPath);
522
- }
523
- else {
524
- let url;
525
- if (marketplace.source.source === "github") {
526
- url = marketplace.source.repo;
605
+ const targetPath = this.getMarketplacePath(marketplace.source);
606
+ if (refreshCheckout) {
607
+ // Builtin official marketplace: prefer the zip-snapshot mirror
608
+ // (content-addressed zip over a plain HTTP base, no git/GitHub
609
+ // needed). Only the builtin is special-cased by name — its
610
+ // `source` stays "github" in settings/cache, so there is zero
611
+ // data migration. Both envs' mirror channels are live and carry
612
+ // content (verified 2026-09-23); on mirror failure
613
+ // (network/timeout/404/bad content) fall back to the git path
614
+ // unless the kill switch forbids it.
615
+ const isOfficialBuiltin = marketplace.name === MarketplaceService.BUILTIN_MARKETPLACE.name;
616
+ let mirrorUpdated = false;
617
+ if (isOfficialBuiltin) {
618
+ const mirrorSha = await fetchOfficialMarketplaceFromMirror(targetPath, this.marketplacesDir);
619
+ if (mirrorSha !== null) {
620
+ mirrorUpdated = true;
621
+ }
622
+ else if (!ALLOW_OFFICIAL_MARKET_GIT_FALLBACK) {
623
+ logWarn(`Skipping update for official marketplace "${marketplace.name}": mirror fetch failed and git fallback is disabled.`);
624
+ continue;
527
625
  }
528
- else {
529
- url = marketplace.source.url;
626
+ }
627
+ if (marketplace.source.source === "github" ||
628
+ marketplace.source.source === "git") {
629
+ if (!mirrorUpdated) {
630
+ if (!isGitAvailable) {
631
+ logWarn(`Skipping update for Git/GitHub marketplace "${marketplace.name}" because Git is not installed.`);
632
+ continue;
633
+ }
634
+ if (existsSync(targetPath)) {
635
+ await this.gitService.pull(targetPath);
636
+ }
637
+ else {
638
+ let url;
639
+ if (marketplace.source.source === "github") {
640
+ url = marketplace.source.repo;
641
+ }
642
+ else {
643
+ url = marketplace.source.url;
644
+ }
645
+ await this.gitService.clone(url, targetPath, marketplace.source.ref);
646
+ }
530
647
  }
531
- await this.gitService.clone(url, targetPath, marketplace.source.ref);
532
648
  }
533
649
  }
534
- const manifest = await this.loadMarketplaceManifest(this.getMarketplacePath(marketplace.source));
535
- marketplace.lastUpdated = new Date().toISOString();
536
- // Update cache metadata
537
- await this.updateCacheMarketplace(marketplace.name, {
538
- lastUpdated: marketplace.lastUpdated,
539
- });
650
+ const manifest = await this.loadMarketplaceManifest(targetPath);
651
+ // 只在真的刷了检出时记账:跳过刷新时把 lastUpdated 顶到当前时间会
652
+ // 让 CLI 的市场列表谎报「上次更新」时间。
653
+ if (refreshCheckout) {
654
+ marketplace.lastUpdated = new Date().toISOString();
655
+ // Update cache metadata
656
+ await this.updateCacheMarketplace(marketplace.name, {
657
+ lastUpdated: marketplace.lastUpdated,
658
+ });
659
+ }
540
660
  if (options?.updatePlugins) {
541
661
  const installedRegistry = await this.getInstalledPlugins();
542
662
  const pluginsToUpdate = installedRegistry.plugins.filter((p) => p.marketplace === marketplace.name);
663
+ // 升级前版本快照:安装完成后比对,只有版本真的变了的才算「已更新」
664
+ // (重新安装同版本 = 无更新,宿主据此提示「已是最新」)。
665
+ const versionBefore = new Map(pluginsToUpdate.map((p) => [p.name, p.version]));
543
666
  for (const plugin of pluginsToUpdate) {
544
667
  const pluginEntry = manifest.plugins.find((p) => p.name === plugin.name);
545
668
  if (!pluginEntry) {
546
669
  logger.warn(`Plugin "${plugin.name}" no longer found in marketplace "${marketplace.name}". Uninstalling...`);
547
670
  try {
548
- await this.uninstallPlugin(`${plugin.name}@${plugin.marketplace}`, plugin.projectPath);
671
+ await this.uninstallPlugin(`${plugin.name}@${plugin.marketplace}`, this.locationOf(plugin));
549
672
  }
550
673
  catch (error) {
551
674
  logError(`Failed to uninstall orphaned plugin "${plugin.name}" from marketplace "${marketplace.name}":`, error);
@@ -553,12 +676,21 @@ export class MarketplaceService {
553
676
  continue;
554
677
  }
555
678
  try {
556
- await this.installPlugin(`${plugin.name}@${plugin.marketplace}`, plugin.projectPath);
679
+ await this.installPlugin(`${plugin.name}@${plugin.marketplace}`, this.locationOf(plugin));
557
680
  }
558
681
  catch (error) {
559
682
  logError(`Failed to update plugin "${plugin.name}" from marketplace "${marketplace.name}":`, error);
560
683
  }
561
684
  }
685
+ const afterRegistry = await this.getInstalledPlugins();
686
+ for (const plugin of afterRegistry.plugins) {
687
+ if (plugin.marketplace !== marketplace.name)
688
+ continue;
689
+ const before = versionBefore.get(plugin.name);
690
+ if (before !== undefined && before !== plugin.version) {
691
+ upgradedPlugins++;
692
+ }
693
+ }
562
694
  }
563
695
  }
564
696
  catch (error) {
@@ -570,53 +702,63 @@ export class MarketplaceService {
570
702
  if (errors.length > 0) {
571
703
  throw new Error(`Some marketplaces failed to update:\n${errors.join("\n")}`);
572
704
  }
705
+ return upgradedPlugins;
573
706
  });
574
707
  }
575
708
  /**
576
- * Automatically updates all marketplaces that have auto-update enabled
709
+ * 交互界面的「批量更新插件」:按当前清单把该市场内所有已安装且有新版本的插件
710
+ * 升级到最新,**不拉取检出**——清单的新鲜度由打开插件市场界面时的那次刷新保证
711
+ * (spec 插件市场 A-013/场景 14)。检出尚未据此刷新的场景(无界面可依赖的非交互
712
+ * 命令)走 [updateMarketplace] 的默认行为。
577
713
  */
578
- async autoUpdateAll() {
579
- return this.withLock(async () => {
580
- const scopedMarketplaces = this.configurationService.getMergedMarketplaces(this.workdir);
581
- const toAutoUpdate = Object.entries(scopedMarketplaces)
582
- .filter(([, config]) => config.autoUpdate)
583
- .map(([name]) => name);
584
- for (const marketplaceName of toAutoUpdate) {
585
- try {
586
- await this.updateMarketplace(marketplaceName, {
587
- updatePlugins: true,
588
- });
589
- }
590
- catch (error) {
591
- logError(`Auto-update failed for marketplace "${marketplaceName}":`, error);
592
- }
593
- }
714
+ async updateMarketplacePlugins(name) {
715
+ return await this.updateMarketplace(name, {
716
+ updatePlugins: true,
717
+ refreshCheckout: false,
594
718
  });
595
719
  }
596
720
  /**
597
- * Toggles auto-update for a marketplace
721
+ * Refreshes the local checkout of every registered marketplace.
722
+ *
723
+ * Callers are the plugin-marketplace surfaces (GUI settings view / CLI plugin
724
+ * manager), refreshed when the user opens them — the agent-startup path no
725
+ * longer refreshes anything (spec 插件市场 A-013 场景 5).
726
+ *
727
+ * This is a fixed behavior with no opt-in/opt-out: `autoUpdate` no longer
728
+ * exists, so all marketplaces are refreshed alike (A-014). Refreshing only
729
+ * updates the checkout — installed plugins are never reinstalled here, so a
730
+ * newer plugin version surfaces as an explicit "更新" (per plugin) /
731
+ * "批量更新插件" (batch) action instead of being applied behind the user's
732
+ * back.
733
+ *
734
+ * Single-flight: a call landing while a refresh is running reuses it instead
735
+ * of pulling every checkout twice (spec 场景 13).
598
736
  */
599
- async toggleAutoUpdate(name, enabled) {
600
- return this.withLock(async () => {
601
- const declaringSource = this.getMarketplaceDeclaringSource(name);
602
- if (!declaringSource || declaringSource === "builtin") {
603
- throw new Error(`Marketplace ${name} not found`);
604
- }
605
- const scoped = this.configurationService.getScopedMarketplaces(this.workdir, declaringSource);
606
- const config = scoped[name];
607
- if (!config) {
608
- throw new Error(`Marketplace ${name} not found`);
737
+ async refreshMarketplaces() {
738
+ if (this.refreshInFlight)
739
+ return this.refreshInFlight;
740
+ this.refreshInFlight = this.withLock(async () => {
741
+ const marketplaces = this.configurationService.getMergedMarketplaces(this.workdir);
742
+ for (const marketplaceName of Object.keys(marketplaces)) {
743
+ try {
744
+ await this.updateMarketplace(marketplaceName);
745
+ }
746
+ catch (error) {
747
+ logError(`Marketplace refresh failed for "${marketplaceName}":`, error);
748
+ }
609
749
  }
610
- config.autoUpdate = enabled;
611
- await this.configurationService.addMarketplaceToScope(this.workdir, declaringSource, name, config);
612
- // Also update cache
613
- await this.updateCacheMarketplace(name, { autoUpdate: enabled });
750
+ }).finally(() => {
751
+ this.refreshInFlight = null;
614
752
  });
753
+ return this.refreshInFlight;
615
754
  }
616
755
  /**
617
756
  * Installs a plugin from a marketplace
757
+ *
758
+ * @param location 该次安装记账的位置(spec plugin A-015)。缺省只在内部调用
759
+ * (宿主加载时补装)出现,此时写入一条不带作用域的历史形态记录。
618
760
  */
619
- async installPlugin(pluginAtMarketplace, projectPath) {
761
+ async installPlugin(pluginAtMarketplace, location) {
620
762
  return this.withLock(async () => {
621
763
  const [pluginName, marketplaceName] = pluginAtMarketplace.split("@");
622
764
  if (!pluginName || !marketplaceName) {
@@ -633,25 +775,32 @@ export class MarketplaceService {
633
775
  if (!pluginEntry) {
634
776
  throw new Error(`Plugin ${pluginName} not found in marketplace ${marketplaceName}`);
635
777
  }
636
- const isGitSource = pluginEntry.source.startsWith("http://") ||
637
- pluginEntry.source.startsWith("https://") ||
638
- pluginEntry.source.startsWith("git@") ||
639
- pluginEntry.source.startsWith("ssh://");
778
+ const parsedSource = parsePluginSource(pluginEntry.source);
779
+ if (!parsedSource) {
780
+ throw new Error(`Plugin ${pluginName} in marketplace ${marketplaceName} declares an unsupported source: ${describePluginSource(pluginEntry.source)}`);
781
+ }
782
+ const isGitSource = parsedSource.kind === "git";
640
783
  let pluginSrcPath;
641
784
  let tempCloneDir;
642
785
  try {
643
- if (isGitSource) {
786
+ if (parsedSource.kind === "git") {
644
787
  tempCloneDir = path.join(this.tmpDir, `clone-${Date.now()}`);
645
- let url = pluginEntry.source;
646
- let ref;
647
- if (url.includes("#")) {
648
- [url, ref] = url.split("#");
788
+ await this.gitService.clone(parsedSource.url, tempCloneDir, parsedSource.ref);
789
+ if (parsedSource.sha) {
790
+ // Without this a `ref` would drift with its branch; a sha that cannot
791
+ // be checked out fails loudly instead of silently using the branch tip
792
+ // (spec plugin A-021 场景 3).
793
+ await this.gitService.checkout(tempCloneDir, parsedSource.sha);
794
+ }
795
+ pluginSrcPath = parsedSource.subdir
796
+ ? path.join(tempCloneDir, parsedSource.subdir)
797
+ : tempCloneDir;
798
+ if (parsedSource.subdir && !existsSync(pluginSrcPath)) {
799
+ throw new Error(`Subdirectory '${parsedSource.subdir}' not found in repository ${parsedSource.url}`);
649
800
  }
650
- await this.gitService.clone(url, tempCloneDir, ref);
651
- pluginSrcPath = tempCloneDir;
652
801
  }
653
802
  else {
654
- pluginSrcPath = path.resolve(marketplacePath, pluginEntry.source);
803
+ pluginSrcPath = path.resolve(marketplacePath, parsedSource.path);
655
804
  }
656
805
  let pluginManifestPath = path.join(pluginSrcPath, ".wave-plugin", "plugin.json");
657
806
  if (!existsSync(pluginManifestPath)) {
@@ -663,7 +812,7 @@ export class MarketplaceService {
663
812
  }
664
813
  const pluginManifestContent = await fs.readFile(pluginManifestPath, "utf-8");
665
814
  const pluginManifest = JSON.parse(pluginManifestContent);
666
- const version = pluginManifest.version || "1.0.0";
815
+ const version = pluginManifest.version || DEFAULT_PLUGIN_VERSION;
667
816
  const tmpPluginDir = path.join(this.tmpDir, `${pluginName}-${Date.now()}`);
668
817
  try {
669
818
  if (isGitSource) {
@@ -682,13 +831,14 @@ export class MarketplaceService {
682
831
  const installedRegistry = await this.getInstalledPlugins();
683
832
  const existingIndex = installedRegistry.plugins.findIndex((p) => p.name === pluginName &&
684
833
  p.marketplace === marketplaceName &&
685
- p.projectPath === projectPath);
834
+ (location ? this.matchesInstallLocation(p, location) : !p.scope));
686
835
  const installedPlugin = {
687
836
  name: pluginName,
688
837
  marketplace: marketplaceName,
689
838
  version,
690
839
  cachePath,
691
- projectPath,
840
+ scope: location?.scope,
841
+ projectPath: location?.projectPath,
692
842
  };
693
843
  if (existingIndex >= 0) {
694
844
  installedRegistry.plugins[existingIndex] = installedPlugin;
@@ -716,45 +866,116 @@ export class MarketplaceService {
716
866
  }
717
867
  /**
718
868
  * Uninstalls a plugin
869
+ *
870
+ * With a [location], only that scope's install record is removed — other
871
+ * scopes (including other projects' project/local records) keep theirs, and
872
+ * the shared cache directory is deleted only once no record references it
873
+ * (spec plugin A-015). Without one, every record of the plugin is removed
874
+ * (used by [updatePlugin] and the marketplace-level cleanup).
719
875
  */
720
- async uninstallPlugin(pluginAtMarketplace, projectPath) {
876
+ async uninstallPlugin(pluginAtMarketplace, location) {
721
877
  return this.withLock(async () => {
722
878
  const [pluginName, marketplaceName] = pluginAtMarketplace.split("@");
723
879
  if (!pluginName || !marketplaceName) {
724
880
  throw new Error("Invalid plugin format. Use name@marketplace");
725
881
  }
726
882
  const installedRegistry = await this.getInstalledPlugins();
727
- const pluginIndex = installedRegistry.plugins.findIndex((p) => p.name === pluginName &&
728
- p.marketplace === marketplaceName &&
729
- p.projectPath === projectPath);
730
- if (pluginIndex === -1) {
731
- throw new Error(`Plugin ${pluginName}@${marketplaceName} is not installed${projectPath ? ` for project ${projectPath}` : ""}`);
732
- }
733
- const pluginToRemove = installedRegistry.plugins[pluginIndex];
734
- installedRegistry.plugins.splice(pluginIndex, 1);
883
+ const allRecords = installedRegistry.plugins.filter((p) => p.name === pluginName && p.marketplace === marketplaceName);
884
+ const pluginToRemove = allRecords.filter((p) => !location || this.matchesInstallLocation(p, location));
885
+ if (pluginToRemove.length === 0) {
886
+ const installedAt = this.describeInstallLocations(allRecords);
887
+ throw new Error(location
888
+ ? `Plugin ${pluginName}@${marketplaceName} is not installed in ${location.scope} scope${location.projectPath ? ` for project ${location.projectPath}` : ""}${installedAt ? `. Installed at: ${installedAt}` : ""}`
889
+ : `Plugin ${pluginName}@${marketplaceName} is not installed`);
890
+ }
891
+ const removed = new Set(pluginToRemove);
892
+ installedRegistry.plugins = installedRegistry.plugins.filter((p) => !removed.has(p));
735
893
  await this.saveInstalledPlugins(installedRegistry);
736
- const isStillReferenced = installedRegistry.plugins.some((p) => p.cachePath === pluginToRemove.cachePath);
737
- if (!isStillReferenced && existsSync(pluginToRemove.cachePath)) {
738
- await fs.rm(pluginToRemove.cachePath, { recursive: true, force: true });
894
+ for (const plugin of pluginToRemove) {
895
+ const isStillReferenced = installedRegistry.plugins.some((p) => p.cachePath === plugin.cachePath);
896
+ if (!isStillReferenced && existsSync(plugin.cachePath)) {
897
+ await fs.rm(plugin.cachePath, { recursive: true, force: true });
898
+ }
739
899
  }
740
900
  });
741
901
  }
742
902
  /**
743
- * Updates a plugin (uninstall followed by install)
903
+ * Moves a plugin's install record to another location(「更换安装作用域」):
904
+ * the record currently at [from] (or the plugin's only record when [from] is
905
+ * omitted or unknown) is rewritten to [to], so that uninstalling the plugin
906
+ * at its new scope still finds it (spec plugin A-015). A duplicate record
907
+ * already at [to] wins and the moved one is dropped.
908
+ */
909
+ async relocatePlugin(pluginAtMarketplace, from, to) {
910
+ return this.withLock(async () => {
911
+ const [pluginName, marketplaceName] = pluginAtMarketplace.split("@");
912
+ if (!pluginName || !marketplaceName) {
913
+ throw new Error("Invalid plugin format. Use name@marketplace");
914
+ }
915
+ const installedRegistry = await this.getInstalledPlugins();
916
+ const records = installedRegistry.plugins.filter((p) => p.name === pluginName && p.marketplace === marketplaceName);
917
+ const source = from
918
+ ? records.find((p) => this.matchesInstallLocation(p, from))
919
+ : records[0];
920
+ if (!source)
921
+ return;
922
+ const duplicate = records.find((p) => p !== source && this.matchesInstallLocation(p, to));
923
+ if (duplicate) {
924
+ installedRegistry.plugins = installedRegistry.plugins.filter((p) => p !== source);
925
+ }
926
+ else {
927
+ source.scope = to.scope;
928
+ source.projectPath = to.projectPath;
929
+ }
930
+ await this.saveInstalledPlugins(installedRegistry);
931
+ });
932
+ }
933
+ /**
934
+ * Updates a plugin (uninstall followed by install). Every install record of
935
+ * the plugin is re-created at its original location, so the install scopes
936
+ * stay unchanged (spec plugin「设置页插件市场」场景 12 / A-015).
744
937
  */
745
938
  async updatePlugin(pluginAtMarketplace) {
746
939
  return this.withLock(async () => {
940
+ const [pluginName, marketplaceName] = pluginAtMarketplace.split("@");
941
+ const before = await this.getInstalledPlugins();
942
+ const locations = before.plugins
943
+ .filter((p) => p.name === pluginName && p.marketplace === marketplaceName)
944
+ .map((p) => this.locationOf(p));
747
945
  await this.uninstallPlugin(pluginAtMarketplace);
748
- return this.installPlugin(pluginAtMarketplace);
946
+ const installed = await this.installPlugin(pluginAtMarketplace, locations[0] ?? { scope: "user" });
947
+ if (locations.length > 1) {
948
+ const restored = await this.getInstalledPlugins();
949
+ for (const location of locations.slice(1)) {
950
+ restored.plugins.push({
951
+ name: installed.name,
952
+ marketplace: installed.marketplace,
953
+ version: installed.version,
954
+ cachePath: installed.cachePath,
955
+ scope: location.scope,
956
+ projectPath: location.projectPath,
957
+ });
958
+ }
959
+ await this.saveInstalledPlugins(restored);
960
+ }
961
+ return installed;
749
962
  });
750
963
  }
751
964
  }
752
- MarketplaceService.isLockedInProcess = false;
965
+ /**
966
+ * 已持有市场操作锁的调用链标记(重入判据)。
967
+ *
968
+ * 用 AsyncLocalStorage 而不是进程内布尔值:锁内发起的嵌套调用(如
969
+ * refreshMarketplaces → updateMarketplace → installPlugin)必须跳过再次加锁,
970
+ * 否则会自锁;但**另一条并发调用链**必须老老实实排队。布尔值无法区分这两者
971
+ * ——第二个并发调用会以为「已在锁内」而跳过锁直接执行,于是两个操作并行改同一
972
+ * 个市场检出(并发两次 `git pull` 同一检出必然失败,spec 插件市场 场景 14)。
973
+ */
974
+ MarketplaceService.lockChain = new AsyncLocalStorage();
753
975
  MarketplaceService.BUILTIN_MARKETPLACE = {
754
976
  name: "wave-plugins-official",
755
977
  source: {
756
978
  source: "github",
757
979
  repo: "netease-lcap/wave-plugins-official",
758
980
  },
759
- autoUpdate: true,
760
981
  };