@proxysoul/soulforge 2.2.1 → 2.3.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 (3) hide show
  1. package/README.md +7 -2
  2. package/dist/index.js +118 -27
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -63,7 +63,7 @@ SoulForge doesn't work that way. On startup, it builds a **live dependency graph
63
63
 
64
64
  - **Lock-in mode.** Hides agent narration during work, shows only tool activity and the final answer. Toggle via `/lock-in` or config.
65
65
  - **Embedded Neovim.** Your actual config, plugins, and LSP servers. The AI works through the same editor you use. [Deep dive →](docs/architecture.md)
66
- - **17 providers.** Anthropic, OpenAI, Google, xAI, Groq, DeepSeek, Mistral, Amazon Bedrock, Fireworks, GitHub Copilot, GitHub Models, Ollama, OpenRouter, LLM Gateway, Vercel AI Gateway, Proxy, and any OpenAI-compatible API.
66
+ - **18 providers.** Anthropic, OpenAI, Google, xAI, Groq, DeepSeek, Mistral, Amazon Bedrock, Fireworks, GitHub Copilot, GitHub Models, Ollama, LM Studio, OpenRouter, LLM Gateway, Vercel AI Gateway, Proxy, and any OpenAI-compatible API.
67
67
  - **Task router.** Assign different models to different jobs. Spark agents (explore/investigate) and ember agents (code edits) can each use different models. You pick what goes where. [Deep dive →](docs/architecture.md)
68
68
  - **Code execution (Smithy).** Sandboxed code execution via Anthropic's `code_execution` tool. The agent can run Python to process data, do calculations, or batch tool calls programmatically.
69
69
  - **User steering.** Type while the agent works. Messages queue up and reach the agent at the next step. [Deep dive →](docs/steering.md)
@@ -88,7 +88,7 @@ SoulForge doesn't work that way. On startup, it builds a **live dependency graph
88
88
  | **Task routing** | Per-task model assignment (spark, ember, web search, verify, desloppify, compact) | Single model | Single model | Per-agent model | Single model |
89
89
  | **Compound tools** | `read` (batch + surgical), `multi_edit` (atomic), `rename_symbol`, `move_symbol`, `rename_file`, `refactor`, `project` | Rename via LSP | — | — | — |
90
90
  | **Editor** | Embedded Neovim (your config, your plugins) | No editor | No editor | No editor | No editor |
91
- | **Providers** | 17 + custom OpenAI-compatible | Anthropic only | Multi-model | OpenAI only | 100+ LLMs |
91
+ | **Providers** | 18 + custom OpenAI-compatible | Anthropic only | Multi-model | OpenAI only | 100+ LLMs |
92
92
  | **License** | BSL 1.1 (source-available) | Proprietary | Proprietary | Apache 2.0 | Apache 2.0 |
93
93
 
