feat(brain): cross-wing tunnels — bidirectional wikilinks + auto-detect
Adds the `brain_tunnel` MCP tool and auto-tunnel behaviour for `brain_write`, so concepts that appear in multiple wings become navigable from any of them. New surface in package brain: - WriteTunnel(brainDir, src, tgt) — appends a `## See also` bidirectional wikilink between two notes in different wings. Idempotent (link not duplicated on re-call) and reuses an existing See also section. - DetectTunnels(brainDir, content) — walks brain/wiki/, returns TunnelCandidates for notes whose title appears in content. Tags whole-word case-insensitive hits as Exact=true and substring-only hits as Exact=false. - AutoTunnel(brainDir, src, content) — wraps DetectTunnels: writes cross-wing exact matches, stages fuzzy matches into brain/raw/tunnel-candidates-<YYYY-MM-DD>.md for human review. MCP wiring: - `brain_tunnel` tool: explicit manual link (source, target). - `brain_write` with wing+hall now triggers AutoTunnel on the new content. Failures are logged and never abort the primary write. readTitleAndCreated also humanises the slug fallback (hyphens → spaces) so titleless notes participate in content matching. Closes hyperguild#16. Tests: idempotency, same-wing rejection, missing-note rejection, See-also reuse, exact/fuzzy detection, slug fallback, MCP tool happy path, auto-tunnel hook (cross-wing exact → linked; same-wing → skipped; fuzzy → candidates file).
This commit is contained in:
@@ -60,6 +60,14 @@ func (s *Server) tools() []map[string]any {
|
||||
"hall": enum("optional memory type (requires wing)", halls...),
|
||||
}),
|
||||
},
|
||||
{
|
||||
"name": "brain_tunnel",
|
||||
"description": "Create an explicit bidirectional [[wikilink]] between two notes in different wings. Idempotent.",
|
||||
"inputSchema": schema([]string{"source", "target"}, map[string]any{
|
||||
"source": str("path of source note relative to brain dir, e.g. wiki/jepa-fx/decisions/val-vol.md"),
|
||||
"target": str("path of target note (must be in a different wing)"),
|
||||
}),
|
||||
},
|
||||
{
|
||||
"name": "brain_index",
|
||||
"description": "Regenerate _index.md (Map of Content) for one or all wings under brain/wiki/. Auto-called after brain_write with wing+hall.",
|
||||
@@ -174,16 +182,38 @@ func (s *Server) brainWrite(ctx context.Context, args json.RawMessage) (json.Raw
|
||||
return nil, err
|
||||
}
|
||||
// Auto-regenerate the wing _index.md when the write landed in the
|
||||
// structured wiki. A failure here is best-effort — log and move on,
|
||||
// since the note itself is already written.
|
||||
// structured wiki, and auto-tunnel cross-wing matches. Both are
|
||||
// best-effort: the note is already written.
|
||||
if a.Wing != "" && a.Hall != "" {
|
||||
if err := brain.BuildWingIndex(s.brainDir, a.Wing); err != nil {
|
||||
slog.Warn("brain_write: auto-index failed", "wing", a.Wing, "err", err)
|
||||
}
|
||||
if err := brain.AutoTunnel(s.brainDir, relPath, a.Content); err != nil {
|
||||
slog.Warn("brain_write: auto-tunnel failed", "src", relPath, "err", err)
|
||||
}
|
||||
}
|
||||
return json.Marshal(map[string]string{"path": relPath})
|
||||
}
|
||||
|
||||
type brainTunnelArgs struct {
|
||||
Source string `json:"source"`
|
||||
Target string `json:"target"`
|
||||
}
|
||||
|
||||
func (s *Server) brainTunnel(ctx context.Context, args json.RawMessage) (json.RawMessage, error) {
|
||||
var a brainTunnelArgs
|
||||
if err := json.Unmarshal(args, &a); err != nil {
|
||||
return nil, fmt.Errorf("parse args: %w", err)
|
||||
}
|
||||
if a.Source == "" || a.Target == "" {
|
||||
return nil, fmt.Errorf("source and target are required")
|
||||
}
|
||||
if err := brain.WriteTunnel(s.brainDir, a.Source, a.Target); err != nil {
|
||||
return nil, fmt.Errorf("tunnel: %w", err)
|
||||
}
|
||||
return json.Marshal(map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
type brainIndexArgs struct {
|
||||
Wing string `json:"wing,omitempty"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user