clawport-ui 0.8.5 → 0.8.7

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.
@@ -164,15 +164,24 @@ export function OnboardingWizard({ forceOpen, onClose }: OnboardingWizardProps)
164
164
 
165
165
  // Check crons (validates gateway + openclaw binary)
166
166
  fetch('/api/crons')
167
- .then(r => {
168
- if (!r.ok) throw new Error(`HTTP ${r.status}`)
167
+ .then(async r => {
168
+ if (!r.ok) {
169
+ const body = await r.json().catch(() => null)
170
+ const serverMsg = body?.error
171
+ throw new Error(serverMsg || `HTTP ${r.status}`)
172
+ }
169
173
  return r.json()
170
174
  })
171
175
  .then(() => {
172
176
  setCronsStatus('ok')
173
177
  })
174
- .catch(() => {
175
- setCronsError('Could not reach OpenClaw gateway. Run: openclaw gateway run')
178
+ .catch((err: Error) => {
179
+ const msg = err.message || ''
180
+ if (msg.includes('Failed to fetch cron') || msg.includes('JSON') || msg.includes('Unexpected token')) {
181
+ setCronsError('Cron list failed -- OpenClaw CLI may be printing log output before JSON. Try: OPENCLAW_LOG_LEVEL=error clawport dev')
182
+ } else {
183
+ setCronsError('Could not reach OpenClaw gateway. Run: openclaw gateway run')
184
+ }
176
185
  setCronsStatus('error')
177
186
  })
178
187
  }
@@ -41,4 +41,20 @@ hooks: Unrecognized key: "allowedAgentIds"
41
41
  it('throws on empty string', () => {
42
42
  expect(() => extractJson('')).toThrow()
43
43
  })
44
+
45
+ it('skips bracketed log lines like [plugins] before real JSON', () => {
46
+ const raw = `[plugins] [debug] Database schema initialized
47
+ [plugins] [debug] Loading context engine
48
+ {"jobs":[{"name":"daily-report","schedule":"0 8 * * *"}]}`
49
+ const result = extractJson(raw) as Record<string, unknown>
50
+ expect(result.jobs).toEqual([{ name: 'daily-report', schedule: '0 8 * * *' }])
51
+ })
52
+
53
+ it('skips bracketed log lines before JSON array', () => {
54
+ const raw = `[plugins] [debug] Database schema initialized
55
+ [plugins] [info] Ready
56
+ [{"id":"pulse","name":"daily-pulse"}]`
57
+ const result = extractJson(raw)
58
+ expect(result).toEqual([{ id: 'pulse', name: 'daily-pulse' }])
59
+ })
44
60
  })
package/lib/cli-utils.ts CHANGED
@@ -1,25 +1,38 @@
1
1
  /**
2
2
  * Extract a JSON value from CLI output that may contain non-JSON preamble.
3
3
  *
4
- * Some OpenClaw versions print validation warnings (e.g. "Unrecognized key")
5
- * to stdout before the JSON payload. This function finds the first `[` or `{`
6
- * and parses from there, so ClawPort doesn't break on noisy CLI output.
4
+ * Some OpenClaw versions print validation warnings or debug log lines
5
+ * (e.g. "[plugins] [debug] ...") to stdout before the JSON payload.
6
+ * This function finds the actual JSON structure by trying each `[` or `{`
7
+ * position until one parses successfully.
7
8
  */
8
9
  export function extractJson(raw: string): unknown {
9
10
  // Fast path: raw is already valid JSON
10
11
  const trimmed = raw.trim()
11
12
  if (trimmed.startsWith('[') || trimmed.startsWith('{')) {
12
- return JSON.parse(trimmed)
13
+ try {
14
+ return JSON.parse(trimmed)
15
+ } catch {
16
+ // May start with [ but be a log line like "[plugins] ..." -- fall through
17
+ }
13
18
  }
14
19
 
15
- // Find the first JSON structure in the output
16
- const arrStart = raw.indexOf('[')
17
- const objStart = raw.indexOf('{')
18
- const starts = [arrStart, objStart].filter(i => i >= 0)
19
- if (starts.length === 0) {
20
- throw new SyntaxError('No JSON found in CLI output')
20
+ // Try each potential JSON start position
21
+ let pos = 0
22
+ while (pos < raw.length) {
23
+ const arrStart = raw.indexOf('[', pos)
24
+ const objStart = raw.indexOf('{', pos)
25
+ const candidates = [arrStart, objStart].filter(i => i >= 0)
26
+ if (candidates.length === 0) break
27
+
28
+ const start = Math.min(...candidates)
29
+ try {
30
+ return JSON.parse(raw.slice(start))
31
+ } catch {
32
+ // This wasn't the real JSON start -- advance past it
33
+ pos = start + 1
34
+ }
21
35
  }
22
36
 
23
- const start = Math.min(...starts)
24
- return JSON.parse(raw.slice(start))
37
+ throw new SyntaxError('No JSON found in CLI output')
25
38
  }
package/lib/crons.test.ts CHANGED
@@ -254,18 +254,25 @@ describe('getCrons - error and lastError', () => {
254
254
  })
255
255
  })