94
94
  > *Competitor features verified as of March 29, 2026. [Let us know](https://github.com/ProxySoul/soulforge/issues) if something's changed.*
@@ -215,6 +215,7 @@ soulforge --headless --diff "fix the bug" # Show changed files
215
215
  | [**GitHub Copilot**](https://github.com/features/copilot) | OAuth token from IDE ([setup](docs/copilot-provider.md)) |
216
216
  | [**GitHub Models**](https://github.com/marketplace/models) | `GITHUB_MODELS_API_KEY` (PAT with `models:read`) |
217
217
  | [**Ollama**](https://ollama.ai) | Auto-detected |
218
+ | [**LM Studio**](https://lmstudio.ai) | Auto-detected |
218
219
  | [**OpenRouter**](https://openrouter.ai) | `OPENROUTER_API_KEY` |
219
220
  | [**Vercel AI Gateway**](https://vercel.com/ai-gateway) | `AI_GATEWAY_API_KEY` |
220
221
  | [**Proxy**](https://github.com/router-for-me/CLIProxyAPI) | `PROXY_API_KEY` |
@@ -226,6 +227,10 @@ soulforge --headless --diff "fix the bug" # Show changed files
226
227
 
227
228
  **GitHub Models**: Free playground API with per-token billing. Create a fine-grained PAT with `models:read` scope. Lower rate limits than Copilot.
228
229
 
230
+ **Ollama**: Auto-detected at `localhost:11434`. Override with `OLLAMA_HOST=http://host:port`.
231
+
232
+ **LM Studio**: Auto-detected at `localhost:1234`. Uses the [REST API v0](https://lmstudio.ai/docs/developer/rest/endpoints) for rich model data (context length, type filtering). Override with `LM_STUDIO_URL=http://host:port`. Optional auth: set `LM_API_TOKEN` if you've enabled authentication in LM Studio.
233
+
229
234
  Add custom providers in config, no code changes:
230
235
 
231
236
  ```json
package/dist/index.js CHANGED
@@ -39260,7 +39260,7 @@ var package_default;
39260
39260
  var init_package = __esm(() => {
39261
39261
  package_default = {
39262
39262
  name: "@proxysoul/soulforge",
39263
- version: "2.2.1",
39263
+ version: "2.3.0",
39264
39264
  description: "Graph-powered code intelligence \u2014 multi-agent coding with codebase-aware AI",
39265
39265
  repository: {
39266
39266
  type: "git",
@@ -39982,7 +39982,8 @@ function buildCustomProvider(config2) {
39982
39982
  id: config2.id,
39983
39983
  name: config2.name ?? config2.id,
39984
39984
  envVar,
39985
- icon: "\u25C7",
39985
+ icon: "\uF29F",
39986
+ asciiIcon: "\u25C7",
39986
39987
  custom: true,
39987
39988
  createModel(modelId) {
39988
39989
  const apiKey = envVar ? getProviderApiKey(envVar) ?? "" : "custom";
@@ -49575,6 +49576,75 @@ var init_llmgateway = __esm(() => {
49575
49576
  };
49576
49577
  });
49577
49578
 
49579
+ // src/core/llm/providers/lmstudio.ts
49580
+ function getBaseOrigin() {
49581
+ return (process.env.LM_STUDIO_URL ?? "http://localhost:1234").replace(/\/+$/, "");
49582
+ }
49583
+ function openaiBase() {
49584
+ return `${getBaseOrigin()}/v1`;
49585
+ }
49586
+ function restBase() {
49587
+ return `${getBaseOrigin()}/api/v0`;
49588
+ }
49589
+ function getApiToken() {
49590
+ return process.env.LM_API_TOKEN ?? "lm-studio";
49591
+ }
49592
+ function authHeaders() {
49593
+ const token = getApiToken();
49594
+ return token && token !== "lm-studio" ? {
49595
+ Authorization: `Bearer ${token}`
49596
+ } : {};
49597
+ }
49598
+ var lmstudio;
49599
+ var init_lmstudio = __esm(() => {
49600
+ init_dist6();
49601
+ lmstudio = {
49602
+ id: "lmstudio",
49603
+ name: "LM Studio",
49604
+ envVar: "LM_API_TOKEN",
49605
+ secretKey: "lm-api-token",
49606
+ icon: "\uEA79",
49607
+ asciiIcon: "L",
49608
+ description: "Local models via LM Studio \u2014 no key needed",
49609
+ createModel(modelId) {
49610
+ const client = createOpenAI({
49611
+ baseURL: openaiBase(),
49612
+ apiKey: getApiToken()
49613
+ });
49614
+ return client.chat(modelId);
49615
+ },
49616
+ async fetchModels() {
49617
+ const res = await fetch(`${restBase()}/models`, {
49618
+ headers: authHeaders(),
49619
+ signal: AbortSignal.timeout(3000)
49620
+ });
49621
+ if (!res.ok)
49622
+ throw new Error(`LM Studio API ${String(res.status)}`);
49623
+ const data = await res.json();
49624
+ if (!Array.isArray(data.data))
49625
+ return null;
49626
+ return data.data.filter((m) => m.type === "llm" || m.type === "vlm").map((m) => ({
49627
+ id: m.id,
49628
+ name: m.id,
49629
+ contextWindow: m.max_context_length
49630
+ }));
49631
+ },
49632
+ fallbackModels: [],
49633
+ async checkAvailability() {
49634
+ try {
49635
+ const res = await fetch(`${restBase()}/models`, {
49636
+ headers: authHeaders(),
49637
+ signal: AbortSignal.timeout(1000)
49638
+ });
49639
+ return res.ok;
49640
+ } catch {
49641
+ return false;
49642
+ }
49643
+ },
49644
+ contextWindows: []
49645
+ };
49646
+ });
49647
+
49578
49648
  // node_modules/vercel-minimax-ai-provider/node_modules/@ai-sdk/anthropic/node_modules/@ai-sdk/provider/dist/index.mjs
49579
49649
  function getErrorMessage4(error48) {
49580
49650
  if (error48 == null) {
@@ -58676,6 +58746,9 @@ var init_mistral = __esm(() => {
58676
58746
  });
58677
58747
 
58678
58748
  // src/core/llm/providers/ollama.ts
58749
+ function getOllamaHost() {
58750
+ return (process.env.OLLAMA_HOST ?? "http://localhost:11434").replace(/\/+$/, "");
58751
+ }
58679
58752
  var ollama;
58680
58753
  var init_ollama = __esm(() => {
58681
58754
  init_dist6();
@@ -58683,18 +58756,18 @@ var init_ollama = __esm(() => {
58683
58756
  id: "ollama",
58684
58757
  name: "Ollama",
58685
58758
  envVar: "",
58686
- icon: "\uD83E\uDD99",
58687
- asciiIcon: "\uD83E\uDD99",
58759
+ icon: "\uEBA2",
58760
+ asciiIcon: "O",
58688
58761
  description: "Local models \u2014 no key needed",
58689
58762
  createModel(modelId) {
58690
58763
  const client = createOpenAI({
58691
- baseURL: "http://localhost:11434/v1",
58764
+ baseURL: `${getOllamaHost()}/v1`,
58692
58765
  apiKey: "ollama"
58693
58766
  });
58694
58767
  return client.chat(modelId);
58695
58768
  },
58696
58769
  async fetchModels() {
58697
- const res = await fetch("http://localhost:11434/api/tags");
58770
+ const res = await fetch(`${getOllamaHost()}/api/tags`);
58698
58771
  if (!res.ok)
58699
58772
  throw new Error(`Ollama API ${String(res.status)}`);
58700
58773
  const data = await res.json();
@@ -58721,7 +58794,7 @@ var init_ollama = __esm(() => {
58721
58794
  }],
58722
58795
  async checkAvailability() {
58723
58796
  try {
58724
- const res = await fetch("http://localhost:11434/api/tags", {
58797
+ const res = await fetch(`${getOllamaHost()}/api/tags`, {
58725
58798
  signal: AbortSignal.timeout(1000)
58726
58799
  });
58727
58800
  return res.ok;
@@ -77733,6 +77806,7 @@ __export(exports_providers, {
77733
77806
  ollama: () => ollama,
77734
77807
  mistral: () => mistral2,
77735
77808
  minimax: () => minimax2,
77809
+ lmstudio: () => lmstudio,
77736
77810
  llmgateway: () => llmgateway2,
77737
77811
  groq: () => groq2,
77738
77812
  google: () => google2,
@@ -77793,6 +77867,7 @@ var init_providers = __esm(() => {
77793
77867
  init_google();
77794
77868
  init_groq();
77795
77869
  init_llmgateway();
77870
+ init_lmstudio();
77796
77871
  init_minimax();
77797
77872
  init_mistral();
77798
77873
  init_ollama();
@@ -77811,6 +77886,7 @@ var init_providers = __esm(() => {
77811
77886
  init_google();
77812
77887
  init_groq();
77813
77888
  init_llmgateway();
77889
+ init_lmstudio();
77814
77890
  init_minimax();
77815
77891
  init_mistral();
77816
77892
  init_ollama();
@@ -77819,7 +77895,7 @@ var init_providers = __esm(() => {
77819
77895
  init_proxy();
77820
77896
  init_vercel_gateway();
77821
77897
  init_xai();
77822
- BUILTIN_PROVIDERS = [llmgateway2, anthropic2, proxy2, vercelGatewayProvider, openai2, xai2, google2, groq2, deepseek2, mistral2, bedrock2, fireworks2, minimax2, copilot, githubModels, openrouter2, ollama];
77898
+ BUILTIN_PROVIDERS = [llmgateway2, anthropic2, proxy2, vercelGatewayProvider, openai2, xai2, google2, groq2, deepseek2, mistral2, bedrock2, fireworks2, minimax2, copilot, githubModels, openrouter2, ollama, lmstudio];
77823
77899
  allProviders = [...BUILTIN_PROVIDERS];
77824
77900
  providerMap = new Map(allProviders.map((p) => [p.id, p]));
77825
77901
  });
@@ -448981,6 +449057,12 @@ function matchCopilotPricing(model) {
448981
449057
  }
448982
449058
  return;
448983
449059
  }
449060
+ function isModelLocal(modelId) {
449061
+ const slash = modelId.indexOf("/");
449062
+ if (slash < 0)
449063
+ return false;
449064
+ return LOCAL_PROVIDERS.has(modelId.slice(0, slash).toLowerCase());
449065
+ }
448984
449066
  function isModelFree(modelId) {
448985
449067
  const id = modelId.toLowerCase();
448986
449068
  if (id.endsWith(":free") || id.endsWith("-free"))
@@ -448995,7 +449077,7 @@ function isModelFree(modelId) {
448995
449077
  }
448996
449078
  function matchPricing(modelId) {
448997
449079
  const id = modelId.toLowerCase();
448998
- if (isModelFree(modelId))
449080
+ if (isModelLocal(modelId) || isModelFree(modelId))
448999
449081
  return FREE_PRICING;
449000
449082
  if (id.startsWith("copilot/")) {
449001
449083
  const model = id.slice("copilot/".length);
@@ -449219,7 +449301,7 @@ function startMemoryPoll(intervalMs = 2000) {
449219
449301
  });
449220
449302
  }, intervalMs);
449221
449303
  }
449222
- var MODEL_PRICING, FREE_PRICING, DEFAULT_PRICING, FREE, M025, M033, M1, M32, M30, COPILOT_PRICING, ZERO_USAGE, ZERO_PROCESS_RSS, useStatusBarStore, memPollStarted = false, memPollTimer = null;
449304
+ var MODEL_PRICING, FREE_PRICING, DEFAULT_PRICING, FREE, M025, M033, M1, M32, M30, COPILOT_PRICING, LOCAL_PROVIDERS, ZERO_USAGE, ZERO_PROCESS_RSS, useStatusBarStore, memPollStarted = false, memPollTimer = null;
449223
449305
  var init_statusbar = __esm(() => {
449224
449306
  init_esm();
449225
449307
  init_middleware();
@@ -449619,6 +449701,7 @@ var init_statusbar = __esm(() => {
449619
449701
  "claude-opus-4.6": M32,
449620
449702
  "claude-opus-4.6-fast": M30
449621
449703
  };
449704
+ LOCAL_PROVIDERS = new Set(["ollama", "lmstudio"]);
449622
449705
  ZERO_USAGE = {
449623
449706
  prompt: 0,
449624
449707
  completion: 0,
@@ -475094,8 +475177,10 @@ function fmtCost(usd) {
475094
475177
  return `$${usd.toFixed(3)}`;
475095
475178
  return `$${usd.toFixed(2)}`;
475096
475179
  }
475097
- function buildContent2(costCents, cacheHitPct, free) {
475180
+ function buildContent2(costCents, cacheHitPct, free, local) {
475098
475181
  const tk = getThemeTokens();
475182
+ if (local)
475183
+ return new StyledText([fg(tk.success)("Local")]);
475099
475184
  if (free)
475100
475185
  return new StyledText([fg(tk.success)("FREE")]);
475101
475186
  const cost = costCents / 100;
@@ -475110,11 +475195,13 @@ function TokenDisplay() {
475110
475195
  const cacheHitRef = import_react59.useRef(0);
475111
475196
  const currentCostRef = import_react59.useRef(0);
475112
475197
  const freeRef = import_react59.useRef(false);
475198
+ const localRef = import_react59.useRef(false);
475113
475199
  import_react59.useEffect(() => useStatusBarStore.subscribe((state) => {
475114
475200
  const usage = state.tokenUsage;
475115
475201
  const breakdown = usage.modelBreakdown;
475116
475202
  const modelIds = Object.keys(breakdown);
475117
- freeRef.current = modelIds.length > 0 && modelIds.every((mid) => isModelFree(mid));
475203
+ localRef.current = modelIds.length > 0 && modelIds.every((mid) => isModelLocal(mid));
475204
+ freeRef.current = !localRef.current && modelIds.length > 0 && modelIds.every((mid_0) => isModelFree(mid_0));
475118
475205
  const rawCost = breakdown && modelIds.length > 0 ? computeTotalCostFromBreakdown(breakdown) : 0;
475119
475206
  costRef.current = Math.round(rawCost * 100);
475120
475207
  const totalInput = usage.prompt + usage.subagentInput + usage.cacheRead + usage.cacheWrite;
@@ -475128,7 +475215,7 @@ function TokenDisplay() {
475128
475215
  currentCostRef.current = approach2(currentCostRef.current, target);
475129
475216
  try {
475130
475217
  if (textRef.current)
475131
- textRef.current.content = buildContent2(currentCostRef.current, cacheHitRef.current, freeRef.current);
475218
+ textRef.current.content = buildContent2(currentCostRef.current, cacheHitRef.current, freeRef.current, localRef.current);
475132
475219
  } catch {}
475133
475220
  }, STEP_MS2);
475134
475221
  return () => clearInterval(timer);
@@ -475136,7 +475223,7 @@ function TokenDisplay() {
475136
475223
  return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
475137
475224
  ref: textRef,
475138
475225
  truncate: true,
475139
- content: buildContent2(currentCostRef.current, cacheHitRef.current, freeRef.current)
475226
+ content: buildContent2(currentCostRef.current, cacheHitRef.current, freeRef.current, localRef.current)
475140
475227
  }, undefined, false, undefined, this);
475141
475228
  }
475142
475229
  var import_react59, STEP_MS2 = 50, EASE2 = 0.35;
@@ -491963,9 +492050,10 @@ function StatusDashboard({
491963
492050
  innerW
491964
492051
  }, "t-total", false, undefined, this));
491965
492052
  const sortedBd = Object.entries(su.modelBreakdown ?? {}).sort(([midA, a2], [midB, b5]) => computeModelCost(midB, b5) - computeModelCost(midA, a2));
491966
- const allFree = sortedBd.length > 0 && sortedBd.every(([mid_0]) => isModelFree(mid_0));
492053
+ const allLocal = sortedBd.length > 0 && sortedBd.every(([mid_0]) => isModelLocal(mid_0));
492054
+ const allFree = !allLocal && sortedBd.length > 0 && sortedBd.every(([mid_1]) => isModelFree(mid_1));
491967
492055
  const totalCost = sortedBd.length > 0 ? computeTotalCostFromBreakdown(su.modelBreakdown ?? {}) : 0;
491968
- if (totalCost > 0 || allFree) {
492056
+ if (totalCost > 0 || allFree || allLocal) {
491969
492057
  const fmtCost2 = (c) => c < 0.01 ? `${c.toFixed(3)}` : `${c.toFixed(2)}`;
491970
492058
  lines.push(/* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(Spacer, {
491971
492059
  innerW
@@ -491976,27 +492064,28 @@ function StatusDashboard({
491976
492064
  innerW
491977
492065
  }, "h-cost", false, undefined, this));
491978
492066
  const costLabelW = Math.min(30, innerW - 20);
491979
- for (const [mid_1, usage_0] of sortedBd) {
491980
- const free = isModelFree(mid_1);
491981
- const c_0 = computeModelCost(mid_1, usage_0);
491982
- if (c_0 <= 0 && !free)
492067
+ for (const [mid_2, usage_0] of sortedBd) {
492068
+ const local = isModelLocal(mid_2);
492069
+ const free = !local && isModelFree(mid_2);
492070
+ const c_0 = computeModelCost(mid_2, usage_0);
492071
+ if (c_0 <= 0 && !free && !local)
491983
492072
  continue;
491984
492073
  const pct = totalCost > 0 ? Math.round(c_0 / totalCost * 100) : 0;
491985
492074
  const maxModelW = costLabelW - 4;
491986
- const shortId = mid_1.length > maxModelW ? `${mid_1.slice(0, maxModelW - 1)}\u2026` : mid_1;
492075
+ const shortId = mid_2.length > maxModelW ? `${mid_2.slice(0, maxModelW - 1)}\u2026` : mid_2;
491987
492076
  lines.push(/* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(EntryRow, {
491988
492077
  label: ` ${shortId}`,
491989
- value: free ? "FREE" : `${fmtCost2(c_0)} (${String(pct)}%)`,
491990
- valueColor: free ? t2.success : t2.textPrimary,
492078
+ value: local ? "Local" : free ? "FREE" : `${fmtCost2(c_0)} (${String(pct)}%)`,
492079
+ valueColor: local || free ? t2.success : t2.textPrimary,
491991
492080
  labelW: costLabelW,
491992
492081
  rightAlign: true,
491993
492082
  innerW
491994
- }, `cost-${mid_1}`, false, undefined, this));
492083
+ }, `cost-${mid_2}`, false, undefined, this));
491995
492084
  }
491996
492085
  lines.push(/* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(EntryRow, {
491997
492086
  label: " Total",
491998
- value: allFree ? "FREE" : fmtCost2(totalCost),
491999
- valueColor: allFree ? t2.success : t2.warning,
492087
+ value: allLocal ? "Local" : allFree ? "FREE" : fmtCost2(totalCost),
492088
+ valueColor: allLocal || allFree ? t2.success : t2.warning,
492000
492089
  labelW: costLabelW,
492001
492090
  rightAlign: true,
492002
492091
  innerW
@@ -492012,7 +492101,9 @@ function StatusDashboard({
492012
492101
  innerW
492013
492102
  }, "h-tabs", false, undefined, this));
492014
492103
  const fmtCost_0 = (c_1, modelIds) => {
492015
- if (modelIds && modelIds.length > 0 && modelIds.every((mid_2) => isModelFree(mid_2)))
492104
+ if (modelIds && modelIds.length > 0 && modelIds.every((mid_3) => isModelLocal(mid_3)))
492105
+ return "Local";
492106
+ if (modelIds && modelIds.length > 0 && modelIds.every((mid_4) => isModelFree(mid_4)))
492016
492107
  return "FREE";
492017
492108
  return c_1 <= 0 ? "\u2014" : c_1 < 0.01 ? `$${c_1.toFixed(3)}` : `$${c_1.toFixed(2)}`;
492018
492109
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proxysoul/soulforge",
3
- "version": "2.2.1",
3
+ "version": "2.3.0",
4
4
  "description": "Graph-powered code intelligence — multi-agent coding with codebase-aware AI",
5
5
  "repository": {
6
6
  "type": "git",