btca-server 1.0.91 → 1.0.93
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/README.md +7 -1
- package/package.json +1 -1
- package/src/agent/loop.ts +13 -4
- package/src/agent/service.ts +23 -23
- package/src/collections/service.ts +93 -3
- package/src/collections/virtual-metadata.ts +3 -1
- package/src/config/config.test.ts +28 -0
- package/src/config/index.ts +7 -2
- package/src/errors.test.ts +65 -0
- package/src/errors.ts +99 -6
- package/src/index.ts +47 -14
- package/src/providers/auth.ts +1 -1
- package/src/providers/minimax.ts +1 -1
- package/src/resources/impls/npm.test.ts +337 -0
- package/src/resources/impls/npm.ts +498 -0
- package/src/resources/index.ts +12 -1
- package/src/resources/schema.ts +38 -1
- package/src/resources/service.test.ts +26 -1
- package/src/resources/service.ts +59 -17
- package/src/resources/types.ts +12 -1
- package/src/stream/service.ts +5 -3
- package/src/stream/types.ts +2 -1
- package/src/validation/index.test.ts +29 -1
- package/src/validation/index.ts +139 -1
package/README.md
CHANGED
|
@@ -125,7 +125,7 @@ The server reads configuration from `~/.btca/config.toml` or your local project'
|
|
|
125
125
|
|
|
126
126
|
- **AI Provider**: OpenCode AI provider (e.g., "opencode", "anthropic")
|
|
127
127
|
- **Model**: AI model to use (e.g., "claude-3-7-sonnet-20250219")
|
|
128
|
-
- **Resources**: Local directories
|
|
128
|
+
- **Resources**: Local directories, git repositories, or npm packages to query
|
|
129
129
|
|
|
130
130
|
Example config.toml:
|
|
131
131
|
|
|
@@ -144,6 +144,12 @@ type = "git"
|
|
|
144
144
|
name = "some-repo"
|
|
145
145
|
url = "https://github.com/user/repo"
|
|
146
146
|
branch = "main"
|
|
147
|
+
|
|
148
|
+
[[resources]]
|
|
149
|
+
type = "npm"
|
|
150
|
+
name = "react-npm"
|
|
151
|
+
package = "react"
|
|
152
|
+
version = "latest"
|
|
147
153
|
```
|
|
148
154
|
|
|
149
155
|
## Supported Providers
|
package/package.json
CHANGED
package/src/agent/loop.ts
CHANGED
|
@@ -61,10 +61,19 @@ export namespace AgentLoop {
|
|
|
61
61
|
'- list: List directory contents',
|
|
62
62
|
'',
|
|
63
63
|
'Guidelines:',
|
|
64
|
-
'-
|
|
65
|
-
'-
|
|
66
|
-
'-
|
|
67
|
-
'-
|
|
64
|
+
'- Ground answers in the loaded resources. Do not rely on unstated prior knowledge.',
|
|
65
|
+
'- Search efficiently: start with one focused list/glob pass, then read likely files; only expand search when evidence is insufficient.',
|
|
66
|
+
'- Prefer targeted grep/read over broad repeated scans once candidate files are known.',
|
|
67
|
+
'- Give clear, unambiguous answers. State assumptions, prerequisites, and important version-sensitive caveats.',
|
|
68
|
+
'- For implementation/how-to questions, provide complete step-by-step instructions with commands and code snippets.',
|
|
69
|
+
'- Be concise but thorough in your responses.',
|
|
70
|
+
'- End every answer with a "Sources" section.',
|
|
71
|
+
'- For git resources, source links must be full GitHub blob URLs.',
|
|
72
|
+
'- In "Sources", format git citations as markdown links: "- [repo/relative/path.ext](https://github.com/.../blob/.../repo/relative/path.ext)".',
|
|
73
|
+
'- Do not use raw URLs as link labels.',
|
|
74
|
+
'- Do not repeat a URL in parentheses after a link.',
|
|
75
|
+
'- Do not output sources in "url (url)" format.',
|
|
76
|
+
'- For local resources, cite local file paths (no GitHub URL required).',
|
|
68
77
|
'- If you cannot find the answer, say so clearly',
|
|
69
78
|
'',
|
|
70
79
|
agentInstructions
|
package/src/agent/service.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { Result } from 'better-result';
|
|
6
6
|
|
|
7
7
|
import { Config } from '../config/index.ts';
|
|
8
|
-
import { type TaggedErrorOptions } from '../errors.ts';
|
|
8
|
+
import { getErrorHint, getErrorMessage, type TaggedErrorOptions } from '../errors.ts';
|
|
9
9
|
import { Metrics } from '../metrics/index.ts';
|
|
10
10
|
import { Auth, getSupportedProviders } from '../providers/index.ts';
|
|
11
11
|
import type { CollectionResult } from '../collections/types.ts';
|
|
@@ -217,30 +217,30 @@ export namespace Agent {
|
|
|
217
217
|
|
|
218
218
|
await cleanup();
|
|
219
219
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
220
|
+
if (!Result.isOk(runResult)) {
|
|
221
|
+
const cause = runResult.error;
|
|
222
|
+
Metrics.error('agent.ask.error', { error: Metrics.errorInfo(cause) });
|
|
223
|
+
throw new AgentError({
|
|
224
|
+
message: getErrorMessage(cause),
|
|
225
|
+
hint:
|
|
226
|
+
getErrorHint(cause) ?? 'This may be a temporary issue. Try running the command again.',
|
|
227
|
+
cause
|
|
228
|
+
});
|
|
229
|
+
}
|
|
228
230
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
err: (error) => {
|
|
236
|
-
Metrics.error('agent.ask.error', { error: Metrics.errorInfo(error) });
|
|
237
|
-
throw new AgentError({
|
|
238
|
-
message: 'Failed to get response from AI',
|
|
239
|
-
hint: 'This may be a temporary issue. Try running the command again.',
|
|
240
|
-
cause: error
|
|
241
|
-
});
|
|
242
|
-
}
|
|
231
|
+
const result = runResult.value;
|
|
232
|
+
Metrics.info('agent.ask.complete', {
|
|
233
|
+
provider: config.provider,
|
|
234
|
+
model: config.model,
|
|
235
|
+
answerLength: result.answer.length,
|
|
236
|
+
eventCount: result.events.length
|
|
243
237
|
});
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
answer: result.answer,
|
|
241
|
+
model: result.model,
|
|
242
|
+
events: result.events
|
|
243
|
+
};
|
|
244
244
|
};
|
|
245
245
|
|
|
246
246
|
/**
|
|
@@ -7,8 +7,9 @@ import { Transaction } from '../context/transaction.ts';
|
|
|
7
7
|
import { CommonHints, getErrorHint, getErrorMessage } from '../errors.ts';
|
|
8
8
|
import { Metrics } from '../metrics/index.ts';
|
|
9
9
|
import { Resources } from '../resources/service.ts';
|
|
10
|
-
import { isGitResource } from '../resources/schema.ts';
|
|
10
|
+
import { isGitResource, isNpmResource } from '../resources/schema.ts';
|
|
11
11
|
import { FS_RESOURCE_SYSTEM_NOTE, type BtcaFsResource } from '../resources/types.ts';
|
|
12
|
+
import { parseNpmReference } from '../validation/index.ts';
|
|
12
13
|
import { CollectionError, getCollectionKey, type CollectionResult } from './types.ts';
|
|
13
14
|
import { VirtualFs } from '../vfs/virtual-fs.ts';
|
|
14
15
|
import {
|
|
@@ -25,14 +26,51 @@ export namespace Collections {
|
|
|
25
26
|
}) => Promise<CollectionResult>;
|
|
26
27
|
};
|
|
27
28
|
|
|
28
|
-
const
|
|
29
|
+
const encodePathSegments = (value: string) => value.split('/').map(encodeURIComponent).join('/');
|
|
30
|
+
|
|
31
|
+
const trimGitSuffix = (url: string) => url.replace(/\.git$/u, '').replace(/\/+$/u, '');
|
|
32
|
+
const getNpmCitationAlias = (metadata?: VirtualResourceMetadata) => {
|
|
33
|
+
if (!metadata?.package) return undefined;
|
|
34
|
+
return `npm:${metadata.package}@${metadata.version ?? 'latest'}`;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const createCollectionInstructionBlock = (
|
|
38
|
+
resource: BtcaFsResource,
|
|
39
|
+
metadata?: VirtualResourceMetadata
|
|
40
|
+
): string => {
|
|
29
41
|
const focusLines = resource.repoSubPaths.map(
|
|
30
42
|
(subPath) => `Focus: ./${resource.fsName}/${subPath}`
|
|
31
43
|
);
|
|
44
|
+
const gitRef = metadata?.branch ?? metadata?.commit;
|
|
45
|
+
const githubPrefix =
|
|
46
|
+
resource.type === 'git' && metadata?.url && gitRef
|
|
47
|
+
? `${trimGitSuffix(metadata.url)}/blob/${encodeURIComponent(gitRef)}`
|
|
48
|
+
: undefined;
|
|
49
|
+
const npmCitationAlias = resource.type === 'npm' ? getNpmCitationAlias(metadata) : undefined;
|
|
32
50
|
const lines = [
|
|
33
51
|
`## Resource: ${resource.name}`,
|
|
34
52
|
FS_RESOURCE_SYSTEM_NOTE,
|
|
35
53
|
`Path: ./${resource.fsName}`,
|
|
54
|
+
resource.type === 'git' && metadata?.url ? `Repo URL: ${trimGitSuffix(metadata.url)}` : '',
|
|
55
|
+
resource.type === 'git' && metadata?.branch ? `Repo Branch: ${metadata.branch}` : '',
|
|
56
|
+
resource.type === 'git' && metadata?.commit ? `Repo Commit: ${metadata.commit}` : '',
|
|
57
|
+
resource.type === 'npm' && metadata?.package ? `NPM Package: ${metadata.package}` : '',
|
|
58
|
+
resource.type === 'npm' && metadata?.version ? `NPM Version: ${metadata.version}` : '',
|
|
59
|
+
resource.type === 'npm' && metadata?.url ? `NPM URL: ${metadata.url}` : '',
|
|
60
|
+
npmCitationAlias ? `NPM Citation Alias: ${npmCitationAlias}` : '',
|
|
61
|
+
githubPrefix ? `GitHub Blob Prefix: ${githubPrefix}` : '',
|
|
62
|
+
githubPrefix
|
|
63
|
+
? `GitHub Citation Rule: Convert virtual paths under ./${resource.fsName}/ to repo-relative paths, then encode each path segment for GitHub URLs (example segment: "+page.server.js" -> "${encodeURIComponent('+page.server.js')}").`
|
|
64
|
+
: '',
|
|
65
|
+
githubPrefix
|
|
66
|
+
? `GitHub Citation Example: ${githubPrefix}/${encodePathSegments('src/routes/blog/+page.server.js')}`
|
|
67
|
+
: '',
|
|
68
|
+
resource.type !== 'git'
|
|
69
|
+
? 'Citation Rule: Cite local file paths only for this resource (no GitHub URL).'
|
|
70
|
+
: '',
|
|
71
|
+
npmCitationAlias
|
|
72
|
+
? `NPM Citation Rule: In "Sources", cite npm files using "${npmCitationAlias}/<file>" (for example, "${npmCitationAlias}/README.md"). Do not cite encoded virtual folder names.`
|
|
73
|
+
: '',
|
|
36
74
|
...focusLines,
|
|
37
75
|
resource.specialAgentInstructions ? `Notes: ${resource.specialAgentInstructions}` : ''
|
|
38
76
|
].filter(Boolean);
|
|
@@ -159,6 +197,32 @@ export namespace Collections {
|
|
|
159
197
|
const ANON_PREFIX = 'anonymous:';
|
|
160
198
|
const getAnonymousUrlFromName = (name: string) =>
|
|
161
199
|
name.startsWith(ANON_PREFIX) ? name.slice(ANON_PREFIX.length) : undefined;
|
|
200
|
+
const NPM_ANON_PREFIX = `${ANON_PREFIX}npm:`;
|
|
201
|
+
const NPM_META_FILE = '.btca-npm-meta.json';
|
|
202
|
+
const getAnonymousNpmReferenceFromName = (name: string) =>
|
|
203
|
+
name.startsWith(NPM_ANON_PREFIX) ? name.slice(ANON_PREFIX.length) : undefined;
|
|
204
|
+
|
|
205
|
+
const readNpmMeta = async (resourcePath: string) => {
|
|
206
|
+
const result = await Result.gen(async function* () {
|
|
207
|
+
const content = yield* Result.await(
|
|
208
|
+
Result.tryPromise(() => Bun.file(path.join(resourcePath, NPM_META_FILE)).text())
|
|
209
|
+
);
|
|
210
|
+
const parsed = yield* Result.try(
|
|
211
|
+
() =>
|
|
212
|
+
JSON.parse(content) as {
|
|
213
|
+
packageName?: string;
|
|
214
|
+
resolvedVersion?: string;
|
|
215
|
+
packageUrl?: string;
|
|
216
|
+
}
|
|
217
|
+
);
|
|
218
|
+
return Result.ok(parsed);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
return result.match({
|
|
222
|
+
ok: (value) => value,
|
|
223
|
+
err: () => null
|
|
224
|
+
});
|
|
225
|
+
};
|
|
162
226
|
|
|
163
227
|
const buildVirtualMetadata = async (args: {
|
|
164
228
|
resource: BtcaFsResource;
|
|
@@ -174,6 +238,27 @@ export namespace Collections {
|
|
|
174
238
|
repoSubPaths: args.resource.repoSubPaths,
|
|
175
239
|
loadedAt: args.loadedAt
|
|
176
240
|
};
|
|
241
|
+
|
|
242
|
+
if (args.resource.type === 'npm') {
|
|
243
|
+
const configuredDefinition =
|
|
244
|
+
args.definition && isNpmResource(args.definition) ? args.definition : null;
|
|
245
|
+
const anonymousReference = getAnonymousNpmReferenceFromName(args.resource.name);
|
|
246
|
+
const anonymousNpm = anonymousReference ? parseNpmReference(anonymousReference) : null;
|
|
247
|
+
const cached = await readNpmMeta(args.resourcePath);
|
|
248
|
+
const packageName =
|
|
249
|
+
configuredDefinition?.package ?? cached?.packageName ?? anonymousNpm?.packageName;
|
|
250
|
+
const version =
|
|
251
|
+
configuredDefinition?.version ?? cached?.resolvedVersion ?? anonymousNpm?.version;
|
|
252
|
+
const url = cached?.packageUrl ?? anonymousNpm?.packageUrl;
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
...base,
|
|
256
|
+
...(packageName ? { package: packageName } : {}),
|
|
257
|
+
...(version ? { version } : {}),
|
|
258
|
+
...(url ? { url } : {})
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
177
262
|
if (args.resource.type !== 'git') return base;
|
|
178
263
|
|
|
179
264
|
const configuredDefinition =
|
|
@@ -267,7 +352,12 @@ export namespace Collections {
|
|
|
267
352
|
resources: metadataResources
|
|
268
353
|
});
|
|
269
354
|
|
|
270
|
-
const
|
|
355
|
+
const metadataByName = new Map(
|
|
356
|
+
metadataResources.map((resource) => [resource.name, resource])
|
|
357
|
+
);
|
|
358
|
+
const instructionBlocks = loadedResources.map((resource) =>
|
|
359
|
+
createCollectionInstructionBlock(resource, metadataByName.get(resource.name))
|
|
360
|
+
);
|
|
271
361
|
|
|
272
362
|
return Result.ok({
|
|
273
363
|
path: collectionPath,
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
export type VirtualResourceMetadata = {
|
|
2
2
|
name: string;
|
|
3
3
|
fsName: string;
|
|
4
|
-
type: 'git' | 'local';
|
|
4
|
+
type: 'git' | 'local' | 'npm';
|
|
5
5
|
path: string;
|
|
6
6
|
repoSubPaths: readonly string[];
|
|
7
7
|
url?: string;
|
|
8
8
|
branch?: string;
|
|
9
9
|
commit?: string;
|
|
10
|
+
package?: string;
|
|
11
|
+
version?: string;
|
|
10
12
|
loadedAt: string;
|
|
11
13
|
};
|
|
12
14
|
|
|
@@ -66,6 +66,34 @@ describe('Config', () => {
|
|
|
66
66
|
expect(config.getResource('svelte')).toBeDefined();
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
+
it('loads npm resources from config', async () => {
|
|
70
|
+
const projectConfig = {
|
|
71
|
+
$schema: 'https://btca.dev/btca.schema.json',
|
|
72
|
+
provider: 'test-provider',
|
|
73
|
+
model: 'test-model',
|
|
74
|
+
resources: [
|
|
75
|
+
{
|
|
76
|
+
name: 'reactNpm',
|
|
77
|
+
type: 'npm',
|
|
78
|
+
package: 'react',
|
|
79
|
+
version: 'latest'
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
await fs.writeFile(path.join(testDir, 'btca.config.jsonc'), JSON.stringify(projectConfig));
|
|
85
|
+
process.chdir(testDir);
|
|
86
|
+
|
|
87
|
+
const config = await Config.load();
|
|
88
|
+
const npmResource = config.getResource('reactNpm');
|
|
89
|
+
expect(npmResource).toBeDefined();
|
|
90
|
+
expect(npmResource?.type).toBe('npm');
|
|
91
|
+
if (npmResource?.type === 'npm') {
|
|
92
|
+
expect(npmResource.package).toBe('react');
|
|
93
|
+
expect(npmResource.version).toBe('latest');
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
69
97
|
it('handles JSONC with comments', async () => {
|
|
70
98
|
const projectConfigWithComments = `{
|
|
71
99
|
// This is a comment
|
package/src/config/index.ts
CHANGED
|
@@ -71,6 +71,7 @@ const StoredConfigSchema = z.object({
|
|
|
71
71
|
type StoredConfig = z.infer<typeof StoredConfigSchema>;
|
|
72
72
|
type ProviderOptionsConfig = z.infer<typeof ProviderOptionsSchema>;
|
|
73
73
|
type ProviderOptionsMap = z.infer<typeof ProviderOptionsMapSchema>;
|
|
74
|
+
type ConfigScope = 'project' | 'global';
|
|
74
75
|
|
|
75
76
|
// Legacy config schemas (btca.json format from old CLI)
|
|
76
77
|
// There are two legacy formats:
|
|
@@ -137,7 +138,7 @@ export namespace Config {
|
|
|
137
138
|
provider: string,
|
|
138
139
|
model: string,
|
|
139
140
|
providerOptions?: ProviderOptionsConfig
|
|
140
|
-
) => Promise<{ provider: string; model: string }>;
|
|
141
|
+
) => Promise<{ provider: string; model: string; savedTo: ConfigScope }>;
|
|
141
142
|
addResource: (resource: ResourceDefinition) => Promise<ResourceDefinition>;
|
|
142
143
|
removeResource: (name: string) => Promise<void>;
|
|
143
144
|
clearResources: () => Promise<{ cleared: number }>;
|
|
@@ -689,7 +690,11 @@ export namespace Config {
|
|
|
689
690
|
setMutableConfig(updated);
|
|
690
691
|
await saveConfig(configPath, updated);
|
|
691
692
|
Metrics.info('config.model.updated', { provider, model });
|
|
692
|
-
return {
|
|
693
|
+
return {
|
|
694
|
+
provider,
|
|
695
|
+
model,
|
|
696
|
+
savedTo: currentProjectConfig ? 'project' : 'global'
|
|
697
|
+
};
|
|
693
698
|
},
|
|
694
699
|
|
|
695
700
|
addResource: async (resource: ResourceDefinition) => {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { formatErrorForDisplay, getErrorHint, getErrorMessage, getErrorTag } from './errors.ts';
|
|
4
|
+
|
|
5
|
+
describe('errors', () => {
|
|
6
|
+
it('unwraps panic wrappers to the underlying tagged error details', () => {
|
|
7
|
+
const providerError = Object.assign(new Error('Provider "opencode" is not authenticated.'), {
|
|
8
|
+
_tag: 'ProviderNotAuthenticatedError',
|
|
9
|
+
hint: 'Run "opencode auth" to configure provider credentials.'
|
|
10
|
+
});
|
|
11
|
+
const wrapped = {
|
|
12
|
+
_tag: 'Panic',
|
|
13
|
+
message: 'match err handler threw',
|
|
14
|
+
cause: {
|
|
15
|
+
_tag: 'AgentError',
|
|
16
|
+
message: 'Failed to get response from AI',
|
|
17
|
+
hint: 'This may be a temporary issue. Try running the command again.',
|
|
18
|
+
cause: {
|
|
19
|
+
_tag: 'UnhandledException',
|
|
20
|
+
message: 'Unhandled exception: Provider "opencode" is not authenticated.',
|
|
21
|
+
cause: providerError
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
expect(getErrorTag(wrapped)).toBe('ProviderNotAuthenticatedError');
|
|
27
|
+
expect(getErrorMessage(wrapped)).toBe('Provider "opencode" is not authenticated.');
|
|
28
|
+
expect(getErrorHint(wrapped)).toBe('Run "opencode auth" to configure provider credentials.');
|
|
29
|
+
expect(formatErrorForDisplay(wrapped)).toBe(
|
|
30
|
+
'Provider "opencode" is not authenticated.\n\nHint: Run "opencode auth" to configure provider credentials.'
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('keeps contextual non-wrapper error messages', () => {
|
|
35
|
+
const error = {
|
|
36
|
+
_tag: 'CollectionError',
|
|
37
|
+
message: 'Failed to load resource "npm:prettier": missing docs index',
|
|
38
|
+
hint: 'Try running "btca clear" and reload resources.',
|
|
39
|
+
cause: {
|
|
40
|
+
_tag: 'Panic',
|
|
41
|
+
message: 'match err handler threw',
|
|
42
|
+
cause: new Error('boom')
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
expect(getErrorTag(error)).toBe('CollectionError');
|
|
47
|
+
expect(getErrorMessage(error)).toBe(
|
|
48
|
+
'Failed to load resource "npm:prettier": missing docs index'
|
|
49
|
+
);
|
|
50
|
+
expect(getErrorHint(error)).toBe('Try running "btca clear" and reload resources.');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('handles cyclic cause chains without throwing', () => {
|
|
54
|
+
const cyclic = { _tag: 'Panic', message: 'match err handler threw' } as {
|
|
55
|
+
_tag: string;
|
|
56
|
+
message: string;
|
|
57
|
+
cause?: unknown;
|
|
58
|
+
};
|
|
59
|
+
cyclic.cause = cyclic;
|
|
60
|
+
|
|
61
|
+
expect(getErrorTag(cyclic)).toBe('Panic');
|
|
62
|
+
expect(getErrorMessage(cyclic)).toBe('match err handler threw');
|
|
63
|
+
expect(getErrorHint(cyclic)).toBeUndefined();
|
|
64
|
+
});
|
|
65
|
+
});
|
package/src/errors.ts
CHANGED
|
@@ -11,22 +11,115 @@ export type TaggedErrorLike = {
|
|
|
11
11
|
readonly hint?: string;
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
+
const MAX_CAUSE_DEPTH = 12;
|
|
15
|
+
const WRAPPER_TAGS = new Set(['Panic', 'UnhandledException']);
|
|
16
|
+
const isObjectRecord = (value: unknown): value is Record<string, unknown> =>
|
|
17
|
+
typeof value === 'object' && value !== null;
|
|
18
|
+
|
|
19
|
+
const readStringField = (value: unknown, field: '_tag' | 'message' | 'hint') => {
|
|
20
|
+
if (!isObjectRecord(value) || !(field in value)) return undefined;
|
|
21
|
+
const candidate = value[field];
|
|
22
|
+
return typeof candidate === 'string' && candidate.length > 0 ? candidate : undefined;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const readCauseField = (value: unknown) => {
|
|
26
|
+
if (!isObjectRecord(value) || !('cause' in value)) return undefined;
|
|
27
|
+
return value.cause;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const isWrapperMessage = (message?: string) =>
|
|
31
|
+
Boolean(
|
|
32
|
+
message &&
|
|
33
|
+
(message.startsWith('Unhandled exception:') ||
|
|
34
|
+
/handler threw$/u.test(message) ||
|
|
35
|
+
/callback threw$/u.test(message))
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const normalizeMessage = (message: string) => {
|
|
39
|
+
if (!message.startsWith('Unhandled exception:')) return message;
|
|
40
|
+
const stripped = message.slice('Unhandled exception:'.length).trim();
|
|
41
|
+
return stripped.length > 0 ? stripped : message;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const isWrapperEntry = (entry: unknown) => {
|
|
45
|
+
const tag = readStringField(entry, '_tag');
|
|
46
|
+
const message = readStringField(entry, 'message');
|
|
47
|
+
const hasCause = readCauseField(entry) !== undefined;
|
|
48
|
+
const isAgentWrapper =
|
|
49
|
+
tag === 'AgentError' && message === 'Failed to get response from AI' && hasCause;
|
|
50
|
+
return (
|
|
51
|
+
isAgentWrapper || (tag !== undefined && WRAPPER_TAGS.has(tag)) || isWrapperMessage(message)
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const getErrorChain = (error: unknown) => {
|
|
56
|
+
const chain: unknown[] = [];
|
|
57
|
+
const visited = new Set<unknown>();
|
|
58
|
+
let current: unknown = error;
|
|
59
|
+
let depth = 0;
|
|
60
|
+
|
|
61
|
+
while (current !== undefined && depth < MAX_CAUSE_DEPTH && !visited.has(current)) {
|
|
62
|
+
chain.push(current);
|
|
63
|
+
visited.add(current);
|
|
64
|
+
const cause = readCauseField(current);
|
|
65
|
+
if (cause === undefined) break;
|
|
66
|
+
current = cause;
|
|
67
|
+
depth += 1;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return chain;
|
|
71
|
+
};
|
|
72
|
+
|
|
14
73
|
export const getErrorTag = (error: unknown): string => {
|
|
15
|
-
|
|
74
|
+
const chain = getErrorChain(error);
|
|
75
|
+
|
|
76
|
+
for (const entry of chain) {
|
|
77
|
+
const tag = readStringField(entry, '_tag');
|
|
78
|
+
if (tag && !isWrapperEntry(entry)) return tag;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (const entry of chain) {
|
|
82
|
+
const tag = readStringField(entry, '_tag');
|
|
83
|
+
if (tag) return tag;
|
|
84
|
+
}
|
|
85
|
+
|
|
16
86
|
return 'UnknownError';
|
|
17
87
|
};
|
|
18
88
|
|
|
19
89
|
export const getErrorMessage = (error: unknown): string => {
|
|
20
|
-
|
|
21
|
-
|
|
90
|
+
const chain = getErrorChain(error);
|
|
91
|
+
|
|
92
|
+
for (const entry of chain) {
|
|
93
|
+
const message = readStringField(entry, 'message');
|
|
94
|
+
if (message && !isWrapperEntry(entry)) return message;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (const entry of chain) {
|
|
98
|
+
const message = readStringField(entry, 'message');
|
|
99
|
+
if (message && !isWrapperMessage(message)) return message;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const entry of chain) {
|
|
103
|
+
const message = readStringField(entry, 'message');
|
|
104
|
+
if (message) return normalizeMessage(message);
|
|
105
|
+
}
|
|
106
|
+
|
|
22
107
|
return String(error);
|
|
23
108
|
};
|
|
24
109
|
|
|
25
110
|
export const getErrorHint = (error: unknown): string | undefined => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
111
|
+
const chain = getErrorChain(error);
|
|
112
|
+
|
|
113
|
+
for (const entry of chain) {
|
|
114
|
+
const hint = readStringField(entry, 'hint');
|
|
115
|
+
if (hint && !isWrapperEntry(entry)) return hint;
|
|
29
116
|
}
|
|
117
|
+
|
|
118
|
+
for (const entry of chain) {
|
|
119
|
+
const hint = readStringField(entry, 'hint');
|
|
120
|
+
if (hint) return hint;
|
|
121
|
+
}
|
|
122
|
+
|
|
30
123
|
return undefined;
|
|
31
124
|
};
|
|
32
125
|
|
package/src/index.ts
CHANGED
|
@@ -12,12 +12,13 @@ import { getErrorMessage, getErrorTag, getErrorHint } from './errors.ts';
|
|
|
12
12
|
import { Metrics } from './metrics/index.ts';
|
|
13
13
|
import { ModelsDevPricing } from './pricing/models-dev.ts';
|
|
14
14
|
import { Resources } from './resources/service.ts';
|
|
15
|
-
import { GitResourceSchema, LocalResourceSchema } from './resources/schema.ts';
|
|
15
|
+
import { GitResourceSchema, LocalResourceSchema, NpmResourceSchema } from './resources/schema.ts';
|
|
16
16
|
import { StreamService } from './stream/service.ts';
|
|
17
17
|
import type { BtcaStreamMetaEvent } from './stream/types.ts';
|
|
18
18
|
import {
|
|
19
19
|
LIMITS,
|
|
20
20
|
normalizeGitHubUrl,
|
|
21
|
+
parseNpmReference,
|
|
21
22
|
validateGitUrl,
|
|
22
23
|
validateResourceReference
|
|
23
24
|
} from './validation/index.ts';
|
|
@@ -82,6 +83,8 @@ const ResourceReferenceField = z.string().superRefine((value, ctx) => {
|
|
|
82
83
|
});
|
|
83
84
|
|
|
84
85
|
const normalizeQuestionResourceReference = (reference: string): string => {
|
|
86
|
+
const npmReference = parseNpmReference(reference);
|
|
87
|
+
if (npmReference) return npmReference.normalizedReference;
|
|
85
88
|
const gitUrlResult = validateGitUrl(reference);
|
|
86
89
|
if (gitUrlResult.valid) return gitUrlResult.value;
|
|
87
90
|
return reference;
|
|
@@ -165,9 +168,18 @@ const AddLocalResourceRequestSchema = z.object({
|
|
|
165
168
|
specialNotes: LocalResourceSchema.shape.specialNotes
|
|
166
169
|
});
|
|
167
170
|
|
|
171
|
+
const AddNpmResourceRequestSchema = z.object({
|
|
172
|
+
type: z.literal('npm'),
|
|
173
|
+
name: NpmResourceSchema.shape.name,
|
|
174
|
+
package: NpmResourceSchema.shape.package,
|
|
175
|
+
version: NpmResourceSchema.shape.version,
|
|
176
|
+
specialNotes: NpmResourceSchema.shape.specialNotes
|
|
177
|
+
});
|
|
178
|
+
|
|
168
179
|
const AddResourceRequestSchema = z.discriminatedUnion('type', [
|
|
169
180
|
AddGitResourceRequestSchema,
|
|
170
|
-
AddLocalResourceRequestSchema
|
|
181
|
+
AddLocalResourceRequestSchema,
|
|
182
|
+
AddNpmResourceRequestSchema
|
|
171
183
|
]);
|
|
172
184
|
|
|
173
185
|
const RemoveResourceRequestSchema = z.object({
|
|
@@ -188,16 +200,16 @@ class RequestError extends Error {
|
|
|
188
200
|
|
|
189
201
|
const decodeJson = async <T>(req: Request, schema: z.ZodType<T>): Promise<T> => {
|
|
190
202
|
const bodyResult = await Result.tryPromise(() => req.json());
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
203
|
+
if (!Result.isOk(bodyResult)) {
|
|
204
|
+
throw new RequestError('Failed to parse request JSON', bodyResult.error);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const parsed = schema.safeParse(bodyResult.value);
|
|
208
|
+
if (!parsed.success) {
|
|
209
|
+
throw new RequestError('Invalid request body', parsed.error);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return parsed.data;
|
|
201
213
|
};
|
|
202
214
|
|
|
203
215
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -242,6 +254,9 @@ const createApp = (deps: {
|
|
|
242
254
|
tag === 'ConfigError' ||
|
|
243
255
|
tag === 'InvalidProviderError' ||
|
|
244
256
|
tag === 'InvalidModelError' ||
|
|
257
|
+
tag === 'ProviderNotAuthenticatedError' ||
|
|
258
|
+
tag === 'ProviderAuthTypeError' ||
|
|
259
|
+
tag === 'ProviderNotFoundError' ||
|
|
245
260
|
tag === 'ProviderNotConnectedError' ||
|
|
246
261
|
tag === 'ProviderOptionsError'
|
|
247
262
|
? 400
|
|
@@ -287,7 +302,8 @@ const createApp = (deps: {
|
|
|
287
302
|
searchPaths: r.searchPaths ?? null,
|
|
288
303
|
specialNotes: r.specialNotes ?? null
|
|
289
304
|
};
|
|
290
|
-
}
|
|
305
|
+
}
|
|
306
|
+
if (r.type === 'local') {
|
|
291
307
|
return {
|
|
292
308
|
name: r.name,
|
|
293
309
|
type: r.type,
|
|
@@ -295,6 +311,13 @@ const createApp = (deps: {
|
|
|
295
311
|
specialNotes: r.specialNotes ?? null
|
|
296
312
|
};
|
|
297
313
|
}
|
|
314
|
+
return {
|
|
315
|
+
name: r.name,
|
|
316
|
+
type: r.type,
|
|
317
|
+
package: r.package,
|
|
318
|
+
version: r.version ?? null,
|
|
319
|
+
specialNotes: r.specialNotes ?? null
|
|
320
|
+
};
|
|
298
321
|
})
|
|
299
322
|
});
|
|
300
323
|
})
|
|
@@ -435,7 +458,8 @@ const createApp = (deps: {
|
|
|
435
458
|
};
|
|
436
459
|
const added = await config.addResource(resource);
|
|
437
460
|
return c.json(added, 201);
|
|
438
|
-
}
|
|
461
|
+
}
|
|
462
|
+
if (decoded.type === 'local') {
|
|
439
463
|
const resource = {
|
|
440
464
|
type: 'local' as const,
|
|
441
465
|
name: decoded.name,
|
|
@@ -445,6 +469,15 @@ const createApp = (deps: {
|
|
|
445
469
|
const added = await config.addResource(resource);
|
|
446
470
|
return c.json(added, 201);
|
|
447
471
|
}
|
|
472
|
+
const resource = {
|
|
473
|
+
type: 'npm' as const,
|
|
474
|
+
name: decoded.name,
|
|
475
|
+
package: decoded.package,
|
|
476
|
+
...(decoded.version ? { version: decoded.version } : {}),
|
|
477
|
+
...(decoded.specialNotes ? { specialNotes: decoded.specialNotes } : {})
|
|
478
|
+
};
|
|
479
|
+
const added = await config.addResource(resource);
|
|
480
|
+
return c.json(added, 201);
|
|
448
481
|
})
|
|
449
482
|
|
|
450
483
|
// DELETE /config/resources - Remove a resource
|
package/src/providers/auth.ts
CHANGED
|
@@ -181,7 +181,7 @@ export namespace Auth {
|
|
|
181
181
|
return 'Run "btca connect -p minimax" and enter your API key. Get your API key at https://platform.minimax.io/user-center/basic-information.';
|
|
182
182
|
default:
|
|
183
183
|
return 'Run "btca connect" and configure credentials for this provider.';
|
|
184
|
-
|
|
184
|
+
}
|
|
185
185
|
};
|
|
186
186
|
|
|
187
187
|
/**
|
package/src/providers/minimax.ts
CHANGED
|
@@ -7,7 +7,7 @@ const readEnv = (key: string) => {
|
|
|
7
7
|
return value && value.trim().length > 0 ? value.trim() : undefined;
|
|
8
8
|
};
|
|
9
9
|
|
|
10
|
-
export const MINIMAX_MODELS = ['MiniMax-M2.1'] as const;
|
|
10
|
+
export const MINIMAX_MODELS = ['MiniMax-M2.1', 'MiniMax-M2.5'] as const;
|
|
11
11
|
|
|
12
12
|
export type MinimaxModel = (typeof MINIMAX_MODELS)[number];
|
|
13
13
|
|