@nac3/forge-cli 1.0.33 → 1.0.36

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 (59) hide show
  1. package/dist/chat/claude.d.ts.map +1 -1
  2. package/dist/chat/claude.js +15 -1
  3. package/dist/chat/claude.js.map +1 -1
  4. package/dist/chat/panel.d.ts.map +1 -1
  5. package/dist/chat/panel.js +260 -0
  6. package/dist/chat/panel.js.map +1 -1
  7. package/dist/chat/server.d.ts.map +1 -1
  8. package/dist/chat/server.js +223 -0
  9. package/dist/chat/server.js.map +1 -1
  10. package/dist/llm/cohere_client.d.ts +1 -0
  11. package/dist/llm/cohere_client.d.ts.map +1 -1
  12. package/dist/llm/cohere_client.js +5 -0
  13. package/dist/llm/cohere_client.js.map +1 -1
  14. package/dist/llm/deepseek_client.d.ts +1 -0
  15. package/dist/llm/deepseek_client.d.ts.map +1 -1
  16. package/dist/llm/deepseek_client.js +5 -0
  17. package/dist/llm/deepseek_client.js.map +1 -1
  18. package/dist/llm/federation_client.d.ts +3 -0
  19. package/dist/llm/federation_client.d.ts.map +1 -1
  20. package/dist/llm/federation_client.js +2 -0
  21. package/dist/llm/federation_client.js.map +1 -1
  22. package/dist/llm/federation_host.d.ts.map +1 -1
  23. package/dist/llm/federation_host.js +1 -0
  24. package/dist/llm/federation_host.js.map +1 -1
  25. package/dist/llm/gemini_client.d.ts +14 -0
  26. package/dist/llm/gemini_client.d.ts.map +1 -1
  27. package/dist/llm/gemini_client.js +0 -0
  28. package/dist/llm/gemini_client.js.map +1 -1
  29. package/dist/llm/managed_client.d.ts +1 -0
  30. package/dist/llm/managed_client.d.ts.map +1 -1
  31. package/dist/llm/managed_client.js +5 -1
  32. package/dist/llm/managed_client.js.map +1 -1
  33. package/dist/llm/mistral_client.d.ts +1 -0
  34. package/dist/llm/mistral_client.d.ts.map +1 -1
  35. package/dist/llm/mistral_client.js +5 -0
  36. package/dist/llm/mistral_client.js.map +1 -1
  37. package/dist/llm/multi_provider_client.d.ts +5 -0
  38. package/dist/llm/multi_provider_client.d.ts.map +1 -1
  39. package/dist/llm/multi_provider_client.js +6 -0
  40. package/dist/llm/multi_provider_client.js.map +1 -1
  41. package/dist/llm/ollama_client.d.ts +1 -0
  42. package/dist/llm/ollama_client.d.ts.map +1 -1
  43. package/dist/llm/ollama_client.js +5 -0
  44. package/dist/llm/ollama_client.js.map +1 -1
  45. package/dist/llm/openai_client.d.ts +1 -0
  46. package/dist/llm/openai_client.d.ts.map +1 -1
  47. package/dist/llm/openai_client.js +6 -0
  48. package/dist/llm/openai_client.js.map +1 -1
  49. package/dist/llm/single_step.d.ts +1 -0
  50. package/dist/llm/single_step.d.ts.map +1 -1
  51. package/dist/llm/single_step.js +23 -5
  52. package/dist/llm/single_step.js.map +1 -1
  53. package/dist/llm/xai_client.d.ts +1 -0
  54. package/dist/llm/xai_client.d.ts.map +1 -1
  55. package/dist/llm/xai_client.js +5 -0
  56. package/dist/llm/xai_client.js.map +1 -1
  57. package/dist/version.d.ts +1 -1
  58. package/dist/version.js +1 -1
  59. package/package.json +1 -1
@@ -1918,6 +1918,28 @@ async function route(req, res, ctx) {
1918
1918
  await handleBrainConfigSet(req, res, ctx);
1919
1919
  return;
1920
1920
  }
