@castlekit/castle 0.4.2 → 0.4.3

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 (69) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +2 -2
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/server/app/_global-error.html +2 -2
  5. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  6. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  11. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  12. package/.next/standalone/.next/server/app/_not-found.rsc +1 -1
  13. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  15. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  16. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  17. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  19. package/.next/standalone/.next/server/app/chat.html +1 -1
  20. package/.next/standalone/.next/server/app/chat.rsc +1 -1
  21. package/.next/standalone/.next/server/app/chat.segments/_full.segment.rsc +1 -1
  22. package/.next/standalone/.next/server/app/chat.segments/_head.segment.rsc +1 -1
  23. package/.next/standalone/.next/server/app/chat.segments/_index.segment.rsc +1 -1
  24. package/.next/standalone/.next/server/app/chat.segments/_tree.segment.rsc +1 -1
  25. package/.next/standalone/.next/server/app/chat.segments/chat/__PAGE__.segment.rsc +1 -1
  26. package/.next/standalone/.next/server/app/chat.segments/chat.segment.rsc +1 -1
  27. package/.next/standalone/.next/server/app/index.html +1 -1
  28. package/.next/standalone/.next/server/app/index.rsc +1 -1
  29. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  30. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  31. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  32. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  33. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  34. package/.next/standalone/.next/server/app/settings.html +1 -1
  35. package/.next/standalone/.next/server/app/settings.rsc +1 -1
  36. package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +1 -1
  37. package/.next/standalone/.next/server/app/settings.segments/_head.segment.rsc +1 -1
  38. package/.next/standalone/.next/server/app/settings.segments/_index.segment.rsc +1 -1
  39. package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
  40. package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +1 -1
  41. package/.next/standalone/.next/server/app/settings.segments/settings.segment.rsc +1 -1
  42. package/.next/standalone/.next/server/app/ui-kit.html +1 -1
  43. package/.next/standalone/.next/server/app/ui-kit.rsc +1 -1
  44. package/.next/standalone/.next/server/app/ui-kit.segments/_full.segment.rsc +1 -1
  45. package/.next/standalone/.next/server/app/ui-kit.segments/_head.segment.rsc +1 -1
  46. package/.next/standalone/.next/server/app/ui-kit.segments/_index.segment.rsc +1 -1
  47. package/.next/standalone/.next/server/app/ui-kit.segments/_tree.segment.rsc +1 -1
  48. package/.next/standalone/.next/server/app/ui-kit.segments/ui-kit/__PAGE__.segment.rsc +1 -1
  49. package/.next/standalone/.next/server/app/ui-kit.segments/ui-kit.segment.rsc +1 -1
  50. package/.next/standalone/.next/server/chunks/[root-of-the-server]__2824c41d._.js +1 -1
  51. package/.next/standalone/.next/server/chunks/[root-of-the-server]__361bce14._.js +1 -1
  52. package/.next/standalone/.next/server/middleware-manifest.json +5 -5
  53. package/.next/standalone/.next/server/pages/404.html +1 -1
  54. package/.next/standalone/.next/server/pages/500.html +2 -2
  55. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  56. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  57. package/.next/standalone/CHANGELOG.md +10 -0
  58. package/.next/standalone/package.json +1 -1
  59. package/.next/standalone/src/app/api/openclaw/agents/[id]/avatar/route.ts +51 -91
  60. package/.next/standalone/src/app/api/openclaw/agents/route.ts +12 -0
  61. package/package.json +1 -1
  62. package/src/app/api/openclaw/agents/[id]/avatar/route.ts +51 -91
  63. package/src/app/api/openclaw/agents/route.ts +12 -0
  64. /package/.next/standalone/.next/static/{-R3HvmmPKzJ9Jfjg7hRqd → PzpTWZTNAsAIh96H48auo}/_buildManifest.js +0 -0
  65. /package/.next/standalone/.next/static/{-R3HvmmPKzJ9Jfjg7hRqd → PzpTWZTNAsAIh96H48auo}/_clientMiddlewareManifest.json +0 -0
  66. /package/.next/standalone/.next/static/{-R3HvmmPKzJ9Jfjg7hRqd → PzpTWZTNAsAIh96H48auo}/_ssgManifest.js +0 -0
  67. /package/.next/static/{-R3HvmmPKzJ9Jfjg7hRqd → PzpTWZTNAsAIh96H48auo}/_buildManifest.js +0 -0
  68. /package/.next/static/{-R3HvmmPKzJ9Jfjg7hRqd → PzpTWZTNAsAIh96H48auo}/_clientMiddlewareManifest.json +0 -0
  69. /package/.next/static/{-R3HvmmPKzJ9Jfjg7hRqd → PzpTWZTNAsAIh96H48auo}/_ssgManifest.js +0 -0
