openclaw-server 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/package.json +29 -0
  2. package/packs/default/faq.yaml +8 -0
  3. package/packs/default/intents.yaml +19 -0
  4. package/packs/default/pack.yaml +12 -0
  5. package/packs/default/policies.yaml +1 -0
  6. package/packs/default/scenarios.yaml +1 -0
  7. package/packs/default/synonyms.yaml +1 -0
  8. package/packs/default/templates.yaml +16 -0
  9. package/packs/default/tools.yaml +1 -0
  10. package/readme.md +1219 -0
  11. package/src/auth.ts +24 -0
  12. package/src/better-sqlite3.d.ts +17 -0
  13. package/src/config.ts +63 -0
  14. package/src/core/matcher.ts +214 -0
  15. package/src/core/normalizer.test.ts +37 -0
  16. package/src/core/normalizer.ts +183 -0
  17. package/src/core/pack-loader.ts +97 -0
  18. package/src/core/reply-engine.test.ts +76 -0
  19. package/src/core/reply-engine.ts +256 -0
  20. package/src/core/request-adapter.ts +65 -0
  21. package/src/core/session-store.ts +48 -0
  22. package/src/core/stream-renderer.ts +237 -0
  23. package/src/core/tool-engine.ts +60 -0
  24. package/src/debug-log.ts +211 -0
  25. package/src/index.ts +23 -0
  26. package/src/openai.ts +79 -0
  27. package/src/response-api.ts +107 -0
  28. package/src/routes/admin.ts +32 -0
  29. package/src/routes/chat-completions.ts +173 -0
  30. package/src/routes/health.ts +7 -0
  31. package/src/routes/models.ts +21 -0
  32. package/src/routes/request-validation.ts +33 -0
  33. package/src/routes/responses.ts +182 -0
  34. package/src/routes/tasks.ts +138 -0
  35. package/src/runtime-stats.ts +80 -0
  36. package/src/server.test.ts +776 -0
  37. package/src/server.ts +108 -0
  38. package/src/tasks/chat-integration.ts +70 -0
  39. package/src/tasks/service.ts +320 -0
  40. package/src/tasks/store.test.ts +183 -0
  41. package/src/tasks/store.ts +602 -0
  42. package/src/tasks/time-parser.test.ts +94 -0
  43. package/src/tasks/time-parser.ts +610 -0
  44. package/src/tasks/timezone.ts +171 -0
  45. package/src/tasks/types.ts +128 -0
  46. package/src/types.ts +202 -0
  47. package/src/weather/chat-integration.ts +56 -0
  48. package/src/weather/location-catalog.ts +166 -0
  49. package/src/weather/open-meteo-provider.ts +221 -0
  50. package/src/weather/parser.test.ts +23 -0
  51. package/src/weather/parser.ts +102 -0
  52. package/src/weather/service.test.ts +54 -0
  53. package/src/weather/service.ts +188 -0
  54. package/src/weather/types.ts +56 -0
