@proxysoul/soulforge 2.18.3 → 2.18.4

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/index.js CHANGED
@@ -5202,25 +5202,56 @@ function loadConfig() {
5202
5202
  if (!existsSync10(configDir2)) {
5203
5203
  mkdirSync8(configDir2, { recursive: true, mode: 448 });
5204
5204
  }
5205
- if (!existsSync10(configFile)) {
5206
- writeFileSync7(configFile, JSON.stringify(DEFAULT_CONFIG, null, 2));
5207
- if (!_presetOverlay)
5208
- return DEFAULT_CONFIG;
5209
- }
5210
5205
  let userConfig = {};
5211
- try {
5212
- userConfig = JSON.parse(readFileSync8(configFile, "utf-8"));
5213
- } catch (err2) {
5214
- logBackgroundError("config", `Failed to parse ${configFile}: ${err2 instanceof Error ? err2.message : String(err2)} \u2014 using defaults`);
5215
- if (!_presetOverlay)
5216
- return DEFAULT_CONFIG;
5206
+ let fileExists = existsSync10(configFile);
5207
+ if (!fileExists) {
5208
+ writeFileSync7(configFile, JSON.stringify(DEFAULT_CONFIG, null, 2));
5209
+ fileExists = true;
5210
+ } else {
5211
+ try {
5212
+ userConfig = JSON.parse(readFileSync8(configFile, "utf-8"));
5213
+ } catch (err2) {
5214
+ logBackgroundError("config", `Failed to parse ${configFile}: ${err2 instanceof Error ? err2.message : String(err2)} \u2014 using defaults`);
5215
+ userConfig = {};
5216
+ }
5217
5217
  }
5218
5218
  let merged = { ...DEFAULT_CONFIG };
5219
5219
  if (_presetOverlay)
5220
5220
  merged = applyConfigPatch(merged, _presetOverlay);
5221
- merged = { ...merged, ...userConfig };
5221
+ const userPatch = diffAgainstDefaults(userConfig);
5222
+ if (Object.keys(userPatch).length > 0) {
5223
+ merged = applyConfigPatch(merged, userPatch);
5224
+ }
5222
5225
  return merged;
5223
5226
  }
5227
+ function diffAgainstDefaults(userConfig) {
5228
+ const out2 = {};
5229
+ const defaults = DEFAULT_CONFIG;
5230
+ const nested = new Set(NESTED_KEYS);
5231
+ for (const [key, value] of Object.entries(userConfig)) {
5232
+ if (value === undefined)
5233
+ continue;
5234
+ const def = defaults[key];
5235
+ if (nested.has(key) && value && typeof value === "object" && !Array.isArray(value) && def && typeof def === "object" && !Array.isArray(def)) {
5236
+ const subPatch = {};
5237
+ const defRec = def;
5238
+ for (const [subKey, subValue] of Object.entries(value)) {
5239
+ if (subValue === undefined)
5240
+ continue;
5241
+ if (JSON.stringify(subValue) !== JSON.stringify(defRec[subKey])) {
5242
+ subPatch[subKey] = subValue;
5243
+ }
5244
+ }
5245
+ if (Object.keys(subPatch).length > 0)
5246
+ out2[key] = subPatch;
5247
+ continue;
5248
+ }
5249
+ if (JSON.stringify(value) !== JSON.stringify(def)) {
5250
+ out2[key] = value;
5251
+ }
5252
+ }
5253
+ return out2;
5254
+ }
5224
5255
  function loadProjectConfig(cwd) {
5225
5256
  const projectFile = join11(cwd, ".soulforge", "config.json");
5226
5257
  if (!existsSync10(projectFile))
@@ -71714,7 +71745,7 @@ var package_default;
71714
71745
  var init_package = __esm(() => {
71715
71746
  package_default = {
71716
71747
  name: "@proxysoul/soulforge",
71717
- version: "2.18.3",
71748
+ version: "2.18.4",
71718
71749
  description: "Graph-powered code intelligence \u2014 multi-agent coding with codebase-aware AI",
71719
71750
  repository: {
71720
71751
  type: "git",
@@ -74060,6 +74091,17 @@ function createReasoningFetchWrapper(reasoningBody) {
74060
74091
  }
74061
74092
 
74062
74093
  // src/core/llm/providers/custom.ts
74094
+ function normalizeBaseURLPath(baseURL) {
74095
+ return baseURL.replace(/\/v1(?:\/)?$/i, "").replace(/\/+$/, "");
74096
+ }
74097
+ function resolveModelsAPIUrl(config2) {
74098
+ if (config2.modelsAPI === false)
74099
+ return null;
74100
+ if (config2.modelsAPI)
74101
+ return config2.modelsAPI;
74102
+ const normalized = normalizeBaseURLPath(config2.baseURL);
74103
+ return `${normalized}/models`;
74104
+ }
74063
74105
  function normalizeModels(models) {
74064
74106
  if (!models || models.length === 0)
74065
74107
  return [];
@@ -74097,22 +74139,41 @@ function buildCustomProvider(config2) {
74097
74139
  return client.chatModel(modelId);
74098
74140
  },
74099
74141
  async fetchModels() {
74100
- if (!config2.modelsAPI)
74142
+ const modelsUrl = resolveModelsAPIUrl(config2);
74143
+ if (!modelsUrl)
74101
74144
  return null;
74102
74145
  const apiKey = envVar ? getProviderApiKey(envVar) ?? "" : "";
74103
74146
  const headers = { "Content-Type": "application/json" };
74104
74147
  if (apiKey)
74105
74148
  headers.Authorization = `Bearer ${apiKey}`;
74106
- const res = await fetch(config2.modelsAPI, {
74107
- headers,
74108
- signal: AbortSignal.timeout(5000)
74109
- });
74149
+ let res;
74150
+ try {
74151
+ res = await fetch(modelsUrl, {
74152
+ headers,
74153
+ signal: AbortSignal.timeout(2000)
74154
+ });
74155
+ } catch {
74156
+ return null;
74157
+ }
74110
74158
  if (!res.ok)
74111
74159
  return null;
74112
- const data = await res.json();
74113
- if (!Array.isArray(data.data))
74160
+ let parsed;
74161
+ try {
74162
+ parsed = await res.json();
74163
+ } catch {
74114
74164
  return null;
74115
- return data.data.map((m) => ({ id: m.id, name: m.id }));
74165
+ }
74166
+ if (!Array.isArray(parsed.data))
74167
+ return null;
74168
+ return parsed.data.map((m) => {
74169
+ const rawContext = m.context_length ?? m.max_context_length ?? m.model_info?.max_input_tokens ?? m.meta?.n_ctx_train;
74170
+ const contextWindow = typeof rawContext === "number" ? rawContext : typeof rawContext === "string" ? Number(rawContext) : undefined;
74171
+ return {
74172
+ id: m.id,
74173
+ name: m.id,
74174
+ ...contextWindow !== undefined && !Number.isNaN(contextWindow) ? { contextWindow } : {}
74175
+ };
74176
+ });
74116
74177
  },
74117
74178
  fallbackModels: normalizeModels(config2.models),
74118
74179
  contextWindows: [],
@@ -468144,17 +468205,14 @@ function expandEnv(value) {
468144
468205
  return value;
468145
468206
  }
468146
468207
  function validatePreset(raw2, origin) {
468147
- if (!raw2 || typeof raw2 !== "object") {
468148
- throw new Error(`Preset at ${origin} is not an object`);
468149
- }
468150
- const obj = raw2;
468151
- if (typeof obj.name !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(obj.name)) {
468152
- throw new Error(`Preset at ${origin}: invalid or missing "name"`);
468153
- }
468154
- if (typeof obj.version !== "string" || !/^\d+\.\d+\.\d+$/.test(obj.version)) {
468155
- throw new Error(`Preset at ${origin}: invalid or missing "version" (semver required)`);
468208
+ const expanded = expandEnv(raw2);
468209
+ const parsed = PresetSchema.safeParse(expanded);
468210
+ if (!parsed.success) {
468211
+ const issue2 = parsed.error.issues[0];
468212
+ const path = issue2?.path.length ? issue2.path.join(".") : "<root>";
468213
+ throw new Error(`Preset at ${origin}: ${path} \u2014 ${issue2?.message ?? "invalid shape"}`);
468156
468214
  }
468157
- return expandEnv(obj);
468215
+ return parsed.data;
468158
468216
  }
468159
468217
  function cacheFile(name39, version2) {
468160
468218
  return join55(getCacheDir(), `${name39}@${version2}.json`);
@@ -468249,11 +468307,28 @@ async function resolvePresets(specs, opts = {}) {
468249
468307
  failures
468250
468308
  };
468251
468309
  }
468252
- var NETWORK_TIMEOUT_MS2 = 1e4, MAX_PRESET_BYTES;
468310
+ var NETWORK_TIMEOUT_MS2 = 1e4, MAX_PRESET_BYTES, PresetSchema;
468253
468311
  var init_loader3 = __esm(() => {
468312
+ init_zod();
468254
468313
  init_platform();
468255
468314
  init_registry2();
468256
468315
  MAX_PRESET_BYTES = 512 * 1024;
468316
+ PresetSchema = exports_external.object({
468317
+ name: exports_external.string().regex(/^[a-z0-9][a-z0-9-]*$/, "name must be lowercase, start with [a-z0-9], hyphen-separated"),
468318
+ version: exports_external.string().regex(/^\d+\.\d+\.\d+$/, "version must be semver (x.y.z)"),
468319
+ description: exports_external.string().optional(),
468320
+ author: exports_external.string().optional(),
468321
+ homepage: exports_external.string().optional(),
468322
+ tags: exports_external.array(exports_external.string()).optional(),
468323
+ router: exports_external.record(exports_external.string(), exports_external.unknown()).optional(),
468324
+ routerRules: exports_external.unknown().optional(),
468325
+ providers: exports_external.unknown().optional(),
468326
+ theme: exports_external.unknown().optional(),
468327
+ themes: exports_external.record(exports_external.string(), exports_external.unknown()).optional(),
468328
+ hooks: exports_external.record(exports_external.string(), exports_external.unknown()).optional(),
468329
+ prompts: exports_external.record(exports_external.string(), exports_external.unknown()).optional(),
468330
+ config: exports_external.record(exports_external.string(), exports_external.unknown()).optional()
468331
+ }).passthrough();
468257
468332
  });
468258
468333
 
468259
468334
  // src/core/presets/merge.ts
@@ -29346,30 +29346,58 @@ function loadConfig() {
29346
29346
  mode: 448
29347
29347
  });
29348
29348
  }
29349
- if (!existsSync7(configFile)) {
29350
- writeFileSync3(configFile, JSON.stringify(DEFAULT_CONFIG, null, 2));
29351
- if (!_presetOverlay)
29352
- return DEFAULT_CONFIG;
29353
- }
29354
29349
  let userConfig = {};
29355
- try {
29356
- userConfig = JSON.parse(readFileSync4(configFile, "utf-8"));
29357
- } catch (err2) {
29358
- logBackgroundError("config", `Failed to parse ${configFile}: ${err2 instanceof Error ? err2.message : String(err2)} \u2014 using defaults`);
29359
- if (!_presetOverlay)
29360
- return DEFAULT_CONFIG;
29350
+ let fileExists = existsSync7(configFile);
29351
+ if (!fileExists) {
29352
+ writeFileSync3(configFile, JSON.stringify(DEFAULT_CONFIG, null, 2));
29353
+ fileExists = true;
29354
+ } else {
29355
+ try {
29356
+ userConfig = JSON.parse(readFileSync4(configFile, "utf-8"));
29357
+ } catch (err2) {
29358
+ logBackgroundError("config", `Failed to parse ${configFile}: ${err2 instanceof Error ? err2.message : String(err2)} \u2014 using defaults`);
29359
+ userConfig = {};
29360
+ }
29361
29361
  }
29362
29362
  let merged = {
29363
29363
  ...DEFAULT_CONFIG
29364
29364
  };
29365
29365
  if (_presetOverlay)
29366
29366
  merged = applyConfigPatch(merged, _presetOverlay);
29367
- merged = {
29368
- ...merged,
29369
- ...userConfig
29370
- };
29367
+ const userPatch = diffAgainstDefaults(userConfig);
29368
+ if (Object.keys(userPatch).length > 0) {
29369
+ merged = applyConfigPatch(merged, userPatch);
29370
+ }
29371
29371
  return merged;
29372
29372
  }
29373
+ function diffAgainstDefaults(userConfig) {
29374
+ const out2 = {};
29375
+ const defaults = DEFAULT_CONFIG;
29376
+ const nested = new Set(NESTED_KEYS);
29377
+ for (const [key2, value] of Object.entries(userConfig)) {
29378
+ if (value === undefined)
29379
+ continue;
29380
+ const def = defaults[key2];
29381
+ if (nested.has(key2) && value && typeof value === "object" && !Array.isArray(value) && def && typeof def === "object" && !Array.isArray(def)) {
29382
+ const subPatch = {};
29383
+ const defRec = def;
29384
+ for (const [subKey, subValue] of Object.entries(value)) {
29385
+ if (subValue === undefined)
29386
+ continue;
29387
+ if (JSON.stringify(subValue) !== JSON.stringify(defRec[subKey])) {
29388
+ subPatch[subKey] = subValue;
29389
+ }
29390
+ }
29391
+ if (Object.keys(subPatch).length > 0)
29392
+ out2[key2] = subPatch;
29393
+ continue;
29394
+ }
29395
+ if (JSON.stringify(value) !== JSON.stringify(def)) {
29396
+ out2[key2] = value;
29397
+ }
29398
+ }
29399
+ return out2;
29400
+ }
29373
29401
  function loadProjectConfig(cwd) {
29374
29402
  const projectFile = join9(cwd, ".soulforge", "config.json");
29375
29403
  if (!existsSync7(projectFile))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proxysoul/soulforge",
3
- "version": "2.18.3",
3
+ "version": "2.18.4",
4
4
  "description": "Graph-powered code intelligence — multi-agent coding with codebase-aware AI",
5
5
  "repository": {
6
6
  "type": "git",
@@ -78,12 +78,43 @@ if (!existsSync(entry)) {
78
78
  process.exit(1);
79
79
  }
80
80
 
81
+ // detached:true puts the child in its own process group (POSIX) / detaches
82
+ // from the parent console (Windows). The child's cleanup path calls
83
+ // `kill(-pid, SIGTERM)` to reap orphaned grandchildren — without a separate
84
+ // group that signal would also terminate this launcher, causing zsh/bash to
85
+ // print "terminated soulforge" and scroll past the child's exit banner.
81
86
  const child = spawn(bun, [entry, ...process.argv.slice(2)], {
82
87
  stdio: "inherit",
88
+ detached: !isWindows,
83
89
  windowsHide: false,
84
90
  });
91
+
92
+ // Forward terminal signals to the child's group. On POSIX with detached:true
93
+ // the TTY no longer broadcasts SIGINT to the child automatically, so we relay.
94
+ // On Windows the console still routes Ctrl+C/Break to the child; these
95
+ // handlers are harmless no-ops there (signal forwarding via process.kill on
96
+ // Windows just terminates — child handles its own console events).
97
+ const FORWARDED = ["SIGINT", "SIGTERM", "SIGHUP"];
98
+ for (const sig of FORWARDED) {
99
+ process.on(sig, () => {
100
+ try {
101
+ if (isWindows) {
102
+ // Windows: signal the child directly; no process-group concept.
103
+ child.kill(sig);
104
+ } else {
105
+ // POSIX: signal the child's process group so its own children get it too.
106
+ process.kill(-child.pid, sig);
107
+ }
108
+ } catch {
109
+ // Child already gone — let our own exit logic run.
110
+ }
111
+ });
112
+ }
113
+
85
114
  child.on("exit", (code, signal) => {
86
115
  if (signal) {
116
+ // Re-raise so the parent shell sees the correct exit status, but only
117
+ // for genuine signal terminations — clean exits with code 0 fall through.
87
118
  process.kill(process.pid, signal);
88
119
  } else {
89
120
  process.exit(code ?? 0);