@@ -1,8 +1,7 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
2
+ import { writeFileSync, mkdirSync } from "fs";
3
3
  import { join } from "path";
4
4
  import { homedir } from "os";
5
- import JSON5 from "json5";
6
5
  import sharp from "sharp";
7
6
  import { ensureGateway } from "@/lib/gateway-connection";
8
7
  import { checkCsrf } from "@/lib/api-security";
@@ -20,21 +19,6 @@ const ALLOWED_TYPES = new Set([
20
19
  "image/gif",
21
20
  ]);
22
21
 
23
- interface AgentConfig {
24
- id: string;
25
- workspace?: string;
26
- identity?: Record<string, unknown>;
27
- [key: string]: unknown;
28
- }
29
-
30
- interface OpenClawConfig {
31
- agents?: {
32
- list?: AgentConfig[];
33
- [key: string]: unknown;
34
- };
35
- [key: string]: unknown;
36
- }
37
-
38
22
  /**
39
23
  * Resize and compress an avatar image to 256x256, under 100KB.
40
24
  */
@@ -59,27 +43,13 @@ async function processAvatar(input: Buffer): Promise<{ data: Buffer; ext: string
59
43
  return { data, ext: ".jpg" };
60
44
  }
61
45
 
62
- /**
63
- * Find the OpenClaw config file path.
64
- */
65
- function getOpenClawConfigPath(): string | null {
66
- const paths = [
67
- join(homedir(), ".openclaw", "openclaw.json"),
68
- join(homedir(), ".openclaw", "openclaw.json5"),
69
- ];
70
- for (const p of paths) {
71
- if (existsSync(p)) return p;
72
- }
73
- return null;
74
- }
75
-
76
46
  /**
77
47
  * POST /api/openclaw/agents/[id]/avatar
78
48
  *
79
49
  * Upload a new avatar image for an agent.
80
50
  * - Resizes to 256x256 and compresses under 100KB
81
- * - Saves to the agent's workspace directory
82
- * - Writes config file directly (Gateway hot-reloads identity changes, no restart needed)
51
+ * - Saves to Castle's own avatars directory (~/.castle/avatars/)
52
+ * - Updates OpenClaw config via Gateway's config.patch RPC (never writes to OpenClaw files directly)
83
53
  */
84
54
  export async function POST(
85
55
  request: NextRequest,
@@ -128,46 +98,9 @@ export async function POST(
128
98
  );
129
99
  }
130
100
 
131
- // Read the OpenClaw config file directly
132
- const configPath = getOpenClawConfigPath();
133
- if (!configPath) {
134
- return NextResponse.json(
135
- { error: "OpenClaw config file not found" },
136
- { status: 500 }
137
- );
138
- }
139
-
140
- let config: OpenClawConfig;
141
- try {
142
- config = JSON5.parse(readFileSync(configPath, "utf-8"));
143
- } catch (err) {
144
- return NextResponse.json(
145
- { error: `Failed to read config: ${err instanceof Error ? err.message : "unknown"}` },
146
- { status: 500 }
147
- );
148
- }
149
-
150
- const agents = config.agents?.list || [];
151
- const agent = agents.find((a) => a.id === safeId);
152
-
153
- if (!agent) {
154
- return NextResponse.json({ error: "Agent not found" }, { status: 404 });
155
- }
156
-
157
- if (!agent.workspace) {
158
- return NextResponse.json(
159
- { error: "Agent has no workspace configured" },
160
- { status: 400 }
161
- );
162
- }
163
-
164
- const workspacePath = agent.workspace.startsWith("~")
165
- ? join(homedir(), agent.workspace.slice(1))
166
- : agent.workspace;
167
-
168
- // Save processed avatar to workspace
169
- const avatarsDir = join(workspacePath, "avatars");
170
- const fileName = `avatar${processed.ext}`;
101
+ // Save processed avatar to Castle's own directory — never write to OpenClaw's filesystem
102
+ const avatarsDir = join(homedir(), ".castle", "avatars");
103
+ const fileName = `${safeId}${processed.ext}`;
171
104
  const filePath = join(avatarsDir, fileName);
