palabre 0.8.0 → 0.8.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.
package/README.md CHANGED
@@ -7,13 +7,13 @@
7
7
  <a href="https://palab.re"><img src="https://img.shields.io/badge/docs-palab.re-18181B?logo=netlify&logoColor=7C3AED" alt="Documentation"></a>
8
8
  </p>
9
9
 
10
- ![PALABRE](docs/assets/palabre-logo-text-og.png)
10
+ ![Palabre CLI orchestrating a debate between AI agents](docs/assets/palabre-cli-orchestre-debates-between-AI-agents.png)
11
11
 
12
12
  [English](#english) | [Français](#français)
13
13
 
14
14
  ## English
15
15
 
16
- PALABRE is a CLI/TUI orchestrator that lets multiple AI agents installed on your machine work together: Claude Code, Codex CLI, Gemini CLI, Antigravity CLI, OpenCode, Mistral Vibe, and Ollama.
16
+ PALABRE is a CLI/TUI orchestrator that lets multiple AI agents installed on your machine work together: Claude Code, Codex CLI, Antigravity CLI, OpenCode, Mistral Vibe, and Ollama.
17
17
 
18
18
  It does not replace your tools: it drives them. You keep your subscriptions, default models, terminal habits, and local files. PALABRE can run a debate between two agents or an Ask request where several agents answer independently before a comparative summary. It then exports the session as Markdown.
19
19
 
@@ -37,7 +37,6 @@ palabre --help
37
37
  ### Quick Start
38
38
 
39
39
  ```bash
40
- palabre init
41
40
  palabre doctor
42
41
  palabre
43
42
  ```
@@ -54,16 +53,15 @@ palabre codex-claude "Preview" --context src --show-prompt
54
53
  palabre context scan src docs --json
55
54
  ```
56
55
 
57
- In an interactive terminal, Palabre uses the TUI by default. `palabre` opens the home screen, `/ask` switches from debate to independent answers, `/agents` and `/roles` help you choose the active setup, and `--terminal` forces the older raw rendering suitable for logs.
56
+ In an interactive terminal, Palabre uses the TUI by default. `palabre` opens the home screen, creates the global config on first launch when needed, and refreshes detected known agents before showing the UI. `/ask` switches from debate to independent answers, `/agents` and `/roles` help you choose the active setup, `/history` shows recent exports, and `/home` returns to the home screen. `--terminal` forces the older raw rendering suitable for logs. `palabre init` remains available for explicit setup, especially with `--local`.
58
57
 
59
58
  ### Supported Agents
60
59
 
61
60
  - Claude Code via `claude --print`
62
61
  - Codex CLI via `codex exec`
63
- - Gemini CLI via `gemini --prompt -`
64
62
  - Antigravity CLI via `agy --print` in a pseudo-terminal
65
63
  - OpenCode via `opencode run`
66
- - Mistral Vibe via `vibe --output text --agent plan --trust --prompt`
64
+ - Mistral Vibe via `vibe --output text --trust --prompt`
67
65
  - Ollama via the local HTTP API
68
66
 
69
67
  PALABRE does not list models: they change often and depend on each CLI or user account. `--model-a`, `--model-b`, and `--summary-model` simply pass the raw value to the selected agent.
@@ -80,7 +78,7 @@ The NDJSON v1 stream is treated as a public integration API. Compatible addition
80
78
 
81
79
  ### Skill for AI agents
82
80
 
83
- PALABRE ships a ready-to-use skill that teaches an AI agent when and how to run Palabre sessions. It follows the open [agentskills.io](https://agentskills.io) standard, so it is portable across Hermes Agent, Claude, Codex, Gemini CLI, and any skills-compatible agent.
81
+ PALABRE ships a ready-to-use skill that teaches an AI agent when and how to run Palabre sessions. It follows the open [agentskills.io](https://agentskills.io) standard, so it is portable across Hermes Agent, Claude, Codex, and any skills-compatible agent.
84
82
 
85
83
  Install it in **Hermes Agent**:
86
84
 
@@ -94,7 +92,7 @@ The skill is versioned under [skills/palabre](./skills/palabre).
94
92
 
95
93
  ### Privacy
96
94
 
97
- PALABRE runs locally and does not send data to a PALABRE-owned server. Data sent to agents depends on the tools you use: check the privacy policies of Claude Code, Codex CLI, Gemini CLI, Antigravity CLI, OpenCode, Mistral Vibe, Ollama, or any custom agent you configure.
95
+ PALABRE runs locally and does not send data to a PALABRE-owned server. Data sent to agents depends on the tools you use: check the privacy policies of Claude Code, Codex CLI, Antigravity CLI, OpenCode, Mistral Vibe, Ollama, or any custom agent you configure.
98
96
 
99
97
  If an agent fails during the debate or final summary, PALABRE keeps the partial Markdown export with an interruption section whenever possible.
100
98
 
@@ -119,9 +117,11 @@ Public roadmap: [docs/guide/fr/roadmap.md](./docs/guide/fr/roadmap.md). Changes:
119
117
 
120
118
  MIT. See [LICENSE](./LICENSE).
121
119
 
120
+ ![PALABRE](docs/assets/palabre-logo-text-og.png)
121
+
122
122
  ## Français
123
123
 
124
- PALABRE est un orchestrateur CLI/TUI qui fait travailler plusieurs agents IA installés sur votre machine : Claude Code, Codex CLI, Gemini CLI, Antigravity CLI, OpenCode, Mistral Vibe et Ollama.
124
+ PALABRE est un orchestrateur CLI/TUI qui fait travailler plusieurs agents IA installés sur votre machine : Claude Code, Codex CLI, Antigravity CLI, OpenCode, Mistral Vibe et Ollama.
125
125
 
126
126
  Il ne remplace pas vos outils : il les pilote. Vous gardez vos abonnements, vos modèles par défaut, vos habitudes de terminal et vos fichiers en local. PALABRE peut lancer un débat entre deux agents ou une demande Ask où plusieurs agents répondent indépendamment avant une synthèse comparative. Il exporte ensuite la session en Markdown.
127
127
 
@@ -145,7 +145,6 @@ palabre --help
145
145
  ### Démarrage rapide
146
146
 
147
147
  ```bash
148
- palabre init
149
148
  palabre doctor
150
149
  palabre
151
150
  ```
@@ -162,16 +161,15 @@ palabre codex-claude "Preview" --context src --show-prompt
162
161
  palabre context scan src docs --json
163
162
  ```
164
163
 
165
- Dans un terminal interactif, Palabre utilise l'interface TUI par défaut. `palabre` ouvre l'accueil, `/ask` passe du débat aux réponses indépendantes, `/agents` et `/roles` aident à choisir la configuration courante, et `--terminal` force l'ancien rendu brut adapté aux logs.
164
+ Dans un terminal interactif, Palabre utilise l'interface TUI par défaut. `palabre` ouvre l'accueil, crée la config globale au premier lancement si nécessaire, et rafraîchit les agents connus détectés avant d'afficher l'interface. `/ask` passe du débat aux réponses indépendantes, `/agents` et `/roles` aident à choisir la configuration courante, `/history` affiche les derniers exports, et `/home` revient à l'accueil. `--terminal` force l'ancien rendu brut adapté aux logs. `palabre init` reste disponible pour un setup explicite, notamment avec `--local`.
166
165
 
167
166
  ### Agents supportés
168
167
 
169
168
  - Claude Code via `claude --print`
170
169
  - Codex CLI via `codex exec`
171
- - Gemini CLI via `gemini --prompt -`
172
170
  - Antigravity CLI via `agy --print` en pseudo-terminal
173
171
  - OpenCode via `opencode run`
174
- - Mistral Vibe via `vibe --output text --agent plan --trust --prompt`
172
+ - Mistral Vibe via `vibe --output text --trust --prompt`
175
173
  - Ollama via l'API locale HTTP
176
174
 
177
175
  PALABRE ne liste pas les modèles : ils changent souvent et dépendent de chaque CLI ou compte utilisateur. `--model-a`, `--model-b` et `--summary-model` transmettent simplement la valeur brute à l'agent concerné.
@@ -188,7 +186,7 @@ Le flux NDJSON v1 est traité comme une API publique d'intégration. Les ajouts
188
186
 
189
187
  ### Skill pour agents IA
190
188
 
191
- PALABRE fournit un skill prêt à l'emploi qui apprend à un agent IA quand et comment lancer des sessions Palabre. Il suit le standard ouvert [agentskills.io](https://agentskills.io) : il est donc portable entre Hermes Agent, Claude, Codex, Gemini CLI et tout agent compatible skills.
189
+ PALABRE fournit un skill prêt à l'emploi qui apprend à un agent IA quand et comment lancer des sessions Palabre. Il suit le standard ouvert [agentskills.io](https://agentskills.io) : il est donc portable entre Hermes Agent, Claude, Codex et tout agent compatible skills.
192
190
 
193
191
  Installation dans **Hermes Agent** :
194
192
 
@@ -202,7 +200,7 @@ Le skill est versionné dans [skills/palabre](./skills/palabre).
202
200
 
203
201
  ### Confidentialité
204
202
 
205
- PALABRE tourne localement et n'envoie aucune donnée à un serveur appartenant à PALABRE. Les données envoyées aux agents dépendent des outils que vous utilisez : vérifiez les politiques de confidentialité de Claude Code, Codex CLI, Gemini CLI, Antigravity CLI, OpenCode, Mistral Vibe, Ollama ou de tout autre agent configuré.
203
+ PALABRE tourne localement et n'envoie aucune donnée à un serveur appartenant à PALABRE. Les données envoyées aux agents dépendent des outils que vous utilisez : vérifiez les politiques de confidentialité de Claude Code, Codex CLI, Antigravity CLI, OpenCode, Mistral Vibe, Ollama ou de tout autre agent configuré.
206
204
 
207
205
  Si un agent échoue pendant le débat ou la synthèse, PALABRE conserve l'export Markdown partiel avec une section d'interruption quand c'est possible.
208
206
 
@@ -82,7 +82,7 @@ export class CliPtyAdapter {
82
82
  return;
83
83
  }
84
84
  const content = cleanTerminalOutput(output);
85
- if (exitCode && exitCode !== 0 && !content) {
85
+ if (exitCode && exitCode !== 0) {
86
86
  reject(createPtyExitError(this.name, exitCode, output));
87
87
  return;
88
88
  }
@@ -4,7 +4,7 @@ import { formatAgentPrompt } from "../prompt.js";
4
4
  import { DEFAULT_MAX_OUTPUT_BYTES, DEFAULT_TIMEOUT_MS, withModelArgs } from "./cli-shared.js";
5
5
  import { cleanTerminalOutput } from "./terminal.js";
6
6
  /**
7
- * Adapter pour les CLIs batch (Codex, Claude, Gemini…).
7
+ * Adapter pour les CLIs batch (Codex, Claude, OpenCode, Vibe...).
8
8
  * Lance un sous-processus, injecte le prompt via stdin ou argument, capture stdout.
9
9
  * Garantit : rejection des sorties vides (sauf `allowEmptyOutput`), des timeouts et des exit codes non nuls sans stdout.
10
10
  */
@@ -47,7 +47,8 @@ export class CliAdapter {
47
47
  ? [...baseArgs, renderedPrompt]
48
48
  : baseArgs;
49
49
  return new Promise((resolve, reject) => {
50
- const child = spawn(this.config.command, args, {
50
+ const spawnCommand = shellCommandForSpawn(this.config.command, args, this.config.shell ?? false);
51
+ const child = spawn(spawnCommand.command, spawnCommand.args, {
51
52
  stdio: ["pipe", "pipe", "pipe"],
52
53
  shell: this.config.shell ?? false
53
54
  });
@@ -147,7 +148,7 @@ export class CliAdapter {
147
148
  }));
148
149
  });
149
150
  const finishFromExitCode = (code) => {
150
- if (code && code !== 0 && !stdout.trim()) {
151
+ if (code && code !== 0) {
151
152
  finish(createCliExitError(this.name, code, stderr));
152
153
  return;
153
154
  }
@@ -307,6 +308,35 @@ function clipLine(value, maxLength) {
307
308
  ? value
308
309
  : `${value.slice(0, maxLength - 1)}…`;
309
310
  }
311
+ function shellCommandForSpawn(command, args, shell) {
312
+ if (!shell) {
313
+ return { command, args };
314
+ }
315
+ if (process.platform !== "win32") {
316
+ return {
317
+ command: [command, ...args].map(quotePosixShellArg).join(" "),
318
+ args: []
319
+ };
320
+ }
321
+ return {
322
+ command: [command, ...args].map(quoteWindowsShellArg).join(" "),
323
+ args: []
324
+ };
325
+ }
326
+ function quoteWindowsShellArg(value) {
327
+ if (value.length === 0) {
328
+ return "\"\"";
329
+ }
330
+ return `"${value
331
+ .replace(/(\\*)"/g, "$1$1\\\"")
332
+ .replace(/(\\+)$/g, "$1$1")}"`;
333
+ }
334
+ function quotePosixShellArg(value) {
335
+ if (value.length === 0) {
336
+ return "''";
337
+ }
338
+ return `'${value.replace(/'/g, "'\\''")}'`;
339
+ }
310
340
  function cancelledError(adapterName) {
311
341
  return new AdapterError("cancelled", adapterName, `${adapterName} cancelled by user.`);
312
342
  }
@@ -116,7 +116,7 @@ export class OllamaAdapter {
116
116
  throw new AdapterError("model-unavailable", this.name, `Modele Ollama indisponible: ${this.config.model}. Modeles detectes: ${models.join(", ") || "aucun"}. ` +
117
117
  "Utilise --pull-models ou autoPullModel: true pour autoriser le telechargement.", { model: this.config.model, availableModels: models });
118
118
  }
119
- process.stdout.write(`\n[ollama] Modele absent, telechargement: ${this.config.model}\n`);
119
+ process.stderr.write(`\n[ollama] Modele absent, telechargement: ${this.config.model}\n`);
120
120
  await this.pullModel(baseUrl);
121
121
  if (!(await this.isModelAvailable(baseUrl))) {
122
122
  throw new AdapterError("model-pull-failed", this.name, `Le modele Ollama ${this.config.model} reste indisponible apres telechargement.`);
@@ -6,7 +6,6 @@
6
6
  const KNOWN_CLI_AGENTS = [
7
7
  { configKey: "codex", commandAliases: ["codex"], discoveryKey: "codex" },
8
8
  { configKey: "claude", commandAliases: ["claude"], discoveryKey: "claude" },
9
- { configKey: "gemini", commandAliases: ["gemini"], discoveryKey: "gemini" },
10
9
  { configKey: "antigravity", commandAliases: ["agy", "antigravity"], discoveryKey: "antigravity" },
11
10
  { configKey: "opencode", commandAliases: ["opencode"], discoveryKey: "opencode" },
12
11
  { configKey: "vibe", commandAliases: ["vibe"], discoveryKey: "vibe" }
@@ -37,7 +36,7 @@ export function detectionForCommand(command, discovery) {
37
36
  }
38
37
  /**
39
38
  * Liste les clés d'agents connus effectivement détectés localement, dans
40
- * l'ordre canonique (`codex`, `claude`, `gemini`, `antigravity`, `opencode`, `vibe`,
39
+ * l'ordre canonique (`codex`, `claude`, `antigravity`, `opencode`, `vibe`,
41
40
  * puis `ollama-local`).
42
41
  */
43
42
  export function detectedAgentNames(discovery) {
package/dist/args.js CHANGED
@@ -45,8 +45,8 @@ const FLAG_SPECS = {
45
45
  turns: { arity: "single" },
46
46
  renderer: { arity: "single" },
47
47
  // Valeurs multiples.
48
- agents: { arity: "multi", max: 4 },
49
- "ask-agents": { arity: "multi", max: 4 },
48
+ agents: { arity: "multi" },
49
+ "ask-agents": { arity: "multi" },
50
50
  "set-defaults": { arity: "multi", max: 2 },
51
51
  files: { arity: "multi" },
52
52
  context: { arity: "multi" }
@@ -67,6 +67,8 @@ const COMMANDS = new Set([
67
67
  "agents",
68
68
  "preset",
69
69
  "presets",
70
+ "history",
71
+ "historique",
70
72
  "context"
71
73
  ]);
72
74
  /**
package/dist/config.js CHANGED
@@ -54,23 +54,6 @@ export const exampleConfig = {
54
54
  role: "reviewer",
55
55
  tier: "primary"
56
56
  },
57
- gemini: {
58
- type: "cli",
59
- command: "gemini",
60
- args: [
61
- "--output-format",
62
- "text",
63
- "--approval-mode",
64
- "plan",
65
- "--skip-trust",
66
- "--prompt",
67
- "-"
68
- ],
69
- promptMode: "stdin",
70
- shell: process.platform === "win32",
71
- role: "reviewer",
72
- tier: "primary"
73
- },
74
57
  antigravity: {
75
58
  type: "cli-pty",
76
59
  command: "agy",
@@ -102,8 +85,6 @@ export const exampleConfig = {
102
85
  args: [
103
86
  "--output",
104
87
  "text",
105
- "--agent",
106
- "plan",
107
88
  "--trust",
108
89
  "--prompt"
109
90
  ],
@@ -234,11 +215,34 @@ export function syncDetectedAgents(config, discovery) {
234
215
  const discoveredConfig = createConfigFromDiscovery(discovery);
235
216
  const missingAgents = detectedAgentNames(discovery).filter((agentName) => !config.agents[agentName]);
236
217
  applyDetectedCommands(config, discovery);
218
+ migrateKnownAgentDefaults(config);
237
219
  for (const agentName of missingAgents) {
238
220
  config.agents[agentName] = discoveredConfig.agents[agentName];
239
221
  }
240
222
  return missingAgents;
241
223
  }
224
+ function migrateKnownAgentDefaults(config) {
225
+ migrateVibePlanAgent(config);
226
+ }
227
+ function migrateVibePlanAgent(config) {
228
+ const agent = config.agents.vibe;
229
+ if (agent?.type !== "cli" || !isLegacyVibePlanArgs(agent.args)) {
230
+ return;
231
+ }
232
+ agent.args = ["--output", "text", "--trust", "--prompt"];
233
+ }
234
+ function isLegacyVibePlanArgs(args) {
235
+ return JSON.stringify(args) === JSON.stringify(["--output", "text", "--agent", "plan", "--trust", "--prompt"]);
236
+ }
237
+ export function syncDetectedAgentsDetailed(config, discovery) {
238
+ const before = JSON.stringify(config.agents);
239
+ const addedAgents = syncDetectedAgents(config, discovery);
240
+ const changed = JSON.stringify(config.agents) !== before;
241
+ return {
242
+ addedAgents,
243
+ changed
244
+ };
245
+ }
242
246
  export function syncOllamaModel(config, discovery) {
243
247
  const agent = config.agents["ollama-local"];
244
248
  if (agent?.type !== "ollama" || discovery.ollama.models.length === 0) {
@@ -279,7 +283,7 @@ function chooseDefaultOllamaModel(discovery) {
279
283
  return discovery.ollama.models[0] ?? DEFAULT_OLLAMA_MODEL;
280
284
  }
281
285
  function chooseDefaultSummaryAgent(pair) {
282
- for (const preferred of ["claude", "codex", "antigravity", "vibe", "gemini"]) {
286
+ for (const preferred of ["claude", "codex", "antigravity", "vibe"]) {
283
287
  if (pair.includes(preferred)) {
284
288
  return preferred;
285
289
  }
@@ -305,16 +309,12 @@ function chooseDefaultPair(discovery) {
305
309
  if (discovery.antigravity.available && discovery.ollama.available) {
306
310
  return ["antigravity", "ollama-local"];
307
311
  }
308
- if (discovery.gemini.available && discovery.ollama.available) {
309
- return ["gemini", "ollama-local"];
310
- }
311
312
  const cliAgents = [
312
313
  discovery.codex.available ? "codex" : undefined,
313
314
  discovery.claude.available ? "claude" : undefined,
314
315
  discovery.antigravity.available ? "antigravity" : undefined,
315
316
  discovery.opencode.available ? "opencode" : undefined,
316
- discovery.vibe.available ? "vibe" : undefined,
317
- discovery.gemini.available ? "gemini" : undefined
317
+ discovery.vibe.available ? "vibe" : undefined
318
318
  ].filter((agent) => Boolean(agent));
319
319
  if (cliAgents.length >= 2) {
320
320
  return [cliAgents[0], cliAgents[1]];
package/dist/context.js CHANGED
@@ -80,7 +80,11 @@ async function addContextPaths(paths, cwd, state) {
80
80
  const uniquePaths = [...new Set(paths.map((item) => item.trim()).filter(Boolean))];
81
81
  for (const inputPath of uniquePaths) {
82
82
  const absolutePath = path.resolve(cwd, inputPath);
83
- const fileStat = await stat(absolutePath);
83
+ const fileStat = await stat(absolutePath).catch(() => undefined);
84
+ if (!fileStat) {
85
+ state.warnings.push(state.messages.context.ignoredNotFileOrDirectory(inputPath));
86
+ continue;
87
+ }
84
88
  if (fileStat.isFile()) {
85
89
  await addContextFile(absolutePath, cwd, state);
86
90
  continue;
package/dist/discovery.js CHANGED
@@ -7,10 +7,9 @@ import { executableExtensions } from "./exec.js";
7
7
  * Antigravity est exposé selon les installations sous `agy` ou `antigravity`.
8
8
  */
9
9
  export async function discoverLocalTools() {
10
- const [codex, claude, gemini, antigravity, opencode, vibe, ollamaCommand] = await Promise.all([
10
+ const [codex, claude, antigravity, opencode, vibe, ollamaCommand] = await Promise.all([
11
11
  detectCommand("codex"),
12
12
  detectFirstCommand(process.platform === "win32" ? ["claude.exe", "claude"] : ["claude"]),
13
- detectCommand("gemini"),
14
13
  detectFirstCommand(["agy", "antigravity"]),
15
14
  detectCommand("opencode"),
16
15
  detectCommand("vibe"),
@@ -20,7 +19,6 @@ export async function discoverLocalTools() {
20
19
  return {
21
20
  codex,
22
21
  claude,
23
- gemini,
24
22
  antigravity,
25
23
  opencode,
26
24
  vibe,
package/dist/doctor.js CHANGED
@@ -43,7 +43,6 @@ export async function runDoctor(explicitConfigPath, plain = false, explicitLangu
43
43
  lines.push(info(t.doctor.localTools, "tools"));
44
44
  lines.push(formatCommand("Codex CLI", discovery.codex.available, discovery.codex.command, discovery.codex.path, t));
45
45
  lines.push(formatCommand("Claude CLI", discovery.claude.available, discovery.claude.command, discovery.claude.path, t));
46
- lines.push(formatCommand("Gemini CLI", discovery.gemini.available, discovery.gemini.command, discovery.gemini.path, t));
47
46
  lines.push(formatCommand("Antigravity CLI", discovery.antigravity.available, discovery.antigravity.command, discovery.antigravity.path, t));
48
47
  lines.push(formatCommand("OpenCode CLI", discovery.opencode.available, discovery.opencode.command, discovery.opencode.path, t));
49
48
  lines.push(formatCommand("Mistral Vibe CLI", discovery.vibe.available, discovery.vibe.command, discovery.vibe.path, t));
@@ -0,0 +1,85 @@
1
+ import { readdir, readFile, stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+ const maxHeaderBytes = 12_000;
4
+ export async function listHistoryEntries(outputDir, limit = 10) {
5
+ const resolved = path.resolve(outputDir);
6
+ let entries;
7
+ try {
8
+ entries = await readdir(resolved, { withFileTypes: true });
9
+ }
10
+ catch {
11
+ return [];
12
+ }
13
+ const markdownFiles = entries
14
+ .filter((entry) => entry.isFile() && /\.(debate|ask)\.md$/i.test(entry.name))
15
+ .map((entry) => path.join(resolved, entry.name));
16
+ const history = await Promise.all(markdownFiles.map(readHistoryFile));
17
+ return history
18
+ .filter((entry) => Boolean(entry))
19
+ .sort((left, right) => right.mtimeMs - left.mtimeMs)
20
+ .slice(0, limit);
21
+ }
22
+ async function readHistoryFile(filePath) {
23
+ try {
24
+ const [metadata, raw] = await Promise.all([
25
+ stat(filePath),
26
+ readFile(filePath, "utf8")
27
+ ]);
28
+ const header = raw.slice(0, maxHeaderBytes);
29
+ const table = parseMetadataTable(header);
30
+ const fileName = path.basename(filePath);
31
+ const mode = fileName.endsWith(".ask.md") ? "ask" : "debate";
32
+ return {
33
+ fileName,
34
+ path: filePath,
35
+ mode,
36
+ topic: table.Sujet ?? table.Subject ?? topicFromFileName(fileName),
37
+ agents: table.Agents ?? "",
38
+ date: table["Date locale"] ?? table["Local date"] ?? table["Session demarree a"] ?? table["Session started at"] ?? "",
39
+ count: countFromTable(mode, table),
40
+ mtimeMs: metadata.mtimeMs
41
+ };
42
+ }
43
+ catch {
44
+ return undefined;
45
+ }
46
+ }
47
+ function countFromTable(mode, table) {
48
+ if (mode === "ask") {
49
+ const received = table["Reponses recues"] ?? table["Received responses"];
50
+ const requested = table["Reponses attendues"] ?? table["Expected responses"];
51
+ return received && requested ? `${received}/${requested}` : received ?? requested ?? "";
52
+ }
53
+ const played = table["Tours joues"] ?? table["Played turns"];
54
+ const requested = table["Tours demandes"] ?? table["Requested turns"];
55
+ return played && requested ? `${played}/${requested}` : played ?? requested ?? "";
56
+ }
57
+ function parseMetadataTable(markdown) {
58
+ const fields = {};
59
+ const lines = markdown.split(/\r?\n/);
60
+ for (const line of lines) {
61
+ const match = line.match(/^\|\s*([^|]+?)\s*\|\s*([^|]*?)\s*\|$/);
62
+ if (!match)
63
+ continue;
64
+ const key = stripMarkdown(match[1] ?? "");
65
+ const value = stripMarkdown(match[2] ?? "");
66
+ if (!key || key === "Champ" || key === "Field" || /^-+$/.test(key))
67
+ continue;
68
+ fields[key] = value;
69
+ }
70
+ return fields;
71
+ }
72
+ function stripMarkdown(value) {
73
+ return value
74
+ .replace(/\*\*/g, "")
75
+ .replace(/`/g, "")
76
+ .trim();
77
+ }
78
+ function topicFromFileName(fileName) {
79
+ return fileName
80
+ .replace(/^palabre-/, "")
81
+ .replace(/\.(debate|ask)\.md$/i, "")
82
+ .replace(/-\d{4}-\d{2}-\d{2}t.*$/i, "")
83
+ .replace(/-/g, " ")
84
+ .trim() || fileName;
85
+ }