@hybridaione/hybridclaw 0.2.2 → 0.2.6
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 +85 -0
- package/CONTRIBUTING.md +33 -0
- package/README.md +41 -16
- package/SECURITY.md +17 -0
- package/biome.json +35 -0
- package/config.example.json +71 -8
- package/container/package-lock.json +2 -2
- package/container/package.json +1 -1
- package/container/src/approval-policy.ts +1303 -0
- package/container/src/browser-tools.ts +431 -136
- package/container/src/extensions.ts +36 -12
- package/container/src/hybridai-client.ts +34 -13
- package/container/src/index.ts +451 -109
- package/container/src/ipc.ts +5 -3
- package/container/src/token-usage.ts +20 -10
- package/container/src/tools.ts +599 -225
- package/container/src/types.ts +32 -2
- package/container/src/web-fetch.ts +89 -32
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +10 -2
- 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 +19 -3
- 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 +4 -0
- package/dist/channels/discord/inbound.d.ts.map +1 -1
- package/dist/channels/discord/inbound.js +45 -4
- 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 +6 -3
- package/dist/channels/discord/runtime.d.ts.map +1 -1
- package/dist/channels/discord/runtime.js +621 -125
- 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 +16 -8
- 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 +47 -22
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +19 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +103 -18
- package/dist/config.js.map +1 -1
- package/dist/container-runner.d.ts.map +1 -1
- package/dist/container-runner.js +58 -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 +563 -73
- 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 +179 -24
- 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 +48 -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/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 +9 -7
- package/dist/prompt-hooks.js.map +1 -1
- package/dist/runtime-config.d.ts +98 -1
- package/dist/runtime-config.d.ts.map +1 -1
- package/dist/runtime-config.js +477 -23
- 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 +29 -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 +146 -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 +37 -13
- package/package.json +8 -2
- package/src/agent.ts +16 -3
- 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 +57 -13
- package/src/channels/discord/human-delay.ts +48 -0
- package/src/channels/discord/inbound.ts +66 -7
- 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 +1048 -182
- package/src/channels/discord/stream.ts +73 -27
- 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 +141 -56
- package/src/config.ts +192 -34
- package/src/container-runner.ts +132 -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 +1019 -201
- package/src/gateway-types.ts +33 -0
- package/src/gateway.ts +321 -48
- package/src/health.ts +66 -26
- package/src/heartbeat.ts +84 -22
- package/src/hybridai-bots.ts +14 -5
- 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 +40 -17
- package/src/runtime-config.ts +1114 -99
- package/src/scheduled-task-runner.ts +63 -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 +195 -2
- package/src/update.ts +79 -23
- package/src/workspace.ts +63 -11
- package/tests/approval-policy.test.ts +224 -0
- package/tests/discord.basic.test.ts +82 -2
- package/tests/discord.human-presence.test.ts +85 -0
- package/tests/gateway-service.media-routing.test.ts +8 -2
- package/tests/memory-service.test.ts +1114 -0
- package/tests/token-efficiency.basic.test.ts +8 -2
- 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}`);
|
|
@@ -1321,7 +1526,11 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
|
|
|
1321
1526
|
case 'browser_console':
|
|
1322
1527
|
case 'browser_network':
|
|
1323
1528
|
case 'browser_close': {
|
|
1324
|
-
return await executeBrowserTool(
|
|
1529
|
+
return await executeBrowserTool(
|
|
1530
|
+
name,
|
|
1531
|
+
args,
|
|
1532
|
+
currentSessionId || 'default',
|
|
1533
|
+
);
|
|
1325
1534
|
}
|
|
1326
1535
|
|
|
1327
1536
|
case 'cron': {
|
|
@@ -1335,7 +1544,8 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
|
|
|
1335
1544
|
else if (t.everyMs) {
|
|
1336
1545
|
const secs = t.everyMs / 1000;
|
|
1337
1546
|
if (secs < 120) schedule = `every ${secs}s`;
|
|
1338
|
-
else if (secs < 7200)
|
|
1547
|
+
else if (secs < 7200)
|
|
1548
|
+
schedule = `every ${Math.round(secs / 60)}m`;
|
|
1339
1549
|
else schedule = `every ${Math.round(secs / 3600)}h`;
|
|
1340
1550
|
} else schedule = t.cronExpr;
|
|
1341
1551
|
const status = t.enabled ? 'enabled' : 'disabled';
|
|
@@ -1349,22 +1559,37 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
|
|
|
1349
1559
|
|
|
1350
1560
|
if (args.at) {
|
|
1351
1561
|
const runAt = new Date(args.at);
|
|
1352
|
-
if (isNaN(runAt.getTime()))
|
|
1353
|
-
|
|
1354
|
-
|
|
1562
|
+
if (Number.isNaN(runAt.getTime()))
|
|
1563
|
+
return `Error: invalid ISO-8601 timestamp: ${args.at}`;
|
|
1564
|
+
if (runAt.getTime() <= Date.now())
|
|
1565
|
+
return `Error: timestamp must be in the future: ${args.at}`;
|
|
1566
|
+
pendingSchedules.push({
|
|
1567
|
+
action: 'add',
|
|
1568
|
+
runAt: runAt.toISOString(),
|
|
1569
|
+
prompt: args.prompt,
|
|
1570
|
+
});
|
|
1355
1571
|
return `Scheduled one-shot task at ${runAt.toISOString()}: ${args.prompt}`;
|
|
1356
1572
|
}
|
|
1357
1573
|
|
|
1358
1574
|
if (args.cron) {
|
|
1359
|
-
pendingSchedules.push({
|
|
1575
|
+
pendingSchedules.push({
|
|
1576
|
+
action: 'add',
|
|
1577
|
+
cronExpr: args.cron,
|
|
1578
|
+
prompt: args.prompt,
|
|
1579
|
+
});
|
|
1360
1580
|
return `Scheduled recurring task with cron "${args.cron}": ${args.prompt}`;
|
|
1361
1581
|
}
|
|
1362
1582
|
|
|
1363
1583
|
if (args.every) {
|
|
1364
1584
|
const secs = Number(args.every);
|
|
1365
|
-
if (isNaN(secs) || secs < 10)
|
|
1585
|
+
if (Number.isNaN(secs) || secs < 10)
|
|
1586
|
+
return 'Error: "every" must be a number of seconds >= 10';
|
|
1366
1587
|
const everyMs = Math.round(secs * 1000);
|
|
1367
|
-
pendingSchedules.push({
|
|
1588
|
+
pendingSchedules.push({
|
|
1589
|
+
action: 'add',
|
|
1590
|
+
everyMs,
|
|
1591
|
+
prompt: args.prompt,
|
|
1592
|
+
});
|
|
1368
1593
|
return `Scheduled interval task every ${secs}s: ${args.prompt}`;
|
|
1369
1594
|
}
|
|
1370
1595
|
|
|
@@ -1385,14 +1610,21 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
|
|
|
1385
1610
|
return `Error: delegation limit reached for this turn (${MAX_PENDING_DELEGATIONS}).`;
|
|
1386
1611
|
}
|
|
1387
1612
|
|
|
1388
|
-
const modeRaw =
|
|
1389
|
-
|
|
1613
|
+
const modeRaw =
|
|
1614
|
+
typeof args.mode === 'string' ? args.mode.trim().toLowerCase() : '';
|
|
1615
|
+
if (
|
|
1616
|
+
modeRaw &&
|
|
1617
|
+
modeRaw !== 'single' &&
|
|
1618
|
+
modeRaw !== 'parallel' &&
|
|
1619
|
+
modeRaw !== 'chain'
|
|
1620
|
+
) {
|
|
1390
1621
|
return 'Error: mode must be one of "single", "parallel", or "chain".';
|
|
1391
1622
|
}
|
|
1392
1623
|
|
|
1393
1624
|
const label = typeof args.label === 'string' ? args.label.trim() : '';
|
|
1394
1625
|
const model = typeof args.model === 'string' ? args.model.trim() : '';
|
|
1395
|
-
const prompt =
|
|
1626
|
+
const prompt =
|
|
1627
|
+
typeof args.prompt === 'string' ? args.prompt.trim() : '';
|
|
1396
1628
|
const tasksResult = normalizeDelegationTaskList({
|
|
1397
1629
|
raw: args.tasks,
|
|
1398
1630
|
fallbackModel: model || undefined,
|
|
@@ -1421,7 +1653,10 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
|
|
|
1421
1653
|
mode = 'single';
|
|
1422
1654
|
}
|
|
1423
1655
|
|
|
1424
|
-
if (
|
|
1656
|
+
if (
|
|
1657
|
+
(hasTasks ? 1 : 0) + (hasChain ? 1 : 0) + (hasPrompt ? 1 : 0) > 1 &&
|
|
1658
|
+
!modeRaw
|
|
1659
|
+
) {
|
|
1425
1660
|
return 'Error: provide one delegation mode payload: "prompt", "tasks", or "chain".';
|
|
1426
1661
|
}
|
|
1427
1662
|
|
|
@@ -1439,8 +1674,10 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
|
|
|
1439
1674
|
};
|
|
1440
1675
|
summary = label ? `${label}: ${prompt}` : prompt;
|
|
1441
1676
|
} else if (mode === 'parallel') {
|
|
1442
|
-
if (!hasTasks)
|
|
1443
|
-
|
|
1677
|
+
if (!hasTasks)
|
|
1678
|
+
return 'Error: tasks are required for mode="parallel".';
|
|
1679
|
+
if (hasPrompt || hasChain)
|
|
1680
|
+
return 'Error: mode="parallel" accepts only "tasks" plus optional label/model.';
|
|
1444
1681
|
effect = {
|
|
1445
1682
|
action: 'delegate',
|
|
1446
1683
|
mode,
|
|
@@ -1451,7 +1688,8 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
|
|
|
1451
1688
|
summary = `${tasksResult.tasks.length} parallel task(s)`;
|
|
1452
1689
|
} else {
|
|
1453
1690
|
if (!hasChain) return 'Error: chain is required for mode="chain".';
|
|
1454
|
-
if (hasPrompt || hasTasks)
|
|
1691
|
+
if (hasPrompt || hasTasks)
|
|
1692
|
+
return 'Error: mode="chain" accepts only "chain" plus optional label/model.';
|
|
1455
1693
|
effect = {
|
|
1456
1694
|
action: 'delegate',
|
|
1457
1695
|
mode,
|
|
@@ -1486,8 +1724,16 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1486
1724
|
type: 'object',
|
|
1487
1725
|
properties: {
|
|
1488
1726
|
path: { type: 'string', description: 'Path to the file to read' },
|
|
1489
|
-
offset: {
|
|
1490
|
-
|
|
1727
|
+
offset: {
|
|
1728
|
+
type: 'number',
|
|
1729
|
+
description:
|
|
1730
|
+
'Line number to start reading from (1-indexed, default: 1)',
|
|
1731
|
+
},
|
|
1732
|
+
limit: {
|
|
1733
|
+
type: 'number',
|
|
1734
|
+
description:
|
|
1735
|
+
'Maximum number of lines to read before truncation logic (optional)',
|
|
1736
|
+
},
|
|
1491
1737
|
},
|
|
1492
1738
|
required: ['path'],
|
|
1493
1739
|
},
|
|
@@ -1521,7 +1767,10 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1521
1767
|
path: { type: 'string', description: 'Path to the file to edit' },
|
|
1522
1768
|
old: { type: 'string', description: 'Text to find and replace' },
|
|
1523
1769
|
new: { type: 'string', description: 'Replacement text' },
|
|
1524
|
-
count: {
|
|
1770
|
+
count: {
|
|
1771
|
+
type: 'number',
|
|
1772
|
+
description: 'Number of replacements (default: 1)',
|
|
1773
|
+
},
|
|
1525
1774
|
},
|
|
1526
1775
|
required: ['path', 'old', 'new'],
|
|
1527
1776
|
},
|
|
@@ -1549,7 +1798,10 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1549
1798
|
parameters: {
|
|
1550
1799
|
type: 'object',
|
|
1551
1800
|
properties: {
|
|
1552
|
-
pattern: {
|
|
1801
|
+
pattern: {
|
|
1802
|
+
type: 'string',
|
|
1803
|
+
description: 'Glob pattern to match files',
|
|
1804
|
+
},
|
|
1553
1805
|
},
|
|
1554
1806
|
required: ['pattern'],
|
|
1555
1807
|
},
|
|
@@ -1563,8 +1815,15 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1563
1815
|
parameters: {
|
|
1564
1816
|
type: 'object',
|
|
1565
1817
|
properties: {
|
|
1566
|
-
pattern: {
|
|
1567
|
-
|
|
1818
|
+
pattern: {
|
|
1819
|
+
type: 'string',
|
|
1820
|
+
description: 'Regex pattern to search for',
|
|
1821
|
+
},
|
|
1822
|
+
path: {
|
|
1823
|
+
type: 'string',
|
|
1824
|
+
description:
|
|
1825
|
+
'Directory or file to search in (default: workspace root)',
|
|
1826
|
+
},
|
|
1568
1827
|
},
|
|
1569
1828
|
required: ['pattern'],
|
|
1570
1829
|
},
|
|
@@ -1580,8 +1839,16 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1580
1839
|
type: 'object',
|
|
1581
1840
|
properties: {
|
|
1582
1841
|
command: { type: 'string', description: 'Shell command to execute' },
|
|
1583
|
-
timeoutMs: {
|
|
1584
|
-
|
|
1842
|
+
timeoutMs: {
|
|
1843
|
+
type: 'number',
|
|
1844
|
+
description:
|
|
1845
|
+
'Optional command timeout in milliseconds (default 240000, max 900000)',
|
|
1846
|
+
},
|
|
1847
|
+
timeoutSeconds: {
|
|
1848
|
+
type: 'number',
|
|
1849
|
+
description:
|
|
1850
|
+
'Optional command timeout in seconds (used when timeoutMs is omitted)',
|
|
1851
|
+
},
|
|
1585
1852
|
},
|
|
1586
1853
|
required: ['command'],
|
|
1587
1854
|
},
|
|
@@ -1596,14 +1863,42 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1596
1863
|
parameters: {
|
|
1597
1864
|
type: 'object',
|
|
1598
1865
|
properties: {
|
|
1599
|
-
action: {
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1866
|
+
action: {
|
|
1867
|
+
type: 'string',
|
|
1868
|
+
description:
|
|
1869
|
+
'Action: "read", "append", "write", "replace", "remove", "list", or "search"',
|
|
1870
|
+
},
|
|
1871
|
+
file_path: {
|
|
1872
|
+
type: 'string',
|
|
1873
|
+
description:
|
|
1874
|
+
'Target file path. Allowed: MEMORY.md, USER.md, memory/YYYY-MM-DD.md',
|
|
1875
|
+
},
|
|
1876
|
+
target: {
|
|
1877
|
+
type: 'string',
|
|
1878
|
+
description:
|
|
1879
|
+
'Optional shorthand target: "memory", "user", or "daily"',
|
|
1880
|
+
},
|
|
1881
|
+
date: {
|
|
1882
|
+
type: 'string',
|
|
1883
|
+
description:
|
|
1884
|
+
'Date for target="daily" in YYYY-MM-DD format (defaults to today)',
|
|
1885
|
+
},
|
|
1886
|
+
content: {
|
|
1887
|
+
type: 'string',
|
|
1888
|
+
description: 'Text payload for append/write',
|
|
1889
|
+
},
|
|
1890
|
+
old_text: {
|
|
1891
|
+
type: 'string',
|
|
1892
|
+
description: 'Existing substring for replace/remove',
|
|
1893
|
+
},
|
|
1894
|
+
new_text: {
|
|
1895
|
+
type: 'string',
|
|
1896
|
+
description: 'Replacement text for replace',
|
|
1897
|
+
},
|
|
1898
|
+
query: {
|
|
1899
|
+
type: 'string',
|
|
1900
|
+
description: 'Case-insensitive query string for search',
|
|
1901
|
+
},
|
|
1607
1902
|
},
|
|
1608
1903
|
required: ['action'],
|
|
1609
1904
|
},
|
|
@@ -1620,12 +1915,14 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1620
1915
|
properties: {
|
|
1621
1916
|
action: {
|
|
1622
1917
|
type: 'string',
|
|
1623
|
-
description:
|
|
1918
|
+
description:
|
|
1919
|
+
'Action to perform: "read", "member-info", or "channel-info".',
|
|
1624
1920
|
enum: ['read', 'member-info', 'channel-info'],
|
|
1625
1921
|
},
|
|
1626
1922
|
channelId: {
|
|
1627
1923
|
type: 'string',
|
|
1628
|
-
description:
|
|
1924
|
+
description:
|
|
1925
|
+
'Discord channel id. Defaults to current channel for read/channel-info.',
|
|
1629
1926
|
},
|
|
1630
1927
|
guildId: {
|
|
1631
1928
|
type: 'string',
|
|
@@ -1641,7 +1938,8 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1641
1938
|
},
|
|
1642
1939
|
username: {
|
|
1643
1940
|
type: 'string',
|
|
1644
|
-
description:
|
|
1941
|
+
description:
|
|
1942
|
+
'Discord username/display name/@handle to resolve for member-info.',
|
|
1645
1943
|
},
|
|
1646
1944
|
user: {
|
|
1647
1945
|
type: 'string',
|
|
@@ -1651,9 +1949,18 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1651
1949
|
type: 'number',
|
|
1652
1950
|
description: 'Read limit for action="read" (default 20, max 100).',
|
|
1653
1951
|
},
|
|
1654
|
-
before: {
|
|
1655
|
-
|
|
1656
|
-
|
|
1952
|
+
before: {
|
|
1953
|
+
type: 'string',
|
|
1954
|
+
description: 'Read messages before this message id.',
|
|
1955
|
+
},
|
|
1956
|
+
after: {
|
|
1957
|
+
type: 'string',
|
|
1958
|
+
description: 'Read messages after this message id.',
|
|
1959
|
+
},
|
|
1960
|
+
around: {
|
|
1961
|
+
type: 'string',
|
|
1962
|
+
description: 'Read messages around this message id.',
|
|
1963
|
+
},
|
|
1657
1964
|
target: { type: 'string', description: 'Alias for channelId.' },
|
|
1658
1965
|
to: { type: 'string', description: 'Alias for channelId.' },
|
|
1659
1966
|
},
|
|
@@ -1670,10 +1977,25 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1670
1977
|
parameters: {
|
|
1671
1978
|
type: 'object',
|
|
1672
1979
|
properties: {
|
|
1673
|
-
query: {
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1980
|
+
query: {
|
|
1981
|
+
type: 'string',
|
|
1982
|
+
description: 'Search query over prior session transcripts',
|
|
1983
|
+
},
|
|
1984
|
+
limit: {
|
|
1985
|
+
type: 'number',
|
|
1986
|
+
description:
|
|
1987
|
+
'Maximum number of sessions to summarize (default 3, max 5)',
|
|
1988
|
+
},
|
|
1989
|
+
role_filter: {
|
|
1990
|
+
type: 'string',
|
|
1991
|
+
description:
|
|
1992
|
+
'Optional comma-separated roles to match (e.g. "user,assistant")',
|
|
1993
|
+
},
|
|
1994
|
+
include_current: {
|
|
1995
|
+
type: 'boolean',
|
|
1996
|
+
description:
|
|
1997
|
+
'Include the current session in results (default false)',
|
|
1998
|
+
},
|
|
1677
1999
|
},
|
|
1678
2000
|
required: ['query'],
|
|
1679
2001
|
},
|
|
@@ -1695,7 +2017,8 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1695
2017
|
},
|
|
1696
2018
|
maxChars: {
|
|
1697
2019
|
type: 'number',
|
|
1698
|
-
description:
|
|
2020
|
+
description:
|
|
2021
|
+
'Maximum characters to return (default 50000, max 50000)',
|
|
1699
2022
|
},
|
|
1700
2023
|
},
|
|
1701
2024
|
required: ['url'],
|
|
@@ -1713,7 +2036,8 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1713
2036
|
properties: {
|
|
1714
2037
|
image_url: {
|
|
1715
2038
|
type: 'string',
|
|
1716
|
-
description:
|
|
2039
|
+
description:
|
|
2040
|
+
'Local image path (preferred) or Discord CDN HTTPS URL.',
|
|
1717
2041
|
},
|
|
1718
2042
|
question: {
|
|
1719
2043
|
type: 'string',
|
|
@@ -1721,7 +2045,8 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1721
2045
|
},
|
|
1722
2046
|
fallback_url: {
|
|
1723
2047
|
type: 'string',
|
|
1724
|
-
description:
|
|
2048
|
+
description:
|
|
2049
|
+
'Optional fallback Discord CDN URL if image_url cannot be read.',
|
|
1725
2050
|
},
|
|
1726
2051
|
original_url: {
|
|
1727
2052
|
type: 'string',
|
|
@@ -1736,14 +2061,14 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1736
2061
|
type: 'function',
|
|
1737
2062
|
function: {
|
|
1738
2063
|
name: 'image',
|
|
1739
|
-
description:
|
|
1740
|
-
'Alias of vision_analyze for image analysis.',
|
|
2064
|
+
description: 'Alias of vision_analyze for image analysis.',
|
|
1741
2065
|
parameters: {
|
|
1742
2066
|
type: 'object',
|
|
1743
2067
|
properties: {
|
|
1744
2068
|
image_url: {
|
|
1745
2069
|
type: 'string',
|
|
1746
|
-
description:
|
|
2070
|
+
description:
|
|
2071
|
+
'Local image path (preferred) or Discord CDN HTTPS URL.',
|
|
1747
2072
|
},
|
|
1748
2073
|
question: {
|
|
1749
2074
|
type: 'string',
|
|
@@ -1751,7 +2076,8 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1751
2076
|
},
|
|
1752
2077
|
fallback_url: {
|
|
1753
2078
|
type: 'string',
|
|
1754
|
-
description:
|
|
2079
|
+
description:
|
|
2080
|
+
'Optional fallback Discord CDN URL if image_url cannot be read.',
|
|
1755
2081
|
},
|
|
1756
2082
|
original_url: {
|
|
1757
2083
|
type: 'string',
|
|
@@ -1774,38 +2100,65 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1774
2100
|
properties: {
|
|
1775
2101
|
mode: {
|
|
1776
2102
|
type: 'string',
|
|
1777
|
-
description:
|
|
2103
|
+
description:
|
|
2104
|
+
'Optional explicit mode: "single", "parallel", or "chain". Inferred automatically when omitted.',
|
|
1778
2105
|
enum: ['single', 'parallel', 'chain'],
|
|
1779
2106
|
},
|
|
1780
|
-
prompt: {
|
|
1781
|
-
|
|
1782
|
-
|
|
2107
|
+
prompt: {
|
|
2108
|
+
type: 'string',
|
|
2109
|
+
description:
|
|
2110
|
+
'Single-mode task instructions. Must be self-contained and specific.',
|
|
2111
|
+
},
|
|
2112
|
+
label: {
|
|
2113
|
+
type: 'string',
|
|
2114
|
+
description: 'Optional short label for completion messages',
|
|
2115
|
+
},
|
|
2116
|
+
model: {
|
|
2117
|
+
type: 'string',
|
|
2118
|
+
description: 'Optional model override for delegated run(s)',
|
|
2119
|
+
},
|
|
1783
2120
|
tasks: {
|
|
1784
2121
|
type: 'array',
|
|
1785
|
-
description:
|
|
2122
|
+
description:
|
|
2123
|
+
'Parallel-mode independent tasks (1-6 items). Each task must be self-contained.',
|
|
1786
2124
|
minItems: 1,
|
|
1787
2125
|
maxItems: 6,
|
|
1788
2126
|
items: {
|
|
1789
2127
|
type: 'object',
|
|
1790
2128
|
properties: {
|
|
1791
|
-
prompt: {
|
|
2129
|
+
prompt: {
|
|
2130
|
+
type: 'string',
|
|
2131
|
+
description:
|
|
2132
|
+
'Task instructions with explicit goal/scope/constraints.',
|
|
2133
|
+
},
|
|
1792
2134
|
label: { type: 'string', description: 'Optional task label' },
|
|
1793
|
-
model: {
|
|
2135
|
+
model: {
|
|
2136
|
+
type: 'string',
|
|
2137
|
+
description: 'Optional per-task model override',
|
|
2138
|
+
},
|
|
1794
2139
|
},
|
|
1795
2140
|
required: ['prompt'],
|
|
1796
2141
|
},
|
|
1797
2142
|
},
|
|
1798
2143
|
chain: {
|
|
1799
2144
|
type: 'array',
|
|
1800
|
-
description:
|
|
2145
|
+
description:
|
|
2146
|
+
'Chain-mode dependent steps (1-6 items). Use `{previous}` to inject prior step output.',
|
|
1801
2147
|
minItems: 1,
|
|
1802
2148
|
maxItems: 6,
|
|
1803
2149
|
items: {
|
|
1804
2150
|
type: 'object',
|
|
1805
2151
|
properties: {
|
|
1806
|
-
prompt: {
|
|
2152
|
+
prompt: {
|
|
2153
|
+
type: 'string',
|
|
2154
|
+
description:
|
|
2155
|
+
'Step instructions (supports `{previous}`) with expected output.',
|
|
2156
|
+
},
|
|
1807
2157
|
label: { type: 'string', description: 'Optional step label' },
|
|
1808
|
-
model: {
|
|
2158
|
+
model: {
|
|
2159
|
+
type: 'string',
|
|
2160
|
+
description: 'Optional per-step model override',
|
|
2161
|
+
},
|
|
1809
2162
|
},
|
|
1810
2163
|
required: ['prompt'],
|
|
1811
2164
|
},
|
|
@@ -1828,12 +2181,33 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1828
2181
|
parameters: {
|
|
1829
2182
|
type: 'object',
|
|
1830
2183
|
properties: {
|
|
1831
|
-
action: {
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
2184
|
+
action: {
|
|
2185
|
+
type: 'string',
|
|
2186
|
+
description: 'Action to perform: "list", "add", or "remove"',
|
|
2187
|
+
},
|
|
2188
|
+
prompt: {
|
|
2189
|
+
type: 'string',
|
|
2190
|
+
description: 'Task prompt / reminder text (required for "add")',
|
|
2191
|
+
},
|
|
2192
|
+
at: {
|
|
2193
|
+
type: 'string',
|
|
2194
|
+
description:
|
|
2195
|
+
'ISO-8601 timestamp for one-shot schedule (e.g. "2025-01-15T14:30:00Z")',
|
|
2196
|
+
},
|
|
2197
|
+
cron: {
|
|
2198
|
+
type: 'string',
|
|
2199
|
+
description:
|
|
2200
|
+
'Cron expression for recurring schedule (e.g. "0 9 * * *")',
|
|
2201
|
+
},
|
|
2202
|
+
every: {
|
|
2203
|
+
type: 'number',
|
|
2204
|
+
description:
|
|
2205
|
+
'Interval in seconds for simple recurring schedule (minimum 10)',
|
|
2206
|
+
},
|
|
2207
|
+
taskId: {
|
|
2208
|
+
type: 'number',
|
|
2209
|
+
description: 'Task ID to remove (required for "remove")',
|
|
2210
|
+
},
|
|
1837
2211
|
},
|
|
1838
2212
|
required: ['action'],
|
|
1839
2213
|
},
|