@lobu/worker 3.0.8 → 3.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/index.d.ts.map +1 -1
  2. package/dist/index.js +4 -6
  3. package/dist/index.js.map +1 -1
  4. package/dist/openclaw/session-context.d.ts.map +1 -1
  5. package/dist/openclaw/session-context.js +1 -1
  6. package/dist/openclaw/session-context.js.map +1 -1
  7. package/package.json +2 -2
  8. package/USAGE.md +0 -120
  9. package/docs/custom-base-image.md +0 -88
  10. package/scripts/worker-entrypoint.sh +0 -184
  11. package/src/__tests__/audio-provider-suggestions.test.ts +0 -198
  12. package/src/__tests__/embedded-just-bash-bootstrap.test.ts +0 -39
  13. package/src/__tests__/embedded-tools.test.ts +0 -558
  14. package/src/__tests__/instructions.test.ts +0 -59
  15. package/src/__tests__/memory-flush-runtime.test.ts +0 -138
  16. package/src/__tests__/memory-flush.test.ts +0 -64
  17. package/src/__tests__/model-resolver.test.ts +0 -156
  18. package/src/__tests__/processor.test.ts +0 -225
  19. package/src/__tests__/setup.ts +0 -109
  20. package/src/__tests__/sse-client.test.ts +0 -48
  21. package/src/__tests__/tool-policy.test.ts +0 -269
  22. package/src/__tests__/worker.test.ts +0 -89
  23. package/src/core/error-handler.ts +0 -70
  24. package/src/core/project-scanner.ts +0 -65
  25. package/src/core/types.ts +0 -125
  26. package/src/core/url-utils.ts +0 -9
  27. package/src/core/workspace.ts +0 -138
  28. package/src/embedded/just-bash-bootstrap.ts +0 -228
  29. package/src/gateway/gateway-integration.ts +0 -287
  30. package/src/gateway/message-batcher.ts +0 -128
  31. package/src/gateway/sse-client.ts +0 -955
  32. package/src/gateway/types.ts +0 -68
  33. package/src/index.ts +0 -146
  34. package/src/instructions/builder.ts +0 -80
  35. package/src/instructions/providers.ts +0 -27
  36. package/src/modules/lifecycle.ts +0 -92
  37. package/src/openclaw/custom-tools.ts +0 -290
  38. package/src/openclaw/instructions.ts +0 -38
  39. package/src/openclaw/model-resolver.ts +0 -150
  40. package/src/openclaw/plugin-loader.ts +0 -427
  41. package/src/openclaw/processor.ts +0 -216
  42. package/src/openclaw/session-context.ts +0 -277
  43. package/src/openclaw/tool-policy.ts +0 -212
  44. package/src/openclaw/tools.ts +0 -208
  45. package/src/openclaw/worker.ts +0 -1792
  46. package/src/server.ts +0 -329
  47. package/src/shared/audio-provider-suggestions.ts +0 -132
  48. package/src/shared/processor-utils.ts +0 -33
  49. package/src/shared/provider-auth-hints.ts +0 -64
  50. package/src/shared/tool-display-config.ts +0 -75
  51. package/src/shared/tool-implementations.ts +0 -768
  52. package/tsconfig.json +0 -21