1921
+ /* F4 brain federation -- connect to another Forge's brain, mint
1922
+ authorize/route codes, host the answer loop, all from the panel. */
1923
+ if (req.method === 'GET' && url.pathname === '/api/forge/federation') {
1924
+ await handleFederationGet(req, res);
1925
+ return;
1926
+ }
1927
+ if (req.method === 'POST' && url.pathname === '/api/forge/federation/join') {
1928
+ await handleFederationJoin(req, res);
1929
+ return;
1930
+ }
1931
+ if (req.method === 'POST' && url.pathname === '/api/forge/federation/leave') {
1932
+ await handleFederationLeave(req, res);
1933
+ return;
1934
+ }
1935
+ if (req.method === 'POST' && url.pathname === '/api/forge/federation/invite') {
1936
+ await handleFederationInvite(req, res);
1937
+ return;
1938
+ }
1939
+ if (req.method === 'POST' && url.pathname === '/api/forge/federation/host') {
1940
+ await handleFederationHost(req, res);
1941
+ return;
1942
+ }
1921
1943
  /* PND-069 -- modelos disponibles por provider (API en vivo con
1922
1944
  fallback estatico). */
1923
1945
  if (req.method === 'GET' && url.pathname === '/api/forge/brain-models') {
@@ -3176,6 +3198,21 @@ async function handleBrainConfigSet(req, res, ctx) {
3176
3198
  if (model)
3177
3199
  cfg[tier].model = model;
3178
3200
  }
3201
+ /* F1 multimodal tier -- the dedicated brain for image-bearing turns.
3202
+ * Optional: clear reverts to auto vision routing. Mirrors the CLI
3203
+ * (yf brain config --multimodal-*) so the panel reaches parity. */
3204
+ if (body.multimodal_clear === true) {
3205
+ delete cfg.multimodal;
3206
+ }
3207
+ else if (body.multimodal && typeof body.multimodal === 'object') {
3208
+ const b = body.multimodal;
3209
+ const provRaw = typeof b.provider === 'string'
3210
+ && ALL_BRAIN_PROVIDERS.includes(b.provider) ? b.provider : undefined;
3211
+ const modelRaw = typeof b.model === 'string' && b.model.trim() !== '' ? b.model.trim() : undefined;
3212
+ const provider = (provRaw ?? cfg.multimodal?.provider ?? cfg.frontier.provider);
3213
+ const model = modelRaw ?? cfg.multimodal?.model ?? cfg.frontier.model;
3214
+ cfg.multimodal = { provider, model };
3215
+ }
3179
3216
  await writeBrainConfig(cfg);
3180
3217
  /* Apply the brain mode LIVE: hot-swap the provider so flipping
3181
3218
  * Plan/Console takes effect on the very next turn -- no panel restart.
@@ -3193,6 +3230,192 @@ async function handleBrainConfigSet(req, res, ctx) {
3193
3230
  restart_needed_for: applied_live ? [] : ['anthropic_mode'],
3194
3231
  });
3195
3232
  }
3233
+ /* ============================================================
3234
+ * F4 brain federation -- settings-panel handlers (2026-06-21).
3235
+ *
3236
+ * The federation protocol (client/host/broker/CLI) already exists;
3237
+ * this exposes it in the panel so a non-terminal user can: connect
3238
+ * to another Forge's brain (child join), mint an authorize/route
3239
+ * code (parent invite), see status, leave, and toggle hosting. The
3240
+ * host answer-loop runs INSIDE the panel process while the toggle is
3241
+ * on; closing the panel (process exit) stops it.
3242
+ *
3243
+ * Routing is automatic: once federation.json is active, cost_router's
3244
+ * maybeApplyFederation() points every turn at the parent's brain, so
3245
+ * join only has to write the config + vault key (CLI parity).
3246
+ * ============================================================ */
3247
+ const federationHostRuntime = {
3248
+ running: false, controller: null,
3249
+ };
3250
+ /** Mask the child id in a fed handle for display: fed:<parent>:<id> -> fed:<parent>:<id6>... */
3251
+ function maskFedHandle(fedHandle) {
3252
+ const i = fedHandle.lastIndexOf(':');
3253
+ if (i < 0)
3254
+ return fedHandle;
3255
+ return fedHandle.slice(0, i + 1) + fedHandle.slice(i + 1, i + 7) + '...';
3256
+ }
3257
+ /** GET /api/forge/federation -- current child link, parent invite
3258
+ * capability, and host-loop state. */
3259
+ async function handleFederationGet(_req, res) {
3260
+ const { readFederationConfig, getFederationKey } = await import('../core/federation_config.js');
3261
+ const { resolveParentAuth } = await import('../llm/federation_host.js');
3262
+ const cfg = await readFederationConfig();
3263
+ const keyPresent = (await getFederationKey()) !== null;
3264
+ const auth = await resolveParentAuth();
3265
+ sendJson(res, 200, {
3266
+ ok: true,
3267
+ child: cfg ? {
3268
+ active: cfg.active,
3269
+ parent_handle: cfg.parent_handle,
3270
+ fed_handle: maskFedHandle(cfg.fed_handle),
3271
+ base_url: cfg.base_url,
3272
+ joined_at: cfg.joined_at,
3273
+ key_present: keyPresent,
3274
+ } : null,
3275
+ parent: { can_invite: !!auth, handle: auth ? auth.handle : null },
3276
+ host: { running: federationHostRuntime.running },
3277
+ });
3278
+ }
3279
+ /** POST /api/forge/federation/join { token } -- redeem a parent's
3280
+ * code; from now on every turn routes to the parent's brain. */
3281
+ async function handleFederationJoin(req, res) {
3282
+ const { setFederationKey, writeFederationConfig } = await import('../core/federation_config.js');
3283
+ const { HITO4_BASE_URL } = await import('../license/hito4_client.js');
3284
+ let body;
3285
+ try {
3286
+ body = JSON.parse(await readBody(req));
3287
+ }
3288
+ catch {
3289
+ sendJson(res, 400, { ok: false, error: 'invalid json body' });
3290
+ return;
3291
+ }
3292
+ const token = typeof body.token === 'string' ? body.token.trim() : '';
3293
+ if (!token) {
3294
+ sendJson(res, 400, { ok: false, error: 'falta el codigo de federacion' });
3295
+ return;
3296
+ }
3297
+ const baseUrl = (typeof body.base_url === 'string' && body.base_url ? body.base_url : HITO4_BASE_URL).replace(/\/+$/, '');
3298
+ let r;
3299
+ try {
3300
+ r = await fetch(baseUrl + '/v1/federation/redeem', {
3301
+ method: 'POST', headers: { 'content-type': 'application/json' },
3302
+ body: JSON.stringify({ token }),
3303
+ });
3304
+ }
3305
+ catch (e) {
3306
+ sendJson(res, 502, { ok: false, error: 'no pude contactar el broker: ' + (e instanceof Error ? e.message : String(e)) });
3307
+ return;
3308
+ }
3309
+ const j = await r.json().catch(() => ({}));
3310
+ if (!r.ok || !j.ok || !j.fed_handle || !j.fed_key || !j.parent_handle) {
3311
+ sendJson(res, r.status === 400 ? 400 : 502, {
3312
+ ok: false,
3313
+ error: (j.error || ('HTTP ' + r.status)) + (r.status === 400 ? ' (codigo invalido o vencido)' : ''),
3314
+ });
3315
+ return;
3316
+ }
3317
+ await setFederationKey(j.fed_key);
3318
+ await writeFederationConfig({
3319
+ active: true,
3320
+ parent_handle: j.parent_handle,
3321
+ fed_handle: j.fed_handle,
3322
+ base_url: baseUrl,
3323
+ joined_at: new Date().toISOString(),
3324
+ });
3325
+ sendJson(res, 200, { ok: true, parent_handle: j.parent_handle });
3326
+ }
3327
+ /** POST /api/forge/federation/leave -- drop the link; route locally. */
3328
+ async function handleFederationLeave(_req, res) {
3329
+ const { readFederationConfig, clearFederationConfig } = await import('../core/federation_config.js');
3330
+ const cfg = await readFederationConfig();
3331
+ await clearFederationConfig();
3332
+ sendJson(res, 200, { ok: true, was_federated: !!cfg, parent_handle: cfg ? cfg.parent_handle : null });
3333
+ }
3334
+ /** POST /api/forge/federation/invite { ttl_seconds? } -- PARENT mints a
3335
+ * single-use authorize/route code to lend its brain to a child. */
3336
+ async function handleFederationInvite(req, res) {
3337
+ const { resolveParentAuth } = await import('../llm/federation_host.js');
3338
+ const { HITO4_BASE_URL, signBearer } = await import('../license/hito4_client.js');
3339
+ const auth = await resolveParentAuth();
3340
+ if (!auth) {
3341
+ sendJson(res, 400, { ok: false, error: 'Necesitas una licencia activa (handle provisto) para prestar tu brain.' });
3342
+ return;
3343
+ }
3344
+ let body = {};
3345
+ try {
3346
+ body = JSON.parse(await readBody(req));
3347
+ }
3348
+ catch { /* empty body ok */ }
3349
+ const baseUrl = (typeof body.base_url === 'string' && body.base_url ? body.base_url : HITO4_BASE_URL).replace(/\/+$/, '');
3350
+ const payload = {};
3351
+ if (Number.isFinite(Number(body.ttl_seconds)))
3352
+ payload.ttl_seconds = Number(body.ttl_seconds);
3353
+ let r;
3354
+ try {
3355
+ r = await fetch(baseUrl + '/v1/federation/invite', {
3356
+ method: 'POST',
3357
+ headers: {
3358
+ 'authorization': 'Bearer ' + signBearer(auth.secret, auth.handle),
3359
+ 'x-yf-user-handle': auth.handle,
3360
+ 'content-type': 'application/json',
3361
+ },
3362
+ body: JSON.stringify(payload),
3363
+ });
3364
+ }
3365
+ catch (e) {
3366
+ sendJson(res, 502, { ok: false, error: 'no pude contactar el broker: ' + (e instanceof Error ? e.message : String(e)) });
3367
+ return;
3368
+ }
3369
+ const j = await r.json().catch(() => ({}));
3370
+ if (!r.ok || !j.ok || !j.token) {
3371
+ sendJson(res, 502, { ok: false, error: j.error || ('HTTP ' + r.status) });
3372
+ return;
3373
+ }
3374
+ sendJson(res, 200, { ok: true, token: j.token, expires_in: j.expires_in ?? 1800 });
3375
+ }
3376
+ /** POST /api/forge/federation/host { enabled } -- start/stop the answer
3377
+ * loop inside the panel process so children get this brain's replies. */
3378
+ async function handleFederationHost(req, res) {
3379
+ let body = {};
3380
+ try {
3381
+ body = JSON.parse(await readBody(req));
3382
+ }
3383
+ catch { /* default */ }
3384
+ const enable = body.enabled === true;
3385
+ if (enable) {
3386
+ if (federationHostRuntime.running) {
3387
+ sendJson(res, 200, { ok: true, running: true });
3388
+ return;
3389
+ }
3390
+ const { resolveParentAuth, runFederationHost } = await import('../llm/federation_host.js');
3391
+ const auth = await resolveParentAuth();
3392
+ if (!auth) {
3393
+ sendJson(res, 400, { ok: false, error: 'Necesitas una licencia activa para hospedar.' });
3394
+ return;
3395
+ }
3396
+ const controller = new AbortController();
3397
+ federationHostRuntime.controller = controller;
3398
+ federationHostRuntime.running = true;
3399
+ /* Fire-and-forget: the loop runs until aborted (panel close or toggle
3400
+ * off). Reset the flag if it ever returns/throws on its own. */
3401
+ void runFederationHost({ signal: controller.signal })
3402
+ .catch(() => undefined)
3403
+ .finally(() => {
3404
+ if (federationHostRuntime.controller === controller) {
3405
+ federationHostRuntime.running = false;
3406
+ federationHostRuntime.controller = null;
3407
+ }
3408
+ });
3409
+ sendJson(res, 200, { ok: true, running: true });
3410
+ return;
3411
+ }
3412
+ /* Disable. */
3413
+ if (federationHostRuntime.controller)
3414
+ federationHostRuntime.controller.abort();
3415
+ federationHostRuntime.running = false;
3416
+ federationHostRuntime.controller = null;
3417
+ sendJson(res, 200, { ok: true, running: false });
3418
+ }
3196
3419
  /** PND-064 -- POST /api/forge/panel-invoke. Paridad total de tools
3197
3420
  * en modo Plan: el MCP server local reenvia aca cada nac3_invoke
3198
3421
  * del subproceso `claude -p`. Corre el MISMO dispatcher que el