@@ -0,0 +1,21 @@
1
+ import type { Express } from "express";
2
+ import { requireBearerAuth } from "../auth.js";
3
+ import type { AppContext } from "../server.js";
4
+
5
+ export function registerModelsRoute(app: Express, context: AppContext): void {
6
+ app.get("/v1/models", (req, res) => {
7
+ if (!requireBearerAuth(req, res, context.config)) {
8
+ context.recordAuthFailure();
9
+ return;
10
+ }
11
+
12
+ res.json({
13
+ object: "list",
14
+ data: context.pack.manifest.models.map((id) => ({
15
+ id,
16
+ object: "model",
17
+ owned_by: "openclaw-server",
18
+ })),
19
+ });
20
+ });
21
+ }
@@ -0,0 +1,33 @@
1
+ import type { ToolDefinition, ToolChoice } from "../types.js";
2
+
3
+ function resolveRequestedToolName(toolChoice: ToolChoice | undefined): string | undefined {
4
+ if (!toolChoice || typeof toolChoice !== "object") {
5
+ return undefined;
6
+ }
7
+ return toolChoice.function.name;
8
+ }
9
+
10
+ export function validateToolChoice(params: {
11
+ tools: ToolDefinition[];
12
+ toolChoice: ToolChoice | undefined;
13
+ }): string | undefined {
14
+ const { tools, toolChoice } = params;
15
+ if (!toolChoice || toolChoice === "auto" || toolChoice === "none") {
16
+ return undefined;
17
+ }
18
+ if (tools.length === 0) {
19
+ return "tool_choice was provided but no tools were supplied";
20
+ }
21
+ if (toolChoice === "required") {
22
+ return undefined;
23
+ }
24
+
25
+ const requestedName = resolveRequestedToolName(toolChoice);
26
+ if (!requestedName) {
27
+ return "tool_choice.function.name is required";
28
+ }
29
+ if (!tools.some((tool) => tool.function.name === requestedName)) {
30
+ return `tool_choice requested unknown tool: ${requestedName}`;
31
+ }
32
+ return undefined;
33
+ }
@@ -0,0 +1,182 @@
1
+ import type { Express } from "express";
2
+ import { requireBearerAuth } from "../auth.js";
3
+ import { normalizeRequest } from "../core/normalizer.js";
4
+ import { responseRequestToChatRequest } from "../core/request-adapter.js";
5
+ import { streamResponsesText, streamResponsesToolCall } from "../core/stream-renderer.js";
6
+ import {
7
+ logRequestDebug,
8
+ logResponseSelection,
9
+ logTaskInspection,
10
+ logWeatherInspection,
11
+ } from "../debug-log.js";
12
+ import {
13
+ buildResponsesResponse,
14
+ createResponseId,
15
+ createResponseOutputId,
16
+ } from "../response-api.js";
17
+ import type { AppContext } from "../server.js";
18
+ import { buildTaskEngineResult, inspectTaskMessage } from "../tasks/chat-integration.js";
19
+ import { ResponsesRequestSchema } from "../types.js";
20
+ import { inspectWeatherMessage, respondToWeatherMessage } from "../weather/chat-integration.js";
21
+ import { validateToolChoice } from "./request-validation.js";
22
+
23
+ export function registerResponsesRoute(app: Express, context: AppContext): void {
24
+ app.post("/v1/responses", async (req, res) => {
25
+ if (!requireBearerAuth(req, res, context.config)) {
26
+ context.recordAuthFailure();
27
+ return;
28
+ }
29
+
30
+ const parsed = ResponsesRequestSchema.safeParse(req.body);
31
+ if (!parsed.success) {
32
+ const issue = parsed.error.issues[0];
33
+ res.status(400).json({
34
+ error: {
35
+ message: issue ? `${issue.path.join(".")}: ${issue.message}` : "Invalid request body",
36
+ type: "invalid_request_error",
37
+ },
38
+ });
39
+ return;
40
+ }
41
+
42
+ const toolChoiceError = validateToolChoice({
43
+ tools: parsed.data.tools ?? [],
44
+ toolChoice: parsed.data.tool_choice,
45
+ });
46
+ if (toolChoiceError) {
47
+ res.status(400).json({
48
+ error: {
49
+ message: toolChoiceError,
50
+ type: "invalid_request_error",
51
+ },
52
+ });
53
+ return;
54
+ }
55
+
56
+ const turn = normalizeRequest({
57
+ request: responseRequestToChatRequest(parsed.data),
58
+ defaultModelId: context.config.defaultModelId,
59
+ });
60
+ logRequestDebug({
61
+ enabled: context.config.debugLoggingEnabled,
62
+ route: "/v1/responses",
63
+ body: parsed.data,
64
+ turn,
65
+ previewChars: context.config.debugPreviewChars,
66
+ });
67
+ if (!turn.userText) {
68
+ logRequestDebug({
69
+ enabled: context.config.debugLoggingEnabled,
70
+ route: "/v1/responses",
71
+ body: parsed.data,
72
+ turn,
73
+ previewChars: context.config.debugPreviewChars,
74
+ note: "missing_user_text",
75
+ });
76
+ res.status(400).json({
77
+ error: {
78
+ message: "Missing user input in input.",
79
+ type: "invalid_request_error",
80
+ },
81
+ });
82
+ return;
83
+ }
84
+
85
+ const responseId = createResponseId();
86
+ const now = typeof req.body?.now === "string" ? req.body.now : undefined;
87
+ const taskMatch = inspectTaskMessage({
88
+ taskService: context.taskService,
89
+ turn,
90
+ explicitUser: parsed.data.user,
91
+ now,
92
+ });
93
+ logTaskInspection({
94
+ enabled: context.config.debugLoggingEnabled,
95
+ route: "/v1/responses",
96
+ userId: taskMatch.userId,
97
+ text: turn.userText,
98
+ inspection: taskMatch.inspection,
99
+ previewChars: context.config.debugPreviewChars,
100
+ });
101
+
102
+ const taskResult = taskMatch.inspection.shouldHandle
103
+ ? buildTaskEngineResult({
104
+ turn,
105
+ taskResult: context.taskService.processMessage({
106
+ userId: taskMatch.userId,
107
+ text: turn.userText,
108
+ now,
109
+ }),
110
+ })
111
+ : undefined;
112
+ const weatherMatch = !taskResult
113
+ ? inspectWeatherMessage({
114
+ weatherService: context.weatherService,
115
+ turn,
116
+ explicitUser: parsed.data.user,
117
+ })
118
+ : undefined;
119
+ if (weatherMatch) {
120
+ logWeatherInspection({
121
+ enabled: context.config.debugLoggingEnabled,
122
+ route: "/v1/responses",
123
+ userId: weatherMatch.userId,
124
+ text: turn.userText,
125
+ inspection: weatherMatch.inspection,
126
+ previewChars: context.config.debugPreviewChars,
127
+ });
128
+ }
129
+
130
+ const weatherResult =
131
+ !taskResult && weatherMatch?.inspection.shouldHandle
132
+ ? await respondToWeatherMessage({
133
+ weatherService: context.weatherService,
134
+ turn,
135
+ explicitUser: parsed.data.user,
136
+ })
137
+ : undefined;
138
+ const result = taskResult ?? weatherResult ?? context.replyEngine.respond(turn);
139
+ logResponseSelection({
140
+ enabled: context.config.debugLoggingEnabled,
141
+ route: "/v1/responses",
142
+ source: taskResult ? "task" : weatherResult ? "weather" : "reply",
143
+ finishReason: result.finishReason,
144
+ matchedIntentId: result.matchedIntentId,
145
+ templateId: result.templateId,
146
+ text: result.text,
147
+ previewChars: context.config.debugPreviewChars,
148
+ });
149
+ context.recordCompletion({
150
+ stream: turn.stream,
151
+ finishReason: result.finishReason,
152
+ });
153
+
154
+ const outputItemId = createResponseOutputId(
155
+ result.finishReason === "tool_calls" ? "fc" : "msg",
156
+ );
157
+
158
+ if (!turn.stream) {
159
+ res.json(buildResponsesResponse({ id: responseId, result, outputItemId }));
160
+ return;
161
+ }
162
+
163
+ if (result.finishReason === "tool_calls" && result.toolCalls?.length) {
164
+ await streamResponsesToolCall({
165
+ res,
166
+ id: responseId,
167
+ result,
168
+ outputItemId,
169
+ });
170
+ return;
171
+ }
172
+
173
+ await streamResponsesText({
174
+ res,
175
+ id: responseId,
176
+ result,
177
+ outputItemId,
178
+ initialDelayMs: context.config.streamInitialDelayMs,
179
+ chunkChars: context.config.streamChunkChars,
180
+ });
181
+ });
182
+ }
@@ -0,0 +1,138 @@
1
+ import type { Express } from "express";
2
+ import { requireBearerAuth } from "../auth.js";
3
+ import { logResponseSelection, logTaskInspection } from "../debug-log.js";
4
+ import type { AppContext } from "../server.js";
5
+ import {
6
+ TaskChatRequestSchema,
7
+ TaskListQuerySchema,
8
+ TaskReminderQuerySchema,
9
+ TaskStatsQuerySchema,
10
+ } from "../tasks/types.js";
11
+
12
+ function validationError(res: import("express").Response, issue?: { path: PropertyKey[]; message: string }) {
13
+ res.status(400).json({
14
+ error: {
15
+ message: issue ? `${issue.path.join(".")}: ${issue.message}` : "Invalid request",
16
+ type: "invalid_request_error",
17
+ },
18
+ });
19
+ }
20
+
21
+ export function registerTaskRoutes(app: Express, context: AppContext): void {
22
+ app.post("/v1/tasks/chat", (req, res) => {
23
+ if (!requireBearerAuth(req, res, context.config)) {
24
+ context.recordAuthFailure();
25
+ return;
26
+ }
27
+
28
+ const parsed = TaskChatRequestSchema.safeParse(req.body);
29
+ if (!parsed.success) {
30
+ validationError(res, parsed.error.issues[0]);
31
+ return;
32
+ }
33
+
34
+ const inspection = context.taskService.inspectMessage({
35
+ userId: parsed.data.user,
36
+ text: parsed.data.text,
37
+ now: parsed.data.now,
38
+ });
39
+ logTaskInspection({
40
+ enabled: context.config.debugLoggingEnabled,
41
+ route: "/v1/tasks/chat",
42
+ userId: parsed.data.user,
43
+ text: parsed.data.text,
44
+ inspection,
45
+ previewChars: context.config.debugPreviewChars,
46
+ });
47
+
48
+ const result = context.taskService.processMessage({
49
+ userId: parsed.data.user,
50
+ text: parsed.data.text,
51
+ now: parsed.data.now,
52
+ });
53
+ logResponseSelection({
54
+ enabled: context.config.debugLoggingEnabled,
55
+ route: "/v1/tasks/chat",
56
+ source: "task",
57
+ finishReason: "stop",
58
+ matchedIntentId: `task.${result.intent}`,
59
+ templateId: `task.${result.intent}`,
60
+ text: result.reply,
61
+ previewChars: context.config.debugPreviewChars,
62
+ });
63
+ res.json(result);
64
+ });
65
+
66
+ app.get("/v1/tasks", (req, res) => {
67
+ if (!requireBearerAuth(req, res, context.config)) {
68
+ context.recordAuthFailure();
69
+ return;
70
+ }
71
+
72
+ const parsed = TaskListQuerySchema.safeParse({
73
+ user: typeof req.query.user === "string" ? req.query.user : undefined,
74
+ scope: typeof req.query.scope === "string" ? req.query.scope : undefined,
75
+ now: typeof req.query.now === "string" ? req.query.now : undefined,
76
+ });
77
+ if (!parsed.success) {
78
+ validationError(res, parsed.error.issues[0]);
79
+ return;
80
+ }
81
+
82
+ const data = context.taskService.listTasks(
83
+ parsed.data.user,
84
+ parsed.data.scope ?? "today",
85
+ parsed.data.now,
86
+ );
87
+ res.json({ object: "list", data });
88
+ });
89
+
90
+ app.get("/v1/tasks/reminders/pending", (req, res) => {
91
+ if (!requireBearerAuth(req, res, context.config)) {
92
+ context.recordAuthFailure();
93
+ return;
94
+ }
95
+
96
+ const parsed = TaskReminderQuerySchema.safeParse({
97
+ user: typeof req.query.user === "string" ? req.query.user : undefined,
98
+ now: typeof req.query.now === "string" ? req.query.now : undefined,
99
+ });
100
+ if (!parsed.success) {
101
+ validationError(res, parsed.error.issues[0]);
102
+ return;
103
+ }
104
+
105
+ const data = context.taskService.listPendingReminders(parsed.data.user, parsed.data.now);
106
+ res.json({ object: "list", data, count: data.length });
107
+ });
108
+
109
+ app.get("/v1/tasks/stats", (req, res) => {
110
+ if (!requireBearerAuth(req, res, context.config)) {
111
+ context.recordAuthFailure();
112
+ return;
113
+ }
114
+
115
+ const parsed = TaskStatsQuerySchema.safeParse({
116
+ user: typeof req.query.user === "string" ? req.query.user : undefined,
117
+ now: typeof req.query.now === "string" ? req.query.now : undefined,
118
+ });
119
+ if (!parsed.success) {
120
+ validationError(res, parsed.error.issues[0]);
121
+ return;
122
+ }
123
+
124
+ const data = context.taskService.getStats(parsed.data.user, parsed.data.now);
125
+ res.json({ object: "task.stats", data });
126
+ });
127
+
128
+ app.post("/admin/tasks/dispatch", (req, res) => {
129
+ if (!requireBearerAuth(req, res, context.config)) {
130
+ context.recordAuthFailure();
131
+ return;
132
+ }
133
+
134
+ const now = typeof req.body?.now === "string" ? req.body.now : undefined;
135
+ const dispatched = context.taskService.dispatchDueReminders(now);
136
+ res.json({ ok: true, dispatched });
137
+ });
138
+ }
@@ -0,0 +1,80 @@
1
+ import { SessionStore } from "./core/session-store.js";
2
+ import type { LoadedPack } from "./types.js";
3
+
4
+ export type RuntimeStatsSnapshot = {
5
+ startedAt: string;
6
+ lastRequestAt?: string;
7
+ lastReloadAt?: string;
8
+ requestsTotal: number;
9
+ streamedRequests: number;
10
+ textResponses: number;
11
+ toolCallResponses: number;
12
+ authFailures: number;
13
+ reloadCount: number;
14
+ activeSessions: number;
15
+ pack: {
16
+ id: string;
17
+ models: string[];
18
+ intents: number;
19
+ faqs: number;
20
+ scenarios: number;
21
+ templates: number;
22
+ };
23
+ };
24
+
25
+ export class RuntimeStats {
26
+ private readonly startedAt = new Date().toISOString();
27
+ private lastRequestAt?: string;
28
+ private lastReloadAt?: string;
29
+ private requestsTotal = 0;
30
+ private streamedRequests = 0;
31
+ private textResponses = 0;
32
+ private toolCallResponses = 0;
33
+ private authFailures = 0;
34
+ private reloadCount = 0;
35
+
36
+ recordAuthFailure(): void {
37
+ this.authFailures += 1;
38
+ }
39
+
40
+ recordCompletion(params: { stream: boolean; finishReason: "stop" | "tool_calls" }): void {
41
+ this.requestsTotal += 1;
42
+ this.lastRequestAt = new Date().toISOString();
43
+ if (params.stream) {
44
+ this.streamedRequests += 1;
45
+ }
46
+ if (params.finishReason === "tool_calls") {
47
+ this.toolCallResponses += 1;
48
+ return;
49
+ }
50
+ this.textResponses += 1;
51
+ }
52
+
53
+ recordReload(): void {
54
+ this.reloadCount += 1;
55
+ this.lastReloadAt = new Date().toISOString();
56
+ }
57
+
58
+ snapshot(pack: LoadedPack, sessionStore: SessionStore): RuntimeStatsSnapshot {
59
+ return {
60
+ startedAt: this.startedAt,
61
+ lastRequestAt: this.lastRequestAt,
62
+ lastReloadAt: this.lastReloadAt,
63
+ requestsTotal: this.requestsTotal,
64
+ streamedRequests: this.streamedRequests,
65
+ textResponses: this.textResponses,
66
+ toolCallResponses: this.toolCallResponses,
67
+ authFailures: this.authFailures,
68
+ reloadCount: this.reloadCount,
69
+ activeSessions: sessionStore.size(),
70
+ pack: {
71
+ id: pack.manifest.id,
72
+ models: [...pack.manifest.models],
73
+ intents: pack.intents.length,
74
+ faqs: pack.faqs.length,
75
+ scenarios: pack.scenariosById.size,
76
+ templates: pack.templatesById.size,
77
+ },
78
+ };
79
+ }
80
+ }