@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.
Files changed (135) hide show
  1. package/dist/agent/agent.d.ts +8 -0
  2. package/dist/agent/agent.js +123 -22
  3. package/dist/agent/background.d.ts +58 -0
  4. package/dist/agent/background.js +139 -0
  5. package/dist/agent/enhance-prompt.d.ts +3 -1
  6. package/dist/agent/enhance-prompt.js +6 -2
  7. package/dist/agent/goal.d.ts +54 -0
  8. package/dist/agent/goal.js +65 -0
  9. package/dist/agents/agent-files.d.ts +43 -0
  10. package/dist/agents/agent-files.js +109 -0
  11. package/dist/checkpoints/store.js +5 -1
  12. package/dist/commands/custom-commands.d.ts +9 -4
  13. package/dist/commands/custom-commands.js +13 -2
  14. package/dist/config-file.d.ts +43 -0
  15. package/dist/config-file.js +249 -5
  16. package/dist/config-merge.d.ts +13 -0
  17. package/dist/config-merge.js +67 -0
  18. package/dist/config-schema.d.ts +340 -0
  19. package/dist/config-schema.js +126 -0
  20. package/dist/config.js +2 -1
  21. package/dist/context/compaction.js +4 -0
  22. package/dist/context/mask.js +1 -1
  23. package/dist/custom-providers.d.ts +95 -0
  24. package/dist/custom-providers.js +120 -0
  25. package/dist/git/auto-commit.d.ts +2 -2
  26. package/dist/git/auto-commit.js +5 -4
  27. package/dist/github/github-tools.d.ts +2 -2
  28. package/dist/hooks/runner.js +3 -2
  29. package/dist/import/claude-code.d.ts +39 -0
  30. package/dist/import/claude-code.js +254 -0
  31. package/dist/index.d.ts +32 -4
  32. package/dist/index.js +24 -3
  33. package/dist/lsp/client.d.ts +5 -0
  34. package/dist/lsp/client.js +39 -3
  35. package/dist/mcp/index.d.ts +5 -1
  36. package/dist/mcp/index.js +3 -1
  37. package/dist/mcp/manager.d.ts +17 -3
  38. package/dist/mcp/manager.js +27 -2
  39. package/dist/mcp/marketplace.d.ts +2 -0
  40. package/dist/mcp/marketplace.js +10 -4
  41. package/dist/mcp/mcp-client.d.ts +12 -2
  42. package/dist/mcp/mcp-client.js +74 -17
  43. package/dist/mcp/mcp-config.d.ts +2 -0
  44. package/dist/mcp/mcp-config.js +1 -0
  45. package/dist/mcp/oauth.d.ts +55 -0
  46. package/dist/mcp/oauth.js +197 -0
  47. package/dist/mcp/resource-tools.d.ts +8 -0
  48. package/dist/mcp/resource-tools.js +56 -0
  49. package/dist/memory/session-digest.d.ts +12 -0
  50. package/dist/memory/session-digest.js +47 -0
  51. package/dist/memory/stale.js +2 -1
  52. package/dist/model-alias.d.ts +18 -2
  53. package/dist/model-alias.js +27 -3
  54. package/dist/model-catalog.js +0 -1
  55. package/dist/model-list.js +2 -1
  56. package/dist/model-overrides.d.ts +30 -0
  57. package/dist/model-overrides.js +48 -0
  58. package/dist/orchestrator/specialists-registry.js +2 -3
  59. package/dist/orchestrator/specialists.js +4 -4
  60. package/dist/policy/ca-certs.d.ts +8 -0
  61. package/dist/policy/ca-certs.js +30 -0
  62. package/dist/policy/proxy.d.ts +10 -3
  63. package/dist/policy/proxy.js +33 -6
  64. package/dist/pricing.js +4 -0
  65. package/dist/providers.d.ts +7 -0
  66. package/dist/providers.js +39 -5
  67. package/dist/semantic/embedder.d.ts +15 -0
  68. package/dist/semantic/embedder.js +25 -0
  69. package/dist/semantic/semantic-index.d.ts +10 -2
  70. package/dist/semantic/semantic-index.js +16 -7
  71. package/dist/sessions/session-store.js +2 -1
  72. package/dist/skills/builtin.d.ts +2 -0
  73. package/dist/skills/builtin.js +68 -0
  74. package/dist/skills/index.d.ts +2 -0
  75. package/dist/skills/index.js +2 -0
  76. package/dist/skills/loader.d.ts +10 -3
  77. package/dist/skills/loader.js +20 -7
  78. package/dist/skills/marketplace.js +2 -1
  79. package/dist/skills/remote.d.ts +14 -0
  80. package/dist/skills/remote.js +131 -0
  81. package/dist/skills/types.d.ts +10 -0
  82. package/dist/steering/extra-instructions.d.ts +8 -0
  83. package/dist/steering/extra-instructions.js +104 -0
  84. package/dist/steering/folder-instructions.d.ts +8 -0
  85. package/dist/steering/folder-instructions.js +28 -0
  86. package/dist/steering/select.js +2 -1
  87. package/dist/timed-fetch.d.ts +12 -0
  88. package/dist/timed-fetch.js +83 -0
  89. package/dist/tools/apply-patch.d.ts +52 -0
  90. package/dist/tools/apply-patch.js +197 -0
  91. package/dist/tools/ask-user.d.ts +27 -0
  92. package/dist/tools/ask-user.js +32 -0
  93. package/dist/tools/browser-tools.d.ts +6 -6
  94. package/dist/tools/browser-tools.js +3 -13
  95. package/dist/tools/codebase-search.d.ts +4 -2
  96. package/dist/tools/codebase-search.js +2 -2
  97. package/dist/tools/edit-file.js +5 -3
  98. package/dist/tools/fetch-url.d.ts +7 -0
  99. package/dist/tools/fetch-url.js +52 -19
  100. package/dist/tools/find-symbol.d.ts +2 -2
  101. package/dist/tools/ignore-rules.js +2 -1
  102. package/dist/tools/index.d.ts +5 -1
  103. package/dist/tools/index.js +10 -1
  104. package/dist/tools/memory.d.ts +6 -6
  105. package/dist/tools/notebook.d.ts +2 -2
  106. package/dist/tools/permission-rules.d.ts +32 -0
  107. package/dist/tools/permission-rules.js +182 -0
  108. package/dist/tools/read-file.d.ts +5 -0
  109. package/dist/tools/read-file.js +7 -2
  110. package/dist/tools/read-only.d.ts +1 -1
  111. package/dist/tools/read-only.js +2 -0
  112. package/dist/tools/run-command.d.ts +4 -2
  113. package/dist/tools/run-command.js +12 -8
  114. package/dist/tools/sandbox-loader.js +3 -13
  115. package/dist/tools/search-files.d.ts +8 -2
  116. package/dist/tools/search-files.js +43 -0
  117. package/dist/tools/shared.d.ts +5 -0
  118. package/dist/tools/shared.js +30 -2
  119. package/dist/tools/shell-env.d.ts +7 -0
  120. package/dist/tools/shell-env.js +9 -0
  121. package/dist/tools/sub-agent-wait.d.ts +29 -0
  122. package/dist/tools/sub-agent-wait.js +60 -0
  123. package/dist/tools/sub-agent.d.ts +50 -4
  124. package/dist/tools/sub-agent.js +210 -125
  125. package/dist/tools/text-encoding.d.ts +10 -0
  126. package/dist/tools/text-encoding.js +40 -0
  127. package/dist/tools/todo.d.ts +36 -0
  128. package/dist/tools/todo.js +42 -0
  129. package/dist/tools/write-file.js +15 -9
  130. package/dist/tools/write-tools.d.ts +12 -0
  131. package/dist/tools/write-tools.js +18 -0
  132. package/dist/types.d.ts +73 -0
  133. package/dist/worktree/worktree.d.ts +3 -0
  134. package/dist/worktree/worktree.js +22 -0
  135. package/package.json +1 -1
@@ -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
- * Route all `fetch` traffic through the proxy in `settings` (default: the environment).
22
- * Idempotent. Returns whether a proxy was installed.
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 async function installProxy(settings = proxyFromEnv()) {
25
- if (installed || !hasProxy(settings))
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
- setGlobalDispatcher(new EnvHttpProxyAgent({
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
@@ -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() && EXTENSIONS.has(path.extname(entry.name))) {
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
- for (const top of scored.slice(0, k)) {
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
- const text = rawText === undefined ? undefined : stripSteering(rawText);
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,2 @@
1
+ import type { Skill } from './types.js';
2
+ export declare function builtinSkills(): Skill[];
@@ -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
+ }
@@ -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';
@@ -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';
@@ -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): Promise<Skill[]>;
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): SkillRegistry;
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): Promise<SkillRegistry>;
55
+ export declare function loadSkillsRegistryAsync(cwd: string, extraDirs?: readonly string[], remote?: RemoteSkillOptions & {
56
+ onWarning?: (warning: string) => void;
57
+ }): Promise<SkillRegistry>;
@@ -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 (rel.startsWith('..') || path.isAbsolute(rel)) {
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>;