256
256
 
257
- // --- Error propagation (current implementation throws) ---
257
+ // --- Graceful degradation (returns empty array on failure) ---
258
258
 
259
- describe('getCrons - error propagation', () => {
260
- it('throws when execSync throws (CLI not installed)', async () => {
259
+ describe('getCrons - graceful degradation', () => {
260
+ it('returns empty array when execSync throws (CLI not installed)', async () => {
261
261
  mockExecSync.mockImplementation(() => { throw new Error('ENOENT') })
262
- await expect(getCrons()).rejects.toThrow('Failed to fetch cron jobs')
263
- await expect(getCrons()).rejects.toThrow('ENOENT')
262
+ const result = await getCrons()
263
+ expect(result).toEqual([])
264
264
  })
265
265
 
266
- it('throws for invalid JSON output', async () => {
266
+ it('returns empty array for invalid JSON output', async () => {
267
267
  mockExecSync.mockReturnValue('not valid json {{')
268
- await expect(getCrons()).rejects.toThrow('Failed to fetch cron jobs')
268
+ const result = await getCrons()
269
+ expect(result).toEqual([])
270
+ })
271
+
272
+ it('returns empty array when OPENCLAW_BIN is not set', async () => {
273
+ vi.unstubAllEnvs()
274
+ const result = await getCrons()
275
+ expect(result).toEqual([])
269
276
  })
270
277
  })
271
278
 
package/lib/crons.ts CHANGED
@@ -32,11 +32,22 @@ export async function getCrons(): Promise<CronJob[]> {
32
32
  }
33
33
 
34
34
  try {
35
- const openclawBin = requireEnv('OPENCLAW_BIN')
36
- const raw = execSync(`${openclawBin} cron list --json`, {
37
- encoding: 'utf-8',
38
- timeout: 10000,
39
- })
35
+ const openclawBin = process.env.OPENCLAW_BIN
36
+ if (!openclawBin) {
37
+ // No binary configured -- return empty list instead of crashing
38
+ return []
39
+ }
40
+
41
+ let raw: string
42
+ try {
43
+ raw = execSync(`${openclawBin} cron list --json`, {
44
+ encoding: 'utf-8',
45
+ timeout: 10000,
46
+ })
47
+ } catch {
48
+ // CLI failed (binary not found, no crons, gateway down) -- return empty
49
+ return []
50
+ }
40
51
 
41
52
  const parsed = extractJson(raw) as Record<string, unknown>
42
53
  const jobs: unknown[] = Array.isArray(parsed)
@@ -113,9 +124,8 @@ export async function getCrons(): Promise<CronJob[]> {
113
124
 
114
125
  _cronsCache = { result, ts: Date.now() }
115
126
  return result
116
- } catch (err) {
117
- throw new Error(
118
- `Failed to fetch cron jobs: ${err instanceof Error ? err.message : String(err)}`
119
- )
127
+ } catch {
128
+ // JSON extraction or parsing failed -- return empty rather than 500
129
+ return []
120
130
  }
121
131
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawport-ui",
3
- "version": "0.8.5",
3
+ "version": "0.8.7",
4
4
  "description": "Open-source dashboard for managing, monitoring, and chatting with your OpenClaw AI agents.",
5
5
  "homepage": "https://clawport.dev",
6
6
  "repository": {