wave-agent-sdk 0.13.6 → 0.14.1

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 (85) hide show
  1. package/dist/agent.d.ts.map +1 -1
  2. package/dist/agent.js +4 -2
  3. package/dist/core/plugin.d.ts +2 -2
  4. package/dist/core/plugin.d.ts.map +1 -1
  5. package/dist/core/plugin.js +7 -7
  6. package/dist/managers/aiManager.d.ts +3 -0
  7. package/dist/managers/aiManager.d.ts.map +1 -1
  8. package/dist/managers/aiManager.js +93 -8
  9. package/dist/managers/backgroundTaskManager.d.ts.map +1 -1
  10. package/dist/managers/backgroundTaskManager.js +0 -12
  11. package/dist/managers/messageManager.d.ts +15 -0
  12. package/dist/managers/messageManager.d.ts.map +1 -1
  13. package/dist/managers/messageManager.js +52 -2
  14. package/dist/managers/permissionManager.d.ts +4 -0
  15. package/dist/managers/permissionManager.d.ts.map +1 -1
  16. package/dist/managers/permissionManager.js +6 -0
  17. package/dist/managers/pluginManager.d.ts.map +1 -1
  18. package/dist/managers/pluginManager.js +1 -1
  19. package/dist/managers/subagentManager.d.ts.map +1 -1
  20. package/dist/managers/subagentManager.js +23 -17
  21. package/dist/prompts/index.d.ts +2 -1
  22. package/dist/prompts/index.d.ts.map +1 -1
  23. package/dist/prompts/index.js +50 -25
  24. package/dist/services/MarketplaceService.d.ts +53 -12
  25. package/dist/services/MarketplaceService.d.ts.map +1 -1
  26. package/dist/services/MarketplaceService.js +311 -123
  27. package/dist/services/aiService.d.ts.map +1 -1
  28. package/dist/services/aiService.js +11 -1
  29. package/dist/services/configurationService.d.ts +17 -1
  30. package/dist/services/configurationService.d.ts.map +1 -1
  31. package/dist/services/configurationService.js +104 -0
  32. package/dist/services/pluginLoader.d.ts +6 -0
  33. package/dist/services/pluginLoader.d.ts.map +1 -1
  34. package/dist/services/pluginLoader.js +52 -7
  35. package/dist/tools/agentTool.d.ts.map +1 -1
  36. package/dist/tools/agentTool.js +14 -2
  37. package/dist/tools/bashTool.d.ts.map +1 -1
  38. package/dist/tools/bashTool.js +27 -5
  39. package/dist/tools/types.d.ts +1 -0
  40. package/dist/tools/types.d.ts.map +1 -1
  41. package/dist/tools/webFetchTool.d.ts.map +1 -1
  42. package/dist/tools/webFetchTool.js +202 -78
  43. package/dist/types/configuration.d.ts +7 -0
  44. package/dist/types/configuration.d.ts.map +1 -1
  45. package/dist/types/marketplace.d.ts +28 -1
  46. package/dist/types/marketplace.d.ts.map +1 -1
  47. package/dist/types/messaging.d.ts +1 -0
  48. package/dist/types/messaging.d.ts.map +1 -1
  49. package/dist/types/plugins.d.ts +13 -1
  50. package/dist/types/plugins.d.ts.map +1 -1
  51. package/dist/utils/convertMessagesForAPI.js +1 -1
  52. package/dist/utils/groupMessagesByApiRound.d.ts +24 -0
  53. package/dist/utils/groupMessagesByApiRound.d.ts.map +1 -0
  54. package/dist/utils/groupMessagesByApiRound.js +97 -0
  55. package/dist/utils/messageOperations.d.ts +1 -0
  56. package/dist/utils/messageOperations.d.ts.map +1 -1
  57. package/dist/utils/microcompact.d.ts +7 -0
  58. package/dist/utils/microcompact.d.ts.map +1 -0
  59. package/dist/utils/microcompact.js +78 -0
  60. package/package.json +2 -1
  61. package/src/agent.ts +4 -2
  62. package/src/core/plugin.ts +13 -7
  63. package/src/managers/aiManager.ts +117 -15
  64. package/src/managers/backgroundTaskManager.ts +1 -20
  65. package/src/managers/messageManager.ts +64 -2
  66. package/src/managers/permissionManager.ts +7 -0
  67. package/src/managers/pluginManager.ts +4 -1
  68. package/src/managers/subagentManager.ts +28 -24
  69. package/src/prompts/index.ts +51 -25
  70. package/src/services/MarketplaceService.ts +425 -134
  71. package/src/services/aiService.ts +14 -1
  72. package/src/services/configurationService.ts +131 -0
  73. package/src/services/pluginLoader.ts +66 -7
  74. package/src/tools/agentTool.ts +14 -2
  75. package/src/tools/bashTool.ts +27 -5
  76. package/src/tools/types.ts +1 -0
  77. package/src/tools/webFetchTool.ts +276 -86
  78. package/src/types/configuration.ts +8 -0
  79. package/src/types/marketplace.ts +26 -1
  80. package/src/types/messaging.ts +1 -0
  81. package/src/types/plugins.ts +13 -1
  82. package/src/utils/convertMessagesForAPI.ts +1 -1
  83. package/src/utils/groupMessagesByApiRound.ts +120 -0
  84. package/src/utils/messageOperations.ts +1 -0
  85. package/src/utils/microcompact.ts +101 -0
