wave-agent-sdk 0.14.1 → 0.14.3
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.
- package/builtin/skills/settings/HOOKS.md +69 -0
- package/builtin/skills/settings/PLUGINS.md +171 -0
- package/builtin/skills/settings/SKILL.md +8 -3
- package/builtin/skills/settings/SUBAGENTS.md +21 -2
- package/dist/agent.d.ts +2 -2
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +12 -3
- package/dist/managers/aiManager.d.ts +6 -6
- package/dist/managers/aiManager.d.ts.map +1 -1
- package/dist/managers/aiManager.js +122 -59
- package/dist/managers/backgroundTaskManager.d.ts.map +1 -1
- package/dist/managers/backgroundTaskManager.js +28 -18
- package/dist/managers/hookManager.d.ts +16 -1
- package/dist/managers/hookManager.d.ts.map +1 -1
- package/dist/managers/hookManager.js +97 -8
- package/dist/managers/messageManager.d.ts +19 -4
- package/dist/managers/messageManager.d.ts.map +1 -1
- package/dist/managers/messageManager.js +63 -18
- package/dist/managers/pluginManager.d.ts +1 -0
- package/dist/managers/pluginManager.d.ts.map +1 -1
- package/dist/managers/pluginManager.js +7 -0
- package/dist/managers/subagentManager.d.ts +5 -0
- package/dist/managers/subagentManager.d.ts.map +1 -1
- package/dist/managers/subagentManager.js +35 -0
- package/dist/prompts/index.d.ts +1 -1
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +1 -1
- package/dist/services/MarketplaceService.d.ts +0 -11
- package/dist/services/MarketplaceService.d.ts.map +1 -1
- package/dist/services/MarketplaceService.js +21 -89
- package/dist/services/aiService.d.ts +3 -3
- package/dist/services/aiService.d.ts.map +1 -1
- package/dist/services/aiService.js +7 -7
- package/dist/services/hook.d.ts.map +1 -1
- package/dist/services/hook.js +15 -0
- package/dist/services/initializationService.d.ts.map +1 -1
- package/dist/services/initializationService.js +24 -1
- package/dist/services/interactionService.js +1 -1
- package/dist/services/pluginLoader.d.ts +5 -6
- package/dist/services/pluginLoader.d.ts.map +1 -1
- package/dist/services/pluginLoader.js +43 -53
- package/dist/services/session.d.ts +1 -1
- package/dist/services/session.js +7 -7
- package/dist/services/taskManager.d.ts +1 -1
- package/dist/services/taskManager.js +1 -1
- package/dist/types/core.d.ts +1 -1
- package/dist/types/core.d.ts.map +1 -1
- package/dist/types/hooks.d.ts +9 -1
- package/dist/types/hooks.d.ts.map +1 -1
- package/dist/types/hooks.js +2 -0
- package/dist/types/marketplace.d.ts +1 -26
- package/dist/types/marketplace.d.ts.map +1 -1
- package/dist/types/messaging.d.ts +3 -3
- package/dist/types/messaging.d.ts.map +1 -1
- package/dist/types/plugins.d.ts +3 -13
- package/dist/types/plugins.d.ts.map +1 -1
- package/dist/utils/convertMessagesForAPI.d.ts +1 -1
- package/dist/utils/convertMessagesForAPI.d.ts.map +1 -1
- package/dist/utils/convertMessagesForAPI.js +18 -7
- package/dist/utils/groupMessagesByApiRound.d.ts +1 -1
- package/dist/utils/groupMessagesByApiRound.js +6 -6
- package/dist/utils/messageOperations.d.ts.map +1 -1
- package/dist/utils/messageOperations.js +3 -3
- package/dist/utils/subagentParser.d.ts +8 -1
- package/dist/utils/subagentParser.d.ts.map +1 -1
- package/dist/utils/subagentParser.js +18 -3
- package/package.json +1 -1
- package/src/agent.ts +16 -3
- package/src/managers/aiManager.ts +142 -63
- package/src/managers/backgroundTaskManager.ts +32 -22
- package/src/managers/hookManager.ts +125 -10
- package/src/managers/messageManager.ts +76 -22
- package/src/managers/pluginManager.ts +10 -0
- package/src/managers/subagentManager.ts +47 -0
- package/src/prompts/index.ts +1 -1
- package/src/services/MarketplaceService.ts +26 -127
- package/src/services/aiService.ts +11 -11
- package/src/services/hook.ts +17 -0
- package/src/services/initializationService.ts +33 -1
- package/src/services/interactionService.ts +1 -1
- package/src/services/pluginLoader.ts +51 -67
- package/src/services/session.ts +7 -7
- package/src/services/taskManager.ts +1 -1
- package/src/types/core.ts +1 -1
- package/src/types/hooks.ts +16 -2
- package/src/types/marketplace.ts +1 -24
- package/src/types/messaging.ts +3 -3
- package/src/types/plugins.ts +3 -13
- package/src/utils/convertMessagesForAPI.ts +24 -9
- package/src/utils/groupMessagesByApiRound.ts +6 -6
- package/src/utils/messageOperations.ts +3 -5
- package/src/utils/subagentParser.ts +31 -4
|
@@ -8,12 +8,12 @@ import {
|
|
|
8
8
|
InstalledPlugin,
|
|
9
9
|
InstalledPluginsRegistry,
|
|
10
10
|
MarketplaceManifest,
|
|
11
|
-
MarketplacePluginEntry,
|
|
12
11
|
MarketplaceSource,
|
|
13
12
|
} from "../types/marketplace.js";
|
|
14
13
|
import { GitService } from "./GitService.js";
|
|
15
14
|
import { ConfigurationService } from "./configurationService.js";
|
|
16
15
|
import type { MarketplaceConfig, Scope } from "../types/configuration.js";
|
|
16
|
+
import { logger } from "../utils/globalLogger.js";
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Marketplace Service
|
|
@@ -320,42 +320,19 @@ export class MarketplaceService {
|
|
|
320
320
|
await fs.rename(tmpPath, this.installedPluginsPath);
|
|
321
321
|
}
|
|
322
322
|
|
|
323
|
-
/**
|
|
324
|
-
* Finds the first existing marketplace manifest path.
|
|
325
|
-
* Prefers .wave-plugin/ for backward compatibility, falls back to .claude-plugin/.
|
|
326
|
-
* Returns null if neither exists.
|
|
327
|
-
*/
|
|
328
|
-
private async findMarketplaceManifestPath(
|
|
329
|
-
dir: string,
|
|
330
|
-
): Promise<string | null> {
|
|
331
|
-
const waveManifestPath = path.join(dir, ".wave-plugin", "marketplace.json");
|
|
332
|
-
const claudeManifestPath = path.join(
|
|
333
|
-
dir,
|
|
334
|
-
".claude-plugin",
|
|
335
|
-
"marketplace.json",
|
|
336
|
-
);
|
|
337
|
-
|
|
338
|
-
if (existsSync(waveManifestPath)) {
|
|
339
|
-
return waveManifestPath;
|
|
340
|
-
}
|
|
341
|
-
if (existsSync(claudeManifestPath)) {
|
|
342
|
-
return claudeManifestPath;
|
|
343
|
-
}
|
|
344
|
-
return null;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
323
|
/**
|
|
348
324
|
* Loads a marketplace manifest from a local path
|
|
349
325
|
*/
|
|
350
326
|
async loadMarketplaceManifest(
|
|
351
327
|
marketplacePath: string,
|
|
352
328
|
): Promise<MarketplaceManifest> {
|
|
353
|
-
const manifestPath =
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
329
|
+
const manifestPath = path.join(
|
|
330
|
+
marketplacePath,
|
|
331
|
+
".wave-plugin",
|
|
332
|
+
"marketplace.json",
|
|
333
|
+
);
|
|
334
|
+
if (!existsSync(manifestPath)) {
|
|
335
|
+
throw new Error(`Marketplace manifest not found at ${manifestPath}`);
|
|
359
336
|
}
|
|
360
337
|
const content = await fs.readFile(manifestPath, "utf-8");
|
|
361
338
|
const manifest = JSON.parse(content);
|
|
@@ -665,7 +642,7 @@ export class MarketplaceService {
|
|
|
665
642
|
(p) => p.name === plugin.name,
|
|
666
643
|
);
|
|
667
644
|
if (!pluginEntry) {
|
|
668
|
-
|
|
645
|
+
logger.warn(
|
|
669
646
|
`Plugin "${plugin.name}" no longer found in marketplace "${marketplace.name}". Uninstalling...`,
|
|
670
647
|
);
|
|
671
648
|
try {
|
|
@@ -767,74 +744,6 @@ export class MarketplaceService {
|
|
|
767
744
|
});
|
|
768
745
|
}
|
|
769
746
|
|
|
770
|
-
/**
|
|
771
|
-
* Resolves a plugin source into a consistent format for installation.
|
|
772
|
-
* Handles both string sources (local paths or git URLs) and object-style MarketplaceSource.
|
|
773
|
-
*/
|
|
774
|
-
private resolvePluginSource(
|
|
775
|
-
pluginEntry: MarketplacePluginEntry,
|
|
776
|
-
marketplacePath: string,
|
|
777
|
-
): { isGit: boolean; url?: string; ref?: string; localPath: string } {
|
|
778
|
-
const { source } = pluginEntry;
|
|
779
|
-
|
|
780
|
-
if (typeof source === "string") {
|
|
781
|
-
// String source: could be a git URL or a relative local path
|
|
782
|
-
const isGitUrl =
|
|
783
|
-
source.startsWith("http://") ||
|
|
784
|
-
source.startsWith("https://") ||
|
|
785
|
-
source.startsWith("git@") ||
|
|
786
|
-
source.startsWith("ssh://");
|
|
787
|
-
|
|
788
|
-
if (isGitUrl) {
|
|
789
|
-
let url = source;
|
|
790
|
-
let ref: string | undefined;
|
|
791
|
-
if (url.includes("#")) {
|
|
792
|
-
[url, ref] = url.split("#");
|
|
793
|
-
}
|
|
794
|
-
return { isGit: true, url, ref, localPath: "" };
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
// Relative local path
|
|
798
|
-
return { isGit: false, localPath: path.resolve(marketplacePath, source) };
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
// Object-style source
|
|
802
|
-
if (source.source === "git") {
|
|
803
|
-
return { isGit: true, url: source.url, ref: source.ref, localPath: "" };
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
if (source.source === "github") {
|
|
807
|
-
return {
|
|
808
|
-
isGit: true,
|
|
809
|
-
url: `https://github.com/${source.repo}.git`,
|
|
810
|
-
ref: source.ref,
|
|
811
|
-
localPath: "",
|
|
812
|
-
};
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
if (source.source === "url") {
|
|
816
|
-
let url = source.url;
|
|
817
|
-
let ref = source.ref;
|
|
818
|
-
if (url.includes("#")) {
|
|
819
|
-
[url, ref] = url.split("#");
|
|
820
|
-
}
|
|
821
|
-
return { isGit: true, url, ref, localPath: "" };
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
if (source.source === "directory") {
|
|
825
|
-
return {
|
|
826
|
-
isGit: false,
|
|
827
|
-
localPath: path.resolve(marketplacePath, source.path),
|
|
828
|
-
};
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
// Exhaustiveness: this should be unreachable given the union type
|
|
832
|
-
const _exhaustive: never = source;
|
|
833
|
-
throw new Error(
|
|
834
|
-
`Unsupported plugin source type: ${(_exhaustive as { source: string }).source}`,
|
|
835
|
-
);
|
|
836
|
-
}
|
|
837
|
-
|
|
838
747
|
/**
|
|
839
748
|
* Installs a plugin from a marketplace
|
|
840
749
|
*/
|
|
@@ -863,46 +772,36 @@ export class MarketplaceService {
|
|
|
863
772
|
);
|
|
864
773
|
}
|
|
865
774
|
|
|
866
|
-
const
|
|
775
|
+
const isGitSource =
|
|
776
|
+
pluginEntry.source.startsWith("http://") ||
|
|
777
|
+
pluginEntry.source.startsWith("https://") ||
|
|
778
|
+
pluginEntry.source.startsWith("git@") ||
|
|
779
|
+
pluginEntry.source.startsWith("ssh://");
|
|
867
780
|
|
|
868
781
|
let pluginSrcPath: string;
|
|
869
782
|
let tempCloneDir: string | undefined;
|
|
870
783
|
|
|
871
784
|
try {
|
|
872
|
-
if (
|
|
785
|
+
if (isGitSource) {
|
|
873
786
|
tempCloneDir = path.join(this.tmpDir, `clone-${Date.now()}`);
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
787
|
+
let url = pluginEntry.source;
|
|
788
|
+
let ref: string | undefined;
|
|
789
|
+
if (url.includes("#")) {
|
|
790
|
+
[url, ref] = url.split("#");
|
|
791
|
+
}
|
|
792
|
+
await this.gitService.clone(url, tempCloneDir, ref);
|
|
879
793
|
pluginSrcPath = tempCloneDir;
|
|
880
794
|
} else {
|
|
881
|
-
pluginSrcPath =
|
|
795
|
+
pluginSrcPath = path.resolve(marketplacePath, pluginEntry.source);
|
|
882
796
|
}
|
|
883
797
|
|
|
884
|
-
|
|
885
|
-
const wavePluginPath = path.join(
|
|
798
|
+
const pluginManifestPath = path.join(
|
|
886
799
|
pluginSrcPath,
|
|
887
800
|
".wave-plugin",
|
|
888
801
|
"plugin.json",
|
|
889
802
|
);
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
".claude-plugin",
|
|
893
|
-
"plugin.json",
|
|
894
|
-
);
|
|
895
|
-
|
|
896
|
-
if (existsSync(wavePluginPath)) {
|
|
897
|
-
pluginManifestPath = wavePluginPath;
|
|
898
|
-
} else if (existsSync(claudePluginPath)) {
|
|
899
|
-
pluginManifestPath = claudePluginPath;
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
if (!pluginManifestPath) {
|
|
903
|
-
throw new Error(
|
|
904
|
-
`Plugin manifest not found at ${pluginSrcPath}. Neither .wave-plugin/plugin.json nor .claude-plugin/plugin.json exists.`,
|
|
905
|
-
);
|
|
803
|
+
if (!existsSync(pluginManifestPath)) {
|
|
804
|
+
throw new Error(`Plugin manifest not found at ${pluginManifestPath}`);
|
|
906
805
|
}
|
|
907
806
|
|
|
908
807
|
const pluginManifestContent = await fs.readFile(
|
|
@@ -917,7 +816,7 @@ export class MarketplaceService {
|
|
|
917
816
|
`${pluginName}-${Date.now()}`,
|
|
918
817
|
);
|
|
919
818
|
try {
|
|
920
|
-
if (
|
|
819
|
+
if (isGitSource) {
|
|
921
820
|
await fs.rename(pluginSrcPath, tmpPluginDir);
|
|
922
821
|
tempCloneDir = undefined;
|
|
923
822
|
} else {
|
|
@@ -23,7 +23,7 @@ import * as fs from "fs";
|
|
|
23
23
|
import * as path from "path";
|
|
24
24
|
|
|
25
25
|
import {
|
|
26
|
-
|
|
26
|
+
COMPACT_MESSAGES_SYSTEM_PROMPT,
|
|
27
27
|
WEB_CONTENT_SYSTEM_PROMPT,
|
|
28
28
|
BTW_SYSTEM_PROMPT,
|
|
29
29
|
} from "../prompts/index.js";
|
|
@@ -751,7 +751,7 @@ async function processStreamingResponse(
|
|
|
751
751
|
return result;
|
|
752
752
|
}
|
|
753
753
|
|
|
754
|
-
export interface
|
|
754
|
+
export interface CompactMessagesOptions {
|
|
755
755
|
// Resolved configuration
|
|
756
756
|
gatewayConfig: GatewayConfig;
|
|
757
757
|
modelConfig: ModelConfig;
|
|
@@ -762,7 +762,7 @@ export interface CompressMessagesOptions {
|
|
|
762
762
|
model?: string;
|
|
763
763
|
}
|
|
764
764
|
|
|
765
|
-
export interface
|
|
765
|
+
export interface CompactMessagesResult {
|
|
766
766
|
content: string;
|
|
767
767
|
usage?: {
|
|
768
768
|
prompt_tokens: number;
|
|
@@ -771,9 +771,9 @@ export interface CompressMessagesResult {
|
|
|
771
771
|
};
|
|
772
772
|
}
|
|
773
773
|
|
|
774
|
-
export async function
|
|
775
|
-
options:
|
|
776
|
-
): Promise<
|
|
774
|
+
export async function compactMessages(
|
|
775
|
+
options: CompactMessagesOptions,
|
|
776
|
+
): Promise<CompactMessagesResult> {
|
|
777
777
|
const { gatewayConfig, modelConfig, messages, abortSignal } = options;
|
|
778
778
|
|
|
779
779
|
// Apply global 1 QPS rate limit
|
|
@@ -832,7 +832,7 @@ export async function compressMessages(
|
|
|
832
832
|
messages: [
|
|
833
833
|
{
|
|
834
834
|
role: "system",
|
|
835
|
-
content:
|
|
835
|
+
content: COMPACT_MESSAGES_SYSTEM_PROMPT,
|
|
836
836
|
},
|
|
837
837
|
...cleanedMessages,
|
|
838
838
|
{
|
|
@@ -849,7 +849,7 @@ export async function compressMessages(
|
|
|
849
849
|
const content = response.choices[0]?.message?.content?.trim();
|
|
850
850
|
if (!content) {
|
|
851
851
|
throw new Error(
|
|
852
|
-
"Failed to
|
|
852
|
+
"Failed to compact conversation history: Empty response from AI",
|
|
853
853
|
);
|
|
854
854
|
}
|
|
855
855
|
const usage = response.usage
|
|
@@ -866,10 +866,10 @@ export async function compressMessages(
|
|
|
866
866
|
};
|
|
867
867
|
} catch (error) {
|
|
868
868
|
if ((error as Error).name === "AbortError") {
|
|
869
|
-
logger.info("
|
|
870
|
-
throw new Error("
|
|
869
|
+
logger.info("Compaction request was aborted");
|
|
870
|
+
throw new Error("Compaction request was aborted");
|
|
871
871
|
}
|
|
872
|
-
logger.error("Failed to
|
|
872
|
+
logger.error("Failed to compact messages:", error);
|
|
873
873
|
throw error;
|
|
874
874
|
}
|
|
875
875
|
}
|
package/src/services/hook.ts
CHANGED
|
@@ -84,6 +84,23 @@ async function buildHookJsonInput(
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
// Add SessionStart-specific fields
|
|
88
|
+
if (context.event === "SessionStart") {
|
|
89
|
+
if (context.source !== undefined) {
|
|
90
|
+
jsonInput.source = context.source;
|
|
91
|
+
}
|
|
92
|
+
if (context.agentType !== undefined) {
|
|
93
|
+
jsonInput.agent_type = context.agentType;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Add SessionEnd-specific fields
|
|
98
|
+
if (context.event === "SessionEnd") {
|
|
99
|
+
if (context.endSource !== undefined) {
|
|
100
|
+
jsonInput.end_source = context.endSource;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
87
104
|
return jsonInput;
|
|
88
105
|
}
|
|
89
106
|
|
|
@@ -178,6 +178,38 @@ export class InitializationService {
|
|
|
178
178
|
// Don't throw error to prevent app startup failure
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
// Execute SessionStart hooks
|
|
182
|
+
try {
|
|
183
|
+
const phaseStart = performance.now();
|
|
184
|
+
const sessionStartResult = await hookManager.executeSessionStartHooks(
|
|
185
|
+
"startup",
|
|
186
|
+
messageManager.getSessionId(),
|
|
187
|
+
messageManager.getTranscriptPath(),
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
// Inject additionalContext as a meta user message (matches Claude Code)
|
|
191
|
+
if (sessionStartResult.additionalContext) {
|
|
192
|
+
messageManager.addUserMessage({
|
|
193
|
+
content: `<system-reminder>\nSessionStart hook additional context: ${sessionStartResult.additionalContext}\n</system-reminder>`,
|
|
194
|
+
isMeta: true,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Inject initialUserMessage as a meta user message
|
|
199
|
+
if (sessionStartResult.initialUserMessage) {
|
|
200
|
+
messageManager.addUserMessage({
|
|
201
|
+
content: sessionStartResult.initialUserMessage,
|
|
202
|
+
isMeta: true,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
logger?.debug(
|
|
207
|
+
`Initialization Phase [SessionStart Hooks] took ${(performance.now() - phaseStart).toFixed(2)}ms`,
|
|
208
|
+
);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
logger?.warn("SessionStart hooks execution failed:", error);
|
|
211
|
+
}
|
|
212
|
+
|
|
181
213
|
// Trigger WorktreeCreate hook if this is a new worktree
|
|
182
214
|
if (agentOptions.isNewWorktree && hookManager) {
|
|
183
215
|
try {
|
|
@@ -318,7 +350,7 @@ export class InitializationService {
|
|
|
318
350
|
if (sessionToRestore) {
|
|
319
351
|
messageManager.initializeFromSession(sessionToRestore);
|
|
320
352
|
|
|
321
|
-
// Update task manager with the root session ID to ensure continuity across
|
|
353
|
+
// Update task manager with the root session ID to ensure continuity across compactions
|
|
322
354
|
taskManager.setTaskListId(
|
|
323
355
|
sessionToRestore.rootSessionId || sessionToRestore.id,
|
|
324
356
|
);
|
|
@@ -183,7 +183,7 @@ export class InteractionService {
|
|
|
183
183
|
// 6. Initialize session state last
|
|
184
184
|
messageManager.initializeFromSession(sessionData);
|
|
185
185
|
|
|
186
|
-
// Update task manager with the root session ID to ensure continuity across
|
|
186
|
+
// Update task manager with the root session ID to ensure continuity across compactions
|
|
187
187
|
taskManager.setTaskListId(sessionData.rootSessionId || sessionData.id);
|
|
188
188
|
|
|
189
189
|
// 7. Load tasks for the restored session
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import * as fs from "fs/promises";
|
|
2
|
-
import * as fsSync from "fs";
|
|
3
2
|
import * as path from "path";
|
|
4
3
|
import {
|
|
5
4
|
PluginManifest,
|
|
@@ -11,82 +10,29 @@ import {
|
|
|
11
10
|
} from "../types/index.js";
|
|
12
11
|
import { scanCommandsDirectory } from "../utils/customCommands.js";
|
|
13
12
|
import { parseSkillFile } from "../utils/skillParser.js";
|
|
13
|
+
import {
|
|
14
|
+
parseAgentFile,
|
|
15
|
+
type SubagentConfiguration,
|
|
16
|
+
} from "../utils/subagentParser.js";
|
|
14
17
|
import { resolveMcpConfig } from "../managers/mcpManager.js";
|
|
18
|
+
import { logger } from "../utils/globalLogger.js";
|
|
15
19
|
|
|
16
20
|
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
|
-
|
|
56
21
|
/**
|
|
57
22
|
* Load and validate a plugin manifest from a directory
|
|
58
23
|
* @param pluginPath Absolute path to the plugin directory
|
|
59
24
|
*/
|
|
60
25
|
static async loadManifest(pluginPath: string): Promise<PluginManifest> {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
throw new Error(
|
|
64
|
-
`Plugin manifest not found at ${pluginPath}. Neither .wave-plugin/plugin.json nor .claude-plugin/plugin.json exists.`,
|
|
65
|
-
);
|
|
66
|
-
}
|
|
26
|
+
const dotWavePluginPath = path.join(pluginPath, ".wave-plugin");
|
|
27
|
+
const manifestPath = path.join(dotWavePluginPath, "plugin.json");
|
|
67
28
|
|
|
68
|
-
//
|
|
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)
|
|
29
|
+
// T018: Ensure plugin.json is the only file in .wave-plugin/
|
|
76
30
|
try {
|
|
77
|
-
const entries = await fs.readdir(
|
|
78
|
-
const
|
|
79
|
-
if (pluginDirName === ".claude-plugin") {
|
|
80
|
-
allowedFiles.push("marketplace.json");
|
|
81
|
-
}
|
|
82
|
-
const misplaced = entries.filter((e) => !allowedFiles.includes(e));
|
|
31
|
+
const entries = await fs.readdir(dotWavePluginPath);
|
|
32
|
+
const misplaced = entries.filter((e) => e !== "plugin.json");
|
|
83
33
|
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.";
|
|
88
34
|
throw new Error(
|
|
89
|
-
`Misplaced files/directories in
|
|
35
|
+
`Misplaced files/directories in .wave-plugin/: ${misplaced.join(", ")}. Only plugin.json should be in this directory.`,
|
|
90
36
|
);
|
|
91
37
|
}
|
|
92
38
|
} catch (error) {
|
|
@@ -95,7 +41,7 @@ export class PluginLoader {
|
|
|
95
41
|
(error as { code?: string }).code === "ENOENT"
|
|
96
42
|
) {
|
|
97
43
|
throw new Error(
|
|
98
|
-
`Plugin manifest directory not found at ${
|
|
44
|
+
`Plugin manifest directory not found at ${dotWavePluginPath}`,
|
|
99
45
|
);
|
|
100
46
|
}
|
|
101
47
|
throw error;
|
|
@@ -212,12 +158,50 @@ export class PluginLoader {
|
|
|
212
158
|
const hooksPath = path.join(pluginPath, "hooks", "hooks.json");
|
|
213
159
|
try {
|
|
214
160
|
const content = await fs.readFile(hooksPath, "utf-8");
|
|
215
|
-
|
|
161
|
+
const parsed = JSON.parse(content);
|
|
162
|
+
// Claude Code wrapper format: { "hooks": { "SessionStart": [...] } }
|
|
163
|
+
// (optional "description" sibling key)
|
|
164
|
+
if (parsed && typeof parsed === "object" && "hooks" in parsed) {
|
|
165
|
+
return parsed.hooks as PartialHookConfiguration;
|
|
166
|
+
}
|
|
167
|
+
return undefined;
|
|
216
168
|
} catch {
|
|
217
169
|
return undefined;
|
|
218
170
|
}
|
|
219
171
|
}
|
|
220
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Load agent configurations from a plugin's agents directory
|
|
175
|
+
*/
|
|
176
|
+
static async loadAgents(
|
|
177
|
+
pluginPath: string,
|
|
178
|
+
): Promise<SubagentConfiguration[]> {
|
|
179
|
+
const agentsPath = path.join(pluginPath, "agents");
|
|
180
|
+
const agents: SubagentConfiguration[] = [];
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
const entries = await fs.readdir(agentsPath, { withFileTypes: true });
|
|
184
|
+
for (const entry of entries) {
|
|
185
|
+
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
186
|
+
const agentFilePath = path.join(agentsPath, entry.name);
|
|
187
|
+
try {
|
|
188
|
+
const config = parseAgentFile(agentFilePath, "plugin", pluginPath);
|
|
189
|
+
agents.push(config);
|
|
190
|
+
} catch (parseError) {
|
|
191
|
+
// Log error but continue with other files
|
|
192
|
+
logger?.warn(
|
|
193
|
+
`Warning: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
} catch {
|
|
199
|
+
// agents directory might not exist
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return agents;
|
|
203
|
+
}
|
|
204
|
+
|
|
221
205
|
/**
|
|
222
206
|
* Validate the plugin manifest structure
|
|
223
207
|
*/
|
package/src/services/session.ts
CHANGED
|
@@ -816,7 +816,7 @@ export async function sessionExistsInJsonl(
|
|
|
816
816
|
/**
|
|
817
817
|
* Get the content of the first message in a session
|
|
818
818
|
* For user role: get text block content
|
|
819
|
-
* For assistant role: get
|
|
819
|
+
* For assistant role: get compact block content
|
|
820
820
|
* @param sessionId - Session ID to get first message from
|
|
821
821
|
* @param workdir - Working directory for session operations
|
|
822
822
|
* @returns Promise that resolves to the first message content or null if not found
|
|
@@ -996,18 +996,18 @@ export async function loadFullMessageThread(
|
|
|
996
996
|
|
|
997
997
|
sessionIds.unshift(currentId);
|
|
998
998
|
// Add messages from this session to the beginning of the list
|
|
999
|
-
// But skip the "
|
|
999
|
+
// But skip the "compact" block if it's not the first session in our traversal (which is the latest)
|
|
1000
1000
|
// Actually, we should probably keep all messages and let the UI/logic handle it.
|
|
1001
|
-
// But wait, if we are concatenating, the "
|
|
1002
|
-
// So if we have session N-1 and session N, we should probably skip the
|
|
1001
|
+
// But wait, if we are concatenating, the "compact" block in session N summarizes session N-1.
|
|
1002
|
+
// So if we have session N-1 and session N, we should probably skip the compact block in session N.
|
|
1003
1003
|
|
|
1004
1004
|
const messages = sessionData.messages;
|
|
1005
1005
|
if (allMessages.length > 0) {
|
|
1006
1006
|
// If we already have messages (from "later" sessions),
|
|
1007
1007
|
// we are now adding messages from an "earlier" session.
|
|
1008
|
-
// The later session's first message might be a "
|
|
1009
|
-
if (allMessages[0].blocks.some((b) => b.type === "
|
|
1010
|
-
// Remove the
|
|
1008
|
+
// The later session's first message might be a "compact" block.
|
|
1009
|
+
if (allMessages[0].blocks.some((b) => b.type === "compact")) {
|
|
1010
|
+
// Remove the compact block from the later session's messages
|
|
1011
1011
|
// because we are now providing the actual messages it summarized.
|
|
1012
1012
|
allMessages.shift();
|
|
1013
1013
|
}
|
|
@@ -30,7 +30,7 @@ export class TaskManager extends EventEmitter {
|
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* Syncs the task list ID with the current session's root session ID.
|
|
33
|
-
* This is typically called when the session is cleared or
|
|
33
|
+
* This is typically called when the session is cleared or compacted.
|
|
34
34
|
*/
|
|
35
35
|
public async syncWithSession(): Promise<void> {
|
|
36
36
|
const messageManager = this.container.get<MessageManager>("MessageManager");
|
package/src/types/core.ts
CHANGED
|
@@ -25,7 +25,7 @@ export interface Usage {
|
|
|
25
25
|
completion_tokens: number; // Tokens generated in completions
|
|
26
26
|
total_tokens: number; // Sum of prompt + completion tokens
|
|
27
27
|
model?: string; // Model used for the operation (e.g., "gpt-4", "gpt-3.5-turbo")
|
|
28
|
-
operation_type?: "agent" | "
|
|
28
|
+
operation_type?: "agent" | "compact"; // Type of operation that generated usage
|
|
29
29
|
|
|
30
30
|
// Cache-related tokens (Claude models only)
|
|
31
31
|
cache_read_input_tokens?: number; // Tokens read from cache
|
package/src/types/hooks.ts
CHANGED
|
@@ -21,7 +21,9 @@ export type HookEvent =
|
|
|
21
21
|
| "SubagentStop"
|
|
22
22
|
| "PermissionRequest"
|
|
23
23
|
| "WorktreeCreate"
|
|
24
|
-
| "CwdChanged"
|
|
24
|
+
| "CwdChanged"
|
|
25
|
+
| "SessionStart"
|
|
26
|
+
| "SessionEnd";
|
|
25
27
|
|
|
26
28
|
// Individual hook command configuration
|
|
27
29
|
export interface HookCommand {
|
|
@@ -94,6 +96,10 @@ export class HookConfigurationError extends Error {
|
|
|
94
96
|
}
|
|
95
97
|
}
|
|
96
98
|
|
|
99
|
+
export type SessionStartSource = "startup" | "resume" | "compact";
|
|
100
|
+
|
|
101
|
+
export type SessionEndSource = "exit" | "stop" | "compact";
|
|
102
|
+
|
|
97
103
|
// Type guards for runtime validation
|
|
98
104
|
export function isValidHookEvent(event: string): event is HookEvent {
|
|
99
105
|
return [
|
|
@@ -105,6 +111,8 @@ export function isValidHookEvent(event: string): event is HookEvent {
|
|
|
105
111
|
"PermissionRequest",
|
|
106
112
|
"WorktreeCreate",
|
|
107
113
|
"CwdChanged",
|
|
114
|
+
"SessionStart",
|
|
115
|
+
"SessionEnd",
|
|
108
116
|
].includes(event);
|
|
109
117
|
}
|
|
110
118
|
|
|
@@ -161,7 +169,7 @@ export interface HookJsonInput {
|
|
|
161
169
|
session_id: string; // Format: "wave_session_{uuid}_{shortId}"
|
|
162
170
|
transcript_path: string; // Format: "~/.wave/sessions/session_{shortId}.json"
|
|
163
171
|
cwd: string; // Absolute path to current working directory
|
|
164
|
-
hook_event_name: HookEvent; // "PreToolUse" | "PostToolUse" | "UserPromptSubmit" | "Stop" | "SubagentStop" | "PermissionRequest" | "WorktreeCreate" | "CwdChanged"
|
|
172
|
+
hook_event_name: HookEvent; // "PreToolUse" | "PostToolUse" | "UserPromptSubmit" | "Stop" | "SubagentStop" | "PermissionRequest" | "WorktreeCreate" | "CwdChanged" | "SessionStart"
|
|
165
173
|
|
|
166
174
|
// Optional fields based on event type
|
|
167
175
|
tool_name?: string; // Present for PreToolUse, PostToolUse, PermissionRequest
|
|
@@ -172,6 +180,9 @@ export interface HookJsonInput {
|
|
|
172
180
|
name?: string; // Present for WorktreeCreate events
|
|
173
181
|
old_cwd?: string; // Present for CwdChanged events
|
|
174
182
|
new_cwd?: string; // Present for CwdChanged events
|
|
183
|
+
source?: SessionStartSource; // Present for SessionStart events
|
|
184
|
+
agent_type?: string; // Present for SessionStart events
|
|
185
|
+
end_source?: SessionEndSource; // Present for SessionEnd events
|
|
175
186
|
}
|
|
176
187
|
|
|
177
188
|
// Extended context interface for passing additional data to hook executor
|
|
@@ -187,6 +198,9 @@ export interface ExtendedHookExecutionContext extends HookExecutionContext {
|
|
|
187
198
|
worktreeName?: string; // Worktree name (WorktreeCreate only)
|
|
188
199
|
oldCwd?: string; // Previous working directory (CwdChanged only)
|
|
189
200
|
newCwd?: string; // New working directory (CwdChanged only)
|
|
201
|
+
source?: SessionStartSource; // Session start source (SessionStart only)
|
|
202
|
+
agentType?: string; // Agent type identifier (SessionStart only)
|
|
203
|
+
endSource?: SessionEndSource; // Session end source (SessionEnd only)
|
|
190
204
|
}
|
|
191
205
|
|
|
192
206
|
// Environment variables injected into hook processes
|