clawport-ui 0.4.0 → 0.4.2
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/app/api/chat/[id]/route.ts +8 -2
- package/lib/agents-registry.ts +210 -41
- package/lib/agents.test.ts +319 -53
- package/package.json +1 -1
- package/scripts/setup.mjs +46 -0
|
@@ -129,10 +129,16 @@ export async function POST(
|
|
|
129
129
|
Connection: 'keep-alive',
|
|
130
130
|
},
|
|
131
131
|
})
|
|
132
|
-
} catch (err) {
|
|
132
|
+
} catch (err: unknown) {
|
|
133
133
|
console.error('Chat API error:', err)
|
|
134
|
+
|
|
135
|
+
let userMessage = 'Chat failed. Make sure OpenClaw gateway is running.'
|
|
136
|
+
if (err instanceof Error && 'status' in err && (err as { status: number }).status === 405) {
|
|
137
|
+
userMessage = 'Gateway returned 405. Enable the HTTP endpoint: set gateway.http.endpoints.chatCompletions.enabled = true in ~/.openclaw/openclaw.json, then restart the gateway.'
|
|
138
|
+
}
|
|
139
|
+
|
|
134
140
|
return new Response(
|
|
135
|
-
JSON.stringify({ error:
|
|
141
|
+
JSON.stringify({ error: userMessage }),
|
|
136
142
|
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
|
137
143
|
)
|
|
138
144
|
}
|
package/lib/agents-registry.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFileSync, existsSync, readdirSync } from 'fs'
|
|
2
|
-
import { join } from 'path'
|
|
2
|
+
import { join, basename } from 'path'
|
|
3
3
|
import bundledRegistry from '@/lib/agents.json'
|
|
4
4
|
import type { Agent } from '@/lib/types'
|
|
5
5
|
|
|
@@ -13,13 +13,60 @@ const DISCOVER_COLORS = [
|
|
|
13
13
|
'#06b6d4', '#ec4899', '#84cc16', '#8b5cf6', '#ef4444',
|
|
14
14
|
]
|
|
15
15
|
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Heading / identity extraction helpers (exported for testing)
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
16
20
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
21
|
+
* Parse a SOUL.md heading into a clean agent name and optional title.
|
|
22
|
+
*
|
|
23
|
+
* Handles all observed heading formats:
|
|
24
|
+
* "# SOUL.md — VERA" → name "VERA"
|
|
25
|
+
* "# SOUL.md — KAZE, Flight Research Agent" → name "KAZE", title "Flight Research Agent"
|
|
26
|
+
* "# ECHO — Community Voice Monitor" → name "ECHO", title "Community Voice Monitor"
|
|
27
|
+
* "# SOUL.md - Who You Are" → name null (not a useful agent name)
|
|
28
|
+
* "# QUILL — Herald's Content Writer" → name "QUILL", title "Herald's Content Writer"
|
|
19
29
|
*/
|
|
20
|
-
function
|
|
30
|
+
export function parseSoulHeading(content: string): { name: string | null; title: string | null } {
|
|
21
31
|
const match = content.match(/^#\s+(.+)/m)
|
|
22
|
-
|
|
32
|
+
if (!match) return { name: null, title: null }
|
|
33
|
+
|
|
34
|
+
let heading = match[1].trim()
|
|
35
|
+
|
|
36
|
+
// Strip "SOUL.md — ", "SOUL.md - ", "SOUL.md:" prefixes
|
|
37
|
+
heading = heading.replace(/^SOUL\.md\s*[—–\-:]\s*/i, '')
|
|
38
|
+
|
|
39
|
+
// If the heading is generic (e.g. "Who You Are"), it's not a useful agent name
|
|
40
|
+
if (/^who\s+you\s+are/i.test(heading)) return { name: null, title: null }
|
|
41
|
+
|
|
42
|
+
// Split on em-dash / en-dash to separate name from description
|
|
43
|
+
const dashParts = heading.split(/\s*[—–]\s*/)
|
|
44
|
+
const nameAndMaybeTitle = dashParts[0].trim()
|
|
45
|
+
const descAfterDash = dashParts.length > 1 ? dashParts.slice(1).join(' — ').trim() : null
|
|
46
|
+
|
|
47
|
+
// The name might contain a comma: "LUMEN, SEO Team Director"
|
|
48
|
+
const commaParts = nameAndMaybeTitle.split(/,\s*/)
|
|
49
|
+
const name = commaParts[0].trim() || null
|
|
50
|
+
const titleFromComma = commaParts.length > 1 ? commaParts.slice(1).join(', ').trim() : null
|
|
51
|
+
|
|
52
|
+
const title = titleFromComma || descAfterDash || null
|
|
53
|
+
return { name, title }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Parse an IDENTITY.md file for agent name and emoji.
|
|
58
|
+
*
|
|
59
|
+
* Expected format (generated by OpenClaw bootstrap):
|
|
60
|
+
* - **Name:** Jarvis
|
|
61
|
+
* - **Emoji:** 🤖
|
|
62
|
+
*/
|
|
63
|
+
export function parseIdentity(content: string): { name: string | null; emoji: string | null } {
|
|
64
|
+
const nameMatch = content.match(/\*\*Name:\*\*\s*(.+)/i)
|
|
65
|
+
const emojiMatch = content.match(/\*\*Emoji:\*\*\s*(\S+)/i)
|
|
66
|
+
return {
|
|
67
|
+
name: nameMatch ? nameMatch[1].trim() : null,
|
|
68
|
+
emoji: emojiMatch ? emojiMatch[1].trim() : null,
|
|
69
|
+
}
|
|
23
70
|
}
|
|
24
71
|
|
|
25
72
|
/**
|
|
@@ -32,93 +79,215 @@ function slugToName(slug: string): string {
|
|
|
32
79
|
.join(' ')
|
|
33
80
|
}
|
|
34
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Read a file and return its content, or null on any error.
|
|
84
|
+
*/
|
|
85
|
+
function safeRead(filePath: string): string | null {
|
|
86
|
+
try {
|
|
87
|
+
if (!existsSync(filePath)) return null
|
|
88
|
+
return readFileSync(filePath, 'utf-8')
|
|
89
|
+
} catch {
|
|
90
|
+
return null
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Scan a directory for sub-agent .md files (e.g. sub-agents/QUILL.md, members/WRITER.md).
|
|
96
|
+
* Returns an array of { fileName, content } for each .md file found.
|
|
97
|
+
*/
|
|
98
|
+
function scanSubAgentDir(dirPath: string): { fileName: string; content: string }[] {
|
|
99
|
+
if (!existsSync(dirPath)) return []
|
|
100
|
+
try {
|
|
101
|
+
return readdirSync(dirPath)
|
|
102
|
+
.filter(f => f.endsWith('.md'))
|
|
103
|
+
.map(f => {
|
|
104
|
+
const content = safeRead(join(dirPath, f))
|
|
105
|
+
return content ? { fileName: f, content } : null
|
|
106
|
+
})
|
|
107
|
+
.filter((x): x is { fileName: string; content: string } => x !== null)
|
|
108
|
+
} catch {
|
|
109
|
+
return []
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// Auto-discovery
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
|
|
35
117
|
/**
|
|
36
118
|
* Auto-discover agents from $WORKSPACE_PATH.
|
|
37
119
|
*
|
|
38
|
-
* Scans the workspace for
|
|
39
|
-
*
|
|
120
|
+
* Scans the workspace for:
|
|
121
|
+
* - IDENTITY.md at root (for root agent name/emoji)
|
|
122
|
+
* - SOUL.md at root (root orchestrator)
|
|
123
|
+
* - agents/<dir>/SOUL.md (top-level agents)
|
|
124
|
+
* - agents/<dir>/sub-agents/*.md (nested sub-agents)
|
|
125
|
+
* - agents/<dir>/members/*.md (team member agents)
|
|
126
|
+
*
|
|
40
127
|
* Returns null if nothing can be discovered.
|
|
41
128
|
*/
|
|
42
129
|
function discoverAgents(workspacePath: string): AgentEntry[] | null {
|
|
43
130
|
const agentsDir = join(workspacePath, 'agents')
|
|
44
131
|
const rootSoulPath = join(workspacePath, 'SOUL.md')
|
|
132
|
+
const identityPath = join(workspacePath, 'IDENTITY.md')
|
|
45
133
|
const hasRoot = existsSync(rootSoulPath)
|
|
46
134
|
|
|
47
|
-
// Scan agents/ directory for subdirectories
|
|
48
|
-
let
|
|
135
|
+
// Scan agents/ directory for all subdirectories (even without SOUL.md, may have sub-agents)
|
|
136
|
+
let allAgentDirs: string[] = []
|
|
49
137
|
if (existsSync(agentsDir)) {
|
|
50
138
|
try {
|
|
51
139
|
const entries = readdirSync(agentsDir, { withFileTypes: true })
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
.map(e => e.name)
|
|
55
|
-
} catch {
|
|
56
|
-
// Can't read directory
|
|
57
|
-
}
|
|
140
|
+
allAgentDirs = entries.filter(e => e.isDirectory()).map(e => e.name)
|
|
141
|
+
} catch {}
|
|
58
142
|
}
|
|
59
143
|
|
|
60
|
-
// Need at least a root or
|
|
61
|
-
if (!hasRoot &&
|
|
144
|
+
// Need at least a root or agent directories to discover
|
|
145
|
+
if (!hasRoot && allAgentDirs.length === 0) return null
|
|
62
146
|
|
|
63
147
|
const discovered: AgentEntry[] = []
|
|
148
|
+
let colorIndex = 0
|
|
64
149
|
|
|
65
|
-
//
|
|
150
|
+
// --- Root agent ---
|
|
66
151
|
let rootId = 'main'
|
|
67
152
|
let rootName = 'Main'
|
|
153
|
+
let rootEmoji = '\u{1F916}'
|
|
154
|
+
|
|
68
155
|
if (hasRoot) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
156
|
+
// Try IDENTITY.md first (most reliable source for name/emoji)
|
|
157
|
+
const identityContent = safeRead(identityPath)
|
|
158
|
+
if (identityContent) {
|
|
159
|
+
const identity = parseIdentity(identityContent)
|
|
160
|
+
if (identity.name) {
|
|
161
|
+
rootName = identity.name
|
|
162
|
+
rootId = identity.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'main'
|
|
75
163
|
}
|
|
76
|
-
|
|
164
|
+
if (identity.emoji) rootEmoji = identity.emoji
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Fall back to SOUL.md heading if IDENTITY.md didn't provide a name
|
|
168
|
+
if (rootName === 'Main') {
|
|
169
|
+
const soulContent = safeRead(rootSoulPath)
|
|
170
|
+
if (soulContent) {
|
|
171
|
+
const parsed = parseSoulHeading(soulContent)
|
|
172
|
+
if (parsed.name) {
|
|
173
|
+
rootName = parsed.name
|
|
174
|
+
rootId = parsed.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'main'
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Collect all agent IDs that report to root (including those without SOUL.md but with sub-agents)
|
|
180
|
+
const directReportIds: string[] = []
|
|
77
181
|
|
|
78
182
|
discovered.push({
|
|
79
183
|
id: rootId,
|
|
80
184
|
name: rootName,
|
|
81
185
|
title: 'Orchestrator',
|
|
82
186
|
reportsTo: null,
|
|
83
|
-
directReports:
|
|
187
|
+
directReports: directReportIds, // populated below
|
|
84
188
|
soulPath: 'SOUL.md',
|
|
85
189
|
voiceId: null,
|
|
86
|
-
color: DISCOVER_COLORS[
|
|
87
|
-
emoji:
|
|
190
|
+
color: DISCOVER_COLORS[colorIndex++ % DISCOVER_COLORS.length],
|
|
191
|
+
emoji: rootEmoji,
|
|
88
192
|
tools: ['read', 'write', 'exec', 'message'],
|
|
89
193
|
memoryPath: null,
|
|
90
194
|
description: 'Top-level orchestrator.',
|
|
91
195
|
})
|
|
196
|
+
|
|
197
|
+
// We'll fill directReportIds as we discover agents below
|
|
198
|
+
for (const dirName of allAgentDirs) {
|
|
199
|
+
const hasSoul = existsSync(join(agentsDir, dirName, 'SOUL.md'))
|
|
200
|
+
const hasSubAgents = existsSync(join(agentsDir, dirName, 'sub-agents'))
|
|
201
|
+
const hasMembers = existsSync(join(agentsDir, dirName, 'members'))
|
|
202
|
+
if (hasSoul || hasSubAgents || hasMembers) {
|
|
203
|
+
directReportIds.push(dirName)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
92
206
|
}
|
|
93
207
|
|
|
94
|
-
//
|
|
95
|
-
|
|
208
|
+
// --- Top-level agents ---
|
|
209
|
+
for (const dirName of allAgentDirs) {
|
|
210
|
+
const soulFile = join(agentsDir, dirName, 'SOUL.md')
|
|
211
|
+
const hasSoul = existsSync(soulFile)
|
|
212
|
+
const subAgentsDir = join(agentsDir, dirName, 'sub-agents')
|
|
213
|
+
const membersDir = join(agentsDir, dirName, 'members')
|
|
214
|
+
const hasSubAgents = existsSync(subAgentsDir)
|
|
215
|
+
const hasMembers = existsSync(membersDir)
|
|
216
|
+
|
|
217
|
+
// Skip directories with nothing useful
|
|
218
|
+
if (!hasSoul && !hasSubAgents && !hasMembers) continue
|
|
219
|
+
|
|
96
220
|
let name = slugToName(dirName)
|
|
97
221
|
let title = 'Agent'
|
|
222
|
+
const subAgentIds: string[] = []
|
|
98
223
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
if (
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
224
|
+
// Parse SOUL.md for name and title
|
|
225
|
+
if (hasSoul) {
|
|
226
|
+
const content = safeRead(soulFile)
|
|
227
|
+
if (content) {
|
|
228
|
+
const parsed = parseSoulHeading(content)
|
|
229
|
+
if (parsed.name) name = parsed.name
|
|
230
|
+
if (parsed.title) title = parsed.title
|
|
231
|
+
|
|
232
|
+
// Also check for Role: or Title: lines as fallback
|
|
233
|
+
if (title === 'Agent') {
|
|
234
|
+
const roleMatch = content.match(/^##\s*Role\b/mi)
|
|
235
|
+
if (roleMatch) {
|
|
236
|
+
// Try to get first line after ## Role
|
|
237
|
+
const afterRole = content.slice(roleMatch.index! + roleMatch[0].length)
|
|
238
|
+
const firstLine = afterRole.match(/\n\s*\n(.+)/)?.[1]?.trim()
|
|
239
|
+
// Don't use long role descriptions as titles
|
|
240
|
+
if (firstLine && firstLine.length < 80) title = firstLine
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Discover sub-agents (sub-agents/ and members/ directories)
|
|
247
|
+
const subAgentFiles = [
|
|
248
|
+
...scanSubAgentDir(subAgentsDir),
|
|
249
|
+
...scanSubAgentDir(membersDir),
|
|
250
|
+
]
|
|
251
|
+
|
|
252
|
+
for (const sub of subAgentFiles) {
|
|
253
|
+
const subId = `${dirName}-${basename(sub.fileName, '.md').toLowerCase()}`
|
|
254
|
+
subAgentIds.push(subId)
|
|
255
|
+
|
|
256
|
+
const subParsed = parseSoulHeading(sub.content)
|
|
257
|
+
const subName = subParsed.name || slugToName(basename(sub.fileName, '.md'))
|
|
258
|
+
const subTitle = subParsed.title || 'Agent'
|
|
259
|
+
|
|
260
|
+
discovered.push({
|
|
261
|
+
id: subId,
|
|
262
|
+
name: subName,
|
|
263
|
+
title: subTitle,
|
|
264
|
+
reportsTo: dirName,
|
|
265
|
+
directReports: [],
|
|
266
|
+
soulPath: null, // sub-agents use non-standard paths, soul loaded differently
|
|
267
|
+
voiceId: null,
|
|
268
|
+
color: DISCOVER_COLORS[colorIndex++ % DISCOVER_COLORS.length],
|
|
269
|
+
emoji: subName.charAt(0).toUpperCase(),
|
|
270
|
+
tools: ['read', 'write'],
|
|
271
|
+
memoryPath: null,
|
|
272
|
+
description: `${subName} agent.`,
|
|
273
|
+
})
|
|
274
|
+
}
|
|
106
275
|
|
|
107
276
|
discovered.push({
|
|
108
277
|
id: dirName,
|
|
109
278
|
name,
|
|
110
279
|
title,
|
|
111
280
|
reportsTo: hasRoot ? rootId : null,
|
|
112
|
-
directReports:
|
|
113
|
-
soulPath: `agents/${dirName}/SOUL.md
|
|
281
|
+
directReports: subAgentIds,
|
|
282
|
+
soulPath: hasSoul ? `agents/${dirName}/SOUL.md` : null,
|
|
114
283
|
voiceId: null,
|
|
115
|
-
color: DISCOVER_COLORS[
|
|
284
|
+
color: DISCOVER_COLORS[colorIndex++ % DISCOVER_COLORS.length],
|
|
116
285
|
emoji: name.charAt(0).toUpperCase(),
|
|
117
286
|
tools: ['read', 'write'],
|
|
118
287
|
memoryPath: null,
|
|
119
288
|
description: `${name} agent.`,
|
|
120
289
|
})
|
|
121
|
-
}
|
|
290
|
+
}
|
|
122
291
|
|
|
123
292
|
return discovered.length > 0 ? discovered : null
|
|
124
293
|
}
|
package/lib/agents.test.ts
CHANGED
|
@@ -122,6 +122,7 @@ vi.mock('@/lib/agents.json', () => ({
|
|
|
122
122
|
|
|
123
123
|
// We need to import AFTER mocks are set up
|
|
124
124
|
import { getAgents, getAgent } from './agents'
|
|
125
|
+
import { parseSoulHeading, parseIdentity } from './agents-registry'
|
|
125
126
|
|
|
126
127
|
beforeEach(() => {
|
|
127
128
|
vi.clearAllMocks()
|
|
@@ -131,15 +132,105 @@ beforeEach(() => {
|
|
|
131
132
|
mockReaddirSync.mockReturnValue([])
|
|
132
133
|
})
|
|
133
134
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// parseSoulHeading -- heading extraction unit tests
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
describe('parseSoulHeading', () => {
|
|
140
|
+
it('strips "SOUL.md — " prefix', () => {
|
|
141
|
+
const result = parseSoulHeading('# SOUL.md — VERA\nContent here')
|
|
142
|
+
expect(result.name).toBe('VERA')
|
|
143
|
+
expect(result.title).toBeNull()
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('strips "SOUL.md - " prefix (regular dash)', () => {
|
|
147
|
+
const result = parseSoulHeading('# SOUL.md - HERALD\nContent')
|
|
148
|
+
expect(result.name).toBe('HERALD')
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('returns null name for generic "Who You Are" heading', () => {
|
|
152
|
+
const result = parseSoulHeading('# SOUL.md - Who You Are\nContent')
|
|
153
|
+
expect(result.name).toBeNull()
|
|
154
|
+
expect(result.title).toBeNull()
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('parses name and title from em-dash separated heading', () => {
|
|
158
|
+
const result = parseSoulHeading('# ECHO — Community Voice Monitor\nContent')
|
|
159
|
+
expect(result.name).toBe('ECHO')
|
|
160
|
+
expect(result.title).toBe('Community Voice Monitor')
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('parses name and title from comma-separated heading after prefix strip', () => {
|
|
164
|
+
const result = parseSoulHeading('# SOUL.md — KAZE, Flight Research Agent\nContent')
|
|
165
|
+
expect(result.name).toBe('KAZE')
|
|
166
|
+
expect(result.title).toBe('Flight Research Agent')
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('parses name and title from "LUMEN, SEO Team Director" after prefix strip', () => {
|
|
170
|
+
const result = parseSoulHeading('# SOUL.md — LUMEN, SEO Team Director\nContent')
|
|
171
|
+
expect(result.name).toBe('LUMEN')
|
|
172
|
+
expect(result.title).toBe('SEO Team Director')
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('parses clean heading without prefix', () => {
|
|
176
|
+
const result = parseSoulHeading('# CARTOGRAPHER — Keyword Territory Agent\nContent')
|
|
177
|
+
expect(result.name).toBe('CARTOGRAPHER')
|
|
178
|
+
expect(result.title).toBe('Keyword Territory Agent')
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('parses sub-agent heading', () => {
|
|
182
|
+
const result = parseSoulHeading('# QUILL — Herald\'s Content Writer\n## Role')
|
|
183
|
+
expect(result.name).toBe('QUILL')
|
|
184
|
+
expect(result.title).toBe('Herald\'s Content Writer')
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('returns null for content with no heading', () => {
|
|
188
|
+
const result = parseSoulHeading('No heading here, just text.')
|
|
189
|
+
expect(result.name).toBeNull()
|
|
190
|
+
expect(result.title).toBeNull()
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('handles en-dash separator', () => {
|
|
194
|
+
const result = parseSoulHeading('# AGENT – Task Runner\nContent')
|
|
195
|
+
expect(result.name).toBe('AGENT')
|
|
196
|
+
expect(result.title).toBe('Task Runner')
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('handles simple name with no separator', () => {
|
|
200
|
+
const result = parseSoulHeading('# MyBot\nContent')
|
|
201
|
+
expect(result.name).toBe('MyBot')
|
|
202
|
+
expect(result.title).toBeNull()
|
|
203
|
+
})
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// parseIdentity
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
|
|
210
|
+
describe('parseIdentity', () => {
|
|
211
|
+
it('extracts name and emoji from IDENTITY.md format', () => {
|
|
212
|
+
const content = `# IDENTITY.md - Who Am I?
|
|
213
|
+
|
|
214
|
+
- **Name:** Jarvis
|
|
215
|
+
- **Creature:** AI assistant
|
|
216
|
+
- **Emoji:** 🤖`
|
|
217
|
+
const result = parseIdentity(content)
|
|
218
|
+
expect(result.name).toBe('Jarvis')
|
|
219
|
+
expect(result.emoji).toBe('🤖')
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('returns nulls when fields are missing', () => {
|
|
223
|
+
const result = parseIdentity('# Just a heading\nNo identity fields here.')
|
|
224
|
+
expect(result.name).toBeNull()
|
|
225
|
+
expect(result.emoji).toBeNull()
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it('handles name without emoji', () => {
|
|
229
|
+
const result = parseIdentity('- **Name:** CustomBot')
|
|
230
|
+
expect(result.name).toBe('CustomBot')
|
|
231
|
+
expect(result.emoji).toBeNull()
|
|
232
|
+
})
|
|
233
|
+
})
|
|
143
234
|
|
|
144
235
|
// ---------------------------------------------------------------------------
|
|
145
236
|
// Registry loading: bundled fallback vs workspace override vs auto-discovery
|
|
@@ -212,7 +303,6 @@ describe('agent registry loading', () => {
|
|
|
212
303
|
})
|
|
213
304
|
|
|
214
305
|
const agents = await getAgents()
|
|
215
|
-
// Should fall back to bundled agents, not crash
|
|
216
306
|
expect(agents.length).toBe(bundledAgents.length)
|
|
217
307
|
expect(agents.map(a => a.id)).toContain('jarvis')
|
|
218
308
|
})
|
|
@@ -244,7 +334,6 @@ describe('agent registry loading', () => {
|
|
|
244
334
|
|
|
245
335
|
mockExistsSync.mockImplementation((p: string) => {
|
|
246
336
|
if (p === '/tmp/ws/clawport/agents.json') return true
|
|
247
|
-
// These would trigger discovery, but override takes priority
|
|
248
337
|
if (p === '/tmp/ws/SOUL.md') return true
|
|
249
338
|
if (p === '/tmp/ws/agents') return true
|
|
250
339
|
return false
|
|
@@ -265,50 +354,214 @@ describe('agent registry loading', () => {
|
|
|
265
354
|
// ---------------------------------------------------------------------------
|
|
266
355
|
|
|
267
356
|
describe('auto-discovery from workspace', () => {
|
|
268
|
-
it('discovers agents
|
|
357
|
+
it('discovers agents and uses IDENTITY.md for root name/emoji', async () => {
|
|
269
358
|
vi.stubEnv('WORKSPACE_PATH', '/tmp/ws')
|
|
270
359
|
|
|
271
360
|
mockExistsSync.mockImplementation((p: string) => {
|
|
272
361
|
if (p === '/tmp/ws/clawport/agents.json') return false
|
|
273
362
|
if (p === '/tmp/ws/SOUL.md') return true
|
|
363
|
+
if (p === '/tmp/ws/IDENTITY.md') return true
|
|
274
364
|
if (p === '/tmp/ws/agents') return true
|
|
275
|
-
if (p === '/tmp/ws/agents/
|
|
276
|
-
if (p === '/tmp/ws/agents/
|
|
365
|
+
if (p === '/tmp/ws/agents/echo/SOUL.md') return true
|
|
366
|
+
if (p === '/tmp/ws/agents/echo/sub-agents') return false
|
|
367
|
+
if (p === '/tmp/ws/agents/echo/members') return false
|
|
277
368
|
return false
|
|
278
369
|
})
|
|
279
370
|
|
|
280
371
|
mockReaddirSync.mockReturnValue([
|
|
281
|
-
{ name: '
|
|
282
|
-
{ name: 'bot-b', isDirectory: () => true },
|
|
283
|
-
{ name: 'README.md', isDirectory: () => false },
|
|
372
|
+
{ name: 'echo', isDirectory: () => true },
|
|
284
373
|
])
|
|
285
374
|
|
|
286
375
|
mockReadFileSync.mockImplementation((p: string) => {
|
|
287
|
-
if (p === '/tmp/ws/
|
|
288
|
-
if (p === '/tmp/ws/
|
|
289
|
-
if (p === '/tmp/ws/agents/
|
|
376
|
+
if (p === '/tmp/ws/IDENTITY.md') return '- **Name:** Jarvis\n- **Emoji:** 🤖'
|
|
377
|
+
if (p === '/tmp/ws/SOUL.md') return '# SOUL.md - Who You Are\nContent'
|
|
378
|
+
if (p === '/tmp/ws/agents/echo/SOUL.md') return '# ECHO — Community Voice Monitor\nContent'
|
|
290
379
|
throw new Error('ENOENT')
|
|
291
380
|
})
|
|
292
381
|
|
|
293
382
|
const agents = await getAgents()
|
|
294
|
-
|
|
383
|
+
const root = agents.find(a => a.reportsTo === null)!
|
|
384
|
+
expect(root.name).toBe('Jarvis')
|
|
385
|
+
expect(root.id).toBe('jarvis')
|
|
386
|
+
expect(root.emoji).toBe('🤖')
|
|
387
|
+
expect(root.directReports).toContain('echo')
|
|
388
|
+
|
|
389
|
+
const echo = agents.find(a => a.id === 'echo')!
|
|
390
|
+
expect(echo.name).toBe('ECHO')
|
|
391
|
+
expect(echo.title).toBe('Community Voice Monitor')
|
|
392
|
+
expect(echo.reportsTo).toBe('jarvis')
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
it('falls back to SOUL.md heading when IDENTITY.md is missing', async () => {
|
|
396
|
+
vi.stubEnv('WORKSPACE_PATH', '/tmp/ws')
|
|
397
|
+
|
|
398
|
+
mockExistsSync.mockImplementation((p: string) => {
|
|
399
|
+
if (p === '/tmp/ws/clawport/agents.json') return false
|
|
400
|
+
if (p === '/tmp/ws/IDENTITY.md') return false
|
|
401
|
+
if (p === '/tmp/ws/SOUL.md') return true
|
|
402
|
+
if (p === '/tmp/ws/agents') return true
|
|
403
|
+
if (p === '/tmp/ws/agents/bot/SOUL.md') return true
|
|
404
|
+
if (p === '/tmp/ws/agents/bot/sub-agents') return false
|
|
405
|
+
if (p === '/tmp/ws/agents/bot/members') return false
|
|
406
|
+
return false
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
mockReaddirSync.mockReturnValue([
|
|
410
|
+
{ name: 'bot', isDirectory: () => true },
|
|
411
|
+
])
|
|
412
|
+
|
|
413
|
+
mockReadFileSync.mockImplementation((p: string) => {
|
|
414
|
+
if (p === '/tmp/ws/SOUL.md') return '# SOUL.md — MyBot\nContent'
|
|
415
|
+
if (p === '/tmp/ws/agents/bot/SOUL.md') return '# Bot Agent\nContent'
|
|
416
|
+
throw new Error('ENOENT')
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
const agents = await getAgents()
|
|
420
|
+
const root = agents.find(a => a.reportsTo === null)!
|
|
421
|
+
expect(root.name).toBe('MyBot')
|
|
422
|
+
expect(root.id).toBe('mybot')
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
it('uses "Main" as root name when SOUL.md heading is generic', async () => {
|
|
426
|
+
vi.stubEnv('WORKSPACE_PATH', '/tmp/ws')
|
|
427
|
+
|
|
428
|
+
mockExistsSync.mockImplementation((p: string) => {
|
|
429
|
+
if (p === '/tmp/ws/clawport/agents.json') return false
|
|
430
|
+
if (p === '/tmp/ws/IDENTITY.md') return false
|
|
431
|
+
if (p === '/tmp/ws/SOUL.md') return true
|
|
432
|
+
if (p === '/tmp/ws/agents') return true
|
|
433
|
+
if (p === '/tmp/ws/agents/bot/SOUL.md') return true
|
|
434
|
+
if (p === '/tmp/ws/agents/bot/sub-agents') return false
|
|
435
|
+
if (p === '/tmp/ws/agents/bot/members') return false
|
|
436
|
+
return false
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
mockReaddirSync.mockReturnValue([
|
|
440
|
+
{ name: 'bot', isDirectory: () => true },
|
|
441
|
+
])
|
|
295
442
|
|
|
443
|
+
mockReadFileSync.mockImplementation((p: string) => {
|
|
444
|
+
if (p === '/tmp/ws/SOUL.md') return '# SOUL.md - Who You Are\nContent'
|
|
445
|
+
if (p === '/tmp/ws/agents/bot/SOUL.md') return '# Bot\nContent'
|
|
446
|
+
throw new Error('ENOENT')
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
const agents = await getAgents()
|
|
296
450
|
const root = agents.find(a => a.reportsTo === null)!
|
|
297
|
-
expect(root.name).toBe('
|
|
298
|
-
expect(root.
|
|
299
|
-
|
|
300
|
-
|
|
451
|
+
expect(root.name).toBe('Main')
|
|
452
|
+
expect(root.id).toBe('main')
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
it('discovers sub-agents in sub-agents/ directory', async () => {
|
|
456
|
+
vi.stubEnv('WORKSPACE_PATH', '/tmp/ws')
|
|
457
|
+
|
|
458
|
+
mockExistsSync.mockImplementation((p: string) => {
|
|
459
|
+
if (p === '/tmp/ws/clawport/agents.json') return false
|
|
460
|
+
if (p === '/tmp/ws/SOUL.md') return true
|
|
461
|
+
if (p === '/tmp/ws/IDENTITY.md') return false
|
|
462
|
+
if (p === '/tmp/ws/agents') return true
|
|
463
|
+
if (p === '/tmp/ws/agents/herald/SOUL.md') return true
|
|
464
|
+
if (p === '/tmp/ws/agents/herald/sub-agents') return true
|
|
465
|
+
if (p === '/tmp/ws/agents/herald/sub-agents/QUILL.md') return true
|
|
466
|
+
if (p === '/tmp/ws/agents/herald/sub-agents/MAVEN.md') return true
|
|
467
|
+
if (p === '/tmp/ws/agents/herald/members') return false
|
|
468
|
+
return false
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
// First call: agents/ dir, second call: sub-agents/ dir
|
|
472
|
+
mockReaddirSync
|
|
473
|
+
.mockReturnValueOnce([{ name: 'herald', isDirectory: () => true }])
|
|
474
|
+
.mockReturnValueOnce(['QUILL.md', 'MAVEN.md'])
|
|
475
|
+
|
|
476
|
+
mockReadFileSync.mockImplementation((p: string) => {
|
|
477
|
+
if (p === '/tmp/ws/SOUL.md') return '# SOUL.md - Who You Are'
|
|
478
|
+
if (p === '/tmp/ws/agents/herald/SOUL.md') return '# SOUL.md — HERALD\n## Role'
|
|
479
|
+
if (p === '/tmp/ws/agents/herald/sub-agents/QUILL.md') return '# QUILL — Content Writer'
|
|
480
|
+
if (p === '/tmp/ws/agents/herald/sub-agents/MAVEN.md') return '# MAVEN — LinkedIn Strategist'
|
|
481
|
+
throw new Error('ENOENT')
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
const agents = await getAgents()
|
|
485
|
+
const herald = agents.find(a => a.id === 'herald')!
|
|
486
|
+
expect(herald.name).toBe('HERALD')
|
|
487
|
+
expect(herald.directReports).toContain('herald-quill')
|
|
488
|
+
expect(herald.directReports).toContain('herald-maven')
|
|
489
|
+
|
|
490
|
+
const quill = agents.find(a => a.id === 'herald-quill')!
|
|
491
|
+
expect(quill.name).toBe('QUILL')
|
|
492
|
+
expect(quill.title).toBe('Content Writer')
|
|
493
|
+
expect(quill.reportsTo).toBe('herald')
|
|
494
|
+
|
|
495
|
+
const maven = agents.find(a => a.id === 'herald-maven')!
|
|
496
|
+
expect(maven.name).toBe('MAVEN')
|
|
497
|
+
expect(maven.title).toBe('LinkedIn Strategist')
|
|
498
|
+
expect(maven.reportsTo).toBe('herald')
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
it('discovers members/ directory agents', async () => {
|
|
502
|
+
vi.stubEnv('WORKSPACE_PATH', '/tmp/ws')
|
|
503
|
+
|
|
504
|
+
mockExistsSync.mockImplementation((p: string) => {
|
|
505
|
+
if (p === '/tmp/ws/clawport/agents.json') return false
|
|
506
|
+
if (p === '/tmp/ws/SOUL.md') return false
|
|
507
|
+
if (p === '/tmp/ws/agents') return true
|
|
508
|
+
if (p === '/tmp/ws/agents/seo-team/SOUL.md') return true
|
|
509
|
+
if (p === '/tmp/ws/agents/seo-team/sub-agents') return false
|
|
510
|
+
if (p === '/tmp/ws/agents/seo-team/members') return true
|
|
511
|
+
if (p === '/tmp/ws/agents/seo-team/members/WRITER.md') return true
|
|
512
|
+
return false
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
mockReaddirSync
|
|
516
|
+
.mockReturnValueOnce([{ name: 'seo-team', isDirectory: () => true }])
|
|
517
|
+
.mockReturnValueOnce(['WRITER.md'])
|
|
518
|
+
|
|
519
|
+
mockReadFileSync.mockImplementation((p: string) => {
|
|
520
|
+
if (p === '/tmp/ws/agents/seo-team/SOUL.md') return '# SOUL.md — LUMEN, SEO Team Director'
|
|
521
|
+
if (p === '/tmp/ws/agents/seo-team/members/WRITER.md') return '# WRITER — Conversion-Focused Content Creator'
|
|
522
|
+
throw new Error('ENOENT')
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
const agents = await getAgents()
|
|
526
|
+
const lumen = agents.find(a => a.id === 'seo-team')!
|
|
527
|
+
expect(lumen.name).toBe('LUMEN')
|
|
528
|
+
expect(lumen.title).toBe('SEO Team Director')
|
|
529
|
+
expect(lumen.directReports).toContain('seo-team-writer')
|
|
530
|
+
|
|
531
|
+
const writer = agents.find(a => a.id === 'seo-team-writer')!
|
|
532
|
+
expect(writer.name).toBe('WRITER')
|
|
533
|
+
expect(writer.title).toBe('Conversion-Focused Content Creator')
|
|
534
|
+
expect(writer.reportsTo).toBe('seo-team')
|
|
535
|
+
})
|
|
536
|
+
|
|
537
|
+
it('discovers dirs without SOUL.md if they have sub-agents', async () => {
|
|
538
|
+
vi.stubEnv('WORKSPACE_PATH', '/tmp/ws')
|
|
539
|
+
|
|
540
|
+
mockExistsSync.mockImplementation((p: string) => {
|
|
541
|
+
if (p === '/tmp/ws/clawport/agents.json') return false
|
|
542
|
+
if (p === '/tmp/ws/SOUL.md') return false
|
|
543
|
+
if (p === '/tmp/ws/agents') return true
|
|
544
|
+
if (p === '/tmp/ws/agents/team/SOUL.md') return false // no SOUL.md!
|
|
545
|
+
if (p === '/tmp/ws/agents/team/sub-agents') return true
|
|
546
|
+
if (p === '/tmp/ws/agents/team/sub-agents/WORKER.md') return true
|
|
547
|
+
if (p === '/tmp/ws/agents/team/members') return false
|
|
548
|
+
return false
|
|
549
|
+
})
|
|
301
550
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
551
|
+
mockReaddirSync
|
|
552
|
+
.mockReturnValueOnce([{ name: 'team', isDirectory: () => true }])
|
|
553
|
+
.mockReturnValueOnce(['WORKER.md'])
|
|
554
|
+
|
|
555
|
+
mockReadFileSync.mockImplementation((p: string) => {
|
|
556
|
+
if (p === '/tmp/ws/agents/team/sub-agents/WORKER.md') return '# WORKER — Task Runner'
|
|
557
|
+
throw new Error('ENOENT')
|
|
558
|
+
})
|
|
308
559
|
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
expect(
|
|
560
|
+
const agents = await getAgents()
|
|
561
|
+
const team = agents.find(a => a.id === 'team')!
|
|
562
|
+
expect(team.name).toBe('Team')
|
|
563
|
+
expect(team.soulPath).toBeNull()
|
|
564
|
+
expect(team.directReports).toContain('team-worker')
|
|
312
565
|
})
|
|
313
566
|
|
|
314
567
|
it('discovers agents without root SOUL.md (flat structure)', async () => {
|
|
@@ -319,6 +572,8 @@ describe('auto-discovery from workspace', () => {
|
|
|
319
572
|
if (p === '/tmp/ws/SOUL.md') return false
|
|
320
573
|
if (p === '/tmp/ws/agents') return true
|
|
321
574
|
if (p === '/tmp/ws/agents/worker/SOUL.md') return true
|
|
575
|
+
if (p === '/tmp/ws/agents/worker/sub-agents') return false
|
|
576
|
+
if (p === '/tmp/ws/agents/worker/members') return false
|
|
322
577
|
return false
|
|
323
578
|
})
|
|
324
579
|
|
|
@@ -345,59 +600,69 @@ describe('auto-discovery from workspace', () => {
|
|
|
345
600
|
expect(agents.length).toBe(bundledAgents.length)
|
|
346
601
|
})
|
|
347
602
|
|
|
348
|
-
it('
|
|
603
|
+
it('uses directory slug as name when SOUL.md has no heading', async () => {
|
|
349
604
|
vi.stubEnv('WORKSPACE_PATH', '/tmp/ws')
|
|
350
605
|
|
|
351
606
|
mockExistsSync.mockImplementation((p: string) => {
|
|
352
607
|
if (p === '/tmp/ws/clawport/agents.json') return false
|
|
353
|
-
if (p === '/tmp/ws/SOUL.md') return
|
|
608
|
+
if (p === '/tmp/ws/SOUL.md') return false
|
|
354
609
|
if (p === '/tmp/ws/agents') return true
|
|
355
|
-
if (p === '/tmp/ws/agents/
|
|
356
|
-
if (p === '/tmp/ws/agents/
|
|
610
|
+
if (p === '/tmp/ws/agents/my-bot/SOUL.md') return true
|
|
611
|
+
if (p === '/tmp/ws/agents/my-bot/sub-agents') return false
|
|
612
|
+
if (p === '/tmp/ws/agents/my-bot/members') return false
|
|
357
613
|
return false
|
|
358
614
|
})
|
|
359
615
|
|
|
360
616
|
mockReaddirSync.mockReturnValue([
|
|
361
|
-
{ name: '
|
|
362
|
-
{ name: 'empty', isDirectory: () => true },
|
|
617
|
+
{ name: 'my-bot', isDirectory: () => true },
|
|
363
618
|
])
|
|
364
619
|
|
|
365
620
|
mockReadFileSync.mockImplementation((p: string) => {
|
|
366
|
-
if (p === '/tmp/ws/SOUL.md') return '
|
|
367
|
-
if (p === '/tmp/ws/agents/valid/SOUL.md') return '# Valid Agent'
|
|
621
|
+
if (p === '/tmp/ws/agents/my-bot/SOUL.md') return 'No heading here, just text.'
|
|
368
622
|
throw new Error('ENOENT')
|
|
369
623
|
})
|
|
370
624
|
|
|
371
625
|
const agents = await getAgents()
|
|
372
|
-
|
|
373
|
-
expect(agents).
|
|
374
|
-
expect(agents.
|
|
626
|
+
expect(agents).toHaveLength(1)
|
|
627
|
+
expect(agents[0].id).toBe('my-bot')
|
|
628
|
+
expect(agents[0].name).toBe('My Bot') // slugToName
|
|
375
629
|
})
|
|
376
630
|
|
|
377
|
-
it('
|
|
631
|
+
it('strips SOUL.md prefix from agent names in discovery', async () => {
|
|
378
632
|
vi.stubEnv('WORKSPACE_PATH', '/tmp/ws')
|
|
379
633
|
|
|
380
634
|
mockExistsSync.mockImplementation((p: string) => {
|
|
381
635
|
if (p === '/tmp/ws/clawport/agents.json') return false
|
|
382
636
|
if (p === '/tmp/ws/SOUL.md') return false
|
|
383
637
|
if (p === '/tmp/ws/agents') return true
|
|
384
|
-
if (p === '/tmp/ws/agents/
|
|
638
|
+
if (p === '/tmp/ws/agents/vera/SOUL.md') return true
|
|
639
|
+
if (p === '/tmp/ws/agents/vera/sub-agents') return false
|
|
640
|
+
if (p === '/tmp/ws/agents/vera/members') return false
|
|
641
|
+
if (p === '/tmp/ws/agents/kaze/SOUL.md') return true
|
|
642
|
+
if (p === '/tmp/ws/agents/kaze/sub-agents') return false
|
|
643
|
+
if (p === '/tmp/ws/agents/kaze/members') return false
|
|
385
644
|
return false
|
|
386
645
|
})
|
|
387
646
|
|
|
388
647
|
mockReaddirSync.mockReturnValue([
|
|
389
|
-
{ name: '
|
|
648
|
+
{ name: 'vera', isDirectory: () => true },
|
|
649
|
+
{ name: 'kaze', isDirectory: () => true },
|
|
390
650
|
])
|
|
391
651
|
|
|
392
652
|
mockReadFileSync.mockImplementation((p: string) => {
|
|
393
|
-
if (p === '/tmp/ws/agents/
|
|
653
|
+
if (p === '/tmp/ws/agents/vera/SOUL.md') return '# SOUL.md — VERA'
|
|
654
|
+
if (p === '/tmp/ws/agents/kaze/SOUL.md') return '# SOUL.md — KAZE, Flight Research Agent'
|
|
394
655
|
throw new Error('ENOENT')
|
|
395
656
|
})
|
|
396
657
|
|
|
397
658
|
const agents = await getAgents()
|
|
398
|
-
|
|
399
|
-
expect(
|
|
400
|
-
expect(
|
|
659
|
+
const vera = agents.find(a => a.id === 'vera')!
|
|
660
|
+
expect(vera.name).toBe('VERA')
|
|
661
|
+
expect(vera.title).toBe('Agent') // no title from heading alone
|
|
662
|
+
|
|
663
|
+
const kaze = agents.find(a => a.id === 'kaze')!
|
|
664
|
+
expect(kaze.name).toBe('KAZE')
|
|
665
|
+
expect(kaze.title).toBe('Flight Research Agent')
|
|
401
666
|
})
|
|
402
667
|
})
|
|
403
668
|
|
|
@@ -459,12 +724,12 @@ describe('getAgents', () => {
|
|
|
459
724
|
// Block auto-discovery, allow SOUL file reads for bundled agents
|
|
460
725
|
if (p === '/tmp/ws/clawport/agents.json') return false
|
|
461
726
|
if (p === '/tmp/ws/SOUL.md') return false
|
|
727
|
+
if (p === '/tmp/ws/IDENTITY.md') return false
|
|
462
728
|
if (p === '/tmp/ws/agents') return false
|
|
463
729
|
return true
|
|
464
730
|
})
|
|
465
731
|
mockReadFileSync.mockReturnValue('# Agent SOUL content')
|
|
466
732
|
const agents = await getAgents()
|
|
467
|
-
// vera has soulPath: 'agents/vera/SOUL.md' which won't conflict with discovery
|
|
468
733
|
const vera = agents.find(a => a.id === 'vera')!
|
|
469
734
|
expect(vera.soul).toBe('# Agent SOUL content')
|
|
470
735
|
})
|
|
@@ -474,6 +739,7 @@ describe('getAgents', () => {
|
|
|
474
739
|
mockExistsSync.mockImplementation((p: string) => {
|
|
475
740
|
if (p === '/tmp/ws/clawport/agents.json') return false
|
|
476
741
|
if (p === '/tmp/ws/SOUL.md') return false
|
|
742
|
+
if (p === '/tmp/ws/IDENTITY.md') return false
|
|
477
743
|
if (p === '/tmp/ws/agents') return false
|
|
478
744
|
return true
|
|
479
745
|
})
|
package/package.json
CHANGED
package/scripts/setup.mjs
CHANGED
|
@@ -64,6 +64,34 @@ function detectGatewayToken() {
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
function checkHttpEndpointEnabled() {
|
|
68
|
+
const configPath = join(homedir(), '.openclaw', 'openclaw.json')
|
|
69
|
+
if (!existsSync(configPath)) return null // can't check
|
|
70
|
+
try {
|
|
71
|
+
const config = JSON.parse(readFileSync(configPath, 'utf-8'))
|
|
72
|
+
return config?.gateway?.http?.endpoints?.chatCompletions?.enabled === true
|
|
73
|
+
} catch {
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function enableHttpEndpoint() {
|
|
79
|
+
const configPath = join(homedir(), '.openclaw', 'openclaw.json')
|
|
80
|
+
if (!existsSync(configPath)) return false
|
|
81
|
+
try {
|
|
82
|
+
const config = JSON.parse(readFileSync(configPath, 'utf-8'))
|
|
83
|
+
if (!config.gateway) config.gateway = {}
|
|
84
|
+
if (!config.gateway.http) config.gateway.http = {}
|
|
85
|
+
if (!config.gateway.http.endpoints) config.gateway.http.endpoints = {}
|
|
86
|
+
if (!config.gateway.http.endpoints.chatCompletions) config.gateway.http.endpoints.chatCompletions = {}
|
|
87
|
+
config.gateway.http.endpoints.chatCompletions.enabled = true
|
|
88
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8')
|
|
89
|
+
return true
|
|
90
|
+
} catch {
|
|
91
|
+
return false
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
67
95
|
async function checkGatewayRunning() {
|
|
68
96
|
try {
|
|
69
97
|
const res = await fetch('http://127.0.0.1:18789/', {
|
|
@@ -123,6 +151,24 @@ async function main() {
|
|
|
123
151
|
console.log(` ${yellow('!')} Gateway not responding at localhost:18789`)
|
|
124
152
|
console.log(` ${dim('Start it with: openclaw gateway run')}`)
|
|
125
153
|
}
|
|
154
|
+
|
|
155
|
+
// Check HTTP chat completions endpoint
|
|
156
|
+
const httpEnabled = checkHttpEndpointEnabled()
|
|
157
|
+
if (httpEnabled === true) {
|
|
158
|
+
console.log(` ${green('+')} HTTP chat completions endpoint ${dim('enabled')}`)
|
|
159
|
+
} else if (httpEnabled === false) {
|
|
160
|
+
console.log(` ${yellow('!')} HTTP chat completions endpoint is ${bold('disabled')}`)
|
|
161
|
+
console.log(` ${dim('ClawPort needs this to chat with agents.')}`)
|
|
162
|
+
const enable = await ask(` ${yellow('?')} Enable it in openclaw.json? (Y/n) `)
|
|
163
|
+
if (enable.toLowerCase() !== 'n') {
|
|
164
|
+
if (enableHttpEndpoint()) {
|
|
165
|
+
console.log(` ${green('+')} Enabled! ${dim('Restart the gateway for this to take effect.')}`)
|
|
166
|
+
} else {
|
|
167
|
+
console.log(` ${red('x')} Could not update openclaw.json. Enable it manually:`)
|
|
168
|
+
console.log(` ${dim('Set gateway.http.endpoints.chatCompletions.enabled = true in ~/.openclaw/openclaw.json')}`)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
126
172
|
console.log()
|
|
127
173
|
|
|
128
174
|
// Handle missing values
|