@ikarmic/forge-core 5.0.0 → 5.1.1
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 +1 -1
- package/dist/agent/agent.d.ts +8 -0
- package/dist/agent/agent.js +148 -24
- package/dist/agent/background.d.ts +58 -0
- package/dist/agent/background.js +139 -0
- package/dist/agent/enhance-prompt.d.ts +3 -1
- package/dist/agent/enhance-prompt.js +6 -2
- package/dist/agent/goal.d.ts +54 -0
- package/dist/agent/goal.js +65 -0
- package/dist/agent/prompts.d.ts +25 -1
- package/dist/agent/prompts.js +61 -17
- package/dist/agents/agent-files.d.ts +43 -0
- package/dist/agents/agent-files.js +109 -0
- package/dist/checkpoints/store.js +5 -1
- package/dist/commands/custom-commands.d.ts +9 -4
- package/dist/commands/custom-commands.js +13 -2
- package/dist/config-file.d.ts +43 -0
- package/dist/config-file.js +249 -5
- package/dist/config-merge.d.ts +13 -0
- package/dist/config-merge.js +67 -0
- package/dist/config-schema.d.ts +340 -0
- package/dist/config-schema.js +126 -0
- package/dist/config.js +2 -1
- package/dist/context/compaction.js +4 -0
- package/dist/context/mask.js +1 -1
- package/dist/custom-providers.d.ts +95 -0
- package/dist/custom-providers.js +120 -0
- package/dist/git/auto-commit.d.ts +2 -2
- package/dist/git/auto-commit.js +5 -4
- package/dist/github/github-tools.d.ts +2 -2
- package/dist/hooks/runner.js +3 -2
- package/dist/import/claude-code.d.ts +39 -0
- package/dist/import/claude-code.js +254 -0
- package/dist/index.d.ts +34 -5
- package/dist/index.js +27 -4
- package/dist/journey/answers-block.d.ts +17 -0
- package/dist/journey/answers-block.js +15 -0
- package/dist/journey/index.d.ts +9 -0
- package/dist/journey/index.js +8 -0
- package/dist/journey/journey.d.ts +13 -0
- package/dist/journey/journey.js +40 -0
- package/dist/journey/offer.d.ts +25 -0
- package/dist/journey/offer.js +48 -0
- package/dist/journey/questions.d.ts +23 -0
- package/dist/journey/questions.js +49 -0
- package/dist/journey/stages.d.ts +13 -0
- package/dist/journey/stages.js +107 -0
- package/dist/journey/survey.d.ts +10 -0
- package/dist/journey/survey.js +203 -0
- package/dist/journey/trigger.d.ts +19 -0
- package/dist/journey/trigger.js +79 -0
- package/dist/journey/types.d.ts +72 -0
- package/dist/journey/types.js +1 -0
- package/dist/lsp/client.d.ts +5 -0
- package/dist/lsp/client.js +39 -3
- package/dist/mcp/index.d.ts +5 -1
- package/dist/mcp/index.js +3 -1
- package/dist/mcp/manager.d.ts +17 -3
- package/dist/mcp/manager.js +27 -2
- package/dist/mcp/marketplace.d.ts +2 -0
- package/dist/mcp/marketplace.js +10 -4
- package/dist/mcp/mcp-client.d.ts +12 -2
- package/dist/mcp/mcp-client.js +74 -17
- package/dist/mcp/mcp-config.d.ts +2 -0
- package/dist/mcp/mcp-config.js +1 -0
- package/dist/mcp/oauth.d.ts +55 -0
- package/dist/mcp/oauth.js +197 -0
- package/dist/mcp/resource-tools.d.ts +8 -0
- package/dist/mcp/resource-tools.js +56 -0
- package/dist/memory/session-digest.d.ts +12 -0
- package/dist/memory/session-digest.js +47 -0
- package/dist/memory/stale.js +2 -1
- package/dist/model-alias.d.ts +18 -2
- package/dist/model-alias.js +27 -3
- package/dist/model-catalog.js +0 -1
- package/dist/model-list.js +2 -1
- package/dist/model-overrides.d.ts +30 -0
- package/dist/model-overrides.js +48 -0
- package/dist/orchestrator/specialists-registry.js +2 -3
- package/dist/orchestrator/specialists.js +4 -4
- package/dist/policy/ca-certs.d.ts +8 -0
- package/dist/policy/ca-certs.js +30 -0
- package/dist/policy/proxy.d.ts +10 -3
- package/dist/policy/proxy.js +33 -6
- package/dist/pricing.js +4 -0
- package/dist/providers.d.ts +7 -0
- package/dist/providers.js +39 -5
- package/dist/semantic/embedder.d.ts +15 -0
- package/dist/semantic/embedder.js +25 -0
- package/dist/semantic/semantic-index.d.ts +10 -2
- package/dist/semantic/semantic-index.js +16 -7
- package/dist/sessions/session-store.js +2 -1
- package/dist/skills/builtin.d.ts +2 -0
- package/dist/skills/builtin.js +68 -0
- package/dist/skills/index.d.ts +2 -0
- package/dist/skills/index.js +2 -0
- package/dist/skills/loader.d.ts +10 -3
- package/dist/skills/loader.js +20 -7
- package/dist/skills/marketplace.js +2 -1
- package/dist/skills/remote.d.ts +14 -0
- package/dist/skills/remote.js +131 -0
- package/dist/skills/types.d.ts +10 -0
- package/dist/specs/generate.d.ts +18 -9
- package/dist/specs/generate.js +174 -14
- package/dist/specs/index.d.ts +2 -2
- package/dist/specs/index.js +1 -1
- package/dist/specs/store.d.ts +14 -2
- package/dist/specs/store.js +26 -1
- package/dist/specs/types.d.ts +11 -0
- package/dist/steering/extra-instructions.d.ts +8 -0
- package/dist/steering/extra-instructions.js +104 -0
- package/dist/steering/folder-instructions.d.ts +8 -0
- package/dist/steering/folder-instructions.js +28 -0
- package/dist/steering/select.js +2 -1
- package/dist/timed-fetch.d.ts +12 -0
- package/dist/timed-fetch.js +83 -0
- package/dist/tools/apply-patch.d.ts +52 -0
- package/dist/tools/apply-patch.js +197 -0
- package/dist/tools/ask-user.d.ts +27 -0
- package/dist/tools/ask-user.js +60 -0
- package/dist/tools/browser-tools.d.ts +6 -6
- package/dist/tools/browser-tools.js +3 -13
- package/dist/tools/codebase-search.d.ts +4 -2
- package/dist/tools/codebase-search.js +2 -2
- package/dist/tools/edit-file.js +5 -3
- package/dist/tools/fetch-url.d.ts +7 -0
- package/dist/tools/fetch-url.js +52 -19
- package/dist/tools/find-symbol.d.ts +2 -2
- package/dist/tools/ignore-rules.js +2 -1
- package/dist/tools/index.d.ts +5 -1
- package/dist/tools/index.js +10 -1
- package/dist/tools/memory.d.ts +6 -6
- package/dist/tools/notebook.d.ts +2 -2
- package/dist/tools/permission-rules.d.ts +32 -0
- package/dist/tools/permission-rules.js +182 -0
- package/dist/tools/read-file.d.ts +5 -0
- package/dist/tools/read-file.js +12 -2
- package/dist/tools/read-only.d.ts +1 -1
- package/dist/tools/read-only.js +2 -0
- package/dist/tools/run-command.d.ts +4 -2
- package/dist/tools/run-command.js +12 -8
- package/dist/tools/sandbox-loader.js +3 -13
- package/dist/tools/search-files.d.ts +8 -2
- package/dist/tools/search-files.js +43 -0
- package/dist/tools/shared.d.ts +5 -0
- package/dist/tools/shared.js +30 -2
- package/dist/tools/shell-env.d.ts +7 -0
- package/dist/tools/shell-env.js +9 -0
- package/dist/tools/sub-agent-wait.d.ts +29 -0
- package/dist/tools/sub-agent-wait.js +60 -0
- package/dist/tools/sub-agent.d.ts +50 -4
- package/dist/tools/sub-agent.js +213 -125
- package/dist/tools/text-encoding.d.ts +10 -0
- package/dist/tools/text-encoding.js +40 -0
- package/dist/tools/todo.d.ts +36 -0
- package/dist/tools/todo.js +42 -0
- package/dist/tools/write-file.js +15 -9
- package/dist/tools/write-tools.d.ts +12 -0
- package/dist/tools/write-tools.js +18 -0
- package/dist/types.d.ts +98 -0
- package/dist/worktree/worktree.d.ts +3 -0
- package/dist/worktree/worktree.js +22 -0
- package/package.json +1 -1
package/dist/mcp/mcp-client.js
CHANGED
|
@@ -24,12 +24,14 @@
|
|
|
24
24
|
import { readFileSync } from 'node:fs';
|
|
25
25
|
import { dirname, join } from 'node:path';
|
|
26
26
|
import { fileURLToPath } from 'node:url';
|
|
27
|
+
import { UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js';
|
|
27
28
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
28
29
|
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
29
30
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
30
31
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
31
32
|
import { filterMcpServers, getOrgPolicy } from '../policy/policy.js';
|
|
32
33
|
import { jsonSchemaToZod } from './json-schema-to-zod.js';
|
|
34
|
+
import { ForgeOAuthProvider, McpAuthRequiredError, defaultMcpAuthDir, readSavedAuth } from './oauth.js';
|
|
33
35
|
/**
|
|
34
36
|
* Self-identifying info we send to every MCP server during the `initialize`
|
|
35
37
|
* handshake. Version is read from our own package.json so it stays in sync
|
|
@@ -53,7 +55,7 @@ function readPackageVersion() {
|
|
|
53
55
|
}
|
|
54
56
|
}
|
|
55
57
|
/** Connect to every server in a parsed MCP config. Failed connections are skipped with a warning. */
|
|
56
|
-
export async function connectAll(config) {
|
|
58
|
+
export async function connectAll(config, options = {}) {
|
|
57
59
|
const { allowed, blocked } = filterMcpServers(config.mcpServers, getOrgPolicy()?.policy);
|
|
58
60
|
for (const name of blocked) {
|
|
59
61
|
process.stderr.write(`forge: MCP server "${name}" is blocked by organization policy
|
|
@@ -65,10 +67,14 @@ export async function connectAll(config) {
|
|
|
65
67
|
if (spec.disabled)
|
|
66
68
|
continue;
|
|
67
69
|
try {
|
|
68
|
-
const server = await connectOne(name, spec);
|
|
70
|
+
const server = await connectOne(name, spec, options);
|
|
69
71
|
results.push(server);
|
|
70
72
|
}
|
|
71
73
|
catch (err) {
|
|
74
|
+
if (err instanceof McpAuthRequiredError) {
|
|
75
|
+
process.stderr.write(`forge: MCP server "${name}" needs you to sign in. Run: forge mcp login ${name}\n`);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
72
78
|
// Skip failed servers so one bad entry doesn't take down the whole
|
|
73
79
|
// session; surface the error so the caller can log it.
|
|
74
80
|
process.stderr.write(`forge: MCP server "${name}" failed to connect: ${err.message}\n`);
|
|
@@ -76,21 +82,68 @@ export async function connectAll(config) {
|
|
|
76
82
|
}
|
|
77
83
|
return results;
|
|
78
84
|
}
|
|
85
|
+
export { McpAuthRequiredError } from './oauth.js';
|
|
86
|
+
/** A 401 from a server we connected to without credentials. */
|
|
87
|
+
function isUnauthorized(err) {
|
|
88
|
+
const e = err;
|
|
89
|
+
return err instanceof UnauthorizedError || e?.code === 401 || (typeof e?.message === 'string' && /\b401\b/.test(e.message));
|
|
90
|
+
}
|
|
79
91
|
/** Connect to a single server and pull its tools. */
|
|
80
|
-
export async function connectOne(name, spec) {
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
92
|
+
export async function connectOne(name, spec, options = {}) {
|
|
93
|
+
const timeout = spec.timeout ? { timeout: spec.timeout } : undefined;
|
|
94
|
+
let client = new Client(CLIENT_INFO, { capabilities: {} });
|
|
95
|
+
const oauth = options.oauth;
|
|
96
|
+
// A static Authorization header is the user's own choice of credentials: leave OAuth out of it.
|
|
97
|
+
const hasStaticAuth = Object.keys(spec.headers ?? {}).some((h) => h.toLowerCase() === 'authorization');
|
|
98
|
+
const wantsOAuth = Boolean(oauth && spec.url && !spec.command && !hasStaticAuth);
|
|
99
|
+
if (!wantsOAuth) {
|
|
100
|
+
await client.connect(buildTransport(spec), timeout);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
const url = spec.url;
|
|
104
|
+
const haveTokens = Boolean((await readSavedAuth(oauth.storeDir ?? defaultMcpAuthDir(), url))?.tokens);
|
|
105
|
+
let provider;
|
|
106
|
+
if (haveTokens || oauth.interactive) {
|
|
107
|
+
provider = await ForgeOAuthProvider.create(name, url, oauth);
|
|
108
|
+
}
|
|
109
|
+
let transport = buildTransport(spec, provider);
|
|
110
|
+
try {
|
|
111
|
+
await client.connect(transport, timeout);
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
if (!provider) {
|
|
115
|
+
// Tried without credentials and the server refused: it needs a sign-in the user has not done yet.
|
|
116
|
+
if (isUnauthorized(err))
|
|
117
|
+
throw new McpAuthRequiredError(name);
|
|
118
|
+
throw err;
|
|
119
|
+
}
|
|
120
|
+
if (!(err instanceof UnauthorizedError))
|
|
121
|
+
throw err;
|
|
122
|
+
// The SDK started the sign-in (the browser is open); wait for the user, then connect again with the tokens.
|
|
123
|
+
try {
|
|
124
|
+
const code = await provider.waitForCode();
|
|
125
|
+
await transport.finishAuth(code);
|
|
126
|
+
}
|
|
127
|
+
catch (signInErr) {
|
|
128
|
+
provider.cancel();
|
|
129
|
+
throw signInErr;
|
|
130
|
+
}
|
|
131
|
+
await client.close().catch(() => undefined);
|
|
132
|
+
client = new Client(CLIENT_INFO, { capabilities: {} });
|
|
133
|
+
transport = buildTransport(spec, provider);
|
|
134
|
+
await client.connect(transport, timeout);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const { tools: mcpTools } = await client.listTools(undefined, spec.timeout ? { timeout: spec.timeout } : undefined);
|
|
138
|
+
const tools = mcpTools.map((t) => mcpToolToForgeTool(name, t, client, spec.timeout));
|
|
139
|
+
return { name, client, tools, instructions: client.getInstructions()?.trim() || undefined };
|
|
87
140
|
}
|
|
88
141
|
/**
|
|
89
142
|
* Pick the right SDK transport for the server spec. The transport type is
|
|
90
143
|
* either explicit (`type: 'sse'` etc.) or inferred from the shape of the
|
|
91
144
|
* spec: `command` present ⇒ stdio, `url` present ⇒ SSE.
|
|
92
145
|
*/
|
|
93
|
-
function buildTransport(spec) {
|
|
146
|
+
function buildTransport(spec, authProvider) {
|
|
94
147
|
const explicit = spec.type;
|
|
95
148
|
const isStdio = explicit === 'stdio' || (!explicit && Boolean(spec.command));
|
|
96
149
|
const isSse = explicit === 'sse' || (!explicit && Boolean(spec.url) && !explicit);
|
|
@@ -118,10 +171,17 @@ function buildTransport(spec) {
|
|
|
118
171
|
const headers = spec.headers ?? {};
|
|
119
172
|
const requestInit = headers ? { headers } : {};
|
|
120
173
|
if (explicit === 'streamable-http') {
|
|
121
|
-
return new StreamableHTTPClientTransport(new URL(spec.url), { requestInit });
|
|
174
|
+
return new StreamableHTTPClientTransport(new URL(spec.url), { requestInit, ...(authProvider ? { authProvider } : {}) });
|
|
122
175
|
}
|
|
123
176
|
// Default for HTTP-transport specs: SSE (preserves prior behavior).
|
|
124
|
-
return new SSEClientTransport(new URL(spec.url), { requestInit });
|
|
177
|
+
return new SSEClientTransport(new URL(spec.url), { requestInit, ...(authProvider ? { authProvider } : {}) });
|
|
178
|
+
}
|
|
179
|
+
/** The instructions servers sent with `initialize`, as one prompt block (empty when none sent any). */
|
|
180
|
+
export function mcpInstructionsBlock(servers) {
|
|
181
|
+
return servers
|
|
182
|
+
.filter((s) => s.instructions)
|
|
183
|
+
.map((s) => `<mcp-server-instructions server="${s.name.replace(/"/g, '')}">\n${s.instructions}\n</mcp-server-instructions>`)
|
|
184
|
+
.join('\n\n');
|
|
125
185
|
}
|
|
126
186
|
/** Close all clients and their underlying transports. */
|
|
127
187
|
export async function closeAll(servers) {
|
|
@@ -133,7 +193,7 @@ export async function closeAll(servers) {
|
|
|
133
193
|
* namespace of the built-in tools and tells the model which server a tool
|
|
134
194
|
* came from at a glance.
|
|
135
195
|
*/
|
|
136
|
-
function mcpToolToForgeTool(serverName, mcpTool, client) {
|
|
196
|
+
function mcpToolToForgeTool(serverName, mcpTool, client, timeout) {
|
|
137
197
|
const fullName = `mcp__${serverName}__${mcpTool.name}`;
|
|
138
198
|
const description = [
|
|
139
199
|
mcpTool.description ?? '',
|
|
@@ -150,10 +210,7 @@ function mcpToolToForgeTool(serverName, mcpTool, client) {
|
|
|
150
210
|
// destructive so the user gets a confirmation prompt unless --auto.
|
|
151
211
|
destructive: true,
|
|
152
212
|
execute: async (args) => {
|
|
153
|
-
const result = await client.callTool({
|
|
154
|
-
name: mcpTool.name,
|
|
155
|
-
arguments: args,
|
|
156
|
-
});
|
|
213
|
+
const result = await client.callTool({ name: mcpTool.name, arguments: args }, undefined, timeout ? { timeout } : undefined);
|
|
157
214
|
return mcpResultToValue(result);
|
|
158
215
|
},
|
|
159
216
|
};
|
package/dist/mcp/mcp-config.d.ts
CHANGED
|
@@ -45,6 +45,8 @@ export interface McpServerSpec {
|
|
|
45
45
|
headers?: Record<string, string>;
|
|
46
46
|
/** Keep the entry but do not connect it. */
|
|
47
47
|
disabled?: boolean;
|
|
48
|
+
/** Milliseconds to wait for a connection or a tool call before giving up (default: the SDK's 60 s). */
|
|
49
|
+
timeout?: number;
|
|
48
50
|
}
|
|
49
51
|
/**
|
|
50
52
|
* Load and validate a `forge.mcp.json` file. Throws if the file is missing,
|
package/dist/mcp/mcp-config.js
CHANGED
|
@@ -9,6 +9,7 @@ const serverSchema = z
|
|
|
9
9
|
url: z.string().optional(),
|
|
10
10
|
headers: z.record(z.string(), z.string()).optional(),
|
|
11
11
|
disabled: z.boolean().optional(),
|
|
12
|
+
timeout: z.number().int().positive().max(3_600_000).optional(),
|
|
12
13
|
})
|
|
13
14
|
.refine((s) => Boolean(s.command) || Boolean(s.url), {
|
|
14
15
|
message: 'Each MCP server must have either `command` (stdio) or `url` (sse/streamable-http).',
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js';
|
|
2
|
+
import type { OAuthClientInformationMixed, OAuthClientMetadata, OAuthTokens } from '@modelcontextprotocol/sdk/shared/auth.js';
|
|
3
|
+
export interface McpOAuthOptions {
|
|
4
|
+
/** Defaults to `~/.forge/mcp-auth`. */
|
|
5
|
+
storeDir?: string;
|
|
6
|
+
/** Show the authorization page to the user (their browser). Throw to refuse. */
|
|
7
|
+
openUrl: (url: string) => void | Promise<void>;
|
|
8
|
+
/** True: may open the browser and wait for the user. False: only use tokens already saved. */
|
|
9
|
+
interactive?: boolean;
|
|
10
|
+
/** How long to wait for the user to finish in the browser. Default five minutes. */
|
|
11
|
+
signInTimeoutMs?: number;
|
|
12
|
+
}
|
|
13
|
+
/** The server needs the user to sign in, and this connection was not allowed to ask. */
|
|
14
|
+
export declare class McpAuthRequiredError extends Error {
|
|
15
|
+
readonly server: string;
|
|
16
|
+
constructor(server: string);
|
|
17
|
+
}
|
|
18
|
+
interface SavedAuth {
|
|
19
|
+
url: string;
|
|
20
|
+
port?: number;
|
|
21
|
+
clientInformation?: OAuthClientInformationMixed;
|
|
22
|
+
tokens?: OAuthTokens;
|
|
23
|
+
}
|
|
24
|
+
export declare function defaultMcpAuthDir(): string;
|
|
25
|
+
export declare function readSavedAuth(dir: string, serverUrl: string): Promise<SavedAuth | undefined>;
|
|
26
|
+
export declare function forgetSavedAuth(dir: string, serverUrl: string): Promise<void>;
|
|
27
|
+
export declare class ForgeOAuthProvider implements OAuthClientProvider {
|
|
28
|
+
readonly serverName: string;
|
|
29
|
+
private readonly serverUrl;
|
|
30
|
+
private readonly dir;
|
|
31
|
+
private readonly port;
|
|
32
|
+
private readonly options;
|
|
33
|
+
private verifier;
|
|
34
|
+
private expectedState;
|
|
35
|
+
private receiver;
|
|
36
|
+
private constructor();
|
|
37
|
+
static create(serverName: string, serverUrl: string, options: McpOAuthOptions): Promise<ForgeOAuthProvider>;
|
|
38
|
+
get redirectUrl(): string;
|
|
39
|
+
get clientMetadata(): OAuthClientMetadata;
|
|
40
|
+
state(): string;
|
|
41
|
+
private saved;
|
|
42
|
+
clientInformation(): Promise<OAuthClientInformationMixed | undefined>;
|
|
43
|
+
saveClientInformation(info: OAuthClientInformationMixed): Promise<void>;
|
|
44
|
+
tokens(): Promise<OAuthTokens | undefined>;
|
|
45
|
+
saveTokens(tokens: OAuthTokens): Promise<void>;
|
|
46
|
+
saveCodeVerifier(v: string): void;
|
|
47
|
+
codeVerifier(): string;
|
|
48
|
+
invalidateCredentials(scope: 'all' | 'client' | 'tokens' | 'verifier' | 'discovery'): Promise<void>;
|
|
49
|
+
/** Called by the SDK once it has the authorization URL. Without `interactive` this is where the connection stops. */
|
|
50
|
+
redirectToAuthorization(url: URL): Promise<void>;
|
|
51
|
+
/** After the SDK reported the sign-in as started: the code the browser brings back. */
|
|
52
|
+
waitForCode(): Promise<string>;
|
|
53
|
+
cancel(): void;
|
|
54
|
+
}
|
|
55
|
+
export {};
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth sign-in for MCP servers behind it (authorization code with PKCE, dynamic client registration).
|
|
3
|
+
* The SDK does the protocol; this file supplies what it leaves to the application: where tokens are kept,
|
|
4
|
+
* where the browser returns to (a one-shot page on 127.0.0.1), and a state value that must come back unchanged.
|
|
5
|
+
*
|
|
6
|
+
* Tokens live in `~/.forge/mcp-auth/<server>.json`, readable by the user only, one file per server URL.
|
|
7
|
+
* Signing in is always the user's act: a server that needs it is reported as such, and the browser opens only
|
|
8
|
+
* when `interactive` is set. A URL that is not http or https is never opened.
|
|
9
|
+
*/
|
|
10
|
+
import { randomBytes, createHash } from 'node:crypto';
|
|
11
|
+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
12
|
+
import http from 'node:http';
|
|
13
|
+
import { homedir } from 'node:os';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
/** The server needs the user to sign in, and this connection was not allowed to ask. */
|
|
16
|
+
export class McpAuthRequiredError extends Error {
|
|
17
|
+
server;
|
|
18
|
+
constructor(server) {
|
|
19
|
+
super(`MCP server "${server}" needs you to sign in`);
|
|
20
|
+
this.server = server;
|
|
21
|
+
this.name = 'McpAuthRequiredError';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function defaultMcpAuthDir() {
|
|
25
|
+
return path.join(homedir(), '.forge', 'mcp-auth');
|
|
26
|
+
}
|
|
27
|
+
function fileFor(dir, serverUrl) {
|
|
28
|
+
return path.join(dir, `${createHash('sha256').update(serverUrl).digest('hex').slice(0, 32)}.json`);
|
|
29
|
+
}
|
|
30
|
+
export async function readSavedAuth(dir, serverUrl) {
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(await readFile(fileFor(dir, serverUrl), 'utf8'));
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function writeSavedAuth(dir, auth) {
|
|
39
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
40
|
+
await writeFile(fileFor(dir, auth.url), JSON.stringify(auth, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
41
|
+
}
|
|
42
|
+
export async function forgetSavedAuth(dir, serverUrl) {
|
|
43
|
+
await rm(fileFor(dir, serverUrl), { force: true });
|
|
44
|
+
}
|
|
45
|
+
/** A port on 127.0.0.1: the one used last time if it is free (so a registered redirect URL keeps working), else any. */
|
|
46
|
+
async function freePort(preferred) {
|
|
47
|
+
const tryListen = (port) => new Promise((resolve) => {
|
|
48
|
+
const s = http.createServer();
|
|
49
|
+
s.once('error', () => resolve(undefined));
|
|
50
|
+
s.listen(port, '127.0.0.1', () => {
|
|
51
|
+
const got = s.address().port;
|
|
52
|
+
s.close(() => resolve(got));
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
return (preferred ? await tryListen(preferred) : undefined) ?? (await tryListen(0)) ?? 0;
|
|
56
|
+
}
|
|
57
|
+
/** A one-shot page the browser returns to. Resolves with the code; a wrong `state` is ignored, an `error` rejects. */
|
|
58
|
+
async function receiveCode(port, expectedState, timeoutMs) {
|
|
59
|
+
let resolveCode;
|
|
60
|
+
let rejectCode;
|
|
61
|
+
const code = new Promise((resolve, reject) => {
|
|
62
|
+
resolveCode = resolve;
|
|
63
|
+
rejectCode = reject;
|
|
64
|
+
});
|
|
65
|
+
const page = (title) => `<!doctype html><meta charset="utf-8"><title>${title}</title><body style="font-family:sans-serif;margin:3em"><h2>${title}</h2><p>You can close this tab and go back to your editor.</p>`;
|
|
66
|
+
const server = http.createServer((req, res) => {
|
|
67
|
+
const url = new URL(req.url ?? '/', `http://127.0.0.1:${port}`);
|
|
68
|
+
if (url.pathname !== '/callback') {
|
|
69
|
+
res.writeHead(404).end();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (url.searchParams.get('state') !== expectedState) {
|
|
73
|
+
res.writeHead(400, { 'content-type': 'text/html' }).end(page('This sign-in link is not for this session'));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const error = url.searchParams.get('error');
|
|
77
|
+
const got = url.searchParams.get('code');
|
|
78
|
+
if (error || !got) {
|
|
79
|
+
res.writeHead(400, { 'content-type': 'text/html' }).end(page('Sign-in was not completed'));
|
|
80
|
+
rejectCode(new Error(`sign-in failed: ${error ?? 'no code returned'}`));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
res.writeHead(200, { 'content-type': 'text/html' }).end(page('Signed in to Forge'));
|
|
84
|
+
resolveCode(got);
|
|
85
|
+
});
|
|
86
|
+
await new Promise((resolve, reject) => {
|
|
87
|
+
server.once('error', reject);
|
|
88
|
+
server.listen(port, '127.0.0.1', resolve);
|
|
89
|
+
});
|
|
90
|
+
const timer = setTimeout(() => rejectCode(new Error('sign-in timed out')), timeoutMs);
|
|
91
|
+
const close = () => {
|
|
92
|
+
clearTimeout(timer);
|
|
93
|
+
server.closeAllConnections();
|
|
94
|
+
server.close();
|
|
95
|
+
};
|
|
96
|
+
code.then(close, close);
|
|
97
|
+
return { code, close };
|
|
98
|
+
}
|
|
99
|
+
export class ForgeOAuthProvider {
|
|
100
|
+
serverName;
|
|
101
|
+
serverUrl;
|
|
102
|
+
dir;
|
|
103
|
+
port;
|
|
104
|
+
options;
|
|
105
|
+
verifier = '';
|
|
106
|
+
expectedState = '';
|
|
107
|
+
receiver;
|
|
108
|
+
constructor(serverName, serverUrl, dir, port, options) {
|
|
109
|
+
this.serverName = serverName;
|
|
110
|
+
this.serverUrl = serverUrl;
|
|
111
|
+
this.dir = dir;
|
|
112
|
+
this.port = port;
|
|
113
|
+
this.options = options;
|
|
114
|
+
}
|
|
115
|
+
static async create(serverName, serverUrl, options) {
|
|
116
|
+
const dir = options.storeDir ?? defaultMcpAuthDir();
|
|
117
|
+
const saved = await readSavedAuth(dir, serverUrl);
|
|
118
|
+
const port = await freePort(saved?.port);
|
|
119
|
+
await writeSavedAuth(dir, { ...(saved ?? { url: serverUrl }), url: serverUrl, port });
|
|
120
|
+
return new ForgeOAuthProvider(serverName, serverUrl, dir, port, options);
|
|
121
|
+
}
|
|
122
|
+
get redirectUrl() {
|
|
123
|
+
return `http://127.0.0.1:${this.port}/callback`;
|
|
124
|
+
}
|
|
125
|
+
get clientMetadata() {
|
|
126
|
+
return {
|
|
127
|
+
client_name: 'Ikarmic Forge',
|
|
128
|
+
redirect_uris: [this.redirectUrl],
|
|
129
|
+
grant_types: ['authorization_code', 'refresh_token'],
|
|
130
|
+
response_types: ['code'],
|
|
131
|
+
token_endpoint_auth_method: 'none',
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
state() {
|
|
135
|
+
this.expectedState = randomBytes(16).toString('hex');
|
|
136
|
+
return this.expectedState;
|
|
137
|
+
}
|
|
138
|
+
async saved() {
|
|
139
|
+
return (await readSavedAuth(this.dir, this.serverUrl)) ?? { url: this.serverUrl };
|
|
140
|
+
}
|
|
141
|
+
async clientInformation() {
|
|
142
|
+
const info = (await this.saved()).clientInformation;
|
|
143
|
+
// A registration made for another redirect URL cannot be used here: register again.
|
|
144
|
+
if (info?.redirect_uris && !info.redirect_uris.includes(this.redirectUrl))
|
|
145
|
+
return undefined;
|
|
146
|
+
return info;
|
|
147
|
+
}
|
|
148
|
+
async saveClientInformation(info) {
|
|
149
|
+
await writeSavedAuth(this.dir, { ...(await this.saved()), clientInformation: info });
|
|
150
|
+
}
|
|
151
|
+
async tokens() {
|
|
152
|
+
return (await this.saved()).tokens;
|
|
153
|
+
}
|
|
154
|
+
async saveTokens(tokens) {
|
|
155
|
+
await writeSavedAuth(this.dir, { ...(await this.saved()), tokens });
|
|
156
|
+
}
|
|
157
|
+
saveCodeVerifier(v) {
|
|
158
|
+
this.verifier = v;
|
|
159
|
+
}
|
|
160
|
+
codeVerifier() {
|
|
161
|
+
return this.verifier;
|
|
162
|
+
}
|
|
163
|
+
async invalidateCredentials(scope) {
|
|
164
|
+
const s = await this.saved();
|
|
165
|
+
if (scope === 'all' || scope === 'tokens')
|
|
166
|
+
delete s.tokens;
|
|
167
|
+
if (scope === 'all' || scope === 'client')
|
|
168
|
+
delete s.clientInformation;
|
|
169
|
+
await writeSavedAuth(this.dir, s);
|
|
170
|
+
}
|
|
171
|
+
/** Called by the SDK once it has the authorization URL. Without `interactive` this is where the connection stops. */
|
|
172
|
+
async redirectToAuthorization(url) {
|
|
173
|
+
if (!this.options.interactive)
|
|
174
|
+
throw new McpAuthRequiredError(this.serverName);
|
|
175
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
|
|
176
|
+
throw new Error(`MCP server "${this.serverName}" gave a sign-in address that is not a web page (${url.protocol}); refused`);
|
|
177
|
+
}
|
|
178
|
+
this.receiver?.close();
|
|
179
|
+
this.receiver = await receiveCode(this.port, this.expectedState, this.options.signInTimeoutMs ?? 5 * 60_000);
|
|
180
|
+
await this.options.openUrl(url.href);
|
|
181
|
+
}
|
|
182
|
+
/** After the SDK reported the sign-in as started: the code the browser brings back. */
|
|
183
|
+
async waitForCode() {
|
|
184
|
+
if (!this.receiver)
|
|
185
|
+
throw new Error('no sign-in is waiting');
|
|
186
|
+
try {
|
|
187
|
+
return await this.receiver.code;
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
this.receiver = undefined;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
cancel() {
|
|
194
|
+
this.receiver?.close();
|
|
195
|
+
this.receiver = undefined;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ConnectedMcpServer } from './mcp-client.js';
|
|
2
|
+
import type { ToolDefinition } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* `mcp_list_resources` and `mcp_read_resource`: let the model see and read what connected MCP servers publish
|
|
5
|
+
* (notes, files, schemas). Both only read, so neither asks for approval. `servers` is a function because
|
|
6
|
+
* servers connect, fail and reconnect while a chat is open.
|
|
7
|
+
*/
|
|
8
|
+
export declare function createMcpResourceTools(servers: () => readonly ConnectedMcpServer[]): ToolDefinition[];
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const MAX_RESOURCE_CHARS = 100_000;
|
|
3
|
+
/**
|
|
4
|
+
* `mcp_list_resources` and `mcp_read_resource`: let the model see and read what connected MCP servers publish
|
|
5
|
+
* (notes, files, schemas). Both only read, so neither asks for approval. `servers` is a function because
|
|
6
|
+
* servers connect, fail and reconnect while a chat is open.
|
|
7
|
+
*/
|
|
8
|
+
export function createMcpResourceTools(servers) {
|
|
9
|
+
const listParams = z.object({ server: z.string().optional().describe('Only this MCP server. Omit for every server.') });
|
|
10
|
+
const readParams = z.object({
|
|
11
|
+
server: z.string().describe('MCP server name, as shown by mcp_list_resources.'),
|
|
12
|
+
uri: z.string().describe('Resource URI, as shown by mcp_list_resources.'),
|
|
13
|
+
});
|
|
14
|
+
const list = {
|
|
15
|
+
name: 'mcp_list_resources',
|
|
16
|
+
description: 'List the resources (documents, data, files) that connected MCP servers publish. Returns each resource server, uri, name and description.',
|
|
17
|
+
parameters: listParams,
|
|
18
|
+
execute: async (args) => {
|
|
19
|
+
const out = [];
|
|
20
|
+
for (const s of servers()) {
|
|
21
|
+
if (args.server && s.name !== args.server)
|
|
22
|
+
continue;
|
|
23
|
+
try {
|
|
24
|
+
const { resources } = await s.client.listResources();
|
|
25
|
+
for (const r of resources) {
|
|
26
|
+
out.push({ server: s.name, uri: r.uri, name: r.name, ...(r.description ? { description: r.description } : {}), ...(r.mimeType ? { mimeType: r.mimeType } : {}) });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// A server without resources support (or one that is down) just contributes nothing.
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return { resources: out };
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
const read = {
|
|
37
|
+
name: 'mcp_read_resource',
|
|
38
|
+
description: 'Read one resource from a connected MCP server by its server name and uri (see mcp_list_resources).',
|
|
39
|
+
parameters: readParams,
|
|
40
|
+
execute: async (args) => {
|
|
41
|
+
const server = servers().find((s) => s.name === args.server);
|
|
42
|
+
if (!server)
|
|
43
|
+
throw new Error(`No MCP server "${args.server}" is connected.`);
|
|
44
|
+
const result = await server.client.readResource({ uri: args.uri });
|
|
45
|
+
const text = result.contents
|
|
46
|
+
.map((c) => ('text' in c && typeof c.text === 'string' ? c.text : `[binary content${c.mimeType ? `, ${c.mimeType}` : ''}]`))
|
|
47
|
+
.join('\n');
|
|
48
|
+
return {
|
|
49
|
+
server: args.server,
|
|
50
|
+
uri: args.uri,
|
|
51
|
+
text: text.length > MAX_RESOURCE_CHARS ? `${text.slice(0, MAX_RESOURCE_CHARS)}\n…(truncated)` : text,
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
return [list, read];
|
|
56
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { LanguageModel } from 'ai';
|
|
2
|
+
import type { ModelConfig } from '../types.js';
|
|
3
|
+
export interface DigestItem {
|
|
4
|
+
role: 'user' | 'assistant';
|
|
5
|
+
text: string;
|
|
6
|
+
}
|
|
7
|
+
/** A conversation with fewer than two user messages is a question, not work worth remembering. */
|
|
8
|
+
export declare function shouldDigest(items: readonly DigestItem[]): boolean;
|
|
9
|
+
/** The model's reply as a digest: think block, fences and quotes removed; undefined when nothing usable is left (RB-1). */
|
|
10
|
+
export declare function cleanDigest(reply: string): string | undefined;
|
|
11
|
+
/** Summarise a finished chat for project memory with `model` (usually the cheap model); never throws. */
|
|
12
|
+
export declare function generateSessionDigest(items: readonly DigestItem[], model: ModelConfig | undefined, modelFactory?: (cfg: ModelConfig) => LanguageModel): Promise<string | undefined>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { generateText } from 'ai';
|
|
2
|
+
import { stripThinkTags } from '../agent/think-tags.js';
|
|
3
|
+
import { createLanguageModel } from '../providers.js';
|
|
4
|
+
const MAX_DIGEST_CHARS = 600;
|
|
5
|
+
const MAX_INPUT_CHARS = 12_000;
|
|
6
|
+
const INSTRUCTION = 'Below is a conversation between a user and a coding agent. Write what a later session needs to know: what was changed or decided, which files were involved, and anything left undone. ' +
|
|
7
|
+
'Two to five short bullet lines, each starting with "- ". Do not copy code. Reply with the bullets only, no preface, no code fences.';
|
|
8
|
+
/** A conversation with fewer than two user messages is a question, not work worth remembering. */
|
|
9
|
+
export function shouldDigest(items) {
|
|
10
|
+
return items.filter((i) => i.role === 'user').length >= 2;
|
|
11
|
+
}
|
|
12
|
+
/** The model's reply as a digest: think block, fences and quotes removed; undefined when nothing usable is left (RB-1). */
|
|
13
|
+
export function cleanDigest(reply) {
|
|
14
|
+
const text = stripThinkTags(reply)
|
|
15
|
+
.trim()
|
|
16
|
+
.replace(/^```[a-z]*\s*\n?/i, '')
|
|
17
|
+
.replace(/\n?```$/, '')
|
|
18
|
+
.trim()
|
|
19
|
+
.replace(/^["'“”](.*)["'“”]$/s, '$1')
|
|
20
|
+
.trim();
|
|
21
|
+
if (!text)
|
|
22
|
+
return undefined;
|
|
23
|
+
return text.length > MAX_DIGEST_CHARS ? `${text.slice(0, MAX_DIGEST_CHARS - 1).trimEnd()}…` : text;
|
|
24
|
+
}
|
|
25
|
+
/** The end of the conversation, which is where its outcome is, within the input budget. */
|
|
26
|
+
function transcript(items) {
|
|
27
|
+
const lines = items.map((i) => `${i.role === 'user' ? 'User' : 'Agent'}: ${i.text.trim()}`);
|
|
28
|
+
let out = '';
|
|
29
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
30
|
+
if (out.length + lines[i].length > MAX_INPUT_CHARS)
|
|
31
|
+
break;
|
|
32
|
+
out = `${lines[i]}\n\n${out}`;
|
|
33
|
+
}
|
|
34
|
+
return out.trim();
|
|
35
|
+
}
|
|
36
|
+
/** Summarise a finished chat for project memory with `model` (usually the cheap model); never throws. */
|
|
37
|
+
export async function generateSessionDigest(items, model, modelFactory = createLanguageModel) {
|
|
38
|
+
if (!model || !shouldDigest(items))
|
|
39
|
+
return undefined;
|
|
40
|
+
try {
|
|
41
|
+
const result = await generateText({ model: modelFactory(model), prompt: `${INSTRUCTION}\n\nConversation:\n${transcript(items)}` });
|
|
42
|
+
return cleanDigest(result.text);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
package/dist/memory/stale.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { stat } from 'node:fs/promises';
|
|
7
7
|
import path from 'node:path';
|
|
8
|
+
import { isOutsideRel } from '../tools/shared.js';
|
|
8
9
|
/** Key a section in the stale map. */
|
|
9
10
|
export function staleKey(s) {
|
|
10
11
|
return `${s.source}#${s.heading}`;
|
|
@@ -23,7 +24,7 @@ export async function checkStale(sections, root, opts = {}) {
|
|
|
23
24
|
const file = hash === -1 ? ref : ref.slice(0, hash);
|
|
24
25
|
const symbol = hash === -1 ? '' : ref.slice(hash + 1);
|
|
25
26
|
const rel = path.relative(root, path.resolve(root, file));
|
|
26
|
-
if (path.isAbsolute(file) ||
|
|
27
|
+
if (path.isAbsolute(file) || isOutsideRel(rel)) {
|
|
27
28
|
reasons.push(`${ref} is outside the project`);
|
|
28
29
|
continue;
|
|
29
30
|
}
|
package/dist/model-alias.d.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* User-defined model aliases from `~/.forge/config.json`:
|
|
3
|
+
* "aliases": { "fast": "anthropic:claude-haiku-4-5", "big": "opus" }
|
|
4
|
+
* A value is `provider:model` or a bare model id (current provider), and may
|
|
5
|
+
* name another alias.
|
|
6
|
+
*/
|
|
7
|
+
import { type CustomProviderConfig, type ProviderEndpoint } from './custom-providers.js';
|
|
8
|
+
import type { ModelConfig, ProviderName } from './types.js';
|
|
2
9
|
export type ModelAliases = Record<string, string>;
|
|
3
10
|
/** Follow alias links from `name`. Names that are not aliases come back unchanged. */
|
|
4
11
|
export declare function expandModelAlias(name: string, aliases: ModelAliases | undefined): string;
|
|
@@ -7,9 +14,18 @@ export declare function splitModelRef(ref: string): {
|
|
|
7
14
|
provider?: ProviderName;
|
|
8
15
|
model: string;
|
|
9
16
|
};
|
|
10
|
-
|
|
17
|
+
/**
|
|
18
|
+
* `provider:model` for the eleven built-in providers, and `id:model` for a preset or `customProviders` id
|
|
19
|
+
* (`groq:llama-3.3-70b-versatile`), which resolves to an openai-compatible model with its endpoint. Ollama and
|
|
20
|
+
* LM Studio use `name:tag` model ids (`mistral:7b`), so for them only a prefix the user defined counts.
|
|
21
|
+
*/
|
|
22
|
+
export declare function resolveModelAlias(name: string, aliases: ModelAliases | undefined, defaultProvider: ProviderName, custom?: Record<string, CustomProviderConfig>): {
|
|
11
23
|
provider: ProviderName;
|
|
12
24
|
model: string;
|
|
25
|
+
endpoint?: ProviderEndpoint;
|
|
13
26
|
};
|
|
27
|
+
export type ResolvedModelRef = ReturnType<typeof resolveModelAlias>;
|
|
28
|
+
/** A resolved model reference laid over a base model: the base's other settings stay, its endpoint never leaks into a different provider. */
|
|
29
|
+
export declare function applyResolved(base: ModelConfig, resolved: ResolvedModelRef): ModelConfig;
|
|
14
30
|
/** Human-readable alias list for `forge config`; '' when there are none. */
|
|
15
31
|
export declare function formatAliasTable(aliases: ModelAliases | undefined): string;
|
package/dist/model-alias.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* A value is `provider:model` or a bare model id (current provider), and may
|
|
5
5
|
* name another alias.
|
|
6
6
|
*/
|
|
7
|
+
import { PROVIDER_PRESETS, apiModelId, registeredCustomProviders, resolveProviderEndpoint } from './custom-providers.js';
|
|
7
8
|
import { MODEL_CATALOG } from './model-catalog.js';
|
|
8
9
|
const MAX_HOPS = 5;
|
|
9
10
|
const PROVIDERS = new Set(Object.keys(MODEL_CATALOG));
|
|
@@ -34,9 +35,32 @@ export function splitModelRef(ref) {
|
|
|
34
35
|
}
|
|
35
36
|
return { model: ref };
|
|
36
37
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
/**
|
|
39
|
+
* `provider:model` for the eleven built-in providers, and `id:model` for a preset or `customProviders` id
|
|
40
|
+
* (`groq:llama-3.3-70b-versatile`), which resolves to an openai-compatible model with its endpoint. Ollama and
|
|
41
|
+
* LM Studio use `name:tag` model ids (`mistral:7b`), so for them only a prefix the user defined counts.
|
|
42
|
+
*/
|
|
43
|
+
export function resolveModelAlias(name, aliases, defaultProvider, custom) {
|
|
44
|
+
const providers = custom ?? registeredCustomProviders();
|
|
45
|
+
const expanded = expandModelAlias(name, aliases);
|
|
46
|
+
const { provider, model } = splitModelRef(expanded);
|
|
47
|
+
if (provider)
|
|
48
|
+
return { provider, model };
|
|
49
|
+
const idx = expanded.indexOf(':');
|
|
50
|
+
if (idx > 0) {
|
|
51
|
+
const prefix = expanded.slice(0, idx);
|
|
52
|
+
const local = defaultProvider === 'ollama' || defaultProvider === 'lmstudio';
|
|
53
|
+
const known = Object.prototype.hasOwnProperty.call(providers ?? {}, prefix) || (!local && Object.prototype.hasOwnProperty.call(PROVIDER_PRESETS, prefix));
|
|
54
|
+
const endpoint = known ? resolveProviderEndpoint(prefix, providers) : undefined;
|
|
55
|
+
if (endpoint)
|
|
56
|
+
return { provider: 'openai-compatible', model: apiModelId(prefix, expanded.slice(idx + 1), providers), endpoint };
|
|
57
|
+
}
|
|
58
|
+
return { provider: defaultProvider, model };
|
|
59
|
+
}
|
|
60
|
+
/** A resolved model reference laid over a base model: the base's other settings stay, its endpoint never leaks into a different provider. */
|
|
61
|
+
export function applyResolved(base, resolved) {
|
|
62
|
+
const { endpoint: _stale, ...rest } = base;
|
|
63
|
+
return { ...rest, ...resolved };
|
|
40
64
|
}
|
|
41
65
|
/** Human-readable alias list for `forge config`; '' when there are none. */
|
|
42
66
|
export function formatAliasTable(aliases) {
|
package/dist/model-catalog.js
CHANGED
|
@@ -38,7 +38,6 @@ export const MODEL_CATALOG = {
|
|
|
38
38
|
{ id: 'MiniMax-M3', label: 'MiniMax M3' },
|
|
39
39
|
{ id: 'MiniMax-M2.7', label: 'MiniMax M2.7' },
|
|
40
40
|
{ id: 'MiniMax-M2.7-highspeed', label: 'MiniMax M2.7 highspeed' },
|
|
41
|
-
{ id: 'MiniMax-M2', label: 'MiniMax M2' },
|
|
42
41
|
],
|
|
43
42
|
bedrock: [
|
|
44
43
|
{ id: 'us.anthropic.claude-sonnet-4-5-20250929-v1:0', label: 'Claude Sonnet 4.5 (Bedrock, US)' },
|