@jossuealcala/madre 0.4.1 → 0.5.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.
@@ -38,6 +38,55 @@ const definitions = [
38
38
  },
39
39
  ];
40
40
 
41
+ const OFFICIAL_DESTINATIONS = {
42
+ codex: { label: 'OpenAI', hosts: ['api.openai.com'], variable: 'OPENAI_BASE_URL' },
43
+ claude: { label: 'Anthropic', hosts: ['api.anthropic.com'], variable: 'ANTHROPIC_BASE_URL' },
44
+ };
45
+
46
+ const enabled = (value) => /^(1|true|yes)$/i.test(String(value ?? '').trim());
47
+
48
+ function customRoutesAllowed(agentId, env) {
49
+ const raw = String(env.PULSE_ALLOW_CUSTOM_AGENT_ENDPOINTS ?? '').trim().toLowerCase();
50
+ if (/^(1|true|all)$/.test(raw)) return true;
51
+ return raw.split(',').map((item) => item.trim()).includes(agentId);
52
+ }
53
+
54
+ function routeHost(value) {
55
+ try { return new URL(value).hostname.toLowerCase(); } catch { return null; }
56
+ }
57
+
58
+ // A CLI owns its network connection, but MADRE owns the document handed to that CLI. Name the
59
+ // actual destination without ever returning a credential, path or query string. Unknown base URLs
60
+ // are held at the door until the human explicitly trusts that agent's endpoint.
61
+ export function agentRouteFromEnvironment(agentId, env = process.env) {
62
+ if (agentId === 'claude' && enabled(env.CLAUDE_CODE_USE_BEDROCK)) {
63
+ return { kind: 'provider', destination: 'Amazon Bedrock', source: 'CLAUDE_CODE_USE_BEDROCK', host: null, custom: false, allowed: true };
64
+ }
65
+ if (agentId === 'claude' && enabled(env.CLAUDE_CODE_USE_VERTEX)) {
66
+ return { kind: 'provider', destination: 'Google Vertex AI', source: 'CLAUDE_CODE_USE_VERTEX', host: null, custom: false, allowed: true };
67
+ }
68
+ if (agentId === 'claude' && enabled(env.CLAUDE_CODE_USE_FOUNDRY)) {
69
+ return { kind: 'provider', destination: 'Microsoft Foundry', source: 'CLAUDE_CODE_USE_FOUNDRY', host: null, custom: false, allowed: true };
70
+ }
71
+
72
+ const official = OFFICIAL_DESTINATIONS[agentId];
73
+ if (!official) return null;
74
+ const raw = String(env[official.variable] ?? '').trim();
75
+ if (!raw) return { kind: 'official', destination: official.label, source: null, host: null, custom: false, allowed: true };
76
+ const host = routeHost(raw);
77
+ const isOfficial = Boolean(host && official.hosts.some((candidate) => host === candidate || host.endsWith(`.${candidate}`)));
78
+ if (isOfficial) return { kind: 'official', destination: official.label, source: official.variable, host, custom: false, allowed: true };
79
+ const allowed = customRoutesAllowed(agentId, env);
80
+ return {
81
+ kind: 'custom',
82
+ destination: host ? `custom endpoint · ${host}` : 'custom endpoint',
83
+ source: official.variable,
84
+ host,
85
+ custom: true,
86
+ allowed,
87
+ };
88
+ }
89
+
41
90
  async function executable(path) {
42
91
  try {
43
92
  await access(path, constants.X_OK);
@@ -99,6 +148,7 @@ export async function detectAgents({ env = process.env } = {}) {
99
148
  adapter: definition.adapter ?? null,
100
149
  path,
101
150
  version: path ? await readVersion(path, definition.versionArgs) : null,
151
+ route: agentRouteFromEnvironment(definition.id, env),
102
152
  };
103
153
  }));
104
154
  }
package/src/server.mjs CHANGED
@@ -47,6 +47,9 @@ import { applyKey, keyPlanFor } from './credentials.mjs';
47
47
  import { loadConfig as readConfig, updateConfig } from './config.mjs';
