@ikarmic/forge-core 5.0.0 → 5.1.0
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/dist/agent/agent.d.ts +8 -0
- package/dist/agent/agent.js +123 -22
- 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/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 +32 -4
- package/dist/index.js +24 -3
- 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/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 +32 -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 +7 -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 +210 -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 +73 -0
- package/dist/worktree/worktree.d.ts +3 -0
- package/dist/worktree/worktree.js +22 -0
- package/package.json +1 -1
package/dist/policy/proxy.js
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* well-known global symbol, so installing it from the npm `undici` package also
|
|
6
6
|
* routes Node's bundled fetch.
|
|
7
7
|
*/
|
|
8
|
+
import https from 'node:https';
|
|
9
|
+
import { rootCertificates } from 'node:tls';
|
|
8
10
|
/** Proxy settings from the standard environment variables (either case). */
|
|
9
11
|
export function proxyFromEnv(env = process.env) {
|
|
10
12
|
const pick = (a, b) => env[a] || env[b] || undefined;
|
|
@@ -17,15 +19,40 @@ export function hasProxy(p) {
|
|
|
17
19
|
return Boolean(p.https || p.http);
|
|
18
20
|
}
|
|
19
21
|
let installed = false;
|
|
22
|
+
let installedFetchFn;
|
|
20
23
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
24
|
+
* A fetch that goes through the dispatcher installed by {@link installProxy} (the proxy and the extra CA roots), or
|
|
25
|
+
* undefined when none is installed. Needed because a host can replace the global `fetch` with one that ignores the
|
|
26
|
+
* global dispatcher: VS Code does in its extension host. Calls that must honour the proxy and the roots pass this one
|
|
27
|
+
* explicitly instead of relying on the global.
|
|
23
28
|
*/
|
|
24
|
-
export
|
|
25
|
-
|
|
29
|
+
export function installedFetch() {
|
|
30
|
+
return installedFetchFn;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Route all `fetch` traffic through the proxy in `settings` (default: the environment) and trust `extraCa`
|
|
34
|
+
* (PEM text) on top of the built-in roots. Idempotent. Returns whether anything was installed.
|
|
35
|
+
*/
|
|
36
|
+
export async function installProxy(settings = proxyFromEnv(), extraCa = []) {
|
|
37
|
+
if (installed || (!hasProxy(settings) && extraCa.length === 0))
|
|
26
38
|
return false;
|
|
27
|
-
const { EnvHttpProxyAgent, setGlobalDispatcher } = await import('undici');
|
|
28
|
-
|
|
39
|
+
const { Agent, EnvHttpProxyAgent, fetch: undiciFetch, setGlobalDispatcher } = await import('undici');
|
|
40
|
+
const useDispatcher = (dispatcher) => {
|
|
41
|
+
setGlobalDispatcher(dispatcher);
|
|
42
|
+
installedFetchFn = ((input, init) => undiciFetch(input, { ...init, dispatcher }));
|
|
43
|
+
};
|
|
44
|
+
const tls = extraCa.length > 0 ? { ca: [...rootCertificates, ...extraCa] } : undefined;
|
|
45
|
+
// Not everything goes through undici: VS Code replaces the extension host's fetch with one built on node:https, which
|
|
46
|
+
// reads the global agent. Trust the roots there too.
|
|
47
|
+
if (tls)
|
|
48
|
+
https.globalAgent.options.ca = tls.ca;
|
|
49
|
+
if (!hasProxy(settings)) {
|
|
50
|
+
useDispatcher(new Agent({ connect: tls }));
|
|
51
|
+
installed = true;
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
useDispatcher(new EnvHttpProxyAgent({
|
|
55
|
+
...(tls ? { requestTls: tls, proxyTls: tls } : {}),
|
|
29
56
|
// Plain-http targets are forwarded as absolute-URI requests (the classic proxy protocol);
|
|
30
57
|
// https targets always use CONNECT tunnels.
|
|
31
58
|
proxyTunnel: false,
|
package/dist/pricing.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { overrideCost } from './model-overrides.js';
|
|
1
2
|
/**
|
|
2
3
|
* Pricing table. Keys are matched with case-insensitive substring matching
|
|
3
4
|
* against the model id (after stripping any "models/" / "claude-" /
|
|
@@ -99,6 +100,9 @@ export function lookupPricing(model, override) {
|
|
|
99
100
|
const cache = typeof model === 'string' ? {} : cacheMultipliersFor(model);
|
|
100
101
|
if (override)
|
|
101
102
|
return { ...cache, ...override, known: true };
|
|
103
|
+
const configured = overrideCost(model);
|
|
104
|
+
if (configured)
|
|
105
|
+
return { ...cache, ...configured, known: true };
|
|
102
106
|
const id = (typeof model === 'string' ? model : model.model).toLowerCase();
|
|
103
107
|
// Strip only the OpenRouter-style 'models/' prefix so 'models/gemini-1.5-pro'
|
|
104
108
|
// resolves the same way as 'gemini-1.5-pro'. We do NOT strip provider prefixes
|
package/dist/providers.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { LanguageModel } from 'ai';
|
|
2
|
+
import type { ProviderEndpoint } from './custom-providers.js';
|
|
2
3
|
import type { ModelConfig, ProviderName } from './types.js';
|
|
3
4
|
/**
|
|
4
5
|
* Build a Vercel AI SDK `LanguageModel` from a `ModelConfig`.
|
|
@@ -14,6 +15,12 @@ import type { ModelConfig, ProviderName } from './types.js';
|
|
|
14
15
|
* dependency footprint small (no separate `ollama-ai-provider` or
|
|
15
16
|
* `openrouter-ai-provider` packages).
|
|
16
17
|
*/
|
|
18
|
+
/**
|
|
19
|
+
* A model on a named provider: the OpenAI chat protocol (default) or the Anthropic message protocol, at the
|
|
20
|
+
* endpoint's URL with its key and headers. `timeoutMs` and `chunkTimeoutMs` wrap the HTTP calls.
|
|
21
|
+
* `fetchImpl` exists for tests.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createEndpointModel(endpoint: ProviderEndpoint, model: string, fetchImpl?: typeof fetch): LanguageModel;
|
|
17
24
|
export declare function createLanguageModel(config: ModelConfig): LanguageModel;
|
|
18
25
|
/**
|
|
19
26
|
* Resolve a model from the environment when no config is supplied. Throws a
|
package/dist/providers.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { anthropic } from '@ai-sdk/anthropic';
|
|
2
|
-
import { google } from '@ai-sdk/google';
|
|
2
|
+
import { createGoogleGenerativeAI, google } from '@ai-sdk/google';
|
|
3
|
+
import { createAnthropic } from '@ai-sdk/anthropic';
|
|
3
4
|
import { createOpenAI, openai } from '@ai-sdk/openai';
|
|
4
5
|
import { defaultModelFor, hasProviderKey } from './config.js';
|
|
6
|
+
import { installedFetch } from './policy/proxy.js';
|
|
7
|
+
import { withTimeouts } from './timed-fetch.js';
|
|
5
8
|
import { lazyLanguageModel } from './lazy-model.js';
|
|
6
9
|
/**
|
|
7
10
|
* Default Ollama base URL. Ollama serves an OpenAI-compatible API at
|
|
@@ -33,25 +36,51 @@ const MINIMAX_BASE_URL = 'https://api.minimax.io/v1';
|
|
|
33
36
|
* dependency footprint small (no separate `ollama-ai-provider` or
|
|
34
37
|
* `openrouter-ai-provider` packages).
|
|
35
38
|
*/
|
|
39
|
+
/**
|
|
40
|
+
* A model on a named provider: the OpenAI chat protocol (default) or the Anthropic message protocol, at the
|
|
41
|
+
* endpoint's URL with its key and headers. `timeoutMs` and `chunkTimeoutMs` wrap the HTTP calls.
|
|
42
|
+
* `fetchImpl` exists for tests.
|
|
43
|
+
*/
|
|
44
|
+
export function createEndpointModel(endpoint, model, fetchImpl = installedFetch() ?? fetch) {
|
|
45
|
+
const timed = withTimeouts(fetchImpl, { ...(endpoint.timeoutMs ? { timeoutMs: endpoint.timeoutMs } : {}), ...(endpoint.chunkTimeoutMs ? { chunkTimeoutMs: endpoint.chunkTimeoutMs } : {}) });
|
|
46
|
+
const common = {
|
|
47
|
+
baseURL: endpoint.baseUrl,
|
|
48
|
+
...(endpoint.headers ? { headers: endpoint.headers } : {}),
|
|
49
|
+
fetch: timed,
|
|
50
|
+
};
|
|
51
|
+
if (endpoint.api === 'anthropic')
|
|
52
|
+
return createAnthropic({ ...common, apiKey: endpoint.apiKey ?? '' })(model);
|
|
53
|
+
return createOpenAI({ ...common, apiKey: endpoint.apiKey ?? 'none' }).chat(model);
|
|
54
|
+
}
|
|
55
|
+
/** `{ fetch }` when a proxy or extra CA roots are installed (see installedFetch), else nothing: the SDK then uses the global one. */
|
|
56
|
+
function viaInstalled() {
|
|
57
|
+
const f = installedFetch();
|
|
58
|
+
return f ? { fetch: f } : {};
|
|
59
|
+
}
|
|
36
60
|
export function createLanguageModel(config) {
|
|
61
|
+
if (config.endpoint)
|
|
62
|
+
return createEndpointModel(config.endpoint, config.model);
|
|
63
|
+
const net = viaInstalled();
|
|
37
64
|
switch (config.provider) {
|
|
38
65
|
case 'openai':
|
|
39
|
-
return openai(config.model);
|
|
66
|
+
return net.fetch ? createOpenAI(net)(config.model) : openai(config.model);
|
|
40
67
|
case 'anthropic':
|
|
41
|
-
return anthropic(config.model);
|
|
68
|
+
return net.fetch ? createAnthropic(net)(config.model) : anthropic(config.model);
|
|
42
69
|
case 'google':
|
|
43
|
-
return google(config.model);
|
|
70
|
+
return net.fetch ? createGoogleGenerativeAI(net)(config.model) : google(config.model);
|
|
44
71
|
case 'ollama':
|
|
45
72
|
return createOpenAI({
|
|
46
73
|
baseURL: process.env.OLLAMA_BASE_URL ?? DEFAULT_OLLAMA_BASE_URL,
|
|
47
74
|
// Ollama doesn't require an API key: pass a placeholder so the
|
|
48
75
|
// OpenAI SDK's auth header doesn't trip on undefined.
|
|
49
76
|
apiKey: process.env.OLLAMA_API_KEY ?? 'ollama',
|
|
77
|
+
...net,
|
|
50
78
|
}).chat(config.model);
|
|
51
79
|
case 'openrouter':
|
|
52
80
|
return createOpenAI({
|
|
53
81
|
baseURL: OPENROUTER_BASE_URL,
|
|
54
82
|
apiKey: process.env.OPENROUTER_API_KEY ?? '',
|
|
83
|
+
...net,
|
|
55
84
|
headers: {
|
|
56
85
|
// OpenRouter recommends an app-name header so they can attribute
|
|
57
86
|
// traffic; helps if you ever need to reach support.
|
|
@@ -64,18 +93,20 @@ export function createLanguageModel(config) {
|
|
|
64
93
|
return createOpenAI({
|
|
65
94
|
baseURL: process.env.MINIMAX_BASE_URL ?? MINIMAX_BASE_URL,
|
|
66
95
|
apiKey: process.env.MINIMAX_API_KEY ?? '',
|
|
96
|
+
...net,
|
|
67
97
|
}).chat(config.model);
|
|
68
98
|
case 'lmstudio':
|
|
69
99
|
return createOpenAI({
|
|
70
100
|
baseURL: process.env.LMSTUDIO_BASE_URL ?? DEFAULT_LMSTUDIO_BASE_URL,
|
|
71
101
|
apiKey: process.env.LMSTUDIO_API_KEY ?? 'lm-studio',
|
|
102
|
+
...net,
|
|
72
103
|
}).chat(config.model);
|
|
73
104
|
case 'openai-compatible': {
|
|
74
105
|
const baseURL = process.env.OPENAI_COMPATIBLE_BASE_URL;
|
|
75
106
|
if (!baseURL) {
|
|
76
107
|
throw new Error('Provider "openai-compatible" needs OPENAI_COMPATIBLE_BASE_URL (e.g. http://localhost:8000/v1).');
|
|
77
108
|
}
|
|
78
|
-
return createOpenAI({ baseURL, apiKey: process.env.OPENAI_COMPATIBLE_API_KEY ?? 'none' }).chat(config.model);
|
|
109
|
+
return createOpenAI({ baseURL, apiKey: process.env.OPENAI_COMPATIBLE_API_KEY ?? 'none', ...net }).chat(config.model);
|
|
79
110
|
}
|
|
80
111
|
// The three cloud-account SDKs are heavy (~1.2 s to import together), so they load on first use.
|
|
81
112
|
case 'bedrock':
|
|
@@ -83,6 +114,7 @@ export function createLanguageModel(config) {
|
|
|
83
114
|
const { createAmazonBedrock } = await import('@ai-sdk/amazon-bedrock');
|
|
84
115
|
return createAmazonBedrock({
|
|
85
116
|
region: process.env.AWS_REGION ?? 'us-east-1',
|
|
117
|
+
...net,
|
|
86
118
|
...(process.env.AWS_BEARER_TOKEN_BEDROCK ? { apiKey: process.env.AWS_BEARER_TOKEN_BEDROCK } : {}),
|
|
87
119
|
...(process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY
|
|
88
120
|
? {
|
|
@@ -99,12 +131,14 @@ export function createLanguageModel(config) {
|
|
|
99
131
|
return createVertex({
|
|
100
132
|
...(process.env.GOOGLE_VERTEX_PROJECT ? { project: process.env.GOOGLE_VERTEX_PROJECT } : {}),
|
|
101
133
|
location: process.env.GOOGLE_VERTEX_LOCATION ?? 'us-central1',
|
|
134
|
+
...net,
|
|
102
135
|
})(config.model);
|
|
103
136
|
});
|
|
104
137
|
case 'azure':
|
|
105
138
|
return lazyLanguageModel({ provider: 'azure', modelId: config.model }, async () => {
|
|
106
139
|
const { createAzure } = await import('@ai-sdk/azure');
|
|
107
140
|
return createAzure({
|
|
141
|
+
...net,
|
|
108
142
|
...(process.env.AZURE_API_KEY ? { apiKey: process.env.AZURE_API_KEY } : {}),
|
|
109
143
|
...(process.env.AZURE_RESOURCE_NAME ? { resourceName: process.env.AZURE_RESOURCE_NAME } : {}),
|
|
110
144
|
...(process.env.AZURE_OPENAI_BASE_URL ? { baseURL: process.env.AZURE_OPENAI_BASE_URL } : {}),
|
|
@@ -26,6 +26,21 @@ export interface OpenAiCompatibleEmbedderOptions {
|
|
|
26
26
|
fetchImpl?: typeof fetch;
|
|
27
27
|
}
|
|
28
28
|
export declare function openAiCompatibleEmbedder(options: OpenAiCompatibleEmbedderOptions): Embedder;
|
|
29
|
+
export interface EmbedderSpec {
|
|
30
|
+
kind: 'hashing' | 'openai' | 'ollama' | 'custom';
|
|
31
|
+
model?: string;
|
|
32
|
+
/** For `custom`: an OpenAI-compatible `/embeddings` endpoint, up to and including `/v1`. */
|
|
33
|
+
baseUrl?: string;
|
|
34
|
+
/** For `custom`: the name of the environment variable holding the key (never the key itself). */
|
|
35
|
+
apiKeyEnv?: string;
|
|
36
|
+
batchSize?: number;
|
|
37
|
+
fetchImpl?: typeof fetch;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* An embedder from explicit settings. A choice that lacks what it needs (no key, no endpoint) gives the offline
|
|
41
|
+
* hashing embedder rather than an embedder that fails every search.
|
|
42
|
+
*/
|
|
43
|
+
export declare function buildEmbedder(spec: EmbedderSpec, env?: NodeJS.ProcessEnv): Embedder;
|
|
29
44
|
/**
|
|
30
45
|
* `FORGE_EMBED_PROVIDER` = `hashing` (default) | `openai` | `ollama`.
|
|
31
46
|
* `FORGE_EMBED_MODEL` overrides the model. Falls back to hashing when the
|
|
@@ -85,6 +85,31 @@ export function openAiCompatibleEmbedder(options) {
|
|
|
85
85
|
},
|
|
86
86
|
};
|
|
87
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* An embedder from explicit settings. A choice that lacks what it needs (no key, no endpoint) gives the offline
|
|
90
|
+
* hashing embedder rather than an embedder that fails every search.
|
|
91
|
+
*/
|
|
92
|
+
export function buildEmbedder(spec, env = process.env) {
|
|
93
|
+
const common = { ...(spec.batchSize ? { batchSize: spec.batchSize } : {}), ...(spec.fetchImpl ? { fetchImpl: spec.fetchImpl } : {}) };
|
|
94
|
+
switch (spec.kind) {
|
|
95
|
+
case 'openai':
|
|
96
|
+
if (!env.OPENAI_API_KEY)
|
|
97
|
+
return hashingEmbedder();
|
|
98
|
+
return openAiCompatibleEmbedder({ baseUrl: env.OPENAI_BASE_URL ?? 'https://api.openai.com/v1', model: spec.model || 'text-embedding-3-small', apiKey: env.OPENAI_API_KEY, ...common });
|
|
99
|
+
case 'ollama': {
|
|
100
|
+
const host = (env.OLLAMA_BASE_URL ?? 'http://localhost:11434').replace(/\/+$/, '').replace(/\/v1$/, '');
|
|
101
|
+
return openAiCompatibleEmbedder({ baseUrl: `${host}/v1`, model: spec.model || 'nomic-embed-text', ...common });
|
|
102
|
+
}
|
|
103
|
+
case 'custom': {
|
|
104
|
+
if (!spec.baseUrl || !spec.model)
|
|
105
|
+
return hashingEmbedder();
|
|
106
|
+
const apiKey = spec.apiKeyEnv ? env[spec.apiKeyEnv] : undefined;
|
|
107
|
+
return openAiCompatibleEmbedder({ baseUrl: spec.baseUrl, model: spec.model, ...(apiKey ? { apiKey } : {}), ...common });
|
|
108
|
+
}
|
|
109
|
+
default:
|
|
110
|
+
return hashingEmbedder();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
88
113
|
/**
|
|
89
114
|
* `FORGE_EMBED_PROVIDER` = `hashing` (default) | `openai` | `ollama`.
|
|
90
115
|
* `FORGE_EMBED_MODEL` overrides the model. Falls back to hashing when the
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { type Embedder } from './embedder.js';
|
|
2
2
|
import { type SemanticManifest } from './vector-store.js';
|
|
3
|
+
export interface SemanticOptions {
|
|
4
|
+
/** File extensions to index, with the dot. Default: the common source and Markdown ones. */
|
|
5
|
+
extensions?: readonly string[];
|
|
6
|
+
/** Hits scoring below this (cosine similarity) are left out. Default: none cut. */
|
|
7
|
+
minScore?: number;
|
|
8
|
+
}
|
|
9
|
+
/** `"ts, .py,md"` as `['.ts', '.py', '.md']`; undefined (use the defaults) when nothing usable is left. */
|
|
10
|
+
export declare function parseExtensions(text: string | undefined): string[] | undefined;
|
|
3
11
|
export interface SemanticHit {
|
|
4
12
|
path: string;
|
|
5
13
|
startLine: number;
|
|
@@ -8,5 +16,5 @@ export interface SemanticHit {
|
|
|
8
16
|
snippet: string;
|
|
9
17
|
}
|
|
10
18
|
/** Bring `.forge/index/embeddings.json` up to date: re-embed only changed files, drop deleted ones. */
|
|
11
|
-
export declare function ensureSemanticFresh(cwd: string, embedder: Embedder): Promise<SemanticManifest>;
|
|
12
|
-
export declare function searchSemantic(cwd: string, embedder: Embedder, query: string, k?: number): Promise<SemanticHit[]>;
|
|
19
|
+
export declare function ensureSemanticFresh(cwd: string, embedder: Embedder, options?: SemanticOptions): Promise<SemanticManifest>;
|
|
20
|
+
export declare function searchSemantic(cwd: string, embedder: Embedder, query: string, k?: number, options?: SemanticOptions): Promise<SemanticHit[]>;
|
|
@@ -7,9 +7,17 @@ import { chunkSource } from './chunker.js';
|
|
|
7
7
|
import { cosine } from './embedder.js';
|
|
8
8
|
import { decodeVector, encodeVector, loadSemanticManifest, saveSemanticManifest } from './vector-store.js';
|
|
9
9
|
const EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.py', '.go', '.rs', '.java', '.md']);
|
|
10
|
+
/** `"ts, .py,md"` as `['.ts', '.py', '.md']`; undefined (use the defaults) when nothing usable is left. */
|
|
11
|
+
export function parseExtensions(text) {
|
|
12
|
+
const list = (text ?? '')
|
|
13
|
+
.split(',')
|
|
14
|
+
.map((e) => e.trim().toLowerCase().replace(/^\.?/, '.'))
|
|
15
|
+
.filter((e) => /^\.[a-z0-9]{1,12}$/.test(e));
|
|
16
|
+
return list.length ? [...new Set(list)] : undefined;
|
|
17
|
+
}
|
|
10
18
|
const MAX_FILE_BYTES = 512 * 1024;
|
|
11
19
|
const SNIPPET_LINES = 40;
|
|
12
|
-
async function collect(cwd) {
|
|
20
|
+
async function collect(cwd, extensions) {
|
|
13
21
|
const out = [];
|
|
14
22
|
const rules = IgnoreRules.forRoot(cwd);
|
|
15
23
|
async function walk(dir) {
|
|
@@ -29,7 +37,7 @@ async function collect(cwd) {
|
|
|
29
37
|
if (entry.isDirectory()) {
|
|
30
38
|
await walk(abs);
|
|
31
39
|
}
|
|
32
|
-
else if (entry.isFile() &&
|
|
40
|
+
else if (entry.isFile() && extensions.has(path.extname(entry.name).toLowerCase())) {
|
|
33
41
|
const s = await stat(abs);
|
|
34
42
|
if (s.size <= MAX_FILE_BYTES) {
|
|
35
43
|
out.push({ rel: path.relative(cwd, abs).split(path.sep).join('/'), abs, ext: path.extname(entry.name), mtimeMs: s.mtimeMs });
|
|
@@ -41,9 +49,9 @@ async function collect(cwd) {
|
|
|
41
49
|
return out;
|
|
42
50
|
}
|
|
43
51
|
/** Bring `.forge/index/embeddings.json` up to date: re-embed only changed files, drop deleted ones. */
|
|
44
|
-
export async function ensureSemanticFresh(cwd, embedder) {
|
|
52
|
+
export async function ensureSemanticFresh(cwd, embedder, options = {}) {
|
|
45
53
|
const manifest = await loadSemanticManifest(cwd, embedder.id);
|
|
46
|
-
const files = await collect(cwd);
|
|
54
|
+
const files = await collect(cwd, options.extensions ? new Set(options.extensions) : EXTENSIONS);
|
|
47
55
|
const seen = new Set(files.map((f) => f.rel));
|
|
48
56
|
let changed = false;
|
|
49
57
|
for (const f of files) {
|
|
@@ -68,8 +76,8 @@ export async function ensureSemanticFresh(cwd, embedder) {
|
|
|
68
76
|
await saveSemanticManifest(cwd, manifest);
|
|
69
77
|
return manifest;
|
|
70
78
|
}
|
|
71
|
-
export async function searchSemantic(cwd, embedder, query, k = 8) {
|
|
72
|
-
const manifest = await ensureSemanticFresh(cwd, embedder);
|
|
79
|
+
export async function searchSemantic(cwd, embedder, query, k = 8, options = {}) {
|
|
80
|
+
const manifest = await ensureSemanticFresh(cwd, embedder, options);
|
|
73
81
|
const [q] = await embedder.embed([query]);
|
|
74
82
|
if (!q)
|
|
75
83
|
return [];
|
|
@@ -80,7 +88,8 @@ export async function searchSemantic(cwd, embedder, query, k = 8) {
|
|
|
80
88
|
}
|
|
81
89
|
scored.sort((a, b) => b.score - a.score);
|
|
82
90
|
const hits = [];
|
|
83
|
-
|
|
91
|
+
const floor = options.minScore;
|
|
92
|
+
for (const top of scored.filter((c) => floor === undefined || c.score >= floor).slice(0, k)) {
|
|
84
93
|
let text;
|
|
85
94
|
try {
|
|
86
95
|
const lines = (await readFile(path.join(cwd, top.rel), 'utf8')).split(/\r?\n/);
|
|
@@ -155,7 +155,8 @@ function extractPreview(messages) {
|
|
|
155
155
|
if (!firstUser)
|
|
156
156
|
return undefined;
|
|
157
157
|
const rawText = typeof firstUser.content === 'string' ? firstUser.content : extractText(firstUser.content);
|
|
158
|
-
|
|
158
|
+
// The `<context ...>` blocks an @mention appends are attachments, not the user's words.
|
|
159
|
+
const text = rawText === undefined ? undefined : stripSteering(rawText).replace(/\n\n<context [^>]*>[\s\S]*?<\/context>/g, '');
|
|
159
160
|
if (!text)
|
|
160
161
|
return undefined;
|
|
161
162
|
const oneLine = text.replace(/\s+/g, ' ').trim();
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { USER_ONLY_CONFIG_KEYS } from '../config-merge.js';
|
|
2
|
+
/** One line per key of `~/.forge/config.json`. Typed against the file's keys: a new key does not compile until it has a line. */
|
|
3
|
+
const KEY_HELP = {
|
|
4
|
+
provider: 'default provider id',
|
|
5
|
+
model: 'default model id, alias or provider:model',
|
|
6
|
+
completionModel: 'model for inline completion',
|
|
7
|
+
compactionModel: 'model that writes the summary when a long chat is condensed',
|
|
8
|
+
disabledProviders: 'providers hidden from the model picker',
|
|
9
|
+
reasoningEffort: 'none, minimal, low, medium, high or xhigh',
|
|
10
|
+
autoApproveTools: 'tools that run without asking',
|
|
11
|
+
autoApproveCommands: 'commands you chose "Always allow"; written by Forge, do not hand-edit',
|
|
12
|
+
permission: 'ordered rules per tool kind (read, edit, bash, webfetch, websearch, task, external_directory), each allow, ask or deny; the last match wins',
|
|
13
|
+
customProviders: 'providers beyond the built-in ones: id to { name, baseUrl, apiKeyEnv or apiKey, headers, api, timeoutMs, models }',
|
|
14
|
+
modelOverrides: 'context and output limits and prices per model',
|
|
15
|
+
modelFilters: 'per provider whitelist and blacklist of model ids',
|
|
16
|
+
subagentModel: 'model sub-agents use when none is named',
|
|
17
|
+
instructions: 'extra instruction files, globs and http(s) URLs added to the system prompt',
|
|
18
|
+
skillPaths: 'extra folders, or http(s) URLs of a skill file or a skill index ({ "skills": [{ "url": "..." }] }), to read skills from',
|
|
19
|
+
caCertificates: 'PEM files of extra certificate authorities to trust; needs a reload',
|
|
20
|
+
aliases: 'short names for models',
|
|
21
|
+
modeModels: 'model per chat mode',
|
|
22
|
+
sessionRetentionDays: 'delete saved chats older than this many days',
|
|
23
|
+
display: 'blockDefault (collapsed or expanded), detail (compact or full), showReasoning (true or false), and blocks: per kind of block, reasoning (hidden, collapsed or expanded) and terminal, edit, mcp (collapsed or expanded). The kinds are exactly reasoning, terminal, edit and mcp; use no other name',
|
|
24
|
+
};
|
|
25
|
+
function configBody() {
|
|
26
|
+
const userOnly = USER_ONLY_CONFIG_KEYS.join(', ');
|
|
27
|
+
const keys = Object.keys(KEY_HELP).map((k) => `- \`${k}\`: ${KEY_HELP[k]}`).join('\n');
|
|
28
|
+
return `# Changing Forge's settings
|
|
29
|
+
|
|
30
|
+
Use this when the user asks to change how Forge behaves: a model, a provider, permissions, MCP servers, agents, commands, skills.
|
|
31
|
+
|
|
32
|
+
## Where things live
|
|
33
|
+
|
|
34
|
+
- \`~/.forge/config.json\`: the user's own settings. Applies in every project.
|
|
35
|
+
- \`<workspace>/.forge/config.json\`: the project's settings, read only when the workspace is trusted. It can set provider, model, completionModel, compactionModel, reasoningEffort, subagentModel, modelOverrides, aliases, modeModels and display, and add stricter permission rules (ask, deny). It can never loosen anything.
|
|
36
|
+
- User-only keys (a project file cannot set them, Forge ignores them there with a warning): ${userOnly}.
|
|
37
|
+
- \`~/.forge/mcp.json\` and \`<workspace>/.forge/mcp.json\`: MCP servers, \`{ "mcpServers": { "name": { "command", "args", "env" } or { "url", "headers" } } }\`.
|
|
38
|
+
- \`<workspace>/.forge/agents/*.md\` and \`~/.forge/agents/*.md\`: agents; front matter (description, mode, model, tools, deny, steps, temperature) then the system prompt.
|
|
39
|
+
- \`<workspace>/.forge/commands/*.md\` and \`~/.forge/commands/*.md\`: slash commands; \`$ARGUMENTS\` is replaced by what the user types after the name.
|
|
40
|
+
- \`<workspace>/.forge/skills/\`, \`<workspace>/skills/\`, \`~/.forge/skills/\`: skills.
|
|
41
|
+
- \`AGENTS.md\` (or \`CLAUDE.md\`) in the workspace or a folder: instructions for the model.
|
|
42
|
+
- In VS Code the same choices also exist as \`forge.*\` settings, edited on the Forge Settings page or in settings.json.
|
|
43
|
+
|
|
44
|
+
## Keys of config.json
|
|
45
|
+
|
|
46
|
+
${keys}
|
|
47
|
+
|
|
48
|
+
## How to change them
|
|
49
|
+
|
|
50
|
+
0. Your file tools only reach the workspace, so you cannot edit \`~/.forge/config.json\`. For a key the project file may set (see above), edit \`<workspace>/.forge/config.json\` and say plainly that the change is workspace-scoped (only for this workspace, and only when it is trusted). For a user-only key, or when the person wants it everywhere, show the exact JSON to add and tell them to paste it into \`~/.forge/config.json\` or use the Forge Settings page; do not write it into the project file.
|
|
51
|
+
1. Read the file first. If it does not exist, create it with only the keys you are setting.
|
|
52
|
+
2. Change only what was asked. Keep every other key and keep the file valid JSON (double quotes, no trailing commas, no comments).
|
|
53
|
+
3. A secret never goes in a project file. Name an environment variable (\`apiKeyEnv\`) instead of pasting a key.
|
|
54
|
+
4. Do not widen permissions (\`autoApproveTools\`, \`permission\` allow rules, \`autoApproveCommands\`) unless the user asked for exactly that, and say what the change allows.
|
|
55
|
+
5. After saving, say what changed and where. Most changes apply on the next message; \`caCertificates\` and the language need a window reload, MCP servers and skills need \`/reload\`.
|
|
56
|
+
6. Do not run Forge commands that write to the real \`~/.forge\` to test a change. Edit the file and let the user check it.
|
|
57
|
+
`;
|
|
58
|
+
}
|
|
59
|
+
export function builtinSkills() {
|
|
60
|
+
return [
|
|
61
|
+
{
|
|
62
|
+
name: 'forge-config',
|
|
63
|
+
description: "Change Forge's own settings: models, providers, permissions, MCP servers, agents, commands, skills and instructions. Use when the user asks to configure or set up Forge itself.",
|
|
64
|
+
body: configBody(),
|
|
65
|
+
source: { kind: 'builtin', path: '(built into Forge)' },
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
}
|
package/dist/skills/index.d.ts
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* internal directory layout.
|
|
7
7
|
*/
|
|
8
8
|
export type { Skill, SkillRegistry, SkillRoots, SkillSource } from './types.js';
|
|
9
|
+
export { builtinSkills } from './builtin.js';
|
|
10
|
+
export { loadRemoteSkills } from './remote.js';
|
|
9
11
|
export { GLOBAL_SKILLS_DIR, LOCAL_SKILLS_DIR, PROJECT_SKILLS_DIR, discoverSkills, loadSkillsRegistry, loadSkillsRegistryAsync, parseSkillsDir, resolveSkillRoots, skillsRegistryFromList, } from './loader.js';
|
|
10
12
|
export { parseSkill } from './parser.js';
|
|
11
13
|
export { buildSkillsBlock, composeSkillsPrompt, scoreSkill, selectSkills, } from './activator.js';
|
package/dist/skills/index.js
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* callers can use skills + the community catalog without knowing the
|
|
6
6
|
* internal directory layout.
|
|
7
7
|
*/
|
|
8
|
+
export { builtinSkills } from './builtin.js';
|
|
9
|
+
export { loadRemoteSkills } from './remote.js';
|
|
8
10
|
export { GLOBAL_SKILLS_DIR, LOCAL_SKILLS_DIR, PROJECT_SKILLS_DIR, discoverSkills, loadSkillsRegistry, loadSkillsRegistryAsync, parseSkillsDir, resolveSkillRoots, skillsRegistryFromList, } from './loader.js';
|
|
9
11
|
export { parseSkill } from './parser.js';
|
|
10
12
|
export { buildSkillsBlock, composeSkillsPrompt, scoreSkill, selectSkills, } from './activator.js';
|
package/dist/skills/loader.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* root wins (local > project > global). The result is an immutable
|
|
7
7
|
* snapshot: call {@link reload} to re-read disk.
|
|
8
8
|
*/
|
|
9
|
+
import { type RemoteSkillOptions } from './remote.js';
|
|
9
10
|
import type { Skill, SkillRegistry, SkillRoots, SkillSource } from './types.js';
|
|
10
11
|
/** Local skill root: `<cwd>/skills/`. Lives next to the project's source so it can be checked into git. */
|
|
11
12
|
export declare const LOCAL_SKILLS_DIR = "skills";
|
|
@@ -31,7 +32,9 @@ export declare function parseSkillsDir(dir: string, kind: SkillSource['kind']):
|
|
|
31
32
|
* Discover all skills visible from `cwd`. Walks local → project → global,
|
|
32
33
|
* merges, and resolves duplicates by precedence (first-wins).
|
|
33
34
|
*/
|
|
34
|
-
export declare function discoverSkills(cwd: string
|
|
35
|
+
export declare function discoverSkills(cwd: string, extraDirs?: readonly string[], remote?: RemoteSkillOptions & {
|
|
36
|
+
onWarning?: (warning: string) => void;
|
|
37
|
+
}): Promise<Skill[]>;
|
|
35
38
|
/**
|
|
36
39
|
* Build an immutable registry from a pre-loaded skill list. Use this when
|
|
37
40
|
* tests want to inject a known set without touching the filesystem. Use
|
|
@@ -42,9 +45,13 @@ export declare function skillsRegistryFromList(skills: Skill[], roots: SkillRoot
|
|
|
42
45
|
* Build a registry by discovering skills from disk relative to `cwd`.
|
|
43
46
|
* The registry caches its initial load; call `reload()` to refresh.
|
|
44
47
|
*/
|
|
45
|
-
export declare function loadSkillsRegistry(cwd: string
|
|
48
|
+
export declare function loadSkillsRegistry(cwd: string, extraDirs?: readonly string[], remote?: RemoteSkillOptions & {
|
|
49
|
+
onWarning?: (warning: string) => void;
|
|
50
|
+
}): SkillRegistry;
|
|
46
51
|
/**
|
|
47
52
|
* Async variant: build a registry and immediately load its initial contents.
|
|
48
53
|
* Useful for callers that want a fully-populated registry in one step.
|
|
49
54
|
*/
|
|
50
|
-
export declare function loadSkillsRegistryAsync(cwd: string
|
|
55
|
+
export declare function loadSkillsRegistryAsync(cwd: string, extraDirs?: readonly string[], remote?: RemoteSkillOptions & {
|
|
56
|
+
onWarning?: (warning: string) => void;
|
|
57
|
+
}): Promise<SkillRegistry>;
|
package/dist/skills/loader.js
CHANGED
|
@@ -11,6 +11,8 @@ import { readdir, readFile } from 'node:fs/promises';
|
|
|
11
11
|
import { homedir } from 'node:os';
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import { discoverPlugins } from '../plugins/loader.js';
|
|
14
|
+
import { builtinSkills } from './builtin.js';
|
|
15
|
+
import { isHttpUrl, loadRemoteSkills } from './remote.js';
|
|
14
16
|
import { parseSkill } from './parser.js';
|
|
15
17
|
/** Local skill root: `<cwd>/skills/`. Lives next to the project's source so it can be checked into git. */
|
|
16
18
|
export const LOCAL_SKILLS_DIR = 'skills';
|
|
@@ -61,19 +63,30 @@ export async function parseSkillsDir(dir, kind) {
|
|
|
61
63
|
* Discover all skills visible from `cwd`. Walks local → project → global,
|
|
62
64
|
* merges, and resolves duplicates by precedence (first-wins).
|
|
63
65
|
*/
|
|
64
|
-
export async function discoverSkills(cwd) {
|
|
66
|
+
export async function discoverSkills(cwd, extraDirs = [], remote = {}) {
|
|
65
67
|
const roots = resolveSkillRoots(cwd);
|
|
66
68
|
const [localSkills, projectSkills, globalSkills] = await Promise.all([
|
|
67
69
|
parseSkillsDir(roots.local, 'local'),
|
|
68
70
|
parseSkillsDir(roots.project, 'project'),
|
|
69
71
|
parseSkillsDir(roots.global, 'global'),
|
|
70
72
|
]);
|
|
73
|
+
// Folders named in the `skillPaths` setting rank after the user's own folders and before plugins.
|
|
74
|
+
// An entry that is an http(s) URL is fetched (one skill file, or an index of them); the rest are folders.
|
|
75
|
+
const extraSkills = (await Promise.all(extraDirs.map(async (d) => {
|
|
76
|
+
if (isHttpUrl(d)) {
|
|
77
|
+
const got = await loadRemoteSkills(d, remote);
|
|
78
|
+
for (const w of got.warnings)
|
|
79
|
+
remote.onWarning?.(w);
|
|
80
|
+
return got.skills;
|
|
81
|
+
}
|
|
82
|
+
return parseSkillsDir(path.resolve(cwd, d.startsWith('~') ? path.join(homedir(), d.slice(1)) : d), 'global');
|
|
83
|
+
}))).flat();
|
|
71
84
|
// Plugin skills rank below the user's own: they never shadow a skill written locally.
|
|
72
85
|
const pluginSkills = (await Promise.all((await discoverPlugins(cwd)).map((p) => (p.skillsDir ? parseSkillsDir(p.skillsDir, p.scope) : Promise.resolve([]))))).flat();
|
|
73
|
-
// Order matters: first to claim a name wins. local > project > global > plugins.
|
|
86
|
+
// Order matters: first to claim a name wins. local > project > global > plugins > built in.
|
|
74
87
|
const seen = new Set();
|
|
75
88
|
const merged = [];
|
|
76
|
-
for (const list of [localSkills, projectSkills, globalSkills, pluginSkills]) {
|
|
89
|
+
for (const list of [localSkills, projectSkills, globalSkills, extraSkills, pluginSkills, builtinSkills()]) {
|
|
77
90
|
for (const skill of list) {
|
|
78
91
|
if (seen.has(skill.name))
|
|
79
92
|
continue;
|
|
@@ -105,11 +118,11 @@ export function skillsRegistryFromList(skills, roots) {
|
|
|
105
118
|
* Build a registry by discovering skills from disk relative to `cwd`.
|
|
106
119
|
* The registry caches its initial load; call `reload()` to refresh.
|
|
107
120
|
*/
|
|
108
|
-
export function loadSkillsRegistry(cwd) {
|
|
121
|
+
export function loadSkillsRegistry(cwd, extraDirs = [], remote = {}) {
|
|
109
122
|
const roots = resolveSkillRoots(cwd);
|
|
110
123
|
let cache = null;
|
|
111
124
|
const refresh = async () => {
|
|
112
|
-
cache = await discoverSkills(cwd);
|
|
125
|
+
cache = await discoverSkills(cwd, extraDirs, remote);
|
|
113
126
|
return cache;
|
|
114
127
|
};
|
|
115
128
|
return {
|
|
@@ -123,8 +136,8 @@ export function loadSkillsRegistry(cwd) {
|
|
|
123
136
|
* Async variant: build a registry and immediately load its initial contents.
|
|
124
137
|
* Useful for callers that want a fully-populated registry in one step.
|
|
125
138
|
*/
|
|
126
|
-
export async function loadSkillsRegistryAsync(cwd) {
|
|
127
|
-
const registry = loadSkillsRegistry(cwd);
|
|
139
|
+
export async function loadSkillsRegistryAsync(cwd, extraDirs = [], remote = {}) {
|
|
140
|
+
const registry = loadSkillsRegistry(cwd, extraDirs, remote);
|
|
128
141
|
await registry.reload();
|
|
129
142
|
return registry;
|
|
130
143
|
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { normalizeGithubSource } from '../mcp/marketplace.js';
|
|
12
12
|
import { parseSkill } from './parser.js';
|
|
13
13
|
import { DEFAULT_SKILL_CATALOG_URL, SKILL_CATALOG_VERSION, } from './marketplace-types.js';
|
|
14
|
+
import { isOutsideRel } from '../tools/shared.js';
|
|
14
15
|
/**
|
|
15
16
|
* Sanitize a catalog-provided name to a safe filename charset before it is
|
|
16
17
|
* used to build a path on disk. Mirrors `installDirNameFor`'s sanitization
|
|
@@ -168,7 +169,7 @@ export async function installSkillFromCatalog(params) {
|
|
|
168
169
|
const filePath = path.join(dest, `${safeName}.md`);
|
|
169
170
|
const resolvedDest = path.resolve(dest);
|
|
170
171
|
const rel = path.relative(resolvedDest, path.resolve(filePath));
|
|
171
|
-
if (
|
|
172
|
+
if (isOutsideRel(rel)) {
|
|
172
173
|
return null;
|
|
173
174
|
}
|
|
174
175
|
await fs.writeFile(filePath, rawContent, 'utf8');
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Skill } from './types.js';
|
|
2
|
+
export interface RemoteSkillOptions {
|
|
3
|
+
fetch?: typeof fetch;
|
|
4
|
+
/** Defaults to `~/.forge/skill-cache`. */
|
|
5
|
+
cacheDir?: string;
|
|
6
|
+
ttlMs?: number;
|
|
7
|
+
now?: () => number;
|
|
8
|
+
}
|
|
9
|
+
export interface RemoteSkills {
|
|
10
|
+
skills: Skill[];
|
|
11
|
+
warnings: string[];
|
|
12
|
+
}
|
|
13
|
+
export declare const isHttpUrl: (s: string) => boolean;
|
|
14
|
+
export declare function loadRemoteSkills(url: string, options?: RemoteSkillOptions): Promise<RemoteSkills>;
|