changebook 0.4.3 → 0.4.4
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/dist/sync.js +4 -1
- package/dist/tools.js +166 -17
- package/package.json +1 -1
- package/server.json +2 -2
package/dist/sync.js
CHANGED
|
@@ -156,7 +156,10 @@ export function buildSection(rows, changes, alerts = [], projectName, pendingTas
|
|
|
156
156
|
// · anunciar antes de atacar un encargo
|
|
157
157
|
// · DECIR el riesgo al usuario (QA 2026-07-19: el guardian aviso, el
|
|
158
158
|
// agente lo uso y siguio callado; desde fuera eso es no hacer nada)
|
|
159
|
-
|
|
159
|
+
// · cargar las tools diferidas de UNA vez (benchmark 2026-07-20: 3 de 7
|
|
160
|
+
// llamadas del agente CON atlas eran ToolSearch cargando esquemas de
|
|
161
|
+
// uno en uno — cada una relee ~45k tokens de contexto)
|
|
162
|
+
'Memoria del proyecto en ChangeBook. Oriéntate con UNA llamada a `atlas_project_brief`. Antes de tocar un archivo: `atlas_file_context` con su ruta. Tras cada commit: `atlas_record_change` con el diff, `commit_hash`, `committed_at` y un `summary` tuyo (abarata el análisis; reenviar lo ya registrado es gratis). Tools diferidas: una sola llamada a ToolSearch con todas, nunca de una en una.',
|
|
160
163
|
'',
|
|
161
164
|
...(projectName
|
|
162
165
|
? [
|
package/dist/tools.js
CHANGED
|
@@ -95,11 +95,37 @@ export function quotedInList(values) {
|
|
|
95
95
|
.map((v) => `"${v.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`)
|
|
96
96
|
.join(",");
|
|
97
97
|
}
|
|
98
|
+
export const FILES_CAP = 8;
|
|
99
|
+
export function filesUnionByChange(rows, cap = FILES_CAP) {
|
|
100
|
+
const acc = new Map();
|
|
101
|
+
for (const r of rows) {
|
|
102
|
+
if (!Array.isArray(r.files))
|
|
103
|
+
continue;
|
|
104
|
+
let list = acc.get(r.changelog_id);
|
|
105
|
+
if (!list) {
|
|
106
|
+
list = [];
|
|
107
|
+
acc.set(r.changelog_id, list);
|
|
108
|
+
}
|
|
109
|
+
for (const f of r.files) {
|
|
110
|
+
if (typeof f === "string" && f.length > 0 && !list.includes(f)) {
|
|
111
|
+
list.push(f);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const out = new Map();
|
|
116
|
+
for (const [id, list] of acc) {
|
|
117
|
+
out.set(id, {
|
|
118
|
+
files: list.slice(0, cap),
|
|
119
|
+
more: Math.max(0, list.length - cap),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
98
124
|
// Consultation metering (never billing): each successful read leaves a row in
|
|
99
125
|
// atlas_reads so the web can show "your agent consulted the atlas N times".
|
|
100
126
|
// Best-effort and non-blocking — metering must never break or slow a read.
|
|
101
127
|
// user_id is filled server-side (column default auth.uid()).
|
|
102
|
-
function recordRead(db, tool, projectFilter, charsServed) {
|
|
128
|
+
function recordRead(db, tool, projectFilter, charsServed, latencyMs) {
|
|
103
129
|
const projectId = /project_id=eq\.([0-9a-f-]+)/.exec(projectFilter)?.[1] ?? null;
|
|
104
130
|
void db
|
|
105
131
|
.insertRow("atlas_reads", {
|
|
@@ -107,6 +133,11 @@ function recordRead(db, tool, projectFilter, charsServed) {
|
|
|
107
133
|
tool,
|
|
108
134
|
source: "stdio",
|
|
109
135
|
chars_served: charsServed,
|
|
136
|
+
// Latencia del handler (encargo 0dbbfbf4): nullable a propósito — es
|
|
137
|
+
// metering, jamás contrato, y un servidor viejo simplemente no la manda.
|
|
138
|
+
...(latencyMs !== undefined
|
|
139
|
+
? { latency_ms: Math.max(0, Math.min(Math.round(latencyMs), 600000)) }
|
|
140
|
+
: {}),
|
|
110
141
|
})
|
|
111
142
|
.catch(() => { });
|
|
112
143
|
}
|
|
@@ -157,6 +188,76 @@ export async function derivaContraHead(dir, hash) {
|
|
|
157
188
|
}
|
|
158
189
|
// ── Registration ──────────────────────────────────────────────────────────────
|
|
159
190
|
export function registerTools(server, db) {
|
|
191
|
+
// Espejo del hospedado (T3 del benchmark 2026-07-20): definiciones
|
|
192
|
+
// exportadas por archivo, sin grep. Usos y tests siguen fuera y se dice.
|
|
193
|
+
server.registerTool("atlas_symbol_lookup", {
|
|
194
|
+
title: "Where is this symbol defined?",
|
|
195
|
+
description: `Files where an exported symbol (function/class/const/interface/type/enum) is DEFINED, with its kind and the commit that last touched it. Mechanical index built from every ingested diff.
|
|
196
|
+
|
|
197
|
+
Definitions only — usages and tests are not indexed; grep for those. Exact match first, substring fallback.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
- symbol (required): the identifier to look up.
|
|
201
|
+
- project (recommended): the repo you are working in (folder name or slug).
|
|
202
|
+
|
|
203
|
+
Returns (structured): { symbol, exact_match, matches: [{ symbol, file, kind, commit }] }`,
|
|
204
|
+
inputSchema: {
|
|
205
|
+
symbol: z
|
|
206
|
+
.string()
|
|
207
|
+
.min(2)
|
|
208
|
+
.max(120)
|
|
209
|
+
.describe("Identifier to look up (exported definition)"),
|
|
210
|
+
project: z
|
|
211
|
+
.string()
|
|
212
|
+
.min(1)
|
|
213
|
+
.max(120)
|
|
214
|
+
.describe("Project to scope to (repo folder name or slug)"),
|
|
215
|
+
},
|
|
216
|
+
annotations: {
|
|
217
|
+
readOnlyHint: true,
|
|
218
|
+
destructiveHint: false,
|
|
219
|
+
idempotentHint: true,
|
|
220
|
+
openWorldHint: true,
|
|
221
|
+
},
|
|
222
|
+
}, async ({ symbol, project }) => {
|
|
223
|
+
try {
|
|
224
|
+
const t0 = Date.now();
|
|
225
|
+
const pf = await db.projectFilterFor(project);
|
|
226
|
+
const base = `symbol_index?select=symbol,file,kind,commit_hash&order=symbol.asc&limit=20` +
|
|
227
|
+
pf;
|
|
228
|
+
let matches = await db.rest(`${base}&symbol=eq.${encodeURIComponent(symbol)}`);
|
|
229
|
+
let exact = true;
|
|
230
|
+
if (matches.length === 0) {
|
|
231
|
+
exact = false;
|
|
232
|
+
matches = await db.rest(`${base}&symbol=ilike.${ilikePattern(symbol)}`);
|
|
233
|
+
}
|
|
234
|
+
const lines = [`# Symbol lookup: ${symbol}`, ""];
|
|
235
|
+
if (matches.length === 0) {
|
|
236
|
+
lines.push("Not in the index. It may be unexported, renamed, or defined before the index existed — fall back to grep and SAY you did, instead of guessing.");
|
|
237
|
+
}
|
|
238
|
+
for (const m of matches) {
|
|
239
|
+
lines.push(`- ${m.symbol} (${m.kind}) — ${m.file}${m.commit_hash ? ` · commit ${m.commit_hash.slice(0, 7)}` : ""}`);
|
|
240
|
+
}
|
|
241
|
+
if (matches.length > 0) {
|
|
242
|
+
lines.push("", "Definitions only — usages and tests are not indexed; grep for those.");
|
|
243
|
+
}
|
|
244
|
+
const salida = toolResult(lines.join("\n"), {
|
|
245
|
+
symbol,
|
|
246
|
+
exact_match: exact,
|
|
247
|
+
matches: matches.map((m) => ({
|
|
248
|
+
symbol: m.symbol,
|
|
249
|
+
file: m.file,
|
|
250
|
+
kind: m.kind,
|
|
251
|
+
commit: m.commit_hash?.slice(0, 7) ?? null,
|
|
252
|
+
})),
|
|
253
|
+
});
|
|
254
|
+
recordRead(db, "atlas_symbol_lookup", pf, servedCharsOf(salida), Date.now() - t0);
|
|
255
|
+
return salida;
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
return errorResult(error);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
160
261
|
server.registerTool("atlas_recent_changes", {
|
|
161
262
|
title: "Recent ChangeBook changes",
|
|
162
263
|
description: `List the most recent analyzed code changes from the ChangeBook changelog (newest first).
|
|
@@ -169,7 +270,7 @@ Args:
|
|
|
169
270
|
- search (optional): case-insensitive text filter over the business and technical summaries.
|
|
170
271
|
- project (recommended): the repo you are working in (folder name or slug). The atlas is per-project — always scope to your own project.
|
|
171
272
|
|
|
172
|
-
Returns (structured): { count, offset, has_more, changes: [{ id, date, business_impact, diff_chars, modules: [{ module, risk }] }] }. Pass include_tech for summary_tech.
|
|
273
|
+
Returns (structured): { count, offset, has_more, changes: [{ id, date, commit, business_impact, diff_chars, files, files_more, modules: [{ module, risk }] }] }. files is a capped union (files_more = paths cut). Pass include_tech for summary_tech.
|
|
173
274
|
|
|
174
275
|
Don't use for per-module deep dives — use atlas_module_detail for that.`,
|
|
175
276
|
inputSchema: {
|
|
@@ -191,8 +292,9 @@ Don't use for per-module deep dives — use atlas_module_detail for that.`,
|
|
|
191
292
|
},
|
|
192
293
|
}, async ({ limit, offset, search, project, include_tech }) => {
|
|
193
294
|
try {
|
|
295
|
+
const t0 = Date.now();
|
|
194
296
|
const pf = await db.projectFilterFor(project);
|
|
195
|
-
let query = `changelog?select=id,business_impact,summary_tech,created_at,diff_character_count` +
|
|
297
|
+
let query = `changelog?select=id,business_impact,summary_tech,created_at,diff_character_count,commit_hash` +
|
|
196
298
|
`&order=created_at.desc&limit=${limit}&offset=${offset}` +
|
|
197
299
|
pf;
|
|
198
300
|
if (search) {
|
|
@@ -201,12 +303,14 @@ Don't use for per-module deep dives — use atlas_module_detail for that.`,
|
|
|
201
303
|
}
|
|
202
304
|
const rows = await db.rest(query);
|
|
203
305
|
const modulesByChange = new Map();
|
|
306
|
+
let filesByChange = new Map();
|
|
204
307
|
if (rows.length > 0) {
|
|
205
308
|
const ids = rows.map((r) => r.id).join(",");
|
|
206
309
|
const mods = await db.rest(
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
|
|
310
|
+
// note/tech/excerpt stay out (~1.5k each per row); `files` comes in
|
|
311
|
+
// deliberately (commit mirror, benchmark 2026-07-20) and is served
|
|
312
|
+
// as a capped union, never raw.
|
|
313
|
+
`change_module?select=changelog_id,module,risk,files&changelog_id=in.(${ids})`);
|
|
210
314
|
for (const m of mods) {
|
|
211
315
|
const list = modulesByChange.get(m.changelog_id);
|
|
212
316
|
if (list)
|
|
@@ -214,6 +318,7 @@ Don't use for per-module deep dives — use atlas_module_detail for that.`,
|
|
|
214
318
|
else
|
|
215
319
|
modulesByChange.set(m.changelog_id, [m]);
|
|
216
320
|
}
|
|
321
|
+
filesByChange = filesUnionByChange(mods);
|
|
217
322
|
}
|
|
218
323
|
const changes = rows.map((r) => ({
|
|
219
324
|
id: r.id,
|
|
@@ -226,6 +331,9 @@ Don't use for per-module deep dives — use atlas_module_detail for that.`,
|
|
|
226
331
|
// que casaron los resultados.
|
|
227
332
|
...(include_tech || search ? { summary_tech: r.summary_tech ?? null } : {}),
|
|
228
333
|
diff_chars: r.diff_character_count ?? null,
|
|
334
|
+
commit: r.commit_hash?.slice(0, 7) ?? null,
|
|
335
|
+
files: filesByChange.get(r.id)?.files ?? [],
|
|
336
|
+
files_more: filesByChange.get(r.id)?.more ?? 0,
|
|
229
337
|
modules: (modulesByChange.get(r.id) ?? []).map((m) => ({
|
|
230
338
|
module: m.module,
|
|
231
339
|
risk: m.risk,
|
|
@@ -242,11 +350,13 @@ Don't use for per-module deep dives — use atlas_module_detail for that.`,
|
|
|
242
350
|
const mods = c.modules
|
|
243
351
|
.map((m) => m.module + (m.risk ? ` [${m.risk}]` : ""))
|
|
244
352
|
.join(", ");
|
|
245
|
-
lines.push(`## ${c.date} — ${c.business_impact}`);
|
|
353
|
+
lines.push(`## ${c.date}${c.commit ? ` · ${c.commit}` : ""} — ${c.business_impact}`);
|
|
246
354
|
if ((include_tech || search) && c.summary_tech)
|
|
247
355
|
lines.push(`- Tech: ${c.summary_tech}`);
|
|
248
356
|
if (mods)
|
|
249
357
|
lines.push(`- Modules: ${mods}`);
|
|
358
|
+
if (c.files.length > 0)
|
|
359
|
+
lines.push(`- Files: ${c.files.join(", ")}${c.files_more > 0 ? ` (+${c.files_more} more)` : ""}`);
|
|
250
360
|
lines.push("");
|
|
251
361
|
}
|
|
252
362
|
if (changes.length === 0) {
|
|
@@ -256,7 +366,7 @@ Don't use for per-module deep dives — use atlas_module_detail for that.`,
|
|
|
256
366
|
}
|
|
257
367
|
const changesText = lines.join("\n");
|
|
258
368
|
const salida = toolResult(changesText, output);
|
|
259
|
-
recordRead(db, "atlas_recent_changes", pf, servedCharsOf(salida));
|
|
369
|
+
recordRead(db, "atlas_recent_changes", pf, servedCharsOf(salida), Date.now() - t0);
|
|
260
370
|
return salida;
|
|
261
371
|
}
|
|
262
372
|
catch (error) {
|
|
@@ -288,6 +398,7 @@ Returns (structured): { count, modules: [{ module, domain, risk, changes, last_c
|
|
|
288
398
|
},
|
|
289
399
|
}, async ({ domain, project }) => {
|
|
290
400
|
try {
|
|
401
|
+
const t0 = Date.now();
|
|
291
402
|
const pf = await db.projectFilterFor(project);
|
|
292
403
|
let query =
|
|
293
404
|
// The aggregation below uses only these columns; note/tech/excerpt
|
|
@@ -331,7 +442,7 @@ Returns (structured): { count, modules: [{ module, domain, risk, changes, last_c
|
|
|
331
442
|
}
|
|
332
443
|
const modulesText = lines.join("\n");
|
|
333
444
|
const salida = toolResult(modulesText, output);
|
|
334
|
-
recordRead(db, "atlas_modules", pf, servedCharsOf(salida));
|
|
445
|
+
recordRead(db, "atlas_modules", pf, servedCharsOf(salida), Date.now() - t0);
|
|
335
446
|
return salida;
|
|
336
447
|
}
|
|
337
448
|
catch (error) {
|
|
@@ -372,6 +483,7 @@ Returns (structured): { module, count, changes: [{ date, commit, risk, note, tec
|
|
|
372
483
|
},
|
|
373
484
|
}, async ({ module, limit, include_excerpts, full, project }) => {
|
|
374
485
|
try {
|
|
486
|
+
const t0 = Date.now();
|
|
375
487
|
const pf = await db.projectFilterFor(project);
|
|
376
488
|
const rows = await db.rest(`change_module?select=changelog_id,module,domain,category,risk,files,note,tech,excerpt,created_at` +
|
|
377
489
|
`&module=eq.${encodeURIComponent(module)}&order=created_at.desc&limit=${limit}` +
|
|
@@ -462,7 +574,7 @@ Returns (structured): { module, count, changes: [{ date, commit, risk, note, tec
|
|
|
462
574
|
lines.push(TEMPORAL_CONTRACT, "");
|
|
463
575
|
const detailText = lines.join("\n");
|
|
464
576
|
const salida = toolResult(detailText, output);
|
|
465
|
-
recordRead(db, "atlas_module_detail", pf, servedCharsOf(salida));
|
|
577
|
+
recordRead(db, "atlas_module_detail", pf, servedCharsOf(salida), Date.now() - t0);
|
|
466
578
|
return salida;
|
|
467
579
|
}
|
|
468
580
|
catch (error) {
|
|
@@ -471,7 +583,7 @@ Returns (structured): { module, count, changes: [{ date, commit, risk, note, tec
|
|
|
471
583
|
});
|
|
472
584
|
server.registerTool("atlas_file_context", {
|
|
473
585
|
title: "Context of the files you are about to edit",
|
|
474
|
-
description: `Everything the atlas knows about specific FILES: which module each belongs to, its risk, open regression alerts on those modules,
|
|
586
|
+
description: `Everything the atlas knows about specific FILES: which module each belongs to, its risk, open regression alerts on those modules, how often they changed recently, and the CURRENT value of watched config constants (with the commit it comes from — no need to re-read the file for those).
|
|
475
587
|
|
|
476
588
|
Call this BEFORE editing a file — one cheap call instead of re-reading the code and its git history.
|
|
477
589
|
|
|
@@ -479,7 +591,7 @@ Args:
|
|
|
479
591
|
- files (required): 1-8 repo-relative paths.
|
|
480
592
|
- project (recommended): the repo you are working in (folder name or slug). The atlas is per-project — always scope to your own project.
|
|
481
593
|
|
|
482
|
-
Returns (structured): { files: [{ file, modules: [{ module, risk, changes, last_changed, last_note }], open_alerts }] }`,
|
|
594
|
+
Returns (structured): { files: [{ file, modules: [{ module, risk, changes, last_changed, last_note }], open_alerts, watched_values: [{ name, value, commit }] }] }`,
|
|
483
595
|
inputSchema: {
|
|
484
596
|
files: z.array(z.string().min(1).max(300)).min(1).max(8)
|
|
485
597
|
.describe("Repo-relative paths you are about to edit"),
|
|
@@ -494,6 +606,7 @@ Returns (structured): { files: [{ file, modules: [{ module, risk, changes, last_
|
|
|
494
606
|
},
|
|
495
607
|
}, async ({ files, project }) => {
|
|
496
608
|
try {
|
|
609
|
+
const t0 = Date.now();
|
|
497
610
|
const pf = await db.projectFilterFor(project);
|
|
498
611
|
const paths = [...new Set(files.map(normalizeRepoPath).filter(Boolean))];
|
|
499
612
|
const perFile = await Promise.all(paths.map(async (file) => {
|
|
@@ -522,10 +635,24 @@ Returns (structured): { files: [{ file, modules: [{ module, risk, changes, last_
|
|
|
522
635
|
const moduleNames = [
|
|
523
636
|
...new Set(perFile.flatMap((f) => f.modules.map((m) => m.module))),
|
|
524
637
|
];
|
|
525
|
-
const alerts =
|
|
526
|
-
|
|
638
|
+
const [alerts, watched] = await Promise.all([
|
|
639
|
+
moduleNames.length
|
|
640
|
+
? db.rest(`regression_alerts?select=module,plain,evidence_symbol,evidence_expect&resolved_at=is.null&module=in.(${encodeURIComponent(quotedInList(moduleNames))})&order=created_at.desc&limit=10` +
|
|
641
|
+
pf)
|
|
642
|
+
: Promise.resolve([]),
|
|
643
|
+
// Constantes vigiladas (espejo del hospedado): el valor VIGENTE con
|
|
644
|
+
// su commit, extraído mecánicamente en cada ingesta — la cura del
|
|
645
|
+
// fallo de frescura del benchmark 2026-07-20. Best-effort: sin la
|
|
646
|
+
// tabla, el contexto sigue sirviéndose.
|
|
647
|
+
db
|
|
648
|
+
.rest(`watched_values?select=file,name,value,commit_hash&file=in.(${encodeURIComponent(quotedInList(paths))})&order=name.asc&limit=40` +
|
|
527
649
|
pf)
|
|
528
|
-
|
|
650
|
+
.catch(() => []),
|
|
651
|
+
]);
|
|
652
|
+
const watchedByFile = new Map();
|
|
653
|
+
for (const w of watched) {
|
|
654
|
+
watchedByFile.set(w.file, [...(watchedByFile.get(w.file) ?? []), w]);
|
|
655
|
+
}
|
|
529
656
|
// Refutación al servir (benchmark 2026-07-20): el mismo grep que el
|
|
530
657
|
// guardián corre en el pre-commit, pero aquí, en la consulta que el
|
|
531
658
|
// agente hace ANTES de editar — 4 de 7 alertas abiertas se caían con
|
|
@@ -576,13 +703,35 @@ Returns (structured): { files: [{ file, modules: [{ module, risk, changes, last_
|
|
|
576
703
|
lines.push(` - ⚠ OPEN ALERT: ${plain}`);
|
|
577
704
|
}
|
|
578
705
|
}
|
|
706
|
+
for (const w of watchedByFile.get(f.file) ?? []) {
|
|
707
|
+
lines.push(`- Current value: ${w.name} = ${w.value}` +
|
|
708
|
+
(w.commit_hash
|
|
709
|
+
? ` (as of commit ${w.commit_hash.slice(0, 7)})`
|
|
710
|
+
: ""));
|
|
711
|
+
}
|
|
579
712
|
lines.push("");
|
|
580
713
|
}
|
|
581
714
|
if (!ancla)
|
|
582
715
|
lines.push(TEMPORAL_CONTRACT, "");
|
|
583
716
|
const contextText = lines.join("\n");
|
|
584
|
-
|
|
585
|
-
|
|
717
|
+
// open_alerts entra en el structured desde el 2026-07-20: la
|
|
718
|
+
// descripción lo prometía y solo viajaba en el texto (bug cazado por
|
|
719
|
+
// el verificador adversarial del benchmark).
|
|
720
|
+
const salida = toolResult(contextText, {
|
|
721
|
+
files: perFile.map((f) => ({
|
|
722
|
+
...f,
|
|
723
|
+
open_alerts: f.modules.flatMap((m) => (alertsByModule.get(m.module) ?? []).map((plain) => ({
|
|
724
|
+
module: m.module,
|
|
725
|
+
plain,
|
|
726
|
+
}))),
|
|
727
|
+
watched_values: (watchedByFile.get(f.file) ?? []).map((w) => ({
|
|
728
|
+
name: w.name,
|
|
729
|
+
value: w.value,
|
|
730
|
+
commit: w.commit_hash?.slice(0, 7) ?? null,
|
|
731
|
+
})),
|
|
732
|
+
})),
|
|
733
|
+
});
|
|
734
|
+
recordRead(db, "atlas_file_context", pf, servedCharsOf(salida), Date.now() - t0);
|
|
586
735
|
return salida;
|
|
587
736
|
}
|
|
588
737
|
catch (error) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "changebook",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"mcpName": "io.github.raulbr90/changebook",
|
|
5
5
|
"description": "ChangeBook for coding agents: MCP server (product memory for Claude Code/Codex) + CLI to sign in, analyze changes and sync the product map.",
|
|
6
6
|
"type": "module",
|
package/server.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.raulbr90/changebook",
|
|
4
4
|
"description": "Query your product's living memory: module map + analyzed change history. Read-only MCP tools.",
|
|
5
|
-
"version": "0.4.
|
|
5
|
+
"version": "0.4.4",
|
|
6
6
|
"websiteUrl": "https://changebook.dev",
|
|
7
7
|
"remotes": [
|
|
8
8
|
{
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"registryType": "npm",
|
|
16
16
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
17
17
|
"identifier": "changebook",
|
|
18
|
-
"version": "0.4.
|
|
18
|
+
"version": "0.4.4",
|
|
19
19
|
"transport": {
|
|
20
20
|
"type": "stdio"
|
|
21
21
|
}
|