feat: add native runtime customization
OpenCode and Continue customization was previously planned but unavailable, and Runtime-native capabilities were not represented consistently across providers. GoodBuddy now provides secure Main-owned customization settings, truthful native inventories, OpenCode Agents and Commands, Continue Rules and Prompts, DSH Web Search/Fetch, MCP Prompt and Resource metadata, and manual compaction where supported. Native capabilities are presented in eleven accessible tabs with Tools separated from Commands, LSP, and Formatters. Tool source and Ask/Execute availability are explicit, external OpenCode remains connection-only, Continue reports unsupported static tool discovery instead of advertising unreachable Skills, and disposable inventory probes avoid retaining background runtimes. Ask remains read-only at the Runtime boundary, Execute keeps the existing authorization controls, and credentials remain confined to Main. Release note: 新增 OpenCode、Continue 与 DeepSeek Harness 的 Runtime 原生定制与真实能力清单;工具来源、Ask/Execute 可用性、上下文压缩和 MCP 元数据现在可清晰查看,同时继续保持 Main 进程凭据保护与现有权限边界。
This commit is contained in:
@@ -5,6 +5,8 @@ const mocks = vi.hoisted(() => {
|
||||
const client = {
|
||||
connect: vi.fn(),
|
||||
listTools: vi.fn(),
|
||||
listPrompts: vi.fn(),
|
||||
listResources: vi.fn(),
|
||||
getServerVersion: vi.fn(),
|
||||
getServerCapabilities: vi.fn(),
|
||||
close: vi.fn()
|
||||
@@ -105,7 +107,74 @@ describe('testMcpServer', () => {
|
||||
serverVersion: '1.0.0',
|
||||
dynamicToolsSupported: false,
|
||||
toolCount: 1,
|
||||
tools: [{ name: 'search', description: 'Search documents' }]
|
||||
tools: [{ name: 'search', description: 'Search documents' }],
|
||||
promptsSupported: false,
|
||||
resourcesSupported: false
|
||||
})
|
||||
})
|
||||
|
||||
it('reports bounded prompt and resource metadata without reading content', async () => {
|
||||
mocks.client.getServerCapabilities.mockReturnValue({
|
||||
tools: { listChanged: false },
|
||||
prompts: { listChanged: true },
|
||||
resources: { subscribe: false, listChanged: true }
|
||||
})
|
||||
mocks.client.listPrompts.mockResolvedValue({
|
||||
prompts: [
|
||||
{
|
||||
name: 'review',
|
||||
description: 'Review a change',
|
||||
arguments: [
|
||||
{
|
||||
name: 'scope',
|
||||
description: 'Files to inspect',
|
||||
required: true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
mocks.client.listResources.mockResolvedValue({
|
||||
resources: [
|
||||
{
|
||||
name: 'Guide',
|
||||
uri: 'docs://guide',
|
||||
description: 'Project guide',
|
||||
mimeType: 'text/markdown'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
await expect(
|
||||
testMcpServer({
|
||||
...common,
|
||||
transport: 'stdio',
|
||||
command: 'node',
|
||||
args: ['server.js']
|
||||
} satisfies ResolvedMcpServer)
|
||||
).resolves.toMatchObject({
|
||||
promptsSupported: true,
|
||||
promptCount: 1,
|
||||
prompts: [
|
||||
{
|
||||
name: 'review',
|
||||
arguments: [
|
||||
{
|
||||
name: 'scope',
|
||||
required: true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
resourcesSupported: true,
|
||||
resourceCount: 1,
|
||||
resources: [
|
||||
{
|
||||
name: 'Guide',
|
||||
uri: 'docs://guide',
|
||||
mimeType: 'text/markdown'
|
||||
}
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -57,6 +57,32 @@ export async function testMcpServer(
|
||||
)
|
||||
const version = client.getServerVersion()
|
||||
const capabilities = client.getServerCapabilities()
|
||||
const promptsSupported = Boolean(capabilities?.prompts)
|
||||
const resourcesSupported = Boolean(capabilities?.resources)
|
||||
const promptResult = promptsSupported
|
||||
? await runWithInactivityLimit(() =>
|
||||
client.listPrompts(undefined, {
|
||||
timeout: MCP_TEST_INACTIVITY_TIMEOUT_MS,
|
||||
signal: controller.signal
|
||||
})
|
||||
).catch((error: unknown) => {
|
||||
controller.signal.throwIfAborted()
|
||||
void error
|
||||
return undefined
|
||||
})
|
||||
: undefined
|
||||
const resourceResult = resourcesSupported
|
||||
? await runWithInactivityLimit(() =>
|
||||
client.listResources(undefined, {
|
||||
timeout: MCP_TEST_INACTIVITY_TIMEOUT_MS,
|
||||
signal: controller.signal
|
||||
})
|
||||
).catch((error: unknown) => {
|
||||
controller.signal.throwIfAborted()
|
||||
void error
|
||||
return undefined
|
||||
})
|
||||
: undefined
|
||||
return {
|
||||
serverName: version?.name.slice(0, 120),
|
||||
serverVersion: version?.version.slice(0, 64),
|
||||
@@ -66,7 +92,30 @@ export async function testMcpServer(
|
||||
tools: result.tools.slice(0, 100).map((tool) => ({
|
||||
name: tool.name.slice(0, 128),
|
||||
description: tool.description?.slice(0, 500)
|
||||
}))
|
||||
})),
|
||||
promptsSupported,
|
||||
promptCount: promptResult?.prompts.length,
|
||||
prompts: promptResult?.prompts.slice(0, 100).map((prompt) => ({
|
||||
name: prompt.name.slice(0, 128),
|
||||
description: prompt.description?.slice(0, 500),
|
||||
arguments: (prompt.arguments ?? [])
|
||||
.slice(0, 32)
|
||||
.map((argument) => ({
|
||||
name: argument.name.slice(0, 128),
|
||||
description: argument.description?.slice(0, 500),
|
||||
required: argument.required === true
|
||||
}))
|
||||
})),
|
||||
resourcesSupported,
|
||||
resourceCount: resourceResult?.resources.length,
|
||||
resources: resourceResult?.resources
|
||||
.slice(0, 100)
|
||||
.map((resource) => ({
|
||||
name: resource.name.slice(0, 200),
|
||||
uri: resource.uri.slice(0, 2_048),
|
||||
description: resource.description?.slice(0, 500),
|
||||
mimeType: resource.mimeType?.slice(0, 200)
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
if (signal?.aborted && !timedOut) {
|
||||
|
||||
Reference in New Issue
Block a user