172
105
 
173
106
  try {
@@ -180,37 +113,64 @@ export async function POST(
180
113
  );
181
114
  }
182
115
 
183
- // Update config file directly — Gateway's file watcher hot-reloads identity changes
184
- // No config.patch, no restart, no WebSocket disconnect
116
+ // Update OpenClaw config via Gateway's config.patch RPC the proper way to
117
+ // modify OpenClaw config without writing to its files directly.
118
+ const gw = ensureGateway();
119
+
120
+ if (!gw.isConnected) {
121
+ // Avatar is saved locally; config update will happen when Gateway reconnects
122
+ return NextResponse.json({
123
+ success: true,
124
+ avatar: filePath,
125
+ size: processed.data.length,
126
+ message: "Avatar saved locally. Gateway not connected — config will update when it reconnects.",
127
+ configUpdated: false,
128
+ });
129
+ }
130
+
185
131
  try {
186
- agent.identity = agent.identity || {};
187
- agent.identity.avatar = `avatars/${fileName}`;
188
- writeFileSync(configPath, JSON5.stringify(config, null, 2), "utf-8");
132
+ // Use config.patch to set the agent's avatar path.
133
+ // Gateway validates and applies the patch, then hot-reloads.
134
+ await gw.request("config.patch", {
135
+ agents: {
136
+ list: [
137
+ {
138
+ id: safeId,
139
+ identity: {
140
+ avatar: filePath,
141
+ },
142
+ },
143
+ ],
144
+ },
145
+ });
146
+ console.log(`[Avatar API] Config patched for agent ${safeId}`);
189
147
  } catch (err) {
190
- return NextResponse.json(
191
- { error: `Failed to update config: ${err instanceof Error ? err.message : "unknown"}` },
192
- { status: 500 }
148
+ console.error(
149
+ `[Avatar API] config.patch failed for agent ${safeId}:`,
150
+ err instanceof Error ? err.message : "unknown"
193
151
  );
152
+ // Avatar is still saved locally — not a total failure
153
+ return NextResponse.json({
154
+ success: true,
155
+ avatar: filePath,
156
+ size: processed.data.length,
157
+ message: "Avatar saved but config update failed. Try restarting the Gateway.",
158
+ configUpdated: false,
159
+ });
194
160
  }
195
161
 
196
- // Wait briefly for Gateway's file watcher to hot-reload (~300ms debounce)
197
- await new Promise((resolve) => setTimeout(resolve, 500));
198
-
199
- // Refresh Castle's agent list from the Gateway
162
+ // Emit a signal so SSE clients re-fetch agents
200
163
  try {
201
- const gw = ensureGateway();
202
- if (gw.isConnected) {
203
- // Emit a signal so SSE clients re-fetch agents
204
- gw.emit("agentAvatarUpdated", { agentId: safeId });
205
- }
164
+ gw.emit("agentAvatarUpdated", { agentId: safeId });
206
165
  } catch {
207
166
  // Non-critical
208
167
  }
209
168
 
210
169
  return NextResponse.json({
211
170
  success: true,
212
- avatar: `avatars/${fileName}`,
171
+ avatar: filePath,
213
172
  size: processed.data.length,
214
173
  message: "Avatar updated",
174
+ configUpdated: true,
215
175
  });
216
176
  }
@@ -1,8 +1,12 @@
1
1
  import { NextResponse } from "next/server";
2
+ import { join, resolve } from "path";
3
+ import { homedir } from "os";
2
4
  import { ensureGateway } from "@/lib/gateway-connection";
3
5
 
4
6
  export const dynamic = "force-dynamic";
5
7
 
8
+ const CASTLE_AVATARS_DIR = join(homedir(), ".castle", "avatars");
9
+
6
10
  interface AgentIdentity {
7
11
  name?: string;
8
12
  theme?: string;
@@ -86,6 +90,14 @@ function resolveAvatarUrl(
86
90
  return `/api/avatars/${key}`;
87
91
  }
88
92
 
93
+ // Absolute path under ~/.castle/avatars/ (from avatar upload via config.patch)
94
+ if (avatar.startsWith("/") || avatar.startsWith("~")) {
95
+ const normalized = resolve(avatar.replace(/^~/, homedir()));
96
+ if (normalized.startsWith(CASTLE_AVATARS_DIR + "/") || normalized === CASTLE_AVATARS_DIR) {
97
+ return `/api/avatars/${agentId}`;
98
+ }
99
+ }
100
+
89
101
  return null;
90
102
  }
