wave-agent-sdk 0.16.8 → 0.16.10
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.map +1 -1
- package/dist/agent.js +3 -0
- package/dist/constants/tools.d.ts +0 -1
- package/dist/constants/tools.d.ts.map +1 -1
- package/dist/constants/tools.js +0 -1
- package/dist/managers/aiManager.d.ts +0 -8
- package/dist/managers/aiManager.d.ts.map +1 -1
- package/dist/managers/aiManager.js +0 -45
- package/dist/managers/mcpManager.d.ts +5 -0
- package/dist/managers/mcpManager.d.ts.map +1 -1
- package/dist/managers/mcpManager.js +107 -12
- package/dist/managers/toolManager.d.ts +0 -6
- package/dist/managers/toolManager.d.ts.map +1 -1
- package/dist/managers/toolManager.js +1 -28
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +1 -12
- package/dist/services/authService.d.ts +10 -0
- package/dist/services/authService.d.ts.map +1 -1
- package/dist/services/authService.js +45 -0
- package/dist/services/configurationService.d.ts +1 -0
- package/dist/services/configurationService.d.ts.map +1 -1
- package/dist/services/configurationService.js +48 -15
- package/dist/services/initializationService.d.ts.map +1 -1
- package/dist/services/initializationService.js +11 -0
- package/dist/services/pluginLoader.d.ts.map +1 -1
- package/dist/services/pluginLoader.js +2 -2
- package/dist/services/remoteSettingsService.d.ts +21 -0
- package/dist/services/remoteSettingsService.d.ts.map +1 -0
- package/dist/services/remoteSettingsService.js +279 -0
- package/dist/telemetry/instrumentation.d.ts +5 -2
- package/dist/telemetry/instrumentation.d.ts.map +1 -1
- package/dist/telemetry/instrumentation.js +8 -4
- package/dist/tools/buildTool.d.ts +0 -2
- package/dist/tools/buildTool.d.ts.map +1 -1
- package/dist/tools/buildTool.js +0 -2
- package/dist/tools/cronCreateTool.d.ts.map +1 -1
- package/dist/tools/cronCreateTool.js +0 -1
- package/dist/tools/cronDeleteTool.d.ts.map +1 -1
- package/dist/tools/cronDeleteTool.js +0 -1
- package/dist/tools/cronListTool.d.ts.map +1 -1
- package/dist/tools/cronListTool.js +0 -1
- package/dist/tools/enterWorktreeTool.d.ts.map +1 -1
- package/dist/tools/enterWorktreeTool.js +0 -1
- package/dist/tools/exitWorktreeTool.d.ts.map +1 -1
- package/dist/tools/exitWorktreeTool.js +0 -1
- package/dist/tools/taskManagementTools.d.ts.map +1 -1
- package/dist/tools/taskManagementTools.js +0 -4
- package/dist/tools/types.d.ts +0 -15
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/webFetchTool.d.ts.map +1 -1
- package/dist/tools/webFetchTool.js +0 -1
- package/dist/types/configuration.d.ts +20 -0
- package/dist/types/configuration.d.ts.map +1 -1
- package/dist/types/mcp.d.ts +3 -1
- package/dist/types/mcp.d.ts.map +1 -1
- package/dist/utils/containerSetup.d.ts.map +1 -1
- package/dist/utils/containerSetup.js +10 -0
- package/dist/utils/mcpUtils.d.ts.map +1 -1
- package/dist/utils/mcpUtils.js +0 -1
- package/dist/utils/openaiClient.d.ts.map +1 -1
- package/dist/utils/openaiClient.js +4 -2
- package/package.json +1 -1
- package/src/agent.ts +3 -0
- package/src/constants/tools.ts +0 -1
- package/src/managers/aiManager.ts +0 -48
- package/src/managers/mcpManager.ts +122 -16
- package/src/managers/toolManager.ts +1 -32
- package/src/prompts/index.ts +0 -13
- package/src/services/authService.ts +56 -0
- package/src/services/configurationService.ts +56 -19
- package/src/services/initializationService.ts +13 -0
- package/src/services/pluginLoader.ts +2 -2
- package/src/services/remoteSettingsService.ts +314 -0
- package/src/telemetry/instrumentation.ts +12 -4
- package/src/tools/buildTool.ts +0 -4
- package/src/tools/cronCreateTool.ts +0 -1
- package/src/tools/cronDeleteTool.ts +0 -1
- package/src/tools/cronListTool.ts +0 -1
- package/src/tools/enterWorktreeTool.ts +0 -1
- package/src/tools/exitWorktreeTool.ts +0 -1
- package/src/tools/taskManagementTools.ts +0 -4
- package/src/tools/types.ts +0 -15
- package/src/tools/webFetchTool.ts +0 -1
- package/src/types/configuration.ts +23 -0
- package/src/types/mcp.ts +8 -1
- package/src/utils/containerSetup.ts +10 -0
- package/src/utils/mcpUtils.ts +0 -1
- package/src/utils/openaiClient.ts +5 -2
- package/dist/tools/toolSearchTool.d.ts +0 -15
- package/dist/tools/toolSearchTool.d.ts.map +0 -1
- package/dist/tools/toolSearchTool.js +0 -200
- package/dist/utils/isDeferredTool.d.ts +0 -19
- package/dist/utils/isDeferredTool.d.ts.map +0 -1
- package/dist/utils/isDeferredTool.js +0 -31
- package/src/tools/toolSearchTool.ts +0 -245
- package/src/utils/isDeferredTool.ts +0 -36
package/src/prompts/index.ts
CHANGED
|
@@ -18,9 +18,7 @@ import {
|
|
|
18
18
|
READ_TOOL_NAME,
|
|
19
19
|
GLOB_TOOL_NAME,
|
|
20
20
|
GREP_TOOL_NAME,
|
|
21
|
-
TOOL_SEARCH_TOOL_NAME,
|
|
22
21
|
} from "../constants/tools.js";
|
|
23
|
-
import { isDeferredTool } from "../utils/isDeferredTool.js";
|
|
24
22
|
|
|
25
23
|
export const BASE_SYSTEM_PROMPT = `You are an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.`;
|
|
26
24
|
|
|
@@ -260,17 +258,6 @@ export function buildSystemPrompt(
|
|
|
260
258
|
prompt += `\n\n${TOOL_POLICY}`;
|
|
261
259
|
}
|
|
262
260
|
|
|
263
|
-
// List available deferred tool names with descriptions so the model knows they exist
|
|
264
|
-
// and can decide which ones to discover via ToolSearch
|
|
265
|
-
const deferredTools = tools.filter(isDeferredTool);
|
|
266
|
-
if (deferredTools.length > 0) {
|
|
267
|
-
const lines = deferredTools.map((t) => {
|
|
268
|
-
const desc = t.config.function?.description;
|
|
269
|
-
return desc ? `${t.name} - ${desc}` : t.name;
|
|
270
|
-
});
|
|
271
|
-
prompt += `\n\n<available-deferred-tools>\n${lines.join("\n")}\nThese tools are NOT loaded yet — call ${TOOL_SEARCH_TOOL_NAME} first to discover their schemas before invoking them.</available-deferred-tools>`;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
261
|
prompt += `\n\n${OUTPUT_EFFICIENCY_PROMPT}`;
|
|
275
262
|
prompt += `\n\n${TONE_AND_STYLE_PROMPT}`;
|
|
276
263
|
|
|
@@ -15,12 +15,16 @@ import {
|
|
|
15
15
|
} from "fs";
|
|
16
16
|
import * as path from "path";
|
|
17
17
|
import * as os from "os";
|
|
18
|
+
import { randomBytes } from "crypto";
|
|
18
19
|
import { createServer, Server } from "http";
|
|
19
20
|
import { URL } from "url";
|
|
20
21
|
import { execFile } from "child_process";
|
|
21
22
|
import { promisify } from "util";
|
|
22
23
|
import type { AuthConfig, AuthUser } from "../types/auth.js";
|
|
23
24
|
|
|
25
|
+
/** Persistent anonymous ID for telemetry fallback when SSO is not authenticated. */
|
|
26
|
+
let _anonymousId: string | undefined;
|
|
27
|
+
|
|
24
28
|
const execFileAsync = promisify(execFile);
|
|
25
29
|
|
|
26
30
|
export class AuthService {
|
|
@@ -312,3 +316,55 @@ export class AuthService {
|
|
|
312
316
|
}
|
|
313
317
|
|
|
314
318
|
export const authService = AuthService.getInstance();
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Get or create a persistent anonymous ID for telemetry.
|
|
322
|
+
*
|
|
323
|
+
* Stored in ~/.wave/config.json as { anonymousId: "..." }.
|
|
324
|
+
* Generated once on first run (32-byte random hex) and reused thereafter.
|
|
325
|
+
* Falls back to an in-memory ID if file I/O fails.
|
|
326
|
+
*/
|
|
327
|
+
export function getOrCreateAnonymousId(): string {
|
|
328
|
+
if (_anonymousId) return _anonymousId;
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
const configPath = path.join(os.homedir(), ".wave", "config.json");
|
|
332
|
+
if (existsSync(configPath)) {
|
|
333
|
+
const content = readFileSync(configPath, "utf-8");
|
|
334
|
+
const config = JSON.parse(content) as { anonymousId?: string };
|
|
335
|
+
if (config.anonymousId) {
|
|
336
|
+
_anonymousId = config.anonymousId;
|
|
337
|
+
return _anonymousId;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Generate and persist
|
|
342
|
+
_anonymousId = randomBytes(32).toString("hex");
|
|
343
|
+
const waveDir = path.dirname(configPath);
|
|
344
|
+
if (!existsSync(waveDir)) {
|
|
345
|
+
mkdirSync(waveDir, { recursive: true });
|
|
346
|
+
}
|
|
347
|
+
const existing = existsSync(configPath)
|
|
348
|
+
? (JSON.parse(readFileSync(configPath, "utf-8")) as Record<
|
|
349
|
+
string,
|
|
350
|
+
unknown
|
|
351
|
+
>)
|
|
352
|
+
: {};
|
|
353
|
+
writeFileSync(
|
|
354
|
+
configPath,
|
|
355
|
+
JSON.stringify({ ...existing, anonymousId: _anonymousId }, null, 2),
|
|
356
|
+
"utf-8",
|
|
357
|
+
);
|
|
358
|
+
chmodSync(configPath, 0o600);
|
|
359
|
+
} catch {
|
|
360
|
+
// File I/O failed — use in-memory fallback
|
|
361
|
+
_anonymousId = randomBytes(32).toString("hex");
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return _anonymousId;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** @internal — reset anonymous ID cache for testing only */
|
|
368
|
+
export function __resetAnonymousIdForTesting(): void {
|
|
369
|
+
_anonymousId = undefined;
|
|
370
|
+
}
|
|
@@ -44,6 +44,10 @@ import {
|
|
|
44
44
|
} from "../utils/constants.js";
|
|
45
45
|
import { ClientOptions } from "openai";
|
|
46
46
|
import { parseCustomHeaders } from "../utils/stringUtils.js";
|
|
47
|
+
import {
|
|
48
|
+
getRemoteSettingsSync,
|
|
49
|
+
mergeRemoteSettings,
|
|
50
|
+
} from "./remoteSettingsService.js";
|
|
47
51
|
|
|
48
52
|
/**
|
|
49
53
|
* Default ConfigurationService implementation
|
|
@@ -101,19 +105,25 @@ export class ConfigurationService {
|
|
|
101
105
|
};
|
|
102
106
|
}
|
|
103
107
|
|
|
108
|
+
// Merge remote settings (highest priority: Remote > Local > Project > User)
|
|
109
|
+
const remoteSettings = getRemoteSettingsSync();
|
|
110
|
+
const finalConfig = remoteSettings
|
|
111
|
+
? mergeRemoteSettings(mergedConfig, remoteSettings)
|
|
112
|
+
: mergedConfig;
|
|
113
|
+
|
|
104
114
|
// Success case
|
|
105
|
-
this.currentConfiguration =
|
|
115
|
+
this.currentConfiguration = finalConfig;
|
|
106
116
|
|
|
107
117
|
// Set environment variables from merged config and inject system variables
|
|
108
118
|
const env = {
|
|
109
|
-
...(
|
|
119
|
+
...(finalConfig.env || {}),
|
|
110
120
|
WAVE_PROJECT_DIR: workdir,
|
|
111
121
|
};
|
|
112
122
|
this.setEnvironmentVars(env);
|
|
113
|
-
|
|
123
|
+
finalConfig.env = env;
|
|
114
124
|
|
|
115
125
|
return {
|
|
116
|
-
configuration:
|
|
126
|
+
configuration: finalConfig,
|
|
117
127
|
success: true,
|
|
118
128
|
sourcePath: "merged configuration",
|
|
119
129
|
warnings: validation.warnings,
|
|
@@ -461,8 +471,7 @@ export class ConfigurationService {
|
|
|
461
471
|
}
|
|
462
472
|
|
|
463
473
|
// Resolve custom headers from environment: env (settings.json) > process.env
|
|
464
|
-
const envCustomHeaders =
|
|
465
|
-
process.env.WAVE_CUSTOM_HEADERS || process.env.WAVE_CUSTOM_HEADERS || "";
|
|
474
|
+
const envCustomHeaders = process.env.WAVE_CUSTOM_HEADERS || "";
|
|
466
475
|
const parsedEnvHeaders = parseCustomHeaders(envCustomHeaders);
|
|
467
476
|
|
|
468
477
|
// Merge headers: env headers < options < override
|
|
@@ -497,19 +506,16 @@ export class ConfigurationService {
|
|
|
497
506
|
maxTokens?: number,
|
|
498
507
|
permissionMode?: PermissionMode,
|
|
499
508
|
): ModelConfig {
|
|
500
|
-
// Resolve agent model: override > options > env (settings.json
|
|
509
|
+
// Resolve agent model: override > options > process.env (includes settings.json env)
|
|
501
510
|
const resolvedAgentModel =
|
|
502
511
|
model ||
|
|
503
512
|
this.options.model ||
|
|
504
513
|
process.env.WAVE_MODEL ||
|
|
505
|
-
|
|
514
|
+
this.currentConfiguration?.model;
|
|
506
515
|
|
|
507
|
-
// Resolve fast model: override > options > env (settings.json
|
|
516
|
+
// Resolve fast model: override > options > process.env (includes settings.json env)
|
|
508
517
|
const resolvedFastModel =
|
|
509
|
-
fastModel ||
|
|
510
|
-
this.options.fastModel ||
|
|
511
|
-
process.env.WAVE_FAST_MODEL ||
|
|
512
|
-
process.env.WAVE_FAST_MODEL;
|
|
518
|
+
fastModel || this.options.fastModel || process.env.WAVE_FAST_MODEL;
|
|
513
519
|
|
|
514
520
|
// Validate required fields
|
|
515
521
|
if (!resolvedAgentModel) {
|
|
@@ -572,8 +578,7 @@ export class ConfigurationService {
|
|
|
572
578
|
}
|
|
573
579
|
|
|
574
580
|
// Try env (settings.json) first, then process.env
|
|
575
|
-
const envMaxInputTokens =
|
|
576
|
-
process.env.WAVE_MAX_INPUT_TOKENS || process.env.WAVE_MAX_INPUT_TOKENS;
|
|
581
|
+
const envMaxInputTokens = process.env.WAVE_MAX_INPUT_TOKENS;
|
|
577
582
|
if (envMaxInputTokens) {
|
|
578
583
|
const parsed = parseInt(envMaxInputTokens, 10);
|
|
579
584
|
if (!isNaN(parsed)) {
|
|
@@ -677,8 +682,7 @@ export class ConfigurationService {
|
|
|
677
682
|
}
|
|
678
683
|
|
|
679
684
|
// Try env (settings.json) first, then process.env
|
|
680
|
-
const envMaxOutputTokens =
|
|
681
|
-
process.env.WAVE_MAX_OUTPUT_TOKENS || process.env.WAVE_MAX_OUTPUT_TOKENS;
|
|
685
|
+
const envMaxOutputTokens = process.env.WAVE_MAX_OUTPUT_TOKENS;
|
|
682
686
|
if (envMaxOutputTokens) {
|
|
683
687
|
const parsed = parseInt(envMaxOutputTokens, 10);
|
|
684
688
|
if (!isNaN(parsed) && parsed > 0) {
|
|
@@ -695,6 +699,28 @@ export class ConfigurationService {
|
|
|
695
699
|
*/
|
|
696
700
|
setModel(model: string): void {
|
|
697
701
|
this.options.model = model;
|
|
702
|
+
this.persistModelToSettings(model);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
private async persistModelToSettings(model: string): Promise<void> {
|
|
706
|
+
const configPath = getUserConfigPaths()[0]; // ~/.wave/settings.json
|
|
707
|
+
const configDir = path.dirname(configPath);
|
|
708
|
+
if (!existsSync(configDir)) {
|
|
709
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
let config: WaveConfiguration = {};
|
|
713
|
+
if (existsSync(configPath)) {
|
|
714
|
+
try {
|
|
715
|
+
const content = await fs.readFile(configPath, "utf-8");
|
|
716
|
+
config = JSON.parse(content);
|
|
717
|
+
} catch {
|
|
718
|
+
// Start fresh if corrupted
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
config.model = model;
|
|
723
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
698
724
|
}
|
|
699
725
|
|
|
700
726
|
/**
|
|
@@ -704,12 +730,16 @@ export class ConfigurationService {
|
|
|
704
730
|
const models = new Set<string>();
|
|
705
731
|
|
|
706
732
|
// Add current model from options or environment
|
|
707
|
-
const currentModel =
|
|
708
|
-
this.options.model || process.env.WAVE_MODEL || process.env.WAVE_MODEL;
|
|
733
|
+
const currentModel = this.options.model || process.env.WAVE_MODEL;
|
|
709
734
|
if (currentModel) {
|
|
710
735
|
models.add(currentModel);
|
|
711
736
|
}
|
|
712
737
|
|
|
738
|
+
// Persisted model from settings (includes remote-merged)
|
|
739
|
+
if (this.currentConfiguration?.model) {
|
|
740
|
+
models.add(this.currentConfiguration.model);
|
|
741
|
+
}
|
|
742
|
+
|
|
713
743
|
// Add models from merged configuration
|
|
714
744
|
if (this.currentConfiguration?.models) {
|
|
715
745
|
Object.keys(this.currentConfiguration.models).forEach((model) => {
|
|
@@ -1139,6 +1169,7 @@ export function loadWaveConfigFromFile(
|
|
|
1139
1169
|
permissions: config.permissions || undefined,
|
|
1140
1170
|
enabledPlugins: config.enabledPlugins || undefined,
|
|
1141
1171
|
language: config.language || undefined,
|
|
1172
|
+
model: config.model || undefined,
|
|
1142
1173
|
autoMemoryEnabled:
|
|
1143
1174
|
config.autoMemoryEnabled !== undefined
|
|
1144
1175
|
? config.autoMemoryEnabled
|
|
@@ -1268,6 +1299,11 @@ export function loadMergedWaveConfig(
|
|
|
1268
1299
|
mergedConfig.language = config.language;
|
|
1269
1300
|
}
|
|
1270
1301
|
|
|
1302
|
+
// Merge model (last one wins)
|
|
1303
|
+
if (config.model !== undefined) {
|
|
1304
|
+
mergedConfig.model = config.model;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1271
1307
|
// Merge autoMemoryEnabled (last one wins)
|
|
1272
1308
|
if (config.autoMemoryEnabled !== undefined) {
|
|
1273
1309
|
mergedConfig.autoMemoryEnabled = config.autoMemoryEnabled;
|
|
@@ -1316,6 +1352,7 @@ export function loadMergedWaveConfig(
|
|
|
1316
1352
|
? mergedConfig.enabledPlugins
|
|
1317
1353
|
: undefined,
|
|
1318
1354
|
language: mergedConfig.language,
|
|
1355
|
+
model: mergedConfig.model,
|
|
1319
1356
|
autoMemoryEnabled: mergedConfig.autoMemoryEnabled,
|
|
1320
1357
|
marketplaces:
|
|
1321
1358
|
mergedConfig.marketplaces &&
|
|
@@ -22,6 +22,7 @@ import type { MemoryRuleManager } from "../managers/MemoryRuleManager.js";
|
|
|
22
22
|
import type { LiveConfigManager } from "../managers/liveConfigManager.js";
|
|
23
23
|
import type { TaskManager } from "./taskManager.js";
|
|
24
24
|
import type { PermissionManager } from "../managers/permissionManager.js";
|
|
25
|
+
import { remoteSettingsService } from "./remoteSettingsService.js";
|
|
25
26
|
|
|
26
27
|
export interface InitializationContext {
|
|
27
28
|
skillManager: SkillManager;
|
|
@@ -288,6 +289,18 @@ export class InitializationService {
|
|
|
288
289
|
// Don't throw error to prevent app startup failure - continue without live reload
|
|
289
290
|
}
|
|
290
291
|
|
|
292
|
+
// Initialize remote settings (fetch server-managed config)
|
|
293
|
+
try {
|
|
294
|
+
const phaseStart = performance.now();
|
|
295
|
+
await remoteSettingsService.initialize();
|
|
296
|
+
logger?.debug(
|
|
297
|
+
`Initialization Phase [Remote Settings] took ${(performance.now() - phaseStart).toFixed(2)}ms`,
|
|
298
|
+
);
|
|
299
|
+
} catch (error) {
|
|
300
|
+
logger?.error("Failed to initialize remote settings:", error);
|
|
301
|
+
// Don't throw error to prevent app startup failure - continue without remote settings
|
|
302
|
+
}
|
|
303
|
+
|
|
291
304
|
// Memory is lazy-cached on first getCombinedMemoryContent call
|
|
292
305
|
// No explicit loading needed during initialization
|
|
293
306
|
|
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
parseAgentFile,
|
|
15
15
|
type SubagentConfiguration,
|
|
16
16
|
} from "../utils/subagentParser.js";
|
|
17
|
-
import { resolveMcpConfig } from "../managers/mcpManager.js";
|
|
18
17
|
import { logger } from "../utils/globalLogger.js";
|
|
19
18
|
|
|
20
19
|
export class PluginLoader {
|
|
@@ -143,7 +142,8 @@ export class PluginLoader {
|
|
|
143
142
|
const mcpPath = path.join(pluginPath, ".mcp.json");
|
|
144
143
|
try {
|
|
145
144
|
const content = await fs.readFile(mcpPath, "utf-8");
|
|
146
|
-
|
|
145
|
+
// Return raw config — let McpManager resolve templates and capture originalUrl
|
|
146
|
+
return JSON.parse(content) as McpConfig;
|
|
147
147
|
} catch {
|
|
148
148
|
return undefined;
|
|
149
149
|
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { authService } from "./authService.js";
|
|
6
|
+
import type {
|
|
7
|
+
RemoteSettingsCache,
|
|
8
|
+
RemoteSettingsFetchResult,
|
|
9
|
+
RemoteSettingsResponse,
|
|
10
|
+
} from "../types/configuration.js";
|
|
11
|
+
import type { WaveConfiguration } from "../types/configuration.js";
|
|
12
|
+
import type { HookEvent, HookEventConfig } from "../types/hooks.js";
|
|
13
|
+
import { logger } from "../utils/globalLogger.js";
|
|
14
|
+
|
|
15
|
+
const CACHE_FILE = path.join(homedir(), ".wave", "remote-settings.json");
|
|
16
|
+
const POLLING_INTERVAL_MS = 60 * 60 * 1000; // 60 minutes
|
|
17
|
+
const FETCH_TIMEOUT_MS = 10_000;
|
|
18
|
+
|
|
19
|
+
let _cachedSettings: RemoteSettingsCache | null = null;
|
|
20
|
+
let _pollingTimer: ReturnType<typeof setInterval> | null = null;
|
|
21
|
+
|
|
22
|
+
function loadCacheFromDisk(): void {
|
|
23
|
+
try {
|
|
24
|
+
if (!fs.existsSync(CACHE_FILE)) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const raw = fs.readFileSync(CACHE_FILE, "utf-8");
|
|
28
|
+
const parsed: RemoteSettingsCache = JSON.parse(raw);
|
|
29
|
+
_cachedSettings = parsed;
|
|
30
|
+
logger.debug("remoteSettings: loaded cache from disk", {
|
|
31
|
+
checksum: parsed.checksum,
|
|
32
|
+
});
|
|
33
|
+
} catch (err) {
|
|
34
|
+
logger.debug("remoteSettings: failed to load cache from disk", { err });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function writeCacheToDisk(): void {
|
|
39
|
+
if (!_cachedSettings) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const dir = path.dirname(CACHE_FILE);
|
|
44
|
+
if (!fs.existsSync(dir)) {
|
|
45
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
46
|
+
}
|
|
47
|
+
fs.writeFileSync(CACHE_FILE, JSON.stringify(_cachedSettings, null, 2), {
|
|
48
|
+
mode: 0o600,
|
|
49
|
+
});
|
|
50
|
+
logger.debug("remoteSettings: wrote cache to disk", {
|
|
51
|
+
checksum: _cachedSettings.checksum,
|
|
52
|
+
});
|
|
53
|
+
} catch (err) {
|
|
54
|
+
logger.debug("remoteSettings: failed to write cache to disk", { err });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function removeCacheFromDisk(): void {
|
|
59
|
+
try {
|
|
60
|
+
if (fs.existsSync(CACHE_FILE)) {
|
|
61
|
+
fs.unlinkSync(CACHE_FILE);
|
|
62
|
+
}
|
|
63
|
+
} catch (err) {
|
|
64
|
+
logger.debug("remoteSettings: failed to remove cache file", { err });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function fetchRemoteSettings(): Promise<RemoteSettingsFetchResult> {
|
|
69
|
+
if (!authService.isSSOAuthenticated()) {
|
|
70
|
+
logger.debug("remoteSettings: skipping fetch — not SSO authenticated");
|
|
71
|
+
return { success: false, error: "Not SSO authenticated" };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const token = authService.getSSOToken();
|
|
75
|
+
const serverUrl = authService.getServerUrl();
|
|
76
|
+
if (!token || !serverUrl) {
|
|
77
|
+
return { success: false, error: "Missing SSO token or server URL" };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const headers: Record<string, string> = {
|
|
81
|
+
Authorization: `Bearer ${token}`,
|
|
82
|
+
};
|
|
83
|
+
if (_cachedSettings?.checksum) {
|
|
84
|
+
headers["If-None-Match"] = _cachedSettings.checksum;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const response = await fetch(`${serverUrl}/api/wave/settings`, {
|
|
89
|
+
method: "GET",
|
|
90
|
+
headers,
|
|
91
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (response.status === 304) {
|
|
95
|
+
logger.debug("remoteSettings: 304 unchanged", {
|
|
96
|
+
checksum: _cachedSettings?.checksum,
|
|
97
|
+
});
|
|
98
|
+
return {
|
|
99
|
+
success: true,
|
|
100
|
+
settings: _cachedSettings!.settings,
|
|
101
|
+
checksum: _cachedSettings!.checksum,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (response.status === 404) {
|
|
106
|
+
logger.debug("remoteSettings: 404 not configured — clearing stale cache");
|
|
107
|
+
_cachedSettings = null;
|
|
108
|
+
removeCacheFromDisk();
|
|
109
|
+
return { success: true, notConfigured: true, settings: null };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!response.ok) {
|
|
113
|
+
const body = await response.text().catch(() => "");
|
|
114
|
+
logger.debug("remoteSettings: fetch failed", {
|
|
115
|
+
status: response.status,
|
|
116
|
+
body: body.slice(0, 200),
|
|
117
|
+
});
|
|
118
|
+
return {
|
|
119
|
+
success: false,
|
|
120
|
+
error: `HTTP ${response.status}`,
|
|
121
|
+
settings: _cachedSettings?.settings ?? null,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const data = (await response.json()) as RemoteSettingsResponse;
|
|
126
|
+
_cachedSettings = {
|
|
127
|
+
uuid: data.uuid,
|
|
128
|
+
checksum: data.checksum,
|
|
129
|
+
settings: data.settings,
|
|
130
|
+
fetchedAt: new Date().toISOString(),
|
|
131
|
+
};
|
|
132
|
+
writeCacheToDisk();
|
|
133
|
+
logger.debug("remoteSettings: fetched new settings", {
|
|
134
|
+
checksum: data.checksum,
|
|
135
|
+
});
|
|
136
|
+
return {
|
|
137
|
+
success: true,
|
|
138
|
+
settings: data.settings,
|
|
139
|
+
checksum: data.checksum,
|
|
140
|
+
};
|
|
141
|
+
} catch (err) {
|
|
142
|
+
logger.debug("remoteSettings: network error, using cache", { err });
|
|
143
|
+
return {
|
|
144
|
+
success: false,
|
|
145
|
+
error: err instanceof Error ? err.message : String(err),
|
|
146
|
+
settings: _cachedSettings?.settings ?? null,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function startPolling(): void {
|
|
152
|
+
if (_pollingTimer) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
_pollingTimer = setInterval(async () => {
|
|
156
|
+
try {
|
|
157
|
+
await fetchRemoteSettings();
|
|
158
|
+
} catch (err) {
|
|
159
|
+
logger.debug("remoteSettings: polling fetch error", { err });
|
|
160
|
+
}
|
|
161
|
+
}, POLLING_INTERVAL_MS);
|
|
162
|
+
_pollingTimer.unref();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function initialize(): void {
|
|
166
|
+
loadCacheFromDisk();
|
|
167
|
+
// Fire-and-forget the initial fetch, then start background polling
|
|
168
|
+
fetchRemoteSettings()
|
|
169
|
+
.then(() => startPolling())
|
|
170
|
+
.catch((err) => {
|
|
171
|
+
logger.debug("remoteSettings: initial fetch failed", { err });
|
|
172
|
+
startPolling();
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function getRemoteSettingsSync(): WaveConfiguration | null {
|
|
177
|
+
return _cachedSettings?.settings ?? null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function refresh(): Promise<RemoteSettingsFetchResult> {
|
|
181
|
+
// Clear in-memory so we force a fresh fetch
|
|
182
|
+
_cachedSettings = null;
|
|
183
|
+
removeCacheFromDisk();
|
|
184
|
+
return fetchRemoteSettings();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function clear(): void {
|
|
188
|
+
_cachedSettings = null;
|
|
189
|
+
removeCacheFromDisk();
|
|
190
|
+
if (_pollingTimer) {
|
|
191
|
+
clearInterval(_pollingTimer);
|
|
192
|
+
_pollingTimer = null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function shutdown(): void {
|
|
197
|
+
if (_pollingTimer) {
|
|
198
|
+
clearInterval(_pollingTimer);
|
|
199
|
+
_pollingTimer = null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function dedupe(arr: string[]): string[] {
|
|
204
|
+
return [...new Set(arr)];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function mergeHooks(
|
|
208
|
+
local: Partial<Record<HookEvent, HookEventConfig[]>> | undefined,
|
|
209
|
+
remote: Partial<Record<HookEvent, HookEventConfig[]>> | undefined,
|
|
210
|
+
): Partial<Record<HookEvent, HookEventConfig[]>> | undefined {
|
|
211
|
+
if (!remote && !local) {
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
if (!remote) {
|
|
215
|
+
return local;
|
|
216
|
+
}
|
|
217
|
+
if (!local) {
|
|
218
|
+
return remote;
|
|
219
|
+
}
|
|
220
|
+
const merged: Partial<Record<HookEvent, HookEventConfig[]>> = { ...local };
|
|
221
|
+
for (const [event, remoteHooks] of Object.entries(remote)) {
|
|
222
|
+
const localHooks = merged[event as HookEvent] ?? [];
|
|
223
|
+
// Concatenate + dedupe by JSON serialization
|
|
224
|
+
const combined = [...localHooks, ...(remoteHooks ?? [])];
|
|
225
|
+
const seen = new Set<string>();
|
|
226
|
+
merged[event as HookEvent] = combined.filter((h) => {
|
|
227
|
+
const key = JSON.stringify(h);
|
|
228
|
+
if (seen.has(key)) {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
seen.add(key);
|
|
232
|
+
return true;
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
return merged;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function mergeRemoteSettings(
|
|
239
|
+
localMerged: WaveConfiguration,
|
|
240
|
+
remote: WaveConfiguration,
|
|
241
|
+
): WaveConfiguration {
|
|
242
|
+
const result: WaveConfiguration = { ...localMerged };
|
|
243
|
+
|
|
244
|
+
// env: merge by key, remote wins per-key
|
|
245
|
+
if (remote.env || localMerged.env) {
|
|
246
|
+
result.env = { ...localMerged.env, ...remote.env };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// permissions
|
|
250
|
+
if (remote.permissions || localMerged.permissions) {
|
|
251
|
+
const lp = localMerged.permissions ?? {};
|
|
252
|
+
const rp = remote.permissions ?? {};
|
|
253
|
+
result.permissions = {
|
|
254
|
+
// allow: concatenate + dedupe
|
|
255
|
+
allow:
|
|
256
|
+
lp.allow || rp.allow
|
|
257
|
+
? dedupe([...(lp.allow ?? []), ...(rp.allow ?? [])])
|
|
258
|
+
: undefined,
|
|
259
|
+
// deny: concatenate + dedupe
|
|
260
|
+
deny:
|
|
261
|
+
lp.deny || rp.deny
|
|
262
|
+
? dedupe([...(lp.deny ?? []), ...(rp.deny ?? [])])
|
|
263
|
+
: undefined,
|
|
264
|
+
// permissionMode: remote wins (scalar)
|
|
265
|
+
permissionMode: rp.permissionMode ?? lp.permissionMode,
|
|
266
|
+
// additionalDirectories: concatenate + dedupe
|
|
267
|
+
additionalDirectories:
|
|
268
|
+
lp.additionalDirectories || rp.additionalDirectories
|
|
269
|
+
? dedupe([
|
|
270
|
+
...(lp.additionalDirectories ?? []),
|
|
271
|
+
...(rp.additionalDirectories ?? []),
|
|
272
|
+
])
|
|
273
|
+
: undefined,
|
|
274
|
+
};
|
|
275
|
+
// Clean up undefined keys
|
|
276
|
+
if (!result.permissions.allow) delete result.permissions.allow;
|
|
277
|
+
if (!result.permissions.deny) delete result.permissions.deny;
|
|
278
|
+
if (!result.permissions.permissionMode)
|
|
279
|
+
delete result.permissions.permissionMode;
|
|
280
|
+
if (!result.permissions.additionalDirectories)
|
|
281
|
+
delete result.permissions.additionalDirectories;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// hooks: concatenate per-event
|
|
285
|
+
result.hooks = mergeHooks(localMerged.hooks, remote.hooks);
|
|
286
|
+
|
|
287
|
+
// Scalar / last-write-wins fields: remote wins
|
|
288
|
+
if (remote.language !== undefined) result.language = remote.language;
|
|
289
|
+
if (remote.model !== undefined) result.model = remote.model;
|
|
290
|
+
if (remote.autoMemoryEnabled !== undefined)
|
|
291
|
+
result.autoMemoryEnabled = remote.autoMemoryEnabled;
|
|
292
|
+
if (remote.autoMemoryFrequency !== undefined)
|
|
293
|
+
result.autoMemoryFrequency = remote.autoMemoryFrequency;
|
|
294
|
+
if (remote.models !== undefined) result.models = remote.models;
|
|
295
|
+
if (remote.marketplaces !== undefined)
|
|
296
|
+
result.marketplaces = remote.marketplaces;
|
|
297
|
+
if (remote.enabledPlugins !== undefined)
|
|
298
|
+
result.enabledPlugins = remote.enabledPlugins;
|
|
299
|
+
|
|
300
|
+
return result;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Singleton object for consumers that prefer a namespace-style import.
|
|
305
|
+
* Usage: import { remoteSettingsService } from "./remoteSettingsService.js"
|
|
306
|
+
*/
|
|
307
|
+
export const remoteSettingsService = {
|
|
308
|
+
initialize,
|
|
309
|
+
getRemoteSettingsSync,
|
|
310
|
+
refresh,
|
|
311
|
+
clear,
|
|
312
|
+
shutdown,
|
|
313
|
+
mergeRemoteSettings,
|
|
314
|
+
} as const;
|
|
@@ -18,7 +18,10 @@ import type {
|
|
|
18
18
|
LogRecordExporter,
|
|
19
19
|
ReadableLogRecord,
|
|
20
20
|
} from "@opentelemetry/sdk-logs";
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
AuthService,
|
|
23
|
+
getOrCreateAnonymousId,
|
|
24
|
+
} from "../services/authService.js";
|
|
22
25
|
|
|
23
26
|
// Lazy-loaded OTEL modules — only imported when telemetry is initialized
|
|
24
27
|
let sdkNode: typeof import("@opentelemetry/sdk-node") | undefined;
|
|
@@ -471,8 +474,11 @@ export function isInitialized(): boolean {
|
|
|
471
474
|
export { JsonlSpanExporter, JsonlLogExporter };
|
|
472
475
|
|
|
473
476
|
/**
|
|
474
|
-
* Get telemetry attributes
|
|
475
|
-
*
|
|
477
|
+
* Get telemetry attributes for the current session.
|
|
478
|
+
*
|
|
479
|
+
* Priority:
|
|
480
|
+
* 1. SSO authenticated → server-provided user.id + user.email
|
|
481
|
+
* 2. Not authenticated → persistent anonymous ID from ~/.wave/config.json
|
|
476
482
|
*/
|
|
477
483
|
export function getTelemetryAttributes(): Record<string, string> {
|
|
478
484
|
try {
|
|
@@ -487,5 +493,7 @@ export function getTelemetryAttributes(): Record<string, string> {
|
|
|
487
493
|
} catch {
|
|
488
494
|
// AuthService not available or not authenticated
|
|
489
495
|
}
|
|
490
|
-
|
|
496
|
+
|
|
497
|
+
// Fallback to anonymous ID
|
|
498
|
+
return { "user.id": getOrCreateAnonymousId() };
|
|
491
499
|
}
|
package/src/tools/buildTool.ts
CHANGED
|
@@ -24,8 +24,6 @@ export interface ToolDef {
|
|
|
24
24
|
params: Record<string, unknown>,
|
|
25
25
|
context: ToolContext,
|
|
26
26
|
) => string;
|
|
27
|
-
shouldDefer?: boolean;
|
|
28
|
-
alwaysLoad?: boolean;
|
|
29
27
|
additionalProperties?: boolean;
|
|
30
28
|
}
|
|
31
29
|
|
|
@@ -59,7 +57,5 @@ export function buildTool(def: ToolDef): ToolPlugin {
|
|
|
59
57
|
execute: def.execute,
|
|
60
58
|
prompt: promptFn,
|
|
61
59
|
formatCompactParams: def.formatCompactParams,
|
|
62
|
-
shouldDefer: def.shouldDefer ?? false,
|
|
63
|
-
alwaysLoad: def.alwaysLoad ?? false,
|
|
64
60
|
};
|
|
65
61
|
}
|