@jossuealcala/madre 0.7.1 → 1.0.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.
- package/CHANGELOG.md +833 -1
- package/README.md +92 -28
- package/SECURITY.md +1 -0
- package/bin/madre.mjs +30 -10
- package/docs/REFERENCE.md +158 -79
- package/docs/SDK.md +23 -19
- package/package.json +1 -1
- package/public/app.js +11599 -4186
- package/public/assets/brand/Geist-latin-ext.woff2 +0 -0
- package/public/assets/brand/Geist-latin.woff2 +0 -0
- package/public/assets/brand/GeistMono-latin-ext.woff2 +0 -0
- package/public/assets/brand/GeistMono-latin.woff2 +0 -0
- package/public/assets/brand/JetBrainsMono-latin-ext.woff2 +0 -0
- package/public/assets/brand/JetBrainsMono-latin.woff2 +0 -0
- package/public/assets/brand/OFL-Geist.txt +93 -0
- package/public/assets/brand/OFL-GeistMono.txt +93 -0
- package/public/assets/brand/OFL-JetBrainsMono.txt +93 -0
- package/public/assets/brand/OFL-Sora.txt +93 -0
- package/public/assets/brand/Sora-latin-ext.woff2 +0 -0
- package/public/assets/brand/Sora-latin.woff2 +0 -0
- package/public/assets/brand/madre-app-180.png +0 -0
- package/public/assets/brand/madre-app-192.png +0 -0
- package/public/assets/brand/madre-app-512.png +0 -0
- package/public/assets/brand/madre-app.svg +1 -0
- package/public/assets/brand/madre-favicon.svg +1 -0
- package/public/assets/news/1.0/alerts.webp +0 -0
- package/public/assets/news/1.0/bar.webp +0 -0
- package/public/assets/news/1.0/finder.webp +0 -0
- package/public/assets/news/1.0/github.webp +0 -0
- package/public/assets/news/1.0/intents.webp +0 -0
- package/public/assets/news/1.0/looks.webp +0 -0
- package/public/assets/news/1.0/modules.webp +0 -0
- package/public/assets/news/1.0/mother.webp +0 -0
- package/public/assets/news/1.0/nostromo.webp +0 -0
- package/public/assets/news/1.0/permissions.webp +0 -0
- package/public/assets/news/1.0/room.webp +0 -0
- package/public/assets/news/1.0/settings.webp +0 -0
- package/public/assets/wallpapers/biznaga.webp +0 -0
- package/public/assets/wallpapers/canela.webp +0 -0
- package/public/assets/wallpapers/jarrito.webp +0 -0
- package/public/assets/wallpapers/magna.webp +0 -0
- package/public/assets/wallpapers/oceano.webp +0 -0
- package/public/assets/wallpapers/paraiso.webp +0 -0
- package/public/assets/wallpapers/quila.webp +0 -0
- package/public/assets/wallpapers/tradicion.webp +0 -0
- package/public/assets/wallpapers/tropical.webp +0 -0
- package/public/assets/wallpapers/urbania.webp +0 -0
- package/public/assets/wallpapers/ventura.webp +0 -0
- package/public/assets/wallpapers/xico.webp +0 -0
- package/public/brands.js +28 -2
- package/public/editor.js +744 -0
- package/public/es.js +2863 -1008
- package/public/i18n.js +1 -1
- package/public/index.html +201 -175
- package/public/inquiry.js +11 -11
- package/public/loader.js +79 -0
- package/public/macroverso.js +1497 -0
- package/public/manifest.webmanifest +14 -0
- package/public/nostromo-engine.js +2222 -0
- package/public/resay.js +24 -0
- package/public/review-ask.js +85 -0
- package/public/salas-mac.css +787 -0
- package/public/salas-mac.html +154 -0
- package/public/salas-mac.js +3264 -0
- package/public/salas.css +85 -0
- package/public/salas.html +47 -0
- package/public/salas.js +200 -0
- package/public/styles.css +6303 -414
- package/public/touch.js +83 -0
- package/public/tour-chapters.js +342 -0
- package/public/tour.js +510 -0
- package/public/troubleshooting.js +159 -140
- package/src/adapters/claude.mjs +4 -3
- package/src/adapters/codex.mjs +49 -8
- package/src/adapters/gemini.mjs +30 -10
- package/src/adapters/madre.mjs +110 -18
- package/src/adapters/process.mjs +42 -2
- package/src/auth-probe.mjs +2 -2
- package/src/bridge/devices.mjs +202 -0
- package/src/bridge/handoff.mjs +45 -0
- package/src/bridge/host.mjs +82 -0
- package/src/bridge/network.mjs +133 -0
- package/src/bridge/policy.mjs +89 -0
- package/src/bridge/qr.mjs +287 -0
- package/src/bridge/server.mjs +269 -0
- package/src/capabilities.mjs +5 -9
- package/src/chats.mjs +59 -22
- package/src/checkpoint.mjs +137 -35
- package/src/choices.mjs +45 -6
- package/src/cli-args.mjs +12 -1
- package/src/commands.mjs +15 -15
- package/src/config.mjs +73 -9
- package/src/conversation-context.mjs +5 -2
- package/src/dataset.mjs +3 -1
- package/src/directives.mjs +0 -5
- package/src/enlaces.mjs +451 -0
- package/src/event-store.mjs +55 -10
- package/src/exam.mjs +5 -5
- package/src/extensions.mjs +0 -2
- package/src/file-ops.mjs +42 -0
- package/src/files.mjs +52 -2
- package/src/github.mjs +407 -0
- package/src/json-file.mjs +63 -0
- package/src/lease.mjs +2 -2
- package/src/lobby-work.mjs +103 -0
- package/src/looks.mjs +55 -0
- package/src/maturity.mjs +3 -3
- package/src/mcp/github-server.mjs +164 -0
- package/src/mcp/image-server.mjs +1 -1
- package/src/mcp/memory-server.mjs +11 -3
- package/src/memory.mjs +38 -12
- package/src/models.mjs +109 -1
- package/src/modules/ahp.mjs +4 -4
- package/src/modules/ash.mjs +1 -1
- package/src/modules/bishop-plan.mjs +207 -0
- package/src/modules/bishop.mjs +645 -0
- package/src/modules/choices.mjs +2 -2
- package/src/modules/correo.mjs +1 -1
- package/src/modules/enlaces.mjs +255 -0
- package/src/modules/git-pulse.mjs +2 -2
- package/src/modules/github.mjs +322 -0
- package/src/modules/image-studio.mjs +3 -3
- package/src/modules/index.mjs +19 -4
- package/src/modules/intents-calibration.mjs +338 -0
- package/src/modules/intents-desk.mjs +481 -0
- package/src/modules/intents-observe.mjs +266 -0
- package/src/modules/intents-policy.mjs +414 -0
- package/src/modules/intents-try.mjs +91 -0
- package/src/modules/intents.mjs +145 -0
- package/src/modules/ollama.mjs +9 -5
- package/src/modules/playwright.mjs +121 -11
- package/src/modules/ripley.mjs +1 -1
- package/src/modules/runs.mjs +34 -0
- package/src/modules/sdk.mjs +10 -4
- package/src/ollama.mjs +25 -1
- package/src/outbound.mjs +19 -17
- package/src/override.mjs +100 -0
- package/src/ports.mjs +23 -1
- package/src/prices.mjs +72 -0
- package/src/privacy.mjs +30 -11
- package/src/quota-sources.mjs +0 -8
- package/src/request-guard.mjs +1 -1
- package/src/review-ask.mjs +111 -0
- package/src/room/archivist.mjs +55 -37
- package/src/room/budget.mjs +17 -5
- package/src/room/control.mjs +88 -8
- package/src/room/economy.mjs +35 -7
- package/src/room/escalation.mjs +2 -0
- package/src/room/guard.mjs +26 -2
- package/src/room/prompt.mjs +32 -13
- package/src/room/runs.mjs +151 -0
- package/src/room/vectors.mjs +19 -4
- package/src/room.mjs +402 -54
- package/src/router.mjs +3 -1
- package/src/runs.mjs +137 -0
- package/src/salas.mjs +313 -0
- package/src/sentinel-errors.mjs +1 -1
- package/src/server-sentinel.mjs +124 -0
- package/src/server.mjs +1578 -167
- package/src/setup.mjs +1 -0
- package/src/tour.mjs +159 -0
- package/src/updates.mjs +48 -8
- package/src/usage-sentinel.mjs +1 -1
- package/src/verdict.mjs +7 -7
- package/src/whats-new.mjs +160 -0
package/src/maturity.mjs
CHANGED
|
@@ -36,13 +36,13 @@ export function maturity({ readiness = null, notes = [], links = [], stats = nul
|
|
|
36
36
|
id: 'volume', label: t('HOW MUCH THERE IS'),
|
|
37
37
|
value: round(share(pairs, VOLUME_TARGET)),
|
|
38
38
|
detail: t('{pairs} of about {target} exchanges worth training on', { pairs, target: VOLUME_TARGET }),
|
|
39
|
-
next: t('Use the
|
|
39
|
+
next: t('Use the project. Nothing else fills this.'),
|
|
40
40
|
},
|
|
41
41
|
{
|
|
42
42
|
id: 'coverage', label: t('HOW MUCH OF IT GETS USED'),
|
|
43
43
|
value: round(share(recalled, standing.length)),
|
|
44
44
|
detail: t('{recalled} of {total} memories have been reached for at least once', { recalled, total: standing.length }),
|
|
45
|
-
next: t('Memories nobody has needed may be noise, or may simply not have come up yet. Ask the
|
|
45
|
+
next: t('Memories nobody has needed may be noise, or may simply not have come up yet. Ask the project about older decisions and see which ones answer.'),
|
|
46
46
|
},
|
|
47
47
|
{
|
|
48
48
|
id: 'weave', label: t('HOW WOVEN IT IS'),
|
|
@@ -64,7 +64,7 @@ export function maturity({ readiness = null, notes = [], links = [], stats = nul
|
|
|
64
64
|
// progress where there is none.
|
|
65
65
|
value: pairs > 0 ? round(1 - Math.min(1, Math.max(0, (noteShare - 0.5) / 0.5))) : 0,
|
|
66
66
|
detail: t('{share}% of the corpus is recall questions, {turns} exchanges are real work', { share: Math.round(noteShare * 100), turns }),
|
|
67
|
-
next: t('Recall pairs are made from notes and cost nothing, so they pile up. Work in the
|
|
67
|
+
next: t('Recall pairs are made from notes and cost nothing, so they pile up. Work in the project to balance them.'),
|
|
68
68
|
},
|
|
69
69
|
{
|
|
70
70
|
id: 'upkeep', label: t('HOW CURRENT IT IS'),
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// MADRE GITHUB: a small MCP server (stdio, JSON-RPC 2.0) that lets an agent read the GitHub side
|
|
3
|
+
// of this project — pull requests, issues, checks — and, in #4 AIRLOCK only, open a pull request,
|
|
4
|
+
// open an issue or leave a comment. There is no MADRE in the middle: this process asks gh, or
|
|
5
|
+
// GitHub's API with the human's token, and nothing else.
|
|
6
|
+
//
|
|
7
|
+
// Reading comes back; writing does not. A comment carries the human's name the moment it lands,
|
|
8
|
+
// so three things hold, each somewhere a prompt cannot reach:
|
|
9
|
+
//
|
|
10
|
+
// · The room hands the write tools to a turn only at #4 AIRLOCK (`sends` in the module, enforced
|
|
11
|
+
// in src/modules/index.mjs). This server checks again: unless PULSE_GITHUB_WRITES=1 it does
|
|
12
|
+
// not list them and refuses to run them.
|
|
13
|
+
// · The credential is gh's own session, which this process never sees, or a token in its
|
|
14
|
+
// environment. Neither is written anywhere: not in a result, not in the outbound log.
|
|
15
|
+
// · The repository is the one the module found in the project's remotes. A tool cannot be
|
|
16
|
+
// pointed at another one.
|
|
17
|
+
//
|
|
18
|
+
// Environment:
|
|
19
|
+
// PULSE_GITHUB_REPO required, owner/repo
|
|
20
|
+
// PULSE_GITHUB_ROOT the project, to read the current branch from git
|
|
21
|
+
// PULSE_GITHUB_AUTH 'gh' or 'token'
|
|
22
|
+
// PULSE_GITHUB_GH the gh executable, with PULSE_GITHUB_AUTH=gh
|
|
23
|
+
// PULSE_GITHUB_TOKEN the token, with PULSE_GITHUB_AUTH=token (from MADRE's vault)
|
|
24
|
+
// PULSE_GITHUB_WRITES '1' only in a #4 AIRLOCK turn
|
|
25
|
+
// PULSE_OUTBOUND_LOG where each request leaves its line
|
|
26
|
+
|
|
27
|
+
import { realpath } from 'node:fs/promises';
|
|
28
|
+
import { fileURLToPath } from 'node:url';
|
|
29
|
+
import { OutboundLog, declareDestinations } from '../outbound.mjs';
|
|
30
|
+
import * as github from '../github.mjs';
|
|
31
|
+
|
|
32
|
+
const SERVER_NAME = 'pulse-github';
|
|
33
|
+
const SERVER_VERSION = '1.0.0';
|
|
34
|
+
const SELF = fileURLToPath(import.meta.url);
|
|
35
|
+
|
|
36
|
+
export const READ_TOOLS = ['github_list_prs', 'github_get_pr', 'github_ci_status', 'github_list_issues', 'github_get_issue'];
|
|
37
|
+
export const WRITE_TOOLS = ['github_create_pr', 'github_comment', 'github_create_issue'];
|
|
38
|
+
|
|
39
|
+
const n = { type: 'integer', minimum: 1 };
|
|
40
|
+
export const TOOLS = [
|
|
41
|
+
{ name: 'github_list_prs', description: 'List the open pull requests of this project\'s GitHub repository: number, title, author, branch, base, draft.', inputSchema: { type: 'object', properties: { limit: { type: 'integer', minimum: 1, maximum: 100, description: 'How many, newest first. Default 20.' } } } },
|
|
42
|
+
{ name: 'github_get_pr', description: 'One pull request: title, state, branch and base, whether it can merge, additions/deletions, changed files, and the review state.', inputSchema: { type: 'object', properties: { number: { ...n, description: 'The pull request number.' } }, required: ['number'] } },
|
|
43
|
+
{ name: 'github_ci_status', description: 'The checks and CI of a branch: one overall state (success, failure, pending, none), each check, and the latest workflow runs. Defaults to the branch the project is on.', inputSchema: { type: 'object', properties: { ref: { type: 'string', description: 'A branch, tag or commit. Default: the current branch.' } } } },
|
|
44
|
+
{ name: 'github_list_issues', description: 'List the open issues (not pull requests): number, title, author, labels, comment count.', inputSchema: { type: 'object', properties: { limit: { type: 'integer', minimum: 1, maximum: 100, description: 'How many, newest first. Default 20.' } } } },
|
|
45
|
+
{ name: 'github_get_issue', description: 'One issue: title, state, author, labels and its text.', inputSchema: { type: 'object', properties: { number: { ...n, description: 'The issue number.' } }, required: ['number'] } },
|
|
46
|
+
{ name: 'github_create_pr', description: 'Open a pull request on GitHub under the human\'s name. It cannot be undone from here: before calling it, state in one line its title, from which branch and into which. The branch must already be pushed.', inputSchema: { type: 'object', properties: { title: { type: 'string' }, body: { type: 'string', description: 'The description, Markdown.' }, head: { type: 'string', description: 'The branch to merge from. Default: the current branch.' }, base: { type: 'string', description: 'The branch to merge into. Default: the repository\'s default branch.' } }, required: ['title'] } },
|
|
47
|
+
{ name: 'github_comment', description: 'Post a comment on a pull request or issue under the human\'s name. It cannot be undone from here: before calling it, state in one line where it goes and what it says.', inputSchema: { type: 'object', properties: { number: { ...n, description: 'The pull request or issue number.' }, body: { type: 'string', description: 'The comment, Markdown.' } }, required: ['number', 'body'] } },
|
|
48
|
+
{ name: 'github_create_issue', description: 'Open an issue on GitHub under the human\'s name. It cannot be undone from here: before calling it, state in one line its title and what it says.', inputSchema: { type: 'object', properties: { title: { type: 'string' }, body: { type: 'string', description: 'The text, Markdown.' } }, required: ['title'] } },
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
// What this process was handed, as the client wants it. The token is read here and only here.
|
|
52
|
+
export function contextFrom(env = process.env) {
|
|
53
|
+
const [owner, repo] = String(env.PULSE_GITHUB_REPO ?? '').split('/');
|
|
54
|
+
const auth = env.PULSE_GITHUB_AUTH === 'gh' && env.PULSE_GITHUB_GH ? { kind: 'gh', gh: env.PULSE_GITHUB_GH }
|
|
55
|
+
: env.PULSE_GITHUB_AUTH === 'token' && env.PULSE_GITHUB_TOKEN ? { kind: 'token', token: env.PULSE_GITHUB_TOKEN }
|
|
56
|
+
: null;
|
|
57
|
+
// gh is handed an environment without the token variable it never needed.
|
|
58
|
+
const { PULSE_GITHUB_TOKEN: _drop, ...clean } = env;
|
|
59
|
+
return { repo: owner && repo ? { owner, repo, slug: `${owner}/${repo}` } : null, root: env.PULSE_GITHUB_ROOT || null, auth, writes: env.PULSE_GITHUB_WRITES === '1', env: clean };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let outbound = null;
|
|
63
|
+
function logFor(env) {
|
|
64
|
+
if (!env.PULSE_OUTBOUND_LOG) return null;
|
|
65
|
+
// This process has its own copy of the declarations; without this, its lines would read as an
|
|
66
|
+
// address nothing here declares.
|
|
67
|
+
declareDestinations('github', github.REACHES);
|
|
68
|
+
outbound ??= new OutboundLog({ file: env.PULSE_OUTBOUND_LOG });
|
|
69
|
+
return outbound;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function callTool(name, args = {}, { env = process.env, api = github } = {}) {
|
|
73
|
+
const here = contextFrom(env);
|
|
74
|
+
if (!here.repo) throw new github.GithubError('NO_REPO', 'No repository was handed to this server.');
|
|
75
|
+
if (!here.auth) throw new github.GithubError('NO_CREDENTIAL', github.ERRORS.NO_SESSION);
|
|
76
|
+
if (WRITE_TOOLS.includes(name) && !here.writes) throw new github.GithubError('NOT_AIRLOCK', 'Writing to GitHub needs a turn in #4 AIRLOCK. Nothing was posted.');
|
|
77
|
+
const common = { repo: here.repo, auth: here.auth, log: logFor(env), env: here.env };
|
|
78
|
+
const branch = async () => (here.root ? api.currentBranch(here.root, { env: here.env }) : null);
|
|
79
|
+
switch (name) {
|
|
80
|
+
case 'github_list_prs': return { repo: here.repo.slug, pulls: await api.listPulls({ ...common, limit: args.limit }) };
|
|
81
|
+
case 'github_get_pr': return await api.getPull({ ...common, number: args.number });
|
|
82
|
+
case 'github_ci_status': {
|
|
83
|
+
const ref = String(args.ref ?? '').trim() || await branch();
|
|
84
|
+
const [ci, runs] = await Promise.all([api.ciStatus({ ...common, ref }), api.workflowRuns({ ...common, branch: ref, limit: 5 }).catch(() => [])]);
|
|
85
|
+
return { ...ci, runs };
|
|
86
|
+
}
|
|
87
|
+
case 'github_list_issues': return { repo: here.repo.slug, issues: await api.listIssues({ ...common, limit: args.limit }) };
|
|
88
|
+
case 'github_get_issue': return await api.getIssue({ ...common, number: args.number });
|
|
89
|
+
case 'github_create_pr': {
|
|
90
|
+
const head = String(args.head ?? '').trim() || await branch();
|
|
91
|
+
return await api.createPull({ ...common, title: args.title, body: args.body ?? '', head, base: String(args.base ?? '').trim() || null });
|
|
92
|
+
}
|
|
93
|
+
case 'github_comment': return await api.comment({ ...common, number: args.number, body: args.body });
|
|
94
|
+
case 'github_create_issue': return await api.createIssue({ ...common, title: args.title, body: args.body ?? '' });
|
|
95
|
+
default: throw new github.GithubError('UNKNOWN_TOOL', `Unknown tool: ${name}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function handleRequest(message, { env = process.env, api = github } = {}) {
|
|
100
|
+
const reply = (result) => ({ jsonrpc: '2.0', id: message.id, result });
|
|
101
|
+
const fail = (code, text) => ({ jsonrpc: '2.0', id: message.id, error: { code, message: text } });
|
|
102
|
+
const writes = env.PULSE_GITHUB_WRITES === '1';
|
|
103
|
+
switch (message.method) {
|
|
104
|
+
case 'initialize':
|
|
105
|
+
return Promise.resolve(reply({ protocolVersion: message.params?.protocolVersion ?? '2025-06-18', capabilities: { tools: {} }, serverInfo: { name: SERVER_NAME, version: SERVER_VERSION } }));
|
|
106
|
+
case 'notifications/initialized':
|
|
107
|
+
case 'initialized':
|
|
108
|
+
return Promise.resolve(null);
|
|
109
|
+
case 'ping':
|
|
110
|
+
return Promise.resolve(reply({}));
|
|
111
|
+
case 'tools/list':
|
|
112
|
+
// Below #4 the write tools are not merely refused: they are not here.
|
|
113
|
+
return Promise.resolve(reply({ tools: TOOLS.filter((tool) => writes || !WRITE_TOOLS.includes(tool.name)) }));
|
|
114
|
+
case 'tools/call': {
|
|
115
|
+
const { name, arguments: args = {} } = message.params ?? {};
|
|
116
|
+
if (!TOOLS.some((tool) => tool.name === name)) return Promise.resolve(fail(-32602, `Unknown tool: ${name}`));
|
|
117
|
+
const writing = WRITE_TOOLS.includes(name);
|
|
118
|
+
return callTool(name, args, { env, api })
|
|
119
|
+
.then((result) => reply({ content: [{ type: 'text', text: writing ? `Posted to GitHub: ${JSON.stringify(result)}` : JSON.stringify(result) }], isError: false }))
|
|
120
|
+
.catch((error) => {
|
|
121
|
+
const detail = error.detail ? ` GitHub said: ${error.detail}` : '';
|
|
122
|
+
const lead = writing ? 'Nothing was posted' : 'GitHub did not answer';
|
|
123
|
+
return reply({ content: [{ type: 'text', text: `${lead} (${error.code ?? 'ERROR'}): ${error.message}${detail}` }], isError: true });
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
default:
|
|
127
|
+
return Promise.resolve(message.id === undefined ? null : fail(-32601, `Method not found: ${message.method}`));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function serve({ input = process.stdin, output = process.stdout, env = process.env } = {}) {
|
|
132
|
+
let buffer = '';
|
|
133
|
+
let pending = 0;
|
|
134
|
+
let ended = false;
|
|
135
|
+
const finish = () => { if (ended && pending === 0) void (outbound?.drain() ?? Promise.resolve()).finally(() => process.exit(0)); };
|
|
136
|
+
input.setEncoding('utf8');
|
|
137
|
+
input.on('data', (chunk) => {
|
|
138
|
+
buffer += chunk;
|
|
139
|
+
let index;
|
|
140
|
+
while ((index = buffer.indexOf('\n')) >= 0) {
|
|
141
|
+
const line = buffer.slice(0, index).trim();
|
|
142
|
+
buffer = buffer.slice(index + 1);
|
|
143
|
+
if (!line) continue;
|
|
144
|
+
let message;
|
|
145
|
+
try { message = JSON.parse(line); } catch { output.write(`${JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } })}\n`); continue; }
|
|
146
|
+
pending += 1;
|
|
147
|
+
void handleRequest(message, { env })
|
|
148
|
+
.then((response) => { if (response) output.write(`${JSON.stringify(response)}\n`); })
|
|
149
|
+
.finally(() => { pending -= 1; finish(); });
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
input.on('end', () => { ended = true; finish(); });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Both paths resolved before they are compared: `new URL(import.meta.url).pathname` would be
|
|
156
|
+
// /C:/… on Windows (WIN-004), and a path through a symlink would never match its target.
|
|
157
|
+
const real = async (path) => (path ? realpath(path).catch(() => path) : null);
|
|
158
|
+
if (process.argv[1] && (await real(process.argv[1])) === (await real(SELF))) {
|
|
159
|
+
if (!process.env.PULSE_GITHUB_REPO) {
|
|
160
|
+
process.stderr.write('PULSE_GITHUB_REPO is required.\n');
|
|
161
|
+
process.exit(2);
|
|
162
|
+
}
|
|
163
|
+
serve();
|
|
164
|
+
}
|
package/src/mcp/image-server.mjs
CHANGED
|
@@ -28,7 +28,7 @@ const FAKE_PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADU
|
|
|
28
28
|
|
|
29
29
|
// Where a Gemini key can be, in the order the room should trust them: what this process was
|
|
30
30
|
// given, then the file the CLI reads on every run — which is also the file MADRE itself writes
|
|
31
|
-
// when a key is pasted into
|
|
31
|
+
// when a key is pasted into Settings › Agents — and last the macOS keychain, which only exists when
|
|
32
32
|
// the CLI's own sign-in put it there. Leaving the file out meant MADRE saved a key in a place it
|
|
33
33
|
// then refused to look, and on Linux, where there is no keychain, that was the only place it
|
|
34
34
|
// could have been: Image Studio could never find a key there at all.
|
|
@@ -11,9 +11,12 @@
|
|
|
11
11
|
// Gemini CLI. Without a key search is lexical only.
|
|
12
12
|
|
|
13
13
|
import { readFile, readdir, stat, realpath } from 'node:fs/promises';
|
|
14
|
+
import { homedir } from 'node:os';
|
|
14
15
|
import { join } from 'node:path';
|
|
15
16
|
import { RoomMemory, formatRecall, formatMemories, MEMORY_KINDS } from '../memory.mjs';
|
|
16
17
|
import { createEmbedder } from '../embeddings.mjs';
|
|
18
|
+
import { loadConfig } from '../config.mjs';
|
|
19
|
+
import { Privacy, privacySettings } from '../privacy.mjs';
|
|
17
20
|
import { resolveGeminiKey } from './image-server.mjs';
|
|
18
21
|
|
|
19
22
|
const SERVER_NAME = 'pulse-memory';
|
|
@@ -71,9 +74,13 @@ export const TOOLS = [
|
|
|
71
74
|
|
|
72
75
|
function clamp(n, min, max, fallback) { const value = Number(n); return Number.isFinite(value) ? Math.min(max, Math.max(min, Math.trunc(value))) : fallback; }
|
|
73
76
|
|
|
74
|
-
export async function openMemory({ dbFile = process.env.PULSE_MEMORY_DB, env = process.env, keyResolver = resolveGeminiKey } = {}) {
|
|
77
|
+
export async function openMemory({ dbFile = process.env.PULSE_MEMORY_DB, env = process.env, keyResolver = resolveGeminiKey, stateRoot = env.PULSE_HOME ?? join(homedir(), '.pulse'), privacy = null } = {}) {
|
|
75
78
|
if (!dbFile) throw new Error('PULSE_MEMORY_DB is required.');
|
|
76
|
-
|
|
79
|
+
// A private term never enters a note, whoever writes it (docs/MACROVERSO.md, S-008): the guard
|
|
80
|
+
// the room attaches to its own index, built from the same config.json every room on this Mac
|
|
81
|
+
// shares, so a note an agent saves through memory_note is kept the way the archivist's are.
|
|
82
|
+
const guard = privacy ?? new Privacy({ ...privacySettings(await loadConfig(stateRoot).catch(() => ({})), env), home: homedir() });
|
|
83
|
+
const memory = await new RoomMemory(dbFile).attachPrivacy(guard).initialize();
|
|
77
84
|
let embedder = null;
|
|
78
85
|
if (env.PULSE_EMBED !== '0') {
|
|
79
86
|
const key = env.PULSE_EMBED_FAKE === '1' ? null : await keyResolver(env).catch(() => null);
|
|
@@ -115,7 +122,8 @@ export async function callTool(name, args = {}, { memory, projectRoot = process.
|
|
|
115
122
|
const added = memory.addMemories([{ kind, text, sources }], { agent, fromSequence: sources.length ? Math.min(...sources) : last, throughSequence: sources.length ? Math.max(...sources) : last, origin: 'noted', messageId });
|
|
116
123
|
if (!added) return `Already remembered: "${text}"`;
|
|
117
124
|
const [saved] = memory.notesSince(before, { agent });
|
|
118
|
-
|
|
125
|
+
// Echoed as it was kept: with a private term already replaced by the marker.
|
|
126
|
+
return `Saved memory #${saved?.id ?? '?'} (${kind}) for every future turn of this room: "${saved?.text ?? text}"`;
|
|
119
127
|
}
|
|
120
128
|
case 'memory_search': {
|
|
121
129
|
const query = String(args.query ?? '').trim();
|
package/src/memory.mjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// match meaning and not only words. Filled in the background.
|
|
11
11
|
// GHOST events never reach the ledger, so they never reach here either.
|
|
12
12
|
|
|
13
|
-
import { mkdir
|
|
13
|
+
import { mkdir } from 'node:fs/promises';
|
|
14
14
|
import { dirname } from 'node:path';
|
|
15
15
|
import { messageEntry } from './conversation-context.mjs';
|
|
16
16
|
import { cosine, toBlob, fromBlob } from './embeddings.mjs';
|
|
@@ -196,6 +196,11 @@ export class RoomMemory {
|
|
|
196
196
|
|
|
197
197
|
#open() {
|
|
198
198
|
this.#db = new this.#Database(this.#file);
|
|
199
|
+
// Another process writes this file too — the memory server an agent's turn starts, `madre
|
|
200
|
+
// dataset` — and SQLite answered «database is locked» at once, so a note, an index line or a
|
|
201
|
+
// verdict was lost (LOG-08 of the 1.0 review). It waits its turn a few seconds instead. A
|
|
202
|
+
// pragma, because the `timeout` option of node:sqlite is newer than the Node 22.5 MADRE needs.
|
|
203
|
+
this.#db.exec('PRAGMA busy_timeout = 5000');
|
|
199
204
|
if (this.#file !== ':memory:') this.#db.exec('PRAGMA journal_mode = WAL');
|
|
200
205
|
this.#createSchema();
|
|
201
206
|
this.#meta = this.#db.prepare('SELECT value FROM meta WHERE key = ?');
|
|
@@ -323,7 +328,9 @@ export class RoomMemory {
|
|
|
323
328
|
let entries = 0;
|
|
324
329
|
for (const row of this.#db.prepare('SELECT text FROM entries').iterate()) if (this.#privacy.hits(row.text)) entries += 1;
|
|
325
330
|
let memories = 0;
|
|
326
|
-
for (const row of this.#db.prepare('SELECT text FROM memories').iterate())
|
|
331
|
+
for (const row of this.#db.prepare('SELECT text, correction FROM memories').iterate()) {
|
|
332
|
+
if (this.#privacy.hits(row.text) || (row.correction && this.#privacy.hits(row.correction))) memories += 1;
|
|
333
|
+
}
|
|
327
334
|
return { entries, memories };
|
|
328
335
|
}
|
|
329
336
|
|
|
@@ -334,13 +341,17 @@ export class RoomMemory {
|
|
|
334
341
|
if (!this.#privacy?.enabled) return { entries: 0, memories: 0 };
|
|
335
342
|
const counts = { entries: 0, memories: 0 };
|
|
336
343
|
const entryRows = this.#db.prepare('SELECT sequence, event_id, timestamp, type, role, sender, target, message_id, text, distilled FROM entries').all();
|
|
337
|
-
|
|
344
|
+
// Preserve every column when replacing a row. In particular, a refuted note must keep its
|
|
345
|
+
// standing and its refutation; a recall must keep its history. The schema is the source of
|
|
346
|
+
// this list so a later additive column cannot silently be reset to its default here.
|
|
347
|
+
const memoryColumns = this.#db.prepare('PRAGMA table_info(memories)').all().map((column) => column.name);
|
|
348
|
+
const memoryRows = this.#db.prepare('SELECT * FROM memories').all();
|
|
338
349
|
const deleteEntry = this.#db.prepare('DELETE FROM entries WHERE sequence = ?');
|
|
339
350
|
const deleteEntryVector = this.#db.prepare('DELETE FROM entry_vectors WHERE sequence = ?');
|
|
340
351
|
const insertEntry = this.#db.prepare('INSERT INTO entries (sequence, event_id, timestamp, type, role, sender, target, message_id, text, distilled) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
|
|
341
352
|
const deleteMemory = this.#db.prepare('DELETE FROM memories WHERE id = ?');
|
|
342
353
|
const deleteMemoryVector = this.#db.prepare('DELETE FROM memory_vectors WHERE id = ?');
|
|
343
|
-
const insertMemory = this.#db.prepare(
|
|
354
|
+
const insertMemory = this.#db.prepare(`INSERT INTO memories (${memoryColumns.map((name) => `"${name.replaceAll('"', '""')}"`).join(', ')}) VALUES (${memoryColumns.map(() => '?').join(', ')})`);
|
|
344
355
|
this.#db.exec('BEGIN');
|
|
345
356
|
try {
|
|
346
357
|
for (const row of entryRows) {
|
|
@@ -353,12 +364,14 @@ export class RoomMemory {
|
|
|
353
364
|
}
|
|
354
365
|
for (const row of memoryRows) {
|
|
355
366
|
const { text, hits } = this.#privacy.redact(row.text);
|
|
356
|
-
|
|
367
|
+
const correction = row.correction ? this.#privacy.redact(row.correction) : { text: null, hits: 0 };
|
|
368
|
+
if (!hits && !correction.hits) continue;
|
|
357
369
|
deleteMemoryVector.run(row.id);
|
|
358
370
|
deleteMemory.run(row.id);
|
|
359
371
|
// Two notes may collapse into the same redacted sentence: the later one is dropped.
|
|
360
|
-
const
|
|
361
|
-
|
|
372
|
+
const norm = memoryKey(row.kind, text);
|
|
373
|
+
const clash = this.#db.prepare('SELECT id FROM memories WHERE norm = ?').get(norm);
|
|
374
|
+
if (!clash) insertMemory.run(...memoryColumns.map((name) => name === 'text' ? text : name === 'norm' ? norm : name === 'correction' ? correction.text : row[name]));
|
|
362
375
|
counts.memories += 1;
|
|
363
376
|
}
|
|
364
377
|
this.#db.exec('COMMIT');
|
|
@@ -610,9 +623,13 @@ export class RoomMemory {
|
|
|
610
623
|
if (!row) return null;
|
|
611
624
|
this.#db.exec('BEGIN');
|
|
612
625
|
try {
|
|
613
|
-
|
|
626
|
+
// What it took out stands again by the rule, the human's verdict included (LOG-10 of the
|
|
627
|
+
// 1.0 review): written straight to HOLD, a note the human had marked bad travelled again.
|
|
628
|
+
const freed = this.#db.prepare('SELECT id FROM memories WHERE refuted_by = ?').all(row.id).map((one) => one.id);
|
|
629
|
+
this.#db.prepare('UPDATE memories SET refuted_by = NULL WHERE refuted_by = ?').run(row.id);
|
|
614
630
|
this.#db.prepare('DELETE FROM memories WHERE id = ?').run(row.id);
|
|
615
631
|
this.#db.prepare('DELETE FROM memory_vectors WHERE id = ?').run(row.id);
|
|
632
|
+
this.#restand(freed);
|
|
616
633
|
this.#db.exec('COMMIT');
|
|
617
634
|
return { id: row.id, text: row.text, restored: row.contradicts ?? null };
|
|
618
635
|
} catch (error) {
|
|
@@ -666,18 +683,27 @@ export class RoomMemory {
|
|
|
666
683
|
this.#db.prepare('DELETE FROM recalls WHERE memory_id = ?').run(id);
|
|
667
684
|
// A note quarantined by this one comes back: with the aberration gone there is nothing
|
|
668
685
|
// holding it out of circulation, and a memory must never be lost to a pointer at nothing.
|
|
669
|
-
|
|
686
|
+
// Where it stands is decided by the rule, the human's verdict included (LOG-10).
|
|
687
|
+
const freed = this.#db.prepare('SELECT id FROM memories WHERE refuted_by = ?').all(id).map((one) => one.id);
|
|
688
|
+
this.#db.prepare('UPDATE memories SET refuted_by = NULL WHERE refuted_by = ?').run(id);
|
|
670
689
|
this.#db.prepare('DELETE FROM memories WHERE id = ?').run(id);
|
|
690
|
+
this.#restand(freed);
|
|
671
691
|
this.#db.exec('COMMIT');
|
|
672
692
|
} catch (error) { this.#db.exec('ROLLBACK'); throw error; }
|
|
673
693
|
return row;
|
|
674
694
|
}
|
|
675
695
|
|
|
676
696
|
// Which notes are about the same thing: pairs whose vectors agree, strongest
|
|
677
|
-
// first, a few per note.
|
|
697
|
+
// first, a few per note. Comparing notes with each other needs only the vectors
|
|
698
|
+
// already stored, not a live embedder: when the one in use has none (Ollama off,
|
|
699
|
+
// the room fell back to another), the model that embedded the most notes draws
|
|
700
|
+
// the links. Empty only when no note was ever embedded.
|
|
678
701
|
memoryLinks({ floor = 0.6, maxPerNode = 4 } = {}) {
|
|
679
|
-
|
|
680
|
-
const
|
|
702
|
+
const stored = this.#db.prepare('SELECT model, COUNT(*) AS n FROM memory_vectors GROUP BY model ORDER BY n DESC').all();
|
|
703
|
+
const current = this.#embedder ? stored.find((row) => row.model === this.#embedder.model) : null;
|
|
704
|
+
const model = current?.model ?? stored[0]?.model;
|
|
705
|
+
if (!model) return [];
|
|
706
|
+
const rows = this.#db.prepare('SELECT id, vec FROM memory_vectors WHERE model = ?').all(model).map((row) => ({ id: row.id, vec: fromBlob(row.vec) }));
|
|
681
707
|
const links = [];
|
|
682
708
|
for (let i = 0; i < rows.length; i += 1) {
|
|
683
709
|
for (let j = i + 1; j < rows.length; j += 1) {
|
package/src/models.mjs
CHANGED
|
@@ -26,6 +26,55 @@ export const KNOWN_MODELS = {
|
|
|
26
26
|
},
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
+
// How hard the model thinks before it answers, for the CLIs that take it per run. Claude Code
|
|
30
|
+
// reads --effort; Codex reads model_reasoning_effort as a config override. Gemini and OpenCode
|
|
31
|
+
// have no per-run knob MADRE can name truthfully (OpenCode's --variant means something different
|
|
32
|
+
// for each provider), so they get no chooser at all rather than one that guesses.
|
|
33
|
+
//
|
|
34
|
+
// Ordered lightest first: the composer draws them as a ladder, and the order is the meaning.
|
|
35
|
+
//
|
|
36
|
+
// Codex's levels belong to each model, not to the CLI. Its own cache (~/.codex/models_cache.json)
|
|
37
|
+
// lists the ones each model takes, and the API refuses any other with a 400 — «'minimal' is not
|
|
38
|
+
// supported with the 'gpt-5.5' model» — that used to reach the room as a lone «{». The list here is
|
|
39
|
+
// every name Codex has used, in order; a turn gets only the ones its model takes.
|
|
40
|
+
export const EFFORTS = {
|
|
41
|
+
claude: ['low', 'medium', 'high', 'xhigh', 'max'],
|
|
42
|
+
codex: ['minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'],
|
|
43
|
+
};
|
|
44
|
+
// What a Codex with no cache to read is offered: the levels its models have always taken.
|
|
45
|
+
const CODEX_SAFE = ['low', 'medium', 'high'];
|
|
46
|
+
|
|
47
|
+
export function isValidEffort(agentId, effort) {
|
|
48
|
+
return typeof effort === 'string' && (EFFORTS[agentId] ?? []).includes(effort);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Whose model a name is, when the name says so. Codex, Claude and Gemini each answer only to their
|
|
52
|
+
// own vendor's models, and a name from another one reaches the CLI as an API error that reads
|
|
53
|
+
// like anything but "wrong model" (Gemini: `models/gpt-5.6-sol is not found for API version
|
|
54
|
+
// v1beta`). OpenCode is left out on purpose: it speaks to many providers, so any name may be its.
|
|
55
|
+
const VENDOR = [
|
|
56
|
+
['codex', /^(gpt-|o\d|codex-|chatgpt-)/i],
|
|
57
|
+
['claude', /^(claude|fable|opus|sonnet|haiku)\b/i],
|
|
58
|
+
['gemini', /^(gemini|gemma|auto$)/i],
|
|
59
|
+
];
|
|
60
|
+
export function modelOwner(model) {
|
|
61
|
+
return VENDOR.find(([, pattern]) => pattern.test(String(model ?? '')))?.[0] ?? null;
|
|
62
|
+
}
|
|
63
|
+
// True when the name clearly belongs to another of the three: the turn is better off on the
|
|
64
|
+
// agent's own default than sent to fail.
|
|
65
|
+
export function belongsElsewhere(agentId, model) {
|
|
66
|
+
if (!VENDOR.some(([id]) => id === agentId)) return false;
|
|
67
|
+
const owner = modelOwner(model);
|
|
68
|
+
return Boolean(owner) && owner !== agentId;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// A CLI saying it does not know the model it was given, in any of the ways the four say it.
|
|
72
|
+
export const MODEL_NOT_FOUND = /models?\/\S+ is not found|not supported for generateContent|model[_ ]not[_ ]found|unknown model|invalid model|model (?:\S+ )?(?:does not exist|is not available|not found)|issue with the selected model/i;
|
|
73
|
+
|
|
74
|
+
export function parseCodexDefaultEffort(toml) {
|
|
75
|
+
return String(toml ?? '').match(/^\s*model_reasoning_effort\s*=\s*"([^"]+)"/m)?.[1] ?? null;
|
|
76
|
+
}
|
|
77
|
+
|
|
29
78
|
export function parseCodexModelCache(json) {
|
|
30
79
|
try {
|
|
31
80
|
const data = JSON.parse(json);
|
|
@@ -42,6 +91,49 @@ export function parseCodexModelCache(json) {
|
|
|
42
91
|
}
|
|
43
92
|
}
|
|
44
93
|
|
|
94
|
+
// Per model, the efforts it takes (lightest first) and the one it uses when told nothing.
|
|
95
|
+
export function parseCodexEfforts(json) {
|
|
96
|
+
try {
|
|
97
|
+
const data = JSON.parse(json);
|
|
98
|
+
const byModel = {};
|
|
99
|
+
const walk = (node) => {
|
|
100
|
+
if (!node || typeof node !== 'object') return;
|
|
101
|
+
if (typeof node.slug === 'string' && Array.isArray(node.supported_reasoning_levels)) {
|
|
102
|
+
const levels = EFFORTS.codex.filter((level) => node.supported_reasoning_levels.some((one) => one?.effort === level));
|
|
103
|
+
if (levels.length) byModel[node.slug] = { levels, default: levels.includes(node.default_reasoning_level) ? node.default_reasoning_level : null };
|
|
104
|
+
}
|
|
105
|
+
for (const value of Object.values(node)) walk(value);
|
|
106
|
+
};
|
|
107
|
+
walk(data);
|
|
108
|
+
return byModel;
|
|
109
|
+
} catch {
|
|
110
|
+
return {};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// The levels one Codex model takes: its own, from the cache; for a name the cache does not list,
|
|
115
|
+
// the ones every listed model shares, so a model MADRE cannot read never meets a refusal.
|
|
116
|
+
export function codexLevels(byModel, model) {
|
|
117
|
+
if (model && byModel[model]) return byModel[model].levels;
|
|
118
|
+
const lists = Object.values(byModel).map((one) => one.levels);
|
|
119
|
+
return lists.length ? lists.reduce((shared, levels) => shared.filter((level) => levels.includes(level))) : CODEX_SAFE;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function readCodexEfforts({ home = homedir() } = {}) {
|
|
123
|
+
const [cache, toml] = await Promise.all([
|
|
124
|
+
readFile(join(home, '.codex', 'models_cache.json'), 'utf8').catch(() => ''),
|
|
125
|
+
readFile(join(home, '.codex', 'config.toml'), 'utf8').catch(() => ''),
|
|
126
|
+
]);
|
|
127
|
+
return { byModel: parseCodexEfforts(cache), model: parseCodexDefaultModel(toml), effort: parseCodexDefaultEffort(toml) };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Whether a turn's effort reaches Codex: the model it runs on — the one chosen, or the CLI's
|
|
131
|
+
// default — has to take it.
|
|
132
|
+
export async function codexTakes(model, effort, { home = homedir() } = {}) {
|
|
133
|
+
const read = await readCodexEfforts({ home });
|
|
134
|
+
return codexLevels(read.byModel, model || read.model).includes(effort);
|
|
135
|
+
}
|
|
136
|
+
|
|
45
137
|
export function parseCodexDefaultModel(toml) {
|
|
46
138
|
return String(toml ?? '').match(/^\s*model\s*=\s*"([^"]+)"/m)?.[1] ?? null;
|
|
47
139
|
}
|
|
@@ -58,9 +150,22 @@ export async function discoverModels({ agents = [], config = {}, home = homedir(
|
|
|
58
150
|
const known = KNOWN_MODELS[agent.id] ?? { defaults: [], note: '' };
|
|
59
151
|
let discovered = [];
|
|
60
152
|
let cliDefault = null;
|
|
153
|
+
let effortDefault = null;
|
|
154
|
+
let efforts = EFFORTS[agent.id] ?? [];
|
|
155
|
+
let effortsByModel;
|
|
156
|
+
let effortsShared;
|
|
61
157
|
if (agent.id === 'codex') {
|
|
62
158
|
discovered = parseCodexModelCache(await readFile(join(home, '.codex', 'models_cache.json'), 'utf8').catch(() => ''));
|
|
63
|
-
|
|
159
|
+
const read = await readCodexEfforts({ home });
|
|
160
|
+
cliDefault = read.model;
|
|
161
|
+
effortDefault = read.effort;
|
|
162
|
+
// The ladder is the model's: the CLI default's here, each model's in the composer once it is
|
|
163
|
+
// chosen. What the CLI does when told nothing is the config's level if that model takes it,
|
|
164
|
+
// or the model's own.
|
|
165
|
+
efforts = codexLevels(read.byModel, cliDefault);
|
|
166
|
+
effortsShared = codexLevels(read.byModel, null);
|
|
167
|
+
effortsByModel = Object.fromEntries(Object.entries(read.byModel).map(([slug, one]) => [slug, { levels: one.levels, default: one.levels.includes(read.effort) ? read.effort : one.default }]));
|
|
168
|
+
if (!efforts.includes(effortDefault)) effortDefault = (cliDefault && read.byModel[cliDefault]?.default) || null;
|
|
64
169
|
}
|
|
65
170
|
if (agent.id === 'opencode' && agent.detected) {
|
|
66
171
|
discovered = await listOpenCode(agent).catch(() => []);
|
|
@@ -71,6 +176,9 @@ export async function discoverModels({ agents = [], config = {}, home = homedir(
|
|
|
71
176
|
models: unique([...custom, ...discovered, ...known.defaults]),
|
|
72
177
|
default: cliDefault,
|
|
73
178
|
note: known.note,
|
|
179
|
+
efforts,
|
|
180
|
+
effortDefault: isValidEffort(agent.id, effortDefault) ? effortDefault : null,
|
|
181
|
+
...(effortsByModel ? { effortsByModel, effortsShared } : {}),
|
|
74
182
|
source: discovered.length ? 'discovered' : 'known',
|
|
75
183
|
};
|
|
76
184
|
}
|
package/src/modules/ahp.mjs
CHANGED
|
@@ -30,12 +30,12 @@ async function detect(projectRoot) {
|
|
|
30
30
|
async function preflight(projectRoot, { toplevel = gitToplevel, npx = findOnPath } = {}) {
|
|
31
31
|
const root = await realpath(projectRoot).catch(() => resolve(projectRoot));
|
|
32
32
|
const problems = [];
|
|
33
|
-
if (!(await npx('npx'))) problems.push('npx is not on PATH for the MADRE server; start MADRE from a terminal where `npx --version` works.');
|
|
33
|
+
if (!(await npx('npx'))) problems.push(t('npx is not on PATH for the MADRE server; start MADRE from a terminal where `npx --version` works.'));
|
|
34
34
|
const top = await toplevel(root);
|
|
35
35
|
if (!top) {
|
|
36
|
-
problems.push('The project is not a git repository. AHP+ pins itself to the git root: run `git init` in the project first.');
|
|
36
|
+
problems.push(t('The project is not a git repository. AHP+ pins itself to the git root: run `git init` in the project first.'));
|
|
37
37
|
} else if (top !== root) {
|
|
38
|
-
problems.push(
|
|
38
|
+
problems.push(t('The project\'s git root is {top}, not the project itself. AHP+ would install there. Run `git init` inside the project, or remove the stray repository at {top}.', { top }));
|
|
39
39
|
}
|
|
40
40
|
return { ok: problems.length === 0, problems, gitRoot: top };
|
|
41
41
|
}
|
|
@@ -50,7 +50,7 @@ function installCommand({ agents = [] }) {
|
|
|
50
50
|
export default defineModule({
|
|
51
51
|
id: 'ahp',
|
|
52
52
|
kind: 'installer',
|
|
53
|
-
name: '
|
|
53
|
+
name: 'Continuity',
|
|
54
54
|
vendor: 'Agent Handoff Protocol Plus',
|
|
55
55
|
package: PACKAGE,
|
|
56
56
|
tracks: { name: PACKAGE, npm: PACKAGE },
|
package/src/modules/ash.mjs
CHANGED
|
@@ -20,7 +20,7 @@ import { t } from '../i18n.mjs';
|
|
|
20
20
|
export default defineModule({
|
|
21
21
|
id: 'ash',
|
|
22
22
|
configKey: 'ash',
|
|
23
|
-
name: '
|
|
23
|
+
name: 'Token saver',
|
|
24
24
|
vendor: 'MADRE',
|
|
25
25
|
version: '1.0.0',
|
|
26
26
|
summary: 'Asks every agent for compact prose without altering what you write. MADRE shows each turn\'s input, output and cache use; savings depend on the agent, task and cache, so no fixed percentage is promised.',
|