91
103
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@castlekit/castle",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "The multi-agent workspace",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,8 +1,7 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
2
+ import { writeFileSync, mkdirSync } from "fs";
3
3
  import { join } from "path";
4
4
  import { homedir } from "os";
5
- import JSON5 from "json5";
6
5
  import sharp from "sharp";
7
6
  import { ensureGateway } from "@/lib/gateway-connection";
8
7
  import { checkCsrf } from "@/lib/api-security";
@@ -20,21 +19,6 @@ const ALLOWED_TYPES = new Set([
20
19
  "image/gif",
21
20
  ]);
22
21
 
23
- interface AgentConfig {
24
- id: string;
25
- workspace?: string;
26
- identity?: Record<string, unknown>;
27
- [key: string]: unknown;
28
- }
29
-
30
- interface OpenClawConfig {
31
- agents?: {
32
- list?: AgentConfig[];
33
- [key: string]: unknown;
34
- };
35
- [key: string]: unknown;
36
- }
37
-
38
22
  /**
39
23
  * Resize and compress an avatar image to 256x256, under 100KB.
40
24
  */
@@ -59,27 +43,13 @@ async function processAvatar(input: Buffer): Promise<{ data: Buffer; ext: string
59
43
  return { data, ext: ".jpg" };
60
44
  }
61
45
 
62
- /**
63
- * Find the OpenClaw config file path.
64
- */
65
- function getOpenClawConfigPath(): string | null {
66
- const paths = [
67
- join(homedir(), ".openclaw", "openclaw.json"),
68
- join(homedir(), ".openclaw", "openclaw.json5"),
69
- ];
70
- for (const p of paths) {
71
- if (existsSync(p)) return p;
72
- }
73
- return null;
74
- }
75
-
76
46
  /**
77
47
  * POST /api/openclaw/agents/[id]/avatar
78
48
  *
79
49
  * Upload a new avatar image for an agent.
80
50
  * - Resizes to 256x256 and compresses under 100KB
81
- * - Saves to the agent's workspace directory
82
- * - Writes config file directly (Gateway hot-reloads identity changes, no restart needed)
51
+ * - Saves to Castle's own avatars directory (~/.castle/avatars/)
52
+ * - Updates OpenClaw config via Gateway's config.patch RPC (never writes to OpenClaw files directly)
83
53
  */
84
54
  export async function POST(
85
55
  request: NextRequest,
@@ -128,46 +98,9 @@ export async function POST(
128
98
  );
129
99
  }
130
100
 
131
- // Read the OpenClaw config file directly
132
- const configPath = getOpenClawConfigPath();
133
- if (!configPath) {
134
- return NextResponse.json(
135
- { error: "OpenClaw config file not found" },
136
- { status: 500 }
137
- );
138
- }
139
-
140
- let config: OpenClawConfig;
141
- try {
142
- config = JSON5.parse(readFileSync(configPath, "utf-8"));
143
- } catch (err) {
144
- return NextResponse.json(
145
- { error: `Failed to read config: ${err instanceof Error ? err.message : "unknown"}` },
146
- { status: 500 }
147
- );
148
- }
149
-
150
- const agents = config.agents?.list || [];
151
- const agent = agents.find((a) => a.id === safeId);
152
-
153
- if (!agent) {
154
- return NextResponse.json({ error: "Agent not found" }, { status: 404 });
155
- }
156
-
157
- if (!agent.workspace) {
158
- return NextResponse.json(
159
- { error: "Agent has no workspace configured" },
160
- { status: 400 }
161
- );
162
- }
163
-
164
- const workspacePath = agent.workspace.startsWith("~")
165
- ? join(homedir(), agent.workspace.slice(1))
166
- : agent.workspace;
167
-
168
- // Save processed avatar to workspace
169
- const avatarsDir = join(workspacePath, "avatars");
170
- const fileName = `avatar${processed.ext}`;
101
+ // Save processed avatar to Castle's own directory — never write to OpenClaw's filesystem
102
+ const avatarsDir = join(homedir(), ".castle", "avatars");
103
+ const fileName = `${safeId}${processed.ext}`;
171
104
  const filePath = join(avatarsDir, fileName);
172
105
 
173
106
  try {
@@ -180,37 +113,64 @@ export async function POST(
180
113
  );
181
114
  }
