@cerefox/memory 1.3.0 → 1.4.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/AGENT_GUIDE.md +11 -8
- package/AGENT_QUICK_REFERENCE.md +12 -5
- package/dist/bin/cerefox.js +214 -58
- package/dist/server-assets/_shared/ef-meta/index.ts +2 -2
- package/dist/server-assets/_shared/mcp-tools/get-document.ts +67 -3
- package/dist/server-assets/_shared/mcp-tools/get-help-content.ts +3 -3
- package/dist/server-assets/_shared/mcp-tools/ingest.ts +2 -1
- package/dist/server-assets/_shared/mcp-tools/partial-edits.ts +82 -14
- package/dist/server-assets/_shared/partial-edits/index.ts +173 -13
- package/dist/server-assets/db/migrations/0021_rename_section_audit_op.sql +36 -0
- package/dist/server-assets/db/rpcs.sql +2 -1
- package/dist/server-assets/db/schema.sql +5 -2
- package/docs/guides/configuration.md +1 -1
- package/docs/guides/connect-agents.md +14 -1
- package/package.json +1 -1
|
@@ -62,25 +62,44 @@ export type EditOperation =
|
|
|
62
62
|
anchor_heading: string;
|
|
63
63
|
scope?: "body_only" | "heading_and_body";
|
|
64
64
|
section_part?: SectionPart;
|
|
65
|
+
}
|
|
66
|
+
| {
|
|
67
|
+
op: "rename_section";
|
|
68
|
+
anchor_heading: string;
|
|
69
|
+
new_heading: string;
|
|
65
70
|
};
|
|
66
71
|
|
|
67
72
|
/** Echo of one applied operation, for audit labels and response text. */
|
|
68
73
|
export interface AppliedOperation {
|
|
69
|
-
op: "insert" | "replace_section" | "delete_section";
|
|
74
|
+
op: "insert" | "replace_section" | "delete_section" | "rename_section";
|
|
70
75
|
/** Resolved anchor path (or "(document)" for end_of_document). */
|
|
71
76
|
path: string;
|
|
72
77
|
/** Human summary: position / scope / section_part actually used. */
|
|
73
78
|
detail: string;
|
|
79
|
+
/**
|
|
80
|
+
* For destructive ops: did the targeted section run to the END of the
|
|
81
|
+
* document it was applied to? (#196 — the last section owns anything
|
|
82
|
+
* appended after it, which is the loss that surprises people.)
|
|
83
|
+
*
|
|
84
|
+
* Decided HERE, at resolution time, rather than by re-parsing the pre-batch
|
|
85
|
+
* document later. In a batch, each operation resolves against the content
|
|
86
|
+
* left by the previous one, so a `rename_section` earlier in the same call
|
|
87
|
+
* means a later op's `path` names a heading the pre-batch outline has never
|
|
88
|
+
* heard of — and a path lookup against that outline silently finds nothing.
|
|
89
|
+
* That is precisely the batch shape `rename_section` was added to enable.
|
|
90
|
+
*/
|
|
91
|
+
reachedEnd?: boolean;
|
|
74
92
|
}
|
|
75
93
|
|
|
76
94
|
/** Anchor matched nothing. The write must never fall back to appending. */
|
|
77
95
|
export class AnchorNotFoundError extends Error {
|
|
78
|
-
|
|
96
|
+
/** `reads` — see AmbiguousPositionError: a read never attempted a write. */
|
|
97
|
+
constructor(anchor: string, outline: OutlineNode[], reads = false) {
|
|
79
98
|
const known = outline.length
|
|
80
99
|
? ` Known headings:\n${outline.map((n) => ` ${n.path}`).join("\n")}`
|
|
81
100
|
: " The document has no headings.";
|
|
82
101
|
super(
|
|
83
|
-
`Anchor not found: "${anchor}"
|
|
102
|
+
`Anchor not found: "${anchor}".${reads ? "" : " No write was performed."}${known}\n` +
|
|
84
103
|
`Anchors match a heading line exactly ("## Title") or a parent path ` +
|
|
85
104
|
`("## Parent > ### Child").`,
|
|
86
105
|
);
|
|
@@ -91,10 +110,11 @@ export class AnchorNotFoundError extends Error {
|
|
|
91
110
|
/** Anchor matched more than one section. Candidates resolve the retry. */
|
|
92
111
|
export class AmbiguousAnchorError extends Error {
|
|
93
112
|
readonly candidates: string[];
|
|
94
|
-
|
|
113
|
+
/** `reads` — see AmbiguousPositionError: a read never attempted a write. */
|
|
114
|
+
constructor(anchor: string, candidates: string[], reads = false) {
|
|
95
115
|
super(
|
|
96
116
|
`Ambiguous anchor: "${anchor}" matches ${candidates.length} sections. ` +
|
|
97
|
-
|
|
117
|
+
`${reads ? "" : "No write was performed. "}Disambiguate by passing one of these paths as anchor_heading:\n` +
|
|
98
118
|
candidates.map((c) => ` ${c}`).join("\n"),
|
|
99
119
|
);
|
|
100
120
|
this.name = "AmbiguousAnchorError";
|
|
@@ -108,7 +128,13 @@ export class AmbiguousAnchorError extends Error {
|
|
|
108
128
|
*/
|
|
109
129
|
export class AmbiguousPositionError extends Error {
|
|
110
130
|
readonly candidates: { section_part: SectionPart; description: string }[];
|
|
111
|
-
|
|
131
|
+
/**
|
|
132
|
+
* `reads` marks a caller that only inspects content (#198's section read).
|
|
133
|
+
* The reassurance "No write was performed" is meant to stop an agent
|
|
134
|
+
* retrying a half-applied batch — on a read it is noise at best, and at
|
|
135
|
+
* worst it implies a write was attempted when none ever could be.
|
|
136
|
+
*/
|
|
137
|
+
constructor(node: OutlineNode, firstChildHeading: string, opName: string, reads = false) {
|
|
112
138
|
const candidates = [
|
|
113
139
|
{
|
|
114
140
|
section_part: "own_body" as const,
|
|
@@ -126,7 +152,7 @@ export class AmbiguousPositionError extends Error {
|
|
|
126
152
|
super(
|
|
127
153
|
`Ambiguous position: "${node.path}" has child sections, so "the end of ` +
|
|
128
154
|
`the section" could mean two different places and ${opName} will not guess. ` +
|
|
129
|
-
|
|
155
|
+
`${reads ? "" : "No write was performed. "}Pass section_part to choose:\n` +
|
|
130
156
|
candidates.map((c) => ` section_part: "${c.section_part}" — ${c.description}`).join("\n"),
|
|
131
157
|
);
|
|
132
158
|
this.name = "AmbiguousPositionError";
|
|
@@ -134,6 +160,30 @@ export class AmbiguousPositionError extends Error {
|
|
|
134
160
|
}
|
|
135
161
|
}
|
|
136
162
|
|
|
163
|
+
/**
|
|
164
|
+
* A rename asked to change the heading's LEVEL, not just its text.
|
|
165
|
+
*
|
|
166
|
+
* `## Q3 plan` → `### Q3 plan` re-parents everything under it: the section
|
|
167
|
+
* stops being a sibling of its neighbours and becomes a child of whichever
|
|
168
|
+
* section precedes it. That is a restructure with a different blast radius
|
|
169
|
+
* from a rename, and doing it silently under the name "rename" is exactly the
|
|
170
|
+
* kind of surprise this contract refuses (#197).
|
|
171
|
+
*/
|
|
172
|
+
export class HeadingLevelChangeError extends Error {
|
|
173
|
+
constructor(fromHeading: string, toHeading: string) {
|
|
174
|
+
const from = (fromHeading.match(/^#+/) ?? [""])[0].length;
|
|
175
|
+
const to = (toHeading.match(/^#+/) ?? [""])[0].length;
|
|
176
|
+
super(
|
|
177
|
+
`rename_section changes heading TEXT, not depth: "${fromHeading}" is level ` +
|
|
178
|
+
`${from} and "${toHeading}" is level ${to}. No write was performed. ` +
|
|
179
|
+
`Changing the level would re-parent every section nested under it. ` +
|
|
180
|
+
`Pass a level-${from} heading (${"#".repeat(from)} ...), or restructure ` +
|
|
181
|
+
`explicitly with delete_section + insert if that is what you meant.`,
|
|
182
|
+
);
|
|
183
|
+
this.name = "HeadingLevelChangeError";
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
137
187
|
/** Structural validation failure of an operations array (before any parsing). */
|
|
138
188
|
export class InvalidOperationError extends Error {
|
|
139
189
|
constructor(index: number, message: string) {
|
|
@@ -240,7 +290,11 @@ function canonicalHeading(text: string): string {
|
|
|
240
290
|
return m ? `${m[1]} ${m[2].trim()}`.trim() : text.trim();
|
|
241
291
|
}
|
|
242
292
|
|
|
243
|
-
export function resolveAnchor(
|
|
293
|
+
export function resolveAnchor(
|
|
294
|
+
outline: OutlineNode[],
|
|
295
|
+
anchorHeading: string,
|
|
296
|
+
reads = false,
|
|
297
|
+
): OutlineNode {
|
|
244
298
|
const anchor = canonicalHeading(anchorHeading);
|
|
245
299
|
|
|
246
300
|
// Try the LITERAL heading first, always — including when the anchor contains
|
|
@@ -253,7 +307,7 @@ export function resolveAnchor(outline: OutlineNode[], anchorHeading: string): Ou
|
|
|
253
307
|
const byHeading = outline.filter((n) => n.heading === anchor);
|
|
254
308
|
if (byHeading.length === 1) return byHeading[0];
|
|
255
309
|
if (byHeading.length > 1) {
|
|
256
|
-
throw new AmbiguousAnchorError(anchor, byHeading.map((n) => n.path));
|
|
310
|
+
throw new AmbiguousAnchorError(anchor, byHeading.map((n) => n.path), reads);
|
|
257
311
|
}
|
|
258
312
|
|
|
259
313
|
// No heading matched literally: interpret it as a parent path.
|
|
@@ -262,11 +316,62 @@ export function resolveAnchor(outline: OutlineNode[], anchorHeading: string): Ou
|
|
|
262
316
|
const byPath = outline.filter((n) => n.path === normalizedPath);
|
|
263
317
|
if (byPath.length === 1) return byPath[0];
|
|
264
318
|
if (byPath.length > 1) {
|
|
265
|
-
throw new AmbiguousAnchorError(anchor, byPath.map((n) => n.path));
|
|
319
|
+
throw new AmbiguousAnchorError(anchor, byPath.map((n) => n.path), reads);
|
|
266
320
|
}
|
|
267
321
|
}
|
|
268
322
|
|
|
269
|
-
throw new AnchorNotFoundError(anchor, outline);
|
|
323
|
+
throw new AnchorNotFoundError(anchor, outline, reads);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* The text a `replace_section` would overwrite — resolved through the SAME
|
|
328
|
+
* functions the write uses (#198).
|
|
329
|
+
*
|
|
330
|
+
* v1.3.0 shipped `replace_section` with no way to see what it was about to
|
|
331
|
+
* destroy: outline mode reports a section's *size*, never its *text*, so the
|
|
332
|
+
* only safe preparation was a full `get_document` — the cost partial edits
|
|
333
|
+
* exist to remove. `cerefox_insert` is guarded structurally (it cannot remove
|
|
334
|
+
* anything); the destructive operation was guarded only by
|
|
335
|
+
* `expected_content_hash`, which protects against a *concurrent* writer, not
|
|
336
|
+
* against a writer who does not know what it is deleting.
|
|
337
|
+
*
|
|
338
|
+
* The binding requirement is that `text` is EXACTLY the extent
|
|
339
|
+
* `replace_section` targets under the same `section_part`. If a read could
|
|
340
|
+
* differ from the write it feeds, the feature would be worse than its absence:
|
|
341
|
+
* absence at least announces itself. That is why this shares `resolveAnchor`
|
|
342
|
+
* and `resolveSectionEnd` rather than reproducing their rules — including the
|
|
343
|
+
* refusal on a section with children, which is the case most likely to diverge.
|
|
344
|
+
*
|
|
345
|
+
* `heading` is returned separately because it is context, not content:
|
|
346
|
+
* `replace_section` keeps the heading, so it is not part of what would be
|
|
347
|
+
* overwritten.
|
|
348
|
+
*/
|
|
349
|
+
export function extractSection(
|
|
350
|
+
content: string,
|
|
351
|
+
anchorHeading: string,
|
|
352
|
+
sectionPart?: SectionPart,
|
|
353
|
+
): {
|
|
354
|
+
heading: string;
|
|
355
|
+
path: string;
|
|
356
|
+
level: number;
|
|
357
|
+
text: string;
|
|
358
|
+
chars: number;
|
|
359
|
+
section_part: SectionPart | null;
|
|
360
|
+
} {
|
|
361
|
+
const outline = parseOutline(content);
|
|
362
|
+
const node = resolveAnchor(outline, anchorHeading, true);
|
|
363
|
+
// Same op label the write would raise under, so an ambiguity refusal reads
|
|
364
|
+
// the same whether the caller was reading or replacing.
|
|
365
|
+
const to = resolveSectionEnd(content, outline, node, sectionPart, "the section read", true);
|
|
366
|
+
const text = content.slice(node.bodyStart, to);
|
|
367
|
+
return {
|
|
368
|
+
heading: node.heading,
|
|
369
|
+
path: node.path,
|
|
370
|
+
level: node.level,
|
|
371
|
+
text,
|
|
372
|
+
chars: text.length,
|
|
373
|
+
section_part: sectionPart ?? null,
|
|
374
|
+
};
|
|
270
375
|
}
|
|
271
376
|
|
|
272
377
|
function firstChild(outline: OutlineNode[], node: OutlineNode): OutlineNode | null {
|
|
@@ -288,6 +393,7 @@ function resolveSectionEnd(
|
|
|
288
393
|
node: OutlineNode,
|
|
289
394
|
sectionPart: SectionPart | undefined,
|
|
290
395
|
opName: string,
|
|
396
|
+
reads = false,
|
|
291
397
|
): number {
|
|
292
398
|
const child = firstChild(outline, node);
|
|
293
399
|
if (!child) return node.subtreeEnd; // leaf: unambiguous
|
|
@@ -315,7 +421,7 @@ function resolveSectionEnd(
|
|
|
315
421
|
//
|
|
316
422
|
// So: no exemption. `hasOwnBody` no longer gates anything, because the
|
|
317
423
|
// presence of children is what makes "the end of this section" ambiguous.
|
|
318
|
-
throw new AmbiguousPositionError(node, child.heading, opName);
|
|
424
|
+
throw new AmbiguousPositionError(node, child.heading, opName, reads);
|
|
319
425
|
}
|
|
320
426
|
|
|
321
427
|
/**
|
|
@@ -386,6 +492,34 @@ function applyOne(
|
|
|
386
492
|
path: node.path,
|
|
387
493
|
detail:
|
|
388
494
|
"replace_section body" + (operation.section_part ? ` (${operation.section_part})` : ""),
|
|
495
|
+
reachedEnd: to >= content.trimEnd().length,
|
|
496
|
+
},
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (operation.op === "rename_section") {
|
|
501
|
+
const node = resolveAnchor(outline, operation.anchor_heading);
|
|
502
|
+
const next = canonicalHeading(operation.new_heading);
|
|
503
|
+
const m = next.match(ATX_HEADING);
|
|
504
|
+
if (!m) {
|
|
505
|
+
throw new InvalidOperationError(
|
|
506
|
+
0,
|
|
507
|
+
`new_heading must be a markdown heading line like "## Title", got ${JSON.stringify(operation.new_heading)}`,
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
if (m[1].length !== node.level) throw new HeadingLevelChangeError(node.heading, next);
|
|
511
|
+
|
|
512
|
+
// Replace the heading LINE only. Not spliceBlock: that normalises
|
|
513
|
+
// surrounding blank lines, which is right for a block of body text and
|
|
514
|
+
// wrong for a single line whose neighbours are the section's own spacing.
|
|
515
|
+
const hadNewline = content[node.bodyStart - 1] === "\n";
|
|
516
|
+
const replacement = next + (hadNewline ? "\n" : "");
|
|
517
|
+
return {
|
|
518
|
+
content: content.slice(0, node.start) + replacement + content.slice(node.bodyStart),
|
|
519
|
+
applied: {
|
|
520
|
+
op: "rename_section",
|
|
521
|
+
path: node.path,
|
|
522
|
+
detail: `rename_section to ${next}`,
|
|
389
523
|
},
|
|
390
524
|
};
|
|
391
525
|
}
|
|
@@ -402,6 +536,7 @@ function applyOne(
|
|
|
402
536
|
path: node.path,
|
|
403
537
|
detail:
|
|
404
538
|
`delete_section (${scope})` + (operation.section_part ? ` (${operation.section_part})` : ""),
|
|
539
|
+
reachedEnd: to >= content.trimEnd().length,
|
|
405
540
|
},
|
|
406
541
|
};
|
|
407
542
|
}
|
|
@@ -453,8 +588,33 @@ export function validateOperations(operations: unknown): EditOperation[] {
|
|
|
453
588
|
if (o.scope !== undefined && o.scope !== "body_only" && o.scope !== "heading_and_body") {
|
|
454
589
|
throw new InvalidOperationError(i, "scope must be body_only or heading_and_body");
|
|
455
590
|
}
|
|
591
|
+
} else if (op === "rename_section") {
|
|
592
|
+
if (typeof o.anchor_heading !== "string" || o.anchor_heading.trim() === "") {
|
|
593
|
+
throw new InvalidOperationError(i, "rename_section requires anchor_heading");
|
|
594
|
+
}
|
|
595
|
+
if (typeof o.new_heading !== "string" || o.new_heading.trim() === "") {
|
|
596
|
+
throw new InvalidOperationError(i, "rename_section requires non-empty new_heading");
|
|
597
|
+
}
|
|
598
|
+
// A rename touches the heading line and nothing else, so a caller that
|
|
599
|
+
// also passed body text has misunderstood which operation they want.
|
|
600
|
+
// Silently ignoring it would lose the text without saying so.
|
|
601
|
+
if (o.text !== undefined) {
|
|
602
|
+
throw new InvalidOperationError(
|
|
603
|
+
i,
|
|
604
|
+
"rename_section changes only the heading and takes no text — use replace_section for the body, or both operations in one call",
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
if (o.section_part !== undefined) {
|
|
608
|
+
throw new InvalidOperationError(
|
|
609
|
+
i,
|
|
610
|
+
"rename_section takes no section_part: it replaces the heading line, so no extent is involved",
|
|
611
|
+
);
|
|
612
|
+
}
|
|
456
613
|
} else {
|
|
457
|
-
throw new InvalidOperationError(
|
|
614
|
+
throw new InvalidOperationError(
|
|
615
|
+
i,
|
|
616
|
+
"op must be insert | replace_section | delete_section | rename_section",
|
|
617
|
+
);
|
|
458
618
|
}
|
|
459
619
|
if (
|
|
460
620
|
o.section_part !== undefined &&
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
-- 0021_rename_section_audit_op.sql — audit the rename operation (iteration 35, #197).
|
|
2
|
+
--
|
|
3
|
+
-- `cerefox_audit_log.operation` is CHECK-constrained, so a new partial-edit
|
|
4
|
+
-- operation cannot be recorded until the constraint knows about it. Migration
|
|
5
|
+
-- 0019 widened it for insert / replace-section / delete-section; this adds
|
|
6
|
+
-- rename-section.
|
|
7
|
+
--
|
|
8
|
+
-- Why the operation exists: there was no way to change a heading's own text.
|
|
9
|
+
-- `replace_section` preserves the heading by design, and delete + re-insert
|
|
10
|
+
-- sacrifices the section's body and position to fix the heading — an agent hit
|
|
11
|
+
-- this on real work (a heading whose date had gone stale), judged that trade
|
|
12
|
+
-- wrong, and left the document stale rather than risk the body. Renaming is
|
|
13
|
+
-- therefore its own operation, and one that structurally cannot touch a body.
|
|
14
|
+
--
|
|
15
|
+
-- Schema version 0.11.0 → 0.11.1. Additive: the constraint only widens, so an
|
|
16
|
+
-- older client against this database is unaffected, and this database against
|
|
17
|
+
-- an older client simply never sees the new value.
|
|
18
|
+
|
|
19
|
+
ALTER TABLE cerefox_audit_log
|
|
20
|
+
DROP CONSTRAINT IF EXISTS cerefox_audit_log_operation_check;
|
|
21
|
+
|
|
22
|
+
ALTER TABLE cerefox_audit_log
|
|
23
|
+
ADD CONSTRAINT cerefox_audit_log_operation_check CHECK (
|
|
24
|
+
operation IN ('create', 'update-content', 'update-metadata', 'delete',
|
|
25
|
+
'status-change', 'archive', 'unarchive', 'restore',
|
|
26
|
+
'relation-set', 'relation-delete',
|
|
27
|
+
'insert', 'replace-section', 'delete-section',
|
|
28
|
+
'rename-section')
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
DO $$
|
|
32
|
+
BEGIN
|
|
33
|
+
RAISE NOTICE
|
|
34
|
+
'Migration 0021: audit log accepts rename-section (iteration 35, #197). '
|
|
35
|
+
'Schema version 0.11.1 — re-apply rpcs.sql on this deploy.';
|
|
36
|
+
END $$;
|
|
@@ -2425,10 +2425,11 @@ SET search_path = public, pg_catalog
|
|
|
2425
2425
|
AS $$
|
|
2426
2426
|
-- Keep in lockstep with the `@version:` marker in schema.sql (cut_release.ts
|
|
2427
2427
|
-- enforces it). Bump whenever schema.sql OR rpcs.sql changes.
|
|
2428
|
+
-- 0.11.1 (iteration 35, #197): audit CHECK accepts 'rename-section'.
|
|
2428
2429
|
-- 0.11.0 supersedes 0.10.6 (v1.2.1, #191): this branch carries that fix plus
|
|
2429
2430
|
-- the partial-edit surface, and both migrations (0019, 0020) are in the
|
|
2430
2431
|
-- sequence, so a store deploying this gets everything from both lines.
|
|
2431
|
-
SELECT '0.11.
|
|
2432
|
+
SELECT '0.11.1'::TEXT;
|
|
2432
2433
|
$$;
|
|
2433
2434
|
|
|
2434
2435
|
-- ── cerefox_content_format_stats ─────────────────────────────────────────────
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
-- Requires extensions: vector (pgvector), uuid-ossp
|
|
6
6
|
-- These are enabled at the top of db_deploy.py before this file is applied.
|
|
7
7
|
--
|
|
8
|
-
-- @version: 0.11.
|
|
8
|
+
-- @version: 0.11.1
|
|
9
9
|
-- The `@version` marker above is read by the schema-version-mismatch banner
|
|
10
10
|
-- (see /api/v1/schema-version). Bump it whenever schema.sql OR rpcs.sql
|
|
11
11
|
-- changes in a way that requires `cerefox server deploy` to be re-run —
|
|
@@ -129,7 +129,10 @@ CREATE TABLE IF NOT EXISTS cerefox_audit_log (
|
|
|
129
129
|
-- iteration 33: partial edits. Distinct from 'update-content'
|
|
130
130
|
-- so the trail separates "added to" from "rewrote" from
|
|
131
131
|
-- "removed" — one entry per operation in a cerefox_edit batch.
|
|
132
|
-
'insert', 'replace-section', 'delete-section'
|
|
132
|
+
'insert', 'replace-section', 'delete-section',
|
|
133
|
+
-- iteration 35 (#197): renaming a heading is neither a
|
|
134
|
+
-- rewrite nor a removal, and the trail should say so.
|
|
135
|
+
'rename-section')
|
|
133
136
|
),
|
|
134
137
|
CONSTRAINT cerefox_audit_log_author_type_check CHECK (author_type IN ('user', 'agent'))
|
|
135
138
|
);
|
|
@@ -273,7 +273,7 @@ cerefox document get <document-id> --version-id <version-id>
|
|
|
273
273
|
| Variable | Default | Description |
|
|
274
274
|
|----------|---------|-------------|
|
|
275
275
|
| `CEREFOX_BACKUP_DIR` | `~/.cerefox/backups` | Local directory where file system backups are stored. Created automatically if it doesn't exist. **Use an absolute path** — a relative value (such as the pre-v0.3.0 `./backups`) resolves against the current working directory, so snapshots scatter depending on where you run the command; `backup create` warns when it sees one. Does **not** follow `CEREFOX_CONFIG_DIR`, so a second environment must set it explicitly. |
|
|
276
|
-
| `CEREFOX_ENV_LABEL` | _(unset)_ | Names a non-production environment (e.g. `staging`). Purely cosmetic and inert when unset. When set: the web UI shows a banner on every page, `doctor` shows `[LABEL]` on its title line, `backup create` puts the label in the snapshot filename and payload, and `backup restore` warns when a snapshot's environment differs from the target's. See [`staging-env.md`](staging-env.md). |
|
|
276
|
+
| `CEREFOX_ENV_LABEL` | _(unset)_ | Names a non-production environment (e.g. `staging`). Purely cosmetic and inert when unset. When set: the web UI shows a banner on every page, `doctor` shows `[LABEL]` on its title line, `backup create` puts the label in the snapshot filename and payload, and `backup restore` warns when a snapshot's environment differs from the target's, and `configure-agent` registers the MCP server as `cerefox-<label>` so a labelled environment sits alongside production instead of overwriting it (v1.4.0, #168). See [`staging-env.md`](staging-env.md). |
|
|
277
277
|
| `CEREFOX_VERSION_RETENTION_HOURS` | **Retired in v1.1.0 — no longer read.** | Version retention is now a property of the store: `cerefox config set version_retention_hours <hours>`, or the **Settings** page. It moved because it used to be passed per-call from each client's environment, so the surviving history depended on which client wrote last. `cerefox doctor` reports the variable if it is still set. |
|
|
278
278
|
|
|
279
279
|
---
|
|
@@ -166,7 +166,20 @@ cerefox configure-agent --tool gemini # ~/.gemini/settings.json
|
|
|
166
166
|
Useful flags: `--dry-run` (print the planned write without touching any file), `--json`
|
|
167
167
|
(machine-readable result), `--config-path <path>` (override the target file), `--no-backup`
|
|
168
168
|
(skip the `.pre-cerefox.bak` backup). The command is idempotent and backs up any existing
|
|
169
|
-
config before writing.
|
|
169
|
+
config before writing.
|
|
170
|
+
|
|
171
|
+
The entry is registered under the server name `cerefox`. If `CEREFOX_ENV_LABEL` is
|
|
172
|
+
set — as it is for a [staging environment](staging-env.md) — the name becomes
|
|
173
|
+
`cerefox-<label>` instead, so a second environment sits **alongside** your production
|
|
174
|
+
entry rather than replacing it, and an agent can hold both at once (v1.4.0, #168).
|
|
175
|
+
The command prints the name it used.
|
|
176
|
+
|
|
177
|
+
A labelled entry additionally carries `CEREFOX_CONFIG_DIR` and `CEREFOX_ENV_LABEL`
|
|
178
|
+
in its `env` block. That is what makes it actually reach the environment it is named
|
|
179
|
+
after: MCP clients spawn a stdio server with the **client's** environment, not the
|
|
180
|
+
shell you ran `configure-agent` in, and a desktop client launched from the dock has
|
|
181
|
+
no shell environment at all. A production entry carries no `env` — unchanged from
|
|
182
|
+
earlier releases. The per-client sections below document the same entries for anyone
|
|
170
183
|
who prefers to edit by hand or needs the remote (`Path A-Remote`) HTTP transport instead.
|
|
171
184
|
|
|
172
185
|
### Path A MCP tools
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cerefox/memory",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Cerefox — user-owned shared memory for AI agents. CLI + stdio MCP server + web UI + ingestion for a knowledge base on your own Supabase project (or fully self-hosted with Cerefox Local).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://github.com/fstamatelopoulos/cerefox",
|