wave-agent-sdk 1.0.9 → 1.1.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 (94) hide show
  1. package/dist/agent.d.ts +9 -6
  2. package/dist/agent.js +35 -33
  3. package/dist/builtin/index.d.ts +1 -0
  4. package/dist/builtin/index.js +20 -0
  5. package/dist/builtin/plugins.d.ts +1 -0
  6. package/dist/builtin/plugins.js +225 -0
  7. package/dist/builtin/skills/artifact.d.ts +1 -0
  8. package/dist/builtin/skills/artifact.js +18 -0
  9. package/dist/builtin/skills/code-review.d.ts +1 -0
  10. package/{builtin/skills/code-review/SKILL.md → dist/builtin/skills/code-review.js} +23 -19
  11. package/dist/builtin/skills/deep-research.d.ts +1 -0
  12. package/{builtin/skills/deep-research/SKILL.md → dist/builtin/skills/deep-research.js} +18 -14
  13. package/dist/builtin/skills/init.d.ts +1 -0
  14. package/{builtin/skills/init/SKILL.md → dist/builtin/skills/init.js} +6 -3
  15. package/dist/builtin/skills/loop.d.ts +1 -0
  16. package/dist/builtin/skills/loop.js +83 -0
  17. package/dist/builtin/skills/settings.d.ts +1 -0
  18. package/dist/builtin/skills/settings.js +1249 -0
  19. package/dist/builtin/skills/simplify.d.ts +1 -0
  20. package/{builtin/skills/simplify/SKILL.md → dist/builtin/skills/simplify.js} +7 -3
  21. package/dist/builtin/subagents.d.ts +1 -0
  22. package/dist/builtin/subagents.js +164 -0
  23. package/dist/index.d.ts +2 -0
  24. package/dist/index.js +2 -0
  25. package/dist/managers/aiManager.d.ts +10 -0
  26. package/dist/managers/aiManager.js +49 -5
  27. package/dist/managers/mcpManager.js +1 -1
  28. package/dist/managers/messageManager.d.ts +6 -0
  29. package/dist/managers/messageManager.js +33 -0
  30. package/dist/managers/subagentManager.d.ts +9 -0
  31. package/dist/managers/subagentManager.js +62 -3
  32. package/dist/services/hook.js +41 -7
  33. package/dist/services/initializationService.js +0 -21
  34. package/dist/services/jsonlHandler.d.ts +37 -2
  35. package/dist/services/jsonlHandler.js +55 -6
  36. package/dist/services/session.d.ts +35 -4
  37. package/dist/services/session.js +233 -36
  38. package/dist/services/worktreeHooks.d.ts +45 -0
  39. package/dist/services/worktreeHooks.js +133 -0
  40. package/dist/tools/agentTool.js +36 -1
  41. package/dist/tools/bashTool.js +120 -57
  42. package/dist/tools/editTool.js +1 -0
  43. package/dist/tools/enterWorktreeTool.d.ts +1 -1
  44. package/dist/tools/enterWorktreeTool.js +44 -31
  45. package/dist/tools/exitWorktreeTool.js +21 -26
  46. package/dist/tools/readTool.js +6 -3
  47. package/dist/tools/types.d.ts +1 -0
  48. package/dist/tools/writeTool.js +1 -0
  49. package/dist/types/agent.d.ts +5 -0
  50. package/dist/types/hooks.d.ts +3 -2
  51. package/dist/types/messaging.d.ts +1 -0
  52. package/dist/types/skills.d.ts +0 -1
  53. package/dist/types/skills.js +0 -1
  54. package/dist/utils/asyncWorkRegistry.d.ts +32 -0
  55. package/dist/utils/asyncWorkRegistry.js +81 -0
  56. package/dist/utils/builtinEmbed.d.ts +21 -0
  57. package/dist/utils/builtinEmbed.js +53 -0
  58. package/dist/utils/configPaths.d.ts +0 -1
  59. package/dist/utils/configPaths.js +5 -26
  60. package/dist/utils/containerSetup.js +5 -0
  61. package/dist/utils/convertMessagesForAPI.js +21 -2
  62. package/dist/utils/messageOperations.d.ts +1 -0
  63. package/dist/utils/skillParser.js +3 -6
  64. package/dist/utils/toolImagePersistence.d.ts +25 -0
  65. package/dist/utils/toolImagePersistence.js +56 -0
  66. package/dist/utils/windowsPaths.d.ts +28 -0
  67. package/dist/utils/windowsPaths.js +47 -0
  68. package/dist/utils/worktreeSession.d.ts +6 -0
  69. package/dist/utils/worktreeUtils.d.ts +7 -0
  70. package/dist/utils/worktreeUtils.js +88 -40
  71. package/package.json +5 -5
  72. package/builtin/plugins/sdd/.wave-plugin/plugin.json +0 -8
  73. package/builtin/plugins/sdd/hooks/hooks.json +0 -14
  74. package/builtin/plugins/sdd/scripts/session-start.js +0 -24
  75. package/builtin/plugins/sdd/scripts/spec-count.js +0 -77
  76. package/builtin/plugins/sdd/skills/specify/SKILL.md +0 -47
  77. package/builtin/plugins/sdd/skills/specify/templates/spec-template.md +0 -47
  78. package/builtin/skills/artifact/SKILL.md +0 -14
  79. package/builtin/skills/loop/SKILL.md +0 -79
  80. package/builtin/skills/settings/ENV.md +0 -78
  81. package/builtin/skills/settings/HOOKS.md +0 -227
  82. package/builtin/skills/settings/MCP.md +0 -137
  83. package/builtin/skills/settings/MEMORY.md +0 -76
  84. package/builtin/skills/settings/MODELS.md +0 -119
  85. package/builtin/skills/settings/PERMISSIONS.md +0 -88
  86. package/builtin/skills/settings/PLUGINS.md +0 -171
  87. package/builtin/skills/settings/SKILL.md +0 -132
  88. package/builtin/skills/settings/SKILLS.md +0 -107
  89. package/builtin/skills/settings/SUBAGENTS.md +0 -77
  90. package/builtin/subagents/bash.md +0 -19
  91. package/builtin/subagents/explore.md +0 -43
  92. package/builtin/subagents/general-purpose.md +0 -20
  93. package/builtin/subagents/plan.md +0 -56
  94. package/builtin/subagents/vision.md +0 -18
