openclaw-node-harness 2.1.1 → 2.2.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.
- package/bin/lane-watchdog.js +31 -21
- package/bin/mesh-agent.js +11 -2
- package/bin/mesh-deploy.js +4 -0
- package/bin/mesh-task-daemon.js +9 -4
- package/bin/mesh.js +9 -24
- package/lib/exec-safety.js +60 -2
- package/lib/kanban-io.js +8 -5
- package/lib/llm-providers.js +12 -1
- package/lib/mesh-collab.js +8 -2
- package/lib/mesh-harness.js +6 -0
- package/lib/mesh-plans.js +20 -2
- package/lib/mesh-tasks.js +26 -10
- package/mission-control/package-lock.json +4188 -3698
- package/mission-control/package.json +2 -2
- package/mission-control/src/app/api/diagnostics/route.ts +8 -0
- package/mission-control/src/app/api/diagnostics/test-runner/route.ts +8 -0
- package/mission-control/src/app/api/memory/graph/route.ts +34 -18
- package/mission-control/src/app/api/memory/search/route.ts +3 -2
- package/mission-control/src/app/api/mesh/identity/route.ts +13 -5
- package/mission-control/src/app/api/mesh/nodes/route.ts +8 -0
- package/mission-control/src/app/api/settings/gateway/route.ts +62 -0
- package/mission-control/src/app/api/souls/[id]/evolution/route.ts +28 -7
- package/mission-control/src/app/api/souls/[id]/propagate/route.ts +10 -3
- package/mission-control/src/app/api/souls/route.ts +6 -4
- package/mission-control/src/app/api/tasks/[id]/route.ts +20 -4
- package/mission-control/src/app/api/tasks/route.ts +68 -9
- package/mission-control/src/lib/config.ts +2 -2
- package/mission-control/src/lib/sync/tasks.ts +4 -1
- package/package.json +1 -1
- package/services/launchd/ai.openclaw.lane-watchdog.plist +1 -1
- package/services/launchd/ai.openclaw.mesh-agent.plist +4 -0
- package/services/launchd/ai.openclaw.mission-control.plist +4 -3
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"gray-matter": "^4.0.3",
|
|
27
27
|
"lucide-react": "^0.575.0",
|
|
28
28
|
"nats": "^2.29.3",
|
|
29
|
-
"next": "16.1
|
|
29
|
+
"next": "^16.2.1",
|
|
30
30
|
"react": "19.2.3",
|
|
31
31
|
"react-dom": "19.2.3",
|
|
32
32
|
"react-force-graph-2d": "^1.29.1",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"@types/node": "^20",
|
|
44
44
|
"@types/react": "^19",
|
|
45
45
|
"@types/react-dom": "^19",
|
|
46
|
-
"drizzle-kit": "^0.31.
|
|
46
|
+
"drizzle-kit": "^0.31.10",
|
|
47
47
|
"eslint": "^9",
|
|
48
48
|
"eslint-config-next": "16.1.6",
|
|
49
49
|
"tailwindcss": "^4.2.1",
|
|
@@ -8,6 +8,7 @@ import { parseTasksMarkdown, serializeTasksMarkdown } from "@/lib/parsers/task-m
|
|
|
8
8
|
export const dynamic = "force-dynamic";
|
|
9
9
|
|
|
10
10
|
export async function GET() {
|
|
11
|
+
try {
|
|
11
12
|
const raw = getRawDb();
|
|
12
13
|
|
|
13
14
|
// Task stats
|
|
@@ -94,4 +95,11 @@ export async function GET() {
|
|
|
94
95
|
nats: natsStatus,
|
|
95
96
|
workspace: workspaceExists,
|
|
96
97
|
});
|
|
98
|
+
} catch (err) {
|
|
99
|
+
console.error("[diagnostics] error:", err);
|
|
100
|
+
return NextResponse.json(
|
|
101
|
+
{ error: err instanceof Error ? err.message : "Internal server error" },
|
|
102
|
+
{ status: err instanceof SyntaxError ? 400 : 500 }
|
|
103
|
+
);
|
|
104
|
+
}
|
|
97
105
|
}
|
|
@@ -41,6 +41,7 @@ async function runTest(suite: string, name: string, fn: TestFn): Promise<TestRes
|
|
|
41
41
|
* All test tasks use the prefix __TEST__ so they can be safely cleaned up.
|
|
42
42
|
*/
|
|
43
43
|
export async function POST() {
|
|
44
|
+
try {
|
|
44
45
|
const results: TestResult[] = [];
|
|
45
46
|
const db = getDb();
|
|
46
47
|
const raw = getRawDb();
|
|
@@ -987,4 +988,11 @@ export async function POST() {
|
|
|
987
988
|
results,
|
|
988
989
|
timestamp: new Date().toISOString(),
|
|
989
990
|
});
|
|
991
|
+
} catch (err) {
|
|
992
|
+
console.error("[diagnostics/test-runner] error:", err);
|
|
993
|
+
return NextResponse.json(
|
|
994
|
+
{ error: err instanceof Error ? err.message : "Internal server error" },
|
|
995
|
+
{ status: err instanceof SyntaxError ? 400 : 500 }
|
|
996
|
+
);
|
|
997
|
+
}
|
|
990
998
|
}
|
|
@@ -18,31 +18,47 @@ import {
|
|
|
18
18
|
import { getRawDb } from "@/lib/db";
|
|
19
19
|
|
|
20
20
|
export async function GET(request: Request) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
try {
|
|
22
|
+
const url = new URL(request.url);
|
|
23
|
+
const boot = url.searchParams.get("boot");
|
|
24
|
+
const format = url.searchParams.get("format");
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
if (boot === "true") {
|
|
27
|
+
const block = formatEntityContextBlock();
|
|
28
|
+
return NextResponse.json({ block });
|
|
29
|
+
}
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
if (format === "viz") {
|
|
32
|
+
return getVizData();
|
|
33
|
+
}
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
const stats = getGraphStats();
|
|
36
|
+
const topEntities = getTopEntities(10).map((entity) => {
|
|
37
|
+
const relations = getEntityRelations(entity.id, 3);
|
|
38
|
+
return { ...entity, relations };
|
|
39
|
+
});
|
|
39
40
|
|
|
40
|
-
|
|
41
|
+
return NextResponse.json({ stats, topEntities });
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.error("[memory/graph] GET error:", err);
|
|
44
|
+
return NextResponse.json(
|
|
45
|
+
{ error: err instanceof Error ? err.message : "Internal server error" },
|
|
46
|
+
{ status: err instanceof SyntaxError ? 400 : 500 }
|
|
47
|
+
);
|
|
48
|
+
}
|
|
41
49
|
}
|
|
42
50
|
|
|
43
51
|
export async function POST() {
|
|
44
|
-
|
|
45
|
-
|
|
52
|
+
try {
|
|
53
|
+
const seeded = seedKnownEntities();
|
|
54
|
+
return NextResponse.json({ seeded, message: `${seeded} entities seeded` });
|
|
55
|
+
} catch (err) {
|
|
56
|
+
console.error("[memory/graph] POST error:", err);
|
|
57
|
+
return NextResponse.json(
|
|
58
|
+
{ error: err instanceof Error ? err.message : "Internal server error" },
|
|
59
|
+
{ status: err instanceof SyntaxError ? 400 : 500 }
|
|
60
|
+
);
|
|
61
|
+
}
|
|
46
62
|
}
|
|
47
63
|
|
|
48
64
|
/**
|
|
@@ -57,7 +57,7 @@ export async function GET(request: NextRequest) {
|
|
|
57
57
|
const source = searchParams.get("source");
|
|
58
58
|
const category = searchParams.get("category");
|
|
59
59
|
const limit = Math.min(parseInt(searchParams.get("limit") || "20", 10), 100);
|
|
60
|
-
const offset = parseInt(searchParams.get("offset") || "0", 10);
|
|
60
|
+
const offset = Math.max(0, Math.min(parseInt(searchParams.get("offset") || "0", 10) || 0, 10000));
|
|
61
61
|
const useDecay = searchParams.get("decay") !== "false"; // default: true
|
|
62
62
|
const decayRate = parseFloat(searchParams.get("decay_rate") || "0.01");
|
|
63
63
|
|
|
@@ -141,6 +141,7 @@ export async function GET(request: NextRequest) {
|
|
|
141
141
|
console.error("GET /api/memory/search error:", err);
|
|
142
142
|
const message =
|
|
143
143
|
err instanceof Error ? err.message : "Failed to search memory";
|
|
144
|
-
|
|
144
|
+
const status = err instanceof SyntaxError || err instanceof RangeError ? 400 : 500;
|
|
145
|
+
return NextResponse.json({ error: message }, { status });
|
|
145
146
|
}
|
|
146
147
|
}
|
|
@@ -3,9 +3,17 @@ import { NODE_ID, NODE_ROLE, NODE_PLATFORM } from "@/lib/config";
|
|
|
3
3
|
export const dynamic = "force-dynamic";
|
|
4
4
|
|
|
5
5
|
export async function GET() {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
try {
|
|
7
|
+
return Response.json({
|
|
8
|
+
nodeId: NODE_ID,
|
|
9
|
+
role: NODE_ROLE,
|
|
10
|
+
platform: NODE_PLATFORM,
|
|
11
|
+
});
|
|
12
|
+
} catch (err) {
|
|
13
|
+
console.error("[mesh/identity] error:", err);
|
|
14
|
+
return Response.json(
|
|
15
|
+
{ error: err instanceof Error ? err.message : "Internal server error" },
|
|
16
|
+
{ status: 500 }
|
|
17
|
+
);
|
|
18
|
+
}
|
|
11
19
|
}
|
|
@@ -85,6 +85,7 @@ const KNOWN_NODES = [
|
|
|
85
85
|
* No synchronous round-trips to nodes. No timeout races. No flickering.
|
|
86
86
|
*/
|
|
87
87
|
export async function GET() {
|
|
88
|
+
try {
|
|
88
89
|
const kv = await getHealthKv();
|
|
89
90
|
const db = getDb();
|
|
90
91
|
const now = Date.now();
|
|
@@ -218,4 +219,11 @@ export async function GET() {
|
|
|
218
219
|
}
|
|
219
220
|
|
|
220
221
|
return NextResponse.json({ nodes, tokenStats });
|
|
222
|
+
} catch (err) {
|
|
223
|
+
console.error("[mesh/nodes] error:", err);
|
|
224
|
+
return NextResponse.json(
|
|
225
|
+
{ error: err instanceof Error ? err.message : "Internal server error" },
|
|
226
|
+
{ status: err instanceof SyntaxError ? 400 : 500 }
|
|
227
|
+
);
|
|
228
|
+
}
|
|
221
229
|
}
|
|
@@ -52,9 +52,63 @@ export async function GET() {
|
|
|
52
52
|
* Update gateway-related settings. Accepts partial updates.
|
|
53
53
|
* Body: { heartbeat?: { target, every? }, maxConcurrent?: number }
|
|
54
54
|
*/
|
|
55
|
+
const ALLOWED_TOP_LEVEL_KEYS = new Set([
|
|
56
|
+
"heartbeat", "maxConcurrent", "compaction", "gateway",
|
|
57
|
+
]);
|
|
58
|
+
const ALLOWED_GATEWAY_KEYS = new Set(["port", "mode", "bind"]);
|
|
59
|
+
const VALID_MODES = new Set(["local", "remote", "hybrid"]);
|
|
60
|
+
const VALID_BINDS = new Set(["loopback", "tailscale", "any"]);
|
|
61
|
+
|
|
55
62
|
export async function PATCH(request: NextRequest) {
|
|
56
63
|
try {
|
|
57
64
|
const body = await request.json();
|
|
65
|
+
|
|
66
|
+
// Reject unknown top-level keys
|
|
67
|
+
const unknownKeys = Object.keys(body).filter((k) => !ALLOWED_TOP_LEVEL_KEYS.has(k));
|
|
68
|
+
if (unknownKeys.length > 0) {
|
|
69
|
+
return NextResponse.json(
|
|
70
|
+
{ error: `Unknown keys: ${unknownKeys.join(", ")}` },
|
|
71
|
+
{ status: 400 }
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Validate gateway sub-object if present
|
|
76
|
+
if (body.gateway !== undefined) {
|
|
77
|
+
if (typeof body.gateway !== "object" || body.gateway === null || Array.isArray(body.gateway)) {
|
|
78
|
+
return NextResponse.json(
|
|
79
|
+
{ error: "gateway must be an object" },
|
|
80
|
+
{ status: 400 }
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
const unknownGw = Object.keys(body.gateway).filter((k) => !ALLOWED_GATEWAY_KEYS.has(k));
|
|
84
|
+
if (unknownGw.length > 0) {
|
|
85
|
+
return NextResponse.json(
|
|
86
|
+
{ error: `Unknown gateway keys: ${unknownGw.join(", ")}` },
|
|
87
|
+
{ status: 400 }
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
if (body.gateway.port !== undefined) {
|
|
91
|
+
if (!Number.isInteger(body.gateway.port) || body.gateway.port < 1 || body.gateway.port > 65535) {
|
|
92
|
+
return NextResponse.json(
|
|
93
|
+
{ error: "gateway.port must be an integer between 1 and 65535" },
|
|
94
|
+
{ status: 400 }
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (body.gateway.mode !== undefined && !VALID_MODES.has(body.gateway.mode)) {
|
|
99
|
+
return NextResponse.json(
|
|
100
|
+
{ error: `gateway.mode must be one of: ${[...VALID_MODES].join(", ")}` },
|
|
101
|
+
{ status: 400 }
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
if (body.gateway.bind !== undefined && !VALID_BINDS.has(body.gateway.bind)) {
|
|
105
|
+
return NextResponse.json(
|
|
106
|
+
{ error: `gateway.bind must be one of: ${[...VALID_BINDS].join(", ")}` },
|
|
107
|
+
{ status: 400 }
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
58
112
|
const config = await loadConfig();
|
|
59
113
|
|
|
60
114
|
if (!config.agents) config.agents = {};
|
|
@@ -79,6 +133,14 @@ export async function PATCH(request: NextRequest) {
|
|
|
79
133
|
config.agents.defaults.compaction = body.compaction;
|
|
80
134
|
}
|
|
81
135
|
|
|
136
|
+
// Gateway settings
|
|
137
|
+
if (body.gateway !== undefined) {
|
|
138
|
+
if (!config.gateway) config.gateway = {};
|
|
139
|
+
if (body.gateway.port !== undefined) config.gateway.port = body.gateway.port;
|
|
140
|
+
if (body.gateway.mode !== undefined) config.gateway.mode = body.gateway.mode;
|
|
141
|
+
if (body.gateway.bind !== undefined) config.gateway.bind = body.gateway.bind;
|
|
142
|
+
}
|
|
143
|
+
|
|
82
144
|
await saveConfig(config);
|
|
83
145
|
|
|
84
146
|
return NextResponse.json({ ok: true });
|
|
@@ -69,7 +69,10 @@ export async function GET(
|
|
|
69
69
|
const lines = content.trim().split("\n").filter(Boolean);
|
|
70
70
|
fullEvents = lines.map((line) => JSON.parse(line));
|
|
71
71
|
} catch (error) {
|
|
72
|
-
// events.jsonl might be empty
|
|
72
|
+
// events.jsonl might be empty, not exist yet, or contain malformed lines
|
|
73
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
74
|
+
console.warn("Failed to parse events.jsonl:", error);
|
|
75
|
+
}
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
// Merge DB records with full event details
|
|
@@ -174,11 +177,20 @@ export async function PATCH(
|
|
|
174
177
|
"events.jsonl"
|
|
175
178
|
);
|
|
176
179
|
const content = await fs.readFile(eventsPath, "utf-8");
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
180
|
+
let events: EvolutionEvent[];
|
|
181
|
+
try {
|
|
182
|
+
events = content
|
|
183
|
+
.trim()
|
|
184
|
+
.split("\n")
|
|
185
|
+
.filter(Boolean)
|
|
186
|
+
.map((line) => JSON.parse(line));
|
|
187
|
+
} catch (parseErr) {
|
|
188
|
+
console.error("Malformed JSONL in events.jsonl:", parseErr);
|
|
189
|
+
return NextResponse.json(
|
|
190
|
+
{ error: "Failed to parse evolution events file (malformed JSONL)" },
|
|
191
|
+
{ status: 500 }
|
|
192
|
+
);
|
|
193
|
+
}
|
|
182
194
|
const event = events.find((e: EvolutionEvent) => e.eventId === eventId);
|
|
183
195
|
|
|
184
196
|
if (!event) {
|
|
@@ -195,7 +207,16 @@ export async function PATCH(
|
|
|
195
207
|
);
|
|
196
208
|
|
|
197
209
|
if (event.proposedChange.action === "add") {
|
|
198
|
-
|
|
210
|
+
let existing;
|
|
211
|
+
try {
|
|
212
|
+
existing = JSON.parse(await fs.readFile(targetPath, "utf-8"));
|
|
213
|
+
} catch (parseErr) {
|
|
214
|
+
console.error(`Malformed JSON in ${targetPath}:`, parseErr);
|
|
215
|
+
return NextResponse.json(
|
|
216
|
+
{ error: `Failed to parse ${event.proposedChange.target} (malformed JSON)` },
|
|
217
|
+
{ status: 500 }
|
|
218
|
+
);
|
|
219
|
+
}
|
|
199
220
|
if (event.proposedChange.target === "genes.json") {
|
|
200
221
|
existing.genes.push(event.proposedChange.content);
|
|
201
222
|
}
|
|
@@ -75,10 +75,17 @@ export async function POST(
|
|
|
75
75
|
const lines = content.trim().split("\n").filter(Boolean);
|
|
76
76
|
const events: EvolutionEventEntry[] = lines.map((line) => JSON.parse(line));
|
|
77
77
|
sourceEvent = events.find((e) => e.eventId === sourceEventId) ?? null;
|
|
78
|
-
} catch {
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
80
|
+
return NextResponse.json(
|
|
81
|
+
{ error: "Source events file not found" },
|
|
82
|
+
{ status: 404 }
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
console.error("Failed to parse source events.jsonl:", error);
|
|
79
86
|
return NextResponse.json(
|
|
80
|
-
{ error: "
|
|
81
|
-
{ status:
|
|
87
|
+
{ error: "Failed to parse source events file (malformed JSONL)" },
|
|
88
|
+
{ status: 500 }
|
|
82
89
|
);
|
|
83
90
|
}
|
|
84
91
|
|
|
@@ -94,9 +94,10 @@ export async function POST(request: NextRequest) {
|
|
|
94
94
|
return NextResponse.json(newSoul, { status: 201 });
|
|
95
95
|
} catch (error) {
|
|
96
96
|
console.error("Failed to register soul:", error);
|
|
97
|
+
const status = error instanceof SyntaxError ? 400 : 500;
|
|
97
98
|
return NextResponse.json(
|
|
98
|
-
{ error: "Failed to register soul" },
|
|
99
|
-
{ status
|
|
99
|
+
{ error: status === 400 ? String(error) : "Failed to register soul" },
|
|
100
|
+
{ status }
|
|
100
101
|
);
|
|
101
102
|
}
|
|
102
103
|
}
|
|
@@ -133,9 +134,10 @@ export async function PATCH(request: NextRequest) {
|
|
|
133
134
|
return NextResponse.json(registry.souls[soulIndex]);
|
|
134
135
|
} catch (error) {
|
|
135
136
|
console.error("Failed to update soul:", error);
|
|
137
|
+
const status = error instanceof SyntaxError ? 400 : 500;
|
|
136
138
|
return NextResponse.json(
|
|
137
|
-
{ error: "Failed to update soul" },
|
|
138
|
-
{ status
|
|
139
|
+
{ error: status === 400 ? String(error) : "Failed to update soul" },
|
|
140
|
+
{ status }
|
|
139
141
|
);
|
|
140
142
|
}
|
|
141
143
|
}
|
|
@@ -9,6 +9,12 @@ import { gatewayNotify } from "@/lib/gateway-notify";
|
|
|
9
9
|
import { AGENT_NAME, HUMAN_NAME } from "@/lib/config";
|
|
10
10
|
import { getNats, sc } from "@/lib/nats";
|
|
11
11
|
|
|
12
|
+
/** Safely parse a JSON string from a DB field, returning fallback on failure. */
|
|
13
|
+
function safeParse(json: string | null, fallback: unknown = []): unknown {
|
|
14
|
+
if (!json) return fallback;
|
|
15
|
+
try { return JSON.parse(json); } catch { return fallback; }
|
|
16
|
+
}
|
|
17
|
+
|
|
12
18
|
/**
|
|
13
19
|
* Push a notification message to the OpenClaw TUI via gateway chat.send + abort.
|
|
14
20
|
* Fire-and-forget — does not block the API response.
|
|
@@ -130,6 +136,18 @@ export async function PATCH(
|
|
|
130
136
|
const body = await request.json();
|
|
131
137
|
const now = new Date().toISOString();
|
|
132
138
|
|
|
139
|
+
// Status validation
|
|
140
|
+
const VALID_STATUSES = new Set([
|
|
141
|
+
"not started", "queued", "ready", "submitted", "running",
|
|
142
|
+
"blocked", "waiting-user", "done", "cancelled", "archived",
|
|
143
|
+
]);
|
|
144
|
+
if (body.status && !VALID_STATUSES.has(body.status)) {
|
|
145
|
+
return NextResponse.json(
|
|
146
|
+
{ error: `Invalid status: ${body.status}` },
|
|
147
|
+
{ status: 400 }
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
133
151
|
// Transition guard: block execution mode changes on in-flight mesh tasks
|
|
134
152
|
if (body.execution !== undefined && body.execution !== existing.execution) {
|
|
135
153
|
if (existing.meshTaskId) {
|
|
@@ -337,10 +355,8 @@ export async function PATCH(
|
|
|
337
355
|
|
|
338
356
|
return NextResponse.json({
|
|
339
357
|
...updated,
|
|
340
|
-
successCriteria: updated?.successCriteria
|
|
341
|
-
|
|
342
|
-
: [],
|
|
343
|
-
artifacts: updated?.artifacts ? JSON.parse(updated.artifacts) : [],
|
|
358
|
+
successCriteria: safeParse(updated?.successCriteria ?? null),
|
|
359
|
+
artifacts: safeParse(updated?.artifacts ?? null),
|
|
344
360
|
});
|
|
345
361
|
} catch (err) {
|
|
346
362
|
console.error("PATCH /api/tasks/[id] error:", err);
|
|
@@ -12,6 +12,12 @@ import { getNats, sc } from "@/lib/nats";
|
|
|
12
12
|
import { WORKSPACE_ROOT, AGENT_NAME } from "@/lib/config";
|
|
13
13
|
import path from "path";
|
|
14
14
|
|
|
15
|
+
/** Safely parse a JSON string from a DB field, returning fallback on failure. */
|
|
16
|
+
function safeParse(json: string | null, fallback: unknown = []): unknown {
|
|
17
|
+
if (!json) return fallback;
|
|
18
|
+
try { return JSON.parse(json); } catch { return fallback; }
|
|
19
|
+
}
|
|
20
|
+
|
|
15
21
|
/**
|
|
16
22
|
* Parse .companion-state.md to extract current active task.
|
|
17
23
|
* Returns a synthetic task object if there's active work, null otherwise.
|
|
@@ -121,11 +127,11 @@ export async function GET(request: NextRequest) {
|
|
|
121
127
|
.all();
|
|
122
128
|
}
|
|
123
129
|
|
|
124
|
-
// Parse JSON fields for the response
|
|
130
|
+
// Parse JSON fields for the response (per-row guard against corrupt data)
|
|
125
131
|
const result = rows.map((t) => ({
|
|
126
132
|
...t,
|
|
127
|
-
successCriteria:
|
|
128
|
-
artifacts:
|
|
133
|
+
successCriteria: safeParse(t.successCriteria),
|
|
134
|
+
artifacts: safeParse(t.artifacts),
|
|
129
135
|
}));
|
|
130
136
|
|
|
131
137
|
return NextResponse.json(result);
|
|
@@ -155,6 +161,60 @@ export async function POST(request: NextRequest) {
|
|
|
155
161
|
);
|
|
156
162
|
}
|
|
157
163
|
|
|
164
|
+
if (body.title.length > 500) {
|
|
165
|
+
return NextResponse.json(
|
|
166
|
+
{ error: "title must be 500 characters or fewer" },
|
|
167
|
+
{ status: 400 }
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (body.success_criteria !== undefined) {
|
|
172
|
+
if (!Array.isArray(body.success_criteria)) {
|
|
173
|
+
if (typeof body.success_criteria === "string") {
|
|
174
|
+
try {
|
|
175
|
+
const parsed = JSON.parse(body.success_criteria);
|
|
176
|
+
if (!Array.isArray(parsed)) {
|
|
177
|
+
return NextResponse.json(
|
|
178
|
+
{ error: "success_criteria must be a JSON array" },
|
|
179
|
+
{ status: 400 }
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
} catch {
|
|
183
|
+
return NextResponse.json(
|
|
184
|
+
{ error: "success_criteria must be a valid JSON array string" },
|
|
185
|
+
{ status: 400 }
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
return NextResponse.json(
|
|
190
|
+
{ error: "success_criteria must be an array or JSON array string" },
|
|
191
|
+
{ status: 400 }
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const VALID_STATUSES = [
|
|
198
|
+
"not started", "queued", "ready", "submitted", "running",
|
|
199
|
+
"blocked", "waiting-user", "done", "cancelled", "archived",
|
|
200
|
+
];
|
|
201
|
+
if (body.status && !VALID_STATUSES.includes(body.status)) {
|
|
202
|
+
return NextResponse.json(
|
|
203
|
+
{ error: `status must be one of: ${VALID_STATUSES.join(", ")}` },
|
|
204
|
+
{ status: 400 }
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (body.scheduled_date) {
|
|
209
|
+
const d = new Date(body.scheduled_date);
|
|
210
|
+
if (isNaN(d.getTime())) {
|
|
211
|
+
return NextResponse.json(
|
|
212
|
+
{ error: "scheduled_date must be a valid ISO date string" },
|
|
213
|
+
{ status: 400 }
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
158
218
|
const status = body.status || "not started";
|
|
159
219
|
const now = new Date();
|
|
160
220
|
// Allow custom IDs for projects/pipelines/phases; auto-generate for tasks
|
|
@@ -227,18 +287,17 @@ export async function POST(request: NextRequest) {
|
|
|
227
287
|
return NextResponse.json(
|
|
228
288
|
{
|
|
229
289
|
...created,
|
|
230
|
-
successCriteria: created?.successCriteria
|
|
231
|
-
|
|
232
|
-
: [],
|
|
233
|
-
artifacts: created?.artifacts ? JSON.parse(created.artifacts) : [],
|
|
290
|
+
successCriteria: safeParse(created?.successCriteria ?? null),
|
|
291
|
+
artifacts: safeParse(created?.artifacts ?? null),
|
|
234
292
|
},
|
|
235
293
|
{ status: 201 }
|
|
236
294
|
);
|
|
237
295
|
} catch (err) {
|
|
238
296
|
console.error("POST /api/tasks error:", err);
|
|
297
|
+
const status = err instanceof SyntaxError ? 400 : 500;
|
|
239
298
|
return NextResponse.json(
|
|
240
|
-
{ error: "Failed to create task" },
|
|
241
|
-
{ status
|
|
299
|
+
{ error: status === 400 ? String(err) : "Failed to create task" },
|
|
300
|
+
{ status }
|
|
242
301
|
);
|
|
243
302
|
}
|
|
244
303
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import path from "path";
|
|
2
|
+
import { hostname, homedir } from "os";
|
|
2
3
|
|
|
3
4
|
export const WORKSPACE_ROOT =
|
|
4
|
-
process.env.WORKSPACE_ROOT || "
|
|
5
|
+
process.env.WORKSPACE_ROOT || path.join(homedir(), ".openclaw", "workspace");
|
|
5
6
|
|
|
6
7
|
export const DB_PATH =
|
|
7
8
|
process.env.DB_PATH ||
|
|
@@ -75,7 +76,6 @@ export function validatePathParam(param: string): string {
|
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
// ── Node Identity (Distributed MC) ──
|
|
78
|
-
import { hostname } from "os";
|
|
79
79
|
|
|
80
80
|
/** This node's unique identifier in the mesh */
|
|
81
81
|
export const NODE_ID = process.env.OPENCLAW_NODE_ID || hostname();
|
|
@@ -283,7 +283,10 @@ export function syncTasksToMarkdown(db: DrizzleDb): void {
|
|
|
283
283
|
if (!fs.existsSync(dir)) {
|
|
284
284
|
fs.mkdirSync(dir, { recursive: true });
|
|
285
285
|
}
|
|
286
|
-
fs.
|
|
286
|
+
const fd = fs.openSync(tmpPath, "w");
|
|
287
|
+
fs.writeSync(fd, markdown, 0, "utf-8");
|
|
288
|
+
fs.fsyncSync(fd);
|
|
289
|
+
fs.closeSync(fd);
|
|
287
290
|
fs.renameSync(tmpPath, ACTIVE_TASKS_MD);
|
|
288
291
|
|
|
289
292
|
// Record mtime of our own write so we don't re-import it
|
package/package.json
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
<key>HOME</key>
|
|
27
27
|
<string>${HOME}</string>
|
|
28
28
|
<key>PATH</key>
|
|
29
|
-
<string
|
|
29
|
+
<string>${HOME}/.bun/bin:${HOME}/.local/bin:${HOME}/.npm-global/bin:${HOME}/bin:${HOME}/.volta/bin:${HOME}/.asdf/shims:${HOME}/Library/Application Support/fnm/aliases/default/bin:${HOME}/.fnm/aliases/default/bin:${HOME}/Library/pnpm:${HOME}/.local/share/pnpm:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
|
|
30
30
|
</dict>
|
|
31
31
|
</dict>
|
|
32
32
|
</plist>
|
|
@@ -31,6 +31,10 @@
|
|
|
31
31
|
<string>${OPENCLAW_NATS}</string>
|
|
32
32
|
<key>MESH_WORKSPACE</key>
|
|
33
33
|
<string>${OPENCLAW_WORKSPACE}</string>
|
|
34
|
+
<key>OPENCLAW_NODE_ID</key>
|
|
35
|
+
<string>${OPENCLAW_NODE_ID}</string>
|
|
36
|
+
<key>OPENCLAW_NODE_ROLE</key>
|
|
37
|
+
<string>${OPENCLAW_NODE_ROLE}</string>
|
|
34
38
|
</dict>
|
|
35
39
|
<key>ThrottleInterval</key>
|
|
36
40
|
<integer>30</integer>
|
|
@@ -11,14 +11,15 @@
|
|
|
11
11
|
<key>ProgramArguments</key>
|
|
12
12
|
<array>
|
|
13
13
|
<string>${HOME}/.openclaw/bin/npm</string>
|
|
14
|
-
<string>
|
|
15
|
-
<string>dev</string>
|
|
14
|
+
<string>start</string>
|
|
16
15
|
</array>
|
|
17
16
|
|
|
18
17
|
<key>EnvironmentVariables</key>
|
|
19
18
|
<dict>
|
|
19
|
+
<key>HOME</key>
|
|
20
|
+
<string>${HOME}</string>
|
|
20
21
|
<key>PATH</key>
|
|
21
|
-
<string
|
|
22
|
+
<string>${HOME}/.bun/bin:${HOME}/.local/bin:${HOME}/.npm-global/bin:${HOME}/bin:${HOME}/.volta/bin:${HOME}/.asdf/shims:${HOME}/Library/Application Support/fnm/aliases/default/bin:${HOME}/.fnm/aliases/default/bin:${HOME}/Library/pnpm:${HOME}/.local/share/pnpm:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
|
|
22
23
|
<key>NODE_ENV</key>
|
|
23
24
|
<string>production</string>
|
|
24
25
|
</dict>
|