182
115
 
183
- // Update config file directly — Gateway's file watcher hot-reloads identity changes
184
- // No config.patch, no restart, no WebSocket disconnect
116
+ // Update OpenClaw config via Gateway's config.patch RPC the proper way to
117
+ // modify OpenClaw config without writing to its files directly.
118
+ const gw = ensureGateway();
119
+
120
+ if (!gw.isConnected) {
121
+ // Avatar is saved locally; config update will happen when Gateway reconnects
122
+ return NextResponse.json({
123
+ success: true,
124
+ avatar: filePath,
125
+ size: processed.data.length,
126
+ message: "Avatar saved locally. Gateway not connected — config will update when it reconnects.",
127
+ configUpdated: false,
128
+ });
129
+ }
130
+
185
131
  try {
186
- agent.identity = agent.identity || {};
187
- agent.identity.avatar = `avatars/${fileName}`;
188
- writeFileSync(configPath, JSON5.stringify(config, null, 2), "utf-8");
132
+ // Use config.patch to set the agent's avatar path.
133
+ // Gateway validates and applies the patch, then hot-reloads.
134
+ await gw.request("config.patch", {
135
+ agents: {
136
+ list: [
137
+ {
138
+ id: safeId,
139
+ identity: {
140
+ avatar: filePath,
141
+ },
142
+ },
143
+ ],
144
+ },
145
+ });
146
+ console.log(`[Avatar API] Config patched for agent ${safeId}`);
189
147
  } catch (err) {
190
- return NextResponse.json(
191
- { error: `Failed to update config: ${err instanceof Error ? err.message : "unknown"}` },
192
- { status: 500 }
148
+ console.error(
149
+ `[Avatar API] config.patch failed for agent ${safeId}:`,
150
+ err instanceof Error ? err.message : "unknown"
193
151
  );
152
+ // Avatar is still saved locally — not a total failure
153
+ return NextResponse.json({
154
+ success: true,
155
+ avatar: filePath,
156
+ size: processed.data.length,
157
+ message: "Avatar saved but config update failed. Try restarting the Gateway.",
158
+ configUpdated: false,
159
+ });
194
160
  }
195
161
 
196
- // Wait briefly for Gateway's file watcher to hot-reload (~300ms debounce)
197
- await new Promise((resolve) => setTimeout(resolve, 500));
198
-
199
- // Refresh Castle's agent list from the Gateway
162
+ // Emit a signal so SSE clients re-fetch agents
200
163
  try {
201
- const gw = ensureGateway();
202
- if (gw.isConnected) {
203
- // Emit a signal so SSE clients re-fetch agents
204
- gw.emit("agentAvatarUpdated", { agentId: safeId });
205
- }
164
+ gw.emit("agentAvatarUpdated", { agentId: safeId });
206
165
  } catch {
207
166
  // Non-critical
208
167
  }
209
168
 
210
169
  return NextResponse.json({
211
170
  success: true,
212
- avatar: `avatars/${fileName}`,
171
+ avatar: filePath,
213
172
  size: processed.data.length,
214
173
  message: "Avatar updated",
174
+ configUpdated: true,
215
175
  });
216
176
  }
@@ -1,8 +1,12 @@
1
1
  import { NextResponse } from "next/server";
2
+ import { join, resolve } from "path";
3
+ import { homedir } from "os";
2
4
  import { ensureGateway } from "@/lib/gateway-connection";
3
5
 
4
6
  export const dynamic = "force-dynamic";
5
7
 
8
+ const CASTLE_AVATARS_DIR = join(homedir(), ".castle", "avatars");
9
+
6
10
  interface AgentIdentity {
7
11
  name?: string;
8
12
  theme?: string;
@@ -86,6 +90,14 @@ function resolveAvatarUrl(
86
90
  return `/api/avatars/${key}`;
87
91
  }
88
92
 
93
+ // Absolute path under ~/.castle/avatars/ (from avatar upload via config.patch)
94
+ if (avatar.startsWith("/") || avatar.startsWith("~")) {
95
+ const normalized = resolve(avatar.replace(/^~/, homedir()));
96
+ if (normalized.startsWith(CASTLE_AVATARS_DIR + "/") || normalized === CASTLE_AVATARS_DIR) {
97
+ return `/api/avatars/${agentId}`;
98
+ }
99
+ }
100
+
89
101
  return null;
90
102
  }
91
103