Skip to content

Commit de3ea46

Browse files
Apply npm-app changes from brandon/base-lite branch
Adds agent resolution logic and updates CLI handlers: - New agent resolution files (resolve.ts and resolve.test.ts) - Updates to CLI handlers for agents, publishing, and subagent management - New traces handler file - Modifications to main CLI and client files 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
1 parent e6a6496 commit de3ea46

8 files changed

Lines changed: 535 additions & 61 deletions

File tree

npm-app/src/agents/resolve.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { describe, it, expect } from 'bun:test'
2+
import { DEFAULT_ORG_PREFIX } from '@codebuff/common/util/agent-name-normalization'
3+
import { resolveCliAgentId } from './resolve'
4+
5+
describe('resolveCliAgentId', () => {
6+
it('returns undefined when input is undefined', () => {
7+
expect(resolveCliAgentId(undefined, [])).toBeUndefined()
8+
})
9+
10+
it('preserves explicitly prefixed identifiers', () => {
11+
expect(resolveCliAgentId('publisher/name', [])).toBe('publisher/name')
12+
expect(resolveCliAgentId(`${DEFAULT_ORG_PREFIX}foo@1.2.3`, [])).toBe(
13+
`${DEFAULT_ORG_PREFIX}foo@1.2.3`,
14+
)
15+
})
16+
it('returns input as-is when it exists locally', () => {
17+
expect(resolveCliAgentId('local-agent', ['local-agent'])).toBe(
18+
'local-agent',
19+
)
20+
})
21+
22+
it('prefixes unknown, unprefixed ids with DEFAULT_ORG_PREFIX', () => {
23+
expect(resolveCliAgentId('unknown', [])).toBe(
24+
`${DEFAULT_ORG_PREFIX}unknown`,
25+
)
26+
})
27+
})

npm-app/src/agents/resolve.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { DEFAULT_ORG_PREFIX } from '@codebuff/common/util/agent-name-normalization'
2+
3+
export function resolveCliAgentId(
4+
input: string | undefined,
5+
localAgentIds: string[],
6+
): string | undefined {
7+
if (!input) return input
8+
9+
// Preserve explicitly prefixed identifiers like publisher/name
10+
if (input.includes('/')) return input
11+
12+
// If it exists locally, use as-is
13+
if (localAgentIds.includes(input)) return input
14+
15+
// Otherwise default to <DEFAULT_ORG_PREFIX><name>
16+
return `${DEFAULT_ORG_PREFIX}${input}`
17+
}

npm-app/src/cli-handlers/agents.ts

Lines changed: 84 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@ import intermediateGitCommitter from '../../../common/src/templates/initial-agen
1919
import advancedFileExplorer from '../../../common/src/templates/initial-agents-dir/examples/03-advanced-file-explorer' with { type: 'text' }
2020
import myCustomAgent from '../../../common/src/templates/initial-agents-dir/my-custom-agent' with { type: 'text' }
2121

22-
import { loadLocalAgents, getLoadedAgentNames } from '../agents/load-agents'
22+
import {
23+
loadLocalAgents,
24+
getLoadedAgentNames,
25+
loadedAgents,
26+
} from '../agents/load-agents'
2327
import { CLI } from '../cli'
2428
import { getProjectRoot } from '../project-files'
2529
import { Spinner } from '../utils/spinner'
@@ -90,35 +94,87 @@ export async function enterAgentsBuffer(rl: any, onExit: () => void) {
9094
customAgentFiles = filterCustomAgentFiles(files)
9195
}
9296

93-
// Add agents section header
94-
actions.push({
95-
id: '__agents_header__',
96-
name:
97-
bold(cyan('Custom Agents')) +
98-
gray(` • ${customAgentFiles.length} in ${AGENT_TEMPLATES_DIR}`),
99-
description: '',
100-
isBuiltIn: false,
101-
isSectionHeader: true,
102-
})
103-
10497
// Build agent list starting with management actions
10598
agentList = [...actions]
10699