package/src/server.ts DELETED
@@ -1,329 +0,0 @@
1
- /**
2
- * Worker HTTP server for serving session data and health checks.
3
- * Lightweight Hono server started before SSE gateway connection.
4
- */
5
-
6
- import { readFile } from "node:fs/promises";
7
- import { createServer } from "node:http";
8
- import { join } from "node:path";
9
- import { getRequestListener } from "@hono/node-server";
10
- import { createLogger } from "@lobu/core";
11
- import { Hono } from "hono";
12
-
13
- const logger = createLogger("worker-http");
14
-
15
- const app = new Hono();
16
-
17
- async function findSessionFile(): Promise<string | null> {
18
- const { readdir, stat } = await import("node:fs/promises");
19
- const workspaceDir = process.env.WORKSPACE_DIR || "/workspace";
20
-
21
- // Direct path: {WORKSPACE_DIR}/.openclaw/session.jsonl
22
- const directPath = join(workspaceDir, ".openclaw", "session.jsonl");
23
- try {
24
- await stat(directPath);
25
- return directPath;
26
- } catch {
27
- // Not found, search subdirectories
28
- }
29
-
30
- // Search one level deep: {WORKSPACE_DIR}/{subdir}/.openclaw/session.jsonl
31
- try {
32
- const entries = await readdir(workspaceDir, { withFileTypes: true });
33
- for (const entry of entries) {
34
- if (entry.isDirectory() && !entry.name.startsWith(".")) {
35
- const subPath = join(
36
- workspaceDir,
37
- entry.name,
38
- ".openclaw",
39
- "session.jsonl"
40
- );
41
- try {
42
- await stat(subPath);
43
- return subPath;
44
- } catch {
45
- // Not in this subdir
46
- }
47
- }
48
- }
49
- } catch {
50
- // Can't read workspace dir
51
- }
52
-
53
- return null;
54
- }
55
-
56
- interface SessionEntry {
57
- type: string;
58
- id: string;
59
- parentId: string | null;
60
- timestamp: string;
61
- message?: {
62
- role: string;
63
- content: unknown;
64
- usage?: { inputTokens?: number; outputTokens?: number };
65
- };
66
- summary?: string;
67
- provider?: string;
68
- modelId?: string;
69
- customType?: string;
70
- content?: unknown;
71
- display?: boolean;
72
- tokensBefore?: number;
73
- firstKeptEntryId?: string;
74
- }
75
-
76
- interface ParsedMessage {
77
- id: string;
78
- type: string;
79
- role?: string;
80
- content: unknown;
81
- model?: string;
82
- timestamp: string;
83
- isVerbose?: boolean;
84
- usage?: { inputTokens?: number; outputTokens?: number };
85
- }
86
-
87
- function parseSessionFile(content: string): {
88
- entries: SessionEntry[];
89
- sessionId?: string;
90
- } {
91
- const lines = content.split("\n").filter((l) => l.trim());
92
- const entries: SessionEntry[] = [];
93
- let sessionId: string | undefined;
94
-
95
- for (const line of lines) {
96
- try {
97
- const parsed = JSON.parse(line);
98
- if (parsed.type === "session") {
99
- sessionId = parsed.id;
100
- continue;
101
- }
102
- entries.push(parsed);
103
- } catch {
104
- // Skip malformed lines
105
- }
106
- }
107
-
108
- return { entries, sessionId };
109
- }
110
-
111
- function entryToMessage(entry: SessionEntry): ParsedMessage | null {
112
- if (entry.type === "message" && entry.message) {
113
- const role = entry.message.role;
114
- const isVerbose = role === "toolResult";
115
- return {
116
- id: entry.id,
117
- type: "message",
118
- role,
119
- content: entry.message.content,
120
- timestamp: entry.timestamp,
121
- isVerbose,
122
- usage: entry.message.usage,
123
- };
124
- }
125
-
126
- if (entry.type === "compaction") {
127
- return {
128
- id: entry.id,
129
- type: "compaction",
130
- content: entry.summary || "",
131
- timestamp: entry.timestamp,
132
- isVerbose: true,
133
- };
134
- }
135
-
136
- if (entry.type === "model_change") {
137
- return {
138
- id: entry.id,
139
- type: "model_change",
140
- content: `${entry.provider}/${entry.modelId}`,
141
- model: `${entry.provider}/${entry.modelId}`,
142
- timestamp: entry.timestamp,
143
- isVerbose: true,
144
- };
145
- }
146
-
147
- if (entry.type === "custom_message") {
148
- return {
149
- id: entry.id,
150
- type: "custom_message",
151
- role: "user",
152
- content: entry.content,
153
- timestamp: entry.timestamp,
154
- isVerbose: !entry.display,
155
- };
156
- }
157
-
158
- return null;
159
- }
160
-
161
- // Health check
162
- app.get("/health", (c) => c.json({ status: "ok" }));
163
-
164
- // Full session messages with cursor-based pagination
165
- app.get("/session/messages", async (c) => {
166
- const cursor = c.req.query("cursor");
167
- const limit = Math.min(parseInt(c.req.query("limit") || "50", 10), 200);
168
-
169
- try {
170
- const sessionPath = await findSessionFile();
171
- if (!sessionPath) {
172
- return c.json({
173
- messages: [],
174
- nextCursor: null,
175
- hasMore: false,
176
- sessionId: "none",
177
- });
178
- }
179
- const content = await readFile(sessionPath, "utf-8");
180
- const { entries, sessionId } = parseSessionFile(content);
181
-
182
- // Convert all entries to messages, filtering nulls
183
- const allMessages: ParsedMessage[] = [];
184
- for (const entry of entries) {
185
- const msg = entryToMessage(entry);
186
- if (msg) allMessages.push(msg);
187
- }
188
-
189
- // Find cursor position
190
- let startIndex = 0;
191
- if (cursor) {
192
- const cursorIdx = allMessages.findIndex((m) => m.id === cursor);
193
- if (cursorIdx >= 0) {
194
- startIndex = cursorIdx + 1;
195
- }
196
- }
197
-
198
- const pageMessages = allMessages.slice(startIndex, startIndex + limit);
199
- const hasMore = startIndex + limit < allMessages.length;
200
- const nextCursor = hasMore
201
- ? pageMessages[pageMessages.length - 1]?.id
202
- : null;
203
-
204
- return c.json({
205
- messages: pageMessages,
206
- nextCursor,
207
- hasMore,
208
- sessionId: sessionId || "unknown",
209
- });
210
- } catch (error: unknown) {
211
- const isNotFound =
212
- error instanceof Error &&
213
- "code" in error &&
214
- (error as NodeJS.ErrnoException).code === "ENOENT";
215
- if (isNotFound) {
216
- return c.json({
217
- messages: [],
218
- nextCursor: null,
219
- hasMore: false,
220
- sessionId: "none",
221
- });
222
- }
223
- logger.error("Failed to read session file", { error });
224
- return c.json({ error: "Failed to read session" }, 500);
225
- }
226
- });
227
-
228
- // Session stats
229
- app.get("/session/stats", async (c) => {
230
- try {
231
- const sessionPath = await findSessionFile();
232
- if (!sessionPath) {
233
- return c.json({
234
- sessionId: "none",
235
- messageCount: 0,
236
- userMessages: 0,
237
- assistantMessages: 0,
238
- totalInputTokens: 0,
239
- totalOutputTokens: 0,
240
- });
241
- }
242
- const content = await readFile(sessionPath, "utf-8");
243
- const { entries, sessionId } = parseSessionFile(content);
244
-
245
- let messageCount = 0;
246
- let userMessages = 0;
247
- let assistantMessages = 0;
248
- let totalInputTokens = 0;
249
- let totalOutputTokens = 0;
250
- let currentModel: string | undefined;
251
-
252
- for (const entry of entries) {
253
- if (entry.type === "message" && entry.message) {
254
- messageCount++;
255
- if (entry.message.role === "user") userMessages++;
256
- if (entry.message.role === "assistant") assistantMessages++;
257
- if (entry.message.usage) {
258
- const u = entry.message.usage as any;
259
- totalInputTokens += u.inputTokens || u.input || 0;
260
- totalOutputTokens += u.outputTokens || u.output || 0;
261
- }
262
- }
263
- if (entry.type === "model_change") {
264
- currentModel = `${entry.provider}/${entry.modelId}`;
265
- }
266
- }
267
-
268
- return c.json({
269
- sessionId: sessionId || "unknown",
270
- messageCount,
271
- userMessages,
272
- assistantMessages,
273
- totalInputTokens,
274
- totalOutputTokens,
275
- currentModel,
276
- });
277
- } catch (error: unknown) {
278
- const isNotFound =
279
- error instanceof Error &&
280
- "code" in error &&
281
- (error as NodeJS.ErrnoException).code === "ENOENT";
282
- if (isNotFound) {
283
- return c.json({
284
- sessionId: "none",
285
- messageCount: 0,
286
- userMessages: 0,
287
- assistantMessages: 0,
288
- totalInputTokens: 0,
289
- totalOutputTokens: 0,
290
- });
291
- }
292
- logger.error("Failed to read session stats", { error });
293
- return c.json({ error: "Failed to read session stats" }, 500);
294
- }
295
- });
296
-
297
- let server: ReturnType<typeof createServer> | null = null;
298
-
299
- export function startWorkerHttpServer(): Promise<number> {
300
- // Use port 0 to let the OS assign a free port (multiple workers share the host network)
301
- const port = parseInt(process.env.WORKER_HTTP_PORT || "0", 10);
302
-
303
- return new Promise((resolve, reject) => {
304
- const listener = getRequestListener(app.fetch);
305
- server = createServer(listener);
306
-
307
- server.on("error", (err) => {
308
- logger.error("Worker HTTP server error", { error: err });
309
- reject(err);
310
- });
311
-
312
- server.listen(port, () => {
313
- const addr = server!.address();
314
- const actualPort = typeof addr === "object" && addr ? addr.port : port;
315
- logger.info(`Worker HTTP server listening on port ${actualPort}`);
316
- resolve(actualPort);
317
- });
318
- });
319
- }
320
-
321
- export function stopWorkerHttpServer(): Promise<void> {
322
- return new Promise((resolve) => {
323
- if (server) {
324
- server.close(() => resolve());
325
- } else {
326
- resolve();
327
- }
328
- });
329
- }
@@ -1,132 +0,0 @@
1
- export interface AudioProviderSuggestions {
2
- providerIds: string[];
3
- providerDisplayList: string;
4
- available: boolean | null;
5
- usedFallback: boolean;
6
- }
7
-
8
- const FALLBACK_PROVIDER_ENTRIES = [
9
- { id: "chatgpt" },
10
- { id: "gemini" },
11
- { id: "elevenlabs" },
12
- ] as const;
13
-
14
- const KNOWN_PROVIDER_LABELS: Record<string, string> = {
15
- chatgpt: "ChatGPT/OpenAI",
16
- openai: "OpenAI",
17
- gemini: "Google Gemini",
18
- google: "Google Gemini",
19
- elevenlabs: "ElevenLabs",
20
- };
21
-
22
- function normalizeProviderId(raw: unknown): string | null {
23
- if (typeof raw !== "string") return null;
24
- const value = raw.trim().toLowerCase();
25
- return value ? value : null;
26
- }
27
-
28
- function prefillProviderIdsFromCapabilityProvider(
29
- providerId: string
30
- ): string[] {
31
- if (providerId === "openai") {
32
- return ["chatgpt", "openai"];
33
- }
34
- if (providerId === "google") {
35
- return ["gemini"];
36
- }
37
- return [providerId];
38
- }
39
-
40
- function normalizeProviderName(providerId: string, rawName: unknown): string {
41
- if (typeof rawName === "string" && rawName.trim()) {
42
- return rawName.trim();
43
- }
44
- return KNOWN_PROVIDER_LABELS[providerId] || providerId;
45
- }
46
-
47
- function getFallbackSuggestions(
48
- available: boolean | null
49
- ): AudioProviderSuggestions {
50
- return {
51
- providerIds: FALLBACK_PROVIDER_ENTRIES.map((entry) => entry.id),
52
- providerDisplayList: "",
53
- available,
54
- usedFallback: true,
55
- };
56
- }
57
-
58
- export function normalizeAudioProviderSuggestions(
59
- payload: unknown
60
- ): AudioProviderSuggestions {
61
- if (typeof payload !== "object" || payload === null) {
62
- return getFallbackSuggestions(null);
63
- }
64
-
65
- const body = payload as {
66
- available?: unknown;
67
- provider?: unknown;
68
- providers?: unknown;
69
- };
70
-
71
- const available = typeof body.available === "boolean" ? body.available : null;
72
-
73
- const ids: string[] = [];
74
- const names: string[] = [];
75
- const addEntry = (providerId: string, providerName: unknown) => {
76
- const mappedIds = prefillProviderIdsFromCapabilityProvider(providerId);
77
- for (const mappedId of mappedIds) {
78
- if (!ids.includes(mappedId)) ids.push(mappedId);
79
- }
80
- const name = normalizeProviderName(providerId, providerName);
81
- if (!names.includes(name)) names.push(name);
82
- };
83
-
84
- if (Array.isArray(body.providers)) {
85
- for (const provider of body.providers) {
86
- if (typeof provider !== "object" || provider === null) continue;
87
- const entry = provider as { provider?: unknown; name?: unknown };
88
- const providerId = normalizeProviderId(entry.provider);
89
- if (!providerId) continue;
90
- addEntry(providerId, entry.name);
91
- }
92
- }
93
-
94
- const primaryProviderId = normalizeProviderId(body.provider);
95
- if (primaryProviderId) {
96
- addEntry(primaryProviderId, undefined);
97
- }
98
-
99
- if (ids.length === 0) {
100
- return getFallbackSuggestions(available);
101
- }
102
-
103
- return {
104
- providerIds: ids,
105
- providerDisplayList: names.join(", "),
106
- available,
107
- usedFallback: false,
108
- };
109
- }
110
-
111
- export async function fetchAudioProviderSuggestions(params: {
112
- gatewayUrl: string;
113
- workerToken: string;
114
- fetchFn?: typeof fetch;
115
- }): Promise<AudioProviderSuggestions> {
116
- const fetchFn = params.fetchFn || fetch;
117
- try {
118
- const response = await fetchFn(
119
- `${params.gatewayUrl}/internal/audio/capabilities`,
120
- {
121
- headers: { Authorization: `Bearer ${params.workerToken}` },
122
- }
123
- );
124
- if (!response.ok) {
125
- return getFallbackSuggestions(null);
126
- }
127
- const payload = (await response.json()) as unknown;
128
- return normalizeAudioProviderSuggestions(payload);
129
- } catch {
130
- return getFallbackSuggestions(null);
131
- }
132
- }
@@ -1,33 +0,0 @@
1
- import { getToolDisplayConfig } from "./tool-display-config";
2
-
3
- /**
4
- * Format MCP-style tool names: prefix__server__tool -> prefix.server.tool
5
- */
6
- function formatMcpToolName(toolName: string): string {
7
- const match = toolName.match(/^([^_]+)__([^_]+)__(.+)$/);
8
- if (!match) return toolName;
9
- const [, prefix, server, tool] = match;
10
- return `${prefix}.${server}.${tool}`;
11
- }
12
-
13
- /**
14
- * Format tool execution for user-friendly display in bullet lists.
15
- * Shared across agent processors.
16
- */
17
- export function formatToolExecution(
18
- toolName: string,
19
- params: Record<string, unknown>,
20
- verboseLogging: boolean
21
- ): string | null {
22
- if (!verboseLogging) return null;
23
-
24
- const config = getToolDisplayConfig(toolName);
25
- const displayName = config ? toolName : formatMcpToolName(toolName);
26
- const emoji = config?.emoji ?? "🔧";
27
-
28
- const inputStr =
29
- Object.keys(params).length > 0
30
- ? `\n\`\`\`json\n${JSON.stringify(params, null, 2)}\n\`\`\``
31
- : "";
32
- return `└ ${emoji} **${displayName}**${inputStr}`;
33
- }
@@ -1,64 +0,0 @@
1
- const PROVIDER_API_KEY_ENV_VARS: Record<string, string> = {
2
- "openai-codex": "OPENAI_API_KEY",
3
- openai: "OPENAI_API_KEY",
4
- google: "GOOGLE_API_KEY",
5
- mistral: "MISTRAL_API_KEY",
6
- anthropic: "ANTHROPIC_API_KEY",
7
- "z-ai": "Z_AI_API_KEY",
8
- };
9
-
10
- function sanitizeProviderToken(value: string): string {
11
- return value
12
- .trim()
13
- .toLowerCase()
14
- .replace(/[^a-z0-9]+/g, "_")
15
- .replace(/^_+|_+$/g, "");
16
- }
17
-
18
- export function getApiKeyEnvVarForProvider(providerName: string): string {
19
- const normalizedProvider = providerName.trim().toLowerCase();
20
- const mapped = PROVIDER_API_KEY_ENV_VARS[normalizedProvider];
21
- if (mapped) {
22
- return mapped;
23
- }
24
-
25
- const sanitized = sanitizeProviderToken(providerName);
26
- if (!sanitized || sanitized === "provider") {
27
- return "API_KEY";
28
- }
29
-
30
- return `${sanitized.toUpperCase()}_API_KEY`;
31
- }
32
-
33
- export function getProviderAuthHintFromError(
34
- errorMessage: string,
35
- defaultProvider?: string
36
- ): { providerName: string; envVar: string } | null {
37
- const needsAuthSetup =
38
- /No API key found|Authentication failed|invalid x-api-key|invalid api[-\s]?key|authentication_error|incorrect api key/i.test(
39
- errorMessage
40
- );
41
- if (!needsAuthSetup) {
42
- return null;
43
- }
44
-
45
- const explicitProviderMatch = errorMessage.match(
46
- /(?:No API key found for|Authentication failed for)\s+"?([A-Za-z0-9_-]+)/i
47
- );
48
- const jsonProviderMatch = errorMessage.match(
49
- /"provider"\s*:\s*"([A-Za-z0-9._-]+)"/i
50
- );
51
- const fallbackProvider = defaultProvider?.trim().toLowerCase() || undefined;
52
- const providerName =
53
- explicitProviderMatch?.[1]?.toLowerCase() ||
54
- jsonProviderMatch?.[1]?.toLowerCase() ||
55
- (fallbackProvider && fallbackProvider !== "undefined"
56
- ? fallbackProvider
57
- : undefined) ||
58
- "provider";
59
-
60
- return {
61
- providerName,
62
- envVar: getApiKeyEnvVarForProvider(providerName),
63
- };
64
- }
@@ -1,75 +0,0 @@
1
- /**
2
- * Shared tool display configuration for progress processors.
3
- * Maps tool names to emoji and description formatting.
4
- */
5
-
6
- export interface ToolDisplayEntry {
7
- emoji: string;
8
- action: string;
9
- getParam: (params: Record<string, unknown>) => string;
10
- }
11
-
12
- const TOOL_DISPLAY_CONFIG: Record<string, ToolDisplayEntry> = {
13
- Write: {
14
- emoji: "✏️",
15
- action: "Writing",
16
- getParam: (p) => `\`${p.file_path || ""}\``,
17
- },
18
- Edit: {
19
- emoji: "✏️",
20
- action: "Editing",
21
- getParam: (p) => `\`${p.file_path || ""}\``,
22
- },
23
- Bash: {
24
- emoji: "👾",
25
- action: "Running",
26
- getParam: (p) => {
27
- const cmd = String(p.command || p.description || "command");
28
- return `\`${cmd.length > 50 ? `${cmd.substring(0, 50)}...` : cmd}\``;
29
- },
30
- },
31
- Read: {
32
- emoji: "📖",
33
- action: "Reading",
34
- getParam: (p) => `\`${p.file_path || p.path || ""}\``,
35
- },
36
- Grep: {
37
- emoji: "🔍",
38
- action: "Searching",
39
- getParam: (p) => `\`${p.pattern || ""}\``,
40
- },
41
- Glob: {
42
- emoji: "🔍",
43
- action: "Finding",
44
- getParam: (p) => `\`${p.pattern || ""}\``,
45
- },
46
- TodoWrite: {
47
- emoji: "📝",
48
- action: "Updating task list",
49
- getParam: () => "",
50
- },
51
- WebFetch: {
52
- emoji: "🌐",
53
- action: "Fetching",
54
- getParam: (p) => `\`${p.url || ""}\``,
55
- },
56
- WebSearch: {
57
- emoji: "🔎",
58
- action: "Searching web",
59
- getParam: (p) => `\`${p.query || ""}\``,
60
- },
61
- };
62
-
63
- /**
64
- * Look up tool display config, case-insensitively.
65
- * OpenClaw uses lowercase tool names (bash, read, write, etc.)
66
- * while some agents use PascalCase (Bash, Read, Write, etc.).
67
- */
68
- export function getToolDisplayConfig(
69
- toolName: string
70
- ): ToolDisplayEntry | undefined {
71
- return (
72
- TOOL_DISPLAY_CONFIG[toolName] ??
73
- TOOL_DISPLAY_CONFIG[toolName.charAt(0).toUpperCase() + toolName.slice(1)]
74
- );
75
- }