@jossuealcala/madre 0.4.0 → 0.4.1

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.
@@ -10,6 +10,12 @@
10
10
  import { createHash } from 'node:crypto';
11
11
  import { homedir, platform, arch, release } from 'node:os';
12
12
  import { diagnose } from '../public/troubleshooting.js';
13
+ import { language } from './i18n.mjs';
14
+
15
+ // The product's own house. The collector lives under it too, so the line a human reads in the
16
+ // outbound log names the product instead of a personal endpoint on someone's cloud account.
17
+ export const HOME_SITE = 'https://madre.run';
18
+ export const REPORT_FORM = { es: `${HOME_SITE}/reportar`, en: `${HOME_SITE}/report` };
13
19
 
14
20
  export const REPORT_WINDOW_MS = 24 * 3600 * 1000;
15
21
  const MAX_ERROR_CHARS = 4000;
@@ -71,7 +77,7 @@ export class ErrorSentinel {
71
77
  this.#save = save;
72
78
  }
73
79
 
74
- settings() { return { ...this.#settings, canSend: Boolean(this.#settings.reportUrl), repo: repoFromPackage(this.#pkg) }; }
80
+ settings() { return { ...this.#settings, canSend: Boolean(this.#settings.reportUrl), repo: repoFromPackage(this.#pkg), home: HOME_SITE }; }
75
81
  async setSettings(patch = {}) {
76
82
  if (typeof patch.autoReport === 'boolean') this.#settings.autoReport = patch.autoReport;
77
83
  if (typeof patch.reportUrl === 'string') this.#settings.reportUrl = patch.reportUrl.trim();
@@ -174,6 +180,18 @@ export class ErrorSentinel {
174
180
  return `https://github.com/${repo}/issues/new?${new URLSearchParams({ title, body, labels: 'sentinel' })}`;
175
181
  }
176
182
 
183
+ // Where a human goes to say something. GitHub is the better home for a report — it is
184
+ // searchable, it deduplicates, and the answer helps whoever hits it next — but it asks for an
185
+ // account first, and somebody who arrived from a post in Spanish will not make one to say "it
186
+ // does not detect Codex". They get the form; the other road stays one click away, both ways.
187
+ formUrl({ about = '' } = {}) {
188
+ const base = REPORT_FORM[language()] ?? REPORT_FORM.en;
189
+ const env = this.environment();
190
+ const query = new URLSearchParams({ v: env.madre, node: env.node, os: env.platform });
191
+ if (about) query.set('about', about.slice(0, 120));
192
+ return `${base}?${query}`;
193
+ }
194
+
177
195
  feedbackUrl({ about = '' } = {}) {
178
196
  const repo = repoFromPackage(this.#pkg);
179
197
  if (!repo) return null;
package/src/server.mjs CHANGED
@@ -18,7 +18,7 @@ import { moduleById, describeModules, findModuleRoute, toolsForTurn as modulesTo
18
18
  import { madreAgent, madreInvoker, MADRE_AGENT_ID, MADRE_ADAPTER } from './adapters/madre.mjs';
19
19
  import { exportDataset, readiness as datasetReadiness } from './dataset.mjs';
20
20
  import { OutboundLog, outboundView, DEFAULT_REPORT_URL } from './outbound.mjs';
21
- import { setLanguage as setRoomLanguage, t } from './i18n.mjs';
21
+ import { language as roomLanguage, setLanguage as setRoomLanguage, t } from './i18n.mjs';
22
22
 
23
23
  const PACKAGE = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8').catch(() => '{}'));
24
24
  let crashHandlersInstalled = false;
@@ -860,8 +860,11 @@ export async function createPulseServer({
860
860
  return sendJson(response, 200, {
861
861
  projectRoot,
862
862
  platform: process.platform,
863
- // What language this machine set MADRE to, for a browser that has never chosen.
864
- language: (await readConfig(root)).language === 'en' ? 'en' : 'es',
863
+ // What language this machine set MADRE to, for a browser that has never chosen. It is
864
+ // the language the room is SPEAKING, not the one written in the file: PULSE_LANGUAGE
865
+ // wins over the config here as it does everywhere else, and reading the file again
866
+ // would hand the page a different answer from the one the server itself is using.
867
+ language: roomLanguage(),
865
868
  // Which conversation this page is looking at, and the others it could open.
866
869
  chats: await listChats(roomDir),
867
870
  agents: agents.map((agent) => ({ ...agent, login: loginPlanFor(agent), install: installPlanFor(agent), key: keyPlanFor(agent.id), ...(accountNoteFor(agent.id) ?? {}) })),
@@ -980,7 +983,7 @@ export async function createPulseServer({
980
983
  const designationOk = (given) => typeof given === 'string' && given.trim().toLowerCase() === basename(canonicalProjectRoot).toLowerCase();
981
984
  // Sentinel: reports, settings, the manual road (a prefilled issue) and the automatic one.
982
985
  if (request.method === 'GET' && url.pathname === '/api/sentinel') {
983
- return sendJson(response, 200, { reports: sentinel.reports(), settings: sentinel.settings(), environment: sentinel.environment(), feedbackUrl: sentinel.feedbackUrl() });
986
+ return sendJson(response, 200, { reports: sentinel.reports(), settings: sentinel.settings(), environment: sentinel.environment(), feedbackUrl: sentinel.feedbackUrl(), formUrl: sentinel.formUrl() });
984
987
  }
985
988
  if (request.method === 'POST' && url.pathname === '/api/sentinel/settings') {
986
989
  const patch = await body(request).catch(() => ({}));
@@ -991,8 +994,13 @@ export async function createPulseServer({
991
994
  const sentinelMatch = url.pathname.match(/^\/api\/sentinel\/([a-f0-9]{10})\/(issue|send)$/);
992
995
  if (sentinelMatch && request.method === (sentinelMatch[2] === 'issue' ? 'GET' : 'POST')) {
993
996
  if (sentinelMatch[2] === 'issue') {
994
- const issue = sentinel.issueUrl(sentinelMatch[1]);
995
- return issue ? sendJson(response, 200, { url: issue }) : sendJson(response, 404, { error: t('No such report, or no repository to file it in.') });
997
+ // Both roads, always: the issue for whoever has an account, the form for whoever does
998
+ // not. Nobody should hit a login wall while holding a bug report. A report that does
999
+ // not exist still gets neither: the form does not need the report to be built, so it
1000
+ // would otherwise hand out a link for nothing.
1001
+ const known = sentinel.reports().some((one) => one.id === sentinelMatch[1]);
1002
+ const issue = known ? sentinel.issueUrl(sentinelMatch[1]) : null;
1003
+ return known ? sendJson(response, 200, { url: issue, form: sentinel.formUrl({ about: sentinelMatch[1] }) }) : sendJson(response, 404, { error: t('No such report, or no repository to file it in.') });
996
1004
  }
997
1005
  const outcome = await sentinel.send(sentinelMatch[1]);
998
1006
  return sendJson(response, outcome.ok ? 200 : (outcome.status === 404 || outcome.status === 412 ? outcome.status : 502), outcome);