107-
// Add custom agents from .agents/templates
108-
if (customAgentFiles.length > 0) {
109-
for (const file of customAgentFiles) {
110-
const agentId = extractAgentIdFromFileName(file)
111-
const agentName = localAgents[agentId] || agentId
100+
// Collect custom agents from .agents/templates
101+
const agentEntries = customAgentFiles.map((file) => {
102+
const agentId = extractAgentIdFromFileName(file)
103+
const filePath = path.join(agentsDir, file)
104+
let mtime = 0
105+
try {
106+
mtime = fs.statSync(filePath).mtimeMs
107+
} catch {}
108+
const def = (loadedAgents as any)[agentId]
109+
return { file, agentId, filePath, mtime, def }
110+
})
111+
112+
const validAgents = agentEntries
113+
.filter((e) => e.def && e.def.id && e.def.model)
114+
.sort((a, b) => b.mtime - a.mtime)
115+
116+
const now = Date.now()
117+
const sevenDaysMs = 7 * 24 * 60 * 60 * 1000
118+
const recentAgents = validAgents.filter((e) => now - e.mtime <= sevenDaysMs)
119+
const otherAgents = validAgents.filter((e) => now - e.mtime > sevenDaysMs)
120+
121+
if (validAgents.length > 0) {
122+
if (recentAgents.length > 0) {
123+
agentList.push({
124+
id: '__recent_agents_header__',
125+
name: bold(cyan('Recently Updated')) + gray(' • last 7 days'),
126+
description: '',
127+
isBuiltIn: false,
128+
isSectionHeader: true,
129+
})
130+
131+
for (const entry of recentAgents) {
132+
const agentName =
133+
localAgents[entry.agentId] || entry.def?.displayName || entry.agentId
134+
agentList.push({
135+
id: entry.agentId,
136+
name: agentName,
137+
description: entry.def?.description || 'Custom user-defined agent',
138+
isBuiltIn: false,
139+
filePath: entry.filePath,
140+
})
141+
}
142+
}
143+
144+
if (otherAgents.length > 0) {
112145
agentList.push({
113-
id: agentId,
114-
name: agentName,
115-
description: 'Custom user-defined agent',
146+
id: '__agents_header__',
147+
name:
148+
bold(cyan('Custom Agents')) +
149+
gray(` • ${otherAgents.length} in ${AGENT_TEMPLATES_DIR}`),
150+
description: '',
116151
isBuiltIn: false,
117-
filePath: path.join(agentsDir, file),
152+
isSectionHeader: true,
118153
})
154+
155+
for (const entry of otherAgents) {
156+
const agentName =
157+
localAgents[entry.agentId] || entry.def?.displayName || entry.agentId
158+
agentList.push({
159+
id: entry.agentId,
160+
name: agentName,
161+
description: entry.def?.description || 'Custom user-defined agent',
162+
isBuiltIn: false,
163+
filePath: entry.filePath,
164+
})
165+
}
119166
}
120167
} else {
121-
// If no custom agents, add a helpful message
168+
// No valid agents; show header + placeholder
169+
agentList.push({
170+
id: '__agents_header__',
171+
name:
172+
bold(cyan('Custom Agents')) +
173+
gray(` • ${customAgentFiles.length} in ${AGENT_TEMPLATES_DIR}`),
174+
description: '',
175+
isBuiltIn: false,
176+
isSectionHeader: true,
177+
})
122178
agentList.push({
123179
id: '__no_agents__',
124180
name: gray('No custom agents found'),
@@ -128,8 +184,6 @@ export async function enterAgentsBuffer(rl: any, onExit: () => void) {
128184
})
129185
}
130186

131-
// No need for special handling here since we now have a proper placeholder
132-
133187
// Initialize selection to first selectable item
134188
selectedIndex = 0
135189
// Find first selectable item (skip section headers, separators, placeholders)
@@ -400,7 +454,7 @@ function renderAgentsList() {
400454
}
401455

402456
// Display status line at bottom
403-
const statusLine = `\n${gray(`Use ↑/↓/j/k to navigate, Enter to select, ESC to go back`)}`
457+
const statusLine = `\n${gray(`Use ↑/↓/j/k to navigate, Enter to select, ESC or q to go back`)}`
404458

405459
process.stdout.write(statusLine)
406460
process.stdout.write(HIDE_CURSOR)
@@ -416,7 +470,11 @@ function setupAgentsKeyHandler(rl: any, onExit: () => void) {
416470

417471
// Add our custom handler
418472
process.stdin.on('keypress', (str: string, key: any) => {
419-
if (key && key.name === 'escape') {
473+
// Support ESC or 'q' (no ctrl/meta) to go back
474+
if (
475+
(key && key.name === 'escape') ||
476+
(!key?.ctrl && !key?.meta && str === 'q')
477+
) {
420478
exitAgentsBuffer(rl)
421479
onExit()
422480
return

npm-app/src/cli-handlers/publish.ts

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {
1212
PublishAgentsResponse,
1313
} from '@codebuff/common/types/api/agents/publish'
1414
import type { DynamicAgentTemplate } from '@codebuff/common/types/dynamic-agent-template'
15+
import { pluralize } from '@codebuff/common/util/string'
1516

1617
/**
1718
* Handle the publish command to upload agent templates to the backend
@@ -105,7 +106,9 @@ import type { DynamicAgentTemplate } from '@codebuff/common/types/dynamic-agent-
105106
return
106107
}
107108

108-
console.log(red(`❌ Failed to publish agents: ${result.error}`))
109+
console.log(red(`❌ Failed to publish your agents`))
110+
if (result.details) console.log(red(`\n${result.details}`))
111+
if (result.hint) console.log(yellow(`\nHint: ${result.hint}`))
109112

110113
// Show helpful guidance based on error type
111114
if (result.error?.includes('Publisher field required')) {
@@ -179,31 +182,13 @@ async function publishAgentTemplates(
179182

180183
if (!response.ok) {
181184
result = result as PublishAgentsErrorResponse
182-
// Extract detailed error information from the response
183-
let errorMessage =
184-
result.error || `HTTP ${response.status}: ${response.statusText}`
185-
186-
// If there are validation details, include them
187-
if (result.details) {
188-
errorMessage += `\n\nDetails: ${result.details}`
189-
}
190-
191-
// If there are specific validation errors, format them nicely
192-
if (result.validationErrors && Array.isArray(result.validationErrors)) {
193-
const formattedErrors = result.validationErrors
194-
.map((err: any) => {
195-
const path =
196-
err.path && err.path.length > 0 ? `${err.path.join('.')}: ` : ''
197-
return ` • ${path}${err.message}`
198-
})
199-
.join('\n')
200-
errorMessage += `\n\nValidation errors:\n${formattedErrors}`
201-
}
202-
185+
// Build clean error object without duplicating details into the error string
203186
return {
204187
success: false,
205-
error: errorMessage,
188+
error:
189+
result.error || `HTTP ${response.status}: ${response.statusText}`,
206190
details: result.details,
191+
hint: result.hint,
207192
statusCode: response.status,
208193
availablePublishers: result.availablePublishers,
209194
validationErrors: result.validationErrors,
@@ -214,18 +199,31 @@ async function publishAgentTemplates(
214199
...result,
215200
statusCode: response.status,
216201
}
217-
} catch (error) {
202+
} catch (err: any) {
218203
// Handle network errors, timeouts, etc.
219-
if (error instanceof TypeError && error.message.includes('fetch')) {
204+
if (err instanceof TypeError && err.message.includes('fetch')) {
220205
return {
221206
success: false,
222207
error: `Network error: Unable to connect to ${websiteUrl}. Please check your internet connection and try again.`,
223208
}
224209
}
225210

211+
const body = err?.responseBody || err?.body || err
212+
const error = body?.error || body?.message || 'Failed to publish'
213+
const details = body?.details
214+
const hint = body?.hint
215+
216+
// Log for visibility
217+
console.error(`❌ Failed to publish: ${error}`)
218+
if (details) console.error(`\nDetails: ${details}`)
219+
if (hint) console.error(`\nHint: ${hint}`)
220+
221+
// Return a valid error object so callers can display the hint
226222
return {
227223
success: false,
228-
error: `Unexpected error: ${error instanceof Error ? error.message : String(error)}`,
229-
}
224+
error,
225+
details,
226+
hint,
227+
} as PublishAgentsResponse
230228
}
231229
}

npm-app/src/cli-handlers/subagent-list.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { pluralize } from '@codebuff/common/util/string'
22
import { green, yellow, cyan, magenta, bold, gray } from 'picocolors'
33

44
import { getSubagentsChronological } from '../subagent-storage'
5-
import { enterSubagentBuffer } from './subagent'
5+
import { enterSubagentBuffer } from './traces'
66
import {
77
ENTER_ALT_BUFFER,
88
EXIT_ALT_BUFFER,
@@ -329,7 +329,8 @@ function renderSubagentList() {
329329
}
330330

331331
// Display status line at bottom
332-
const statusLine = `\n${gray(`Use ↑/↓/j/k to navigate, PgUp/PgDn for fast scroll, Enter to view, ESC to go back`)}`
332+
// Update: mention ESC or q
333+
const statusLine = `\n${gray(`Use ↑/↓/j/k to navigate, PgUp/PgDn for fast scroll, Enter to view, ESC or q to go back`)}`
333334

334335
process.stdout.write(statusLine)
335336
process.stdout.write(HIDE_CURSOR)
@@ -345,7 +346,11 @@ function setupSubagentListKeyHandler(rl: any, onExit: () => void) {
345346

346347
// Add our custom handler
347348
process.stdin.on('keypress', (str: string, key: any) => {
348-
if (key && key.name === 'escape') {
349+
// Support ESC or 'q' (no ctrl/meta) to go back
350+
if (
351+
(key && key.name === 'escape') ||
352+
(!key?.ctrl && !key?.meta && str === 'q')
353+
) {
349354
exitSubagentListBuffer(rl)
350355
onExit()
351356
return

0 commit comments

Comments
 (0)