@jossuealcala/madre 0.7.1 → 0.7.2
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/CHANGELOG.md +61 -1
- package/SECURITY.md +1 -0
- package/docs/REFERENCE.md +4 -1
- package/package.json +1 -1
- package/public/app.js +467 -65
- package/public/es.js +102 -5
- package/public/styles.css +126 -19
- package/src/adapters/claude.mjs +4 -3
- package/src/adapters/codex.mjs +4 -3
- package/src/choices.mjs +30 -6
- package/src/file-ops.mjs +19 -0
- package/src/models.mjs +25 -1
- package/src/modules/index.mjs +2 -1
- package/src/modules/ollama.mjs +5 -1
- package/src/modules/runs.mjs +34 -0
- package/src/ollama.mjs +19 -0
- package/src/room/control.mjs +18 -1
- package/src/room/prompt.mjs +16 -4
- package/src/room/runs.mjs +88 -0
- package/src/room.mjs +73 -11
- package/src/runs.mjs +119 -0
- package/src/server.mjs +55 -7
package/src/server.mjs
CHANGED
|
@@ -17,7 +17,7 @@ import { createEmbedder } from './embeddings.mjs';
|
|
|
17
17
|
import { memoryServerFor } from './memory-tools.mjs';
|
|
18
18
|
import { MotherChannel, CODE000_STRIKES } from './mother.mjs';
|
|
19
19
|
import { ErrorSentinel } from './sentinel-errors.mjs';
|
|
20
|
-
import { probeOllama, ollamaEmbedder, ollamaInvoker, pullModel } from './ollama.mjs';
|
|
20
|
+
import { probeOllama, ollamaEmbedder, ollamaInvoker, pullModel, ollamaVersions, ownOllamaServers } from './ollama.mjs';
|
|
21
21
|
import { moduleById, describeModules, findModuleRoute, toolsForTurn as modulesToolsForTurn, loadExternalModules, loadFailures, moduleFolders, moduleCommands, installModuleFile, installModuleText, verifyModuleText, moduleOrigin, removeExternalModule } from './modules/index.mjs';
|
|
22
22
|
import { madreAgent, madreInvoker, MADRE_AGENT_ID, MADRE_ADAPTER } from './adapters/madre.mjs';
|
|
23
23
|
import { exportDataset, readiness as datasetReadiness } from './dataset.mjs';
|
|
@@ -65,7 +65,7 @@ import { setImageModule } from './capabilities.mjs';
|
|
|
65
65
|
import { resolveGeminiKey } from './image-studio.mjs';
|
|
66
66
|
import { commandByName, listCommands, parseCommand } from './commands.mjs';
|
|
67
67
|
import { listDirectory, searchFiles, readServable, storeAttachment, resolveInside, MAX_ATTACHMENT_BYTES } from './files.mjs';
|
|
68
|
-
import { planFileOp, CREATIONS } from './file-ops.mjs';
|
|
68
|
+
import { planFileOp, freeCopyPath, CREATIONS } from './file-ops.mjs';
|
|
69
69
|
import { Eyecat } from './eyecat-watch.mjs';
|
|
70
70
|
import { economy } from './room/economy.mjs';
|
|
71
71
|
import { maturity } from './maturity.mjs';
|
|
@@ -560,6 +560,7 @@ export async function createPulseServer({
|
|
|
560
560
|
setImageModule(startupConfig.modules?.imageStudio ?? {});
|
|
561
561
|
room.setAsh(Boolean(startupConfig.modules?.ash?.enabled));
|
|
562
562
|
room.setChoices(Boolean(startupConfig.modules?.choices?.enabled));
|
|
563
|
+
room.setRuns(Boolean(startupConfig.modules?.runs?.enabled));
|
|
563
564
|
const recoveredTurns = await room.reconcile();
|
|
564
565
|
if (recoveredTurns) console.error(`MADRE recovered ${recoveredTurns} unfinished turn(s) from a previous run.`);
|
|
565
566
|
const quotaMonitor = new QuotaMonitor({
|
|
@@ -771,6 +772,29 @@ export async function createPulseServer({
|
|
|
771
772
|
await room.record('extension.install.finished', { id: 'ollama', name: 'OLLAMA', ok: status.running, installed: true, detail: status.running ? `running · ${status.chatModel ? `@madre with ${status.chatModel}` : 'no chat model yet'}` : 'Ollama did not answer; open the Ollama app and press RECHECK.', ollama: status });
|
|
772
773
|
return status.running ? { ok: true } : { ok: false, error: t('Ollama did not answer. Open the Ollama app, then press RECHECK.') };
|
|
773
774
|
},
|
|
775
|
+
// After an upgrade the binary is new and the server answering on the port is still the old
|
|
776
|
+
// one, so the card kept reading the old version and the update looked like it did nothing.
|
|
777
|
+
// An `ollama serve` this user started is stopped and started again from the new binary;
|
|
778
|
+
// anything else (the app, a service of another user) is named, not touched.
|
|
779
|
+
async restart() {
|
|
780
|
+
const binary = await findOnPath('ollama');
|
|
781
|
+
if (!binary) return { ok: false, error: t('Ollama is not on this computer yet.') };
|
|
782
|
+
const run = (command, args) => new Promise((resolve) => execFile(command, args, { timeout: 5000 }, (error, stdout, stderr) => resolve(`${stdout ?? ''}${stderr ?? ''}`)));
|
|
783
|
+
const { client } = ollamaVersions(await run(binary, ['--version']));
|
|
784
|
+
const status = await wireOllama();
|
|
785
|
+
if (!status.running) return this.start();
|
|
786
|
+
if (!client || !status.version || status.version === client) return { ok: true, already: true };
|
|
787
|
+
const pids = process.platform === 'win32' || typeof process.getuid !== 'function' ? [] : ownOllamaServers(await run('ps', ['-axo', 'pid=,uid=,args=']), process.getuid());
|
|
788
|
+
if (!pids.length) return { ok: false, error: t('Ollama {client} is installed, but {running} is still the one answering. Quit Ollama (or restart its service) and open it again, then RECHECK.', { client, running: status.version }) };
|
|
789
|
+
for (const pid of pids) { try { process.kill(pid, 'SIGTERM'); } catch { /* already gone */ } }
|
|
790
|
+
let down = status;
|
|
791
|
+
for (let attempt = 0; attempt < 20 && down.running; attempt += 1) {
|
|
792
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
793
|
+
down = await wireOllama();
|
|
794
|
+
}
|
|
795
|
+
if (down.running) return { ok: false, error: t('The old Ollama ({running}) did not stop. Quit it by hand, then press START OLLAMA.', { running: status.version }) };
|
|
796
|
+
return this.start();
|
|
797
|
+
},
|
|
774
798
|
// Pulls stream into the room like a module install; one at a time.
|
|
775
799
|
pull: async (model) => {
|
|
776
800
|
if (installing) return { ok: false, error: `Another install is running (${installing}).` };
|
|
@@ -909,6 +933,7 @@ export async function createPulseServer({
|
|
|
909
933
|
agents: agents.map((agent) => ({ ...agent, login: loginPlanFor(agent), install: installPlanFor(agent), key: keyPlanFor(agent.id), ...(accountNoteFor(agent.id) ?? {}) })),
|
|
910
934
|
ash: { enabled: room.ashEnabled() },
|
|
911
935
|
choices: { enabled: room.choicesEnabled() },
|
|
936
|
+
runs: { enabled: room.runsEnabled() },
|
|
912
937
|
// Read live, not from the config this process loaded at boot. A switch the human moves
|
|
913
938
|
// in MODULES while the room is open has to be the answer the room gives afterwards —
|
|
914
939
|
// otherwise the switch says on and the browser keeps refusing, which is how a setting
|
|
@@ -959,8 +984,18 @@ export async function createPulseServer({
|
|
|
959
984
|
// room guards from agents ask for the project's designation first.
|
|
960
985
|
if (request.method === 'POST' && url.pathname === '/api/tree/op') {
|
|
961
986
|
const payload = await body(request).catch(() => ({}));
|
|
987
|
+
// Pasting a copy where its name is taken — beside itself, most often — names the copy
|
|
988
|
+
// instead of refusing. Only a copy, and only when the page asked: a move or a rename onto
|
|
989
|
+
// a taken name is still a mistake the human should hear about.
|
|
990
|
+
if (payload.operation === 'copy' && payload.unique) {
|
|
991
|
+
payload.to = await freeCopyPath(payload.to, { word: t('copy'), taken: async (path) => {
|
|
992
|
+
const dir = path.includes('/') ? path.slice(0, path.lastIndexOf('/')) : '.';
|
|
993
|
+
const parent = await resolveInside(canonicalProjectRoot, dir);
|
|
994
|
+
return Boolean(parent) && access(join(parent, path.slice(path.lastIndexOf('/') + 1))).then(() => true, () => false);
|
|
995
|
+
} });
|
|
996
|
+
}
|
|
962
997
|
const plan = planFileOp(payload);
|
|
963
|
-
if (!plan.ok) return sendJson(response, 400, { error: plan.error });
|
|
998
|
+
if (!plan.ok) return sendJson(response, 400, { error: t(plan.error) });
|
|
964
999
|
if (plan.guarded && !designationOk(payload.designation)) {
|
|
965
1000
|
return sendJson(response, 403, { error: t('UNABLE TO COMPUTE. UNABLE TO CLARIFY.'), guarded: true });
|
|
966
1001
|
}
|
|
@@ -980,7 +1015,7 @@ export async function createPulseServer({
|
|
|
980
1015
|
else if (plan.operation === 'copy') await cp(source, destination, { recursive: true, errorOnExist: true, force: false });
|
|
981
1016
|
else await rename(source, destination);
|
|
982
1017
|
} catch (error) {
|
|
983
|
-
return sendJson(response, 500, { error: error.message });
|
|
1018
|
+
return sendJson(response, 500, { error: t('That could not be done: {error}', { error: error.message }) });
|
|
984
1019
|
}
|
|
985
1020
|
await room.record(CREATIONS.includes(plan.operation) ? 'project.file.created' : 'project.file.moved', { operation: plan.operation, from: plan.from, to: plan.to, guarded: plan.guarded });
|
|
986
1021
|
return sendJson(response, 200, { ok: true, operation: plan.operation, from: plan.from, to: plan.to });
|
|
@@ -1481,11 +1516,15 @@ export async function createPulseServer({
|
|
|
1481
1516
|
await room.record('extension.install.started', { id: module.id, name: item.name, command: plan.display, platforms: [], alreadyInstalled: true });
|
|
1482
1517
|
void (async () => {
|
|
1483
1518
|
const result = await runInstaller({ command: plan.command, args: plan.args, projectRoot: canonicalProjectRoot, timeoutMs: 900000, onLine: (line) => { void room.record('extension.install.output', { id: module.id, lines: [String(line).slice(0, 500)] }); } });
|
|
1484
|
-
|
|
1519
|
+
// What the plan does once the command is done (Ollama: restart the old server). If it could
|
|
1520
|
+
// not, that is the outcome: «now runs <the old version>» would read as an update that worked.
|
|
1521
|
+
const settled = result.code === 0 ? await (async () => plan.after?.())().catch((error) => ({ ok: false, error: error.message })) : null;
|
|
1485
1522
|
const after = await module.describe(await moduleContext()).catch(() => null);
|
|
1486
1523
|
await room.record('extension.install.finished', {
|
|
1487
|
-
id: module.id, name: item.name, ok: result.code === 0, installed: Boolean(after?.version),
|
|
1488
|
-
detail: result.code
|
|
1524
|
+
id: module.id, name: item.name, ok: result.code === 0 && settled?.ok !== false, installed: Boolean(after?.version),
|
|
1525
|
+
detail: result.code !== 0 ? (result.error ?? `exit ${result.code}`)
|
|
1526
|
+
: settled?.ok === false ? settled.error
|
|
1527
|
+
: `${item.name} now runs ${after?.version ?? 'what it found'}`,
|
|
1489
1528
|
});
|
|
1490
1529
|
installing = null;
|
|
1491
1530
|
})();
|
|
@@ -1617,6 +1656,15 @@ export async function createPulseServer({
|
|
|
1617
1656
|
const stopped = await room.stopPlan(stopMatch[1]);
|
|
1618
1657
|
return sendJson(response, stopped ? 202 : 404, stopped ? { stopped: true } : { error: t('No running plan with that id.') });
|
|
1619
1658
|
}
|
|
1659
|
+
// One line of a ```pulse-run, pressed by the human. The body names the request and the line;
|
|
1660
|
+
// the command itself is read from the room's record, never from the page.
|
|
1661
|
+
if (request.method === 'POST' && url.pathname === '/api/runs') {
|
|
1662
|
+
const payload = await body(request).catch(() => ({}));
|
|
1663
|
+
const index = Number(payload.index);
|
|
1664
|
+
if (typeof payload.responseMessageId !== 'string' || !Number.isInteger(index) || index < 0) return sendJson(response, 400, { error: t('Name the request and the line to run.') });
|
|
1665
|
+
const outcome = await room.runRequested({ responseMessageId: payload.responseMessageId, index });
|
|
1666
|
+
return sendJson(response, outcome.status, outcome.ok ? outcome.result : { error: outcome.error });
|
|
1667
|
+
}
|
|
1620
1668
|
if (request.method === 'GET' && url.pathname === '/api/commands') {
|
|
1621
1669
|
const ctx = await moduleContext();
|
|
1622
1670
|
const fromModules = await Promise.all(moduleCommands().map(async (command) => {
|