@@ -784,6 +784,19 @@ export async function compressMessages(
784
784
  await acquireSlot(abortSignal);
785
785
  }
786
786
 
787
+ // Strip images from messages before compact API call to reduce token usage
788
+ const cleanedMessages = messages.map((msg) => {
789
+ // Handle user/assistant messages with array content
790
+ if (Array.isArray(msg.content)) {
791
+ const textParts = msg.content.filter(
792
+ (part) => part.type === "text",
793
+ ) as import("openai/resources.js").ChatCompletionContentPartText[];
794
+ const text = textParts.map((p) => p.text).join("\n");
795
+ return { ...msg, content: text || "(empty message)" };
796
+ }
797
+ return msg;
798
+ });
799
+
787
800
  // Create OpenAI client with injected configuration
788
801
  const openai = new OpenAIClient({
789
802
  apiKey: gatewayConfig.apiKey,
@@ -821,7 +834,7 @@ export async function compressMessages(
821
834
  role: "system",
822
835
  content: COMPRESS_MESSAGES_SYSTEM_PROMPT,
823
836
  },
824
- ...messages,
837
+ ...cleanedMessages,
825
838
  {
826
839
  role: "user",
827
840
  content: `Please create a detailed summary of the conversation so far.`,
@@ -15,6 +15,7 @@ import type {
15
15
  ConfigurationPaths,
16
16
  WaveConfiguration,
17
17
  Scope,
18
+ MarketplaceConfig,
18
19
  } from "../types/configuration.js";
19
20
  import {
20
21
  getAllConfigPaths,
@@ -796,6 +797,124 @@ export class ConfigurationService {
796
797
  await fs.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
797
798
  }
798
799
 
800
+ /**
801
+ * Get merged marketplaces from all scopes
802
+ */
803
+ getMergedMarketplaces(workdir: string): Record<string, MarketplaceConfig> {
804
+ const mergedConfig = loadMergedWaveConfig(workdir);
805
+ return mergedConfig?.marketplaces || {};
806
+ }
807
+
808
+ /**
809
+ * Get marketplaces at a specific scope
810
+ */
811
+ getScopedMarketplaces(
812
+ workdir: string,
813
+ scope: Scope,
814
+ ): Record<string, MarketplaceConfig> {
815
+ let configPath: string;
816
+ if (scope === "user") {
817
+ configPath = getUserConfigPaths()[0];
818
+ } else if (scope === "project") {
819
+ configPath = getProjectConfigPaths(workdir)[1];
820
+ } else {
821
+ configPath = getProjectConfigPaths(workdir)[0];
822
+ }
823
+ const config = loadWaveConfigFromFile(configPath);
824
+ return config?.marketplaces || {};
825
+ }
826
+
827
+ /**
828
+ * Add a marketplace to the specified scope
829
+ */
830
+ async addMarketplaceToScope(
831
+ workdir: string,
832
+ scope: Scope,
833
+ name: string,
834
+ config: MarketplaceConfig,
835
+ ): Promise<void> {
836
+ if (scope !== "user" && !existsSync(workdir)) {
837
+ throw new Error(`Working directory does not exist: ${workdir}`);
838
+ }
839
+
840
+ let configPath: string;
841
+ if (scope === "user") {
842
+ configPath = getUserConfigPaths()[0];
843
+ } else if (scope === "project") {
844
+ configPath = getProjectConfigPaths(workdir)[1];
845
+ } else {
846
+ configPath = getProjectConfigPaths(workdir)[0];
847
+ }
848
+
849
+ const configDir = path.dirname(configPath);
850
+ if (!existsSync(configDir)) {
851
+ await fs.mkdir(configDir, { recursive: true });
852
+ }
853
+
854
+ let fileConfig: WaveConfiguration = {};
855
+ if (existsSync(configPath)) {
856
+ try {
857
+ const content = await fs.readFile(configPath, "utf-8");
858
+ fileConfig = JSON.parse(content);
859
+ } catch {
860
+ // Start with empty config if file is corrupted
861
+ }
862
+ }
863
+
864
+ if (!fileConfig.marketplaces) {
865
+ fileConfig.marketplaces = {};
866
+ }
867
+ fileConfig.marketplaces[name] = config;
868
+
869
+ await fs.writeFile(
870
+ configPath,
871
+ JSON.stringify(fileConfig, null, 2),
872
+ "utf-8",
873
+ );
874
+ }
875
+
876
+ /**
877
+ * Remove a marketplace from the specified scope
878
+ */
879
+ async removeMarketplaceFromScope(
880
+ workdir: string,
881
+ scope: Scope,
882
+ name: string,
883
+ ): Promise<void> {
884
+ if (scope !== "user" && !existsSync(workdir)) {
885
+ throw new Error(`Working directory does not exist: ${workdir}`);
886
+ }
887
+
888
+ let configPath: string;
889
+ if (scope === "user") {
890
+ configPath = getUserConfigPaths()[0];
891
+ } else if (scope === "project") {
892
+ configPath = getProjectConfigPaths(workdir)[1];
893
+ } else {
894
+ configPath = getProjectConfigPaths(workdir)[0];
895
+ }
896
+
897
+ if (!existsSync(configPath)) {
898
+ return;
899
+ }
900
+
901
+ try {
902
+ const content = await fs.readFile(configPath, "utf-8");
903
+ const fileConfig: WaveConfiguration = JSON.parse(content);
904
+
905
+ if (fileConfig.marketplaces && name in fileConfig.marketplaces) {
906
+ delete fileConfig.marketplaces[name];
907
+ await fs.writeFile(
908
+ configPath,
909
+ JSON.stringify(fileConfig, null, 2),
910
+ "utf-8",
911
+ );
912
+ }
913
+ } catch {
914
+ // Ignore errors for corrupted or non-existent files
915
+ }
916
+ }
917
+
799
918
  /**
800
919
  * Remove a plugin from the enabled plugins in the specified scope
801
920
  */
@@ -991,6 +1110,7 @@ export function loadWaveConfigFromFile(
991
1110
  ? config.autoMemoryEnabled
992
1111
  : undefined,
993
1112
  models: config.models || undefined,
1113
+ marketplaces: config.marketplaces || undefined,
994
1114
  };
995
1115
  } catch (error) {
996
1116
  if (error instanceof SyntaxError) {
@@ -1124,6 +1244,12 @@ export function loadMergedWaveConfig(
1124
1244
  mergedConfig.autoMemoryFrequency = config.autoMemoryFrequency;
1125
1245
  }
1126
1246
 
1247
+ // Merge marketplaces (last one wins for same key)
1248
+ if (config.marketplaces) {
1249
+ if (!mergedConfig.marketplaces) mergedConfig.marketplaces = {};
1250
+ Object.assign(mergedConfig.marketplaces, config.marketplaces);
1251
+ }
1252
+
1127
1253
  // Merge models
1128
1254
  if (config.models) {
1129
1255
  if (!mergedConfig.models) mergedConfig.models = {};
@@ -1157,6 +1283,11 @@ export function loadMergedWaveConfig(
1157
1283
  : undefined,
1158
1284
  language: mergedConfig.language,
1159
1285
  autoMemoryEnabled: mergedConfig.autoMemoryEnabled,
1286
+ marketplaces:
1287
+ mergedConfig.marketplaces &&
1288
+ Object.keys(mergedConfig.marketplaces).length > 0
1289
+ ? mergedConfig.marketplaces
1290
+ : undefined,
1160
1291
  models:
1161
1292
  mergedConfig.models && Object.keys(mergedConfig.models).length > 0
1162
1293
  ? mergedConfig.models
@@ -1,4 +1,5 @@
1
1
  import * as fs from "fs/promises";
2
+ import * as fsSync from "fs";
2
3
  import * as path from "path";
3
4
  import {
4
5
  PluginManifest,
@@ -13,21 +14,79 @@ import { parseSkillFile } from "../utils/skillParser.js";
13
14
  import { resolveMcpConfig } from "../managers/mcpManager.js";
14
15
 
15
16
  export class PluginLoader {
17
+ /**
18
+ * Finds the first existing plugin manifest path.
19
+ * Prefers .wave-plugin/ for backward compatibility, falls back to .claude-plugin/.
20
+ * Returns null if neither exists.
21
+ */
22
+ private static findPluginManifestPath(pluginPath: string): string | null {
23
+ const waveManifestPath = path.join(
24
+ pluginPath,
25
+ ".wave-plugin",
26
+ "plugin.json",
27
+ );
28
+ const claudeManifestPath = path.join(
29
+ pluginPath,
30
+ ".claude-plugin",
31
+ "plugin.json",
32
+ );
33
+
34
+ // Check .wave-plugin first for backward compatibility
35
+ try {
36
+ const waveStat = fsSync.statSync(waveManifestPath);
37
+ if (waveStat.isFile()) {
38
+ return waveManifestPath;
39
+ }
40
+ } catch {
41
+ // .wave-plugin/plugin.json doesn't exist
42
+ }
43
+
44
+ try {
45
+ const claudeStat = fsSync.statSync(claudeManifestPath);
46
+ if (claudeStat.isFile()) {
47
+ return claudeManifestPath;
48
+ }
49
+ } catch {
50
+ // .claude-plugin/plugin.json doesn't exist
51
+ }
52
+
53
+ return null;
54
+ }
55
+
16
56
  /**
17
57
  * Load and validate a plugin manifest from a directory
18
58
  * @param pluginPath Absolute path to the plugin directory
19
59
  */
20
60
  static async loadManifest(pluginPath: string): Promise<PluginManifest> {
21
- const dotWavePluginPath = path.join(pluginPath, ".wave-plugin");
22
- const manifestPath = path.join(dotWavePluginPath, "plugin.json");
61
+ const manifestPath = this.findPluginManifestPath(pluginPath);
62
+ if (!manifestPath) {
63
+ throw new Error(
64
+ `Plugin manifest not found at ${pluginPath}. Neither .wave-plugin/plugin.json nor .claude-plugin/plugin.json exists.`,
65
+ );
66
+ }
23
67
 
24
- // T018: Ensure plugin.json is the only file in .wave-plugin/
68
+ // Determine which directory is being used for validation
69
+ const pluginDirName = manifestPath.includes(".claude-plugin")
70
+ ? ".claude-plugin"
71
+ : ".wave-plugin";
72
+ const pluginDirPath = path.join(pluginPath, pluginDirName);
73
+
74
+ // T018: Ensure plugin.json is the only file in the manifest directory
75
+ // For .claude-plugin/, marketplace.json is also allowed (Claude Code convention)
25
76
  try {
26
- const entries = await fs.readdir(dotWavePluginPath);
27
- const misplaced = entries.filter((e) => e !== "plugin.json");
77
+ const entries = await fs.readdir(pluginDirPath);
78
+ const allowedFiles = ["plugin.json"];
79
+ if (pluginDirName === ".claude-plugin") {
80
+ allowedFiles.push("marketplace.json");
81
+ }
82
+ const misplaced = entries.filter((e) => !allowedFiles.includes(e));
28
83
  if (misplaced.length > 0) {
84
+ const allowedMsg =
85
+ pluginDirName === ".claude-plugin"
86
+ ? "Only plugin.json and marketplace.json should be in this directory."
87
+ : "Only plugin.json should be in this directory.";
29
88
  throw new Error(
30
- `Misplaced files/directories in .wave-plugin/: ${misplaced.join(", ")}. Only plugin.json should be in this directory.`,
89
+ `Misplaced files/directories in ${pluginDirName}/: ${misplaced.join(", ")}. ${allowedMsg}`,
31
90
  );
32
91
  }
33
92
  } catch (error) {
@@ -36,7 +95,7 @@ export class PluginLoader {
36
95
  (error as { code?: string }).code === "ENOENT"
37
96
  ) {
38
97
  throw new Error(
39
- `Plugin manifest directory not found at ${dotWavePluginPath}`,
98
+ `Plugin manifest directory not found at ${pluginDirPath}`,
40
99
  );
41
100
  }
42
101
  throw error;
@@ -71,7 +71,11 @@ When using the Agent tool, you must specify a subagent_type parameter to select
71
71
 
72
72
  - When doing file search, prefer to use the ${AGENT_TOOL_NAME} tool in order to reduce context usage.
73
73
  - You should proactively use the ${AGENT_TOOL_NAME} tool with specialized agents when the task at hand matches the agent's description.
74
- - VERY IMPORTANT: When exploring the codebase to gather context or to answer a question that is not a needle query for a specific file/class/function, it is CRITICAL that you use the ${AGENT_TOOL_NAME} tool with subagent_type=${EXPLORE_SUBAGENT_TYPE} instead of running search commands directly.`;
74
+ - VERY IMPORTANT: When exploring the codebase to gather context or to answer a question that is not a needle query for a specific file/class/function, it is CRITICAL that you use the ${AGENT_TOOL_NAME} tool with subagent_type=${EXPLORE_SUBAGENT_TYPE} instead of running search commands directly.
75
+ - You can optionally run agents in the background using the run_in_background parameter. When an agent runs in the background, you will be automatically notified when it completes — do NOT sleep, poll, or proactively check on its progress. Continue with other work or respond to the user instead.
76
+ - **Foreground vs background**: Use foreground (default) when you need the agent's results before you can proceed — e.g., research agents whose findings inform your next steps. Use background when you have genuinely independent work to do in parallel.
77
+ - **Don't peek.** The tool result includes an output file path — do not Read or tail it unless the user explicitly asks for a progress check. You get a completion notification; trust it. Reading the transcript mid-flight pulls the agent's tool noise into your context, which defeats the point of backgrounding.
78
+ - **Don't race.** After launching, you know nothing about what the agent found. Never fabricate or predict agent results in any format — not as prose, summary, or structured output. The notification arrives as a user-role message in a later turn; it is never something you write yourself. If the user asks a follow-up before the notification lands, tell them the agent is still running — give status, not a guess.`;
75
79
  },
76
80
 
77
81
  execute: async (
@@ -212,9 +216,17 @@ When using the Agent tool, you must specify a subagent_type parameter to select
212
216
  if (run_in_background) {
213
217
  const task = context.backgroundTaskManager?.getTask(result);
214
218
  const outputPath = task?.outputPath;
219
+ const backgroundMsg = [
220
+ `Agent started in background with ID: ${result}.`,
221
+ `The agent is working in the background. You will be notified automatically when it completes.`,
222
+ `Do not duplicate this agent's work — avoid working with the same files or topics it is using.`,
223
+ outputPath
224
+ ? `output_file: ${outputPath}`
225
+ : `Briefly tell the user what you launched and end your response.`,
226
+ ].join("\n");
215
227
  resolve({
216
228
  success: true,
217
- content: `Agent started in background with ID: ${result}.${outputPath ? ` Real-time output: ${outputPath}` : ""}`,
229
+ content: backgroundMsg,
218
230
  shortResult: `Agent started in background: ${result}`,
219
231
  });
220
232
  return;
@@ -139,7 +139,10 @@ Use the gh command via the Bash tool for GitHub-related tasks including working
139
139
  - Do not retry failing commands in a sleep loop — diagnose the root cause.
140
140
  - If waiting for a background task you started with \`run_in_background\`, you will be notified when it completes — do not poll.
141
141
  - If you must poll an external process, use a check command (e.g. \`gh run view\`) rather than sleeping first.
142
- - If you must sleep, keep the duration short (1-5 seconds) to avoid blocking the user.`,
142
+ - If you must sleep, keep the duration short (1-5 seconds) to avoid blocking the user.
143
+
144
+ # CWD management
145
+ Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of \`cd\`. You may use \`cd\` if the User explicitly requests it. When you use \`cd\`, the shell working directory will be reset to the original working directory after the command completes.`,
143
146
  execute: async (
144
147
  args: Record<string, unknown>,
145
148
  context: ToolContext,
@@ -221,9 +224,16 @@ Use the gh command via the Bash tool for GitHub-related tasks including working
221
224
  const { id: taskId } = backgroundTaskManager.startShell(command, timeout);
222
225
  const task = backgroundTaskManager.getTask(taskId);
223
226
  const outputPath = task?.outputPath;
227
+ const backgroundMsg = [
228
+ `Command started in background with ID: ${taskId}.`,
229
+ `You will be notified automatically when it completes.`,
230
+ outputPath
231
+ ? `output_file: ${outputPath}`
232
+ : `Use ${READ_TOOL_NAME} tool with task_id="${taskId}" to read the output.`,
233
+ ].join("\n");
224
234
  return {
225
235
  success: true,
226
- content: `Command started in background with ID: ${taskId}.${outputPath ? ` Real-time output: ${outputPath}` : ` Use ${READ_TOOL_NAME} tool with task_id="${taskId}" to monitor output.`}`,
236
+ content: backgroundMsg,
227
237
  shortResult: `Background process ${taskId} started`,
228
238
  };
229
239
  }
@@ -446,9 +456,20 @@ Use the gh command via the Bash tool for GitHub-related tasks including working
446
456
  }
447
457
  }
448
458
 
449
- // If CWD changed, call the onCwdChange callback
459
+ // If CWD changed, call the onCwdChange callback and add notification
460
+ let cwdChangedNotification = "";
450
461
  if (newCwd && newCwd !== context.workdir && context.onCwdChange) {
451
- context.onCwdChange(newCwd);
462
+ const isInSafeZone =
463
+ context.permissionManager?.isPathInSafeZone?.(newCwd) ?? true;
464
+
465
+ if (isInSafeZone) {
466
+ context.onCwdChange(newCwd);
467
+ } else if (context.originalWorkdir) {
468
+ context.onCwdChange(context.originalWorkdir);
469
+ cwdChangedNotification = `Shell cwd was reset to ${context.originalWorkdir}\n`;
470
+ } else {
471
+ context.onCwdChange(newCwd);
472
+ }
452
473
  }
453
474
 
454
475
  const exitCode = code ?? 0;
@@ -457,7 +478,8 @@ Use the gh command via the Bash tool for GitHub-related tasks including working
457
478
 
458
479
  // Handle large output by truncation and persistence if needed
459
480
  const finalOutput =
460
- combinedOutput || `Command executed with exit code: ${exitCode}`;
481
+ cwdChangedNotification +
482
+ (combinedOutput || `Command executed with exit code: ${exitCode}`);
461
483
  const content = processOutput(finalOutput);
462
484
 
463
485
  const lines = combinedOutput.trim().split("\n");
@@ -58,6 +58,7 @@ export interface ToolContext {
58
58
  abortSignal?: AbortSignal;
59
59
  backgroundTaskManager?: import("../managers/backgroundTaskManager.js").BackgroundTaskManager;
60
60
  workdir: string;
61
+ originalWorkdir?: string;
61
62
  /** Permission mode for this tool execution */
62
63
  permissionMode?: PermissionMode;
63
64
  /** Custom permission callback */