wave-agent-sdk 0.2.1 → 0.5.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.
- package/dist/agent.d.ts +66 -20
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +156 -83
- package/dist/constants/prompts.d.ts +7 -2
- package/dist/constants/prompts.d.ts.map +1 -1
- package/dist/constants/prompts.js +41 -5
- package/dist/constants/tools.d.ts +2 -2
- package/dist/constants/tools.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/managers/MemoryRuleManager.d.ts.map +1 -1
- package/dist/managers/MemoryRuleManager.js +16 -2
- package/dist/managers/aiManager.d.ts +14 -4
- package/dist/managers/aiManager.d.ts.map +1 -1
- package/dist/managers/aiManager.js +61 -9
- package/dist/managers/backgroundBashManager.d.ts.map +1 -1
- package/dist/managers/backgroundBashManager.js +1 -0
- package/dist/managers/backgroundTaskManager.d.ts +35 -0
- package/dist/managers/backgroundTaskManager.d.ts.map +1 -0
- package/dist/managers/backgroundTaskManager.js +249 -0
- package/dist/managers/bashManager.d.ts.map +1 -1
- package/dist/managers/bashManager.js +0 -3
- package/dist/managers/foregroundTaskManager.d.ts +9 -0
- package/dist/managers/foregroundTaskManager.d.ts.map +1 -0
- package/dist/managers/foregroundTaskManager.js +20 -0
- package/dist/managers/liveConfigManager.d.ts +1 -1
- package/dist/managers/liveConfigManager.d.ts.map +1 -1
- package/dist/managers/lspManager.d.ts.map +1 -1
- package/dist/managers/lspManager.js +3 -1
- package/dist/managers/messageManager.d.ts +34 -4
- package/dist/managers/messageManager.d.ts.map +1 -1
- package/dist/managers/messageManager.js +104 -13
- package/dist/managers/permissionManager.d.ts.map +1 -1
- package/dist/managers/permissionManager.js +11 -13
- package/dist/managers/pluginManager.d.ts.map +1 -1
- package/dist/managers/pluginManager.js +3 -2
- package/dist/managers/pluginScopeManager.d.ts +13 -2
- package/dist/managers/pluginScopeManager.d.ts.map +1 -1
- package/dist/managers/pluginScopeManager.js +38 -0
- package/dist/managers/reversionManager.d.ts +39 -0
- package/dist/managers/reversionManager.d.ts.map +1 -0
- package/dist/managers/reversionManager.js +118 -0
- package/dist/managers/slashCommandManager.d.ts +4 -1
- package/dist/managers/slashCommandManager.d.ts.map +1 -1
- package/dist/managers/slashCommandManager.js +16 -6
- package/dist/managers/subagentManager.d.ts +13 -2
- package/dist/managers/subagentManager.d.ts.map +1 -1
- package/dist/managers/subagentManager.js +144 -35
- package/dist/managers/toolManager.d.ts +11 -1
- package/dist/managers/toolManager.d.ts.map +1 -1
- package/dist/managers/toolManager.js +11 -3
- package/dist/services/GitService.d.ts.map +1 -1
- package/dist/services/GitService.js +6 -2
- package/dist/services/MarketplaceService.d.ts +14 -1
- package/dist/services/MarketplaceService.d.ts.map +1 -1
- package/dist/services/MarketplaceService.js +72 -4
- package/dist/services/MemoryRuleService.d.ts +1 -1
- package/dist/services/MemoryRuleService.d.ts.map +1 -1
- package/dist/services/MemoryRuleService.js +13 -2
- package/dist/services/aiService.js +1 -1
- package/dist/services/configurationService.d.ts +18 -2
- package/dist/services/configurationService.d.ts.map +1 -1
- package/dist/services/configurationService.js +62 -0
- package/dist/services/fileWatcher.d.ts +0 -5
- package/dist/services/fileWatcher.d.ts.map +1 -1
- package/dist/services/fileWatcher.js +0 -11
- package/dist/services/memory.js +1 -1
- package/dist/services/pluginLoader.d.ts.map +1 -1
- package/dist/services/pluginLoader.js +6 -1
- package/dist/services/reversionService.d.ts +24 -0
- package/dist/services/reversionService.d.ts.map +1 -0
- package/dist/services/reversionService.js +76 -0
- package/dist/services/session.d.ts +7 -0
- package/dist/services/session.d.ts.map +1 -1
- package/dist/services/session.js +126 -3
- package/dist/tools/bashTool.d.ts +0 -8
- package/dist/tools/bashTool.d.ts.map +1 -1
- package/dist/tools/bashTool.js +52 -174
- package/dist/tools/deleteFileTool.d.ts.map +1 -1
- package/dist/tools/deleteFileTool.js +9 -0
- package/dist/tools/editTool.d.ts.map +1 -1
- package/dist/tools/editTool.js +15 -4
- package/dist/tools/multiEditTool.d.ts.map +1 -1
- package/dist/tools/multiEditTool.js +16 -5
- package/dist/tools/taskOutputTool.d.ts +3 -0
- package/dist/tools/taskOutputTool.d.ts.map +1 -0
- package/dist/tools/taskOutputTool.js +149 -0
- package/dist/tools/taskStopTool.d.ts +3 -0
- package/dist/tools/taskStopTool.d.ts.map +1 -0
- package/dist/tools/taskStopTool.js +65 -0
- package/dist/tools/taskTool.d.ts.map +1 -1
- package/dist/tools/taskTool.js +105 -63
- package/dist/tools/types.d.ts +7 -0
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/writeTool.d.ts.map +1 -1
- package/dist/tools/writeTool.js +9 -0
- package/dist/types/commands.d.ts +1 -0
- package/dist/types/commands.d.ts.map +1 -1
- package/dist/types/configuration.d.ts +3 -0
- package/dist/types/configuration.d.ts.map +1 -1
- package/dist/types/environment.d.ts +2 -1
- package/dist/types/environment.d.ts.map +1 -1
- package/dist/types/environment.js +0 -6
- package/dist/types/history.d.ts +5 -0
- package/dist/types/history.d.ts.map +1 -0
- package/dist/types/history.js +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/types/marketplace.d.ts +4 -0
- package/dist/types/marketplace.d.ts.map +1 -1
- package/dist/types/messaging.d.ts +7 -1
- package/dist/types/messaging.d.ts.map +1 -1
- package/dist/types/processes.d.ts +24 -4
- package/dist/types/processes.d.ts.map +1 -1
- package/dist/types/reversion.d.ts +29 -0
- package/dist/types/reversion.d.ts.map +1 -0
- package/dist/types/reversion.js +1 -0
- package/dist/utils/builtinSubagents.d.ts.map +1 -1
- package/dist/utils/builtinSubagents.js +16 -0
- package/dist/utils/constants.d.ts +2 -2
- package/dist/utils/constants.d.ts.map +1 -1
- package/dist/utils/constants.js +2 -2
- package/dist/utils/editUtils.d.ts +4 -9
- package/dist/utils/editUtils.d.ts.map +1 -1
- package/dist/utils/editUtils.js +54 -55
- package/dist/utils/messageOperations.d.ts +3 -1
- package/dist/utils/messageOperations.d.ts.map +1 -1
- package/dist/utils/messageOperations.js +8 -1
- package/dist/utils/openaiClient.d.ts.map +1 -1
- package/dist/utils/openaiClient.js +56 -26
- package/dist/utils/promptHistory.d.ts +20 -0
- package/dist/utils/promptHistory.d.ts.map +1 -0
- package/dist/utils/promptHistory.js +117 -0
- package/package.json +5 -3
- package/src/agent.ts +193 -109
- package/src/constants/prompts.ts +45 -5
- package/src/constants/tools.ts +2 -2
- package/src/index.ts +1 -1
- package/src/managers/MemoryRuleManager.ts +18 -2
- package/src/managers/aiManager.ts +87 -18
- package/src/managers/backgroundBashManager.ts +1 -0
- package/src/managers/backgroundTaskManager.ts +306 -0
- package/src/managers/bashManager.ts +0 -4
- package/src/managers/foregroundTaskManager.ts +26 -0
- package/src/managers/liveConfigManager.ts +2 -1
- package/src/managers/lspManager.ts +3 -1
- package/src/managers/messageManager.ts +136 -18
- package/src/managers/permissionManager.ts +11 -13
- package/src/managers/pluginManager.ts +4 -3
- package/src/managers/pluginScopeManager.ts +57 -8
- package/src/managers/reversionManager.ts +152 -0
- package/src/managers/slashCommandManager.ts +30 -7
- package/src/managers/subagentManager.ts +176 -31
- package/src/managers/toolManager.ts +23 -4
- package/src/services/GitService.ts +6 -2
- package/src/services/MarketplaceService.ts +100 -4
- package/src/services/MemoryRuleService.ts +18 -6
- package/src/services/aiService.ts +1 -1
- package/src/services/configurationService.ts +79 -1
- package/src/services/fileWatcher.ts +0 -13
- package/src/services/memory.ts +1 -1
- package/src/services/pluginLoader.ts +7 -1
- package/src/services/reversionService.ts +94 -0
- package/src/services/session.ts +161 -3
- package/src/tools/bashTool.ts +73 -200
- package/src/tools/deleteFileTool.ts +15 -0
- package/src/tools/editTool.ts +20 -10
- package/src/tools/multiEditTool.ts +21 -11
- package/src/tools/taskOutputTool.ts +174 -0
- package/src/tools/taskStopTool.ts +72 -0
- package/src/tools/taskTool.ts +130 -74
- package/src/tools/types.ts +7 -0
- package/src/tools/writeTool.ts +14 -0
- package/src/types/commands.ts +3 -0
- package/src/types/configuration.ts +4 -0
- package/src/types/environment.ts +3 -1
- package/src/types/history.ts +4 -0
- package/src/types/index.ts +1 -0
- package/src/types/marketplace.ts +5 -0
- package/src/types/messaging.ts +9 -1
- package/src/types/processes.ts +33 -4
- package/src/types/reversion.ts +29 -0
- package/src/utils/builtinSubagents.ts +18 -0
- package/src/utils/constants.ts +2 -2
- package/src/utils/editUtils.ts +66 -58
- package/src/utils/messageOperations.ts +10 -0
- package/src/utils/openaiClient.ts +69 -35
- package/src/utils/promptHistory.ts +133 -0
- package/dist/utils/bashHistory.d.ts +0 -50
- package/dist/utils/bashHistory.d.ts.map +0 -1
- package/dist/utils/bashHistory.js +0 -256
- package/src/utils/bashHistory.ts +0 -320
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
ValidationResult,
|
|
14
14
|
ConfigurationPaths,
|
|
15
15
|
WaveConfiguration,
|
|
16
|
+
Scope,
|
|
16
17
|
} from "../types/configuration.js";
|
|
17
18
|
import {
|
|
18
19
|
getAllConfigPaths,
|
|
@@ -492,6 +493,26 @@ export class ConfigurationService {
|
|
|
492
493
|
return DEFAULT_WAVE_MAX_INPUT_TOKENS;
|
|
493
494
|
}
|
|
494
495
|
|
|
496
|
+
/**
|
|
497
|
+
* Resolves preferred language with fallbacks
|
|
498
|
+
* Resolution priority: options > settings.json > undefined
|
|
499
|
+
* @param constructorLanguage - Language from constructor (optional)
|
|
500
|
+
* @returns Resolved language or undefined
|
|
501
|
+
*/
|
|
502
|
+
resolveLanguage(constructorLanguage?: string): string | undefined {
|
|
503
|
+
// 1. Constructor options (highest priority)
|
|
504
|
+
if (constructorLanguage !== undefined) {
|
|
505
|
+
return constructorLanguage;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// 2. settings.json (merged)
|
|
509
|
+
if (this.currentConfiguration?.language) {
|
|
510
|
+
return this.currentConfiguration.language;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return undefined;
|
|
514
|
+
}
|
|
515
|
+
|
|
495
516
|
/**
|
|
496
517
|
* Resolves max output tokens with fallbacks
|
|
497
518
|
* Resolution priority: options > env (from settings.json) > process.env > default
|
|
@@ -577,7 +598,7 @@ export class ConfigurationService {
|
|
|
577
598
|
*/
|
|
578
599
|
async updateEnabledPlugin(
|
|
579
600
|
workdir: string,
|
|
580
|
-
scope:
|
|
601
|
+
scope: Scope,
|
|
581
602
|
pluginId: string,
|
|
582
603
|
enabled: boolean,
|
|
583
604
|
): Promise<void> {
|
|
@@ -619,6 +640,48 @@ export class ConfigurationService {
|
|
|
619
640
|
await fs.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
620
641
|
}
|
|
621
642
|
|
|
643
|
+
/**
|
|
644
|
+
* Remove a plugin from the enabled plugins in the specified scope
|
|
645
|
+
*/
|
|
646
|
+
async removeEnabledPlugin(
|
|
647
|
+
workdir: string,
|
|
648
|
+
scope: Scope,
|
|
649
|
+
pluginId: string,
|
|
650
|
+
): Promise<void> {
|
|
651
|
+
if (scope !== "user" && !existsSync(workdir)) {
|
|
652
|
+
throw new Error(`Working directory does not exist: ${workdir}`);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
let configPath: string;
|
|
656
|
+
if (scope === "user") {
|
|
657
|
+
configPath = getUserConfigPaths()[1]; // settings.json
|
|
658
|
+
} else if (scope === "project") {
|
|
659
|
+
configPath = getProjectConfigPaths(workdir)[1]; // settings.json
|
|
660
|
+
} else {
|
|
661
|
+
configPath = getProjectConfigPaths(workdir)[0]; // settings.local.json
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (!existsSync(configPath)) {
|
|
665
|
+
return; // Nothing to remove
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
try {
|
|
669
|
+
const content = await fs.readFile(configPath, "utf-8");
|
|
670
|
+
const config: WaveConfiguration = JSON.parse(content);
|
|
671
|
+
|
|
672
|
+
if (config.enabledPlugins && pluginId in config.enabledPlugins) {
|
|
673
|
+
delete config.enabledPlugins[pluginId];
|
|
674
|
+
await fs.writeFile(
|
|
675
|
+
configPath,
|
|
676
|
+
JSON.stringify(config, null, 2),
|
|
677
|
+
"utf-8",
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
} catch {
|
|
681
|
+
// Ignore errors for corrupted or non-existent files
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
622
685
|
/**
|
|
623
686
|
* Get merged enabled plugins from all scopes
|
|
624
687
|
*/
|
|
@@ -626,6 +689,14 @@ export class ConfigurationService {
|
|
|
626
689
|
const mergedConfig = loadMergedWaveConfig(workdir);
|
|
627
690
|
return mergedConfig?.enabledPlugins || {};
|
|
628
691
|
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Load Wave configuration from a JSON file
|
|
695
|
+
* Supports both hooks and environment variables with proper validation
|
|
696
|
+
*/
|
|
697
|
+
loadWaveConfigFromFile(filePath: string): WaveConfiguration | null {
|
|
698
|
+
return loadWaveConfigFromFile(filePath);
|
|
699
|
+
}
|
|
629
700
|
}
|
|
630
701
|
// =============================================================================
|
|
631
702
|
// Extracted Configuration Functions
|
|
@@ -781,6 +852,7 @@ export function loadWaveConfigFromFile(
|
|
|
781
852
|
env: config.env || undefined,
|
|
782
853
|
permissions: config.permissions || undefined,
|
|
783
854
|
enabledPlugins: config.enabledPlugins || undefined,
|
|
855
|
+
language: config.language || undefined,
|
|
784
856
|
};
|
|
785
857
|
} catch (error) {
|
|
786
858
|
if (error instanceof SyntaxError) {
|
|
@@ -932,6 +1004,11 @@ export function loadMergedWaveConfig(
|
|
|
932
1004
|
if (!mergedConfig.enabledPlugins) mergedConfig.enabledPlugins = {};
|
|
933
1005
|
Object.assign(mergedConfig.enabledPlugins, config.enabledPlugins);
|
|
934
1006
|
}
|
|
1007
|
+
|
|
1008
|
+
// Merge language (last one wins)
|
|
1009
|
+
if (config.language !== undefined) {
|
|
1010
|
+
mergedConfig.language = config.language;
|
|
1011
|
+
}
|
|
935
1012
|
}
|
|
936
1013
|
|
|
937
1014
|
return {
|
|
@@ -953,5 +1030,6 @@ export function loadMergedWaveConfig(
|
|
|
953
1030
|
Object.keys(mergedConfig.enabledPlugins).length > 0
|
|
954
1031
|
? mergedConfig.enabledPlugins
|
|
955
1032
|
: undefined,
|
|
1033
|
+
language: mergedConfig.language,
|
|
956
1034
|
};
|
|
957
1035
|
}
|
|
@@ -163,19 +163,6 @@ export class FileWatcherService extends EventEmitter {
|
|
|
163
163
|
.filter((status): status is FileWatcherStatus => status !== null);
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
/**
|
|
167
|
-
* Configure watcher behavior
|
|
168
|
-
* Runtime configuration updates
|
|
169
|
-
*/
|
|
170
|
-
updateConfig(config: Partial<FileWatcherConfig>): void {
|
|
171
|
-
this.defaultConfig = { ...this.defaultConfig, ...config };
|
|
172
|
-
|
|
173
|
-
// Update existing watchers with new config
|
|
174
|
-
for (const entry of this.watchers.values()) {
|
|
175
|
-
entry.config = { ...entry.config, ...config };
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
166
|
/**
|
|
180
167
|
* Cleanup all watchers
|
|
181
168
|
*/
|
package/src/services/memory.ts
CHANGED
|
@@ -67,7 +67,13 @@ export class PluginLoader {
|
|
|
67
67
|
*/
|
|
68
68
|
static loadCommands(pluginPath: string): CustomSlashCommand[] {
|
|
69
69
|
const commandsPath = path.join(pluginPath, "commands");
|
|
70
|
-
|
|
70
|
+
const commands = scanCommandsDirectory(commandsPath);
|
|
71
|
+
|
|
72
|
+
// Attach plugin path to each command for WAVE_PLUGIN_ROOT support
|
|
73
|
+
return commands.map((command) => ({
|
|
74
|
+
...command,
|
|
75
|
+
pluginPath,
|
|
76
|
+
}));
|
|
71
77
|
}
|
|
72
78
|
|
|
73
79
|
/**
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir, rm } from "fs/promises";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { createHash } from "crypto";
|
|
5
|
+
import { FileSnapshot } from "../types/reversion.js";
|
|
6
|
+
|
|
7
|
+
export class ReversionService {
|
|
8
|
+
private historyBaseDir: string;
|
|
9
|
+
private sessionId: string;
|
|
10
|
+
|
|
11
|
+
constructor(sessionId: string) {
|
|
12
|
+
this.sessionId = sessionId;
|
|
13
|
+
this.historyBaseDir = join(homedir(), ".wave", "file-history", sessionId);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
private getFilePathHash(filePath: string): string {
|
|
17
|
+
return createHash("md5").update(filePath).digest("hex");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private async getNextVersion(fileHashDir: string): Promise<number> {
|
|
21
|
+
try {
|
|
22
|
+
const files = await readFile(join(fileHashDir, "versions"), "utf-8");
|
|
23
|
+
const versions = files
|
|
24
|
+
.split("\n")
|
|
25
|
+
.map((v) => parseInt(v, 10))
|
|
26
|
+
.filter((v) => !isNaN(v));
|
|
27
|
+
return versions.length > 0 ? Math.max(...versions) + 1 : 1;
|
|
28
|
+
} catch {
|
|
29
|
+
return 1;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private async updateVersionsFile(
|
|
34
|
+
fileHashDir: string,
|
|
35
|
+
version: number,
|
|
36
|
+
): Promise<void> {
|
|
37
|
+
await appendFile(join(fileHashDir, "versions"), `${version}\n`, "utf-8");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Saves a single snapshot to the file history directory.
|
|
42
|
+
* Returns the snapshot path.
|
|
43
|
+
*/
|
|
44
|
+
async saveSnapshot(snapshot: FileSnapshot): Promise<string> {
|
|
45
|
+
const fileHash = this.getFilePathHash(snapshot.filePath);
|
|
46
|
+
const fileHashDir = join(this.historyBaseDir, fileHash);
|
|
47
|
+
await this.ensureDirectory(fileHashDir);
|
|
48
|
+
|
|
49
|
+
const version = await this.getNextVersion(fileHashDir);
|
|
50
|
+
const snapshotPath = join(fileHashDir, `v${version}`);
|
|
51
|
+
|
|
52
|
+
const snapshotWithContent = snapshot as FileSnapshot & {
|
|
53
|
+
content: string | null;
|
|
54
|
+
};
|
|
55
|
+
if (snapshotWithContent.content !== null) {
|
|
56
|
+
await writeFile(snapshotPath, snapshotWithContent.content, "utf-8");
|
|
57
|
+
} else {
|
|
58
|
+
// For 'create' operation, the file didn't exist, so we don't write a content file.
|
|
59
|
+
// The absence of the file at snapshotPath will indicate it should be deleted on reversion.
|
|
60
|
+
return ""; // Return empty string to indicate no snapshot file
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
await this.updateVersionsFile(fileHashDir, version);
|
|
64
|
+
return snapshotPath;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Reads snapshot content from the given path.
|
|
69
|
+
*/
|
|
70
|
+
async readSnapshotContent(snapshotPath: string): Promise<string | null> {
|
|
71
|
+
try {
|
|
72
|
+
return await readFile(snapshotPath, "utf-8");
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Deletes all snapshots for this session.
|
|
83
|
+
*/
|
|
84
|
+
async deleteSessionHistory(): Promise<void> {
|
|
85
|
+
await rm(this.historyBaseDir, { recursive: true, force: true });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private async ensureDirectory(dirPath: string): Promise<void> {
|
|
89
|
+
await mkdir(dirPath, { recursive: true });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Helper to avoid appendFile import error if not imported
|
|
94
|
+
import { appendFile } from "fs/promises";
|
package/src/services/session.ts
CHANGED
|
@@ -43,6 +43,15 @@ export interface SessionMetadata {
|
|
|
43
43
|
workdir: string;
|
|
44
44
|
lastActiveAt: Date;
|
|
45
45
|
latestTotalTokens: number;
|
|
46
|
+
firstMessage?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface SessionIndex {
|
|
50
|
+
sessions: Record<
|
|
51
|
+
string,
|
|
52
|
+
Omit<SessionMetadata, "id" | "lastActiveAt"> & { lastActiveAt: string }
|
|
53
|
+
>;
|
|
54
|
+
lastUpdated: string;
|
|
46
55
|
}
|
|
47
56
|
|
|
48
57
|
/**
|
|
@@ -65,6 +74,38 @@ export function generateSubagentFilename(sessionId: string): string {
|
|
|
65
74
|
// Constants
|
|
66
75
|
export const SESSION_DIR = join(homedir(), ".wave", "projects");
|
|
67
76
|
const MAX_SESSION_AGE_DAYS = 14;
|
|
77
|
+
const SESSION_INDEX_FILENAME = "sessions-index.json";
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Update the session index for a project directory
|
|
81
|
+
*/
|
|
82
|
+
async function updateSessionIndex(
|
|
83
|
+
projectDirPath: string,
|
|
84
|
+
metadata: SessionMetadata,
|
|
85
|
+
): Promise<void> {
|
|
86
|
+
const indexPath = join(projectDirPath, SESSION_INDEX_FILENAME);
|
|
87
|
+
let index: SessionIndex = {
|
|
88
|
+
sessions: {},
|
|
89
|
+
lastUpdated: new Date().toISOString(),
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const content = await fs.readFile(indexPath, "utf8");
|
|
94
|
+
index = JSON.parse(content);
|
|
95
|
+
} catch {
|
|
96
|
+
// Index doesn't exist or is invalid, start fresh
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const { id, ...rest } = metadata;
|
|
100
|
+
index.sessions[id] = {
|
|
101
|
+
...rest,
|
|
102
|
+
lastActiveAt: metadata.lastActiveAt.toISOString(),
|
|
103
|
+
firstMessage: metadata.firstMessage || index.sessions[id]?.firstMessage,
|
|
104
|
+
};
|
|
105
|
+
index.lastUpdated = new Date().toISOString();
|
|
106
|
+
|
|
107
|
+
await fs.writeFile(indexPath, JSON.stringify(index, null, 2), "utf8");
|
|
108
|
+
}
|
|
68
109
|
|
|
69
110
|
/**
|
|
70
111
|
* Ensure session directory exists
|
|
@@ -187,6 +228,38 @@ export async function appendMessages(
|
|
|
187
228
|
await jsonlHandler.append(filePath, messagesWithTimestamp, {
|
|
188
229
|
atomic: false,
|
|
189
230
|
});
|
|
231
|
+
|
|
232
|
+
// Update index
|
|
233
|
+
const encoder = new PathEncoder();
|
|
234
|
+
const projectDir = await encoder.getProjectDirectory(workdir, SESSION_DIR);
|
|
235
|
+
const lastMessage = messagesWithTimestamp[messagesWithTimestamp.length - 1];
|
|
236
|
+
|
|
237
|
+
// Get first message content if it's a new session or we don't have it
|
|
238
|
+
let firstMessage: string | undefined;
|
|
239
|
+
try {
|
|
240
|
+
const indexPath = join(projectDir.encodedPath, SESSION_INDEX_FILENAME);
|
|
241
|
+
const content = await fs.readFile(indexPath, "utf8");
|
|
242
|
+
const index = JSON.parse(content) as SessionIndex;
|
|
243
|
+
if (!index.sessions[sessionId]?.firstMessage) {
|
|
244
|
+
firstMessage =
|
|
245
|
+
(await getFirstMessageContent(sessionId, workdir)) || undefined;
|
|
246
|
+
}
|
|
247
|
+
} catch {
|
|
248
|
+
// If index doesn't exist, this might be the first message
|
|
249
|
+
firstMessage =
|
|
250
|
+
(await getFirstMessageContent(sessionId, workdir)) || undefined;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
await updateSessionIndex(projectDir.encodedPath, {
|
|
254
|
+
id: sessionId,
|
|
255
|
+
sessionType,
|
|
256
|
+
workdir,
|
|
257
|
+
lastActiveAt: new Date(lastMessage.timestamp),
|
|
258
|
+
latestTotalTokens: lastMessage.usage
|
|
259
|
+
? extractLatestTotalTokens([lastMessage])
|
|
260
|
+
: 0,
|
|
261
|
+
firstMessage,
|
|
262
|
+
});
|
|
190
263
|
}
|
|
191
264
|
|
|
192
265
|
/**
|
|
@@ -318,6 +391,27 @@ export async function listSessionsFromJsonl(
|
|
|
318
391
|
const baseDir = SESSION_DIR;
|
|
319
392
|
|
|
320
393
|
const projectDir = await encoder.getProjectDirectory(workdir, baseDir);
|
|
394
|
+
|
|
395
|
+
// Try to read from index first
|
|
396
|
+
const indexPath = join(projectDir.encodedPath, SESSION_INDEX_FILENAME);
|
|
397
|
+
try {
|
|
398
|
+
const indexContent = await fs.readFile(indexPath, "utf8");
|
|
399
|
+
const index = JSON.parse(indexContent) as SessionIndex;
|
|
400
|
+
const sessions: SessionMetadata[] = Object.entries(index.sessions)
|
|
401
|
+
.filter(([, meta]) => meta.sessionType === "main")
|
|
402
|
+
.map(([id, meta]) => ({
|
|
403
|
+
id,
|
|
404
|
+
...meta,
|
|
405
|
+
lastActiveAt: new Date(meta.lastActiveAt),
|
|
406
|
+
}));
|
|
407
|
+
|
|
408
|
+
return sessions.sort(
|
|
409
|
+
(a, b) => b.lastActiveAt.getTime() - a.lastActiveAt.getTime(),
|
|
410
|
+
);
|
|
411
|
+
} catch {
|
|
412
|
+
// Fallback to manual listing if index fails
|
|
413
|
+
}
|
|
414
|
+
|
|
321
415
|
let files: string[];
|
|
322
416
|
try {
|
|
323
417
|
files = await fs.readdir(projectDir.encodedPath);
|
|
@@ -370,7 +464,7 @@ export async function listSessionsFromJsonl(
|
|
|
370
464
|
}
|
|
371
465
|
|
|
372
466
|
// Return inline object for performance (no interface instantiation overhead)
|
|
373
|
-
|
|
467
|
+
const sessionMeta: SessionMetadata = {
|
|
374
468
|
id: sessionId,
|
|
375
469
|
sessionType: "main",
|
|
376
470
|
subagentType: undefined, // No longer stored in metadata
|
|
@@ -379,7 +473,19 @@ export async function listSessionsFromJsonl(
|
|
|
379
473
|
latestTotalTokens: lastMessage?.usage
|
|
380
474
|
? extractLatestTotalTokens([lastMessage])
|
|
381
475
|
: 0,
|
|
382
|
-
}
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
// Try to get first message content for the fallback/rebuild case
|
|
479
|
+
try {
|
|
480
|
+
const firstContent = await getFirstMessageContent(sessionId, workdir);
|
|
481
|
+
if (firstContent) {
|
|
482
|
+
sessionMeta.firstMessage = firstContent;
|
|
483
|
+
}
|
|
484
|
+
} catch {
|
|
485
|
+
// Ignore errors getting first message
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
sessions.push(sessionMeta);
|
|
383
489
|
} catch {
|
|
384
490
|
// Skip corrupted session files
|
|
385
491
|
continue;
|
|
@@ -387,9 +493,29 @@ export async function listSessionsFromJsonl(
|
|
|
387
493
|
}
|
|
388
494
|
|
|
389
495
|
// Sort by last active time (most recently active first)
|
|
390
|
-
|
|
496
|
+
const sortedSessions = sessions.sort(
|
|
391
497
|
(a, b) => b.lastActiveAt.getTime() - a.lastActiveAt.getTime(),
|
|
392
498
|
);
|
|
499
|
+
|
|
500
|
+
// Rebuild index if we had to fall back
|
|
501
|
+
try {
|
|
502
|
+
const index: SessionIndex = {
|
|
503
|
+
sessions: {},
|
|
504
|
+
lastUpdated: new Date().toISOString(),
|
|
505
|
+
};
|
|
506
|
+
for (const session of sessions) {
|
|
507
|
+
const { id, ...rest } = session;
|
|
508
|
+
index.sessions[id] = {
|
|
509
|
+
...rest,
|
|
510
|
+
lastActiveAt: session.lastActiveAt.toISOString(),
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
await fs.writeFile(indexPath, JSON.stringify(index, null, 2), "utf8");
|
|
514
|
+
} catch (error) {
|
|
515
|
+
logger.warn(`Failed to rebuild session index for ${workdir}:`, error);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return sortedSessions;
|
|
393
519
|
} catch (error) {
|
|
394
520
|
throw new Error(`Failed to list sessions: ${error}`);
|
|
395
521
|
}
|
|
@@ -432,6 +558,38 @@ export async function cleanupExpiredSessionsFromJsonl(
|
|
|
432
558
|
if (fileAge > maxAge) {
|
|
433
559
|
await fs.unlink(filePath);
|
|
434
560
|
deletedCount++;
|
|
561
|
+
|
|
562
|
+
// Remove from index if it exists
|
|
563
|
+
try {
|
|
564
|
+
const indexPath = join(
|
|
565
|
+
projectDir.encodedPath,
|
|
566
|
+
SESSION_INDEX_FILENAME,
|
|
567
|
+
);
|
|
568
|
+
const indexContent = await fs.readFile(indexPath, "utf8");
|
|
569
|
+
const index = JSON.parse(indexContent) as SessionIndex;
|
|
570
|
+
const uuidMatch = file.match(
|
|
571
|
+
/^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/,
|
|
572
|
+
);
|
|
573
|
+
const subagentMatch = file.match(
|
|
574
|
+
/^subagent-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/,
|
|
575
|
+
);
|
|
576
|
+
const sessionId = uuidMatch
|
|
577
|
+
? uuidMatch[1]
|
|
578
|
+
: subagentMatch
|
|
579
|
+
? subagentMatch[1]
|
|
580
|
+
: null;
|
|
581
|
+
|
|
582
|
+
if (sessionId && index.sessions[sessionId]) {
|
|
583
|
+
delete index.sessions[sessionId];
|
|
584
|
+
await fs.writeFile(
|
|
585
|
+
indexPath,
|
|
586
|
+
JSON.stringify(index, null, 2),
|
|
587
|
+
"utf8",
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
} catch {
|
|
591
|
+
// Ignore index update errors during cleanup
|
|
592
|
+
}
|
|
435
593
|
}
|
|
436
594
|
} catch {
|
|
437
595
|
// Skip failed operations and continue processing other files
|