clawport-ui 0.8.7 → 0.8.9

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.
@@ -3,14 +3,8 @@ export const runtime = 'nodejs'
3
3
  import { getAgent } from '@/lib/agents'
4
4
  import { validateChatMessages } from '@/lib/validation'
5
5
  import { hasImageContent, extractImageAttachments, buildTextPrompt, sendViaOpenClaw } from '@/lib/anthropic'
6
- import OpenAI from 'openai'
7
- import { gatewayBaseUrl } from '@/lib/env'
8
-
9
- // Route through the OpenClaw gateway — no separate API key needed
10
- const openai = new OpenAI({
11
- baseURL: gatewayBaseUrl(),
12
- apiKey: process.env.OPENCLAW_GATEWAY_TOKEN,
13
- })
6
+ import { getOpenAIClient } from '@/lib/openai'
7
+ import type OpenAI from 'openai'
14
8
 
15
9
  const GATEWAY_TOKEN = process.env.OPENCLAW_GATEWAY_TOKEN || ''
16
10
 
@@ -18,6 +12,7 @@ export async function POST(
18
12
  request: Request,
19
13
  { params }: { params: Promise<{ id: string }> }
20
14
  ) {
15
+ const openai = getOpenAIClient()
21
16
  const { id } = await params
22
17
  const agent = await getAgent(id)
23
18
 
@@ -1,13 +1,8 @@
1
1
  export const runtime = 'nodejs'
2
2
 
3
3
  import { getAgent } from '@/lib/agents'
4
- import OpenAI from 'openai'
5
- import { gatewayBaseUrl } from '@/lib/env'
6
-
7
- const openai = new OpenAI({
8
- baseURL: gatewayBaseUrl(),
9
- apiKey: process.env.OPENCLAW_GATEWAY_TOKEN,
10
- })
4
+ import { getOpenAIClient } from '@/lib/openai'
5
+ import type OpenAI from 'openai'
11
6
 
12
7
  const MAX_TITLE = 500
13
8
  const MAX_DESC = 5000
@@ -27,6 +22,7 @@ export async function POST(
27
22
  request: Request,
28
23
  { params }: { params: Promise<{ id: string }> }
29
24
  ) {
25
+ const openai = getOpenAIClient()
30
26
  const { id } = await params
31
27
  const agent = await getAgent(id)
32
28
 
@@ -1,14 +1,9 @@
1
1
  export const runtime = 'nodejs'
2
2
 
3
- import OpenAI from 'openai'
4
- import { gatewayBaseUrl } from '@/lib/env'
5
-
6
- const openai = new OpenAI({
7
- baseURL: gatewayBaseUrl(),
8
- apiKey: process.env.OPENCLAW_GATEWAY_TOKEN,
9
- })
3
+ import { getOpenAIClient } from '@/lib/openai'
10
4
 
11
5
  export async function POST(request: Request) {
6
+ const openai = getOpenAIClient()
12
7
  let formData: FormData
13
8
  try {
14
9
  formData = await request.formData()
@@ -1,14 +1,9 @@
1
1
  export const runtime = 'nodejs'
2
2
 
3
- import OpenAI from 'openai'
4
- import { gatewayBaseUrl } from '@/lib/env'
5
-
6
- const openai = new OpenAI({
7
- baseURL: gatewayBaseUrl(),
8
- apiKey: process.env.OPENCLAW_GATEWAY_TOKEN,
9
- })
3
+ import { getOpenAIClient } from '@/lib/openai'
10
4
 
11
5
  export async function POST(request: Request) {
6
+ const openai = getOpenAIClient()
12
7
  try {
13
8
  const { text, voice } = await request.json()
14
9
 
@@ -331,6 +331,53 @@ describe('getMemoryStatus', () => {
331
331
  expect(status.lastIndexed).toBeNull()
332
332
  expect(status.totalEntries).toBeNull()
333
333
  })
334
+
335
+ it('parses new array-of-agents format from openclaw memory status --deep --json', () => {
336
+ mockExecSync.mockReturnValue(JSON.stringify([
337
+ {
338
+ agentId: 'main',
339
+ status: { files: 9, chunks: 13, dirty: false, provider: 'gemini', vector: { available: true } },
340
+ },
341
+ {
342
+ agentId: 'helper',
343
+ status: { files: 3, chunks: 5, dirty: false, provider: 'gemini', vector: { available: true } },
344
+ },
345
+ ]))
346
+
347
+ const status = getMemoryStatus()
348
+ expect(status.indexed).toBe(true)
349
+ expect(status.totalEntries).toBe(18) // 13 + 5 chunks
350
+ expect(status.vectorAvailable).toBe(true)
351
+ expect(status.embeddingProvider).toBe('gemini')
352
+ })
353
+
354
+ it('reports not indexed when any agent has dirty files', () => {
355
+ mockExecSync.mockReturnValue(JSON.stringify([
356
+ {
357
+ agentId: 'main',
358
+ status: { files: 9, chunks: 13, dirty: false, provider: 'gemini', vector: { available: true } },
359
+ },
360
+ {
361
+ agentId: 'helper',
362
+ status: { files: 3, chunks: 5, dirty: true, provider: 'gemini', vector: { available: true } },
363
+ },
364
+ ]))
365
+
366
+ const status = getMemoryStatus()
367
+ expect(status.indexed).toBe(false)
368
+ })
369
+
370
+ it('reports not indexed when an agent has zero files', () => {
371
+ mockExecSync.mockReturnValue(JSON.stringify([
372
+ {
373
+ agentId: 'main',
374
+ status: { files: 0, chunks: 0, dirty: false, provider: 'gemini', vector: { available: true } },
375
+ },
376
+ ]))
377
+
378
+ const status = getMemoryStatus()
379
+ expect(status.indexed).toBe(false)
380
+ })
334
381
  })
335
382
 
336
383
  // ── computeMemoryStats ──────────────────────────────────────────
package/lib/memory.ts CHANGED
@@ -3,6 +3,7 @@ import { readFileSync, existsSync, statSync, readdirSync } from 'fs'
3
3
  import { join, basename, dirname } from 'path'
4
4
  import { execSync } from 'child_process'
5
5
  import { requireEnv } from '@/lib/env'
6
+ import { extractJson } from '@/lib/cli-utils'
6
7
 
7
8
  // ── Date pattern for daily logs ─────────────────────────────────
8
9
 
@@ -213,21 +214,45 @@ export function getMemoryStatus(): MemoryStatus {
213
214
  }
214
215
 
215
216
  try {
216
- const output = execSync(`${bin} memory status --deep`, {
217
+ const output = execSync(`${bin} memory status --deep --json`, {
217
218
  timeout: 15000,
218
219
  encoding: 'utf-8',
219
220
  stdio: ['pipe', 'pipe', 'pipe'],
220
221
  }).trim()
221
222
 
222
- // Try JSON parse first
223
223
  try {
224
- const data = JSON.parse(output)
224
+ const data = extractJson(output)
225
+
226
+ // New format: array of per-agent status objects
227
+ // [{ agentId, status: { files, chunks, dirty, provider, vector } }, ...]
228
+ if (Array.isArray(data) && data.length > 0) {
229
+ const agents = data as { agentId?: string; status?: Record<string, unknown> }[]
230
+ const primary = agents[0].status ?? {}
231
+ const totalEntries = agents.reduce(
232
+ (sum, a) => sum + (typeof a.status?.chunks === 'number' ? a.status.chunks : 0), 0,
233
+ )
234
+ const allIndexed = agents.every(
235
+ a => (typeof a.status?.files === 'number' && a.status.files > 0) && !a.status?.dirty,
236
+ )
237
+ const vec = primary.vector as Record<string, unknown> | undefined
238
+ return {
239
+ indexed: allIndexed,
240
+ lastIndexed: null,
241
+ totalEntries,
242
+ vectorAvailable: typeof vec?.available === 'boolean' ? vec.available : null,
243
+ embeddingProvider: typeof primary.provider === 'string' ? primary.provider : null,
244
+ raw: output,
245
+ }
246
+ }
247
+
248
+ // Legacy flat object format
249
+ const flat = data as Record<string, unknown>
225
250
  return {
226
- indexed: data.indexed ?? false,
227
- lastIndexed: data.lastIndexed ?? null,
228
- totalEntries: data.totalEntries ?? null,
229
- vectorAvailable: data.vectorAvailable ?? null,
230
- embeddingProvider: data.embeddingProvider ?? null,
251
+ indexed: flat.indexed === true,
252
+ lastIndexed: typeof flat.lastIndexed === 'string' ? flat.lastIndexed : null,
253
+ totalEntries: typeof flat.totalEntries === 'number' ? flat.totalEntries : null,
254
+ vectorAvailable: typeof flat.vectorAvailable === 'boolean' ? flat.vectorAvailable : null,
255
+ embeddingProvider: typeof flat.embeddingProvider === 'string' ? flat.embeddingProvider : null,
231
256
  raw: output,
232
257
  }
233
258
  } catch {
package/lib/openai.ts ADDED
@@ -0,0 +1,20 @@
1
+ import OpenAI from 'openai'
2
+ import { gatewayBaseUrl } from './env'
3
+
4
+ /**
5
+ * Lazy-initialized OpenAI client routed through the OpenClaw gateway.
6
+ *
7
+ * Call inside route handlers, not at module top level, so builds don't
8
+ * fail when environment variables aren't set.
9
+ */
10
+ let _openai: OpenAI | null = null
11
+
12
+ export function getOpenAIClient(): OpenAI {
13
+ if (!_openai) {
14
+ _openai = new OpenAI({
15
+ baseURL: gatewayBaseUrl(),
16
+ apiKey: process.env.OPENCLAW_GATEWAY_TOKEN || '',
17
+ })
18
+ }
19
+ return _openai
20
+ }
@@ -21,15 +21,28 @@ import { execSync } from 'child_process'
21
21
  * Detect the default workspace path.
22
22
  *
23
23
  * Checks in order:
24
- * 1. ~/.openclaw/agents/main/workspace (current agent-scoped layout)
25
- * 2. ~/.openclaw/workspace-main (multi-agent layout, main agent)
26
- * 3. ~/.openclaw/workspace-* (multi-agent layout, any agent)
27
- * 4. ~/.openclaw/workspace (legacy single-workspace layout)
24
+ * 1. ~/.openclaw/openclaw.json agents.defaults.workspace (canonical)
25
+ * 2. ~/.openclaw/agents/main/workspace (current agent-scoped layout)
26
+ * 3. ~/.openclaw/workspace-main (multi-agent layout, main agent)
27
+ * 4. ~/.openclaw/workspace-* (multi-agent layout, any agent)
28
+ * 5. ~/.openclaw/workspace (legacy single-workspace layout)
28
29
  */
29
30
  export function detectWorkspacePath(): string | null {
30
31
  const base = join(homedir(), '.openclaw')
31
32
 
32
- // 1. Current agent-scoped layout
33
+ // 1. Read from openclaw.json (canonical source)
34
+ const configPath = join(base, 'openclaw.json')
35
+ if (existsSync(configPath)) {
36
+ try {
37
+ const config = JSON.parse(readFileSync(configPath, 'utf-8'))
38
+ const ws = config?.agents?.defaults?.workspace
39
+ if (typeof ws === 'string' && existsSync(ws)) return ws
40
+ } catch {
41
+ // Invalid JSON, fall through
42
+ }
43
+ }
44
+
45
+ // 2. Current agent-scoped layout
33
46
  const agentPath = join(base, 'agents', 'main', 'workspace')
34
47
  if (existsSync(agentPath)) return agentPath
35
48
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawport-ui",
3
- "version": "0.8.7",
3
+ "version": "0.8.9",
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": {
package/scripts/setup.mjs CHANGED
@@ -45,7 +45,19 @@ function exec(cmd) {
45
45
  function detectWorkspacePath() {
46
46
  const base = join(homedir(), '.openclaw')
47
47
 
48
- // 1. Current agent-scoped layout
48
+ // 1. Read from openclaw.json (canonical source)
49
+ const configPath = join(base, 'openclaw.json')
50
+ if (existsSync(configPath)) {
51
+ try {
52
+ const config = JSON.parse(readFileSync(configPath, 'utf-8'))
53
+ const ws = config?.agents?.defaults?.workspace
54
+ if (typeof ws === 'string' && existsSync(ws)) return ws
55
+ } catch {
56
+ // Invalid JSON, fall through
57
+ }
58
+ }
59
+
60
+ // 2. Current agent-scoped layout
49
61
  const agentPath = join(base, 'agents', 'main', 'workspace')
50
62
  if (existsSync(agentPath)) return agentPath
51
63