@hybridaione/hybridclaw 0.2.3 → 0.2.7
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/.github/workflows/ci.yml +70 -0
- package/.husky/pre-commit +1 -0
- package/CHANGELOG.md +95 -0
- package/CONTRIBUTING.md +33 -0
- package/README.md +57 -19
- package/SECURITY.md +17 -0
- package/biome.json +35 -0
- package/config.example.json +72 -7
- package/container/package-lock.json +2 -2
- package/container/package.json +1 -1
- package/container/src/approval-policy.ts +1405 -0
- package/container/src/browser-tools.ts +672 -136
- package/container/src/extensions.ts +36 -12
- package/container/src/hybridai-client.ts +65 -16
- package/container/src/index.ts +463 -110
- package/container/src/ipc.ts +5 -3
- package/container/src/token-usage.ts +107 -18
- package/container/src/tools.ts +600 -225
- package/container/src/types.ts +49 -2
- package/container/src/web-fetch.ts +89 -32
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +15 -4
- package/dist/agent.js.map +1 -1
- package/dist/audit-cli.d.ts.map +1 -1
- package/dist/audit-cli.js +4 -2
- package/dist/audit-cli.js.map +1 -1
- package/dist/audit-events.d.ts.map +1 -1
- package/dist/audit-events.js +53 -3
- package/dist/audit-events.js.map +1 -1
- package/dist/audit-trail.d.ts.map +1 -1
- package/dist/audit-trail.js +17 -8
- package/dist/audit-trail.js.map +1 -1
- package/dist/channels/discord/attachments.d.ts.map +1 -1
- package/dist/channels/discord/attachments.js +14 -7
- package/dist/channels/discord/attachments.js.map +1 -1
- package/dist/channels/discord/debounce.d.ts +9 -0
- package/dist/channels/discord/debounce.d.ts.map +1 -0
- package/dist/channels/discord/debounce.js +20 -0
- package/dist/channels/discord/debounce.js.map +1 -0
- package/dist/channels/discord/delivery.d.ts +4 -1
- package/dist/channels/discord/delivery.d.ts.map +1 -1
- package/dist/channels/discord/delivery.js +24 -4
- package/dist/channels/discord/delivery.js.map +1 -1
- package/dist/channels/discord/human-delay.d.ts +16 -0
- package/dist/channels/discord/human-delay.d.ts.map +1 -0
- package/dist/channels/discord/human-delay.js +29 -0
- package/dist/channels/discord/human-delay.js.map +1 -0
- package/dist/channels/discord/inbound.d.ts +31 -0
- package/dist/channels/discord/inbound.d.ts.map +1 -1
- package/dist/channels/discord/inbound.js +156 -6
- package/dist/channels/discord/inbound.js.map +1 -1
- package/dist/channels/discord/mentions.d.ts.map +1 -1
- package/dist/channels/discord/mentions.js +16 -4
- package/dist/channels/discord/mentions.js.map +1 -1
- package/dist/channels/discord/presence.d.ts +33 -0
- package/dist/channels/discord/presence.d.ts.map +1 -0
- package/dist/channels/discord/presence.js +111 -0
- package/dist/channels/discord/presence.js.map +1 -0
- package/dist/channels/discord/rate-limiter.d.ts +14 -0
- package/dist/channels/discord/rate-limiter.d.ts.map +1 -0
- package/dist/channels/discord/rate-limiter.js +49 -0
- package/dist/channels/discord/rate-limiter.js.map +1 -0
- package/dist/channels/discord/reactions.d.ts +38 -0
- package/dist/channels/discord/reactions.d.ts.map +1 -0
- package/dist/channels/discord/reactions.js +151 -0
- package/dist/channels/discord/reactions.js.map +1 -0
- package/dist/channels/discord/runtime.d.ts +7 -4
- package/dist/channels/discord/runtime.d.ts.map +1 -1
- package/dist/channels/discord/runtime.js +755 -150
- package/dist/channels/discord/runtime.js.map +1 -1
- package/dist/channels/discord/stream.d.ts +4 -1
- package/dist/channels/discord/stream.d.ts.map +1 -1
- package/dist/channels/discord/stream.js +19 -12
- package/dist/channels/discord/stream.js.map +1 -1
- package/dist/channels/discord/tool-actions.d.ts.map +1 -1
- package/dist/channels/discord/tool-actions.js +24 -12
- package/dist/channels/discord/tool-actions.js.map +1 -1
- package/dist/channels/discord/typing.d.ts +15 -0
- package/dist/channels/discord/typing.d.ts.map +1 -0
- package/dist/channels/discord/typing.js +106 -0
- package/dist/channels/discord/typing.js.map +1 -0
- package/dist/chunk.d.ts.map +1 -1
- package/dist/chunk.js +4 -2
- package/dist/chunk.js.map +1 -1
- package/dist/cli.js +77 -23
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +24 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +113 -18
- package/dist/config.js.map +1 -1
- package/dist/container-runner.d.ts.map +1 -1
- package/dist/container-runner.js +59 -26
- package/dist/container-runner.js.map +1 -1
- package/dist/container-setup.d.ts.map +1 -1
- package/dist/container-setup.js +10 -9
- package/dist/container-setup.js.map +1 -1
- package/dist/conversation.d.ts +2 -2
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +1 -1
- package/dist/conversation.js.map +1 -1
- package/dist/db.d.ts +118 -2
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +1568 -50
- package/dist/db.js.map +1 -1
- package/dist/delegation-manager.d.ts.map +1 -1
- package/dist/delegation-manager.js +3 -2
- package/dist/delegation-manager.js.map +1 -1
- package/dist/gateway-client.d.ts +2 -2
- package/dist/gateway-client.d.ts.map +1 -1
- package/dist/gateway-client.js +10 -4
- package/dist/gateway-client.js.map +1 -1
- package/dist/gateway-service.d.ts +3 -3
- package/dist/gateway-service.d.ts.map +1 -1
- package/dist/gateway-service.js +624 -87
- package/dist/gateway-service.js.map +1 -1
- package/dist/gateway-types.d.ts +24 -0
- package/dist/gateway-types.d.ts.map +1 -1
- package/dist/gateway-types.js.map +1 -1
- package/dist/gateway.js +303 -26
- package/dist/gateway.js.map +1 -1
- package/dist/health.d.ts.map +1 -1
- package/dist/health.js +20 -10
- package/dist/health.js.map +1 -1
- package/dist/heartbeat.d.ts +4 -0
- package/dist/heartbeat.d.ts.map +1 -1
- package/dist/heartbeat.js +62 -20
- package/dist/heartbeat.js.map +1 -1
- package/dist/hybridai-bots.d.ts.map +1 -1
- package/dist/hybridai-bots.js +4 -2
- package/dist/hybridai-bots.js.map +1 -1
- package/dist/hybridai-models.d.ts +8 -0
- package/dist/hybridai-models.d.ts.map +1 -0
- package/dist/hybridai-models.js +94 -0
- package/dist/hybridai-models.js.map +1 -0
- package/dist/instruction-approval-audit.d.ts.map +1 -1
- package/dist/instruction-approval-audit.js.map +1 -1
- package/dist/instruction-integrity.d.ts.map +1 -1
- package/dist/instruction-integrity.js +8 -2
- package/dist/instruction-integrity.js.map +1 -1
- package/dist/ipc.d.ts.map +1 -1
- package/dist/ipc.js +6 -1
- package/dist/ipc.js.map +1 -1
- package/dist/logger.js.map +1 -1
- package/dist/memory-consolidation.d.ts +17 -0
- package/dist/memory-consolidation.d.ts.map +1 -0
- package/dist/memory-consolidation.js +25 -0
- package/dist/memory-consolidation.js.map +1 -0
- package/dist/memory-service.d.ts +200 -0
- package/dist/memory-service.d.ts.map +1 -0
- package/dist/memory-service.js +294 -0
- package/dist/memory-service.js.map +1 -0
- package/dist/mount-security.d.ts.map +1 -1
- package/dist/mount-security.js +31 -7
- package/dist/mount-security.js.map +1 -1
- package/dist/observability-ingest.d.ts.map +1 -1
- package/dist/observability-ingest.js +32 -11
- package/dist/observability-ingest.js.map +1 -1
- package/dist/onboarding.d.ts.map +1 -1
- package/dist/onboarding.js +32 -9
- package/dist/onboarding.js.map +1 -1
- package/dist/proactive-policy.d.ts.map +1 -1
- package/dist/proactive-policy.js +2 -1
- package/dist/proactive-policy.js.map +1 -1
- package/dist/prompt-hooks.d.ts.map +1 -1
- package/dist/prompt-hooks.js +11 -7
- package/dist/prompt-hooks.js.map +1 -1
- package/dist/runtime-config.d.ts +104 -1
- package/dist/runtime-config.d.ts.map +1 -1
- package/dist/runtime-config.js +503 -24
- package/dist/runtime-config.js.map +1 -1
- package/dist/scheduled-task-runner.d.ts +1 -0
- package/dist/scheduled-task-runner.d.ts.map +1 -1
- package/dist/scheduled-task-runner.js +43 -10
- package/dist/scheduled-task-runner.js.map +1 -1
- package/dist/scheduler.d.ts +43 -4
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +530 -56
- package/dist/scheduler.js.map +1 -1
- package/dist/session-export.d.ts +26 -0
- package/dist/session-export.d.ts.map +1 -0
- package/dist/session-export.js +149 -0
- package/dist/session-export.js.map +1 -0
- package/dist/session-maintenance.d.ts.map +1 -1
- package/dist/session-maintenance.js +75 -13
- package/dist/session-maintenance.js.map +1 -1
- package/dist/session-transcripts.d.ts.map +1 -1
- package/dist/session-transcripts.js.map +1 -1
- package/dist/side-effects.d.ts.map +1 -1
- package/dist/side-effects.js +14 -2
- package/dist/side-effects.js.map +1 -1
- package/dist/skills-guard.d.ts.map +1 -1
- package/dist/skills-guard.js +893 -130
- package/dist/skills-guard.js.map +1 -1
- package/dist/skills.d.ts +5 -0
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +29 -15
- package/dist/skills.js.map +1 -1
- package/dist/token-efficiency.d.ts.map +1 -1
- package/dist/token-efficiency.js.map +1 -1
- package/dist/tui.js +92 -11
- package/dist/tui.js.map +1 -1
- package/dist/types.d.ts +150 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +24 -1
- package/dist/types.js.map +1 -1
- package/dist/update.d.ts.map +1 -1
- package/dist/update.js +42 -14
- package/dist/update.js.map +1 -1
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +49 -9
- package/dist/workspace.js.map +1 -1
- package/docs/chat.html +9 -3
- package/docs/index.html +72 -13
- package/package.json +8 -2
- package/src/agent.ts +31 -4
- package/src/audit-cli.ts +44 -16
- package/src/audit-events.ts +69 -5
- package/src/audit-trail.ts +41 -15
- package/src/channels/discord/attachments.ts +81 -27
- package/src/channels/discord/debounce.ts +25 -0
- package/src/channels/discord/delivery.ts +65 -14
- package/src/channels/discord/human-delay.ts +48 -0
- package/src/channels/discord/inbound.ts +190 -8
- package/src/channels/discord/mentions.ts +42 -18
- package/src/channels/discord/presence.ts +148 -0
- package/src/channels/discord/rate-limiter.ts +58 -0
- package/src/channels/discord/reactions.ts +211 -0
- package/src/channels/discord/runtime.ts +1161 -205
- package/src/channels/discord/stream.ts +85 -31
- package/src/channels/discord/tool-actions.ts +78 -37
- package/src/channels/discord/typing.ts +140 -0
- package/src/chunk.ts +12 -4
- package/src/cli.ts +174 -57
- package/src/config.ts +208 -36
- package/src/container-runner.ts +134 -42
- package/src/container-setup.ts +57 -22
- package/src/conversation.ts +9 -7
- package/src/db.ts +2217 -84
- package/src/delegation-manager.ts +6 -2
- package/src/gateway-client.ts +41 -17
- package/src/gateway-service.ts +996 -220
- package/src/gateway-types.ts +33 -0
- package/src/gateway.ts +487 -52
- package/src/health.ts +66 -26
- package/src/heartbeat.ts +99 -22
- package/src/hybridai-bots.ts +14 -5
- package/src/hybridai-models.ts +158 -0
- package/src/instruction-approval-audit.ts +4 -1
- package/src/instruction-integrity.ts +30 -9
- package/src/ipc.ts +23 -5
- package/src/logger.ts +4 -1
- package/src/memory-consolidation.ts +41 -0
- package/src/memory-service.ts +606 -0
- package/src/mount-security.ts +58 -13
- package/src/observability-ingest.ts +134 -35
- package/src/onboarding.ts +126 -35
- package/src/proactive-policy.ts +3 -1
- package/src/prompt-hooks.ts +42 -17
- package/src/runtime-config.ts +1112 -122
- package/src/scheduled-task-runner.ts +77 -11
- package/src/scheduler.ts +683 -60
- package/src/session-export.ts +196 -0
- package/src/session-maintenance.ts +125 -22
- package/src/session-transcripts.ts +12 -3
- package/src/side-effects.ts +28 -5
- package/src/skills-guard.ts +1067 -219
- package/src/skills.ts +163 -65
- package/src/token-efficiency.ts +31 -9
- package/src/tui.ts +166 -25
- package/src/types.ts +199 -2
- package/src/update.ts +79 -23
- package/src/workspace.ts +63 -11
- package/tests/approval-policy.test.ts +335 -0
- package/tests/discord.basic.test.ts +213 -4
- package/tests/discord.human-presence.test.ts +85 -0
- package/tests/gateway-service.media-routing.test.ts +8 -2
- package/tests/hybridai-client.test.ts +112 -0
- package/tests/hybridai-models.test.ts +46 -0
- package/tests/memory-service.test.ts +1114 -0
- package/tests/token-efficiency.basic.test.ts +8 -2
- package/tests/token-usage.cache.test.ts +128 -0
- package/vitest.e2e.config.ts +3 -1
- package/vitest.integration.config.ts +3 -1
- package/vitest.live.config.ts +3 -1
- package/vitest.unit.config.ts +9 -0
package/container/src/tools.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import { execSync } from 'child_process';
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
|
|
5
|
-
import {
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
BROWSER_TOOL_DEFINITIONS,
|
|
7
|
+
executeBrowserTool,
|
|
8
|
+
setBrowserModelContext,
|
|
9
|
+
} from './browser-tools.js';
|
|
6
10
|
import type {
|
|
7
11
|
DelegationSideEffect,
|
|
8
12
|
DelegationTaskSpec,
|
|
@@ -15,24 +19,24 @@ import { webFetch } from './web-fetch.js';
|
|
|
15
19
|
// --- Exec safety deny-list (defense-in-depth, adapted from PicoClaw) ---
|
|
16
20
|
|
|
17
21
|
const DENY_PATTERNS: RegExp[] = [
|
|
18
|
-
/\brm\s+-[rf]{1,2}\b/,
|
|
22
|
+
/\brm\s+-[rf]{1,2}\b/, // rm -r, rm -f, rm -rf
|
|
19
23
|
/(^|[;&|]\s*)mkfs(?:\.[a-z0-9_+-]+)?\b/, // mkfs command at segment start
|
|
20
|
-
/(^|[;&|]\s*)format(?:\.com|\.exe)?\b/,
|
|
21
|
-
/\bdd\s+if=/,
|
|
22
|
-
/:\(\)\s*\{.*\};\s*:/,
|
|
23
|
-
/\|\s*(sh|bash|zsh)\b/,
|
|
24
|
-
/;\s*rm\s+-[rf]/,
|
|
25
|
-
/&&\s*rm\s+-[rf]/,
|
|
26
|
-
/\|\|\s*rm\s+-[rf]/,
|
|
27
|
-
/\bcurl\b.*\|\s*(sh|bash)/,
|
|
28
|
-
/\bwget\b.*\|\s*(sh|bash)/,
|
|
29
|
-
/\beval\b/,
|
|
30
|
-
/\bsource\s+.*\.sh\b/,
|
|
31
|
-
/\bpkill\b/,
|
|
32
|
-
/\bkillall\b/,
|
|
33
|
-
/\bkill\s+-9\b/,
|
|
34
|
-
/\b(shutdown|reboot|poweroff)\b/,
|
|
35
|
-
/>\s*\/dev\/sd[a-z]\b/,
|
|
24
|
+
/(^|[;&|]\s*)format(?:\.com|\.exe)?\b/, // format command at segment start (Windows)
|
|
25
|
+
/\bdd\s+if=/, // raw disk I/O
|
|
26
|
+
/:\(\)\s*\{.*\};\s*:/, // fork bomb :(){ :|:& };:
|
|
27
|
+
/\|\s*(sh|bash|zsh)\b/, // pipe to shell
|
|
28
|
+
/;\s*rm\s+-[rf]/, // chained rm after semicolon
|
|
29
|
+
/&&\s*rm\s+-[rf]/, // chained rm after &&
|
|
30
|
+
/\|\|\s*rm\s+-[rf]/, // chained rm after ||
|
|
31
|
+
/\bcurl\b.*\|\s*(sh|bash)/, // curl | sh
|
|
32
|
+
/\bwget\b.*\|\s*(sh|bash)/, // wget | sh
|
|
33
|
+
/\beval\b/, // eval execution
|
|
34
|
+
/\bsource\s+.*\.sh\b/, // source shell scripts
|
|
35
|
+
/\bpkill\b/, // process killing
|
|
36
|
+
/\bkillall\b/, // process killing
|
|
37
|
+
/\bkill\s+-9\b/, // force kill
|
|
38
|
+
/\b(shutdown|reboot|poweroff)\b/, // system power control
|
|
39
|
+
/>\s*\/dev\/sd[a-z]\b/, // write to block devices
|
|
36
40
|
];
|
|
37
41
|
|
|
38
42
|
function guardCommand(command: string): string | null {
|
|
@@ -47,7 +51,16 @@ function guardCommand(command: string): string | null {
|
|
|
47
51
|
|
|
48
52
|
// --- Side-effect accumulator for host-processed actions ---
|
|
49
53
|
|
|
50
|
-
type ScheduledTaskInfo = {
|
|
54
|
+
type ScheduledTaskInfo = {
|
|
55
|
+
id: number;
|
|
56
|
+
cronExpr: string;
|
|
57
|
+
runAt: string | null;
|
|
58
|
+
everyMs: number | null;
|
|
59
|
+
prompt: string;
|
|
60
|
+
enabled: number;
|
|
61
|
+
lastRun: string | null;
|
|
62
|
+
createdAt: string;
|
|
63
|
+
};
|
|
51
64
|
|
|
52
65
|
let pendingSchedules: ScheduleSideEffect[] = [];
|
|
53
66
|
let pendingDelegations: DelegationSideEffect[] = [];
|
|
@@ -80,18 +93,23 @@ export function resetSideEffects(): void {
|
|
|
80
93
|
pendingDelegations = [];
|
|
81
94
|
}
|
|
82
95
|
|
|
83
|
-
export function getPendingSideEffects():
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
96
|
+
export function getPendingSideEffects():
|
|
97
|
+
| {
|
|
98
|
+
schedules?: ScheduleSideEffect[];
|
|
99
|
+
delegations?: DelegationSideEffect[];
|
|
100
|
+
}
|
|
101
|
+
| undefined {
|
|
102
|
+
if (pendingSchedules.length === 0 && pendingDelegations.length === 0)
|
|
103
|
+
return undefined;
|
|
88
104
|
return {
|
|
89
105
|
schedules: pendingSchedules.length > 0 ? pendingSchedules : undefined,
|
|
90
106
|
delegations: pendingDelegations.length > 0 ? pendingDelegations : undefined,
|
|
91
107
|
};
|
|
92
108
|
}
|
|
93
109
|
|
|
94
|
-
export function setScheduledTasks(
|
|
110
|
+
export function setScheduledTasks(
|
|
111
|
+
tasks: ScheduledTaskInfo[] | undefined,
|
|
112
|
+
): void {
|
|
95
113
|
injectedTasks = tasks || [];
|
|
96
114
|
}
|
|
97
115
|
|
|
@@ -99,7 +117,11 @@ export function setSessionContext(sessionId: string): void {
|
|
|
99
117
|
currentSessionId = String(sessionId || '');
|
|
100
118
|
}
|
|
101
119
|
|
|
102
|
-
export function setGatewayContext(
|
|
120
|
+
export function setGatewayContext(
|
|
121
|
+
baseUrl?: string,
|
|
122
|
+
apiToken?: string,
|
|
123
|
+
channelId?: string,
|
|
124
|
+
): void {
|
|
103
125
|
gatewayBaseUrl = String(baseUrl || '').trim();
|
|
104
126
|
gatewayApiToken = String(apiToken || '').trim();
|
|
105
127
|
gatewayChannelId = String(channelId || '').trim();
|
|
@@ -128,12 +150,16 @@ function readStringValue(value: unknown): string | undefined {
|
|
|
128
150
|
return trimmed.length > 0 ? trimmed : undefined;
|
|
129
151
|
}
|
|
130
152
|
|
|
131
|
-
function resolveDiscordMessageAction(
|
|
153
|
+
function resolveDiscordMessageAction(
|
|
154
|
+
rawAction: unknown,
|
|
155
|
+
): DiscordMessageToolAction | null {
|
|
132
156
|
const normalized = readStringValue(rawAction)?.toLowerCase();
|
|
133
157
|
if (!normalized) return null;
|
|
134
158
|
if (normalized === 'read' || normalized === 'readmessages') return 'read';
|
|
135
|
-
if (normalized === 'member-info' || normalized === 'memberinfo')
|
|
136
|
-
|
|
159
|
+
if (normalized === 'member-info' || normalized === 'memberinfo')
|
|
160
|
+
return 'member-info';
|
|
161
|
+
if (normalized === 'channel-info' || normalized === 'channelinfo')
|
|
162
|
+
return 'channel-info';
|
|
137
163
|
return null;
|
|
138
164
|
}
|
|
139
165
|
|
|
@@ -143,7 +169,9 @@ function resolveGatewayDiscordActionUrl(): string | null {
|
|
|
143
169
|
return `${base}/api/discord/action`;
|
|
144
170
|
}
|
|
145
171
|
|
|
146
|
-
async function callGatewayDiscordAction(
|
|
172
|
+
async function callGatewayDiscordAction(
|
|
173
|
+
payload: Record<string, unknown>,
|
|
174
|
+
): Promise<string> {
|
|
147
175
|
const url = resolveGatewayDiscordActionUrl();
|
|
148
176
|
if (!url) {
|
|
149
177
|
return 'Error: Discord actions are unavailable because gatewayBaseUrl is not configured.';
|
|
@@ -179,9 +207,10 @@ async function callGatewayDiscordAction(payload: Record<string, unknown>): Promi
|
|
|
179
207
|
}
|
|
180
208
|
|
|
181
209
|
if (!response.ok) {
|
|
182
|
-
const detail =
|
|
183
|
-
|
|
184
|
-
|
|
210
|
+
const detail =
|
|
211
|
+
typeof parsed?.error === 'string'
|
|
212
|
+
? parsed.error
|
|
213
|
+
: rawText || `HTTP ${response.status}`;
|
|
185
214
|
return `Error: Discord action failed (${response.status}): ${detail}`;
|
|
186
215
|
}
|
|
187
216
|
|
|
@@ -189,7 +218,10 @@ async function callGatewayDiscordAction(payload: Record<string, unknown>): Promi
|
|
|
189
218
|
return rawText || JSON.stringify({ ok: true }, null, 2);
|
|
190
219
|
}
|
|
191
220
|
|
|
192
|
-
function normalizeDelegationTask(
|
|
221
|
+
function normalizeDelegationTask(
|
|
222
|
+
raw: unknown,
|
|
223
|
+
fallbackModel?: string,
|
|
224
|
+
): DelegationTaskSpec | null {
|
|
193
225
|
if (typeof raw === 'string') {
|
|
194
226
|
const prompt = raw.trim();
|
|
195
227
|
if (!prompt) return null;
|
|
@@ -202,7 +234,8 @@ function normalizeDelegationTask(raw: unknown, fallbackModel?: string): Delegati
|
|
|
202
234
|
if (!prompt) return null;
|
|
203
235
|
|
|
204
236
|
const label = typeof task.label === 'string' ? task.label.trim() : '';
|
|
205
|
-
const model =
|
|
237
|
+
const model =
|
|
238
|
+
typeof task.model === 'string' ? task.model.trim() : fallbackModel || '';
|
|
206
239
|
const normalized: DelegationTaskSpec = { prompt };
|
|
207
240
|
if (label) normalized.label = label;
|
|
208
241
|
if (model) normalized.model = model;
|
|
@@ -217,10 +250,16 @@ function normalizeDelegationTaskList(params: {
|
|
|
217
250
|
const { raw, fallbackModel, fieldName } = params;
|
|
218
251
|
if (raw == null) return { tasks: [] };
|
|
219
252
|
if (!Array.isArray(raw)) {
|
|
220
|
-
return {
|
|
253
|
+
return {
|
|
254
|
+
tasks: [],
|
|
255
|
+
error: `Error: "${fieldName}" must be an array of task objects.`,
|
|
256
|
+
};
|
|
221
257
|
}
|
|
222
258
|
if (raw.length === 0) {
|
|
223
|
-
return {
|
|
259
|
+
return {
|
|
260
|
+
tasks: [],
|
|
261
|
+
error: `Error: "${fieldName}" must contain at least one task.`,
|
|
262
|
+
};
|
|
224
263
|
}
|
|
225
264
|
if (raw.length > MAX_DELEGATION_BATCH_ITEMS) {
|
|
226
265
|
return {
|
|
@@ -274,7 +313,9 @@ function isSafeDiscordCdnUrl(raw: string): boolean {
|
|
|
274
313
|
return false;
|
|
275
314
|
}
|
|
276
315
|
if (parsed.protocol !== 'https:') return false;
|
|
277
|
-
return DISCORD_CDN_HOST_PATTERNS.some((pattern) =>
|
|
316
|
+
return DISCORD_CDN_HOST_PATTERNS.some((pattern) =>
|
|
317
|
+
pattern.test(parsed.hostname),
|
|
318
|
+
);
|
|
278
319
|
}
|
|
279
320
|
|
|
280
321
|
function normalizeVisionLocalPath(rawPath: string): string | null {
|
|
@@ -286,8 +327,13 @@ function normalizeVisionLocalPath(rawPath: string): string | null {
|
|
|
286
327
|
? path.posix.normalize(normalizedInput)
|
|
287
328
|
: path.posix.normalize(path.posix.join(WORKSPACE_ROOT, normalizedInput));
|
|
288
329
|
if (
|
|
289
|
-
!(
|
|
290
|
-
|
|
330
|
+
!(
|
|
331
|
+
candidate === WORKSPACE_ROOT || candidate.startsWith(`${WORKSPACE_ROOT}/`)
|
|
332
|
+
) &&
|
|
333
|
+
!(
|
|
334
|
+
candidate === DISCORD_MEDIA_CACHE_ROOT ||
|
|
335
|
+
candidate.startsWith(`${DISCORD_MEDIA_CACHE_ROOT}/`)
|
|
336
|
+
)
|
|
291
337
|
) {
|
|
292
338
|
return null;
|
|
293
339
|
}
|
|
@@ -304,8 +350,13 @@ function isKnownDiscordMediaPath(localPath: string): boolean {
|
|
|
304
350
|
return knownPaths.includes(localPath);
|
|
305
351
|
}
|
|
306
352
|
|
|
307
|
-
function inferImageMimeTypeFromPath(
|
|
308
|
-
|
|
353
|
+
function inferImageMimeTypeFromPath(
|
|
354
|
+
localPath: string,
|
|
355
|
+
fallbackMime?: string | null,
|
|
356
|
+
): string {
|
|
357
|
+
const normalizedFallback = String(fallbackMime || '')
|
|
358
|
+
.trim()
|
|
359
|
+
.toLowerCase();
|
|
309
360
|
if (normalizedFallback.startsWith('image/')) return normalizedFallback;
|
|
310
361
|
const ext = path.posix.extname(localPath).toLowerCase();
|
|
311
362
|
if (ext === '.png') return 'image/png';
|
|
@@ -318,13 +369,22 @@ function inferImageMimeTypeFromPath(localPath: string, fallbackMime?: string | n
|
|
|
318
369
|
return 'image/png';
|
|
319
370
|
}
|
|
320
371
|
|
|
321
|
-
async function readVisionImageFromLocalPath(
|
|
372
|
+
async function readVisionImageFromLocalPath(
|
|
373
|
+
localPath: string,
|
|
374
|
+
): Promise<{ buffer: Buffer; mimeType: string; source: string }> {
|
|
322
375
|
const normalizedPath = normalizeVisionLocalPath(localPath);
|
|
323
376
|
if (!normalizedPath) {
|
|
324
|
-
throw new Error(
|
|
377
|
+
throw new Error(
|
|
378
|
+
'local image path must be under /workspace or /discord-media-cache',
|
|
379
|
+
);
|
|
325
380
|
}
|
|
326
|
-
if (
|
|
327
|
-
|
|
381
|
+
if (
|
|
382
|
+
normalizedPath.startsWith(`${DISCORD_MEDIA_CACHE_ROOT}/`) &&
|
|
383
|
+
!isKnownDiscordMediaPath(normalizedPath)
|
|
384
|
+
) {
|
|
385
|
+
throw new Error(
|
|
386
|
+
'requested local image is not part of current media context',
|
|
387
|
+
);
|
|
328
388
|
}
|
|
329
389
|
if (!fs.existsSync(normalizedPath)) {
|
|
330
390
|
throw new Error(`local image not found: ${normalizedPath}`);
|
|
@@ -337,14 +397,21 @@ async function readVisionImageFromLocalPath(localPath: string): Promise<{ buffer
|
|
|
337
397
|
throw new Error(`local image is empty: ${normalizedPath}`);
|
|
338
398
|
}
|
|
339
399
|
if (stat.size > VISION_IMAGE_MAX_BYTES) {
|
|
340
|
-
throw new Error(
|
|
400
|
+
throw new Error(
|
|
401
|
+
`local image exceeds max size (${VISION_IMAGE_MAX_BYTES} bytes)`,
|
|
402
|
+
);
|
|
341
403
|
}
|
|
342
404
|
const buffer = fs.readFileSync(normalizedPath);
|
|
343
405
|
const mediaHint = currentMediaContext.find((entry) => {
|
|
344
|
-
const normalizedEntryPath = entry.path
|
|
406
|
+
const normalizedEntryPath = entry.path
|
|
407
|
+
? normalizeVisionLocalPath(entry.path)
|
|
408
|
+
: null;
|
|
345
409
|
return normalizedEntryPath === normalizedPath;
|
|
346
410
|
});
|
|
347
|
-
const mimeType = inferImageMimeTypeFromPath(
|
|
411
|
+
const mimeType = inferImageMimeTypeFromPath(
|
|
412
|
+
normalizedPath,
|
|
413
|
+
mediaHint?.mimeType,
|
|
414
|
+
);
|
|
348
415
|
if (!mimeType.startsWith('image/')) {
|
|
349
416
|
throw new Error(`unsupported local image type: ${mimeType}`);
|
|
350
417
|
}
|
|
@@ -355,9 +422,13 @@ async function readVisionImageFromLocalPath(localPath: string): Promise<{ buffer
|
|
|
355
422
|
};
|
|
356
423
|
}
|
|
357
424
|
|
|
358
|
-
async function readVisionImageFromUrl(
|
|
425
|
+
async function readVisionImageFromUrl(
|
|
426
|
+
rawUrl: string,
|
|
427
|
+
): Promise<{ buffer: Buffer; mimeType: string; source: string }> {
|
|
359
428
|
if (!isSafeDiscordCdnUrl(rawUrl)) {
|
|
360
|
-
throw new Error(
|
|
429
|
+
throw new Error(
|
|
430
|
+
'remote image URL is blocked (only Discord CDN HTTPS URLs are allowed)',
|
|
431
|
+
);
|
|
361
432
|
}
|
|
362
433
|
const controller = new AbortController();
|
|
363
434
|
const timer = setTimeout(() => controller.abort(), VISION_FETCH_TIMEOUT_MS);
|
|
@@ -373,13 +444,23 @@ async function readVisionImageFromUrl(rawUrl: string): Promise<{ buffer: Buffer;
|
|
|
373
444
|
if (!mimeType.startsWith('image/')) {
|
|
374
445
|
throw new Error(`remote URL is not an image (${mimeType || 'unknown'})`);
|
|
375
446
|
}
|
|
376
|
-
const contentLength = Number.parseInt(
|
|
377
|
-
|
|
378
|
-
|
|
447
|
+
const contentLength = Number.parseInt(
|
|
448
|
+
response.headers.get('content-length') || '',
|
|
449
|
+
10,
|
|
450
|
+
);
|
|
451
|
+
if (
|
|
452
|
+
Number.isFinite(contentLength) &&
|
|
453
|
+
contentLength > VISION_IMAGE_MAX_BYTES
|
|
454
|
+
) {
|
|
455
|
+
throw new Error(
|
|
456
|
+
`remote image exceeds max size (${VISION_IMAGE_MAX_BYTES} bytes)`,
|
|
457
|
+
);
|
|
379
458
|
}
|
|
380
459
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
381
460
|
if (buffer.length > VISION_IMAGE_MAX_BYTES) {
|
|
382
|
-
throw new Error(
|
|
461
|
+
throw new Error(
|
|
462
|
+
`remote image exceeds max size (${VISION_IMAGE_MAX_BYTES} bytes)`,
|
|
463
|
+
);
|
|
383
464
|
}
|
|
384
465
|
return {
|
|
385
466
|
buffer,
|
|
@@ -392,14 +473,21 @@ async function readVisionImageFromUrl(rawUrl: string): Promise<{ buffer: Buffer;
|
|
|
392
473
|
}
|
|
393
474
|
|
|
394
475
|
function visionModelContextError(): string | null {
|
|
395
|
-
if (!currentModelApiKey)
|
|
396
|
-
|
|
397
|
-
if (!
|
|
398
|
-
|
|
476
|
+
if (!currentModelApiKey)
|
|
477
|
+
return 'vision_analyze is not configured: missing API key context.';
|
|
478
|
+
if (!currentModelBaseUrl)
|
|
479
|
+
return 'vision_analyze is not configured: missing base URL context.';
|
|
480
|
+
if (!currentModelName)
|
|
481
|
+
return 'vision_analyze is not configured: missing model context.';
|
|
482
|
+
if (!currentChatbotId)
|
|
483
|
+
return 'vision_analyze is not configured: missing chatbot_id context.';
|
|
399
484
|
return null;
|
|
400
485
|
}
|
|
401
486
|
|
|
402
|
-
async function callVisionModel(
|
|
487
|
+
async function callVisionModel(
|
|
488
|
+
question: string,
|
|
489
|
+
imageDataUrl: string,
|
|
490
|
+
): Promise<{ model: string; analysis: string }> {
|
|
403
491
|
const contextError = visionModelContextError();
|
|
404
492
|
if (contextError) throw new Error(contextError);
|
|
405
493
|
|
|
@@ -427,8 +515,11 @@ async function callVisionModel(question: string, imageDataUrl: string): Promise<
|
|
|
427
515
|
|
|
428
516
|
const rawText = await response.text();
|
|
429
517
|
if (!response.ok) {
|
|
430
|
-
const detail =
|
|
431
|
-
|
|
518
|
+
const detail =
|
|
519
|
+
rawText.length > 600 ? `${rawText.slice(0, 600)}...` : rawText;
|
|
520
|
+
throw new Error(
|
|
521
|
+
`vision API request failed (${response.status}): ${detail}`,
|
|
522
|
+
);
|
|
432
523
|
}
|
|
433
524
|
|
|
434
525
|
let parsed: unknown;
|
|
@@ -454,15 +545,25 @@ async function callVisionModel(question: string, imageDataUrl: string): Promise<
|
|
|
454
545
|
};
|
|
455
546
|
}
|
|
456
547
|
|
|
457
|
-
async function runVisionAnalyze(
|
|
548
|
+
async function runVisionAnalyze(
|
|
549
|
+
args: Record<string, unknown>,
|
|
550
|
+
): Promise<string> {
|
|
458
551
|
const question = readStringValue(args.question);
|
|
459
552
|
if (!question) return 'Error: question is required';
|
|
460
553
|
|
|
461
|
-
const imageRef =
|
|
462
|
-
|
|
554
|
+
const imageRef =
|
|
555
|
+
readStringValue(args.image_url) ||
|
|
556
|
+
readStringValue(args.imageUrl) ||
|
|
557
|
+
readStringValue(args.path);
|
|
558
|
+
const fallbackUrl =
|
|
559
|
+
readStringValue(args.fallback_url) ||
|
|
560
|
+
readStringValue(args.fallbackUrl) ||
|
|
561
|
+
readStringValue(args.original_url);
|
|
463
562
|
if (!imageRef) return 'Error: image_url is required';
|
|
464
563
|
|
|
465
|
-
const candidates = [imageRef, fallbackUrl].filter((value): value is string =>
|
|
564
|
+
const candidates = [imageRef, fallbackUrl].filter((value): value is string =>
|
|
565
|
+
Boolean(value),
|
|
566
|
+
);
|
|
466
567
|
const errors: string[] = [];
|
|
467
568
|
for (const candidate of candidates) {
|
|
468
569
|
try {
|
|
@@ -472,14 +573,18 @@ async function runVisionAnalyze(args: Record<string, unknown>): Promise<string>
|
|
|
472
573
|
: await readVisionImageFromLocalPath(candidate);
|
|
473
574
|
const dataUrl = `data:${image.mimeType};base64,${image.buffer.toString('base64')}`;
|
|
474
575
|
const vision = await callVisionModel(question, dataUrl);
|
|
475
|
-
return JSON.stringify(
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
576
|
+
return JSON.stringify(
|
|
577
|
+
{
|
|
578
|
+
success: true,
|
|
579
|
+
model: vision.model,
|
|
580
|
+
analysis: vision.analysis,
|
|
581
|
+
source: image.source,
|
|
582
|
+
mime_type: image.mimeType,
|
|
583
|
+
size_bytes: image.buffer.length,
|
|
584
|
+
},
|
|
585
|
+
null,
|
|
586
|
+
2,
|
|
587
|
+
);
|
|
483
588
|
} catch (err) {
|
|
484
589
|
const detail = err instanceof Error ? err.message : String(err);
|
|
485
590
|
errors.push(`${candidate}: ${detail}`);
|
|
@@ -502,11 +607,17 @@ const READ_MAX_BYTES = 50 * 1024;
|
|
|
502
607
|
|
|
503
608
|
function abbreviatePreview(text: string): string {
|
|
504
609
|
const lines = text.split('\n');
|
|
505
|
-
const truncated = lines
|
|
506
|
-
|
|
507
|
-
|
|
610
|
+
const truncated = lines
|
|
611
|
+
.slice(0, PREVIEW_MAX_OUTPUT_LINES)
|
|
612
|
+
.map((line) =>
|
|
613
|
+
line.length > PREVIEW_MAX_LINE_LENGTH
|
|
614
|
+
? `${line.slice(0, PREVIEW_MAX_LINE_LENGTH)}...`
|
|
615
|
+
: line,
|
|
616
|
+
);
|
|
508
617
|
if (lines.length > PREVIEW_MAX_OUTPUT_LINES) {
|
|
509
|
-
truncated.push(
|
|
618
|
+
truncated.push(
|
|
619
|
+
`... (${lines.length - PREVIEW_MAX_OUTPUT_LINES} more lines)`,
|
|
620
|
+
);
|
|
510
621
|
}
|
|
511
622
|
return truncated.join('\n');
|
|
512
623
|
}
|
|
@@ -525,7 +636,11 @@ function formatBytes(bytes: number): string {
|
|
|
525
636
|
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
526
637
|
}
|
|
527
638
|
|
|
528
|
-
function truncateReadContent(
|
|
639
|
+
function truncateReadContent(
|
|
640
|
+
content: string,
|
|
641
|
+
maxLines = READ_MAX_LINES,
|
|
642
|
+
maxBytes = READ_MAX_BYTES,
|
|
643
|
+
): ReadTruncationResult {
|
|
529
644
|
const lines = content.split('\n');
|
|
530
645
|
const totalBytes = Buffer.byteLength(content, 'utf-8');
|
|
531
646
|
if (lines.length <= maxLines && totalBytes <= maxBytes) {
|
|
@@ -576,7 +691,11 @@ function truncateReadContent(content: string, maxLines = READ_MAX_LINES, maxByte
|
|
|
576
691
|
function formatBashOutput(content: string): string {
|
|
577
692
|
const raw = content || '(no output)';
|
|
578
693
|
const totalLines = raw.split('\n').length;
|
|
579
|
-
const truncation = truncateReadContent(
|
|
694
|
+
const truncation = truncateReadContent(
|
|
695
|
+
raw,
|
|
696
|
+
BASH_MAX_OUTPUT_LINES,
|
|
697
|
+
BASH_MAX_OUTPUT_BYTES,
|
|
698
|
+
);
|
|
580
699
|
if (!truncation.truncated) return raw;
|
|
581
700
|
|
|
582
701
|
if (truncation.firstLineExceedsLimit) {
|
|
@@ -675,11 +794,13 @@ function resolveMemoryFilePath(args: Record<string, unknown>): string | null {
|
|
|
675
794
|
normalizeMemoryFilePath(args.path);
|
|
676
795
|
if (direct) return direct;
|
|
677
796
|
|
|
678
|
-
const target =
|
|
797
|
+
const target =
|
|
798
|
+
typeof args.target === 'string' ? args.target.trim().toLowerCase() : '';
|
|
679
799
|
if (target === 'memory') return 'MEMORY.md';
|
|
680
800
|
if (target === 'user') return 'USER.md';
|
|
681
801
|
if (target === 'daily') {
|
|
682
|
-
const date =
|
|
802
|
+
const date =
|
|
803
|
+
typeof args.date === 'string' ? normalizeDateStamp(args.date) : null;
|
|
683
804
|
return `memory/${date || currentDateStamp()}.md`;
|
|
684
805
|
}
|
|
685
806
|
|
|
@@ -773,12 +894,14 @@ function collectTranscriptRows(filePath: string): TranscriptRow[] {
|
|
|
773
894
|
}
|
|
774
895
|
parsed.push({
|
|
775
896
|
sessionId: row.sessionId,
|
|
776
|
-
channelId:
|
|
897
|
+
channelId:
|
|
898
|
+
typeof row.channelId === 'string' ? row.channelId : undefined,
|
|
777
899
|
role: row.role,
|
|
778
900
|
userId: typeof row.userId === 'string' ? row.userId : undefined,
|
|
779
901
|
username: row.username == null ? null : String(row.username),
|
|
780
902
|
content: row.content,
|
|
781
|
-
createdAt:
|
|
903
|
+
createdAt:
|
|
904
|
+
typeof row.createdAt === 'string' ? row.createdAt : undefined,
|
|
782
905
|
});
|
|
783
906
|
} catch {
|
|
784
907
|
// Skip malformed row
|
|
@@ -787,7 +910,11 @@ function collectTranscriptRows(filePath: string): TranscriptRow[] {
|
|
|
787
910
|
return parsed;
|
|
788
911
|
}
|
|
789
912
|
|
|
790
|
-
function scoreTranscript(
|
|
913
|
+
function scoreTranscript(
|
|
914
|
+
rows: TranscriptRow[],
|
|
915
|
+
query: string,
|
|
916
|
+
roleFilter: Set<string> | null,
|
|
917
|
+
): number {
|
|
791
918
|
const terms = query
|
|
792
919
|
.toLowerCase()
|
|
793
920
|
.split(/\s+/)
|
|
@@ -808,7 +935,11 @@ function scoreTranscript(rows: TranscriptRow[], query: string, roleFilter: Set<s
|
|
|
808
935
|
return score;
|
|
809
936
|
}
|
|
810
937
|
|
|
811
|
-
function findMatchIndexes(
|
|
938
|
+
function findMatchIndexes(
|
|
939
|
+
rows: TranscriptRow[],
|
|
940
|
+
query: string,
|
|
941
|
+
roleFilter: Set<string> | null,
|
|
942
|
+
): number[] {
|
|
812
943
|
const lower = query.toLowerCase();
|
|
813
944
|
const terms = lower
|
|
814
945
|
.split(/\s+/)
|
|
@@ -821,14 +952,20 @@ function findMatchIndexes(rows: TranscriptRow[], query: string, roleFilter: Set<
|
|
|
821
952
|
const role = rows[i].role.toLowerCase();
|
|
822
953
|
if (roleFilter && !roleFilter.has(role)) continue;
|
|
823
954
|
const content = rows[i].content.toLowerCase();
|
|
824
|
-
if (
|
|
955
|
+
if (
|
|
956
|
+
content.includes(lower) ||
|
|
957
|
+
terms.some((term) => content.includes(term))
|
|
958
|
+
) {
|
|
825
959
|
indexes.push(i);
|
|
826
960
|
}
|
|
827
961
|
}
|
|
828
962
|
return indexes;
|
|
829
963
|
}
|
|
830
964
|
|
|
831
|
-
function summarizeSessionCandidate(
|
|
965
|
+
function summarizeSessionCandidate(
|
|
966
|
+
candidate: SessionSearchCandidate,
|
|
967
|
+
query: string,
|
|
968
|
+
): Record<string, unknown> {
|
|
832
969
|
const rows = candidate.rows;
|
|
833
970
|
const snippets: string[] = [];
|
|
834
971
|
const seenIndexes = new Set<number>();
|
|
@@ -858,12 +995,17 @@ function summarizeSessionCandidate(candidate: SessionSearchCandidate, query: str
|
|
|
858
995
|
}
|
|
859
996
|
}
|
|
860
997
|
|
|
861
|
-
const firstTs =
|
|
862
|
-
|
|
998
|
+
const firstTs =
|
|
999
|
+
rows.find((row) => typeof row.createdAt === 'string')?.createdAt || null;
|
|
1000
|
+
const lastTs =
|
|
1001
|
+
[...rows].reverse().find((row) => typeof row.createdAt === 'string')
|
|
1002
|
+
?.createdAt || null;
|
|
863
1003
|
const summaryParts = [
|
|
864
1004
|
`Matched ${candidate.matchIndexes.length} turn(s) for "${query}".`,
|
|
865
1005
|
userMatches.length > 0 ? `User focus: ${userMatches.join(' | ')}` : '',
|
|
866
|
-
assistantMatches.length > 0
|
|
1006
|
+
assistantMatches.length > 0
|
|
1007
|
+
? `Assistant outcomes: ${assistantMatches.join(' | ')}`
|
|
1008
|
+
: '',
|
|
867
1009
|
].filter(Boolean);
|
|
868
1010
|
|
|
869
1011
|
return {
|
|
@@ -876,7 +1018,10 @@ function summarizeSessionCandidate(candidate: SessionSearchCandidate, query: str
|
|
|
876
1018
|
};
|
|
877
1019
|
}
|
|
878
1020
|
|
|
879
|
-
export async function executeTool(
|
|
1021
|
+
export async function executeTool(
|
|
1022
|
+
name: string,
|
|
1023
|
+
argsJson: string,
|
|
1024
|
+
): Promise<string> {
|
|
880
1025
|
try {
|
|
881
1026
|
const args = JSON.parse(argsJson);
|
|
882
1027
|
|
|
@@ -886,19 +1031,25 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
|
|
|
886
1031
|
return 'Error: path is required';
|
|
887
1032
|
}
|
|
888
1033
|
const filePath = safeJoin(args.path);
|
|
889
|
-
if (!fs.existsSync(filePath))
|
|
1034
|
+
if (!fs.existsSync(filePath))
|
|
1035
|
+
return `Error: File not found: ${args.path}`;
|
|
890
1036
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
891
1037
|
const lines = content.split('\n');
|
|
892
1038
|
const totalFileLines = lines.length;
|
|
893
1039
|
|
|
894
|
-
const rawOffset =
|
|
1040
|
+
const rawOffset =
|
|
1041
|
+
typeof args.offset === 'number' && Number.isFinite(args.offset)
|
|
1042
|
+
? args.offset
|
|
1043
|
+
: 1;
|
|
895
1044
|
const startLine = Math.max(1, Math.floor(rawOffset));
|
|
896
1045
|
if (startLine > totalFileLines) {
|
|
897
1046
|
return `Error: Offset ${startLine} is beyond end of file (${totalFileLines} lines total)`;
|
|
898
1047
|
}
|
|
899
1048
|
|
|
900
1049
|
const rawLimit =
|
|
901
|
-
typeof args.limit === 'number' &&
|
|
1050
|
+
typeof args.limit === 'number' &&
|
|
1051
|
+
Number.isFinite(args.limit) &&
|
|
1052
|
+
args.limit > 0
|
|
902
1053
|
? Math.floor(args.limit)
|
|
903
1054
|
: undefined;
|
|
904
1055
|
|
|
@@ -913,7 +1064,9 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
|
|
|
913
1064
|
const truncation = truncateReadContent(selectedContent);
|
|
914
1065
|
if (truncation.firstLineExceedsLimit) {
|
|
915
1066
|
const firstSelectedLine = selected[0] ?? '';
|
|
916
|
-
const firstLineSize = formatBytes(
|
|
1067
|
+
const firstLineSize = formatBytes(
|
|
1068
|
+
Buffer.byteLength(firstSelectedLine, 'utf-8'),
|
|
1069
|
+
);
|
|
917
1070
|
return `[Line ${startLine} is ${firstLineSize}, exceeds ${formatBytes(READ_MAX_BYTES)} limit. Use bash: sed -n '${startLine}p' ${args.path} | head -c ${READ_MAX_BYTES}]`;
|
|
918
1071
|
}
|
|
919
1072
|
|
|
@@ -947,7 +1100,8 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
|
|
|
947
1100
|
|
|
948
1101
|
case 'edit': {
|
|
949
1102
|
const filePath = safeJoin(args.path);
|
|
950
|
-
if (!fs.existsSync(filePath))
|
|
1103
|
+
if (!fs.existsSync(filePath))
|
|
1104
|
+
return `Error: File not found: ${args.path}`;
|
|
951
1105
|
let content = fs.readFileSync(filePath, 'utf-8');
|
|
952
1106
|
const count = args.count || 1;
|
|
953
1107
|
for (let i = 0; i < count; i++) {
|
|
@@ -956,7 +1110,10 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
|
|
|
956
1110
|
if (i === 0) return `Error: Text not found in ${args.path}`;
|
|
957
1111
|
break;
|
|
958
1112
|
}
|
|
959
|
-
content =
|
|
1113
|
+
content =
|
|
1114
|
+
content.slice(0, idx) +
|
|
1115
|
+
args.new +
|
|
1116
|
+
content.slice(idx + args.old.length);
|
|
960
1117
|
}
|
|
961
1118
|
fs.writeFileSync(filePath, content);
|
|
962
1119
|
return `Edited ${args.path} (${count} replacement${count > 1 ? 's' : ''})`;
|
|
@@ -964,7 +1121,8 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
|
|
|
964
1121
|
|
|
965
1122
|
case 'delete': {
|
|
966
1123
|
const filePath = safeJoin(args.path);
|
|
967
|
-
if (!fs.existsSync(filePath))
|
|
1124
|
+
if (!fs.existsSync(filePath))
|
|
1125
|
+
return `Error: File not found: ${args.path}`;
|
|
968
1126
|
fs.unlinkSync(filePath);
|
|
969
1127
|
return `Deleted ${args.path}`;
|
|
970
1128
|
}
|
|
@@ -977,7 +1135,10 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
|
|
|
977
1135
|
const result = execSync(cmd, { timeout: 10000, encoding: 'utf-8' });
|
|
978
1136
|
if (!result.trim()) return 'No files found.';
|
|
979
1137
|
// Convert absolute paths to relative
|
|
980
|
-
const files = result
|
|
1138
|
+
const files = result
|
|
1139
|
+
.trim()
|
|
1140
|
+
.split('\n')
|
|
1141
|
+
.map((f) => f.replace('/workspace/', ''));
|
|
981
1142
|
return abbreviatePreview(files.join('\n'));
|
|
982
1143
|
} catch {
|
|
983
1144
|
return 'No files found.';
|
|
@@ -1022,23 +1183,28 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
|
|
|
1022
1183
|
message?: string;
|
|
1023
1184
|
};
|
|
1024
1185
|
|
|
1025
|
-
const stdout =
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
? execErr.stderr
|
|
1034
|
-
:
|
|
1035
|
-
|
|
1186
|
+
const stdout =
|
|
1187
|
+
typeof execErr.stdout === 'string'
|
|
1188
|
+
? execErr.stdout
|
|
1189
|
+
: Buffer.isBuffer(execErr.stdout)
|
|
1190
|
+
? execErr.stdout.toString('utf-8')
|
|
1191
|
+
: '';
|
|
1192
|
+
const stderr =
|
|
1193
|
+
typeof execErr.stderr === 'string'
|
|
1194
|
+
? execErr.stderr
|
|
1195
|
+
: Buffer.isBuffer(execErr.stderr)
|
|
1196
|
+
? execErr.stderr.toString('utf-8')
|
|
1197
|
+
: '';
|
|
1198
|
+
const combinedOutput = [stdout, stderr]
|
|
1199
|
+
.filter(Boolean)
|
|
1200
|
+
.join('\n')
|
|
1201
|
+
.trim();
|
|
1036
1202
|
|
|
1037
1203
|
const errorMessage = execErr.message || 'Command failed';
|
|
1038
1204
|
const timeoutLikely =
|
|
1039
|
-
execErr.code === 'ETIMEDOUT'
|
|
1040
|
-
|
|
1041
|
-
|
|
1205
|
+
execErr.code === 'ETIMEDOUT' ||
|
|
1206
|
+
/ETIMEDOUT|timed out/i.test(errorMessage) ||
|
|
1207
|
+
(execErr.signal === 'SIGTERM' && /spawnSync/i.test(errorMessage));
|
|
1042
1208
|
const summary = timeoutLikely
|
|
1043
1209
|
? `Command timed out after ${timeoutMs}ms`
|
|
1044
1210
|
: errorMessage;
|
|
@@ -1049,7 +1215,10 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
|
|
|
1049
1215
|
}
|
|
1050
1216
|
|
|
1051
1217
|
case 'memory': {
|
|
1052
|
-
const action =
|
|
1218
|
+
const action =
|
|
1219
|
+
typeof args.action === 'string'
|
|
1220
|
+
? args.action.trim().toLowerCase()
|
|
1221
|
+
: 'read';
|
|
1053
1222
|
const relativePath = resolveMemoryFilePath(args);
|
|
1054
1223
|
if (!relativePath) {
|
|
1055
1224
|
return 'Error: memory file_path must be MEMORY.md, USER.md, or memory/YYYY-MM-DD.md';
|
|
@@ -1065,7 +1234,10 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
|
|
|
1065
1234
|
}
|
|
1066
1235
|
|
|
1067
1236
|
if (action === 'search') {
|
|
1068
|
-
const query =
|
|
1237
|
+
const query =
|
|
1238
|
+
typeof args.query === 'string'
|
|
1239
|
+
? args.query.trim().toLowerCase()
|
|
1240
|
+
: '';
|
|
1069
1241
|
if (!query) return 'Error: query is required for memory search';
|
|
1070
1242
|
const files = listMemoryFiles();
|
|
1071
1243
|
const matches: string[] = [];
|
|
@@ -1085,7 +1257,9 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
|
|
|
1085
1257
|
}
|
|
1086
1258
|
if (matches.length >= 40) break;
|
|
1087
1259
|
}
|
|
1088
|
-
return matches.length > 0
|
|
1260
|
+
return matches.length > 0
|
|
1261
|
+
? matches.join('\n')
|
|
1262
|
+
: `No memory matches for "${query}".`;
|
|
1089
1263
|
}
|
|
1090
1264
|
|
|
1091
1265
|
if (action === 'read') {
|
|
@@ -1097,11 +1271,14 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
|
|
|
1097
1271
|
}
|
|
1098
1272
|
|
|
1099
1273
|
if (action === 'append') {
|
|
1100
|
-
const content =
|
|
1274
|
+
const content =
|
|
1275
|
+
typeof args.content === 'string' ? args.content.trim() : '';
|
|
1101
1276
|
if (!content) return 'Error: content is required for memory append';
|
|
1102
1277
|
|
|
1103
1278
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
1104
|
-
const existing = fs.existsSync(filePath)
|
|
1279
|
+
const existing = fs.existsSync(filePath)
|
|
1280
|
+
? fs.readFileSync(filePath, 'utf-8')
|
|
1281
|
+
: '';
|
|
1105
1282
|
let next = existing.replace(/\s+$/, '');
|
|
1106
1283
|
if (next.length > 0) next += '\n\n';
|
|
1107
1284
|
next += `${content}\n`;
|
|
@@ -1125,12 +1302,16 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
|
|
|
1125
1302
|
}
|
|
1126
1303
|
|
|
1127
1304
|
if (action === 'replace') {
|
|
1128
|
-
const oldText =
|
|
1129
|
-
|
|
1305
|
+
const oldText =
|
|
1306
|
+
typeof args.old_text === 'string' ? args.old_text : '';
|
|
1307
|
+
const newText =
|
|
1308
|
+
typeof args.new_text === 'string' ? args.new_text : '';
|
|
1130
1309
|
if (!oldText) return 'Error: old_text is required for memory replace';
|
|
1131
|
-
if (!fs.existsSync(filePath))
|
|
1310
|
+
if (!fs.existsSync(filePath))
|
|
1311
|
+
return `Error: File not found: ${relativePath}`;
|
|
1132
1312
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
1133
|
-
if (!content.includes(oldText))
|
|
1313
|
+
if (!content.includes(oldText))
|
|
1314
|
+
return `Error: old_text not found in ${relativePath}`;
|
|
1134
1315
|
const next = content.replace(oldText, newText);
|
|
1135
1316
|
const limit = memoryCharLimit(relativePath);
|
|
1136
1317
|
if (next.length > limit) {
|
|
@@ -1141,11 +1322,14 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
|
|
|
1141
1322
|
}
|
|
1142
1323
|
|
|
1143
1324
|
if (action === 'remove') {
|
|
1144
|
-
const oldText =
|
|
1325
|
+
const oldText =
|
|
1326
|
+
typeof args.old_text === 'string' ? args.old_text : '';
|
|
1145
1327
|
if (!oldText) return 'Error: old_text is required for memory remove';
|
|
1146
|
-
if (!fs.existsSync(filePath))
|
|
1328
|
+
if (!fs.existsSync(filePath))
|
|
1329
|
+
return `Error: File not found: ${relativePath}`;
|
|
1147
1330
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
1148
|
-
if (!content.includes(oldText))
|
|
1331
|
+
if (!content.includes(oldText))
|
|
1332
|
+
return `Error: old_text not found in ${relativePath}`;
|
|
1149
1333
|
fs.writeFileSync(filePath, content.replace(oldText, ''), 'utf-8');
|
|
1150
1334
|
return `Removed matching text from ${relativePath}`;
|
|
1151
1335
|
}
|
|
@@ -1163,10 +1347,10 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
|
|
|
1163
1347
|
|
|
1164
1348
|
if (action === 'read') {
|
|
1165
1349
|
const channelId =
|
|
1166
|
-
readStringValue(args.channelId)
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1350
|
+
readStringValue(args.channelId) ||
|
|
1351
|
+
readStringValue(args.to) ||
|
|
1352
|
+
readStringValue(args.target) ||
|
|
1353
|
+
gatewayChannelId;
|
|
1170
1354
|
if (!channelId) {
|
|
1171
1355
|
return 'Error: channelId is required for message action "read".';
|
|
1172
1356
|
}
|
|
@@ -1186,10 +1370,10 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
|
|
|
1186
1370
|
if (action === 'member-info') {
|
|
1187
1371
|
const guildId = readStringValue(args.guildId);
|
|
1188
1372
|
const userId =
|
|
1189
|
-
readStringValue(args.userId)
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1373
|
+
readStringValue(args.userId) ||
|
|
1374
|
+
readStringValue(args.memberId) ||
|
|
1375
|
+
readStringValue(args.user) ||
|
|
1376
|
+
readStringValue(args.username);
|
|
1193
1377
|
if (!guildId) {
|
|
1194
1378
|
return 'Error: guildId is required for message action "member-info".';
|
|
1195
1379
|
}
|
|
@@ -1202,10 +1386,10 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
|
|
|
1202
1386
|
|
|
1203
1387
|
if (action === 'channel-info') {
|
|
1204
1388
|
const channelId =
|
|
1205
|
-
readStringValue(args.channelId)
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1389
|
+
readStringValue(args.channelId) ||
|
|
1390
|
+
readStringValue(args.to) ||
|
|
1391
|
+
readStringValue(args.target) ||
|
|
1392
|
+
gatewayChannelId;
|
|
1209
1393
|
if (!channelId) {
|
|
1210
1394
|
return 'Error: channelId is required for message action "channel-info".';
|
|
1211
1395
|
}
|
|
@@ -1223,19 +1407,26 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
|
|
|
1223
1407
|
typeof args.limit === 'number' && Number.isFinite(args.limit)
|
|
1224
1408
|
? Math.floor(args.limit)
|
|
1225
1409
|
: 3;
|
|
1226
|
-
const limit = Math.max(
|
|
1410
|
+
const limit = Math.max(
|
|
1411
|
+
1,
|
|
1412
|
+
Math.min(requestedLimit, SESSION_SEARCH_MAX_RESULTS),
|
|
1413
|
+
);
|
|
1227
1414
|
const includeCurrent = args.include_current === true;
|
|
1228
1415
|
const roleFilter = parseRoleFilter(args.role_filter);
|
|
1229
1416
|
|
|
1230
1417
|
const transcriptDir = safeJoin(SESSION_TRANSCRIPTS_DIR);
|
|
1231
1418
|
if (!fs.existsSync(transcriptDir)) {
|
|
1232
|
-
return JSON.stringify(
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1419
|
+
return JSON.stringify(
|
|
1420
|
+
{
|
|
1421
|
+
success: true,
|
|
1422
|
+
query,
|
|
1423
|
+
count: 0,
|
|
1424
|
+
results: [],
|
|
1425
|
+
message: 'No historical transcripts found yet.',
|
|
1426
|
+
},
|
|
1427
|
+
null,
|
|
1428
|
+
2,
|
|
1429
|
+
);
|
|
1239
1430
|
}
|
|
1240
1431
|
|
|
1241
1432
|
const files = fs
|
|
@@ -1249,8 +1440,14 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
|
|
|
1249
1440
|
const rows = collectTranscriptRows(filePath);
|
|
1250
1441
|
if (rows.length === 0) continue;
|
|
1251
1442
|
|
|
1252
|
-
const sessionId =
|
|
1253
|
-
|
|
1443
|
+
const sessionId =
|
|
1444
|
+
rows[0].sessionId || filename.replace(/\.jsonl$/, '');
|
|
1445
|
+
if (
|
|
1446
|
+
!includeCurrent &&
|
|
1447
|
+
currentSessionId &&
|
|
1448
|
+
sessionId === currentSessionId
|
|
1449
|
+
)
|
|
1450
|
+
continue;
|
|
1254
1451
|
|
|
1255
1452
|
const matchIndexes = findMatchIndexes(rows, query, roleFilter);
|
|
1256
1453
|
if (matchIndexes.length === 0) continue;
|
|
@@ -1273,15 +1470,21 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
|
|
|
1273
1470
|
});
|
|
1274
1471
|
|
|
1275
1472
|
const top = candidates.slice(0, limit);
|
|
1276
|
-
const results = top.map((candidate) =>
|
|
1473
|
+
const results = top.map((candidate) =>
|
|
1474
|
+
summarizeSessionCandidate(candidate, query),
|
|
1475
|
+
);
|
|
1277
1476
|
|
|
1278
|
-
return JSON.stringify(
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1477
|
+
return JSON.stringify(
|
|
1478
|
+
{
|
|
1479
|
+
success: true,
|
|
1480
|
+
query,
|
|
1481
|
+
count: results.length,
|
|
1482
|
+
sessions_searched: candidates.length,
|
|
1483
|
+
results,
|
|
1484
|
+
},
|
|
1485
|
+
null,
|
|
1486
|
+
2,
|
|
1487
|
+
);
|
|
1285
1488
|
}
|
|
1286
1489
|
|
|
1287
1490
|
case 'web_fetch': {
|
|
@@ -1294,7 +1497,9 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
|
|
|
1294
1497
|
const meta = `[${result.extractor}] ${result.finalUrl} (${result.status}, ${result.tookMs}ms)`;
|
|
1295
1498
|
const lines = [meta];
|
|
1296
1499
|
if (result.escalationHint) {
|
|
1297
|
-
lines.push(
|
|
1500
|
+
lines.push(
|
|
1501
|
+
`Escalation hint: ${result.escalationHint} (retry with browser_navigate for this URL).`,
|
|
1502
|
+
);
|
|
1298
1503
|
}
|
|
1299
1504
|
if (result.warning) {
|
|
1300
1505
|
lines.push(`Warning: ${result.warning}`);
|
|
@@ -1311,6 +1516,7 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
|
|
|
1311
1516
|
case 'browser_snapshot':
|
|
1312
1517
|
case 'browser_click':
|
|
1313
1518
|
case 'browser_type':
|
|
1519
|
+
case 'browser_upload':
|
|
1314
1520
|
case 'browser_press':
|
|
1315
1521
|
case 'browser_scroll':
|
|
1316
1522
|
case 'browser_back':
|
|
@@ -1321,7 +1527,11 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
|
|
|
1321
1527
|
case 'browser_console':
|
|
1322
1528
|
case 'browser_network':
|
|
1323
1529
|
case 'browser_close': {
|
|
1324
|
-
return await executeBrowserTool(
|
|
1530
|
+
return await executeBrowserTool(
|
|
1531
|
+
name,
|
|
1532
|
+
args,
|
|
1533
|
+
currentSessionId || 'default',
|
|
1534
|
+
);
|
|
1325
1535
|
}
|
|
1326
1536
|
|
|
1327
1537
|
case 'cron': {
|
|
@@ -1335,7 +1545,8 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
|
|
|
1335
1545
|
else if (t.everyMs) {
|
|
1336
1546
|
const secs = t.everyMs / 1000;
|
|
1337
1547
|
if (secs < 120) schedule = `every ${secs}s`;
|
|
1338
|
-
else if (secs < 7200)
|
|
1548
|
+
else if (secs < 7200)
|
|
1549
|
+
schedule = `every ${Math.round(secs / 60)}m`;
|
|
1339
1550
|
else schedule = `every ${Math.round(secs / 3600)}h`;
|
|
1340
1551
|
} else schedule = t.cronExpr;
|
|
1341
1552
|
const status = t.enabled ? 'enabled' : 'disabled';
|
|
@@ -1349,22 +1560,37 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
|
|
|
1349
1560
|
|
|
1350
1561
|
if (args.at) {
|
|
1351
1562
|
const runAt = new Date(args.at);
|
|
1352
|
-
if (isNaN(runAt.getTime()))
|
|
1353
|
-
|
|
1354
|
-
|
|
1563
|
+
if (Number.isNaN(runAt.getTime()))
|
|
1564
|
+
return `Error: invalid ISO-8601 timestamp: ${args.at}`;
|
|
1565
|
+
if (runAt.getTime() <= Date.now())
|
|
1566
|
+
return `Error: timestamp must be in the future: ${args.at}`;
|
|
1567
|
+
pendingSchedules.push({
|
|
1568
|
+
action: 'add',
|
|
1569
|
+
runAt: runAt.toISOString(),
|
|
1570
|
+
prompt: args.prompt,
|
|
1571
|
+
});
|
|
1355
1572
|
return `Scheduled one-shot task at ${runAt.toISOString()}: ${args.prompt}`;
|
|
1356
1573
|
}
|
|
1357
1574
|
|
|
1358
1575
|
if (args.cron) {
|
|
1359
|
-
pendingSchedules.push({
|
|
1576
|
+
pendingSchedules.push({
|
|
1577
|
+
action: 'add',
|
|
1578
|
+
cronExpr: args.cron,
|
|
1579
|
+
prompt: args.prompt,
|
|
1580
|
+
});
|
|
1360
1581
|
return `Scheduled recurring task with cron "${args.cron}": ${args.prompt}`;
|
|
1361
1582
|
}
|
|
1362
1583
|
|
|
1363
1584
|
if (args.every) {
|
|
1364
1585
|
const secs = Number(args.every);
|
|
1365
|
-
if (isNaN(secs) || secs < 10)
|
|
1586
|
+
if (Number.isNaN(secs) || secs < 10)
|
|
1587
|
+
return 'Error: "every" must be a number of seconds >= 10';
|
|
1366
1588
|
const everyMs = Math.round(secs * 1000);
|
|
1367
|
-
pendingSchedules.push({
|
|
1589
|
+
pendingSchedules.push({
|
|
1590
|
+
action: 'add',
|
|
1591
|
+
everyMs,
|
|
1592
|
+
prompt: args.prompt,
|
|
1593
|
+
});
|
|
1368
1594
|
return `Scheduled interval task every ${secs}s: ${args.prompt}`;
|
|
1369
1595
|
}
|
|
1370
1596
|
|
|
@@ -1385,14 +1611,21 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
|
|
|
1385
1611
|
return `Error: delegation limit reached for this turn (${MAX_PENDING_DELEGATIONS}).`;
|
|
1386
1612
|
}
|
|
1387
1613
|
|
|
1388
|
-
const modeRaw =
|
|
1389
|
-
|
|
1614
|
+
const modeRaw =
|
|
1615
|
+
typeof args.mode === 'string' ? args.mode.trim().toLowerCase() : '';
|
|
1616
|
+
if (
|
|
1617
|
+
modeRaw &&
|
|
1618
|
+
modeRaw !== 'single' &&
|
|
1619
|
+
modeRaw !== 'parallel' &&
|
|
1620
|
+
modeRaw !== 'chain'
|
|
1621
|
+
) {
|
|
1390
1622
|
return 'Error: mode must be one of "single", "parallel", or "chain".';
|
|
1391
1623
|
}
|
|
1392
1624
|
|
|
1393
1625
|
const label = typeof args.label === 'string' ? args.label.trim() : '';
|
|
1394
1626
|
const model = typeof args.model === 'string' ? args.model.trim() : '';
|
|
1395
|
-
const prompt =
|
|
1627
|
+
const prompt =
|
|
1628
|
+
typeof args.prompt === 'string' ? args.prompt.trim() : '';
|
|
1396
1629
|
const tasksResult = normalizeDelegationTaskList({
|
|
1397
1630
|
raw: args.tasks,
|
|
1398
1631
|
fallbackModel: model || undefined,
|
|
@@ -1421,7 +1654,10 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
|
|
|
1421
1654
|
mode = 'single';
|
|
1422
1655
|
}
|
|
1423
1656
|
|
|
1424
|
-
if (
|
|
1657
|
+
if (
|
|
1658
|
+
(hasTasks ? 1 : 0) + (hasChain ? 1 : 0) + (hasPrompt ? 1 : 0) > 1 &&
|
|
1659
|
+
!modeRaw
|
|
1660
|
+
) {
|
|
1425
1661
|
return 'Error: provide one delegation mode payload: "prompt", "tasks", or "chain".';
|
|
1426
1662
|
}
|
|
1427
1663
|
|
|
@@ -1439,8 +1675,10 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
|
|
|
1439
1675
|
};
|
|
1440
1676
|
summary = label ? `${label}: ${prompt}` : prompt;
|
|
1441
1677
|
} else if (mode === 'parallel') {
|
|
1442
|
-
if (!hasTasks)
|
|
1443
|
-
|
|
1678
|
+
if (!hasTasks)
|
|
1679
|
+
return 'Error: tasks are required for mode="parallel".';
|
|
1680
|
+
if (hasPrompt || hasChain)
|
|
1681
|
+
return 'Error: mode="parallel" accepts only "tasks" plus optional label/model.';
|
|
1444
1682
|
effect = {
|
|
1445
1683
|
action: 'delegate',
|
|
1446
1684
|
mode,
|
|
@@ -1451,7 +1689,8 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
|
|
|
1451
1689
|
summary = `${tasksResult.tasks.length} parallel task(s)`;
|
|
1452
1690
|
} else {
|
|
1453
1691
|
if (!hasChain) return 'Error: chain is required for mode="chain".';
|
|
1454
|
-
if (hasPrompt || hasTasks)
|
|
1692
|
+
if (hasPrompt || hasTasks)
|
|
1693
|
+
return 'Error: mode="chain" accepts only "chain" plus optional label/model.';
|
|
1455
1694
|
effect = {
|
|
1456
1695
|
action: 'delegate',
|
|
1457
1696
|
mode,
|
|
@@ -1486,8 +1725,16 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1486
1725
|
type: 'object',
|
|
1487
1726
|
properties: {
|
|
1488
1727
|
path: { type: 'string', description: 'Path to the file to read' },
|
|
1489
|
-
offset: {
|
|
1490
|
-
|
|
1728
|
+
offset: {
|
|
1729
|
+
type: 'number',
|
|
1730
|
+
description:
|
|
1731
|
+
'Line number to start reading from (1-indexed, default: 1)',
|
|
1732
|
+
},
|
|
1733
|
+
limit: {
|
|
1734
|
+
type: 'number',
|
|
1735
|
+
description:
|
|
1736
|
+
'Maximum number of lines to read before truncation logic (optional)',
|
|
1737
|
+
},
|
|
1491
1738
|
},
|
|
1492
1739
|
required: ['path'],
|
|
1493
1740
|
},
|
|
@@ -1521,7 +1768,10 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1521
1768
|
path: { type: 'string', description: 'Path to the file to edit' },
|
|
1522
1769
|
old: { type: 'string', description: 'Text to find and replace' },
|
|
1523
1770
|
new: { type: 'string', description: 'Replacement text' },
|
|
1524
|
-
count: {
|
|
1771
|
+
count: {
|
|
1772
|
+
type: 'number',
|
|
1773
|
+
description: 'Number of replacements (default: 1)',
|
|
1774
|
+
},
|
|
1525
1775
|
},
|
|
1526
1776
|
required: ['path', 'old', 'new'],
|
|
1527
1777
|
},
|
|
@@ -1549,7 +1799,10 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1549
1799
|
parameters: {
|
|
1550
1800
|
type: 'object',
|
|
1551
1801
|
properties: {
|
|
1552
|
-
pattern: {
|
|
1802
|
+
pattern: {
|
|
1803
|
+
type: 'string',
|
|
1804
|
+
description: 'Glob pattern to match files',
|
|
1805
|
+
},
|
|
1553
1806
|
},
|
|
1554
1807
|
required: ['pattern'],
|
|
1555
1808
|
},
|
|
@@ -1563,8 +1816,15 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1563
1816
|
parameters: {
|
|
1564
1817
|
type: 'object',
|
|
1565
1818
|
properties: {
|
|
1566
|
-
pattern: {
|
|
1567
|
-
|
|
1819
|
+
pattern: {
|
|
1820
|
+
type: 'string',
|
|
1821
|
+
description: 'Regex pattern to search for',
|
|
1822
|
+
},
|
|
1823
|
+
path: {
|
|
1824
|
+
type: 'string',
|
|
1825
|
+
description:
|
|
1826
|
+
'Directory or file to search in (default: workspace root)',
|
|
1827
|
+
},
|
|
1568
1828
|
},
|
|
1569
1829
|
required: ['pattern'],
|
|
1570
1830
|
},
|
|
@@ -1580,8 +1840,16 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1580
1840
|
type: 'object',
|
|
1581
1841
|
properties: {
|
|
1582
1842
|
command: { type: 'string', description: 'Shell command to execute' },
|
|
1583
|
-
timeoutMs: {
|
|
1584
|
-
|
|
1843
|
+
timeoutMs: {
|
|
1844
|
+
type: 'number',
|
|
1845
|
+
description:
|
|
1846
|
+
'Optional command timeout in milliseconds (default 240000, max 900000)',
|
|
1847
|
+
},
|
|
1848
|
+
timeoutSeconds: {
|
|
1849
|
+
type: 'number',
|
|
1850
|
+
description:
|
|
1851
|
+
'Optional command timeout in seconds (used when timeoutMs is omitted)',
|
|
1852
|
+
},
|
|
1585
1853
|
},
|
|
1586
1854
|
required: ['command'],
|
|
1587
1855
|
},
|
|
@@ -1596,14 +1864,42 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1596
1864
|
parameters: {
|
|
1597
1865
|
type: 'object',
|
|
1598
1866
|
properties: {
|
|
1599
|
-
action: {
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1867
|
+
action: {
|
|
1868
|
+
type: 'string',
|
|
1869
|
+
description:
|
|
1870
|
+
'Action: "read", "append", "write", "replace", "remove", "list", or "search"',
|
|
1871
|
+
},
|
|
1872
|
+
file_path: {
|
|
1873
|
+
type: 'string',
|
|
1874
|
+
description:
|
|
1875
|
+
'Target file path. Allowed: MEMORY.md, USER.md, memory/YYYY-MM-DD.md',
|
|
1876
|
+
},
|
|
1877
|
+
target: {
|
|
1878
|
+
type: 'string',
|
|
1879
|
+
description:
|
|
1880
|
+
'Optional shorthand target: "memory", "user", or "daily"',
|
|
1881
|
+
},
|
|
1882
|
+
date: {
|
|
1883
|
+
type: 'string',
|
|
1884
|
+
description:
|
|
1885
|
+
'Date for target="daily" in YYYY-MM-DD format (defaults to today)',
|
|
1886
|
+
},
|
|
1887
|
+
content: {
|
|
1888
|
+
type: 'string',
|
|
1889
|
+
description: 'Text payload for append/write',
|
|
1890
|
+
},
|
|
1891
|
+
old_text: {
|
|
1892
|
+
type: 'string',
|
|
1893
|
+
description: 'Existing substring for replace/remove',
|
|
1894
|
+
},
|
|
1895
|
+
new_text: {
|
|
1896
|
+
type: 'string',
|
|
1897
|
+
description: 'Replacement text for replace',
|
|
1898
|
+
},
|
|
1899
|
+
query: {
|
|
1900
|
+
type: 'string',
|
|
1901
|
+
description: 'Case-insensitive query string for search',
|
|
1902
|
+
},
|
|
1607
1903
|
},
|
|
1608
1904
|
required: ['action'],
|
|
1609
1905
|
},
|
|
@@ -1620,12 +1916,14 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1620
1916
|
properties: {
|
|
1621
1917
|
action: {
|
|
1622
1918
|
type: 'string',
|
|
1623
|
-
description:
|
|
1919
|
+
description:
|
|
1920
|
+
'Action to perform: "read", "member-info", or "channel-info".',
|
|
1624
1921
|
enum: ['read', 'member-info', 'channel-info'],
|
|
1625
1922
|
},
|
|
1626
1923
|
channelId: {
|
|
1627
1924
|
type: 'string',
|
|
1628
|
-
description:
|
|
1925
|
+
description:
|
|
1926
|
+
'Discord channel id. Defaults to current channel for read/channel-info.',
|
|
1629
1927
|
},
|
|
1630
1928
|
guildId: {
|
|
1631
1929
|
type: 'string',
|
|
@@ -1641,7 +1939,8 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1641
1939
|
},
|
|
1642
1940
|
username: {
|
|
1643
1941
|
type: 'string',
|
|
1644
|
-
description:
|
|
1942
|
+
description:
|
|
1943
|
+
'Discord username/display name/@handle to resolve for member-info.',
|
|
1645
1944
|
},
|
|
1646
1945
|
user: {
|
|
1647
1946
|
type: 'string',
|
|
@@ -1651,9 +1950,18 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1651
1950
|
type: 'number',
|
|
1652
1951
|
description: 'Read limit for action="read" (default 20, max 100).',
|
|
1653
1952
|
},
|
|
1654
|
-
before: {
|
|
1655
|
-
|
|
1656
|
-
|
|
1953
|
+
before: {
|
|
1954
|
+
type: 'string',
|
|
1955
|
+
description: 'Read messages before this message id.',
|
|
1956
|
+
},
|
|
1957
|
+
after: {
|
|
1958
|
+
type: 'string',
|
|
1959
|
+
description: 'Read messages after this message id.',
|
|
1960
|
+
},
|
|
1961
|
+
around: {
|
|
1962
|
+
type: 'string',
|
|
1963
|
+
description: 'Read messages around this message id.',
|
|
1964
|
+
},
|
|
1657
1965
|
target: { type: 'string', description: 'Alias for channelId.' },
|
|
1658
1966
|
to: { type: 'string', description: 'Alias for channelId.' },
|
|
1659
1967
|
},
|
|
@@ -1670,10 +1978,25 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1670
1978
|
parameters: {
|
|
1671
1979
|
type: 'object',
|
|
1672
1980
|
properties: {
|
|
1673
|
-
query: {
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1981
|
+
query: {
|
|
1982
|
+
type: 'string',
|
|
1983
|
+
description: 'Search query over prior session transcripts',
|
|
1984
|
+
},
|
|
1985
|
+
limit: {
|
|
1986
|
+
type: 'number',
|
|
1987
|
+
description:
|
|
1988
|
+
'Maximum number of sessions to summarize (default 3, max 5)',
|
|
1989
|
+
},
|
|
1990
|
+
role_filter: {
|
|
1991
|
+
type: 'string',
|
|
1992
|
+
description:
|
|
1993
|
+
'Optional comma-separated roles to match (e.g. "user,assistant")',
|
|
1994
|
+
},
|
|
1995
|
+
include_current: {
|
|
1996
|
+
type: 'boolean',
|
|
1997
|
+
description:
|
|
1998
|
+
'Include the current session in results (default false)',
|
|
1999
|
+
},
|
|
1677
2000
|
},
|
|
1678
2001
|
required: ['query'],
|
|
1679
2002
|
},
|
|
@@ -1695,7 +2018,8 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1695
2018
|
},
|
|
1696
2019
|
maxChars: {
|
|
1697
2020
|
type: 'number',
|
|
1698
|
-
description:
|
|
2021
|
+
description:
|
|
2022
|
+
'Maximum characters to return (default 50000, max 50000)',
|
|
1699
2023
|
},
|
|
1700
2024
|
},
|
|
1701
2025
|
required: ['url'],
|
|
@@ -1713,7 +2037,8 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1713
2037
|
properties: {
|
|
1714
2038
|
image_url: {
|
|
1715
2039
|
type: 'string',
|
|
1716
|
-
description:
|
|
2040
|
+
description:
|
|
2041
|
+
'Local image path (preferred) or Discord CDN HTTPS URL.',
|
|
1717
2042
|
},
|
|
1718
2043
|
question: {
|
|
1719
2044
|
type: 'string',
|
|
@@ -1721,7 +2046,8 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1721
2046
|
},
|
|
1722
2047
|
fallback_url: {
|
|
1723
2048
|
type: 'string',
|
|
1724
|
-
description:
|
|
2049
|
+
description:
|
|
2050
|
+
'Optional fallback Discord CDN URL if image_url cannot be read.',
|
|
1725
2051
|
},
|
|
1726
2052
|
original_url: {
|
|
1727
2053
|
type: 'string',
|
|
@@ -1736,14 +2062,14 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1736
2062
|
type: 'function',
|
|
1737
2063
|
function: {
|
|
1738
2064
|
name: 'image',
|
|
1739
|
-
description:
|
|
1740
|
-
'Alias of vision_analyze for image analysis.',
|
|
2065
|
+
description: 'Alias of vision_analyze for image analysis.',
|
|
1741
2066
|
parameters: {
|
|
1742
2067
|
type: 'object',
|
|
1743
2068
|
properties: {
|
|
1744
2069
|
image_url: {
|
|
1745
2070
|
type: 'string',
|
|
1746
|
-
description:
|
|
2071
|
+
description:
|
|
2072
|
+
'Local image path (preferred) or Discord CDN HTTPS URL.',
|
|
1747
2073
|
},
|
|
1748
2074
|
question: {
|
|
1749
2075
|
type: 'string',
|
|
@@ -1751,7 +2077,8 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1751
2077
|
},
|
|
1752
2078
|
fallback_url: {
|
|
1753
2079
|
type: 'string',
|
|
1754
|
-
description:
|
|
2080
|
+
description:
|
|
2081
|
+
'Optional fallback Discord CDN URL if image_url cannot be read.',
|
|
1755
2082
|
},
|
|
1756
2083
|
original_url: {
|
|
1757
2084
|
type: 'string',
|
|
@@ -1774,38 +2101,65 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1774
2101
|
properties: {
|
|
1775
2102
|
mode: {
|
|
1776
2103
|
type: 'string',
|
|
1777
|
-
description:
|
|
2104
|
+
description:
|
|
2105
|
+
'Optional explicit mode: "single", "parallel", or "chain". Inferred automatically when omitted.',
|
|
1778
2106
|
enum: ['single', 'parallel', 'chain'],
|
|
1779
2107
|
},
|
|
1780
|
-
prompt: {
|
|
1781
|
-
|
|
1782
|
-
|
|
2108
|
+
prompt: {
|
|
2109
|
+
type: 'string',
|
|
2110
|
+
description:
|
|
2111
|
+
'Single-mode task instructions. Must be self-contained and specific.',
|
|
2112
|
+
},
|
|
2113
|
+
label: {
|
|
2114
|
+
type: 'string',
|
|
2115
|
+
description: 'Optional short label for completion messages',
|
|
2116
|
+
},
|
|
2117
|
+
model: {
|
|
2118
|
+
type: 'string',
|
|
2119
|
+
description: 'Optional model override for delegated run(s)',
|
|
2120
|
+
},
|
|
1783
2121
|
tasks: {
|
|
1784
2122
|
type: 'array',
|
|
1785
|
-
description:
|
|
2123
|
+
description:
|
|
2124
|
+
'Parallel-mode independent tasks (1-6 items). Each task must be self-contained.',
|
|
1786
2125
|
minItems: 1,
|
|
1787
2126
|
maxItems: 6,
|
|
1788
2127
|
items: {
|
|
1789
2128
|
type: 'object',
|
|
1790
2129
|
properties: {
|
|
1791
|
-
prompt: {
|
|
2130
|
+
prompt: {
|
|
2131
|
+
type: 'string',
|
|
2132
|
+
description:
|
|
2133
|
+
'Task instructions with explicit goal/scope/constraints.',
|
|
2134
|
+
},
|
|
1792
2135
|
label: { type: 'string', description: 'Optional task label' },
|
|
1793
|
-
model: {
|
|
2136
|
+
model: {
|
|
2137
|
+
type: 'string',
|
|
2138
|
+
description: 'Optional per-task model override',
|
|
2139
|
+
},
|
|
1794
2140
|
},
|
|
1795
2141
|
required: ['prompt'],
|
|
1796
2142
|
},
|
|
1797
2143
|
},
|
|
1798
2144
|
chain: {
|
|
1799
2145
|
type: 'array',
|
|
1800
|
-
description:
|
|
2146
|
+
description:
|
|
2147
|
+
'Chain-mode dependent steps (1-6 items). Use `{previous}` to inject prior step output.',
|
|
1801
2148
|
minItems: 1,
|
|
1802
2149
|
maxItems: 6,
|
|
1803
2150
|
items: {
|
|
1804
2151
|
type: 'object',
|
|
1805
2152
|
properties: {
|
|
1806
|
-
prompt: {
|
|
2153
|
+
prompt: {
|
|
2154
|
+
type: 'string',
|
|
2155
|
+
description:
|
|
2156
|
+
'Step instructions (supports `{previous}`) with expected output.',
|
|
2157
|
+
},
|
|
1807
2158
|
label: { type: 'string', description: 'Optional step label' },
|
|
1808
|
-
model: {
|
|
2159
|
+
model: {
|
|
2160
|
+
type: 'string',
|
|
2161
|
+
description: 'Optional per-step model override',
|
|
2162
|
+
},
|
|
1809
2163
|
},
|
|
1810
2164
|
required: ['prompt'],
|
|
1811
2165
|
},
|
|
@@ -1828,12 +2182,33 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1828
2182
|
parameters: {
|
|
1829
2183
|
type: 'object',
|
|
1830
2184
|
properties: {
|
|
1831
|
-
action: {
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
2185
|
+
action: {
|
|
2186
|
+
type: 'string',
|
|
2187
|
+
description: 'Action to perform: "list", "add", or "remove"',
|
|
2188
|
+
},
|
|
2189
|
+
prompt: {
|
|
2190
|
+
type: 'string',
|
|
2191
|
+
description: 'Task prompt / reminder text (required for "add")',
|
|
2192
|
+
},
|
|
2193
|
+
at: {
|
|
2194
|
+
type: 'string',
|
|
2195
|
+
description:
|
|
2196
|
+
'ISO-8601 timestamp for one-shot schedule (e.g. "2025-01-15T14:30:00Z")',
|
|
2197
|
+
},
|
|
2198
|
+
cron: {
|
|
2199
|
+
type: 'string',
|
|
2200
|
+
description:
|
|
2201
|
+
'Cron expression for recurring schedule (e.g. "0 9 * * *")',
|
|
2202
|
+
},
|
|
2203
|
+
every: {
|
|
2204
|
+
type: 'number',
|
|
2205
|
+
description:
|
|
2206
|
+
'Interval in seconds for simple recurring schedule (minimum 10)',
|
|
2207
|
+
},
|
|
2208
|
+
taskId: {
|
|
2209
|
+
type: 'number',
|
|
2210
|
+
description: 'Task ID to remove (required for "remove")',
|
|
2211
|
+
},
|
|
1837
2212
|
},
|
|
1838
2213
|
required: ['action'],
|
|
1839
2214
|
},
|