@oyasmi/pipiclaw 0.5.9 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/channel-runner.d.ts +5 -0
- package/dist/agent/channel-runner.js +59 -15
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/memory/consolidation.js +11 -2
- package/dist/memory/session.js +2 -2
- package/dist/memory/sidecar-worker.d.ts +1 -0
- package/dist/memory/sidecar-worker.js +56 -1
- package/dist/paths.d.ts +1 -0
- package/dist/paths.js +1 -0
- package/dist/runtime/bootstrap.d.ts +1 -0
- package/dist/runtime/bootstrap.js +50 -11
- package/dist/runtime/delivery.js +56 -5
- package/dist/runtime/dingtalk.d.ts +2 -0
- package/dist/runtime/dingtalk.js +14 -4
- package/dist/runtime/events.d.ts +3 -0
- package/dist/runtime/events.js +30 -5
- package/dist/security/command-guard.js +4 -0
- package/dist/security/config.d.ts +6 -0
- package/dist/security/config.js +38 -6
- package/dist/security/path-guard.js +4 -0
- package/dist/security/platform.d.ts +1 -0
- package/dist/security/platform.js +3 -0
- package/dist/settings.d.ts +4 -1
- package/dist/settings.js +31 -6
- package/dist/shared/config-diagnostics.d.ts +7 -0
- package/dist/shared/config-diagnostics.js +3 -0
- package/dist/tools/config.d.ts +7 -0
- package/dist/tools/config.js +63 -7
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/index.js +2 -2
- package/dist/web/client.d.ts +1 -0
- package/dist/web/client.js +30 -18
- package/dist/web/config.d.ts +1 -0
- package/dist/web/config.js +1 -0
- package/dist/web/fetch.d.ts +1 -0
- package/dist/web/fetch.js +7 -5
- package/dist/web/search-providers.js +6 -3
- package/package.json +1 -1
package/dist/runtime/dingtalk.js
CHANGED
|
@@ -78,6 +78,7 @@ export class DingTalkBot {
|
|
|
78
78
|
this.isReconnecting = false;
|
|
79
79
|
this.isStopped = false;
|
|
80
80
|
this.reconnectAttempts = 0;
|
|
81
|
+
this.hasReportedReady = false;
|
|
81
82
|
// Deduplication cache (Set for O(1) lookup, order array for FIFO eviction)
|
|
82
83
|
this.processedIds = new Set();
|
|
83
84
|
this.processedIdsOrder = [];
|
|
@@ -180,8 +181,10 @@ export class DingTalkBot {
|
|
|
180
181
|
this.client.registerCallbackListener(TOPIC_ROBOT, (msg) => {
|
|
181
182
|
return this.handleRawMessage(msg);
|
|
182
183
|
});
|
|
183
|
-
|
|
184
|
-
|
|
184
|
+
const connected = await this.doReconnect(true); // Initial connection
|
|
185
|
+
if (!connected) {
|
|
186
|
+
log.logWarning("DingTalk: initial stream connection not ready yet; retrying in background");
|
|
187
|
+
}
|
|
185
188
|
}
|
|
186
189
|
handleRawMessage(msg) {
|
|
187
190
|
// 1. Immediate ACK
|
|
@@ -213,16 +216,17 @@ export class DingTalkBot {
|
|
|
213
216
|
}
|
|
214
217
|
async doReconnect(immediate = false) {
|
|
215
218
|
if (this.isReconnecting || this.isStopped || !this.client)
|
|
216
|
-
return;
|
|
219
|
+
return false;
|
|
217
220
|
this.isReconnecting = true;
|
|
218
221
|
let connectionFailed = false;
|
|
222
|
+
let connected = false;
|
|
219
223
|
if (!immediate && this.reconnectAttempts > 0) {
|
|
220
224
|
const delay = Math.min(1000 * 2 ** this.reconnectAttempts + Math.random() * 1000, 30000);
|
|
221
225
|
log.logInfo(`DingTalk: waiting ${Math.round(delay / 1000)}s before reconnecting...`);
|
|
222
226
|
await this.waitForDelay(delay);
|
|
223
227
|
if (this.isStopped || !this.client) {
|
|
224
228
|
this.isReconnecting = false;
|
|
225
|
-
return;
|
|
229
|
+
return false;
|
|
226
230
|
}
|
|
227
231
|
}
|
|
228
232
|
try {
|
|
@@ -234,6 +238,11 @@ export class DingTalkBot {
|
|
|
234
238
|
this.lastSocketAvailableTime = Date.now();
|
|
235
239
|
this.reconnectAttempts = 0; // Success, reset backoff
|
|
236
240
|
log.logInfo("DingTalk: connected to stream.");
|
|
241
|
+
if (!this.hasReportedReady) {
|
|
242
|
+
log.logConnected();
|
|
243
|
+
this.hasReportedReady = true;
|
|
244
|
+
}
|
|
245
|
+
connected = true;
|
|
237
246
|
// Setup keep alive
|
|
238
247
|
this.clearKeepAliveTimer();
|
|
239
248
|
this.keepAliveTimer = this.setTrackedInterval(() => {
|
|
@@ -291,6 +300,7 @@ export class DingTalkBot {
|
|
|
291
300
|
if (connectionFailed && !this.isStopped) {
|
|
292
301
|
this.scheduleReconnect(0, false);
|
|
293
302
|
}
|
|
303
|
+
return connected;
|
|
294
304
|
}
|
|
295
305
|
async stop() {
|
|
296
306
|
log.logInfo("DingTalk: stopping bot");
|
package/dist/runtime/events.d.ts
CHANGED
package/dist/runtime/events.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Cron } from "croner";
|
|
2
|
-
import { existsSync, mkdirSync, readdirSync, statSync, unlinkSync, watch } from "fs";
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, statSync, unlinkSync, watch, writeFileSync } from "fs";
|
|
3
3
|
import { readFile } from "fs/promises";
|
|
4
4
|
import { join } from "path";
|
|
5
5
|
import * as log from "../log.js";
|
|
@@ -128,10 +128,11 @@ export class EventsWatcher {
|
|
|
128
128
|
}
|
|
129
129
|
if (!event) {
|
|
130
130
|
log.logWarning(`Failed to parse event file after ${MAX_RETRIES} retries: ${filename}`, lastError?.message);
|
|
131
|
-
this.
|
|
131
|
+
this.markInvalid(filename, lastError?.message ?? "Unknown event parse error");
|
|
132
132
|
return;
|
|
133
133
|
}
|
|
134
134
|
this.knownFiles.add(filename);
|
|
135
|
+
this.clearInvalidMarker(filename);
|
|
135
136
|
switch (event.type) {
|
|
136
137
|
case "immediate":
|
|
137
138
|
this.handleImmediate(filename, event);
|
|
@@ -196,7 +197,7 @@ export class EventsWatcher {
|
|
|
196
197
|
const now = Date.now();
|
|
197
198
|
if (!Number.isFinite(atTime)) {
|
|
198
199
|
log.logWarning(`Invalid one-shot time for ${filename}: ${event.at}`);
|
|
199
|
-
this.
|
|
200
|
+
this.markInvalid(filename, `Invalid one-shot time: ${event.at}`);
|
|
200
201
|
return;
|
|
201
202
|
}
|
|
202
203
|
if (atTime <= now) {
|
|
@@ -207,7 +208,7 @@ export class EventsWatcher {
|
|
|
207
208
|
const delay = atTime - now;
|
|
208
209
|
if (delay > MAX_TIMEOUT_MS) {
|
|
209
210
|
log.logWarning(`One-shot event exceeds maximum supported delay for ${filename}: ${event.at}. Use a periodic cron event instead.`);
|
|
210
|
-
this.
|
|
211
|
+
this.markInvalid(filename, `One-shot event exceeds maximum supported delay: ${event.at}`);
|
|
211
212
|
return;
|
|
212
213
|
}
|
|
213
214
|
log.logInfo(`Scheduling one-shot event: ${filename} in ${Math.round(delay / 1000)}s`);
|
|
@@ -230,7 +231,7 @@ export class EventsWatcher {
|
|
|
230
231
|
}
|
|
231
232
|
catch (err) {
|
|
232
233
|
log.logWarning(`Invalid cron schedule for ${filename}: ${event.schedule}`, String(err));
|
|
233
|
-
this.
|
|
234
|
+
this.markInvalid(filename, `Invalid cron schedule: ${event.schedule}\n${String(err)}`);
|
|
234
235
|
}
|
|
235
236
|
}
|
|
236
237
|
execute(filename, event, deleteAfter = true) {
|
|
@@ -279,8 +280,32 @@ export class EventsWatcher {
|
|
|
279
280
|
log.logWarning(`Failed to delete event file: ${filename}`, String(err));
|
|
280
281
|
}
|
|
281
282
|
}
|
|
283
|
+
this.clearInvalidMarker(filename);
|
|
282
284
|
this.knownFiles.delete(filename);
|
|
283
285
|
}
|
|
286
|
+
getInvalidMarkerPath(filename) {
|
|
287
|
+
return join(this.eventsDir, `${filename}.error.txt`);
|
|
288
|
+
}
|
|
289
|
+
markInvalid(filename, message) {
|
|
290
|
+
try {
|
|
291
|
+
writeFileSync(this.getInvalidMarkerPath(filename), [`timestamp: ${new Date().toISOString()}`, `file: ${filename}`, "", message.trim()].join("\n"), "utf-8");
|
|
292
|
+
}
|
|
293
|
+
catch (err) {
|
|
294
|
+
log.logWarning(`Failed to write event error marker: ${filename}`, String(err));
|
|
295
|
+
}
|
|
296
|
+
this.knownFiles.add(filename);
|
|
297
|
+
}
|
|
298
|
+
clearInvalidMarker(filename) {
|
|
299
|
+
const markerPath = this.getInvalidMarkerPath(filename);
|
|
300
|
+
try {
|
|
301
|
+
unlinkSync(markerPath);
|
|
302
|
+
}
|
|
303
|
+
catch (err) {
|
|
304
|
+
if (err instanceof Error && "code" in err && err.code !== "ENOENT") {
|
|
305
|
+
log.logWarning(`Failed to delete event error marker: ${filename}`, String(err));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
284
309
|
sleep(ms) {
|
|
285
310
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
286
311
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { basename } from "node:path";
|
|
2
|
+
import { isWindowsPlatform } from "./platform.js";
|
|
2
3
|
const WHITESPACE = /\s+/;
|
|
3
4
|
function stripNullAndNormalize(text) {
|
|
4
5
|
return text.replace(/\0/g, "").normalize("NFKC");
|
|
@@ -402,6 +403,9 @@ export function guardCommand(command, config) {
|
|
|
402
403
|
if (!config.enabled) {
|
|
403
404
|
return { allowed: true };
|
|
404
405
|
}
|
|
406
|
+
if (isWindowsPlatform()) {
|
|
407
|
+
return { allowed: true };
|
|
408
|
+
}
|
|
405
409
|
const atoms = splitCommandChain(command);
|
|
406
410
|
const normalizedWhole = stripNullAndNormalize(command);
|
|
407
411
|
for (const allowPattern of config.allowPatterns) {
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
+
import type { ConfigDiagnostic } from "../shared/config-diagnostics.js";
|
|
1
2
|
import type { SecurityConfig } from "./types.js";
|
|
3
|
+
export interface LoadedSecurityConfig {
|
|
4
|
+
config: SecurityConfig;
|
|
5
|
+
diagnostics: ConfigDiagnostic[];
|
|
6
|
+
}
|
|
2
7
|
export declare const DEFAULT_SECURITY_CONFIG: SecurityConfig;
|
|
3
8
|
export declare function getSecurityConfigPath(appHomeDir?: string): string;
|
|
9
|
+
export declare function loadSecurityConfigWithDiagnostics(appHomeDir?: string): LoadedSecurityConfig;
|
|
4
10
|
export declare function loadSecurityConfig(appHomeDir?: string): SecurityConfig;
|
package/dist/security/config.js
CHANGED
|
@@ -34,14 +34,30 @@ function asStringArray(value) {
|
|
|
34
34
|
function asOptionalString(value) {
|
|
35
35
|
return typeof value === "string" && value.trim() ? value : undefined;
|
|
36
36
|
}
|
|
37
|
-
function
|
|
37
|
+
function pushInvalidSecurityDiagnostic(diagnostics, configPath, field, message) {
|
|
38
|
+
diagnostics.push({
|
|
39
|
+
source: "security",
|
|
40
|
+
path: configPath,
|
|
41
|
+
severity: "warning",
|
|
42
|
+
message: `${field}: ${message}`,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function mergeSecurityConfig(source, configPath, diagnostics) {
|
|
38
46
|
if (!isRecord(source)) {
|
|
47
|
+
pushInvalidSecurityDiagnostic(diagnostics, configPath, "root", "expected a JSON object; using defaults");
|
|
39
48
|
return DEFAULT_SECURITY_CONFIG;
|
|
40
49
|
}
|
|
41
50
|
const commandGuard = isRecord(source.commandGuard) ? source.commandGuard : {};
|
|
42
51
|
const pathGuard = isRecord(source.pathGuard) ? source.pathGuard : {};
|
|
43
52
|
const networkGuard = isRecord(source.networkGuard) ? source.networkGuard : {};
|
|
44
53
|
const audit = isRecord(source.audit) ? source.audit : {};
|
|
54
|
+
if (networkGuard.maxRedirects !== undefined) {
|
|
55
|
+
const maxRedirects = networkGuard.maxRedirects;
|
|
56
|
+
const isValidMaxRedirects = typeof maxRedirects === "number" && Number.isFinite(maxRedirects) && maxRedirects > 0;
|
|
57
|
+
if (!isValidMaxRedirects) {
|
|
58
|
+
pushInvalidSecurityDiagnostic(diagnostics, configPath, "networkGuard.maxRedirects", "expected a positive integer; using default");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
45
61
|
return {
|
|
46
62
|
enabled: typeof source.enabled === "boolean" ? source.enabled : DEFAULT_SECURITY_CONFIG.enabled,
|
|
47
63
|
commandGuard: {
|
|
@@ -85,17 +101,33 @@ function mergeSecurityConfig(source) {
|
|
|
85
101
|
export function getSecurityConfigPath(appHomeDir = APP_HOME_DIR) {
|
|
86
102
|
return join(appHomeDir, "security.json");
|
|
87
103
|
}
|
|
88
|
-
export function
|
|
104
|
+
export function loadSecurityConfigWithDiagnostics(appHomeDir = APP_HOME_DIR) {
|
|
89
105
|
const configPath = getSecurityConfigPath(appHomeDir);
|
|
90
106
|
if (!existsSync(configPath)) {
|
|
91
|
-
return DEFAULT_SECURITY_CONFIG;
|
|
107
|
+
return { config: DEFAULT_SECURITY_CONFIG, diagnostics: [] };
|
|
92
108
|
}
|
|
93
109
|
try {
|
|
94
110
|
const raw = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
95
|
-
|
|
111
|
+
const diagnostics = [];
|
|
112
|
+
return {
|
|
113
|
+
config: mergeSecurityConfig(raw, configPath, diagnostics),
|
|
114
|
+
diagnostics,
|
|
115
|
+
};
|
|
96
116
|
}
|
|
97
117
|
catch (error) {
|
|
98
|
-
|
|
99
|
-
|
|
118
|
+
return {
|
|
119
|
+
config: DEFAULT_SECURITY_CONFIG,
|
|
120
|
+
diagnostics: [
|
|
121
|
+
{
|
|
122
|
+
source: "security",
|
|
123
|
+
path: configPath,
|
|
124
|
+
severity: "error",
|
|
125
|
+
message: error instanceof Error ? error.message : String(error),
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
};
|
|
100
129
|
}
|
|
101
130
|
}
|
|
131
|
+
export function loadSecurityConfig(appHomeDir = APP_HOME_DIR) {
|
|
132
|
+
return loadSecurityConfigWithDiagnostics(appHomeDir).config;
|
|
133
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, lstatSync, realpathSync } from "node:fs";
|
|
2
2
|
import { homedir, tmpdir } from "node:os";
|
|
3
3
|
import { basename, dirname, isAbsolute, normalize, resolve } from "node:path";
|
|
4
|
+
import { isWindowsPlatform } from "./platform.js";
|
|
4
5
|
const PRIVATE_KEY_EXTENSIONS = new Set([".pem", ".key", ".p12", ".pfx"]);
|
|
5
6
|
const PRIVATE_KEY_NAME_HINTS = /(id_rsa|id_ed25519|private|secret|credentials)/i;
|
|
6
7
|
const PROC_MEM_PATH = /^\/proc\/\d+\/mem(?:\/|$)/;
|
|
@@ -197,6 +198,9 @@ export function guardPath(rawPath, operation, ctx) {
|
|
|
197
198
|
if (!ctx.config.enabled) {
|
|
198
199
|
return { allowed: true, operation, rawPath };
|
|
199
200
|
}
|
|
201
|
+
if (isWindowsPlatform()) {
|
|
202
|
+
return { allowed: true, operation, rawPath };
|
|
203
|
+
}
|
|
200
204
|
const homeDir = ctx.homeDir ?? homedir();
|
|
201
205
|
const effectiveCtx = {
|
|
202
206
|
...ctx,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function isWindowsPlatform(): boolean;
|
package/dist/settings.d.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* This module currently provides only PipiclawSettingsManager.
|
|
8
8
|
*/
|
|
9
9
|
import type { Transport } from "@mariozechner/pi-ai";
|
|
10
|
+
import type { ConfigDiagnostic } from "./shared/config-diagnostics.js";
|
|
10
11
|
type PackageSource = string | {
|
|
11
12
|
source: string;
|
|
12
13
|
extensions?: string[];
|
|
@@ -83,10 +84,13 @@ export interface PipiclawSettings {
|
|
|
83
84
|
export declare class PipiclawSettingsManager {
|
|
84
85
|
private settingsPath;
|
|
85
86
|
private settings;
|
|
87
|
+
private loadErrors;
|
|
86
88
|
constructor(baseDir: string);
|
|
87
89
|
private load;
|
|
88
90
|
private save;
|
|
89
91
|
reload(): void;
|
|
92
|
+
drainErrors(): SettingsError[];
|
|
93
|
+
getDiagnostics(): ConfigDiagnostic[];
|
|
90
94
|
getCompactionSettings(): PipiclawCompactionSettings;
|
|
91
95
|
getCompactionEnabled(): boolean;
|
|
92
96
|
setCompactionEnabled(enabled: boolean): void;
|
|
@@ -176,6 +180,5 @@ export declare class PipiclawSettingsManager {
|
|
|
176
180
|
getProjectSettings(): Settings;
|
|
177
181
|
applyOverrides(overrides: Partial<Settings>): void;
|
|
178
182
|
flush(): Promise<void>;
|
|
179
|
-
drainErrors(): SettingsError[];
|
|
180
183
|
}
|
|
181
184
|
export {};
|
package/dist/settings.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
10
10
|
import { dirname, join } from "path";
|
|
11
|
+
import * as log from "./log.js";
|
|
11
12
|
const DEFAULT_COMPACTION = {
|
|
12
13
|
enabled: true,
|
|
13
14
|
reserveTokens: 16384,
|
|
@@ -40,18 +41,32 @@ const DEFAULT_SESSION_MEMORY = {
|
|
|
40
41
|
*/
|
|
41
42
|
export class PipiclawSettingsManager {
|
|
42
43
|
constructor(baseDir) {
|
|
44
|
+
this.loadErrors = [];
|
|
43
45
|
this.settingsPath = join(baseDir, "settings.json");
|
|
44
46
|
this.settings = this.load();
|
|
45
47
|
}
|
|
46
48
|
load() {
|
|
49
|
+
this.loadErrors = [];
|
|
47
50
|
if (!existsSync(this.settingsPath)) {
|
|
48
51
|
return {};
|
|
49
52
|
}
|
|
50
53
|
try {
|
|
51
54
|
const content = readFileSync(this.settingsPath, "utf-8");
|
|
52
|
-
|
|
55
|
+
const parsed = JSON.parse(content);
|
|
56
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
57
|
+
this.loadErrors.push({
|
|
58
|
+
scope: "global",
|
|
59
|
+
error: new Error(`Expected a JSON object in ${this.settingsPath}`),
|
|
60
|
+
});
|
|
61
|
+
return {};
|
|
62
|
+
}
|
|
63
|
+
return parsed;
|
|
53
64
|
}
|
|
54
|
-
catch {
|
|
65
|
+
catch (error) {
|
|
66
|
+
this.loadErrors.push({
|
|
67
|
+
scope: "global",
|
|
68
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
69
|
+
});
|
|
55
70
|
return {};
|
|
56
71
|
}
|
|
57
72
|
}
|
|
@@ -64,12 +79,25 @@ export class PipiclawSettingsManager {
|
|
|
64
79
|
writeFileSync(this.settingsPath, JSON.stringify(this.settings, null, 2), "utf-8");
|
|
65
80
|
}
|
|
66
81
|
catch (error) {
|
|
67
|
-
|
|
82
|
+
log.logWarning(`Could not save settings file`, `${this.settingsPath}\n${String(error)}`);
|
|
68
83
|
}
|
|
69
84
|
}
|
|
70
85
|
reload() {
|
|
71
86
|
this.settings = this.load();
|
|
72
87
|
}
|
|
88
|
+
drainErrors() {
|
|
89
|
+
const errors = this.loadErrors;
|
|
90
|
+
this.loadErrors = [];
|
|
91
|
+
return errors;
|
|
92
|
+
}
|
|
93
|
+
getDiagnostics() {
|
|
94
|
+
return this.loadErrors.map(({ error }) => ({
|
|
95
|
+
source: "settings",
|
|
96
|
+
path: this.settingsPath,
|
|
97
|
+
severity: "error",
|
|
98
|
+
message: error.message,
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
73
101
|
getCompactionSettings() {
|
|
74
102
|
return {
|
|
75
103
|
...DEFAULT_COMPACTION,
|
|
@@ -377,7 +405,4 @@ export class PipiclawSettingsManager {
|
|
|
377
405
|
flush() {
|
|
378
406
|
return Promise.resolve();
|
|
379
407
|
}
|
|
380
|
-
drainErrors() {
|
|
381
|
-
return [];
|
|
382
|
-
}
|
|
383
408
|
}
|
package/dist/tools/config.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ConfigDiagnostic } from "../shared/config-diagnostics.js";
|
|
1
2
|
export type WebSearchProvider = "brave" | "tavily" | "jina" | "searxng" | "duckduckgo";
|
|
2
3
|
export interface PipiclawWebSearchConfig {
|
|
3
4
|
provider: WebSearchProvider;
|
|
@@ -10,6 +11,7 @@ export interface PipiclawWebFetchConfig {
|
|
|
10
11
|
maxChars: number;
|
|
11
12
|
timeoutMs: number;
|
|
12
13
|
maxImageBytes: number;
|
|
14
|
+
maxResponseBytes: number;
|
|
13
15
|
preferJina: boolean;
|
|
14
16
|
enableJinaFallback: boolean;
|
|
15
17
|
defaultExtractMode: "markdown" | "text";
|
|
@@ -25,6 +27,11 @@ export interface PipiclawToolsConfig {
|
|
|
25
27
|
web: PipiclawWebToolsConfig;
|
|
26
28
|
};
|
|
27
29
|
}
|
|
30
|
+
export interface LoadedToolsConfig {
|
|
31
|
+
config: PipiclawToolsConfig;
|
|
32
|
+
diagnostics: ConfigDiagnostic[];
|
|
33
|
+
}
|
|
28
34
|
export declare const DEFAULT_TOOLS_CONFIG: PipiclawToolsConfig;
|
|
29
35
|
export declare function getToolsConfigPath(appHomeDir?: string): string;
|
|
36
|
+
export declare function loadToolsConfigWithDiagnostics(appHomeDir?: string): LoadedToolsConfig;
|
|
30
37
|
export declare function loadToolsConfig(appHomeDir?: string): PipiclawToolsConfig;
|
package/dist/tools/config.js
CHANGED
|
@@ -19,6 +19,7 @@ export const DEFAULT_TOOLS_CONFIG = {
|
|
|
19
19
|
maxChars: 50_000,
|
|
20
20
|
timeoutMs: 30_000,
|
|
21
21
|
maxImageBytes: 10 * 1024 * 1024,
|
|
22
|
+
maxResponseBytes: 5 * 1024 * 1024,
|
|
22
23
|
preferJina: false,
|
|
23
24
|
enableJinaFallback: false,
|
|
24
25
|
defaultExtractMode: "markdown",
|
|
@@ -52,8 +53,17 @@ function asOptionalProxy(value) {
|
|
|
52
53
|
const trimmed = value.trim();
|
|
53
54
|
return trimmed.length > 0 ? trimmed : null;
|
|
54
55
|
}
|
|
55
|
-
function
|
|
56
|
+
function pushInvalidValueDiagnostic(diagnostics, configPath, field, message) {
|
|
57
|
+
diagnostics.push({
|
|
58
|
+
source: "tools",
|
|
59
|
+
path: configPath,
|
|
60
|
+
severity: "warning",
|
|
61
|
+
message: `${field}: ${message}`,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function mergeToolsConfig(source, configPath, diagnostics) {
|
|
56
65
|
if (!isRecord(source)) {
|
|
66
|
+
pushInvalidValueDiagnostic(diagnostics, configPath, "root", "expected a JSON object; using defaults");
|
|
57
67
|
return DEFAULT_TOOLS_CONFIG;
|
|
58
68
|
}
|
|
59
69
|
const tools = isRecord(source.tools) ? source.tools : {};
|
|
@@ -63,8 +73,37 @@ function mergeToolsConfig(source) {
|
|
|
63
73
|
const providerValue = asTrimmedString(search.provider, DEFAULT_TOOLS_CONFIG.tools.web.search.provider).toLowerCase();
|
|
64
74
|
const provider = WEB_SEARCH_PROVIDERS.includes(providerValue)
|
|
65
75
|
? providerValue
|
|
66
|
-
:
|
|
76
|
+
: (() => {
|
|
77
|
+
if (search.provider !== undefined) {
|
|
78
|
+
pushInvalidValueDiagnostic(diagnostics, configPath, "tools.web.search.provider", `unknown provider "${String(search.provider)}"; using ${DEFAULT_TOOLS_CONFIG.tools.web.search.provider}`);
|
|
79
|
+
}
|
|
80
|
+
return DEFAULT_TOOLS_CONFIG.tools.web.search.provider;
|
|
81
|
+
})();
|
|
67
82
|
const defaultExtractMode = asTrimmedString(fetch.defaultExtractMode, DEFAULT_TOOLS_CONFIG.tools.web.fetch.defaultExtractMode);
|
|
83
|
+
if (web.proxy !== undefined && web.proxy !== null && typeof web.proxy !== "string") {
|
|
84
|
+
pushInvalidValueDiagnostic(diagnostics, configPath, "tools.web.proxy", "expected a string or null; using null");
|
|
85
|
+
}
|
|
86
|
+
if (search.maxResults !== undefined && clampInteger(search.maxResults, -1, 1, 10) === -1) {
|
|
87
|
+
pushInvalidValueDiagnostic(diagnostics, configPath, "tools.web.search.maxResults", "expected an integer between 1 and 10; using default");
|
|
88
|
+
}
|
|
89
|
+
if (search.timeoutMs !== undefined && clampInteger(search.timeoutMs, -1, 1) === -1) {
|
|
90
|
+
pushInvalidValueDiagnostic(diagnostics, configPath, "tools.web.search.timeoutMs", "expected a positive integer; using default");
|
|
91
|
+
}
|
|
92
|
+
if (fetch.maxChars !== undefined && clampInteger(fetch.maxChars, -1, 100) === -1) {
|
|
93
|
+
pushInvalidValueDiagnostic(diagnostics, configPath, "tools.web.fetch.maxChars", "expected an integer >= 100; using default");
|
|
94
|
+
}
|
|
95
|
+
if (fetch.timeoutMs !== undefined && clampInteger(fetch.timeoutMs, -1, 1) === -1) {
|
|
96
|
+
pushInvalidValueDiagnostic(diagnostics, configPath, "tools.web.fetch.timeoutMs", "expected a positive integer; using default");
|
|
97
|
+
}
|
|
98
|
+
if (fetch.maxImageBytes !== undefined && clampInteger(fetch.maxImageBytes, -1, 1) === -1) {
|
|
99
|
+
pushInvalidValueDiagnostic(diagnostics, configPath, "tools.web.fetch.maxImageBytes", "expected a positive integer; using default");
|
|
100
|
+
}
|
|
101
|
+
if (fetch.maxResponseBytes !== undefined && clampInteger(fetch.maxResponseBytes, -1, 1) === -1) {
|
|
102
|
+
pushInvalidValueDiagnostic(diagnostics, configPath, "tools.web.fetch.maxResponseBytes", "expected a positive integer; using default");
|
|
103
|
+
}
|
|
104
|
+
if (fetch.defaultExtractMode !== undefined && defaultExtractMode !== "text" && defaultExtractMode !== "markdown") {
|
|
105
|
+
pushInvalidValueDiagnostic(diagnostics, configPath, "tools.web.fetch.defaultExtractMode", `expected "markdown" or "text"; using ${DEFAULT_TOOLS_CONFIG.tools.web.fetch.defaultExtractMode}`);
|
|
106
|
+
}
|
|
68
107
|
return {
|
|
69
108
|
tools: {
|
|
70
109
|
web: {
|
|
@@ -81,6 +120,7 @@ function mergeToolsConfig(source) {
|
|
|
81
120
|
maxChars: clampInteger(fetch.maxChars, DEFAULT_TOOLS_CONFIG.tools.web.fetch.maxChars, 100),
|
|
82
121
|
timeoutMs: clampInteger(fetch.timeoutMs, DEFAULT_TOOLS_CONFIG.tools.web.fetch.timeoutMs, 1),
|
|
83
122
|
maxImageBytes: clampInteger(fetch.maxImageBytes, DEFAULT_TOOLS_CONFIG.tools.web.fetch.maxImageBytes, 1),
|
|
123
|
+
maxResponseBytes: clampInteger(fetch.maxResponseBytes, DEFAULT_TOOLS_CONFIG.tools.web.fetch.maxResponseBytes, 1),
|
|
84
124
|
preferJina: typeof fetch.preferJina === "boolean"
|
|
85
125
|
? fetch.preferJina
|
|
86
126
|
: DEFAULT_TOOLS_CONFIG.tools.web.fetch.preferJina,
|
|
@@ -98,17 +138,33 @@ function mergeToolsConfig(source) {
|
|
|
98
138
|
export function getToolsConfigPath(appHomeDir = APP_HOME_DIR) {
|
|
99
139
|
return appHomeDir === APP_HOME_DIR ? TOOLS_CONFIG_PATH : join(appHomeDir, "tools.json");
|
|
100
140
|
}
|
|
101
|
-
export function
|
|
141
|
+
export function loadToolsConfigWithDiagnostics(appHomeDir = APP_HOME_DIR) {
|
|
102
142
|
const configPath = getToolsConfigPath(appHomeDir);
|
|
103
143
|
if (!existsSync(configPath)) {
|
|
104
|
-
return DEFAULT_TOOLS_CONFIG;
|
|
144
|
+
return { config: DEFAULT_TOOLS_CONFIG, diagnostics: [] };
|
|
105
145
|
}
|
|
106
146
|
try {
|
|
107
147
|
const raw = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
108
|
-
|
|
148
|
+
const diagnostics = [];
|
|
149
|
+
return {
|
|
150
|
+
config: mergeToolsConfig(raw, configPath, diagnostics),
|
|
151
|
+
diagnostics,
|
|
152
|
+
};
|
|
109
153
|
}
|
|
110
154
|
catch (error) {
|
|
111
|
-
|
|
112
|
-
|
|
155
|
+
return {
|
|
156
|
+
config: DEFAULT_TOOLS_CONFIG,
|
|
157
|
+
diagnostics: [
|
|
158
|
+
{
|
|
159
|
+
source: "tools",
|
|
160
|
+
path: configPath,
|
|
161
|
+
severity: "error",
|
|
162
|
+
message: error instanceof Error ? error.message : String(error),
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
};
|
|
113
166
|
}
|
|
114
167
|
}
|
|
168
|
+
export function loadToolsConfig(appHomeDir = APP_HOME_DIR) {
|
|
169
|
+
return loadToolsConfigWithDiagnostics(appHomeDir).config;
|
|
170
|
+
}
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { Executor, SandboxConfig } from "../sandbox.js";
|
|
|
4
4
|
import type { SecurityConfig, SecurityRuntimeContext } from "../security/types.js";
|
|
5
5
|
import type { PipiclawMemoryRecallSettings } from "../settings.js";
|
|
6
6
|
import type { SubAgentDiscoveryResult } from "../subagents/discovery.js";
|
|
7
|
+
import type { PipiclawToolsConfig } from "./config.js";
|
|
7
8
|
export interface CreatePipiclawToolsOptions {
|
|
8
9
|
executor: Executor;
|
|
9
10
|
getCurrentModel: () => Model<Api>;
|
|
@@ -16,6 +17,8 @@ export interface CreatePipiclawToolsOptions {
|
|
|
16
17
|
sandboxConfig: SandboxConfig;
|
|
17
18
|
getSubAgentDiscovery: () => SubAgentDiscoveryResult;
|
|
18
19
|
getMemoryRecallSettings: () => PipiclawMemoryRecallSettings;
|
|
20
|
+
securityConfig?: SecurityConfig;
|
|
21
|
+
toolsConfig?: PipiclawToolsConfig;
|
|
19
22
|
}
|
|
20
23
|
export interface CreatePipiclawBaseToolsOptions {
|
|
21
24
|
securityConfig?: SecurityConfig;
|
package/dist/tools/index.js
CHANGED
|
@@ -25,8 +25,8 @@ export function createPipiclawBaseTools(executor, options = {}) {
|
|
|
25
25
|
];
|
|
26
26
|
}
|
|
27
27
|
export function createPipiclawTools(options) {
|
|
28
|
-
const securityConfig = loadSecurityConfig(APP_HOME_DIR);
|
|
29
|
-
const toolsConfig = loadToolsConfig(APP_HOME_DIR);
|
|
28
|
+
const securityConfig = options.securityConfig ?? loadSecurityConfig(APP_HOME_DIR);
|
|
29
|
+
const toolsConfig = options.toolsConfig ?? loadToolsConfig(APP_HOME_DIR);
|
|
30
30
|
const securityContext = {
|
|
31
31
|
workspaceDir: options.workspaceDir,
|
|
32
32
|
workspacePath: options.workspacePath,
|
package/dist/web/client.d.ts
CHANGED
package/dist/web/client.js
CHANGED
|
@@ -108,24 +108,36 @@ export class WebHttpClient {
|
|
|
108
108
|
throw error;
|
|
109
109
|
}
|
|
110
110
|
const agent = getProxyAgent(currentUrl, this.context.webConfig.proxy);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
111
|
+
let response;
|
|
112
|
+
try {
|
|
113
|
+
response = await axios.request({
|
|
114
|
+
method,
|
|
115
|
+
url: currentUrl,
|
|
116
|
+
data,
|
|
117
|
+
headers: {
|
|
118
|
+
"User-Agent": WEB_USER_AGENT,
|
|
119
|
+
Accept: "*/*",
|
|
120
|
+
...options.headers,
|
|
121
|
+
},
|
|
122
|
+
responseType: "arraybuffer",
|
|
123
|
+
validateStatus: () => true,
|
|
124
|
+
timeout: options.timeoutMs,
|
|
125
|
+
signal: options.signal,
|
|
126
|
+
maxRedirects: 0,
|
|
127
|
+
maxContentLength: options.maxResponseBytes ?? Number.POSITIVE_INFINITY,
|
|
128
|
+
proxy: false,
|
|
129
|
+
httpAgent: agent,
|
|
130
|
+
httpsAgent: agent,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
if (options.maxResponseBytes &&
|
|
135
|
+
typeof error?.message === "string" &&
|
|
136
|
+
error.message.includes("maxContentLength")) {
|
|
137
|
+
throw new Error(`Response exceeds maxResponseBytes (${options.maxResponseBytes} bytes)`);
|
|
138
|
+
}
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
129
141
|
const headers = normalizeHeaders(response.headers);
|
|
130
142
|
const body = Buffer.isBuffer(response.data) ? response.data : Buffer.from(response.data);
|
|
131
143
|
if (isRedirectStatus(response.status) && headers.location) {
|
package/dist/web/config.d.ts
CHANGED
package/dist/web/config.js
CHANGED
|
@@ -12,6 +12,7 @@ export function resolveWebFetchRequest(config, url, extractMode, maxChars) {
|
|
|
12
12
|
maxChars: clamp(maxChars, config.maxChars, 100),
|
|
13
13
|
timeoutMs: config.timeoutMs,
|
|
14
14
|
maxImageBytes: config.maxImageBytes,
|
|
15
|
+
maxResponseBytes: config.maxResponseBytes,
|
|
15
16
|
preferJina: config.preferJina,
|
|
16
17
|
enableJinaFallback: config.enableJinaFallback,
|
|
17
18
|
};
|
package/dist/web/fetch.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export declare function runWebFetch(context: WebFetchExecutionContext, request:
|
|
|
17
17
|
extractMode: "markdown" | "text";
|
|
18
18
|
maxChars: number;
|
|
19
19
|
maxImageBytes: number;
|
|
20
|
+
maxResponseBytes: number;
|
|
20
21
|
preferJina: boolean;
|
|
21
22
|
enableJinaFallback: boolean;
|
|
22
23
|
}, signal?: AbortSignal): Promise<WebFetchOutput>;
|