wave-agent-sdk 0.18.7 → 0.19.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.
- package/builtin/skills/settings/ENV.md +0 -1
- package/builtin/skills/settings/HOOKS.md +10 -0
- package/builtin/skills/settings/MODELS.md +55 -10
- package/builtin/skills/settings/SKILL.md +8 -4
- package/builtin/skills/settings/SKILLS.md +6 -4
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +12 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/managers/aiManager.d.ts.map +1 -1
- package/dist/managers/aiManager.js +20 -4
- package/dist/managers/mcpManager.d.ts.map +1 -1
- package/dist/managers/mcpManager.js +7 -2
- package/dist/managers/messageManager.d.ts +3 -5
- package/dist/managers/messageManager.d.ts.map +1 -1
- package/dist/managers/messageManager.js +17 -77
- package/dist/managers/skillManager.d.ts +1 -0
- package/dist/managers/skillManager.d.ts.map +1 -1
- package/dist/managers/skillManager.js +11 -2
- package/dist/prompts/index.d.ts +3 -2
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +5 -1
- package/dist/services/aiService.d.ts.map +1 -1
- package/dist/services/aiService.js +16 -47
- package/dist/services/configurationService.d.ts.map +1 -1
- package/dist/services/configurationService.js +25 -19
- package/dist/services/hook.d.ts.map +1 -1
- package/dist/services/hook.js +6 -0
- package/dist/services/initializationService.d.ts.map +1 -1
- package/dist/services/initializationService.js +1 -1
- package/dist/services/interactionService.js +1 -1
- package/dist/services/reversionService.d.ts +5 -1
- package/dist/services/reversionService.d.ts.map +1 -1
- package/dist/services/reversionService.js +50 -22
- package/dist/services/session.d.ts +4 -13
- package/dist/services/session.d.ts.map +1 -1
- package/dist/services/session.js +59 -215
- package/dist/telemetry/sessionTracing.d.ts +14 -4
- package/dist/telemetry/sessionTracing.d.ts.map +1 -1
- package/dist/telemetry/sessionTracing.js +134 -8
- package/dist/tools/bashTool.d.ts.map +1 -1
- package/dist/tools/bashTool.js +16 -1
- package/dist/types/config.d.ts +11 -2
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/hooks.d.ts +1 -0
- package/dist/types/hooks.d.ts.map +1 -1
- package/dist/types/messaging.d.ts +0 -1
- package/dist/types/messaging.d.ts.map +1 -1
- package/dist/types/skills.d.ts +1 -0
- package/dist/types/skills.d.ts.map +1 -1
- package/dist/types/telemetry.d.ts +14 -1
- package/dist/types/telemetry.d.ts.map +1 -1
- package/dist/utils/atomicWrite.d.ts +15 -0
- package/dist/utils/atomicWrite.d.ts.map +1 -0
- package/dist/utils/atomicWrite.js +59 -0
- package/dist/utils/cacheControlUtils.d.ts +3 -15
- package/dist/utils/cacheControlUtils.d.ts.map +1 -1
- package/dist/utils/cacheControlUtils.js +4 -35
- package/dist/utils/containerSetup.d.ts.map +1 -1
- package/dist/utils/containerSetup.js +5 -1
- package/dist/utils/convertMessagesForAPI.d.ts +9 -1
- package/dist/utils/convertMessagesForAPI.d.ts.map +1 -1
- package/dist/utils/convertMessagesForAPI.js +74 -55
- package/dist/utils/mcpUtils.d.ts.map +1 -1
- package/dist/utils/mcpUtils.js +2 -1
- package/dist/utils/modelCapabilities.d.ts +22 -0
- package/dist/utils/modelCapabilities.d.ts.map +1 -0
- package/dist/utils/modelCapabilities.js +24 -0
- package/dist/utils/skillParser.d.ts.map +1 -1
- package/dist/utils/skillParser.js +3 -1
- package/package.json +1 -1
- package/src/agent.ts +11 -5
- package/src/index.ts +1 -0
- package/src/managers/aiManager.ts +22 -2
- package/src/managers/mcpManager.ts +8 -2
- package/src/managers/messageManager.ts +22 -104
- package/src/managers/skillManager.ts +16 -2
- package/src/prompts/index.ts +6 -1
- package/src/services/aiService.ts +15 -85
- package/src/services/configurationService.ts +27 -41
- package/src/services/hook.ts +7 -0
- package/src/services/initializationService.ts +1 -3
- package/src/services/interactionService.ts +1 -1
- package/src/services/reversionService.ts +50 -27
- package/src/services/session.ts +63 -262
- package/src/telemetry/sessionTracing.ts +167 -12
- package/src/tools/bashTool.ts +23 -1
- package/src/types/config.ts +12 -2
- package/src/types/hooks.ts +1 -0
- package/src/types/messaging.ts +0 -1
- package/src/types/skills.ts +1 -0
- package/src/types/telemetry.ts +17 -1
- package/src/utils/atomicWrite.ts +61 -0
- package/src/utils/cacheControlUtils.ts +5 -38
- package/src/utils/containerSetup.ts +5 -1
- package/src/utils/convertMessagesForAPI.ts +87 -57
- package/src/utils/mcpUtils.ts +2 -1
- package/src/utils/modelCapabilities.ts +30 -0
- package/src/utils/skillParser.ts +3 -1
|
@@ -30,6 +30,7 @@ import { logger } from "../utils/globalLogger.js";
|
|
|
30
30
|
export class SkillManager extends EventEmitter {
|
|
31
31
|
private personalSkillsPath: string;
|
|
32
32
|
private personalClaudeSkillsPath: string;
|
|
33
|
+
private personalAgentsSkillsPath: string;
|
|
33
34
|
private scanTimeout: number;
|
|
34
35
|
private workdir: string;
|
|
35
36
|
|
|
@@ -50,6 +51,8 @@ export class SkillManager extends EventEmitter {
|
|
|
50
51
|
options.personalSkillsPath || join(homedir(), ".wave", "skills");
|
|
51
52
|
this.personalClaudeSkillsPath =
|
|
52
53
|
options.personalClaudeSkillsPath || join(homedir(), ".claude", "skills");
|
|
54
|
+
this.personalAgentsSkillsPath =
|
|
55
|
+
options.personalAgentsSkillsPath || join(homedir(), ".agents", "skills");
|
|
53
56
|
this.scanTimeout = options.scanTimeout || 5000;
|
|
54
57
|
this.workdir = options.workdir || process.cwd();
|
|
55
58
|
this.watchEnabled = options.watch ?? false;
|
|
@@ -131,8 +134,10 @@ export class SkillManager extends EventEmitter {
|
|
|
131
134
|
const pathsToWatch = [
|
|
132
135
|
this.personalSkillsPath,
|
|
133
136
|
this.personalClaudeSkillsPath,
|
|
137
|
+
this.personalAgentsSkillsPath,
|
|
134
138
|
join(this.workdir, ".wave", "skills"),
|
|
135
139
|
join(this.workdir, ".claude", "skills"),
|
|
140
|
+
join(this.workdir, ".agents", "skills"),
|
|
136
141
|
];
|
|
137
142
|
|
|
138
143
|
logger?.debug(`Setting up skill watcher for: ${pathsToWatch.join(", ")}`);
|
|
@@ -225,6 +230,11 @@ export class SkillManager extends EventEmitter {
|
|
|
225
230
|
"builtin",
|
|
226
231
|
);
|
|
227
232
|
|
|
233
|
+
const personalAgentsCollection = await this.discoverSkillCollection(
|
|
234
|
+
this.personalAgentsSkillsPath,
|
|
235
|
+
"personal",
|
|
236
|
+
);
|
|
237
|
+
|
|
228
238
|
const personalClaudeCollection = await this.discoverSkillCollection(
|
|
229
239
|
this.personalClaudeSkillsPath,
|
|
230
240
|
"personal",
|
|
@@ -243,12 +253,14 @@ export class SkillManager extends EventEmitter {
|
|
|
243
253
|
return {
|
|
244
254
|
builtinSkills: builtinCollection.skills,
|
|
245
255
|
personalSkills: new Map([
|
|
246
|
-
...
|
|
256
|
+
...personalAgentsCollection.skills, // lowest priority
|
|
257
|
+
...personalClaudeCollection.skills, // overrides .agents
|
|
247
258
|
...personalCollection.skills, // .wave overrides .claude
|
|
248
259
|
]),
|
|
249
260
|
projectSkills: projectCollection.skills,
|
|
250
261
|
errors: [
|
|
251
262
|
...builtinCollection.errors,
|
|
263
|
+
...personalAgentsCollection.errors,
|
|
252
264
|
...personalClaudeCollection.errors,
|
|
253
265
|
...personalCollection.errors,
|
|
254
266
|
...projectCollection.errors,
|
|
@@ -271,9 +283,11 @@ export class SkillManager extends EventEmitter {
|
|
|
271
283
|
};
|
|
272
284
|
|
|
273
285
|
if (type === "project") {
|
|
274
|
-
// Scan .
|
|
286
|
+
// Scan .agents/skills first, then .claude/skills, then .wave/skills (wave overrides)
|
|
287
|
+
const agentsPath = join(basePath, ".agents", "skills");
|
|
275
288
|
const claudePath = join(basePath, ".claude", "skills");
|
|
276
289
|
const wavePath = join(basePath, ".wave", "skills");
|
|
290
|
+
await this.scanSkillPath(agentsPath, collection);
|
|
277
291
|
await this.scanSkillPath(claudePath, collection);
|
|
278
292
|
await this.scanSkillPath(wavePath, collection);
|
|
279
293
|
} else {
|
package/src/prompts/index.ts
CHANGED
|
@@ -19,7 +19,11 @@ import {
|
|
|
19
19
|
GREP_TOOL_NAME,
|
|
20
20
|
} from "../constants/tools.js";
|
|
21
21
|
|
|
22
|
-
export const BASE_SYSTEM_PROMPT = `You are
|
|
22
|
+
export const BASE_SYSTEM_PROMPT = `You are a coding agent developed by NetEase that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.`;
|
|
23
|
+
|
|
24
|
+
export const MODEL_IDENTITY_PROMPT = `# Model identity
|
|
25
|
+
|
|
26
|
+
If asked about your model, model version, underlying architecture, or product name, identify yourself simply as a coding agent developed by NetEase. Do not reveal the specific model ID, model family, model provider, or product name. Do not mention or hint at any of these details.`;
|
|
23
27
|
|
|
24
28
|
export const DOING_TASKS_PROMPT = `# Doing tasks
|
|
25
29
|
- The user will primarily request you to perform software engineering tasks. These may include solving bugs, adding new functionality, refactoring code, explaining code, and more. When given an unclear or generic instruction, consider it in the context of these software engineering tasks and the current working directory. For example, if the user asks you to change "methodName" to snake case, do not reply with just "method_name", instead find the method in the code and modify the code.
|
|
@@ -265,6 +269,7 @@ export function buildSystemPrompt(
|
|
|
265
269
|
|
|
266
270
|
staticText += `\n\n${OUTPUT_EFFICIENCY_PROMPT}`;
|
|
267
271
|
staticText += `\n\n${TONE_AND_STYLE_PROMPT}`;
|
|
272
|
+
staticText += `\n\n${MODEL_IDENTITY_PROMPT}`;
|
|
268
273
|
|
|
269
274
|
const blocks: SystemPromptBlock[] = [{ text: staticText, cacheable: true }];
|
|
270
275
|
|
|
@@ -13,11 +13,11 @@ import type { GatewayConfig, ModelConfig } from "../types/index.js";
|
|
|
13
13
|
import { ConfigurationError, CONFIG_ERRORS } from "../types/index.js";
|
|
14
14
|
import {
|
|
15
15
|
transformMessagesForExplicitCache,
|
|
16
|
-
supportsPromptCaching,
|
|
17
16
|
extendUsageWithCacheMetrics,
|
|
18
17
|
type ClaudeUsage,
|
|
19
18
|
type ClaudeChatCompletionContentPartText,
|
|
20
19
|
} from "../utils/cacheControlUtils.js";
|
|
20
|
+
import { supportsPromptCaching } from "../utils/modelCapabilities.js";
|
|
21
21
|
|
|
22
22
|
import * as os from "os";
|
|
23
23
|
import * as fs from "fs";
|
|
@@ -265,13 +265,12 @@ export async function callAgent(
|
|
|
265
265
|
});
|
|
266
266
|
|
|
267
267
|
// Determine model early (needed for system prompt construction)
|
|
268
|
-
const currentModel = model || modelConfig.model;
|
|
269
268
|
const resolvedMaxTokens = options.maxTokens ?? modelConfig.maxTokens;
|
|
270
269
|
|
|
271
270
|
// Build system message content
|
|
272
271
|
let systemMessage: ChatCompletionMessageParam;
|
|
273
272
|
if (Array.isArray(systemPrompt)) {
|
|
274
|
-
if (supportsPromptCaching(
|
|
273
|
+
if (supportsPromptCaching(modelConfig.capabilities)) {
|
|
275
274
|
// For Claude models, map blocks to content parts with cache_control on cacheable blocks
|
|
276
275
|
const contentParts: ClaudeChatCompletionContentPartText[] =
|
|
277
276
|
systemPrompt.map((block) => {
|
|
@@ -307,29 +306,16 @@ export async function callAgent(
|
|
|
307
306
|
|
|
308
307
|
processedTools = tools;
|
|
309
308
|
|
|
310
|
-
if (supportsPromptCaching(
|
|
309
|
+
if (supportsPromptCaching(modelConfig.capabilities)) {
|
|
311
310
|
openaiMessages = transformMessagesForExplicitCache(
|
|
312
311
|
openaiMessages,
|
|
313
|
-
|
|
312
|
+
modelConfig.capabilities,
|
|
314
313
|
);
|
|
315
314
|
}
|
|
316
315
|
|
|
317
|
-
// Get model configuration - use injected modelConfig with optional override
|
|
318
|
-
const {
|
|
319
|
-
model: _model,
|
|
320
|
-
fastModel: _fastModel,
|
|
321
|
-
maxTokens: _maxTokens,
|
|
322
|
-
permissionMode: _permissionMode,
|
|
323
|
-
...extraParams
|
|
324
|
-
} = modelConfig;
|
|
325
|
-
void _model;
|
|
326
|
-
void _fastModel;
|
|
327
|
-
void _maxTokens;
|
|
328
|
-
void _permissionMode;
|
|
329
|
-
|
|
330
316
|
const openaiModelConfig = getModelConfig(model || modelConfig.model, {
|
|
331
317
|
max_tokens: resolvedMaxTokens,
|
|
332
|
-
...
|
|
318
|
+
...(modelConfig.options || {}),
|
|
333
319
|
});
|
|
334
320
|
|
|
335
321
|
// Determine if streaming is needed
|
|
@@ -860,25 +846,11 @@ export async function compactMessages(
|
|
|
860
846
|
fetch: gatewayConfig.fetch,
|
|
861
847
|
});
|
|
862
848
|
|
|
863
|
-
//
|
|
864
|
-
|
|
865
|
-
model: _model,
|
|
866
|
-
fastModel: _fastModel,
|
|
867
|
-
maxTokens: _maxTokens,
|
|
868
|
-
permissionMode: _permissionMode,
|
|
869
|
-
fastModelConfig: _fastModelConfig,
|
|
870
|
-
...extraParams
|
|
871
|
-
} = modelConfig;
|
|
872
|
-
void _model;
|
|
873
|
-
void _fastModel;
|
|
874
|
-
void _maxTokens;
|
|
875
|
-
void _permissionMode;
|
|
876
|
-
|
|
877
|
-
// When a fast model override is provided, use the fast model's hyperparams
|
|
878
|
-
// (if configured); otherwise fall back to the agent model's extraParams.
|
|
849
|
+
// When a fast model override is provided, use the fast model's options
|
|
850
|
+
// (if configured); otherwise fall back to the agent model's options.
|
|
879
851
|
const activeExtraParams = options.model
|
|
880
|
-
?
|
|
881
|
-
:
|
|
852
|
+
? modelConfig.fastModelOptions || {}
|
|
853
|
+
: modelConfig.options || {};
|
|
882
854
|
|
|
883
855
|
const openaiModelConfig = getModelConfig(options.model || modelConfig.model, {
|
|
884
856
|
temperature: 0.1,
|
|
@@ -983,25 +955,11 @@ export async function processWebContent(
|
|
|
983
955
|
fetch: gatewayConfig.fetch,
|
|
984
956
|
});
|
|
985
957
|
|
|
986
|
-
//
|
|
987
|
-
|
|
988
|
-
model: _model,
|
|
989
|
-
fastModel: _fastModel,
|
|
990
|
-
maxTokens: _maxTokens,
|
|
991
|
-
permissionMode: _permissionMode,
|
|
992
|
-
fastModelConfig: _fastModelConfig,
|
|
993
|
-
...extraParams
|
|
994
|
-
} = modelConfig;
|
|
995
|
-
void _model;
|
|
996
|
-
void _fastModel;
|
|
997
|
-
void _maxTokens;
|
|
998
|
-
void _permissionMode;
|
|
999
|
-
|
|
1000
|
-
// When a fast model override is provided, use the fast model's hyperparams
|
|
1001
|
-
// (if configured); otherwise fall back to the agent model's extraParams.
|
|
958
|
+
// When a fast model override is provided, use the fast model's options
|
|
959
|
+
// (if configured); otherwise fall back to the agent model's options.
|
|
1002
960
|
const activeExtraParams = options.model
|
|
1003
|
-
?
|
|
1004
|
-
:
|
|
961
|
+
? modelConfig.fastModelOptions || {}
|
|
962
|
+
: modelConfig.options || {};
|
|
1005
963
|
|
|
1006
964
|
const openaiModelConfig = getModelConfig(options.model || modelConfig.model, {
|
|
1007
965
|
temperature: 0.1,
|
|
@@ -1100,23 +1058,10 @@ export async function btw(options: BtwOptions): Promise<BtwResult> {
|
|
|
1100
1058
|
fetch: gatewayConfig.fetch,
|
|
1101
1059
|
});
|
|
1102
1060
|
|
|
1103
|
-
// Get model configuration - use injected agent model
|
|
1104
|
-
const {
|
|
1105
|
-
model: _model,
|
|
1106
|
-
fastModel: _fastModel,
|
|
1107
|
-
maxTokens: _maxTokens,
|
|
1108
|
-
permissionMode: _permissionMode,
|
|
1109
|
-
...extraParams
|
|
1110
|
-
} = modelConfig;
|
|
1111
|
-
void _model;
|
|
1112
|
-
void _fastModel;
|
|
1113
|
-
void _maxTokens;
|
|
1114
|
-
void _permissionMode;
|
|
1115
|
-
|
|
1116
1061
|
const openaiModelConfig = getModelConfig(options.model || modelConfig.model, {
|
|
1117
1062
|
temperature: 0.1,
|
|
1118
1063
|
max_tokens: 4096,
|
|
1119
|
-
...
|
|
1064
|
+
...(modelConfig.options || {}),
|
|
1120
1065
|
});
|
|
1121
1066
|
|
|
1122
1067
|
try {
|
|
@@ -1207,25 +1152,10 @@ export async function evaluateGoal(
|
|
|
1207
1152
|
fetch: gatewayConfig.fetch,
|
|
1208
1153
|
});
|
|
1209
1154
|
|
|
1210
|
-
const {
|
|
1211
|
-
model: _model,
|
|
1212
|
-
fastModel: _fastModel,
|
|
1213
|
-
maxTokens: _maxTokens,
|
|
1214
|
-
permissionMode: _permissionMode,
|
|
1215
|
-
fastModelConfig: _fastModelConfig,
|
|
1216
|
-
} = modelConfig;
|
|
1217
|
-
void _model;
|
|
1218
|
-
void _fastModel;
|
|
1219
|
-
void _maxTokens;
|
|
1220
|
-
void _permissionMode;
|
|
1221
|
-
|
|
1222
|
-
// evaluateGoal always uses the fast model; use its hyperparams if configured.
|
|
1223
|
-
const activeExtraParams = _fastModelConfig || {};
|
|
1224
|
-
|
|
1225
1155
|
const openaiModelConfig = getModelConfig(model, {
|
|
1226
1156
|
temperature: 0,
|
|
1227
1157
|
max_tokens: 200,
|
|
1228
|
-
...
|
|
1158
|
+
...(modelConfig.fastModelOptions || {}),
|
|
1229
1159
|
});
|
|
1230
1160
|
|
|
1231
1161
|
// Strip images from messages to reduce token usage (same as compact)
|
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
} from "./remoteSettingsService.js";
|
|
49
49
|
import { createAuthAwareFetch } from "./authService.js";
|
|
50
50
|
import { ensureWaveRuntimeFilesExcluded } from "../utils/gitUtils.js";
|
|
51
|
+
import { atomicWriteFile } from "../utils/atomicWrite.js";
|
|
51
52
|
|
|
52
53
|
/**
|
|
53
54
|
* Default ConfigurationService implementation
|
|
@@ -533,25 +534,13 @@ export class ConfigurationService {
|
|
|
533
534
|
permissionMode: permissionMode ?? this.options.permissionMode,
|
|
534
535
|
};
|
|
535
536
|
|
|
536
|
-
// Resolve fast model
|
|
537
|
-
//
|
|
538
|
-
|
|
539
|
-
const fastModelConfigSource =
|
|
537
|
+
// Resolve fast model generation params from models[fastModel].options.
|
|
538
|
+
// Set on baseConfig before the modelSpecificConfig spread so it isn't overwritten.
|
|
539
|
+
const fastModelSource =
|
|
540
540
|
resolvedFastModel &&
|
|
541
541
|
this.currentConfiguration?.models?.[resolvedFastModel];
|
|
542
|
-
if (
|
|
543
|
-
|
|
544
|
-
model: _fm,
|
|
545
|
-
fastModel: _ffm,
|
|
546
|
-
maxTokens: _fmt,
|
|
547
|
-
permissionMode: _fpm,
|
|
548
|
-
...fastHyperparams
|
|
549
|
-
} = fastModelConfigSource;
|
|
550
|
-
void _fm;
|
|
551
|
-
void _ffm;
|
|
552
|
-
void _fmt;
|
|
553
|
-
void _fpm;
|
|
554
|
-
baseConfig.fastModelConfig = fastHyperparams;
|
|
542
|
+
if (fastModelSource && fastModelSource.options) {
|
|
543
|
+
baseConfig.fastModelOptions = fastModelSource.options;
|
|
555
544
|
}
|
|
556
545
|
|
|
557
546
|
// Merge model-specific settings from configuration
|
|
@@ -729,7 +718,7 @@ export class ConfigurationService {
|
|
|
729
718
|
}
|
|
730
719
|
|
|
731
720
|
config.model = model;
|
|
732
|
-
await
|
|
721
|
+
await atomicWriteFile(configPath, JSON.stringify(config, null, 2));
|
|
733
722
|
}
|
|
734
723
|
|
|
735
724
|
/**
|
|
@@ -816,11 +805,7 @@ export class ConfigurationService {
|
|
|
816
805
|
|
|
817
806
|
if (!config.permissions.allow.includes(rule)) {
|
|
818
807
|
config.permissions.allow.push(rule);
|
|
819
|
-
await
|
|
820
|
-
localConfigPath,
|
|
821
|
-
JSON.stringify(config, null, 2),
|
|
822
|
-
"utf-8",
|
|
823
|
-
);
|
|
808
|
+
await atomicWriteFile(localConfigPath, JSON.stringify(config, null, 2));
|
|
824
809
|
}
|
|
825
810
|
}
|
|
826
811
|
|
|
@@ -868,7 +853,7 @@ export class ConfigurationService {
|
|
|
868
853
|
|
|
869
854
|
config.enabledPlugins[pluginId] = enabled;
|
|
870
855
|
|
|
871
|
-
await
|
|
856
|
+
await atomicWriteFile(configPath, JSON.stringify(config, null, 2));
|
|
872
857
|
}
|
|
873
858
|
|
|
874
859
|
/**
|
|
@@ -940,11 +925,7 @@ export class ConfigurationService {
|
|
|
940
925
|
}
|
|
941
926
|
fileConfig.marketplaces[name] = config;
|
|
942
927
|
|
|
943
|
-
await
|
|
944
|
-
configPath,
|
|
945
|
-
JSON.stringify(fileConfig, null, 2),
|
|
946
|
-
"utf-8",
|
|
947
|
-
);
|
|
928
|
+
await atomicWriteFile(configPath, JSON.stringify(fileConfig, null, 2));
|
|
948
929
|
}
|
|
949
930
|
|
|
950
931
|
/**
|
|
@@ -978,11 +959,7 @@ export class ConfigurationService {
|
|
|
978
959
|
|
|
979
960
|
if (fileConfig.marketplaces && name in fileConfig.marketplaces) {
|
|
980
961
|
delete fileConfig.marketplaces[name];
|
|
981
|
-
await
|
|
982
|
-
configPath,
|
|
983
|
-
JSON.stringify(fileConfig, null, 2),
|
|
984
|
-
"utf-8",
|
|
985
|
-
);
|
|
962
|
+
await atomicWriteFile(configPath, JSON.stringify(fileConfig, null, 2));
|
|
986
963
|
}
|
|
987
964
|
} catch {
|
|
988
965
|
// Ignore errors for corrupted or non-existent files
|
|
@@ -1020,11 +997,7 @@ export class ConfigurationService {
|
|
|
1020
997
|
|
|
1021
998
|
if (config.enabledPlugins && pluginId in config.enabledPlugins) {
|
|
1022
999
|
delete config.enabledPlugins[pluginId];
|
|
1023
|
-
await
|
|
1024
|
-
configPath,
|
|
1025
|
-
JSON.stringify(config, null, 2),
|
|
1026
|
-
"utf-8",
|
|
1027
|
-
);
|
|
1000
|
+
await atomicWriteFile(configPath, JSON.stringify(config, null, 2));
|
|
1028
1001
|
}
|
|
1029
1002
|
} catch {
|
|
1030
1003
|
// Ignore errors for corrupted or non-existent files
|
|
@@ -1171,6 +1144,15 @@ export function loadWaveConfigFromFile(
|
|
|
1171
1144
|
|
|
1172
1145
|
try {
|
|
1173
1146
|
const content = readFileSync(filePath, "utf-8");
|
|
1147
|
+
|
|
1148
|
+
// Tolerate empty/whitespace-only files: a concurrent writer may have the
|
|
1149
|
+
// file truncated mid-write. Returning null lets callers skip this source
|
|
1150
|
+
// instead of crashing (consistent with the "file not found" path).
|
|
1151
|
+
if (content.trim() === "") {
|
|
1152
|
+
logger.warn(`Empty configuration file (likely mid-write): ${filePath}`);
|
|
1153
|
+
return null;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1174
1156
|
const config = JSON.parse(content) as WaveConfiguration;
|
|
1175
1157
|
|
|
1176
1158
|
return {
|
|
@@ -1189,10 +1171,14 @@ export function loadWaveConfigFromFile(
|
|
|
1189
1171
|
};
|
|
1190
1172
|
} catch (error) {
|
|
1191
1173
|
if (error instanceof SyntaxError) {
|
|
1192
|
-
|
|
1174
|
+
// Tolerate corrupt JSON: a concurrent writer may leave the file in a
|
|
1175
|
+
// partially-written state. Warn and return null so callers skip this
|
|
1176
|
+
// source instead of crashing.
|
|
1177
|
+
logger.warn(`Invalid JSON syntax in ${filePath}: ${error.message}`);
|
|
1178
|
+
return null;
|
|
1193
1179
|
}
|
|
1194
1180
|
|
|
1195
|
-
// Re-throw
|
|
1181
|
+
// Re-throw other errors as-is
|
|
1196
1182
|
throw error;
|
|
1197
1183
|
}
|
|
1198
1184
|
}
|
package/src/services/hook.ts
CHANGED
|
@@ -89,6 +89,13 @@ async function buildHookJsonInput(
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
// Add worktree_path field for WorktreeRemove events
|
|
93
|
+
if (context.event === "WorktreeRemove") {
|
|
94
|
+
if (context.worktreePath !== undefined) {
|
|
95
|
+
jsonInput.worktree_path = context.worktreePath;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
92
99
|
// Add SessionStart-specific fields
|
|
93
100
|
if (context.event === "SessionStart") {
|
|
94
101
|
if (context.source !== undefined) {
|
|
@@ -326,9 +326,7 @@ export class InitializationService {
|
|
|
326
326
|
messageManager.initializeFromSession(sessionToRestore);
|
|
327
327
|
|
|
328
328
|
// Update task manager with the root session ID to ensure continuity across compactions
|
|
329
|
-
taskManager.setTaskListId(
|
|
330
|
-
sessionToRestore.rootSessionId || sessionToRestore.id,
|
|
331
|
-
);
|
|
329
|
+
taskManager.setTaskListId(sessionToRestore.id);
|
|
332
330
|
|
|
333
331
|
// After session is initialized, load tasks for the session
|
|
334
332
|
const tasks = await taskManager.listTasks();
|
|
@@ -166,7 +166,7 @@ export class InteractionService {
|
|
|
166
166
|
messageManager.initializeFromSession(sessionData);
|
|
167
167
|
|
|
168
168
|
// Update task manager with the root session ID to ensure continuity across compactions
|
|
169
|
-
taskManager.setTaskListId(sessionData.
|
|
169
|
+
taskManager.setTaskListId(sessionData.id);
|
|
170
170
|
|
|
171
171
|
// 7. Load tasks for the restored session
|
|
172
172
|
const tasks = await taskManager.listTasks();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { readFile, writeFile, mkdir, rm } from "fs/promises";
|
|
2
|
-
import { join } from "path";
|
|
1
|
+
import { readFile, writeFile, mkdir, rm, readdir, stat } from "fs/promises";
|
|
2
|
+
import { join, dirname } from "path";
|
|
3
3
|
import { homedir } from "os";
|
|
4
4
|
import { createHash } from "crypto";
|
|
5
5
|
import { FileSnapshot } from "../types/reversion.js";
|
|
@@ -9,50 +9,49 @@ export class ReversionService {
|
|
|
9
9
|
private rootSessionId: string;
|
|
10
10
|
|
|
11
11
|
constructor(rootSessionId: string) {
|
|
12
|
-
this.rootSessionId = rootSessionId;
|
|
12
|
+
this.rootSessionId = rootSessionId || "default";
|
|
13
13
|
this.historyBaseDir = join(
|
|
14
14
|
homedir(),
|
|
15
15
|
".wave",
|
|
16
16
|
"file-history",
|
|
17
|
-
rootSessionId
|
|
17
|
+
this.rootSessionId,
|
|
18
18
|
);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
private getFilePathHash(filePath: string): string {
|
|
22
|
-
return createHash("
|
|
22
|
+
return createHash("sha256").update(filePath).digest("hex").slice(0, 16);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
private async getNextVersion(
|
|
25
|
+
private async getNextVersion(filePath: string): Promise<number> {
|
|
26
|
+
const hash = this.getFilePathHash(filePath);
|
|
27
|
+
const prefix = `${hash}@v`;
|
|
26
28
|
try {
|
|
27
|
-
const files = await
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
.
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
const files = await readdir(this.historyBaseDir);
|
|
30
|
+
let maxVersion = 0;
|
|
31
|
+
for (const file of files) {
|
|
32
|
+
if (file.startsWith(prefix)) {
|
|
33
|
+
const v = parseInt(file.slice(prefix.length), 10);
|
|
34
|
+
if (!isNaN(v) && v > maxVersion) {
|
|
35
|
+
maxVersion = v;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return maxVersion + 1;
|
|
33
40
|
} catch {
|
|
34
41
|
return 1;
|
|
35
42
|
}
|
|
36
43
|
}
|
|
37
44
|
|
|
38
|
-
private async updateVersionsFile(
|
|
39
|
-
fileHashDir: string,
|
|
40
|
-
version: number,
|
|
41
|
-
): Promise<void> {
|
|
42
|
-
await appendFile(join(fileHashDir, "versions"), `${version}\n`, "utf-8");
|
|
43
|
-
}
|
|
44
|
-
|
|
45
45
|
/**
|
|
46
46
|
* Saves a single snapshot to the file history directory.
|
|
47
47
|
* Returns the snapshot path.
|
|
48
48
|
*/
|
|
49
49
|
async saveSnapshot(snapshot: FileSnapshot): Promise<string> {
|
|
50
50
|
const fileHash = this.getFilePathHash(snapshot.filePath);
|
|
51
|
-
|
|
52
|
-
await this.ensureDirectory(fileHashDir);
|
|
51
|
+
await this.ensureDirectory(this.historyBaseDir);
|
|
53
52
|
|
|
54
|
-
const version = await this.getNextVersion(
|
|
55
|
-
const snapshotPath = join(
|
|
53
|
+
const version = await this.getNextVersion(snapshot.filePath);
|
|
54
|
+
const snapshotPath = join(this.historyBaseDir, `${fileHash}@v${version}`);
|
|
56
55
|
|
|
57
56
|
const snapshotWithContent = snapshot as FileSnapshot & {
|
|
58
57
|
content: string | null;
|
|
@@ -65,7 +64,6 @@ export class ReversionService {
|
|
|
65
64
|
return ""; // Return empty string to indicate no snapshot file
|
|
66
65
|
}
|
|
67
66
|
|
|
68
|
-
await this.updateVersionsFile(fileHashDir, version);
|
|
69
67
|
return snapshotPath;
|
|
70
68
|
}
|
|
71
69
|
|
|
@@ -90,10 +88,35 @@ export class ReversionService {
|
|
|
90
88
|
await rm(this.historyBaseDir, { recursive: true, force: true });
|
|
91
89
|
}
|
|
92
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Cleans up session directories older than the given number of days.
|
|
93
|
+
* Skips the current session.
|
|
94
|
+
*/
|
|
95
|
+
async cleanupOldSessions(days: number = 30): Promise<void> {
|
|
96
|
+
const threshold = Date.now() - days * 24 * 60 * 60 * 1000;
|
|
97
|
+
const parentDir = dirname(this.historyBaseDir);
|
|
98
|
+
try {
|
|
99
|
+
const entries = await readdir(parentDir);
|
|
100
|
+
await Promise.all(
|
|
101
|
+
entries.map(async (entry) => {
|
|
102
|
+
if (entry === this.rootSessionId) return;
|
|
103
|
+
const dirPath = join(parentDir, entry);
|
|
104
|
+
try {
|
|
105
|
+
const stats = await stat(dirPath);
|
|
106
|
+
if (stats.isDirectory() && stats.mtimeMs < threshold) {
|
|
107
|
+
await rm(dirPath, { recursive: true, force: true });
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
// ENOENT — concurrently deleted, skip
|
|
111
|
+
}
|
|
112
|
+
}),
|
|
113
|
+
);
|
|
114
|
+
} catch {
|
|
115
|
+
// parent dir doesn't exist — nothing to clean
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
93
119
|
private async ensureDirectory(dirPath: string): Promise<void> {
|
|
94
120
|
await mkdir(dirPath, { recursive: true });
|
|
95
121
|
}
|
|
96
122
|
}
|
|
97
|
-
|
|
98
|
-
// Helper to avoid appendFile import error if not imported
|
|
99
|
-
import { appendFile } from "fs/promises";
|