48
48
  import { Privacy, normalizeTerms, privacySettings } from './privacy.mjs';
49
49
  import { checkForUpdate, detectInstall, updateCommand, releaseUrl, applyCommand } from './updates.mjs';
50
+
51
+ // Where a person reads the module contract, in the language the room is speaking.
52
+ const SDK_PAGE = { es: 'https://madre.run/sdk/', en: 'https://madre.run/en/sdk/' };
50
53
  import { moduleUpdate } from './modules/updates.mjs';
51
54
  import { adoptStrays, chatIndex, chatLedger, createChat, deleteChat, isChatId, listChats, openChat as openChatIndex, projectFloor, renameChat, touchChat, MAIN_CHAT } from './chats.mjs';
52
55
  import { spawn } from 'node:child_process';
@@ -193,7 +196,7 @@ export async function createPulseServer({
193
196
  const install = detectInstall({ projectRoot: canonicalProjectRoot });
194
197
  async function versionView({ force = false } = {}) {
195
198
  const check = await checkForUpdate({ name: PACKAGE.name, current: PACKAGE.version, cacheFile: join(root, 'updates.json'), fetchImpl: reportFetch, enabled: updatesEnabled(), force });
196
- return { project: canonicalProjectRoot, ...check, name: PACKAGE.name, install, command: updateCommand(install, PACKAGE.name, check.latest ?? 'latest'), release: check.latest ? releaseUrl(PACKAGE.repository, check.latest) : null, envWins: process.env.PULSE_UPDATE_CHECK !== undefined };
199
+ return { project: canonicalProjectRoot, ...check, name: PACKAGE.name, install, command: updateCommand(install, PACKAGE.name, check.latest ?? 'latest'), release: check.latest ? releaseUrl(PACKAGE.repository, check.latest, roomLanguage()) : null, envWins: process.env.PULSE_UPDATE_CHECK !== undefined };
197
200
  }
198
201
  // Keeping the day's cache warm. MODULES is served from what is already on disk, so the looking
199
202
  // happens after the screen is answered, never in front of it: at most once an hour per room,
@@ -250,7 +253,7 @@ export async function createPulseServer({
250
253
  else if (!fifth.ready && index >= 0) { agents.splice(index, 1); changed = true; }
251
254
  if (room) {
252
255
  room.setInvoker(MADRE_ADAPTER, fifth.ready ? madreInvoker({ memory, ollama: () => ({ ...ollama, chatModel: ollama.madreModel ?? ollama.chatModel }), fetchImpl: reportFetch }) : null);
253
- if (changed) await room.record('agents.updated', { agents: agents.map((agent) => ({ id: agent.id, label: agent.label, detected: agent.detected, ready: agent.ready, version: agent.version, local: Boolean(agent.local) })), removed: fifth.ready ? [] : [MADRE_AGENT_ID], reason: fifth.ready ? `@madre is in the room · ${fifth.version}` : '@madre left the room: Ollama has no chat model running' });
256
+ if (changed) await room.record('agents.updated', { agents: agents.map((agent) => ({ id: agent.id, label: agent.label, detected: agent.detected, ready: agent.ready, version: agent.version, local: Boolean(agent.local), route: agent.route ?? null })), removed: fifth.ready ? [] : [MADRE_AGENT_ID], reason: fifth.ready ? `@madre is in the room · ${fifth.version}` : '@madre left the room: Ollama has no chat model running' });
254
257
  // That a local model is running is not the same as it being of use on this project, and
255
258
  // the second thing is the one a person needs told. Said once per model, in the room, with
256
259
  // the one button that settles it — the crew line above fires only when the crew CHANGES,
@@ -465,7 +468,7 @@ export async function createPulseServer({
465
468
  const index = agents.findIndex((item) => item.id === next.id);
466
469
  if (index === -1) { agents.push(next); changed = true; continue; }
467
470
  const before = agents[index];
468
- if (before.detected !== next.detected || before.ready !== next.ready || before.version !== next.version || before.path !== next.path) {
471
+ if (before.detected !== next.detected || before.ready !== next.ready || before.version !== next.version || before.path !== next.path || JSON.stringify(before.route ?? null) !== JSON.stringify(next.route ?? null)) {
469
472
  agents[index] = { ...before, ...next };
470
473
  changed = true;
471
474
  }
@@ -473,7 +476,7 @@ export async function createPulseServer({
473
476
  await refreshSessions();
474
477
  if (changed && announce) {
475
478
  await room.record('agents.updated', {
476
- agents: agents.map((agent) => ({ id: agent.id, label: agent.label, detected: agent.detected, ready: agent.ready, version: agent.version, local: Boolean(agent.local) })),
479
+ agents: agents.map((agent) => ({ id: agent.id, label: agent.label, detected: agent.detected, ready: agent.ready, version: agent.version, local: Boolean(agent.local), route: agent.route ?? null })),
477
480
  removed: [],
478
481
  reason: 'the room looked again for the agents on this computer',
479
482
  });
@@ -846,7 +849,7 @@ export async function createPulseServer({
846
849
  response.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
847
850
  return response.end(html);
848
851
  }
849
- if (request.method === 'GET' && ['/app.js', '/brands.js', '/troubleshooting.js', '/inquiry.js', '/i18n.js', '/es.js', '/resay.js'].includes(url.pathname)) {
852
+ if (request.method === 'GET' && ['/app.js', '/brands.js', '/troubleshooting.js', '/inquiry.js', '/i18n.js', '/es.js', '/resay.js', '/syntax.js'].includes(url.pathname)) {
850
853
  const js = await readFile(join(publicDirectory, url.pathname.slice(1)));
851
854
  response.writeHead(200, { 'content-type': 'text/javascript; charset=utf-8' });
852
855
  return response.end(js);
@@ -1474,7 +1477,7 @@ export async function createPulseServer({
1474
1477
  if (request.method === 'GET' && url.pathname === '/api/extensions') {
1475
1478
  const extensions = await describeModules(await moduleContext());
1476
1479
  void warmModuleUpdates(extensions);
1477
- return sendJson(response, 200, { installing, extensions, failures: loadFailures, folders: moduleFolders({ stateRoot: root, projectRoot: canonicalProjectRoot }), sdk: 'https://github.com/jossuealcacao-exe/madre/blob/main/docs/SDK.md' });
1480
+ return sendJson(response, 200, { installing, extensions, failures: loadFailures, folders: moduleFolders({ stateRoot: root, projectRoot: canonicalProjectRoot }), sdk: SDK_PAGE[roomLanguage()] ?? SDK_PAGE.es });
1478
1481
  }
1479
1482
  // The human installs a module file an agent wrote (or they did): checked first, then copied into the chosen folder.
1480
1483
  if (request.method === 'POST' && url.pathname === '/api/extensions/install-file') {
package/src/updates.mjs CHANGED
@@ -51,7 +51,15 @@ export function applyCommand({ install, name, version, port, projectRoot }) {
51
51
  }
52
52
  }
53
53
 
54
- export function releaseUrl(repository, version) {
54
+ // Where a person reads what a version brought. The product's own house, in the language the room
55
+ // is speaking — a release page on a code host is written for people who already work there, and
56
+ // whoever pressed this button only wanted to know what changed. The repository stays the fallback
57
+ // for a build with no house to point at.
58
+ export const CHANGES_PAGE = { es: 'https://madre.run/cambios/', en: 'https://madre.run/en/changelog/' };
59
+
60
+ export function releaseUrl(repository, version, language = 'es') {
61
+ const page = CHANGES_PAGE[language] ?? CHANGES_PAGE.es;
62
+ if (page) return version ? `${page}#v${version}` : page;
55
63
  const url = String(repository?.url ?? repository ?? '').replace(/^git\+/, '').replace(/\.git$/, '');
56
64
  return url ? `${url}/releases/tag/v${version}` : null;
57
65
  }