@hybridaione/hybridclaw 0.1.24 → 0.2.1
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/CHANGELOG.md +40 -0
- package/README.md +43 -3
- package/config.example.json +10 -2
- package/container/package-lock.json +2 -2
- package/container/package.json +1 -1
- package/container/src/index.ts +113 -4
- package/container/src/tools.ts +192 -0
- package/container/src/types.ts +2 -0
- package/dist/cli.js +15 -1
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +21 -2
- package/dist/config.js.map +1 -1
- package/dist/container-runner.d.ts +1 -0
- package/dist/container-runner.d.ts.map +1 -1
- package/dist/container-runner.js +17 -2
- package/dist/container-runner.js.map +1 -1
- package/dist/conversation.d.ts +2 -1
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +3 -2
- package/dist/conversation.js.map +1 -1
- package/dist/discord.d.ts +21 -0
- package/dist/discord.d.ts.map +1 -1
- package/dist/discord.js +963 -38
- package/dist/discord.js.map +1 -1
- package/dist/gateway-service.d.ts.map +1 -1
- package/dist/gateway-service.js +384 -60
- package/dist/gateway-service.js.map +1 -1
- package/dist/gateway.js +3 -2
- package/dist/gateway.js.map +1 -1
- package/dist/git-commit.d.ts +2 -0
- package/dist/git-commit.d.ts.map +1 -0
- package/dist/git-commit.js +63 -0
- package/dist/git-commit.js.map +1 -0
- package/dist/health.d.ts.map +1 -1
- package/dist/health.js +37 -4
- package/dist/health.js.map +1 -1
- package/dist/heartbeat.d.ts.map +1 -1
- package/dist/heartbeat.js +1 -0
- package/dist/heartbeat.js.map +1 -1
- package/dist/onboarding.d.ts.map +1 -1
- package/dist/onboarding.js +1 -2
- package/dist/onboarding.js.map +1 -1
- package/dist/prompt-hooks.d.ts +9 -1
- package/dist/prompt-hooks.d.ts.map +1 -1
- package/dist/prompt-hooks.js +34 -1
- package/dist/prompt-hooks.js.map +1 -1
- package/dist/runtime-config.d.ts +9 -1
- package/dist/runtime-config.d.ts.map +1 -1
- package/dist/runtime-config.js +20 -214
- package/dist/runtime-config.js.map +1 -1
- package/dist/tui.js +9 -0
- package/dist/tui.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/update.d.ts +3 -0
- package/dist/update.d.ts.map +1 -0
- package/dist/update.js +331 -0
- package/dist/update.js.map +1 -0
- package/docs/index.html +21 -1
- package/package.json +1 -1
- package/skills/personality/SKILL.md +108 -0
- package/skills/skill-creator/SKILL.md +232 -0
- package/skills/skill-creator/agents/openai.yaml +4 -0
- package/skills/skill-creator/license.txt +202 -0
- package/skills/skill-creator/references/openai_yaml.md +40 -0
- package/skills/skill-creator/references/output-patterns.md +119 -0
- package/skills/skill-creator/references/workflows.md +99 -0
- package/skills/skill-creator/scripts/generate_openai_yaml.py +271 -0
- package/skills/skill-creator/scripts/init_skill.py +238 -0
- package/skills/skill-creator/scripts/package_skill.py +161 -0
- package/skills/skill-creator/scripts/quick_validate.py +291 -0
- package/skills/skill-creator/scripts/test_package_skill.py +80 -0
- package/src/cli.ts +15 -1
- package/src/config.ts +22 -2
- package/src/container-runner.ts +20 -1
- package/src/conversation.ts +8 -1
- package/src/discord.ts +1115 -39
- package/src/gateway-service.ts +418 -60
- package/src/gateway.ts +7 -1
- package/src/health.ts +41 -4
- package/src/heartbeat.ts +1 -0
- package/src/onboarding.ts +1 -2
- package/src/prompt-hooks.ts +47 -2
- package/src/runtime-config.ts +44 -194
- package/src/tui.ts +8 -0
- package/src/types.ts +2 -0
- package/src/update.ts +389 -0
- package/templates/AGENTS.md +19 -0
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,46 @@
|
|
|
8
8
|
|
|
9
9
|
### Fixed
|
|
10
10
|
|
|
11
|
+
## [0.2.1](https://github.com/HybridAIOne/hybridclaw/tree/v0.2.1)
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **Discord `message` tool actions**: Added OpenClaw-style `message` tool support in the container with `read`, `member-info`, and `channel-info` actions, routed via the gateway API.
|
|
16
|
+
- **Gateway Discord action endpoint**: Added `POST /api/discord/action` to execute Discord context actions for tools and automated runs.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- **Discord presence handling**: Switched from prompt-injected presence snapshots to cache-backed presence data returned by `member-info` (`status` + `activities`) when available.
|
|
21
|
+
- **Discord context guidance**: Updated safety prompt policy to explicitly route recap/member lookup questions through `message` tool actions instead of guessing.
|
|
22
|
+
- **Tool allowlists**: Enabled `message` in heartbeat and base subagent allowed tool sets for delegated and automated workflows.
|
|
23
|
+
- **Container gateway auth context**: Container input now carries gateway base URL/token and maps loopback hosts to `host.docker.internal` for in-container API reachability.
|
|
24
|
+
- **Gateway token fallback**: Runtime now generates an internal gateway API token when no explicit token is configured, while preserving env/config overrides.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
## [0.2.0](https://github.com/HybridAIOne/hybridclaw/tree/v0.2.0)
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- **Personality switcher skill**: Added `skills/personality/SKILL.md` with `/personality` command workflow (`list`, `set`, `reset`) and a 25-profile persona set (including expert, style, and role personas like `pirate`, `noir`, `german`, `coach`, `doctor`, `soldier`, and `lawyer`).
|
|
34
|
+
- **Ralph loop runtime mode**: Added configurable autonomous iteration (`proactive.ralph.maxIterations`) in the container tool loop. When enabled, turns continue automatically until the model emits `<choice>STOP</choice>` (or the configured loop budget is reached).
|
|
35
|
+
- **Ralph command controls**: Added gateway/TUI command support for `ralph on|off|set <n>|info`, with immediate current-session container restart to apply loop settings without waiting for idle recycle.
|
|
36
|
+
- **Skill creator authoring toolkit**: Added bundled `skills/skill-creator/` (invocable skill, references, and helper scripts) for initializing, validating, packaging, and generating `agents/openai.yaml` metadata for new skills.
|
|
37
|
+
- **Discord context enrichment pipeline**: Added pending guild-history context, participant alias memory, `@name` mention-to-ID rewrite support, and optional per-channel presence snapshots for better grounded Discord replies.
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- **Personality persistence contract**: Standardized the managed `SOUL.md` personality block to `Name`, `Definition`, and `Rules`, so active persona behavior is fully file-driven.
|
|
42
|
+
- **Personality style policy**: Updated persona rules so style signals are explicitly visible for the active personality (instead of only a subset).
|
|
43
|
+
- **Personality skill prompt mode**: Set personality switching to command-only behavior (`always: false`, `disable-model-invocation: true`) to avoid per-turn prompt overhead while keeping `/personality` invocations available.
|
|
44
|
+
- **Workspace AGENTS template behavior**: Updated `templates/AGENTS.md` group-chat guidance with explicit "Quality > quantity" speaking rules and emoji-reaction social-signal policy (`React Like a Human`, one reaction per message).
|
|
45
|
+
- **Runtime self-awareness hook**: Prompt assembly now always injects runtime metadata (`version`, UTC date, model/default model, chatbot/channel/guild IDs, node/OS/host/workspace) and keeps it active in `minimal` mode.
|
|
46
|
+
- **Discord runtime controls**: Added and hot-wired `discord.{guildMembersIntent,presenceIntent,respondToAllMessages,commandsOnly,commandUserId}` config behavior for intent selection, trigger policy, and command-user authorization.
|
|
47
|
+
- **Gateway status reporting**: `status` command output now includes the running HybridClaw version line.
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
|
|
11
51
|
## [0.1.24](https://github.com/HybridAIOne/hybridclaw/tree/v0.1.24)
|
|
12
52
|
|
|
13
53
|
### Added
|
package/README.md
CHANGED
|
@@ -11,7 +11,15 @@ npm install -g @hybridaione/hybridclaw
|
|
|
11
11
|
hybridclaw onboarding
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
Latest release: [v0.1
|
|
14
|
+
Latest release: [v0.2.1](https://github.com/HybridAIOne/hybridclaw/releases/tag/v0.2.1)
|
|
15
|
+
|
|
16
|
+
## What's new in v0.2.1
|
|
17
|
+
|
|
18
|
+
- Added OpenClaw-style Discord `message` tool actions (`read`, `member-info`, `channel-info`) to the container runtime
|
|
19
|
+
- Added gateway endpoint `POST /api/discord/action` for Discord context lookups from tools
|
|
20
|
+
- Replaced prompt-time Discord presence snapshots with cache-backed `member-info` presence fields (`status`, `activities`)
|
|
21
|
+
- Routed Discord context lookups through gateway API from container with host remapping and token propagation
|
|
22
|
+
- Enabled `message` tool in heartbeat and base subagent allowlists
|
|
15
23
|
|
|
16
24
|
## HybridAI Advantage
|
|
17
25
|
|
|
@@ -79,6 +87,7 @@ HybridClaw best-in-class capabilities:
|
|
|
79
87
|
- formal prompt hook orchestration (`bootstrap`, `memory`, `safety`)
|
|
80
88
|
- Discord conversational UX: edit-in-place streaming responses, fence-safe chunking beyond Discord's 2000-char limit, typing keepalive, debounce batching, reply-chain-aware context, and concise attachment-first screenshot replies
|
|
81
89
|
- token-efficient context assembly: per-message history truncation, hard history budgets with head/tail preservation, and head/tail truncation for oversized bootstrap files
|
|
90
|
+
- runtime self-awareness in prompts: exact HybridClaw version/date, model, and runtime host metadata injected each turn for reliable "what version/model are you?" answers
|
|
82
91
|
- proactive runtime layer with active-hours gating, push delegation (`single`/`parallel`/`chain`), depth-aware tool policy, and retry controls
|
|
83
92
|
- structured audit trail: append-only hash-chained wire logs (`data/audit/<session>/wire.jsonl`) with tamper-evident immutability, normalized SQLite audit tables, and verification/search CLI commands
|
|
84
93
|
- observability export: incremental `events:batch` forwarding with durable cursor tracking and bot-scoped ingest token lifecycle via `ingest-token:ensure`
|
|
@@ -92,11 +101,18 @@ HybridClaw uses typed runtime config in `config.json` (auto-created on first run
|
|
|
92
101
|
|
|
93
102
|
- Start from `config.example.json` (reference)
|
|
94
103
|
- Runtime watches `config.json` and hot-reloads most settings (model defaults, heartbeat, prompt hooks, limits, etc.)
|
|
104
|
+
- `discord.guildMembersIntent` enables richer guild member context and better `@name` mention resolution in replies (requires enabling **Server Members Intent** in Discord Developer Portal)
|
|
105
|
+
- `discord.presenceIntent` enables Discord presence events (requires enabling **Presence Intent** in Discord Developer Portal)
|
|
106
|
+
- `discord.respondToAllMessages` changes guild trigger behavior: `false` (default) replies only on mention/`!claw`; `true` replies to every user message in the channel
|
|
107
|
+
- `discord.commandUserId` restricts `!claw <command>` admin commands to a single Discord user ID (all other messages still use normal chat handling)
|
|
108
|
+
- `discord.commandsOnly` optional hard mode: if `true`, the bot ignores non-`!claw` messages and only accepts prefixed commands (optionally limited by `discord.commandUserId`)
|
|
95
109
|
- `skills.extraDirs` adds additional enterprise/shared skill roots (lowest precedence tier)
|
|
96
|
-
- `proactive.*` controls autonomous behavior (`activeHours`, `delegation`, `autoRetry`)
|
|
110
|
+
- `proactive.*` controls autonomous behavior (`activeHours`, `delegation`, `autoRetry`, `ralph`)
|
|
111
|
+
- `proactive.ralph.maxIterations` enables Ralph loop (`0` off, `-1` unlimited, `>0` extra autonomous iterations before forcing completion)
|
|
112
|
+
- TUI/Gateway command: `ralph on|off|set <n>|info` (`0` off, `-1` unlimited, `1-64` extra iterations)
|
|
97
113
|
- `observability.*` controls push ingest into HybridAI (`events:batch` endpoint, batching, identity metadata)
|
|
98
114
|
- Some settings require restart to fully apply (for example HTTP bind host/port)
|
|
99
|
-
- Default bot is configured via `hybridai.defaultChatbotId` in `config.json`
|
|
115
|
+
- Default bot is configured via `hybridai.defaultChatbotId` in `config.json`
|
|
100
116
|
|
|
101
117
|
Secrets remain in `.env`:
|
|
102
118
|
|
|
@@ -258,6 +274,29 @@ Explicit invocation is supported via:
|
|
|
258
274
|
Example skill in this repo:
|
|
259
275
|
|
|
260
276
|
- `skills/repo-orientation/SKILL.md`
|
|
277
|
+
- `skills/current-time/SKILL.md`
|
|
278
|
+
- `skills/personality/SKILL.md`
|
|
279
|
+
- `skills/skill-creator/SKILL.md`
|
|
280
|
+
|
|
281
|
+
### Personality switching skill
|
|
282
|
+
|
|
283
|
+
HybridClaw includes a command-only personality skill that updates the active persona contract in `SOUL.md`.
|
|
284
|
+
|
|
285
|
+
- List current/available persona: `/personality` (or `/personality list`)
|
|
286
|
+
- Activate persona: `/personality <name>`
|
|
287
|
+
- Reset to default persona: `/personality reset`
|
|
288
|
+
|
|
289
|
+
The skill writes/updates a managed block in `SOUL.md`:
|
|
290
|
+
|
|
291
|
+
- `## Active personality`
|
|
292
|
+
- `Name: ...`
|
|
293
|
+
- `Definition: ...` (copied from the selected profile in `skills/personality/SKILL.md`)
|
|
294
|
+
- `Rules: ...` (runtime style/behavior constraints)
|
|
295
|
+
|
|
296
|
+
Notes:
|
|
297
|
+
|
|
298
|
+
- The personality skill is intentionally command-only (`always: false`, `disable-model-invocation: true`) to avoid adding per-turn prompt overhead.
|
|
299
|
+
- Profiles are defined in `skills/personality/SKILL.md` and currently include 25 switchable personas (expert, style, and role personas).
|
|
261
300
|
|
|
262
301
|
## Agent tools
|
|
263
302
|
|
|
@@ -321,6 +360,7 @@ CLI runtime commands:
|
|
|
321
360
|
- `hybridclaw gateway <command...>` — Send a command to a running gateway (for example `sessions`, `bot info`)
|
|
322
361
|
- `hybridclaw tui` — Start terminal client connected to gateway
|
|
323
362
|
- `hybridclaw onboarding` — Run HybridAI account/API key onboarding
|
|
363
|
+
- `hybridclaw update [status|--check] [--yes]` — Check for updates and upgrade global npm installs (source checkouts get git-based update instructions)
|
|
324
364
|
- `hybridclaw audit ...` — Verify and inspect structured audit trail (`recent`, `search`, `approvals`, `verify`, `instructions`)
|
|
325
365
|
|
|
326
366
|
In Discord, use `!claw help` to see all commands. Key ones:
|
package/config.example.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version":
|
|
2
|
+
"version": 3,
|
|
3
3
|
"security": {
|
|
4
4
|
"trustModelAccepted": false,
|
|
5
5
|
"trustModelAcceptedAt": "",
|
|
@@ -10,7 +10,12 @@
|
|
|
10
10
|
"extraDirs": []
|
|
11
11
|
},
|
|
12
12
|
"discord": {
|
|
13
|
-
"prefix": "!claw"
|
|
13
|
+
"prefix": "!claw",
|
|
14
|
+
"guildMembersIntent": false,
|
|
15
|
+
"presenceIntent": false,
|
|
16
|
+
"respondToAllMessages": false,
|
|
17
|
+
"commandsOnly": false,
|
|
18
|
+
"commandUserId": ""
|
|
14
19
|
},
|
|
15
20
|
"hybridai": {
|
|
16
21
|
"baseUrl": "https://hybridai.one",
|
|
@@ -94,6 +99,9 @@
|
|
|
94
99
|
"maxAttempts": 3,
|
|
95
100
|
"baseDelayMs": 2000,
|
|
96
101
|
"maxDelayMs": 8000
|
|
102
|
+
},
|
|
103
|
+
"ralph": {
|
|
104
|
+
"maxIterations": 0
|
|
97
105
|
}
|
|
98
106
|
}
|
|
99
107
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hybridclaw-agent",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "hybridclaw-agent",
|
|
9
|
-
"version": "0.1
|
|
9
|
+
"version": "0.2.1",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@mozilla/readability": "^0.6.0",
|
|
12
12
|
"agent-browser": "^0.15.1",
|
package/container/package.json
CHANGED
package/container/src/index.ts
CHANGED
|
@@ -10,7 +10,16 @@ import {
|
|
|
10
10
|
estimateTextTokens,
|
|
11
11
|
finalizeTokenUsage,
|
|
12
12
|
} from './token-usage.js';
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
executeTool,
|
|
15
|
+
getPendingSideEffects,
|
|
16
|
+
resetSideEffects,
|
|
17
|
+
setGatewayContext,
|
|
18
|
+
setModelContext,
|
|
19
|
+
setScheduledTasks,
|
|
20
|
+
setSessionContext,
|
|
21
|
+
TOOL_DEFINITIONS,
|
|
22
|
+
} from './tools.js';
|
|
14
23
|
import type { ArtifactMetadata, ChatMessage, ContainerInput, ContainerOutput, ToolDefinition, ToolExecution } from './types.js';
|
|
15
24
|
|
|
16
25
|
const MAX_ITERATIONS = 20;
|
|
@@ -19,6 +28,13 @@ const RETRY_ENABLED = process.env.HYBRIDCLAW_RETRY_ENABLED !== 'false';
|
|
|
19
28
|
const RETRY_MAX_ATTEMPTS = Math.max(1, parseInt(process.env.HYBRIDCLAW_RETRY_MAX_ATTEMPTS || '3', 10));
|
|
20
29
|
const RETRY_BASE_DELAY_MS = Math.max(100, parseInt(process.env.HYBRIDCLAW_RETRY_BASE_DELAY_MS || '2000', 10));
|
|
21
30
|
const RETRY_MAX_DELAY_MS = Math.max(RETRY_BASE_DELAY_MS, parseInt(process.env.HYBRIDCLAW_RETRY_MAX_DELAY_MS || '8000', 10));
|
|
31
|
+
const RAW_RALPH_MAX_EXTRA_ITERATIONS = Number.parseInt(process.env.HYBRIDCLAW_RALPH_MAX_ITERATIONS || '0', 10);
|
|
32
|
+
const RALPH_MAX_EXTRA_ITERATIONS = Number.isFinite(RAW_RALPH_MAX_EXTRA_ITERATIONS)
|
|
33
|
+
? (RAW_RALPH_MAX_EXTRA_ITERATIONS === -1
|
|
34
|
+
? -1
|
|
35
|
+
: Math.max(0, Math.min(64, RAW_RALPH_MAX_EXTRA_ITERATIONS)))
|
|
36
|
+
: 0;
|
|
37
|
+
const RALPH_ENABLED = RALPH_MAX_EXTRA_ITERATIONS !== 0;
|
|
22
38
|
const WORKSPACE_ROOT = '/workspace';
|
|
23
39
|
const ARTIFACT_MIME_TYPES: Record<string, string> = {
|
|
24
40
|
'.gif': 'image/gif',
|
|
@@ -84,6 +100,64 @@ function inferToolError(result: string, blockedReason: string | null): boolean {
|
|
|
84
100
|
return /\b(error|failed|denied|forbidden|timed out|timeout|exception|invalid)\b/i.test(result);
|
|
85
101
|
}
|
|
86
102
|
|
|
103
|
+
function latestUserPrompt(messages: ChatMessage[]): string {
|
|
104
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
105
|
+
const message = messages[i];
|
|
106
|
+
if (message.role !== 'user') continue;
|
|
107
|
+
const text = String(message.content || '').replace(/\s+/g, ' ').trim();
|
|
108
|
+
if (!text) continue;
|
|
109
|
+
return text.slice(0, 1_200);
|
|
110
|
+
}
|
|
111
|
+
return 'Continue the task';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function parseRalphChoice(content: string | null): 'CONTINUE' | 'STOP' | null {
|
|
115
|
+
if (!content) return null;
|
|
116
|
+
const re = /<choice>\s*([^<]*)\s*<\/choice>/gi;
|
|
117
|
+
let match: RegExpExecArray | null = null;
|
|
118
|
+
let lastChoice: string | null = null;
|
|
119
|
+
while (true) {
|
|
120
|
+
match = re.exec(content);
|
|
121
|
+
if (!match) break;
|
|
122
|
+
lastChoice = (match[1] || '').trim().toUpperCase();
|
|
123
|
+
}
|
|
124
|
+
if (lastChoice === 'CONTINUE' || lastChoice === 'STOP') return lastChoice;
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function stripRalphChoiceTags(content: string | null): string | null {
|
|
129
|
+
if (content == null) return content;
|
|
130
|
+
const stripped = content
|
|
131
|
+
.replace(/<choice>\s*[^<]*\s*<\/choice>/gi, '')
|
|
132
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
133
|
+
.trim();
|
|
134
|
+
return stripped || content;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function buildRalphPrompt(taskPrompt: string, missingChoice: boolean): string {
|
|
138
|
+
const punctuatedPrompt = /[.!?]$/.test(taskPrompt) ? taskPrompt : `${taskPrompt}.`;
|
|
139
|
+
const lines = [
|
|
140
|
+
`${punctuatedPrompt} (You are running in an automated loop where the same prompt is fed repeatedly. Only choose STOP when the task is fully complete. Including it will stop further iterations. If you are not 100% sure, choose CONTINUE.)`,
|
|
141
|
+
'',
|
|
142
|
+
'Available branches:',
|
|
143
|
+
'- CONTINUE',
|
|
144
|
+
'- STOP',
|
|
145
|
+
'',
|
|
146
|
+
'Reply with a choice using <choice>...</choice>.',
|
|
147
|
+
];
|
|
148
|
+
if (missingChoice) {
|
|
149
|
+
lines.push('');
|
|
150
|
+
lines.push('Your last response did not include a valid choice. Include exactly one: CONTINUE or STOP.');
|
|
151
|
+
}
|
|
152
|
+
return lines.join('\n');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function resolveMaxModelTurns(): number {
|
|
156
|
+
if (!RALPH_ENABLED) return MAX_ITERATIONS;
|
|
157
|
+
if (RALPH_MAX_EXTRA_ITERATIONS < 0) return Number.MAX_SAFE_INTEGER;
|
|
158
|
+
return Math.max(MAX_ITERATIONS, RALPH_MAX_EXTRA_ITERATIONS + 1);
|
|
159
|
+
}
|
|
160
|
+
|
|
87
161
|
function inferMimeType(filePath: string): string {
|
|
88
162
|
const ext = path.posix.extname(filePath).toLowerCase();
|
|
89
163
|
return ARTIFACT_MIME_TYPES[ext] || 'application/octet-stream';
|
|
@@ -237,9 +311,12 @@ async function processRequest(
|
|
|
237
311
|
const artifacts: ArtifactMetadata[] = [];
|
|
238
312
|
const artifactPaths = new Set<string>();
|
|
239
313
|
const tokenUsage = createTokenUsageStats();
|
|
314
|
+
const ralphSeedPrompt = RALPH_ENABLED ? latestUserPrompt(messages) : '';
|
|
315
|
+
const maxModelTurns = resolveMaxModelTurns();
|
|
316
|
+
let ralphExtraIterations = 0;
|
|
240
317
|
let iterations = 0;
|
|
241
318
|
|
|
242
|
-
while (iterations <
|
|
319
|
+
while (iterations < maxModelTurns) {
|
|
243
320
|
iterations++;
|
|
244
321
|
tokenUsage.modelCalls += 1;
|
|
245
322
|
tokenUsage.estimatedPromptTokens += estimateMessageTokens(history);
|
|
@@ -305,9 +382,39 @@ async function processRequest(
|
|
|
305
382
|
|
|
306
383
|
const toolCalls = choice.message.tool_calls || [];
|
|
307
384
|
if (toolCalls.length === 0) {
|
|
385
|
+
if (RALPH_ENABLED) {
|
|
386
|
+
const branchChoice = parseRalphChoice(choice.message.content);
|
|
387
|
+
if (branchChoice === 'STOP') {
|
|
388
|
+
const completed: ContainerOutput = {
|
|
389
|
+
status: 'success',
|
|
390
|
+
result: stripRalphChoiceTags(choice.message.content),
|
|
391
|
+
toolsUsed: [...new Set(toolsUsed)],
|
|
392
|
+
...(artifacts.length > 0 ? { artifacts } : {}),
|
|
393
|
+
toolExecutions,
|
|
394
|
+
tokenUsage: finalizeTokenUsage(tokenUsage),
|
|
395
|
+
};
|
|
396
|
+
await emitRuntimeEvent({ event: 'turn_end', status: completed.status, toolsUsed: completed.toolsUsed });
|
|
397
|
+
return completed;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const canContinue = RALPH_MAX_EXTRA_ITERATIONS < 0 || ralphExtraIterations < RALPH_MAX_EXTRA_ITERATIONS;
|
|
401
|
+
if (canContinue) {
|
|
402
|
+
ralphExtraIterations += 1;
|
|
403
|
+
history.push({
|
|
404
|
+
role: 'user',
|
|
405
|
+
content: buildRalphPrompt(ralphSeedPrompt, branchChoice == null),
|
|
406
|
+
});
|
|
407
|
+
console.error(
|
|
408
|
+
`[ralph] continue ${ralphExtraIterations}`
|
|
409
|
+
+ (RALPH_MAX_EXTRA_ITERATIONS < 0 ? '' : `/${RALPH_MAX_EXTRA_ITERATIONS}`),
|
|
410
|
+
);
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
308
415
|
const completed: ContainerOutput = {
|
|
309
416
|
status: 'success',
|
|
310
|
-
result: choice.message.content,
|
|
417
|
+
result: stripRalphChoiceTags(choice.message.content),
|
|
311
418
|
toolsUsed: [...new Set(toolsUsed)],
|
|
312
419
|
...(artifacts.length > 0 ? { artifacts } : {}),
|
|
313
420
|
toolExecutions,
|
|
@@ -366,7 +473,7 @@ async function processRequest(
|
|
|
366
473
|
const lastAssistant = history.filter((m) => m.role === 'assistant').pop();
|
|
367
474
|
const completed: ContainerOutput = {
|
|
368
475
|
status: 'success',
|
|
369
|
-
result: lastAssistant?.content || 'Max tool iterations reached.',
|
|
476
|
+
result: stripRalphChoiceTags(lastAssistant?.content || null) || 'Max tool iterations reached.',
|
|
370
477
|
toolsUsed: [...new Set(toolsUsed)],
|
|
371
478
|
...(artifacts.length > 0 ? { artifacts } : {}),
|
|
372
479
|
toolExecutions,
|
|
@@ -401,6 +508,7 @@ async function main(): Promise<void> {
|
|
|
401
508
|
resetSideEffects();
|
|
402
509
|
setScheduledTasks(firstInput.scheduledTasks);
|
|
403
510
|
setSessionContext(firstInput.sessionId);
|
|
511
|
+
setGatewayContext(firstInput.gatewayBaseUrl, firstInput.gatewayApiToken, firstInput.channelId);
|
|
404
512
|
setModelContext(firstInput.baseUrl, storedApiKey, firstInput.model, firstInput.chatbotId);
|
|
405
513
|
|
|
406
514
|
const firstOutput = await processRequest(
|
|
@@ -434,6 +542,7 @@ async function main(): Promise<void> {
|
|
|
434
542
|
resetSideEffects();
|
|
435
543
|
setScheduledTasks(input.scheduledTasks);
|
|
436
544
|
setSessionContext(input.sessionId);
|
|
545
|
+
setGatewayContext(input.gatewayBaseUrl, input.gatewayApiToken, input.channelId);
|
|
437
546
|
setModelContext(input.baseUrl, apiKey, input.model, input.chatbotId);
|
|
438
547
|
|
|
439
548
|
const output = await processRequest(
|
package/container/src/tools.ts
CHANGED
|
@@ -47,9 +47,14 @@ let pendingSchedules: ScheduleSideEffect[] = [];
|
|
|
47
47
|
let pendingDelegations: DelegationSideEffect[] = [];
|
|
48
48
|
let injectedTasks: ScheduledTaskInfo[] = [];
|
|
49
49
|
let currentSessionId = '';
|
|
50
|
+
let gatewayBaseUrl = '';
|
|
51
|
+
let gatewayApiToken = '';
|
|
52
|
+
let gatewayChannelId = '';
|
|
50
53
|
const MAX_PENDING_DELEGATIONS = 3;
|
|
51
54
|
const MAX_DELEGATION_BATCH_ITEMS = 6;
|
|
52
55
|
|
|
56
|
+
type DiscordMessageToolAction = 'read' | 'member-info' | 'channel-info';
|
|
57
|
+
|
|
53
58
|
export function resetSideEffects(): void {
|
|
54
59
|
pendingSchedules = [];
|
|
55
60
|
pendingDelegations = [];
|
|
@@ -74,6 +79,12 @@ export function setSessionContext(sessionId: string): void {
|
|
|
74
79
|
currentSessionId = String(sessionId || '');
|
|
75
80
|
}
|
|
76
81
|
|
|
82
|
+
export function setGatewayContext(baseUrl?: string, apiToken?: string, channelId?: string): void {
|
|
83
|
+
gatewayBaseUrl = String(baseUrl || '').trim();
|
|
84
|
+
gatewayApiToken = String(apiToken || '').trim();
|
|
85
|
+
gatewayChannelId = String(channelId || '').trim();
|
|
86
|
+
}
|
|
87
|
+
|
|
77
88
|
export function setModelContext(
|
|
78
89
|
baseUrl: string,
|
|
79
90
|
apiKey: string,
|
|
@@ -83,6 +94,73 @@ export function setModelContext(
|
|
|
83
94
|
setBrowserModelContext(baseUrl, apiKey, model, chatbotId);
|
|
84
95
|
}
|
|
85
96
|
|
|
97
|
+
function readStringValue(value: unknown): string | undefined {
|
|
98
|
+
if (typeof value !== 'string') return undefined;
|
|
99
|
+
const trimmed = value.trim();
|
|
100
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function resolveDiscordMessageAction(rawAction: unknown): DiscordMessageToolAction | null {
|
|
104
|
+
const normalized = readStringValue(rawAction)?.toLowerCase();
|
|
105
|
+
if (!normalized) return null;
|
|
106
|
+
if (normalized === 'read' || normalized === 'readmessages') return 'read';
|
|
107
|
+
if (normalized === 'member-info' || normalized === 'memberinfo') return 'member-info';
|
|
108
|
+
if (normalized === 'channel-info' || normalized === 'channelinfo') return 'channel-info';
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function resolveGatewayDiscordActionUrl(): string | null {
|
|
113
|
+
const base = gatewayBaseUrl.replace(/\/+$/, '');
|
|
114
|
+
if (!base) return null;
|
|
115
|
+
return `${base}/api/discord/action`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function callGatewayDiscordAction(payload: Record<string, unknown>): Promise<string> {
|
|
119
|
+
const url = resolveGatewayDiscordActionUrl();
|
|
120
|
+
if (!url) {
|
|
121
|
+
return 'Error: Discord actions are unavailable because gatewayBaseUrl is not configured.';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const headers: Record<string, string> = {
|
|
125
|
+
'Content-Type': 'application/json',
|
|
126
|
+
};
|
|
127
|
+
if (gatewayApiToken) {
|
|
128
|
+
headers.Authorization = `Bearer ${gatewayApiToken}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let response: Response;
|
|
132
|
+
try {
|
|
133
|
+
response = await fetch(url, {
|
|
134
|
+
method: 'POST',
|
|
135
|
+
headers,
|
|
136
|
+
body: JSON.stringify(payload),
|
|
137
|
+
});
|
|
138
|
+
} catch (err) {
|
|
139
|
+
return `Error: Discord action request failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const rawText = await response.text();
|
|
143
|
+
let parsed: Record<string, unknown> | null = null;
|
|
144
|
+
try {
|
|
145
|
+
const maybe = JSON.parse(rawText) as unknown;
|
|
146
|
+
if (maybe && typeof maybe === 'object' && !Array.isArray(maybe)) {
|
|
147
|
+
parsed = maybe as Record<string, unknown>;
|
|
148
|
+
}
|
|
149
|
+
} catch {
|
|
150
|
+
parsed = null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!response.ok) {
|
|
154
|
+
const detail = typeof parsed?.error === 'string'
|
|
155
|
+
? parsed.error
|
|
156
|
+
: rawText || `HTTP ${response.status}`;
|
|
157
|
+
return `Error: Discord action failed (${response.status}): ${detail}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (parsed) return JSON.stringify(parsed, null, 2);
|
|
161
|
+
return rawText || JSON.stringify({ ok: true }, null, 2);
|
|
162
|
+
}
|
|
163
|
+
|
|
86
164
|
function normalizeDelegationTask(raw: unknown, fallbackModel?: string): DelegationTaskSpec | null {
|
|
87
165
|
if (typeof raw === 'string') {
|
|
88
166
|
const prompt = raw.trim();
|
|
@@ -802,6 +880,68 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
|
|
|
802
880
|
return `Error: unknown memory action "${action}". Use read, append, write, replace, remove, list, or search.`;
|
|
803
881
|
}
|
|
804
882
|
|
|
883
|
+
case 'message': {
|
|
884
|
+
const action = resolveDiscordMessageAction(args.action);
|
|
885
|
+
if (!action) {
|
|
886
|
+
return 'Error: unsupported message action. Use "read", "member-info", or "channel-info".';
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const payload: Record<string, unknown> = { action };
|
|
890
|
+
|
|
891
|
+
if (action === 'read') {
|
|
892
|
+
const channelId =
|
|
893
|
+
readStringValue(args.channelId)
|
|
894
|
+
|| readStringValue(args.to)
|
|
895
|
+
|| readStringValue(args.target)
|
|
896
|
+
|| gatewayChannelId;
|
|
897
|
+
if (!channelId) {
|
|
898
|
+
return 'Error: channelId is required for message action "read".';
|
|
899
|
+
}
|
|
900
|
+
payload.channelId = channelId;
|
|
901
|
+
|
|
902
|
+
if (typeof args.limit === 'number' && Number.isFinite(args.limit)) {
|
|
903
|
+
payload.limit = Math.max(1, Math.min(100, Math.floor(args.limit)));
|
|
904
|
+
}
|
|
905
|
+
const before = readStringValue(args.before);
|
|
906
|
+
const after = readStringValue(args.after);
|
|
907
|
+
const around = readStringValue(args.around);
|
|
908
|
+
if (before) payload.before = before;
|
|
909
|
+
if (after) payload.after = after;
|
|
910
|
+
if (around) payload.around = around;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
if (action === 'member-info') {
|
|
914
|
+
const guildId = readStringValue(args.guildId);
|
|
915
|
+
const userId =
|
|
916
|
+
readStringValue(args.userId)
|
|
917
|
+
|| readStringValue(args.memberId)
|
|
918
|
+
|| readStringValue(args.user)
|
|
919
|
+
|| readStringValue(args.username);
|
|
920
|
+
if (!guildId) {
|
|
921
|
+
return 'Error: guildId is required for message action "member-info".';
|
|
922
|
+
}
|
|
923
|
+
if (!userId) {
|
|
924
|
+
return 'Error: userId/username is required for message action "member-info".';
|
|
925
|
+
}
|
|
926
|
+
payload.guildId = guildId;
|
|
927
|
+
payload.userId = userId;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
if (action === 'channel-info') {
|
|
931
|
+
const channelId =
|
|
932
|
+
readStringValue(args.channelId)
|
|
933
|
+
|| readStringValue(args.to)
|
|
934
|
+
|| readStringValue(args.target)
|
|
935
|
+
|| gatewayChannelId;
|
|
936
|
+
if (!channelId) {
|
|
937
|
+
return 'Error: channelId is required for message action "channel-info".';
|
|
938
|
+
}
|
|
939
|
+
payload.channelId = channelId;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
return await callGatewayDiscordAction(payload);
|
|
943
|
+
}
|
|
944
|
+
|
|
805
945
|
case 'session_search': {
|
|
806
946
|
const query = typeof args.query === 'string' ? args.query.trim() : '';
|
|
807
947
|
if (!query) return 'Error: query is required for session_search';
|
|
@@ -1191,6 +1331,58 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
|
|
1191
1331
|
},
|
|
1192
1332
|
},
|
|
1193
1333
|
},
|
|
1334
|
+
{
|
|
1335
|
+
type: 'function',
|
|
1336
|
+
function: {
|
|
1337
|
+
name: 'message',
|
|
1338
|
+
description:
|
|
1339
|
+
'OpenClaw-style channel action tool. In Discord-backed sessions supports actions: read, member-info, channel-info.',
|
|
1340
|
+
parameters: {
|
|
1341
|
+
type: 'object',
|
|
1342
|
+
properties: {
|
|
1343
|
+
action: {
|
|
1344
|
+
type: 'string',
|
|
1345
|
+
description: 'Action to perform: "read", "member-info", or "channel-info".',
|
|
1346
|
+
enum: ['read', 'member-info', 'channel-info'],
|
|
1347
|
+
},
|
|
1348
|
+
channelId: {
|
|
1349
|
+
type: 'string',
|
|
1350
|
+
description: 'Discord channel id. Defaults to current channel for read/channel-info.',
|
|
1351
|
+
},
|
|
1352
|
+
guildId: {
|
|
1353
|
+
type: 'string',
|
|
1354
|
+
description: 'Discord guild id (required for member-info).',
|
|
1355
|
+
},
|
|
1356
|
+
userId: {
|
|
1357
|
+
type: 'string',
|
|
1358
|
+
description: 'Discord user id (required for member-info).',
|
|
1359
|
+
},
|
|
1360
|
+
memberId: {
|
|
1361
|
+
type: 'string',
|
|
1362
|
+
description: 'Alias for userId in member-info.',
|
|
1363
|
+
},
|
|
1364
|
+
username: {
|
|
1365
|
+
type: 'string',
|
|
1366
|
+
description: 'Discord username/display name/@handle to resolve for member-info.',
|
|
1367
|
+
},
|
|
1368
|
+
user: {
|
|
1369
|
+
type: 'string',
|
|
1370
|
+
description: 'Alias for username/userId in member-info.',
|
|
1371
|
+
},
|
|
1372
|
+
limit: {
|
|
1373
|
+
type: 'number',
|
|
1374
|
+
description: 'Read limit for action="read" (default 20, max 100).',
|
|
1375
|
+
},
|
|
1376
|
+
before: { type: 'string', description: 'Read messages before this message id.' },
|
|
1377
|
+
after: { type: 'string', description: 'Read messages after this message id.' },
|
|
1378
|
+
around: { type: 'string', description: 'Read messages around this message id.' },
|
|
1379
|
+
target: { type: 'string', description: 'Alias for channelId.' },
|
|
1380
|
+
to: { type: 'string', description: 'Alias for channelId.' },
|
|
1381
|
+
},
|
|
1382
|
+
required: ['action'],
|
|
1383
|
+
},
|
|
1384
|
+
},
|
|
1385
|
+
},
|
|
1194
1386
|
{
|
|
1195
1387
|
type: 'function',
|
|
1196
1388
|
function: {
|
package/container/src/types.ts
CHANGED
|
@@ -67,6 +67,8 @@ export interface ContainerInput {
|
|
|
67
67
|
enableRag: boolean;
|
|
68
68
|
apiKey: string;
|
|
69
69
|
baseUrl: string;
|
|
70
|
+
gatewayBaseUrl?: string;
|
|
71
|
+
gatewayApiToken?: string;
|
|
70
72
|
model: string;
|
|
71
73
|
channelId: string;
|
|
72
74
|
scheduledTasks?: { id: number; cronExpr: string; runAt: string | null; everyMs: number | null; prompt: string; enabled: number; lastRun: string | null; createdAt: string }[];
|
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,8 @@ import path from 'path';
|
|
|
4
4
|
import { spawn, spawnSync } from 'child_process';
|
|
5
5
|
import readline from 'readline/promises';
|
|
6
6
|
import { ensureHybridAICredentials } from './onboarding.js';
|
|
7
|
-
import { DATA_DIR, GATEWAY_BASE_URL, MissingRequiredEnvVarError } from './config.js';
|
|
7
|
+
import { APP_VERSION, DATA_DIR, GATEWAY_BASE_URL, MissingRequiredEnvVarError } from './config.js';
|
|
8
|
+
import { printUpdateUsage, runUpdateCommand } from './update.js';
|
|
8
9
|
async function ensureRuntimeContainer(commandName, required = true) {
|
|
9
10
|
const { ensureContainerImageReady } = await import('./container-setup.js');
|
|
10
11
|
await ensureContainerImageReady({
|
|
@@ -152,6 +153,7 @@ function printMainUsage() {
|
|
|
152
153
|
gateway Manage core runtime (start/stop/status) or run gateway commands
|
|
153
154
|
tui Start terminal adapter (starts gateway automatically when needed)
|
|
154
155
|
onboarding Run HybridAI account/API key onboarding
|
|
156
|
+
update Check and apply HybridClaw CLI updates
|
|
155
157
|
audit Inspect/verify structured audit trail
|
|
156
158
|
help Show general or topic-specific help (e.g. \`hybridclaw help gateway\`)`);
|
|
157
159
|
}
|
|
@@ -205,6 +207,7 @@ Topics:
|
|
|
205
207
|
gateway Help for gateway lifecycle and passthrough commands
|
|
206
208
|
tui Help for terminal client
|
|
207
209
|
onboarding Help for onboarding flow
|
|
210
|
+
update Help for checking/applying CLI updates
|
|
208
211
|
audit Help for audit commands
|
|
209
212
|
help This help`);
|
|
210
213
|
}
|
|
@@ -225,6 +228,9 @@ function printHelpTopic(topic) {
|
|
|
225
228
|
case 'onboarding':
|
|
226
229
|
printOnboardingUsage();
|
|
227
230
|
return true;
|
|
231
|
+
case 'update':
|
|
232
|
+
printUpdateUsage();
|
|
233
|
+
return true;
|
|
228
234
|
case 'audit':
|
|
229
235
|
printAuditUsage();
|
|
230
236
|
return true;
|
|
@@ -639,6 +645,14 @@ async function main() {
|
|
|
639
645
|
await ensureHybridAICredentials({ force: true, commandName: 'hybridclaw onboarding' });
|
|
640
646
|
await ensureRuntimeContainer('hybridclaw onboarding', false);
|
|
641
647
|
break;
|
|
648
|
+
case 'update': {
|
|
649
|
+
if (isHelpRequest(subargs)) {
|
|
650
|
+
printUpdateUsage();
|
|
651
|
+
break;
|
|
652
|
+
}
|
|
653
|
+
await runUpdateCommand(subargs, APP_VERSION);
|
|
654
|
+
break;
|
|
655
|
+
}
|
|
642
656
|
case 'audit': {
|
|
643
657
|
if (isHelpRequest(subargs)) {
|
|
644
658
|
printAuditUsage();
|