wave-agent-sdk 0.16.9 → 0.16.12
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 +5 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +18 -0
- package/dist/constants/toolLimits.d.ts +2 -0
- package/dist/constants/toolLimits.d.ts.map +1 -1
- package/dist/constants/toolLimits.js +2 -0
- package/dist/managers/aiManager.d.ts +5 -0
- package/dist/managers/aiManager.d.ts.map +1 -1
- package/dist/managers/aiManager.js +21 -0
- package/dist/managers/hookManager.d.ts +6 -3
- package/dist/managers/hookManager.d.ts.map +1 -1
- package/dist/managers/hookManager.js +36 -13
- package/dist/managers/mcpManager.d.ts +4 -28
- package/dist/managers/mcpManager.d.ts.map +1 -1
- package/dist/managers/mcpManager.js +10 -127
- package/dist/services/authService.d.ts +33 -1
- package/dist/services/authService.d.ts.map +1 -1
- package/dist/services/authService.js +212 -11
- package/dist/services/configurationService.d.ts +1 -0
- package/dist/services/configurationService.d.ts.map +1 -1
- package/dist/services/configurationService.js +48 -6
- package/dist/services/hook.d.ts +4 -0
- package/dist/services/hook.d.ts.map +1 -1
- package/dist/services/hook.js +10 -0
- package/dist/services/initializationService.d.ts.map +1 -1
- package/dist/services/initializationService.js +11 -0
- package/dist/services/interactionService.d.ts.map +1 -1
- package/dist/services/interactionService.js +0 -12
- package/dist/services/remoteSettingsService.d.ts +21 -0
- package/dist/services/remoteSettingsService.d.ts.map +1 -0
- package/dist/services/remoteSettingsService.js +280 -0
- package/dist/tools/bashTool.d.ts.map +1 -1
- package/dist/tools/bashTool.js +58 -32
- package/dist/tools/types.d.ts +4 -0
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/types/agent.d.ts +7 -0
- package/dist/types/agent.d.ts.map +1 -1
- package/dist/types/auth.d.ts +12 -0
- package/dist/types/auth.d.ts.map +1 -1
- package/dist/types/configuration.d.ts +20 -0
- package/dist/types/configuration.d.ts.map +1 -1
- package/dist/types/hooks.d.ts +5 -1
- package/dist/types/hooks.d.ts.map +1 -1
- package/dist/types/hooks.js +1 -0
- package/dist/types/mcp.d.ts +1 -1
- package/dist/types/mcp.d.ts.map +1 -1
- package/dist/utils/containerSetup.d.ts.map +1 -1
- package/dist/utils/containerSetup.js +9 -8
- package/dist/utils/gitUtils.d.ts +18 -1
- package/dist/utils/gitUtils.d.ts.map +1 -1
- package/dist/utils/gitUtils.js +120 -49
- package/dist/utils/mcpUtils.d.ts.map +1 -1
- package/dist/utils/mcpUtils.js +6 -1
- package/dist/utils/openaiClient.d.ts.map +1 -1
- package/dist/utils/openaiClient.js +4 -2
- package/dist/utils/toolResultStorage.d.ts +46 -0
- package/dist/utils/toolResultStorage.d.ts.map +1 -0
- package/dist/utils/toolResultStorage.js +90 -0
- package/dist/utils/worktreeUtils.d.ts.map +1 -1
- package/dist/utils/worktreeUtils.js +58 -0
- package/package.json +3 -3
- package/src/agent.ts +20 -0
- package/src/constants/toolLimits.ts +3 -0
- package/src/managers/aiManager.ts +37 -0
- package/src/managers/hookManager.ts +42 -17
- package/src/managers/mcpManager.ts +10 -178
- package/src/services/authService.ts +243 -16
- package/src/services/configurationService.ts +58 -6
- package/src/services/hook.ts +15 -0
- package/src/services/initializationService.ts +13 -0
- package/src/services/interactionService.ts +0 -18
- package/src/services/remoteSettingsService.ts +315 -0
- package/src/tools/bashTool.ts +70 -38
- package/src/tools/types.ts +4 -0
- package/src/types/agent.ts +7 -0
- package/src/types/auth.ts +10 -0
- package/src/types/configuration.ts +23 -0
- package/src/types/hooks.ts +7 -1
- package/src/types/mcp.ts +1 -1
- package/src/utils/containerSetup.ts +8 -8
- package/src/utils/gitUtils.ts +123 -48
- package/src/utils/mcpUtils.ts +12 -1
- package/src/utils/openaiClient.ts +5 -2
- package/src/utils/toolResultStorage.ts +117 -0
- package/src/utils/worktreeUtils.ts +63 -0
|
@@ -29,6 +29,7 @@ import { USER_MEMORY_FILE } from "./constants.js";
|
|
|
29
29
|
import { getGitMainRepoRoot } from "./gitUtils.js";
|
|
30
30
|
import { logger } from "./globalLogger.js";
|
|
31
31
|
import { authService } from "../services/authService.js";
|
|
32
|
+
import { remoteSettingsService } from "../services/remoteSettingsService.js";
|
|
32
33
|
export function setupAgentContainer(setupOptions) {
|
|
33
34
|
const { options, workdir, configurationService, systemPrompt, stream, onBackgroundTasksChange, onTasksChange, onPermissionModeChange, handlePlanModeTransition, setPermissionMode, addPermissionRule, addUsage, } = setupOptions;
|
|
34
35
|
const callbacks = options.callbacks || {};
|
|
@@ -89,24 +90,21 @@ export function setupAgentContainer(setupOptions) {
|
|
|
89
90
|
workdir,
|
|
90
91
|
});
|
|
91
92
|
container.register("BackgroundTaskManager", backgroundTaskManager);
|
|
92
|
-
const ssoToken = authService.getSSOToken();
|
|
93
|
-
const serverUrl = options.serverUrl || process.env.WAVE_SERVER_URL;
|
|
94
93
|
if (options.serverUrl) {
|
|
95
94
|
authService.setServerUrl(options.serverUrl);
|
|
96
95
|
}
|
|
97
96
|
const mcpManager = new McpManager(container, {
|
|
98
97
|
callbacks,
|
|
99
98
|
mcpServers: options.mcpServers,
|
|
100
|
-
serverUrl,
|
|
101
|
-
ssoToken,
|
|
102
99
|
});
|
|
103
100
|
container.register("McpManager", mcpManager);
|
|
104
|
-
// Wire up auth change callback to
|
|
101
|
+
// Wire up auth change callback to refresh/clear remote settings
|
|
105
102
|
authService.onAuthChange((event) => {
|
|
106
103
|
if (event === "login") {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
104
|
+
remoteSettingsService.refresh();
|
|
105
|
+
}
|
|
106
|
+
else if (event === "logout") {
|
|
107
|
+
remoteSettingsService.clear();
|
|
110
108
|
}
|
|
111
109
|
});
|
|
112
110
|
const lspManager = options.lspManager || new LspManager(container);
|
|
@@ -128,6 +126,9 @@ export function setupAgentContainer(setupOptions) {
|
|
|
128
126
|
const planManager = new PlanManager(container);
|
|
129
127
|
container.register("PlanManager", planManager);
|
|
130
128
|
const hookManager = new HookManager(container, workdir);
|
|
129
|
+
if (options.hooks) {
|
|
130
|
+
hookManager.loadConfiguration(options.hooks);
|
|
131
|
+
}
|
|
131
132
|
container.register("HookManager", hookManager);
|
|
132
133
|
const skillManager = new SkillManager(container, {
|
|
133
134
|
workdir,
|
package/dist/utils/gitUtils.d.ts
CHANGED
|
@@ -23,7 +23,24 @@ export declare function getGitCommonDir(cwd: string): string;
|
|
|
23
23
|
*/
|
|
24
24
|
export declare function getGitMainRepoRoot(cwd: string): string;
|
|
25
25
|
/**
|
|
26
|
-
*
|
|
26
|
+
* Resolve the git directory for a repository by walking up from `cwd`.
|
|
27
|
+
* Handles both normal repos (.git is a directory) and worktrees
|
|
28
|
+
* (.git is a file pointing to the main repo's worktree git dir).
|
|
29
|
+
* For worktrees, reads the `commondir` file to find the common git dir.
|
|
30
|
+
* @param cwd Working directory to start searching from
|
|
31
|
+
* @returns Absolute path to the git directory, or null if not found
|
|
32
|
+
*/
|
|
33
|
+
export declare function resolveGitDir(cwd: string): string | null;
|
|
34
|
+
/**
|
|
35
|
+
* Get the default remote branch (e.g., origin/main) using filesystem reads.
|
|
36
|
+
* No subprocess calls — matches Claude Code's approach.
|
|
37
|
+
*
|
|
38
|
+
* Priority:
|
|
39
|
+
* 1. Read refs/remotes/origin/HEAD symref → extract branch name (verify it exists)
|
|
40
|
+
* 2. Check if refs/remotes/origin/main ref exists
|
|
41
|
+
* 3. Check if refs/remotes/origin/master ref exists
|
|
42
|
+
* 4. Hardcoded "main" fallback
|
|
43
|
+
*
|
|
27
44
|
* @param cwd Working directory
|
|
28
45
|
* @returns Default remote branch name
|
|
29
46
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gitUtils.d.ts","sourceRoot":"","sources":["../../src/utils/gitUtils.ts"],"names":[],"mappings":"AAIA;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAevD;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAUlD;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAWnD;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAetD;AAED
|
|
1
|
+
{"version":3,"file":"gitUtils.d.ts","sourceRoot":"","sources":["../../src/utils/gitUtils.ts"],"names":[],"mappings":"AAIA;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAevD;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAUlD;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAWnD;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAetD;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkCxD;AAwDD;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA4B1D;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAW1D;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAYvE"}
|
package/dist/utils/gitUtils.js
CHANGED
|
@@ -81,69 +81,140 @@ export function getGitMainRepoRoot(cwd) {
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
84
|
+
* Resolve the git directory for a repository by walking up from `cwd`.
|
|
85
|
+
* Handles both normal repos (.git is a directory) and worktrees
|
|
86
|
+
* (.git is a file pointing to the main repo's worktree git dir).
|
|
87
|
+
* For worktrees, reads the `commondir` file to find the common git dir.
|
|
88
|
+
* @param cwd Working directory to start searching from
|
|
89
|
+
* @returns Absolute path to the git directory, or null if not found
|
|
87
90
|
*/
|
|
88
|
-
export function
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
91
|
+
export function resolveGitDir(cwd) {
|
|
92
|
+
let currentPath = path.resolve(cwd);
|
|
93
|
+
while (currentPath !== path.dirname(currentPath)) {
|
|
94
|
+
const gitPath = path.join(currentPath, ".git");
|
|
95
|
+
if (fsSync.existsSync(gitPath)) {
|
|
96
|
+
const stat = fsSync.statSync(gitPath);
|
|
97
|
+
if (stat.isDirectory()) {
|
|
98
|
+
// Normal repo: .git is a directory
|
|
99
|
+
return gitPath;
|
|
100
|
+
}
|
|
101
|
+
// Worktree: .git is a file containing "gitdir: <path>"
|
|
102
|
+
try {
|
|
103
|
+
const content = fsSync.readFileSync(gitPath, "utf8").trim();
|
|
104
|
+
const prefix = "gitdir: ";
|
|
105
|
+
if (content.startsWith(prefix)) {
|
|
106
|
+
const worktreeGitDir = content.substring(prefix.length);
|
|
107
|
+
// Read commondir to find the common git dir
|
|
108
|
+
const commondirPath = path.join(worktreeGitDir, "commondir");
|
|
109
|
+
if (fsSync.existsSync(commondirPath)) {
|
|
110
|
+
const commondirRel = fsSync
|
|
111
|
+
.readFileSync(commondirPath, "utf8")
|
|
112
|
+
.trim();
|
|
113
|
+
return path.resolve(worktreeGitDir, commondirRel);
|
|
114
|
+
}
|
|
115
|
+
// Fallback: resolve ../.. from the worktree git dir
|
|
116
|
+
return path.resolve(worktreeGitDir, "..", "..");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
currentPath = path.dirname(currentPath);
|
|
100
124
|
}
|
|
101
|
-
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Read a symbolic ref file in the git directory.
|
|
129
|
+
* If the file content starts with "ref: ", returns the target ref path.
|
|
130
|
+
* Symbolic refs are never packed in packed-refs, so only loose refs are checked.
|
|
131
|
+
* @param gitDir Absolute path to the git directory
|
|
132
|
+
* @param refPath Relative path to the ref file (e.g., "refs/remotes/origin/HEAD")
|
|
133
|
+
* @returns The symbolic ref target, or null if not a symbolic ref or on error
|
|
134
|
+
*/
|
|
135
|
+
function readSymref(gitDir, refPath) {
|
|
102
136
|
try {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
137
|
+
const fullPath = path.join(gitDir, refPath);
|
|
138
|
+
const content = fsSync.readFileSync(fullPath, "utf8").trim();
|
|
139
|
+
if (content.startsWith("ref: ")) {
|
|
140
|
+
return content.substring("ref: ".length);
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
108
143
|
}
|
|
109
144
|
catch {
|
|
110
|
-
|
|
145
|
+
return null;
|
|
111
146
|
}
|
|
112
|
-
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Check if a git ref exists, checking both loose refs and packed-refs.
|
|
150
|
+
* @param gitDir Absolute path to the git directory
|
|
151
|
+
* @param refPath Relative path to the ref (e.g., "refs/remotes/origin/main")
|
|
152
|
+
* @returns True if the ref exists, false otherwise
|
|
153
|
+
*/
|
|
154
|
+
function refExists(gitDir, refPath) {
|
|
155
|
+
// 1. Check loose ref
|
|
156
|
+
const loosePath = path.join(gitDir, refPath);
|
|
157
|
+
if (fsSync.existsSync(loosePath)) {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
// 2. Check packed-refs
|
|
113
161
|
try {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
162
|
+
const packedRefsPath = path.join(gitDir, "packed-refs");
|
|
163
|
+
const content = fsSync.readFileSync(packedRefsPath, "utf8");
|
|
164
|
+
for (const line of content.split("\n")) {
|
|
165
|
+
// Skip comments and peeled lines
|
|
166
|
+
if (line.startsWith("#") || line.startsWith("^")) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
// Format: <sha> <refPath>
|
|
170
|
+
const parts = line.split(" ");
|
|
171
|
+
if (parts.length >= 2 && parts[1] === refPath) {
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
119
175
|
}
|
|
120
176
|
catch {
|
|
121
|
-
//
|
|
177
|
+
// packed-refs doesn't exist or can't be read
|
|
122
178
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Get the default remote branch (e.g., origin/main) using filesystem reads.
|
|
183
|
+
* No subprocess calls — matches Claude Code's approach.
|
|
184
|
+
*
|
|
185
|
+
* Priority:
|
|
186
|
+
* 1. Read refs/remotes/origin/HEAD symref → extract branch name (verify it exists)
|
|
187
|
+
* 2. Check if refs/remotes/origin/main ref exists
|
|
188
|
+
* 3. Check if refs/remotes/origin/master ref exists
|
|
189
|
+
* 4. Hardcoded "main" fallback
|
|
190
|
+
*
|
|
191
|
+
* @param cwd Working directory
|
|
192
|
+
* @returns Default remote branch name
|
|
193
|
+
*/
|
|
194
|
+
export function getDefaultRemoteBranch(cwd) {
|
|
195
|
+
const gitDir = resolveGitDir(cwd);
|
|
196
|
+
if (!gitDir) {
|
|
197
|
+
return "main";
|
|
130
198
|
}
|
|
131
|
-
|
|
132
|
-
|
|
199
|
+
// 1. Try reading origin/HEAD symref
|
|
200
|
+
const symref = readSymref(gitDir, "refs/remotes/origin/HEAD");
|
|
201
|
+
if (symref) {
|
|
202
|
+
const branch = symref.replace("refs/remotes/", "");
|
|
203
|
+
// Verify the resolved branch actually exists (origin/HEAD can be stale)
|
|
204
|
+
if (refExists(gitDir, symref)) {
|
|
205
|
+
return branch;
|
|
206
|
+
}
|
|
133
207
|
}
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
return
|
|
137
|
-
cwd,
|
|
138
|
-
encoding: "utf8",
|
|
139
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
140
|
-
}).trim();
|
|
208
|
+
// 2. Check if origin/main exists
|
|
209
|
+
if (refExists(gitDir, "refs/remotes/origin/main")) {
|
|
210
|
+
return "origin/main";
|
|
141
211
|
}
|
|
142
|
-
|
|
143
|
-
|
|
212
|
+
// 3. Check if origin/master exists
|
|
213
|
+
if (refExists(gitDir, "refs/remotes/origin/master")) {
|
|
214
|
+
return "origin/master";
|
|
144
215
|
}
|
|
145
|
-
//
|
|
146
|
-
return "
|
|
216
|
+
// 4. Hardcoded fallback
|
|
217
|
+
return "main";
|
|
147
218
|
}
|
|
148
219
|
/**
|
|
149
220
|
* Check if there are uncommitted changes in the working directory
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcpUtils.d.ts","sourceRoot":"","sources":["../../src/utils/mcpUtils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AACjE,OAAO,KAAK,EAAE,UAAU,EAAc,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC7E,OAAO,KAAK,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"mcpUtils.d.ts","sourceRoot":"","sources":["../../src/utils/mcpUtils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AACjE,OAAO,KAAK,EAAE,UAAU,EAAc,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC7E,OAAO,KAAK,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAgDlE;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,OAAO,EAChB,UAAU,EAAE,MAAM,GACjB,0BAA0B,CAgB5B;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,OAAO,EAChB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,CACX,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,CAAC,EAAE,WAAW,KAClB,OAAO,CAAC;IACX,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACtD,CAAC,GACD,UAAU,CAiCZ;AAED;;GAEG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,eAAe,EAAE,GACzB,eAAe,GAAG,SAAS,CAK7B"}
|
package/dist/utils/mcpUtils.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { processToolResult } from "./toolResultStorage.js";
|
|
2
|
+
import { DEFAULT_MAX_RESULT_SIZE_CHARS } from "../constants/toolLimits.js";
|
|
1
3
|
/**
|
|
2
4
|
* Recursively clean schema to remove unsupported fields
|
|
3
5
|
*/
|
|
@@ -60,9 +62,12 @@ export function createMcpToolPlugin(mcpTool, serverName, executeTool) {
|
|
|
60
62
|
async execute(args, context) {
|
|
61
63
|
try {
|
|
62
64
|
const result = await executeTool(prefixedName, args, context);
|
|
65
|
+
// Process content for size limits — only text content, not images
|
|
66
|
+
const processedContent = processToolResult(result.content || `Executed ${mcpTool.name}`, DEFAULT_MAX_RESULT_SIZE_CHARS, `mcp_${serverName}_${mcpTool.name}`);
|
|
63
67
|
return {
|
|
64
68
|
success: true,
|
|
65
|
-
content:
|
|
69
|
+
content: processedContent,
|
|
70
|
+
images: result.images,
|
|
66
71
|
};
|
|
67
72
|
}
|
|
68
73
|
catch (error) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openaiClient.d.ts","sourceRoot":"","sources":["../../src/utils/openaiClient.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sCAAsC,EACtC,mCAAmC,EACnC,mBAAmB,EACnB,cAAc,EACf,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAGnD,KAAK,YAAY,GACb,sCAAsC,GACtC,mCAAmC,CAAC;AAExC,UAAU,WAAW,CAAC,CAAC;IACrB,IAAI,EAAE,CAAC,CAAC;IACR,QAAQ,EAAE,QAAQ,CAAC;CACpB;AAED,UAAU,UAAU,CAAC,CAAC,CAAE,SAAQ,OAAO,CAAC,CAAC,CAAC;IACxC,YAAY,IAAI,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;CACzC;AAED,qBAAa,YAAY;IACX,OAAO,CAAC,MAAM;gBAAN,MAAM,EAAE,aAAa;IAEzC,IAAI,IAAI;;qBAGO,CAAC,SAAS,YAAY,UACrB,CAAC,YACC;gBAAE,MAAM,CAAC,EAAE,WAAW,CAAA;aAAE,KACjC,UAAU,CACX,CAAC,SAAS,mCAAmC,GACzC,aAAa,CAAC,mBAAmB,CAAC,GAClC,cAAc,CACnB;;MA2BN;YAEa,OAAO;
|
|
1
|
+
{"version":3,"file":"openaiClient.d.ts","sourceRoot":"","sources":["../../src/utils/openaiClient.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sCAAsC,EACtC,mCAAmC,EACnC,mBAAmB,EACnB,cAAc,EACf,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAGnD,KAAK,YAAY,GACb,sCAAsC,GACtC,mCAAmC,CAAC;AAExC,UAAU,WAAW,CAAC,CAAC;IACrB,IAAI,EAAE,CAAC,CAAC;IACR,QAAQ,EAAE,QAAQ,CAAC;CACpB;AAED,UAAU,UAAU,CAAC,CAAC,CAAE,SAAQ,OAAO,CAAC,CAAC,CAAC;IACxC,YAAY,IAAI,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;CACzC;AAED,qBAAa,YAAY;IACX,OAAO,CAAC,MAAM;gBAAN,MAAM,EAAE,aAAa;IAEzC,IAAI,IAAI;;qBAGO,CAAC,SAAS,YAAY,UACrB,CAAC,YACC;gBAAE,MAAM,CAAC,EAAE,WAAW,CAAA;aAAE,KACjC,UAAU,CACX,CAAC,SAAS,mCAAmC,GACzC,aAAa,CAAC,mBAAmB,CAAC,GAClC,cAAc,CACnB;;MA2BN;YAEa,OAAO;YAwIN,oBAAoB;CAqCpC"}
|
|
@@ -107,8 +107,10 @@ export class OpenAIClient {
|
|
|
107
107
|
: response.statusText);
|
|
108
108
|
error.status = response.status;
|
|
109
109
|
error.body = errorBody;
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
const retryableStatus = response.status === 429 ||
|
|
111
|
+
(response.status >= 500 && response.status !== 501);
|
|
112
|
+
if (retryableStatus && attempt < maxRetries) {
|
|
113
|
+
logger.warn("OpenAI API error, retrying...", {
|
|
112
114
|
attempt: attempt + 1,
|
|
113
115
|
status: response.status,
|
|
114
116
|
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared tool result persistence and truncation logic.
|
|
3
|
+
*
|
|
4
|
+
* When a tool result exceeds a size threshold, the full content is saved to a
|
|
5
|
+
* file in /tmp/wave-tool-results/ and the model receives a <persisted-output>
|
|
6
|
+
* preview with the file path so it can use the Read tool to access the full output.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Get (and create if needed) the tool-results directory.
|
|
10
|
+
* Uses /tmp/wave-tool-results/ for simplicity and automatic OS cleanup.
|
|
11
|
+
*/
|
|
12
|
+
export declare function getToolResultsDir(): string;
|
|
13
|
+
/**
|
|
14
|
+
* Persist full tool output to a file in the tool-results directory.
|
|
15
|
+
* Returns the file path on success, or undefined on failure.
|
|
16
|
+
*/
|
|
17
|
+
export declare function persistToolResult(content: string, prefix?: string): string | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Generate a preview from content: first `previewSize` characters with ellipsis.
|
|
20
|
+
*/
|
|
21
|
+
export declare function generatePreview(content: string, previewSize?: number): string;
|
|
22
|
+
/**
|
|
23
|
+
* Build the <persisted-output> wrapper message that the model sees.
|
|
24
|
+
*
|
|
25
|
+
* Example output:
|
|
26
|
+
* <persisted-output>
|
|
27
|
+
* Output too large (150,000 characters). Full output saved to: /tmp/wave-tool-results/mcp_server_tool_12345.txt
|
|
28
|
+
* Preview (first 2,048 characters):
|
|
29
|
+
* {preview content}
|
|
30
|
+
* ...
|
|
31
|
+
* </persisted-output>
|
|
32
|
+
*/
|
|
33
|
+
export declare function buildPersistedOutputMessage(totalChars: number, filePath: string, preview: string): string;
|
|
34
|
+
/**
|
|
35
|
+
* Process tool result: if content exceeds maxChars, persist to file and return
|
|
36
|
+
* truncated content with <persisted-output> wrapper. Otherwise return unchanged.
|
|
37
|
+
*
|
|
38
|
+
* This is the main entry point for both MCP and bash tools.
|
|
39
|
+
*
|
|
40
|
+
* @param content - The tool result content
|
|
41
|
+
* @param maxChars - Maximum characters before persistence kicks in (defaults to DEFAULT_MAX_RESULT_SIZE_CHARS)
|
|
42
|
+
* @param prefix - File name prefix for the persisted file (e.g. "bash", "mcp")
|
|
43
|
+
* @returns The content to send to the model (either original or persisted-output wrapper)
|
|
44
|
+
*/
|
|
45
|
+
export declare function processToolResult(content: string, maxChars?: number, prefix?: string): string;
|
|
46
|
+
//# sourceMappingURL=toolResultStorage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"toolResultStorage.d.ts","sourceRoot":"","sources":["../../src/utils/toolResultStorage.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAaH;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAG1C;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,MAAM,GAAE,MAAe,GACtB,MAAM,GAAG,SAAS,CAWpB;AAED;;GAEG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,EACf,WAAW,GAAE,MAA2B,GACvC,MAAM,CAGR;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,2BAA2B,CACzC,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GACd,MAAM,CAQR;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,QAAQ,GAAE,MAAsC,EAChD,MAAM,GAAE,MAAe,GACtB,MAAM,CAiBR"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared tool result persistence and truncation logic.
|
|
3
|
+
*
|
|
4
|
+
* When a tool result exceeds a size threshold, the full content is saved to a
|
|
5
|
+
* file in /tmp/wave-tool-results/ and the model receives a <persisted-output>
|
|
6
|
+
* preview with the file path so it can use the Read tool to access the full output.
|
|
7
|
+
*/
|
|
8
|
+
import * as fs from "fs";
|
|
9
|
+
import * as path from "path";
|
|
10
|
+
import * as os from "os";
|
|
11
|
+
import { DEFAULT_MAX_RESULT_SIZE_CHARS, PREVIEW_SIZE_BYTES, } from "../constants/toolLimits.js";
|
|
12
|
+
import { logger } from "./globalLogger.js";
|
|
13
|
+
const TOOL_RESULTS_DIR = path.join(os.tmpdir(), "wave-tool-results");
|
|
14
|
+
/**
|
|
15
|
+
* Get (and create if needed) the tool-results directory.
|
|
16
|
+
* Uses /tmp/wave-tool-results/ for simplicity and automatic OS cleanup.
|
|
17
|
+
*/
|
|
18
|
+
export function getToolResultsDir() {
|
|
19
|
+
fs.mkdirSync(TOOL_RESULTS_DIR, { recursive: true });
|
|
20
|
+
return TOOL_RESULTS_DIR;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Persist full tool output to a file in the tool-results directory.
|
|
24
|
+
* Returns the file path on success, or undefined on failure.
|
|
25
|
+
*/
|
|
26
|
+
export function persistToolResult(content, prefix = "tool") {
|
|
27
|
+
try {
|
|
28
|
+
const dir = getToolResultsDir();
|
|
29
|
+
const id = `${prefix}_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
30
|
+
const filePath = path.join(dir, `${id}.txt`);
|
|
31
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
32
|
+
return filePath;
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
logger?.error("Failed to persist tool result:", error);
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Generate a preview from content: first `previewSize` characters with ellipsis.
|
|
41
|
+
*/
|
|
42
|
+
export function generatePreview(content, previewSize = PREVIEW_SIZE_BYTES) {
|
|
43
|
+
if (content.length <= previewSize)
|
|
44
|
+
return content;
|
|
45
|
+
return content.substring(0, previewSize) + "\n...";
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Build the <persisted-output> wrapper message that the model sees.
|
|
49
|
+
*
|
|
50
|
+
* Example output:
|
|
51
|
+
* <persisted-output>
|
|
52
|
+
* Output too large (150,000 characters). Full output saved to: /tmp/wave-tool-results/mcp_server_tool_12345.txt
|
|
53
|
+
* Preview (first 2,048 characters):
|
|
54
|
+
* {preview content}
|
|
55
|
+
* ...
|
|
56
|
+
* </persisted-output>
|
|
57
|
+
*/
|
|
58
|
+
export function buildPersistedOutputMessage(totalChars, filePath, preview) {
|
|
59
|
+
return [
|
|
60
|
+
"<persisted-output>",
|
|
61
|
+
`Output too large (${totalChars.toLocaleString()} characters). Full output saved to: ${filePath}`,
|
|
62
|
+
`Preview (first ${PREVIEW_SIZE_BYTES.toLocaleString()} characters):`,
|
|
63
|
+
preview,
|
|
64
|
+
"</persisted-output>",
|
|
65
|
+
].join("\n");
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Process tool result: if content exceeds maxChars, persist to file and return
|
|
69
|
+
* truncated content with <persisted-output> wrapper. Otherwise return unchanged.
|
|
70
|
+
*
|
|
71
|
+
* This is the main entry point for both MCP and bash tools.
|
|
72
|
+
*
|
|
73
|
+
* @param content - The tool result content
|
|
74
|
+
* @param maxChars - Maximum characters before persistence kicks in (defaults to DEFAULT_MAX_RESULT_SIZE_CHARS)
|
|
75
|
+
* @param prefix - File name prefix for the persisted file (e.g. "bash", "mcp")
|
|
76
|
+
* @returns The content to send to the model (either original or persisted-output wrapper)
|
|
77
|
+
*/
|
|
78
|
+
export function processToolResult(content, maxChars = DEFAULT_MAX_RESULT_SIZE_CHARS, prefix = "tool") {
|
|
79
|
+
if (content.length <= maxChars) {
|
|
80
|
+
return content;
|
|
81
|
+
}
|
|
82
|
+
const filePath = persistToolResult(content, prefix);
|
|
83
|
+
if (filePath) {
|
|
84
|
+
const preview = generatePreview(content);
|
|
85
|
+
return buildPersistedOutputMessage(content.length, filePath, preview);
|
|
86
|
+
}
|
|
87
|
+
// Fallback: truncation only (persistence failed)
|
|
88
|
+
return (content.substring(0, maxChars) +
|
|
89
|
+
"\n\n... (output truncated, failed to persist full output)");
|
|
90
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worktreeUtils.d.ts","sourceRoot":"","sources":["../../src/utils/worktreeUtils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAQH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,mFAAmF;IACnF,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAmBvD;AAED;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CA6B7C;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAKjD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,YAAY,
|
|
1
|
+
{"version":3,"file":"worktreeUtils.d.ts","sourceRoot":"","sources":["../../src/utils/worktreeUtils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAQH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,mFAAmF;IACnF,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAmBvD;AAED;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CA6B7C;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAKjD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,YAAY,CAuJtE;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CA+DvD;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,YAAY,EAAE,MAAM,EACpB,kBAAkB,EAAE,MAAM,GAAG,SAAS,GACrC;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA8BlD"}
|
|
@@ -142,6 +142,64 @@ export function createWorktree(name, cwd) {
|
|
|
142
142
|
throw new Error(`Failed to add worktree: ${innerError.message}`);
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
|
+
if (stderr.includes("not a valid object name") ||
|
|
146
|
+
stderr.includes("unknown revision")) {
|
|
147
|
+
// Base branch not fetched yet — try fetching then retrying
|
|
148
|
+
const branchNameOnly = baseBranch.split("/").pop();
|
|
149
|
+
try {
|
|
150
|
+
execSync(`git fetch origin ${branchNameOnly}`, {
|
|
151
|
+
cwd: repoRoot,
|
|
152
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
153
|
+
env: {
|
|
154
|
+
...process.env,
|
|
155
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
156
|
+
GIT_ASKPASS: "",
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
execSync(`git worktree add -b ${branchName} "${worktreePath}" ${baseBranch}`, {
|
|
160
|
+
cwd: repoRoot,
|
|
161
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
162
|
+
env: {
|
|
163
|
+
...process.env,
|
|
164
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
165
|
+
GIT_ASKPASS: "",
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
return {
|
|
169
|
+
name,
|
|
170
|
+
path: worktreePath,
|
|
171
|
+
branch: branchName,
|
|
172
|
+
repoRoot,
|
|
173
|
+
isNew: true,
|
|
174
|
+
originalHeadCommit,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
// Fetch or retry failed — fall back to HEAD
|
|
179
|
+
try {
|
|
180
|
+
execSync(`git worktree add -b ${branchName} "${worktreePath}" HEAD`, {
|
|
181
|
+
cwd: repoRoot,
|
|
182
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
183
|
+
env: {
|
|
184
|
+
...process.env,
|
|
185
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
186
|
+
GIT_ASKPASS: "",
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
return {
|
|
190
|
+
name,
|
|
191
|
+
path: worktreePath,
|
|
192
|
+
branch: branchName,
|
|
193
|
+
repoRoot,
|
|
194
|
+
isNew: true,
|
|
195
|
+
originalHeadCommit,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
throw new Error(`Failed to create worktree: ${error.message}\n${stderr}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
145
203
|
throw new Error(`Failed to create worktree: ${error.message}\n${stderr}`);
|
|
146
204
|
}
|
|
147
205
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wave-agent-sdk",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.12",
|
|
4
4
|
"description": "SDK for building AI-powered development tools and agents",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -48,11 +48,11 @@
|
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@types/turndown": "^5.0.6",
|
|
51
|
-
"@vitest/coverage-v8": "^4.
|
|
51
|
+
"@vitest/coverage-v8": "^4.1.7",
|
|
52
52
|
"rimraf": "^6.1.2",
|
|
53
53
|
"tsc-alias": "^1.8.16",
|
|
54
54
|
"tsx": "^4.20.4",
|
|
55
|
-
"vitest": "^4.
|
|
55
|
+
"vitest": "^4.1.7"
|
|
56
56
|
},
|
|
57
57
|
"engines": {
|
|
58
58
|
"node": ">=22.0.0"
|
package/src/agent.ts
CHANGED
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
shutdownTelemetry,
|
|
52
52
|
} from "./telemetry/instrumentation.js";
|
|
53
53
|
import { logOTelEvent } from "./telemetry/events.js";
|
|
54
|
+
import { remoteSettingsService } from "./services/remoteSettingsService.js";
|
|
54
55
|
|
|
55
56
|
export class Agent {
|
|
56
57
|
private messageManager: MessageManager;
|
|
@@ -220,6 +221,13 @@ export class Agent {
|
|
|
220
221
|
}
|
|
221
222
|
};
|
|
222
223
|
|
|
224
|
+
// Wire up CWD change callback from AIManager to sync Agent's workdir
|
|
225
|
+
this.aiManager.setOnCwdChange((newCwd) => {
|
|
226
|
+
this.workdir = newCwd;
|
|
227
|
+
this.container.register("Workdir", newCwd);
|
|
228
|
+
this.options.callbacks?.onWorkdirChange?.(newCwd);
|
|
229
|
+
});
|
|
230
|
+
|
|
223
231
|
// Wire up message queue to process when agent becomes idle
|
|
224
232
|
this.messageQueue.onMessageEnqueued = () => {
|
|
225
233
|
// If the AI is NOT loading and command is not running, trigger dequeue
|
|
@@ -282,6 +290,16 @@ export class Agent {
|
|
|
282
290
|
return this.workdir;
|
|
283
291
|
}
|
|
284
292
|
|
|
293
|
+
/**
|
|
294
|
+
* Set the working directory
|
|
295
|
+
* @param newCwd - The new working directory
|
|
296
|
+
*/
|
|
297
|
+
public setWorkdir(newCwd: string): void {
|
|
298
|
+
this.workdir = newCwd;
|
|
299
|
+
this.container.register("Workdir", newCwd);
|
|
300
|
+
this.options.callbacks?.onWorkdirChange?.(newCwd);
|
|
301
|
+
}
|
|
302
|
+
|
|
285
303
|
/** Get project memory content */
|
|
286
304
|
public get projectMemory(): string {
|
|
287
305
|
const memoryService =
|
|
@@ -720,6 +738,8 @@ export class Agent {
|
|
|
720
738
|
error,
|
|
721
739
|
);
|
|
722
740
|
}
|
|
741
|
+
// Cleanup remote settings polling
|
|
742
|
+
remoteSettingsService.shutdown();
|
|
723
743
|
// Cleanup memory store
|
|
724
744
|
}
|
|
725
745
|
|
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
/** System-wide default max result size in characters. */
|
|
6
6
|
export const DEFAULT_MAX_RESULT_SIZE_CHARS = 50_000;
|
|
7
7
|
|
|
8
|
+
/** Per-command cap for bash tool output before persistence. */
|
|
9
|
+
export const BASH_MAX_OUTPUT_CHARS = 30_000;
|
|
10
|
+
|
|
8
11
|
/** Per-command cap for skill bash substitution (inline/block). */
|
|
9
12
|
export const SKILL_BASH_MAX_OUTPUT_CHARS = 30_000;
|
|
10
13
|
|