@@ -7,6 +7,8 @@
7
7
  */
8
8
  import { spawn } from "child_process";
9
9
  import { generateSessionFilePath } from "./session.js";
10
+ import { resolveShellPath } from "../utils/shellResolver.js";
11
+ import { toPosixCommand } from "../utils/windowsPaths.js";
10
12
  // =============================================================================
11
13
  // Hook Execution Functions
12
14
  // =============================================================================
@@ -56,10 +58,10 @@ async function buildHookJsonInput(context) {
56
58
  if (context.subagentType !== undefined) {
57
59
  jsonInput.subagent_type = context.subagentType;
58
60
  }
59
- // Add name field for WorktreeCreate events
61
+ // Add name field for WorktreeCreate events (aligned with Claude Code)
60
62
  if (context.event === "WorktreeCreate") {
61
- if (context.worktreeName !== undefined) {
62
- jsonInput.name = context.worktreeName;
63
+ if (context.name !== undefined) {
64
+ jsonInput.name = context.name;
63
65
  }
64
66
  }
65
67
  // Add worktree_path field for WorktreeRemove events
@@ -133,11 +135,43 @@ export async function executeCommand(command, context, options) {
133
135
  let stdout = "";
134
136
  let stderr = "";
135
137
  let timedOut = false;
136
- // Parse command for shell execution
138
+ // Parse command for shell execution.
139
+ //
140
+ // Windows uses Git Bash (POSIX shell) instead of cmd.exe: cmd.exe's `/c`
141
+ // does not strip quotes from arguments after the first token, so commands
142
+ // like `node "C:\path\script.js"` silently fail (issue #1773). Git Bash
143
+ // parses quotes correctly, and Windows paths in the command are converted
144
+ // to POSIX form first (Git Bash resolves `/c/Users/...` and translates it
145
+ // back to a Windows path when spawning native executables like node.exe).
146
+ // Falls back to cmd.exe when no Git Bash is installed.
137
147
  const isWindows = process.platform === "win32";
138
- const shell = isWindows ? "cmd.exe" : "/bin/sh";
139
- const shellFlag = isWindows ? "/c" : "-c";
140
- const childProcess = spawn(shell, [shellFlag, command], {
148
+ const bashPath = isWindows ? resolveShellPath() : undefined;
149
+ let shell;
150
+ let shellFlag;
151
+ let finalCommand;
152
+ if (bashPath) {
153
+ shell = bashPath;
154
+ shellFlag = "-c";
155
+ finalCommand = toPosixCommand(command);
156
+ // Windows .sh scripts aren't directly executable — run them via bash
157
+ // when the command itself is a .sh file (e.g. `${WAVE_PLUGIN_ROOT}/x.sh`).
158
+ const trimmed = finalCommand.trim();
159
+ const firstToken = trimmed.match(/^("([^"]*)"|'([^']*)'|\S+)/)?.[0] ?? "";
160
+ if (firstToken.endsWith(".sh") && !trimmed.startsWith("bash ")) {
161
+ finalCommand = `bash ${finalCommand}`;
162
+ }
163
+ }
164
+ else if (isWindows) {
165
+ shell = "cmd.exe";
166
+ shellFlag = "/c";
167
+ finalCommand = command;
168
+ }
169
+ else {
170
+ shell = "/bin/sh";
171
+ shellFlag = "-c";
172
+ finalCommand = command;
173
+ }
174
+ const childProcess = spawn(shell, [shellFlag, finalCommand], {
141
175
  stdio: ["pipe", "pipe", "pipe"],
142
176
  cwd: context.projectDir,
143
177
  env: {
@@ -127,27 +127,6 @@ export class InitializationService {
127
127
  catch (error) {
128
128
  logger?.warn("SessionStart hooks execution failed:", error);
129
129
  }
130
- // Trigger WorktreeCreate hook if this is a new worktree
131
- if (agentOptions.isNewWorktree && hookManager) {
132
- try {
133
- logger?.info(`Triggering WorktreeCreate hook for ${agentOptions.worktreeName}...`);
134
- const hookResults = await hookManager.executeHooks("WorktreeCreate", {
135
- event: "WorktreeCreate",
136
- projectDir: workdir,
137
- timestamp: new Date(),
138
- sessionId: messageManager.getSessionId(),
139
- transcriptPath: messageManager.getTranscriptPath(),
140
- cwd: workdir,
141
- worktreeName: agentOptions.worktreeName,
142
- env: Object.fromEntries(Object.entries(configurationService.getMergedEnv()).filter((e) => e[1] !== undefined)),
143
- });
144
- // Process hook results
145
- hookManager.processHookResults("WorktreeCreate", hookResults, messageManager);
146
- }
147
- catch (error) {
148
- logger?.warn("WorktreeCreate hooks execution failed:", error);
149
- }
150
- }
151
130
  // Resolve and validate configuration after loading settings.json
152
131
  resolveAndValidateConfig();
153
132
  // Initialize auto-memory directory
@@ -10,6 +10,20 @@ import type { SessionFilename } from "../types/session.js";
10
10
  export interface JsonlWriteOptions {
11
11
  atomic?: boolean;
12
12
  }
13
+ /**
14
+ * Creation-time metadata persisted in the session file's header line
15
+ * (`{"type":"metadata",...}`). The header is append-only — written once on
16
+ * session creation and never rewritten — so only fields that are fixed at
17
+ * creation time belong here.
18
+ */
19
+ export interface SessionMetadataHeader {
20
+ /** Real working directory (the encoded project dir name is lossy for paths containing "-"). */
21
+ workdir?: string;
22
+ /** ISO 8601 creation timestamp. */
23
+ createdAt?: string;
24
+ /** Git branch at creation time (`git branch --show-current`), when the directory is a git repo. */
25
+ gitBranch?: string;
26
+ }
13
27
  /**
14
28
  * JSONL handler class for message persistence operations
15
29
  */
@@ -17,9 +31,20 @@ export declare class JsonlHandler {
17
31
  private readonly defaultWriteOptions;
18
32
  constructor();
19
33
  /**
20
- * Create a new session file (simplified - no metadata header)
34
+ * Create a new session file.
35
+ *
36
+ * When `metadata` is provided, the first line is a metadata header
37
+ * recording creation-time facts about the session:
38
+ * `{"type":"metadata","workdir":...,"createdAt":...,"gitBranch":...}`. The
39
+ * encoded project dir name is lossy for paths containing "-", so persisting
40
+ * the real path lets session listing show it without decoding; `createdAt`
41
+ * and `gitBranch` similarly avoid lossy/fabricated reconstruction later.
42
+ * The header carries no `timestamp`, so message readers filter it out
43
+ * naturally.
44
+ *
45
+ * Legacy callers that omit `metadata` still get an empty file.
21
46
  */
22
- createSession(filePath: string): Promise<void>;
47
+ createSession(filePath: string, metadata?: SessionMetadataHeader): Promise<void>;
23
48
  /**
24
49
  * Append a single message to JSONL file
25
50
  */
@@ -40,6 +65,16 @@ export declare class JsonlHandler {
40
65
  * Get the last message from JSONL file using efficient file reading (simplified)
41
66
  */
42
67
  getLastMessage(filePath: string): Promise<Message | null>;
68
+ /**
69
+ * Read the creation-time metadata from the session file's header line.
70
+ *
71
+ * Newer session files start with a `{"type":"metadata",...}` line (see
72
+ * `createSession`). Legacy files have no header.
73
+ *
74
+ * @param filePath - Path to the session JSONL file
75
+ * @returns The persisted metadata, or null when the file has no header
76
+ */
77
+ readMetadata(filePath: string): Promise<SessionMetadataHeader | null>;
43
78
  /**
44
79
  * Validate messages before writing
45
80
  */
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { appendFile, readFile, writeFile, stat, mkdir } from "fs/promises";
6
6
  import { dirname } from "path";
7
- import { getLastLine } from "../utils/fileUtils.js";
7
+ import { getLastLine, readFirstNLines } from "../utils/fileUtils.js";
8
8
  /**
9
9
  * JSONL handler class for message persistence operations
10
10
  */
@@ -15,13 +15,26 @@ export class JsonlHandler {
15
15
  };
16
16
  }
17
17
  /**
18
- * Create a new session file (simplified - no metadata header)
18
+ * Create a new session file.
19
+ *
20
+ * When `metadata` is provided, the first line is a metadata header
21
+ * recording creation-time facts about the session:
22
+ * `{"type":"metadata","workdir":...,"createdAt":...,"gitBranch":...}`. The
23
+ * encoded project dir name is lossy for paths containing "-", so persisting
24
+ * the real path lets session listing show it without decoding; `createdAt`
25
+ * and `gitBranch` similarly avoid lossy/fabricated reconstruction later.
26
+ * The header carries no `timestamp`, so message readers filter it out
27
+ * naturally.
28
+ *
29
+ * Legacy callers that omit `metadata` still get an empty file.
19
30
  */
20
- async createSession(filePath) {
31
+ async createSession(filePath, metadata) {
21
32
  // Ensure directory exists
22
33
  await this.ensureDirectory(dirname(filePath));
23
- // Create empty file (no metadata line needed)
24
- await writeFile(filePath, "", "utf8");
34
+ const content = metadata && Object.keys(metadata).length > 0
35
+ ? `${JSON.stringify({ type: "metadata", ...metadata })}\n`
36
+ : "";
37
+ await writeFile(filePath, content, "utf8");
25
38
  }
26
39
  /**
27
40
  * Append a single message to JSONL file
@@ -83,11 +96,14 @@ export class JsonlHandler {
83
96
  return [];
84
97
  }
85
98
  const allMessages = [];
86
- // Parse all messages (no metadata line to skip)
99
+ // Parse all messages, skipping the metadata header line (if any)
87
100
  for (let i = 0; i < lines.length; i++) {
88
101
  const line = lines[i];
89
102
  try {
90
103
  const message = JSON.parse(line);
104
+ // Metadata header line: not a message, skip
105
+ if (message.type === "metadata")
106
+ continue;
91
107
  if (message.timestamp)
92
108
  allMessages.push(message);
93
109
  }
@@ -127,6 +143,10 @@ export class JsonlHandler {
127
143
  }
128
144
  try {
129
145
  const parsed = JSON.parse(lastLine);
146
+ // A file whose only line is the metadata header has no messages yet
147
+ if (parsed.type === "metadata") {
148
+ return null;
149
+ }
130
150
  return parsed;
131
151
  }
132
152
  catch (error) {
@@ -137,6 +157,35 @@ export class JsonlHandler {
137
157
  throw new Error(`Failed to get last message from "${filePath}": ${error}`);
138
158
  }
139
159
  }
160
+ /**
161
+ * Read the creation-time metadata from the session file's header line.
162
+ *
163
+ * Newer session files start with a `{"type":"metadata",...}` line (see
164
+ * `createSession`). Legacy files have no header.
165
+ *
166
+ * @param filePath - Path to the session JSONL file
167
+ * @returns The persisted metadata, or null when the file has no header
168
+ */
169
+ async readMetadata(filePath) {
170
+ try {
171
+ const lines = await readFirstNLines(filePath, 1);
172
+ if (lines.length === 0) {
173
+ return null;
174
+ }
175
+ const header = JSON.parse(lines[0]);
176
+ if (header?.type === "metadata") {
177
+ return {
178
+ workdir: header.workdir,
179
+ createdAt: header.createdAt,
180
+ gitBranch: header.gitBranch,
181
+ };
182
+ }
183
+ }
184
+ catch {
185
+ // Unreadable or invalid first line — treat as a legacy file
186
+ }
187
+ return null;
188
+ }
140
189
  /**
141
190
  * Validate messages before writing
142
191
  */
@@ -33,6 +33,8 @@ export interface SessionMetadata {
33
33
  lastActiveAt: Date;
34
34
  latestTotalTokens: number;
35
35
  firstMessage?: string;
36
+ /** Git branch at session creation time (from the metadata header). */
37
+ branch?: string;
36
38
  }
37
39
  /**
38
40
  * Generate a new session ID using Node.js native crypto.randomUUID()
@@ -123,11 +125,31 @@ export declare function listSessions(workdir: string): Promise<SessionMetadata[]
123
125
  */
124
126
  export declare function listSessionsFromJsonl(workdir: string): Promise<SessionMetadata[]>;
125
127
  /**
126
- * List all sessions across all project directories
128
+ * List all sessions across all project directories.
127
129
  *
128
- * @returns Promise that resolves to array of session metadata objects
129
- */
130
- export declare function listAllSessions(): Promise<SessionMetadata[]>;
130
+ * When `worktreePaths` is provided, only project directories that match a
131
+ * same-repo git worktree (plus the current working directory's own project
132
+ * dir) are scanned — used by the `wave -r` picker's worktree aggregation
133
+ * (`Ctrl+W`). When omitted, every project directory under the sessions dir
134
+ * is scanned (all-projects mode, `Ctrl+A`).
135
+ *
136
+ * No time-based filtering is applied — all sessions whose files still exist
137
+ * are listed, regardless of how long ago they were last active.
138
+ *
139
+ * @param options.worktreePaths - Absolute paths of same-repo worktrees
140
+ * (from `git worktree list`). When provided, scanning is limited to
141
+ * matching project directories.
142
+ * @param options.workdir - Current working directory. Its own project dir is
143
+ * always included in worktree mode so sessions created from a subdirectory
144
+ * inside a worktree are not missed.
145
+ * @returns Promise that resolves to array of session metadata objects,
146
+ * deduplicated by sessionId (newest lastActiveAt wins), sorted by
147
+ * lastActiveAt descending.
148
+ */
149
+ export declare function listAllSessions(options?: {
150
+ worktreePaths?: string[];
151
+ workdir?: string;
152
+ }): Promise<SessionMetadata[]>;
131
153
  /**
132
154
  * Clean up expired sessions older than 14 days based on file modification time
133
155
  *
@@ -161,6 +183,15 @@ export declare function cleanupMetaOnlySessions(): Promise<number>;
161
183
  * @returns Promise that resolves to true if session exists, false otherwise
162
184
  */
163
185
  export declare function sessionExistsInJsonl(sessionId: string, workdir: string, sessionType?: "main" | "subagent"): Promise<boolean>;
186
+ /**
187
+ * Get the content of the first non-meta message in a session file
188
+ * For user role: get text block content
189
+ * For assistant role: get compact block content
190
+ * Skips meta messages (isMeta: true) to find the first meaningful message
191
+ * @param filePath - Path to the session JSONL file
192
+ * @returns Promise that resolves to the first non-meta message content or null if not found
193
+ */
194
+ export declare function getFirstMessageContentFromFile(filePath: string): Promise<string | null>;
164
195
  /**
165
196
  * Get the content of the first non-meta message in a session
166
197
  * For user role: get text block content