mirror of
https://github.com/0xWheatyz/handler.git
synced 2026-08-31 14:06:25 +00:00
Merge pull request #27 from 0xWheatyz/claude/tencentdb-agent-memory-hgqjsy
This commit is contained in:
@@ -362,6 +362,21 @@ All routes require `Authorization: Bearer <AUTH_TOKEN>`. `GET /health` is unauth
|
||||
| `GET /shared/log` | Cross-project feed of entries explicitly marked `global` |
|
||||
| `GET /shared/context` · `GET /shared/context/:key` | Read shared key/value facts |
|
||||
| `PUT /shared/context/:key` | Write a shared fact — requires the shared-write token |
|
||||
| `GET /memory/notes` · `GET /memory/notes/:id` | Memory notes (`?project_id=` scopes, `?q=` searches) |
|
||||
| `GET /memory/graph` | The whole note graph (notes + links) in one read |
|
||||
| `POST`/`PATCH`/`DELETE /memory/notes…` · `POST`/`DELETE /memory/links…` | Author notes/links — requires the admin token |
|
||||
|
||||
## Agent memory
|
||||
|
||||
The distilled, linked knowledge layer over the raw log/transcript history: **notes**
|
||||
(facts, decisions, gotchas, runbooks) scoped to a project or global, connected by
|
||||
**links** into a graph the dashboard's Memory page draws. Every launch gets the bundled
|
||||
`handler-memory` MCP server (injected into `--mcp-config` as
|
||||
`python -m handler.mcpserver`, no operator setup), exposing `memory_search`,
|
||||
`memory_get`, `memory_save`, and `memory_link`; a `SessionStart` hook injects the most
|
||||
recent notes in scope, so knowledge from earlier runs arrives without being asked.
|
||||
Notes live only in the database — like everything else, they survive disposable
|
||||
workers by construction — and deleting an agent never deletes what it learned.
|
||||
|
||||
## Hooks
|
||||
|
||||
@@ -382,6 +397,9 @@ Wired into each agent as `python -m handler.hooks <event>`:
|
||||
is denied on the first failure.
|
||||
- **`Notification`** — POSTs a small JSON payload to `WEBHOOK_URL` (no-op when unset).
|
||||
Never blocks the agent on delivery failure.
|
||||
- **`SessionStart`** — memory recall: injects the most recent memory notes in the
|
||||
agent's scope (its project + global) as additional context, plus a pointer at the
|
||||
handler-memory MCP tools. Best-effort; never blocks the session.
|
||||
|
||||
Hook identity travels via environment variables injected at spawn (`HANDLER_AGENT_ID`,
|
||||
`HANDLER_PROJECT_ID`, `HANDLER_AGENT_NAME`, `HANDLER_AGENT_ROLE`, `DATABASE_URL`), since
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
/* Memory page. The shell (sidebar, banners, store) comes from the root layout; this
|
||||
* route contributes only its section, in the shared scroll frame. */
|
||||
"use client";
|
||||
|
||||
import { MemorySection } from "@/components/sections/MemorySection";
|
||||
|
||||
export default function MemoryPage() {
|
||||
return (
|
||||
<div className="main-scroll">
|
||||
<MemorySection />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,508 @@
|
||||
/* Memory — the distilled knowledge agents leave behind, drawn as the web of notes.
|
||||
* Nodes are notes (colored by kind), edges are the links agents/operators asserted
|
||||
* between them. The layout is a small hand-rolled force simulation — deliberately no
|
||||
* chart library, matching the rest of the hand-rolled UI. Reads come from
|
||||
* GET /memory/graph via the store; writes are direct admin-token calls. */
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import type { MemoryLink, MemoryNote, NoteKind } from "@/lib/api";
|
||||
import { useDashboard, type MemoryNoteBody } from "@/components/store";
|
||||
import { Badge, Button, Card, Input, Select, Textarea } from "@/components/ui";
|
||||
import { fmtFull } from "@/lib/format";
|
||||
|
||||
const KIND_COLORS: Record<string, string> = {
|
||||
fact: "var(--lw-blue-400)",
|
||||
decision: "var(--lw-indigo-400)",
|
||||
gotcha: "var(--lw-warning-fg)",
|
||||
runbook: "var(--lw-success-fg)",
|
||||
};
|
||||
const KINDS: NoteKind[] = ["fact", "decision", "gotcha", "runbook"];
|
||||
|
||||
const W = 900;
|
||||
const H = 560;
|
||||
|
||||
interface Pos {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
/* Deterministic force layout: seed on a circle (stable order = stable picture), then
|
||||
* relax with link springs, pairwise repulsion, and a centering pull. Runs to a fixed
|
||||
* iteration budget synchronously — the graphs here are distilled notes, not big data. */
|
||||
function computeLayout(notes: MemoryNote[], links: MemoryLink[]): Map<number, Pos> {
|
||||
const pos = new Map<number, Pos>();
|
||||
const n = notes.length;
|
||||
if (n === 0) return pos;
|
||||
const cx = W / 2;
|
||||
const cy = H / 2;
|
||||
const seedR = Math.min(W, H) / 2 - 80;
|
||||
notes.forEach((note, i) => {
|
||||
const a = (2 * Math.PI * i) / n;
|
||||
pos.set(note.id, { x: cx + seedR * Math.cos(a), y: cy + seedR * Math.sin(a) });
|
||||
});
|
||||
const ids = notes.map((note) => note.id);
|
||||
const iterations = n > 150 ? 80 : 250;
|
||||
const springLen = 120;
|
||||
for (let it = 0; it < iterations; it++) {
|
||||
const temp = 1 - it / iterations; // cool down
|
||||
const force = new Map<number, Pos>(ids.map((id) => [id, { x: 0, y: 0 }]));
|
||||
// Pairwise repulsion.
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
for (let j = i + 1; j < ids.length; j++) {
|
||||
const a = pos.get(ids[i])!;
|
||||
const b = pos.get(ids[j])!;
|
||||
let dx = a.x - b.x;
|
||||
let dy = a.y - b.y;
|
||||
const d2 = Math.max(dx * dx + dy * dy, 1);
|
||||
const d = Math.sqrt(d2);
|
||||
const rep = 2600 / d2;
|
||||
dx = (dx / d) * rep;
|
||||
dy = (dy / d) * rep;
|
||||
const fa = force.get(ids[i])!;
|
||||
const fb = force.get(ids[j])!;
|
||||
fa.x += dx;
|
||||
fa.y += dy;
|
||||
fb.x -= dx;
|
||||
fb.y -= dy;
|
||||
}
|
||||
}
|
||||
// Link springs.
|
||||
for (const ln of links) {
|
||||
const a = pos.get(ln.src_note_id);
|
||||
const b = pos.get(ln.dst_note_id);
|
||||
if (!a || !b) continue;
|
||||
const dx = b.x - a.x;
|
||||
const dy = b.y - a.y;
|
||||
const d = Math.max(Math.sqrt(dx * dx + dy * dy), 1);
|
||||
const pull = (d - springLen) * 0.02;
|
||||
const fa = force.get(ln.src_note_id)!;
|
||||
const fb = force.get(ln.dst_note_id)!;
|
||||
fa.x += (dx / d) * pull;
|
||||
fa.y += (dy / d) * pull;
|
||||
fb.x -= (dx / d) * pull;
|
||||
fb.y -= (dy / d) * pull;
|
||||
}
|
||||
// Centering pull + apply.
|
||||
for (const id of ids) {
|
||||
const p = pos.get(id)!;
|
||||
const f = force.get(id)!;
|
||||
f.x += (cx - p.x) * 0.005;
|
||||
f.y += (cy - p.y) * 0.005;
|
||||
p.x += Math.max(-12, Math.min(12, f.x)) * temp;
|
||||
p.y += Math.max(-12, Math.min(12, f.y)) * temp;
|
||||
p.x = Math.max(30, Math.min(W - 30, p.x));
|
||||
p.y = Math.max(24, Math.min(H - 24, p.y));
|
||||
}
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
const emptyForm: MemoryNoteBody = { title: "", body: "", kind: "fact", project_id: "", tags: [] };
|
||||
|
||||
export function MemorySection() {
|
||||
const s = useDashboard();
|
||||
const [projectFilter, setProjectFilter] = useState("");
|
||||
const [selectedId, setSelectedId] = useState<number | null>(null);
|
||||
const [hoverId, setHoverId] = useState<number | null>(null);
|
||||
const [form, setForm] = useState<MemoryNoteBody>(emptyForm);
|
||||
const [tagsText, setTagsText] = useState("");
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [linkDst, setLinkDst] = useState("");
|
||||
const [linkRelation, setLinkRelation] = useState("relates_to");
|
||||
|
||||
const notes = useMemo(() => {
|
||||
if (!projectFilter) return s.memory.notes;
|
||||
if (projectFilter === "global") return s.memory.notes.filter((n) => !n.project_id);
|
||||
return s.memory.notes.filter(
|
||||
(n) => n.project_id === projectFilter || !n.project_id,
|
||||
);
|
||||
}, [s.memory.notes, projectFilter]);
|
||||
|
||||
const links = useMemo(() => {
|
||||
const inScope = new Set(notes.map((n) => n.id));
|
||||
return s.memory.links.filter(
|
||||
(ln) => inScope.has(ln.src_note_id) && inScope.has(ln.dst_note_id),
|
||||
);
|
||||
}, [s.memory.links, notes]);
|
||||
|
||||
/* Recompute only when the graph's shape actually changes, not on every poll. */
|
||||
const layoutKey = useMemo(
|
||||
() =>
|
||||
notes.map((n) => n.id).join(",") +
|
||||
"|" +
|
||||
links.map((ln) => ln.id).join(","),
|
||||
[notes, links],
|
||||
);
|
||||
const layout = useMemo(
|
||||
() => computeLayout(notes, links),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[layoutKey],
|
||||
);
|
||||
|
||||
const degree = useMemo(() => {
|
||||
const d = new Map<number, number>();
|
||||
for (const ln of links) {
|
||||
d.set(ln.src_note_id, (d.get(ln.src_note_id) ?? 0) + 1);
|
||||
d.set(ln.dst_note_id, (d.get(ln.dst_note_id) ?? 0) + 1);
|
||||
}
|
||||
return d;
|
||||
}, [links]);
|
||||
|
||||
const selected = notes.find((n) => n.id === selectedId) ?? null;
|
||||
const focusId = hoverId ?? selectedId;
|
||||
const neighborhood = useMemo(() => {
|
||||
if (focusId == null) return null;
|
||||
const set = new Set<number>([focusId]);
|
||||
for (const ln of links) {
|
||||
if (ln.src_note_id === focusId) set.add(ln.dst_note_id);
|
||||
if (ln.dst_note_id === focusId) set.add(ln.src_note_id);
|
||||
}
|
||||
return set;
|
||||
}, [focusId, links]);
|
||||
|
||||
const selectedLinks = useMemo(
|
||||
() =>
|
||||
selected
|
||||
? links.filter(
|
||||
(ln) => ln.src_note_id === selected.id || ln.dst_note_id === selected.id,
|
||||
)
|
||||
: [],
|
||||
[selected, links],
|
||||
);
|
||||
|
||||
const titleOf = (id: number) => s.memory.notes.find((n) => n.id === id)?.title ?? `#${id}`;
|
||||
|
||||
const parseTags = (text: string) =>
|
||||
text
|
||||
.split(",")
|
||||
.map((t) => t.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const saveNote = async () => {
|
||||
const body = { ...form, tags: parseTags(tagsText) };
|
||||
const ok =
|
||||
editing && selected
|
||||
? await s.updateMemoryNote(selected.id, body)
|
||||
: await s.createMemoryNote(body);
|
||||
if (ok) {
|
||||
setForm(emptyForm);
|
||||
setTagsText("");
|
||||
setEditing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const startEdit = () => {
|
||||
if (!selected) return;
|
||||
setForm({
|
||||
title: selected.title,
|
||||
body: selected.body,
|
||||
kind: (KINDS.includes(selected.kind as NoteKind) ? selected.kind : "fact") as NoteKind,
|
||||
project_id: selected.project_id ?? "",
|
||||
tags: selected.tags ?? [],
|
||||
});
|
||||
setTagsText((selected.tags ?? []).join(", "));
|
||||
setEditing(true);
|
||||
};
|
||||
|
||||
const addLink = async () => {
|
||||
if (!selected || !linkDst) return;
|
||||
const ok = await s.createMemoryLink(selected.id, Number(linkDst), linkRelation.trim());
|
||||
if (ok) setLinkDst("");
|
||||
};
|
||||
|
||||
const projectOptions = [
|
||||
{ value: "", label: "All projects" },
|
||||
{ value: "global", label: "Global only" },
|
||||
...s.projects.map((p) => ({ value: p.id, label: p.id })),
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="section-head">
|
||||
<div className="section-title">Memory</div>
|
||||
<div className="section-desc">
|
||||
The web of notes agents and operators leave behind — facts, decisions, gotchas,
|
||||
runbooks — and how they connect.
|
||||
</div>
|
||||
</div>
|
||||
<div className="section-body">
|
||||
<div className="hstack wrap" style={{ alignItems: "flex-end", gap: 12 }}>
|
||||
<div style={{ width: 220 }}>
|
||||
<Select
|
||||
label="Scope"
|
||||
value={projectFilter}
|
||||
onChange={setProjectFilter}
|
||||
options={projectOptions}
|
||||
/>
|
||||
</div>
|
||||
<div className="hstack" style={{ gap: 8, paddingBottom: 6 }}>
|
||||
{KINDS.map((k) => (
|
||||
<span key={k} className="hstack" style={{ gap: 5, alignItems: "center" }}>
|
||||
<span
|
||||
style={{
|
||||
width: 9,
|
||||
height: 9,
|
||||
borderRadius: "50%",
|
||||
background: KIND_COLORS[k],
|
||||
display: "inline-block",
|
||||
}}
|
||||
/>
|
||||
<span className="faint" style={{ fontSize: "var(--text-xs)" }}>
|
||||
{k}
|
||||
</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{notes.length === 0 ? (
|
||||
<div className="empty">
|
||||
No memory notes yet. Agents write them with the handler-memory MCP tools
|
||||
(memory_save / memory_link); you can add one below.
|
||||
</div>
|
||||
) : (
|
||||
<Card>
|
||||
<svg
|
||||
viewBox={`0 0 ${W} ${H}`}
|
||||
style={{ width: "100%", height: "auto", display: "block" }}
|
||||
role="img"
|
||||
aria-label="Graph of memory notes and their links"
|
||||
>
|
||||
{links.map((ln) => {
|
||||
const a = layout.get(ln.src_note_id);
|
||||
const b = layout.get(ln.dst_note_id);
|
||||
if (!a || !b) return null;
|
||||
const focused =
|
||||
focusId != null &&
|
||||
(ln.src_note_id === focusId || ln.dst_note_id === focusId);
|
||||
return (
|
||||
<g key={ln.id}>
|
||||
<line
|
||||
x1={a.x}
|
||||
y1={a.y}
|
||||
x2={b.x}
|
||||
y2={b.y}
|
||||
stroke={focused ? "var(--lw-blue-300)" : "var(--border-strong)"}
|
||||
strokeWidth={focused ? 1.6 : 1}
|
||||
opacity={neighborhood && !focused ? 0.25 : 0.8}
|
||||
/>
|
||||
{focused && (
|
||||
<text
|
||||
x={(a.x + b.x) / 2}
|
||||
y={(a.y + b.y) / 2 - 4}
|
||||
textAnchor="middle"
|
||||
fill="var(--text-faint)"
|
||||
fontSize={10}
|
||||
>
|
||||
{ln.relation}
|
||||
</text>
|
||||
)}
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
{notes.map((n) => {
|
||||
const p = layout.get(n.id);
|
||||
if (!p) return null;
|
||||
const r = 6 + Math.min(degree.get(n.id) ?? 0, 6);
|
||||
const dimmed = neighborhood != null && !neighborhood.has(n.id);
|
||||
const isSelected = n.id === selectedId;
|
||||
return (
|
||||
<g
|
||||
key={n.id}
|
||||
transform={`translate(${p.x},${p.y})`}
|
||||
style={{ cursor: "pointer" }}
|
||||
opacity={dimmed ? 0.3 : 1}
|
||||
onMouseEnter={() => setHoverId(n.id)}
|
||||
onMouseLeave={() => setHoverId(null)}
|
||||
onClick={() => {
|
||||
setSelectedId(n.id === selectedId ? null : n.id);
|
||||
setEditing(false);
|
||||
}}
|
||||
>
|
||||
<circle
|
||||
r={r}
|
||||
fill={KIND_COLORS[n.kind] ?? "var(--lw-neutral-fg)"}
|
||||
stroke={isSelected ? "var(--lw-white)" : "var(--surface-page)"}
|
||||
strokeWidth={isSelected ? 2 : 1}
|
||||
/>
|
||||
<text
|
||||
y={r + 12}
|
||||
textAnchor="middle"
|
||||
fill={dimmed ? "var(--text-faint)" : "var(--text-muted)"}
|
||||
fontSize={10.5}
|
||||
>
|
||||
{n.title.length > 24 ? n.title.slice(0, 24) + "…" : n.title}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{selected && (
|
||||
<Card>
|
||||
<div className="card-head" style={{ marginBottom: 10 }}>
|
||||
<span
|
||||
className="card-title"
|
||||
style={{ fontSize: "var(--text-md)", color: "var(--text-heading)" }}
|
||||
>
|
||||
{selected.title}
|
||||
</span>
|
||||
<span className="hstack" style={{ gap: 8 }}>
|
||||
<Badge>{selected.kind}</Badge>
|
||||
<Badge>{selected.project_id ?? "global"}</Badge>
|
||||
</span>
|
||||
</div>
|
||||
<p style={{ whiteSpace: "pre-wrap", margin: 0 }}>{selected.body}</p>
|
||||
<p className="faint" style={{ fontSize: "var(--text-xs)", margin: "10px 0 0" }}>
|
||||
{selected.agent_id != null ? `written by agent ${selected.agent_id}` : "operator-authored"}
|
||||
{" · updated "}
|
||||
{fmtFull(selected.updated_at)}
|
||||
{selected.tags?.length ? ` · tags: ${selected.tags.join(", ")}` : ""}
|
||||
</p>
|
||||
|
||||
{selectedLinks.length > 0 && (
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<div className="eyebrow" style={{ marginBottom: 6 }}>
|
||||
Links
|
||||
</div>
|
||||
{selectedLinks.map((ln) => {
|
||||
const otherId =
|
||||
ln.src_note_id === selected.id ? ln.dst_note_id : ln.src_note_id;
|
||||
const outgoing = ln.src_note_id === selected.id;
|
||||
return (
|
||||
<div key={ln.id} className="hstack" style={{ gap: 8, marginBottom: 4 }}>
|
||||
<span className="faint" style={{ fontSize: "var(--text-xs)" }}>
|
||||
{outgoing ? `${ln.relation} →` : `← ${ln.relation}`}
|
||||
</span>
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setSelectedId(otherId);
|
||||
}}
|
||||
>
|
||||
{titleOf(otherId)}
|
||||
</a>
|
||||
<Button size="sm" variant="ghost" onClick={() => s.deleteMemoryLink(ln.id)}>
|
||||
remove
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="hstack wrap mt14" style={{ gap: 8, alignItems: "flex-end" }}>
|
||||
<div style={{ width: 260 }}>
|
||||
<Select
|
||||
label="Link to"
|
||||
value={linkDst}
|
||||
onChange={setLinkDst}
|
||||
options={[
|
||||
{ value: "", label: "— pick a note —" },
|
||||
...notes
|
||||
.filter((n) => n.id !== selected.id)
|
||||
.map((n) => ({ value: String(n.id), label: n.title })),
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ width: 160 }}>
|
||||
<Input label="Relation" value={linkRelation} onChange={setLinkRelation} />
|
||||
</div>
|
||||
<Button disabled={!linkDst} onClick={addLink}>
|
||||
Link
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={startEdit}>
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={async () => {
|
||||
await s.deleteMemoryNote(selected.id);
|
||||
setSelectedId(null);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Card>
|
||||
<div className="card-head" style={{ marginBottom: 14 }}>
|
||||
<span
|
||||
className="card-title"
|
||||
style={{ fontSize: "var(--text-md)", color: "var(--text-heading)" }}
|
||||
>
|
||||
{editing && selected ? `Edit note: ${selected.title}` : "New note"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="form-grid">
|
||||
<Input
|
||||
label="Title"
|
||||
value={form.title}
|
||||
onChange={(v) => setForm((f) => ({ ...f, title: v }))}
|
||||
placeholder="Short, searchable headline"
|
||||
/>
|
||||
<Select
|
||||
label="Kind"
|
||||
value={form.kind}
|
||||
onChange={(v) => setForm((f) => ({ ...f, kind: v as NoteKind }))}
|
||||
options={KINDS.map((k) => ({ value: k, label: k }))}
|
||||
/>
|
||||
<Select
|
||||
label="Project"
|
||||
value={form.project_id}
|
||||
onChange={(v) => setForm((f) => ({ ...f, project_id: v }))}
|
||||
options={[
|
||||
{ value: "", label: "Global" },
|
||||
...s.projects.map((p) => ({ value: p.id, label: p.id })),
|
||||
]}
|
||||
/>
|
||||
<Input
|
||||
label="Tags (comma-separated)"
|
||||
value={tagsText}
|
||||
onChange={setTagsText}
|
||||
placeholder="auth, deploy"
|
||||
/>
|
||||
</div>
|
||||
<Textarea
|
||||
label="Body"
|
||||
value={form.body}
|
||||
onChange={(v) => setForm((f) => ({ ...f, body: v }))}
|
||||
rows={5}
|
||||
placeholder="The knowledge itself — written for a reader with no context."
|
||||
/>
|
||||
<div className="hstack mt14" style={{ gap: 8 }}>
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={!form.title.trim() || !form.body.trim()}
|
||||
onClick={saveNote}
|
||||
>
|
||||
{editing ? "Save changes" : "Add note"}
|
||||
</Button>
|
||||
{editing && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setEditing(false);
|
||||
setForm(emptyForm);
|
||||
setTagsText("");
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<p className="faint" style={{ fontSize: "var(--text-xs)", margin: "10px 0 0" }}>
|
||||
Writes require the admin token. Agents add notes themselves through the bundled
|
||||
handler-memory MCP server.
|
||||
</p>
|
||||
</Card>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -29,6 +29,8 @@ import {
|
||||
type Command,
|
||||
type Host,
|
||||
type LogEntry,
|
||||
type MemoryGraph,
|
||||
type NoteKind,
|
||||
type Project,
|
||||
type Schedule,
|
||||
type SharedContext,
|
||||
@@ -43,6 +45,7 @@ export type Section =
|
||||
| "servers"
|
||||
| "activity"
|
||||
| "shared"
|
||||
| "memory"
|
||||
| "claude";
|
||||
|
||||
/* The claude web-login flow, driven through the login_start / login_submit commands.
|
||||
@@ -98,6 +101,8 @@ interface StoreValue {
|
||||
commands: Command[];
|
||||
schedules: Schedule[];
|
||||
shared: { log: LogEntry[]; context: SharedContext[] };
|
||||
/* The whole note graph (scoped client-side by the Memory section's project filter). */
|
||||
memory: MemoryGraph;
|
||||
|
||||
cmd: CmdState;
|
||||
lastError: string;
|
||||
@@ -124,6 +129,13 @@ interface StoreValue {
|
||||
pollCi: () => Promise<void>;
|
||||
setSharedKey: (key: string, value: string) => Promise<boolean>;
|
||||
|
||||
// Memory (notes + links; direct writes like the Claude management pages)
|
||||
createMemoryNote: (b: MemoryNoteBody) => Promise<boolean>;
|
||||
updateMemoryNote: (id: number, b: Partial<MemoryNoteBody>) => Promise<boolean>;
|
||||
deleteMemoryNote: (id: number) => Promise<void>;
|
||||
createMemoryLink: (src: number, dst: number, relation: string) => Promise<boolean>;
|
||||
deleteMemoryLink: (id: number) => Promise<void>;
|
||||
|
||||
// Claude web-login
|
||||
claudeLogin: ClaudeLoginState;
|
||||
startClaudeLogin: () => Promise<void>;
|
||||
@@ -252,6 +264,15 @@ export interface HostBody {
|
||||
generate_ssh_key: boolean;
|
||||
}
|
||||
|
||||
export interface MemoryNoteBody {
|
||||
title: string;
|
||||
body: string;
|
||||
kind: NoteKind;
|
||||
/* Project scope as a select value; "" = a global note. */
|
||||
project_id: string;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
const Ctx = createContext<StoreValue | null>(null);
|
||||
|
||||
export function useDashboard(): StoreValue {
|
||||
@@ -302,6 +323,7 @@ export function DashboardProvider({
|
||||
log: [],
|
||||
context: [],
|
||||
});
|
||||
const [memory, setMemory] = useState<MemoryGraph>({ notes: [], links: [] });
|
||||
const [cmd, setCmd] = useState<CmdState>({ text: "", error: false, busy: false });
|
||||
const [lastError, setLastError] = useState("");
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -453,6 +475,14 @@ export function DashboardProvider({
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadMemory = useCallback(async () => {
|
||||
try {
|
||||
setMemory(await clientRef.current.api<MemoryGraph>("/memory/graph"));
|
||||
} catch (e) {
|
||||
swallow(e);
|
||||
}
|
||||
}, []);
|
||||
|
||||
/* Models load separately from the rest of the Claude page: the Agents section's spawn
|
||||
* dropdown needs them too, without dragging skills/connectors along. */
|
||||
const loadClaudeModels = useCallback(async () => {
|
||||
@@ -505,8 +535,9 @@ export function DashboardProvider({
|
||||
if (s === "activity") await loadCommands();
|
||||
if (s === "schedules") await loadSchedules();
|
||||
if (s === "shared") await loadShared();
|
||||
if (s === "memory") await loadMemory();
|
||||
if (s === "claude") await loadClaude();
|
||||
}, [loadAgents, loadRun, loadApprovals, loadHosts, loadCommands, loadSchedules, loadShared, loadClaude, loadClaudeModels]);
|
||||
}, [loadAgents, loadRun, loadApprovals, loadHosts, loadCommands, loadSchedules, loadShared, loadMemory, loadClaude, loadClaudeModels]);
|
||||
|
||||
// Initial load + polling loop. The first tick populates projects *and* agents (and the
|
||||
// active section) up front, so the Runs inbox is filled without waiting a poll interval.
|
||||
@@ -536,9 +567,10 @@ export function DashboardProvider({
|
||||
if (s === "activity") void loadCommands();
|
||||
if (s === "schedules") void loadSchedules();
|
||||
if (s === "shared") void loadShared();
|
||||
if (s === "memory") void loadMemory();
|
||||
if (s === "claude") void loadClaude();
|
||||
},
|
||||
[loadApprovals, loadHosts, loadCommands, loadSchedules, loadShared, loadClaude, loadClaudeModels],
|
||||
[loadApprovals, loadHosts, loadCommands, loadSchedules, loadShared, loadMemory, loadClaude, loadClaudeModels],
|
||||
);
|
||||
|
||||
const selectProject = useCallback(
|
||||
@@ -1281,6 +1313,92 @@ export function DashboardProvider({
|
||||
[loadShared],
|
||||
);
|
||||
|
||||
// ---- memory actions (direct writes, admin token; mirror the shared/claude style) ----
|
||||
const memoryWrite = useCallback(
|
||||
async (fn: () => Promise<unknown>, okText: string) => {
|
||||
try {
|
||||
await fn();
|
||||
setCmd({ text: okText, error: false, busy: false });
|
||||
await loadMemory();
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (e instanceof AuthError) return false;
|
||||
setCmd({ text: (e as Error).message, error: true, busy: false });
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[loadMemory],
|
||||
);
|
||||
|
||||
const createMemoryNote = useCallback(
|
||||
(b: MemoryNoteBody) =>
|
||||
memoryWrite(
|
||||
() =>
|
||||
clientRef.current.api("/memory/notes", {
|
||||
body: {
|
||||
title: b.title,
|
||||
body: b.body,
|
||||
kind: b.kind,
|
||||
project_id: b.project_id || null,
|
||||
tags: b.tags.length ? b.tags : null,
|
||||
},
|
||||
}),
|
||||
"note saved",
|
||||
),
|
||||
[memoryWrite],
|
||||
);
|
||||
|
||||
const updateMemoryNote = useCallback(
|
||||
(id: number, b: Partial<MemoryNoteBody>) =>
|
||||
memoryWrite(
|
||||
() =>
|
||||
clientRef.current.api(`/memory/notes/${id}`, {
|
||||
method: "PATCH",
|
||||
body: {
|
||||
...(b.title !== undefined ? { title: b.title } : {}),
|
||||
...(b.body !== undefined ? { body: b.body } : {}),
|
||||
...(b.kind !== undefined ? { kind: b.kind } : {}),
|
||||
...(b.project_id !== undefined ? { project_id: b.project_id || null } : {}),
|
||||
...(b.tags !== undefined ? { tags: b.tags.length ? b.tags : null } : {}),
|
||||
},
|
||||
}),
|
||||
"note updated",
|
||||
),
|
||||
[memoryWrite],
|
||||
);
|
||||
|
||||
const deleteMemoryNote = useCallback(
|
||||
async (id: number) => {
|
||||
await memoryWrite(
|
||||
() => clientRef.current.api(`/memory/notes/${id}`, { method: "DELETE" }),
|
||||
"note deleted",
|
||||
);
|
||||
},
|
||||
[memoryWrite],
|
||||
);
|
||||
|
||||
const createMemoryLink = useCallback(
|
||||
(src: number, dst: number, relation: string) =>
|
||||
memoryWrite(
|
||||
() =>
|
||||
clientRef.current.api("/memory/links", {
|
||||
body: { src_note_id: src, dst_note_id: dst, relation: relation || "relates_to" },
|
||||
}),
|
||||
"notes linked",
|
||||
),
|
||||
[memoryWrite],
|
||||
);
|
||||
|
||||
const deleteMemoryLink = useCallback(
|
||||
async (id: number) => {
|
||||
await memoryWrite(
|
||||
() => clientRef.current.api(`/memory/links/${id}`, { method: "DELETE" }),
|
||||
"link removed",
|
||||
);
|
||||
},
|
||||
[memoryWrite],
|
||||
);
|
||||
|
||||
const value: StoreValue = {
|
||||
section,
|
||||
setSection,
|
||||
@@ -1301,6 +1419,7 @@ export function DashboardProvider({
|
||||
commands,
|
||||
schedules,
|
||||
shared,
|
||||
memory,
|
||||
cmd,
|
||||
lastError,
|
||||
loading,
|
||||
@@ -1322,6 +1441,11 @@ export function DashboardProvider({
|
||||
deleteSchedule,
|
||||
pollCi,
|
||||
setSharedKey,
|
||||
createMemoryNote,
|
||||
updateMemoryNote,
|
||||
deleteMemoryNote,
|
||||
createMemoryLink,
|
||||
deleteMemoryLink,
|
||||
claudeLogin,
|
||||
startClaudeLogin,
|
||||
submitClaudeCode,
|
||||
|
||||
@@ -209,6 +209,37 @@ export interface ClaudePermissions {
|
||||
base_allow: string[];
|
||||
}
|
||||
|
||||
/* ---- agent memory (the Memory page's note graph) ---- */
|
||||
|
||||
export type NoteKind = "fact" | "decision" | "gotcha" | "runbook";
|
||||
|
||||
export interface MemoryNote {
|
||||
id: number;
|
||||
project_id?: string | null; // null = global note
|
||||
agent_id?: number | null; // authoring agent; null = operator-authored
|
||||
title: string;
|
||||
body: string;
|
||||
kind: string;
|
||||
tags?: string[] | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface MemoryLink {
|
||||
id: number;
|
||||
src_note_id: number;
|
||||
dst_note_id: number;
|
||||
relation: string;
|
||||
created_by_agent_id?: number | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
/* Everything the graph view draws, in one response (GET /memory/graph). */
|
||||
export interface MemoryGraph {
|
||||
notes: MemoryNote[];
|
||||
links: MemoryLink[];
|
||||
}
|
||||
|
||||
export interface SharedContext {
|
||||
key: string;
|
||||
value: string;
|
||||
|
||||
@@ -18,6 +18,7 @@ export const NAV_ROUTES: NavRoute[] = [
|
||||
{ key: "servers", href: "/servers", label: "Git Servers" },
|
||||
{ key: "activity", href: "/activity", label: "Activity" },
|
||||
{ key: "shared", href: "/shared", label: "Shared" },
|
||||
{ key: "memory", href: "/memory", label: "Memory" },
|
||||
{ key: "claude", href: "/claude", label: "Claude" },
|
||||
];
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ from .routes import (
|
||||
hosts,
|
||||
interaction,
|
||||
login,
|
||||
memory,
|
||||
projects,
|
||||
schedules,
|
||||
shared,
|
||||
@@ -53,6 +54,7 @@ def create_app() -> FastAPI:
|
||||
app.include_router(hosts.router)
|
||||
app.include_router(schedules.router)
|
||||
app.include_router(shared.router)
|
||||
app.include_router(memory.router)
|
||||
|
||||
# Optional CORS, only for operators who host the UI on a different origin than the
|
||||
# API. Empty CORS_ORIGINS => middleware never added => behaviour identical to headless.
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
"""Agent memory: the distilled note store and its link graph.
|
||||
|
||||
Agents write here through the bundled handler-memory MCP server (direct DB, like the
|
||||
hooks); these routes are the dashboard's window plus the operator's editing surface —
|
||||
no worker round-trip, nothing touches a live process, same trust model as the Claude
|
||||
management pages. Reads take the normal token; writes take the admin token (the notes
|
||||
feed every future agent's context, so authoring them is a control-surface action).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy import Connection
|
||||
|
||||
from ...db import repository as repo
|
||||
from ..deps import db_conn, require_admin, require_auth
|
||||
from ..schemas import (
|
||||
MemoryGraphOut,
|
||||
MemoryLinkIn,
|
||||
MemoryLinkOut,
|
||||
MemoryNoteIn,
|
||||
MemoryNoteOut,
|
||||
MemoryNoteUpdateIn,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/memory", tags=["memory"], dependencies=[Depends(require_auth)])
|
||||
|
||||
|
||||
def _note_or_404(conn: Connection, note_id: int) -> dict:
|
||||
note = repo.get_memory_note(conn, note_id)
|
||||
if note is None:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND, detail=f"note {note_id} not found")
|
||||
return note
|
||||
|
||||
|
||||
def _project_or_400(conn: Connection, project_id: str | None) -> None:
|
||||
if project_id is not None and repo.get_project(conn, project_id) is None:
|
||||
raise HTTPException(
|
||||
status.HTTP_400_BAD_REQUEST, detail=f"project '{project_id}' not found"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/notes", response_model=list[MemoryNoteOut])
|
||||
def list_notes(
|
||||
project_id: str | None = Query(None),
|
||||
q: str | None = Query(None),
|
||||
limit: int = Query(200, ge=1, le=1000),
|
||||
offset: int = Query(0, ge=0),
|
||||
conn: Connection = Depends(db_conn),
|
||||
) -> list[dict]:
|
||||
"""Notes in scope, newest first; ``q`` switches to substring search (all terms)."""
|
||||
if q:
|
||||
return repo.search_memory_notes(conn, q, project_id=project_id, limit=limit)
|
||||
return repo.list_memory_notes(conn, project_id=project_id, limit=limit, offset=offset)
|
||||
|
||||
|
||||
@router.get("/graph", response_model=MemoryGraphOut)
|
||||
def graph(
|
||||
project_id: str | None = Query(None),
|
||||
conn: Connection = Depends(db_conn),
|
||||
) -> dict:
|
||||
"""The whole web of notes in one read — what the Memory page draws."""
|
||||
return repo.memory_graph(conn, project_id=project_id)
|
||||
|
||||
|
||||
@router.get("/notes/{note_id}", response_model=MemoryNoteOut)
|
||||
def get_note(note_id: int, conn: Connection = Depends(db_conn)) -> dict:
|
||||
return _note_or_404(conn, note_id)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/notes",
|
||||
response_model=MemoryNoteOut,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
dependencies=[Depends(require_admin)],
|
||||
)
|
||||
def create_note(body: MemoryNoteIn, conn: Connection = Depends(db_conn)) -> dict:
|
||||
_project_or_400(conn, body.project_id)
|
||||
return repo.create_memory_note(
|
||||
conn,
|
||||
title=body.title,
|
||||
body=body.body,
|
||||
kind=body.kind,
|
||||
project_id=body.project_id,
|
||||
agent_id=None, # operator-authored; agents write via the MCP server
|
||||
tags=body.tags,
|
||||
)
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/notes/{note_id}", response_model=MemoryNoteOut, dependencies=[Depends(require_admin)]
|
||||
)
|
||||
def update_note(
|
||||
note_id: int, body: MemoryNoteUpdateIn, conn: Connection = Depends(db_conn)
|
||||
) -> dict:
|
||||
_note_or_404(conn, note_id)
|
||||
fields = body.model_dump(exclude_unset=True)
|
||||
if "project_id" in fields:
|
||||
_project_or_400(conn, fields["project_id"])
|
||||
return repo.update_memory_note(conn, note_id, **fields)
|
||||
|
||||
|
||||
@router.delete("/notes/{note_id}", dependencies=[Depends(require_admin)])
|
||||
def delete_note(note_id: int, conn: Connection = Depends(db_conn)) -> dict:
|
||||
note = _note_or_404(conn, note_id)
|
||||
repo.delete_memory_note(conn, note_id)
|
||||
return {"deleted": note["id"]}
|
||||
|
||||
|
||||
@router.post(
|
||||
"/links",
|
||||
response_model=MemoryLinkOut,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
dependencies=[Depends(require_admin)],
|
||||
)
|
||||
def create_link(body: MemoryLinkIn, conn: Connection = Depends(db_conn)) -> dict:
|
||||
if body.src_note_id == body.dst_note_id:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="a note cannot link to itself")
|
||||
_note_or_404(conn, body.src_note_id)
|
||||
_note_or_404(conn, body.dst_note_id)
|
||||
return repo.create_memory_link(
|
||||
conn, body.src_note_id, body.dst_note_id, relation=body.relation, agent_id=None
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/links/{link_id}", dependencies=[Depends(require_admin)])
|
||||
def delete_link(link_id: int, conn: Connection = Depends(db_conn)) -> dict:
|
||||
if not repo.delete_memory_link(conn, link_id):
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND, detail=f"link {link_id} not found")
|
||||
return {"deleted": link_id}
|
||||
@@ -610,3 +610,64 @@ class SharedContextOut(BaseModel):
|
||||
value: str
|
||||
set_by_agent_id: int | None = None
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
# ---- agent memory (the /memory page + the handler-memory MCP server's store) ----
|
||||
|
||||
NoteKind = Literal["fact", "decision", "gotcha", "runbook"]
|
||||
|
||||
|
||||
class MemoryNoteIn(BaseModel):
|
||||
title: str = Field(min_length=1, max_length=500)
|
||||
body: str = Field(min_length=1)
|
||||
kind: NoteKind = "fact"
|
||||
project_id: str | None = None # null = global note
|
||||
tags: list[str] | None = None
|
||||
|
||||
|
||||
class MemoryNoteUpdateIn(BaseModel):
|
||||
"""Editable note columns; omit a field to leave it unchanged."""
|
||||
|
||||
title: str | None = Field(default=None, min_length=1, max_length=500)
|
||||
body: str | None = Field(default=None, min_length=1)
|
||||
kind: NoteKind | None = None
|
||||
project_id: str | None = None
|
||||
tags: list[str] | None = None
|
||||
|
||||
|
||||
class MemoryNoteOut(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
project_id: str | None = None
|
||||
agent_id: int | None = None
|
||||
title: str
|
||||
body: str
|
||||
kind: str
|
||||
tags: list[str] | None = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class MemoryLinkIn(BaseModel):
|
||||
src_note_id: int
|
||||
dst_note_id: int
|
||||
relation: str = Field(default="relates_to", min_length=1, max_length=100)
|
||||
|
||||
|
||||
class MemoryLinkOut(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
src_note_id: int
|
||||
dst_note_id: int
|
||||
relation: str
|
||||
created_by_agent_id: int | None = None
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class MemoryGraphOut(BaseModel):
|
||||
"""Everything the graph view draws, in one response."""
|
||||
|
||||
notes: list[MemoryNoteOut]
|
||||
links: list[MemoryLinkOut]
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[185],{3471:function(e,a,n){Promise.resolve().then(n.t.bind(n,7960,23)),Promise.resolve().then(n.bind(n,5520))},5520:function(e,a,n){"use strict";n.d(a,{AppFrame:function(){return p}});var t=n(7437),s=n(2265),l=n(9376),r=n(171),o=n(7648);let i=[{key:"runs",href:"/",label:"Runs"},{key:"repositories",href:"/repositories",label:"Repositories"},{key:"agents",href:"/agents",label:"Agents"},{key:"schedules",href:"/schedules",label:"Schedules"},{key:"approvals",href:"/approvals",label:"Approvals"},{key:"servers",href:"/servers",label:"Git Servers"},{key:"activity",href:"/activity",label:"Activity"},{key:"shared",href:"/shared",label:"Shared"},{key:"memory",href:"/memory",label:"Memory"},{key:"claude",href:"/claude",label:"Claude"}];function c(e){var a,n;let t=e.replace(/\/+$/,"")||"/";return"/login"===t?"claude":null!==(n=null===(a=i.find(e=>e.href===t))||void 0===a?void 0:a.key)&&void 0!==n?n:"runs"}let u={runs:{count:e=>e.agents.length,accent:e=>e.agents.some(e=>"paused_for_input"===e.status)},repositories:{count:e=>e.projects.length},agents:{count:e=>e.agents.length},schedules:{count:e=>e.schedules.length},approvals:{count:e=>e.approvals.length},servers:{count:e=>e.hosts.length},activity:{count:e=>e.commands.length},shared:{count:e=>e.shared.context.length},claude:{count:e=>e.claudeSkills.length+e.claudeConnectors.length+e.claudePlugins.length,accent:e=>"done"!==e.claudeLogin.status}};function d(e){let{onSignOut:a,children:n}=e,d=(0,r.Q)(),h=c((0,l.usePathname)()),{setSection:m}=d;return(0,s.useEffect)(()=>{m(h)},[h,m]),(0,t.jsxs)("div",{className:"app",children:[(0,t.jsxs)("aside",{className:"sidebar",children:[(0,t.jsxs)("div",{className:"brand",children:[(0,t.jsx)("span",{className:"logo"}),"Claude Monitor"]}),i.map(e=>{var a,n,s;let l=u[e.key],r=null!==(n=null==l?void 0:l.count(d))&&void 0!==n?n:0,i=null!==(s=null==l?void 0:null===(a=l.accent)||void 0===a?void 0:a.call(l,d))&&void 0!==s&&s;return(0,t.jsxs)(o.default,{href:e.href,className:"nav-item".concat(h===e.key?" active":""),children:[(0,t.jsx)("span",{children:e.label}),(0,t.jsx)("span",{className:"count",style:i?{color:"var(--lw-warning-fg)"}:void 0,children:r||""})]},e.key)}),(0,t.jsx)("div",{className:"sidebar-spacer"}),(0,t.jsxs)("div",{className:"sidebar-foot",children:[(0,t.jsxs)("button",{className:"nav-item",onClick:d.refresh,title:"Refresh now",children:[(0,t.jsx)("span",{children:"Refresh"}),(0,t.jsx)("span",{className:"count",children:"↻"})]}),(0,t.jsx)("button",{className:"nav-item",onClick:a,title:"Sign out / change token",children:(0,t.jsx)("span",{children:"Sign out"})})]})]}),(0,t.jsxs)("main",{className:"main",children:[d.cmd.text&&(0,t.jsx)("p",{className:"banner ".concat(d.cmd.error?"err":"ok"),style:{marginTop:16},children:d.cmd.text}),d.lastError&&(0,t.jsx)("p",{className:"banner err",style:{marginTop:12},children:d.lastError}),n]})]})}function h(e){let{error:a,onSubmit:n}=e,[l,r]=(0,s.useState)("");return(0,t.jsx)("div",{className:"gate",children:(0,t.jsxs)("form",{className:"gate-card",onSubmit:e=>{e.preventDefault();let a=l.trim();a&&n(a)},children:[(0,t.jsxs)("div",{className:"gate-brand",children:[(0,t.jsx)("span",{className:"logo",style:{width:26,height:26,borderRadius:7}}),"Claude Monitor"]}),(0,t.jsx)("p",{className:"muted",style:{fontSize:"var(--text-sm)",margin:0},children:"Paste your API token to continue. Management actions require the admin token; read-only views work with the plain auth token."}),(0,t.jsx)("input",{className:"input",type:"password",autoComplete:"current-password",placeholder:"API token",value:l,onChange:e=>r(e.target.value),autoFocus:!0}),a&&(0,t.jsx)("p",{className:"callout callout-danger",style:{margin:0},children:a}),(0,t.jsx)("button",{className:"btn btn-primary",type:"submit",children:"Continue"})]})})}let m="handler_token";function p(e){let{children:a}=e,[n,o]=(0,s.useState)(null),[i,u]=(0,s.useState)(""),p=(0,l.usePathname)();(0,s.useEffect)(()=>{let e=window.localStorage.getItem(m);e&&o(e)},[]);let g=(0,s.useCallback)(e=>{window.localStorage.setItem(m,e),u(""),o(e)},[]),v=(0,s.useCallback)(()=>{window.localStorage.removeItem(m),o(null)},[]),f=(0,s.useCallback)(()=>{window.localStorage.removeItem(m),o(null),u("Invalid token — please try again.")},[]);return n?(0,t.jsx)(r._,{token:n,onUnauthorized:f,initialSection:c(p),children:(0,t.jsx)(d,{onSignOut:v,children:a})}):(0,t.jsx)(h,{error:i,onSubmit:g})}},7960:function(){}},function(e){e.O(0,[587,258,171,971,117,744],function(){return e(e.s=3471)}),_N_E=e.O()}]);
|
||||
@@ -1 +0,0 @@
|
||||
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[185],{3471:function(e,a,n){Promise.resolve().then(n.t.bind(n,7960,23)),Promise.resolve().then(n.bind(n,5520))},5520:function(e,a,n){"use strict";n.d(a,{AppFrame:function(){return p}});var t=n(7437),s=n(2265),l=n(9376),r=n(171),o=n(7648);let i=[{key:"runs",href:"/",label:"Runs"},{key:"repositories",href:"/repositories",label:"Repositories"},{key:"agents",href:"/agents",label:"Agents"},{key:"schedules",href:"/schedules",label:"Schedules"},{key:"approvals",href:"/approvals",label:"Approvals"},{key:"servers",href:"/servers",label:"Git Servers"},{key:"activity",href:"/activity",label:"Activity"},{key:"shared",href:"/shared",label:"Shared"},{key:"claude",href:"/claude",label:"Claude"}];function c(e){var a,n;let t=e.replace(/\/+$/,"")||"/";return"/login"===t?"claude":null!==(n=null===(a=i.find(e=>e.href===t))||void 0===a?void 0:a.key)&&void 0!==n?n:"runs"}let u={runs:{count:e=>e.agents.length,accent:e=>e.agents.some(e=>"paused_for_input"===e.status)},repositories:{count:e=>e.projects.length},agents:{count:e=>e.agents.length},schedules:{count:e=>e.schedules.length},approvals:{count:e=>e.approvals.length},servers:{count:e=>e.hosts.length},activity:{count:e=>e.commands.length},shared:{count:e=>e.shared.context.length},claude:{count:e=>e.claudeSkills.length+e.claudeConnectors.length+e.claudePlugins.length,accent:e=>"done"!==e.claudeLogin.status}};function d(e){let{onSignOut:a,children:n}=e,d=(0,r.Q)(),h=c((0,l.usePathname)()),{setSection:m}=d;return(0,s.useEffect)(()=>{m(h)},[h,m]),(0,t.jsxs)("div",{className:"app",children:[(0,t.jsxs)("aside",{className:"sidebar",children:[(0,t.jsxs)("div",{className:"brand",children:[(0,t.jsx)("span",{className:"logo"}),"Claude Monitor"]}),i.map(e=>{var a,n,s;let l=u[e.key],r=null!==(n=null==l?void 0:l.count(d))&&void 0!==n?n:0,i=null!==(s=null==l?void 0:null===(a=l.accent)||void 0===a?void 0:a.call(l,d))&&void 0!==s&&s;return(0,t.jsxs)(o.default,{href:e.href,className:"nav-item".concat(h===e.key?" active":""),children:[(0,t.jsx)("span",{children:e.label}),(0,t.jsx)("span",{className:"count",style:i?{color:"var(--lw-warning-fg)"}:void 0,children:r||""})]},e.key)}),(0,t.jsx)("div",{className:"sidebar-spacer"}),(0,t.jsxs)("div",{className:"sidebar-foot",children:[(0,t.jsxs)("button",{className:"nav-item",onClick:d.refresh,title:"Refresh now",children:[(0,t.jsx)("span",{children:"Refresh"}),(0,t.jsx)("span",{className:"count",children:"↻"})]}),(0,t.jsx)("button",{className:"nav-item",onClick:a,title:"Sign out / change token",children:(0,t.jsx)("span",{children:"Sign out"})})]})]}),(0,t.jsxs)("main",{className:"main",children:[d.cmd.text&&(0,t.jsx)("p",{className:"banner ".concat(d.cmd.error?"err":"ok"),style:{marginTop:16},children:d.cmd.text}),d.lastError&&(0,t.jsx)("p",{className:"banner err",style:{marginTop:12},children:d.lastError}),n]})]})}function h(e){let{error:a,onSubmit:n}=e,[l,r]=(0,s.useState)("");return(0,t.jsx)("div",{className:"gate",children:(0,t.jsxs)("form",{className:"gate-card",onSubmit:e=>{e.preventDefault();let a=l.trim();a&&n(a)},children:[(0,t.jsxs)("div",{className:"gate-brand",children:[(0,t.jsx)("span",{className:"logo",style:{width:26,height:26,borderRadius:7}}),"Claude Monitor"]}),(0,t.jsx)("p",{className:"muted",style:{fontSize:"var(--text-sm)",margin:0},children:"Paste your API token to continue. Management actions require the admin token; read-only views work with the plain auth token."}),(0,t.jsx)("input",{className:"input",type:"password",autoComplete:"current-password",placeholder:"API token",value:l,onChange:e=>r(e.target.value),autoFocus:!0}),a&&(0,t.jsx)("p",{className:"callout callout-danger",style:{margin:0},children:a}),(0,t.jsx)("button",{className:"btn btn-primary",type:"submit",children:"Continue"})]})})}let m="handler_token";function p(e){let{children:a}=e,[n,o]=(0,s.useState)(null),[i,u]=(0,s.useState)(""),p=(0,l.usePathname)();(0,s.useEffect)(()=>{let e=window.localStorage.getItem(m);e&&o(e)},[]);let g=(0,s.useCallback)(e=>{window.localStorage.setItem(m,e),u(""),o(e)},[]),v=(0,s.useCallback)(()=>{window.localStorage.removeItem(m),o(null)},[]),f=(0,s.useCallback)(()=>{window.localStorage.removeItem(m),o(null),u("Invalid token — please try again.")},[]);return n?(0,t.jsx)(r._,{token:n,onUnauthorized:f,initialSection:c(p),children:(0,t.jsx)(d,{onSignOut:v,children:a})}):(0,t.jsx)(h,{error:i,onSubmit:g})}},7960:function(){}},function(e){e.O(0,[587,258,171,971,117,744],function(){return e(e.s=3471)}),_N_E=e.O()}]);
|
||||
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,8 +1,8 @@
|
||||
2:I[9107,[],"ClientPageRoot"]
|
||||
3:I[7839,["171","static/chunks/171-19de960b70cee25e.js","263","static/chunks/app/activity/page-809b415dd18cb14e.js"],"default",1]
|
||||
3:I[7839,["171","static/chunks/171-7adbcaabf9d14cdf.js","263","static/chunks/app/activity/page-809b415dd18cb14e.js"],"default",1]
|
||||
4:I[4707,[],""]
|
||||
5:I[6423,[],""]
|
||||
6:I[5520,["258","static/chunks/258-022159d3c3089cd0.js","171","static/chunks/171-19de960b70cee25e.js","185","static/chunks/app/layout-c7c117ef7b86bf6f.js"],"AppFrame"]
|
||||
0:["4cEQAcmEL0RnY9yw8HjTQ",[[["",{"children":["activity",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["activity",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","activity","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/bbd6ee1e7265be74.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L6",null,{"children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]}]],null],null],["$L7",null]]]]
|
||||
6:I[5520,["258","static/chunks/258-022159d3c3089cd0.js","171","static/chunks/171-7adbcaabf9d14cdf.js","185","static/chunks/app/layout-21ca3d760368e2b4.js"],"AppFrame"]
|
||||
0:["2k28Zwu9jDxUf-5jNvRk4",[[["",{"children":["activity",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["activity",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","activity","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/bbd6ee1e7265be74.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L6",null,{"children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]}]],null],null],["$L7",null]]]]
|
||||
7:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"Handler · Claude Activity"}],["$","meta","3",{"name":"description","content":"Monitor and manage Claude Code agents across projects."}],["$","link","4",{"rel":"icon","href":"/icon.svg?bab509b45a421e43","type":"image/svg+xml","sizes":"any"}]]
|
||||
1:null
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,8 +1,8 @@
|
||||
2:I[9107,[],"ClientPageRoot"]
|
||||
3:I[2506,["171","static/chunks/171-19de960b70cee25e.js","718","static/chunks/app/agents/page-65cc70b94cb9abd3.js"],"default",1]
|
||||
3:I[2506,["171","static/chunks/171-7adbcaabf9d14cdf.js","718","static/chunks/app/agents/page-65cc70b94cb9abd3.js"],"default",1]
|
||||
4:I[4707,[],""]
|
||||
5:I[6423,[],""]
|
||||
6:I[5520,["258","static/chunks/258-022159d3c3089cd0.js","171","static/chunks/171-19de960b70cee25e.js","185","static/chunks/app/layout-c7c117ef7b86bf6f.js"],"AppFrame"]
|
||||
0:["4cEQAcmEL0RnY9yw8HjTQ",[[["",{"children":["agents",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["agents",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","agents","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/bbd6ee1e7265be74.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L6",null,{"children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]}]],null],null],["$L7",null]]]]
|
||||
6:I[5520,["258","static/chunks/258-022159d3c3089cd0.js","171","static/chunks/171-7adbcaabf9d14cdf.js","185","static/chunks/app/layout-21ca3d760368e2b4.js"],"AppFrame"]
|
||||
0:["2k28Zwu9jDxUf-5jNvRk4",[[["",{"children":["agents",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["agents",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","agents","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/bbd6ee1e7265be74.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L6",null,{"children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]}]],null],null],["$L7",null]]]]
|
||||
7:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"Handler · Claude Activity"}],["$","meta","3",{"name":"description","content":"Monitor and manage Claude Code agents across projects."}],["$","link","4",{"rel":"icon","href":"/icon.svg?bab509b45a421e43","type":"image/svg+xml","sizes":"any"}]]
|
||||
1:null
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,8 +1,8 @@
|
||||
2:I[9107,[],"ClientPageRoot"]
|
||||
3:I[2651,["171","static/chunks/171-19de960b70cee25e.js","163","static/chunks/app/approvals/page-8d5622be330f4d01.js"],"default",1]
|
||||
3:I[2651,["171","static/chunks/171-7adbcaabf9d14cdf.js","163","static/chunks/app/approvals/page-8d5622be330f4d01.js"],"default",1]
|
||||
4:I[4707,[],""]
|
||||
5:I[6423,[],""]
|
||||
6:I[5520,["258","static/chunks/258-022159d3c3089cd0.js","171","static/chunks/171-19de960b70cee25e.js","185","static/chunks/app/layout-c7c117ef7b86bf6f.js"],"AppFrame"]
|
||||
0:["4cEQAcmEL0RnY9yw8HjTQ",[[["",{"children":["approvals",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["approvals",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","approvals","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/bbd6ee1e7265be74.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L6",null,{"children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]}]],null],null],["$L7",null]]]]
|
||||
6:I[5520,["258","static/chunks/258-022159d3c3089cd0.js","171","static/chunks/171-7adbcaabf9d14cdf.js","185","static/chunks/app/layout-21ca3d760368e2b4.js"],"AppFrame"]
|
||||
0:["2k28Zwu9jDxUf-5jNvRk4",[[["",{"children":["approvals",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["approvals",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","approvals","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/bbd6ee1e7265be74.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L6",null,{"children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]}]],null],null],["$L7",null]]]]
|
||||
7:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"Handler · Claude Activity"}],["$","meta","3",{"name":"description","content":"Monitor and manage Claude Code agents across projects."}],["$","link","4",{"rel":"icon","href":"/icon.svg?bab509b45a421e43","type":"image/svg+xml","sizes":"any"}]]
|
||||
1:null
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,8 +1,8 @@
|
||||
2:I[9107,[],"ClientPageRoot"]
|
||||
3:I[6529,["171","static/chunks/171-19de960b70cee25e.js","877","static/chunks/app/claude/page-235e137771d99edc.js"],"default",1]
|
||||
3:I[6529,["171","static/chunks/171-7adbcaabf9d14cdf.js","877","static/chunks/app/claude/page-235e137771d99edc.js"],"default",1]
|
||||
4:I[4707,[],""]
|
||||
5:I[6423,[],""]
|
||||
6:I[5520,["258","static/chunks/258-022159d3c3089cd0.js","171","static/chunks/171-19de960b70cee25e.js","185","static/chunks/app/layout-c7c117ef7b86bf6f.js"],"AppFrame"]
|
||||
0:["4cEQAcmEL0RnY9yw8HjTQ",[[["",{"children":["claude",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["claude",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","claude","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/bbd6ee1e7265be74.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L6",null,{"children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]}]],null],null],["$L7",null]]]]
|
||||
6:I[5520,["258","static/chunks/258-022159d3c3089cd0.js","171","static/chunks/171-7adbcaabf9d14cdf.js","185","static/chunks/app/layout-21ca3d760368e2b4.js"],"AppFrame"]
|
||||
0:["2k28Zwu9jDxUf-5jNvRk4",[[["",{"children":["claude",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["claude",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","claude","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/bbd6ee1e7265be74.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L6",null,{"children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]}]],null],null],["$L7",null]]]]
|
||||
7:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"Handler · Claude Activity"}],["$","meta","3",{"name":"description","content":"Monitor and manage Claude Code agents across projects."}],["$","link","4",{"rel":"icon","href":"/icon.svg?bab509b45a421e43","type":"image/svg+xml","sizes":"any"}]]
|
||||
1:null
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,8 +1,8 @@
|
||||
2:I[9107,[],"ClientPageRoot"]
|
||||
3:I[3807,["171","static/chunks/171-19de960b70cee25e.js","931","static/chunks/app/page-cf68f34e95b90a40.js"],"default",1]
|
||||
4:I[5520,["258","static/chunks/258-022159d3c3089cd0.js","171","static/chunks/171-19de960b70cee25e.js","185","static/chunks/app/layout-c7c117ef7b86bf6f.js"],"AppFrame"]
|
||||
3:I[3807,["171","static/chunks/171-7adbcaabf9d14cdf.js","931","static/chunks/app/page-cf68f34e95b90a40.js"],"default",1]
|
||||
4:I[5520,["258","static/chunks/258-022159d3c3089cd0.js","171","static/chunks/171-7adbcaabf9d14cdf.js","185","static/chunks/app/layout-21ca3d760368e2b4.js"],"AppFrame"]
|
||||
5:I[4707,[],""]
|
||||
6:I[6423,[],""]
|
||||
0:["4cEQAcmEL0RnY9yw8HjTQ",[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/bbd6ee1e7265be74.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L4",null,{"children":["$","$L5",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L6",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]}]],null],null],["$L7",null]]]]
|
||||
0:["2k28Zwu9jDxUf-5jNvRk4",[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/bbd6ee1e7265be74.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L4",null,{"children":["$","$L5",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L6",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]}]],null],null],["$L7",null]]]]
|
||||
7:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"Handler · Claude Activity"}],["$","meta","3",{"name":"description","content":"Monitor and manage Claude Code agents across projects."}],["$","link","4",{"rel":"icon","href":"/icon.svg?bab509b45a421e43","type":"image/svg+xml","sizes":"any"}]]
|
||||
1:null
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -2,7 +2,7 @@
|
||||
3:I[6374,["626","static/chunks/app/login/page-b08c6695be5632dd.js"],"default",1]
|
||||
4:I[4707,[],""]
|
||||
5:I[6423,[],""]
|
||||
6:I[5520,["258","static/chunks/258-022159d3c3089cd0.js","171","static/chunks/171-19de960b70cee25e.js","185","static/chunks/app/layout-c7c117ef7b86bf6f.js"],"AppFrame"]
|
||||
0:["4cEQAcmEL0RnY9yw8HjTQ",[[["",{"children":["login",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["login",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","login","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/bbd6ee1e7265be74.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L6",null,{"children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]}]],null],null],["$L7",null]]]]
|
||||
6:I[5520,["258","static/chunks/258-022159d3c3089cd0.js","171","static/chunks/171-7adbcaabf9d14cdf.js","185","static/chunks/app/layout-21ca3d760368e2b4.js"],"AppFrame"]
|
||||
0:["2k28Zwu9jDxUf-5jNvRk4",[[["",{"children":["login",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["login",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","login","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/bbd6ee1e7265be74.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L6",null,{"children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]}]],null],null],["$L7",null]]]]
|
||||
7:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"Handler · Claude Activity"}],["$","meta","3",{"name":"description","content":"Monitor and manage Claude Code agents across projects."}],["$","link","4",{"rel":"icon","href":"/icon.svg?bab509b45a421e43","type":"image/svg+xml","sizes":"any"}]]
|
||||
1:null
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,8 @@
|
||||
2:I[9107,[],"ClientPageRoot"]
|
||||
3:I[9244,["171","static/chunks/171-7adbcaabf9d14cdf.js","872","static/chunks/app/memory/page-2d205a5c1ee769c1.js"],"default",1]
|
||||
4:I[4707,[],""]
|
||||
5:I[6423,[],""]
|
||||
6:I[5520,["258","static/chunks/258-022159d3c3089cd0.js","171","static/chunks/171-7adbcaabf9d14cdf.js","185","static/chunks/app/layout-21ca3d760368e2b4.js"],"AppFrame"]
|
||||
0:["2k28Zwu9jDxUf-5jNvRk4",[[["",{"children":["memory",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["memory",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","memory","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/bbd6ee1e7265be74.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L6",null,{"children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]}]],null],null],["$L7",null]]]]
|
||||
7:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"Handler · Claude Activity"}],["$","meta","3",{"name":"description","content":"Monitor and manage Claude Code agents across projects."}],["$","link","4",{"rel":"icon","href":"/icon.svg?bab509b45a421e43","type":"image/svg+xml","sizes":"any"}]]
|
||||
1:null
|
||||
File diff suppressed because one or more lines are too long
@@ -1,8 +1,8 @@
|
||||
2:I[9107,[],"ClientPageRoot"]
|
||||
3:I[3641,["171","static/chunks/171-19de960b70cee25e.js","27","static/chunks/app/repositories/page-fb0c29b8dc7ed817.js"],"default",1]
|
||||
3:I[3641,["171","static/chunks/171-7adbcaabf9d14cdf.js","27","static/chunks/app/repositories/page-fb0c29b8dc7ed817.js"],"default",1]
|
||||
4:I[4707,[],""]
|
||||
5:I[6423,[],""]
|
||||
6:I[5520,["258","static/chunks/258-022159d3c3089cd0.js","171","static/chunks/171-19de960b70cee25e.js","185","static/chunks/app/layout-c7c117ef7b86bf6f.js"],"AppFrame"]
|
||||
0:["4cEQAcmEL0RnY9yw8HjTQ",[[["",{"children":["repositories",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["repositories",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","repositories","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/bbd6ee1e7265be74.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L6",null,{"children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]}]],null],null],["$L7",null]]]]
|
||||
6:I[5520,["258","static/chunks/258-022159d3c3089cd0.js","171","static/chunks/171-7adbcaabf9d14cdf.js","185","static/chunks/app/layout-21ca3d760368e2b4.js"],"AppFrame"]
|
||||
0:["2k28Zwu9jDxUf-5jNvRk4",[[["",{"children":["repositories",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["repositories",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","repositories","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/bbd6ee1e7265be74.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L6",null,{"children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]}]],null],null],["$L7",null]]]]
|
||||
7:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"Handler · Claude Activity"}],["$","meta","3",{"name":"description","content":"Monitor and manage Claude Code agents across projects."}],["$","link","4",{"rel":"icon","href":"/icon.svg?bab509b45a421e43","type":"image/svg+xml","sizes":"any"}]]
|
||||
1:null
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,8 +1,8 @@
|
||||
2:I[9107,[],"ClientPageRoot"]
|
||||
3:I[5124,["171","static/chunks/171-19de960b70cee25e.js","95","static/chunks/app/schedules/page-4398c9c829e1b9fd.js"],"default",1]
|
||||
3:I[5124,["171","static/chunks/171-7adbcaabf9d14cdf.js","95","static/chunks/app/schedules/page-4398c9c829e1b9fd.js"],"default",1]
|
||||
4:I[4707,[],""]
|
||||
5:I[6423,[],""]
|
||||
6:I[5520,["258","static/chunks/258-022159d3c3089cd0.js","171","static/chunks/171-19de960b70cee25e.js","185","static/chunks/app/layout-c7c117ef7b86bf6f.js"],"AppFrame"]
|
||||
0:["4cEQAcmEL0RnY9yw8HjTQ",[[["",{"children":["schedules",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["schedules",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","schedules","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/bbd6ee1e7265be74.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L6",null,{"children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]}]],null],null],["$L7",null]]]]
|
||||
6:I[5520,["258","static/chunks/258-022159d3c3089cd0.js","171","static/chunks/171-7adbcaabf9d14cdf.js","185","static/chunks/app/layout-21ca3d760368e2b4.js"],"AppFrame"]
|
||||
0:["2k28Zwu9jDxUf-5jNvRk4",[[["",{"children":["schedules",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["schedules",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","schedules","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/bbd6ee1e7265be74.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L6",null,{"children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]}]],null],null],["$L7",null]]]]
|
||||
7:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"Handler · Claude Activity"}],["$","meta","3",{"name":"description","content":"Monitor and manage Claude Code agents across projects."}],["$","link","4",{"rel":"icon","href":"/icon.svg?bab509b45a421e43","type":"image/svg+xml","sizes":"any"}]]
|
||||
1:null
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,8 +1,8 @@
|
||||
2:I[9107,[],"ClientPageRoot"]
|
||||
3:I[4646,["171","static/chunks/171-19de960b70cee25e.js","664","static/chunks/app/servers/page-7cfc66d88412e3d1.js"],"default",1]
|
||||
3:I[4646,["171","static/chunks/171-7adbcaabf9d14cdf.js","664","static/chunks/app/servers/page-7cfc66d88412e3d1.js"],"default",1]
|
||||
4:I[4707,[],""]
|
||||
5:I[6423,[],""]
|
||||
6:I[5520,["258","static/chunks/258-022159d3c3089cd0.js","171","static/chunks/171-19de960b70cee25e.js","185","static/chunks/app/layout-c7c117ef7b86bf6f.js"],"AppFrame"]
|
||||
0:["4cEQAcmEL0RnY9yw8HjTQ",[[["",{"children":["servers",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["servers",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","servers","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/bbd6ee1e7265be74.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L6",null,{"children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]}]],null],null],["$L7",null]]]]
|
||||
6:I[5520,["258","static/chunks/258-022159d3c3089cd0.js","171","static/chunks/171-7adbcaabf9d14cdf.js","185","static/chunks/app/layout-21ca3d760368e2b4.js"],"AppFrame"]
|
||||
0:["2k28Zwu9jDxUf-5jNvRk4",[[["",{"children":["servers",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["servers",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","servers","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/bbd6ee1e7265be74.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L6",null,{"children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]}]],null],null],["$L7",null]]]]
|
||||
7:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"Handler · Claude Activity"}],["$","meta","3",{"name":"description","content":"Monitor and manage Claude Code agents across projects."}],["$","link","4",{"rel":"icon","href":"/icon.svg?bab509b45a421e43","type":"image/svg+xml","sizes":"any"}]]
|
||||
1:null
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,8 +1,8 @@
|
||||
2:I[9107,[],"ClientPageRoot"]
|
||||
3:I[9475,["171","static/chunks/171-19de960b70cee25e.js","856","static/chunks/app/shared/page-dc7068fcf86ab8e4.js"],"default",1]
|
||||
3:I[9475,["171","static/chunks/171-7adbcaabf9d14cdf.js","856","static/chunks/app/shared/page-dc7068fcf86ab8e4.js"],"default",1]
|
||||
4:I[4707,[],""]
|
||||
5:I[6423,[],""]
|
||||
6:I[5520,["258","static/chunks/258-022159d3c3089cd0.js","171","static/chunks/171-19de960b70cee25e.js","185","static/chunks/app/layout-c7c117ef7b86bf6f.js"],"AppFrame"]
|
||||
0:["4cEQAcmEL0RnY9yw8HjTQ",[[["",{"children":["shared",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["shared",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","shared","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/bbd6ee1e7265be74.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L6",null,{"children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]}]],null],null],["$L7",null]]]]
|
||||
6:I[5520,["258","static/chunks/258-022159d3c3089cd0.js","171","static/chunks/171-7adbcaabf9d14cdf.js","185","static/chunks/app/layout-21ca3d760368e2b4.js"],"AppFrame"]
|
||||
0:["2k28Zwu9jDxUf-5jNvRk4",[[["",{"children":["shared",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["shared",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","shared","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/bbd6ee1e7265be74.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L6",null,{"children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]}]],null],null],["$L7",null]]]]
|
||||
7:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"Handler · Claude Activity"}],["$","meta","3",{"name":"description","content":"Monitor and manage Claude Code agents across projects."}],["$","link","4",{"rel":"icon","href":"/icon.svg?bab509b45a421e43","type":"image/svg+xml","sizes":"any"}]]
|
||||
1:null
|
||||
|
||||
@@ -19,6 +19,7 @@ from __future__ import annotations
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
from sqlalchemy import Connection
|
||||
|
||||
@@ -65,6 +66,20 @@ def write_mcp_config(working_dir: str, connectors: list[dict]) -> str | None:
|
||||
return path
|
||||
|
||||
|
||||
def memory_server_connector() -> dict:
|
||||
"""The built-in handler-memory MCP server, shaped like a connector row so it flows
|
||||
through ``write_mcp_config`` unchanged. Runs under the worker's own interpreter (the
|
||||
subprocess inherits the agent's spawn env, so identity + DATABASE_URL arrive the
|
||||
same way they do for hooks). Injected ahead of the DB connectors — an operator row
|
||||
named ``handler-memory`` deliberately overrides it."""
|
||||
return {
|
||||
"name": "handler-memory",
|
||||
"transport": "stdio",
|
||||
"command": sys.executable,
|
||||
"args": ["-m", "handler.mcpserver"],
|
||||
}
|
||||
|
||||
|
||||
def _skills_root(home: str | None = None) -> str:
|
||||
return os.path.join(home or os.path.expanduser("~"), ".claude", "skills")
|
||||
|
||||
@@ -144,6 +159,6 @@ def apply(working_dir: str, conn: Connection | None = None) -> dict:
|
||||
else:
|
||||
connectors = repo.list_claude_connectors(conn, enabled_only=True)
|
||||
skills = _load_skills(conn)
|
||||
mcp_path = write_mcp_config(working_dir, connectors)
|
||||
mcp_path = write_mcp_config(working_dir, [memory_server_connector()] + connectors)
|
||||
written = sync_user_skills(skills)
|
||||
return {"mcp_config": mcp_path, "skills_written": len(written)}
|
||||
|
||||
@@ -48,6 +48,11 @@ def build_settings(conn: Connection | None = None) -> dict:
|
||||
"SessionEnd": [
|
||||
{"hooks": [{"type": "command", "command": _hook_command("session_end")}]}
|
||||
],
|
||||
# Memory recall: injects the project's recent memory notes as context at
|
||||
# session start, so knowledge from earlier runs arrives without being asked.
|
||||
"SessionStart": [
|
||||
{"hooks": [{"type": "command", "command": _hook_command("session_start")}]}
|
||||
],
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "AskUserQuestion|Bash",
|
||||
@@ -77,6 +82,10 @@ def build_settings(conn: Connection | None = None) -> dict:
|
||||
deny = list(stored.get("deny", []))
|
||||
ask = list(stored.get("ask", []))
|
||||
plugins = repo.list_claude_plugins(conn, enabled_only=True)
|
||||
# The bundled memory MCP server's tools are always usable — memory is only worth
|
||||
# anything if agents can actually reach it under auto-deny.
|
||||
if "mcp__handler-memory" not in allow:
|
||||
allow.append("mcp__handler-memory")
|
||||
permissions: dict = {"defaultMode": mode, "allow": allow}
|
||||
if deny:
|
||||
permissions["deny"] = deny
|
||||
|
||||
@@ -36,6 +36,8 @@ from .tables import (
|
||||
commands,
|
||||
forge_hosts,
|
||||
log_entries,
|
||||
memory_links,
|
||||
memory_notes,
|
||||
projects,
|
||||
runtime_secrets,
|
||||
schedules,
|
||||
@@ -383,6 +385,18 @@ def _purge_agent_dependents(conn: Connection, agent_ids: list[int]) -> None:
|
||||
.where(shared_context.c.set_by_agent_id.in_(agent_ids))
|
||||
.values(set_by_agent_id=None)
|
||||
)
|
||||
# Memory outlives its authors (that's the point) — null the attribution like
|
||||
# shared_context, never delete the notes.
|
||||
conn.execute(
|
||||
memory_notes.update()
|
||||
.where(memory_notes.c.agent_id.in_(agent_ids))
|
||||
.values(agent_id=None)
|
||||
)
|
||||
conn.execute(
|
||||
memory_links.update()
|
||||
.where(memory_links.c.created_by_agent_id.in_(agent_ids))
|
||||
.values(created_by_agent_id=None)
|
||||
)
|
||||
conn.execute(approvals.delete().where(approvals.c.approved_by_agent_id.in_(agent_ids)))
|
||||
conn.execute(checkmarks.delete().where(checkmarks.c.agent_id.in_(agent_ids)))
|
||||
conn.execute(log_entries.delete().where(log_entries.c.agent_id.in_(agent_ids)))
|
||||
@@ -409,6 +423,22 @@ def delete_project(conn: Connection, project_id: str) -> bool:
|
||||
).all()
|
||||
]
|
||||
_purge_agent_dependents(conn, agent_ids)
|
||||
# Project-scoped memory goes with the project (links first — SQLite doesn't always
|
||||
# enforce the ON DELETE CASCADE, so clear them explicitly). Global notes stay.
|
||||
note_ids = [
|
||||
row[0]
|
||||
for row in conn.execute(
|
||||
select(memory_notes.c.id).where(memory_notes.c.project_id == project_id)
|
||||
).all()
|
||||
]
|
||||
if note_ids:
|
||||
conn.execute(
|
||||
memory_links.delete().where(
|
||||
memory_links.c.src_note_id.in_(note_ids)
|
||||
| memory_links.c.dst_note_id.in_(note_ids)
|
||||
)
|
||||
)
|
||||
conn.execute(memory_notes.delete().where(memory_notes.c.id.in_(note_ids)))
|
||||
conn.execute(schedules.delete().where(schedules.c.project_id == project_id))
|
||||
conn.execute(approvals.delete().where(approvals.c.project_id == project_id))
|
||||
conn.execute(commands.delete().where(commands.c.project_id == project_id))
|
||||
@@ -1227,3 +1257,175 @@ def get_latest_claimed_command(conn: Connection, type: str) -> dict | None:
|
||||
.limit(1)
|
||||
).first()
|
||||
return _row_to_dict(row)
|
||||
|
||||
# ------------------------------------------------------------------ agent memory (notes)
|
||||
|
||||
|
||||
def list_memory_notes(
|
||||
conn: Connection,
|
||||
project_id: str | None = None,
|
||||
include_global: bool = True,
|
||||
limit: int = 200,
|
||||
offset: int = 0,
|
||||
) -> list[dict]:
|
||||
"""Notes in scope, newest first. ``project_id=None`` means everything (the dashboard
|
||||
graph); a project id narrows to that project — plus the global notes unless told not
|
||||
to (the MCP server's read scope: my project + what everyone shares)."""
|
||||
stmt = select(memory_notes)
|
||||
if project_id is not None:
|
||||
scope = memory_notes.c.project_id == project_id
|
||||
if include_global:
|
||||
scope = scope | memory_notes.c.project_id.is_(None)
|
||||
stmt = stmt.where(scope)
|
||||
rows = conn.execute(
|
||||
stmt.order_by(memory_notes.c.id.desc()).limit(limit).offset(offset)
|
||||
).all()
|
||||
return [dict(r._mapping) for r in rows]
|
||||
|
||||
|
||||
def get_memory_note(conn: Connection, note_id: int) -> dict | None:
|
||||
row = conn.execute(select(memory_notes).where(memory_notes.c.id == note_id)).first()
|
||||
return _row_to_dict(row)
|
||||
|
||||
|
||||
def search_memory_notes(
|
||||
conn: Connection,
|
||||
query: str,
|
||||
project_id: str | None = None,
|
||||
include_global: bool = True,
|
||||
limit: int = 20,
|
||||
) -> list[dict]:
|
||||
"""Case-insensitive substring search over title/body/kind, every term required.
|
||||
|
||||
Deliberately plain LIKE, not FTS: it renders identically on Postgres and SQLite and
|
||||
is comfortably fast at the scale of a distilled note store. An empty query returns
|
||||
the most recent notes in scope — 'what's new' is a real question agents ask.
|
||||
"""
|
||||
terms = [t for t in query.split() if t]
|
||||
stmt = select(memory_notes)
|
||||
if project_id is not None:
|
||||
scope = memory_notes.c.project_id == project_id
|
||||
if include_global:
|
||||
scope = scope | memory_notes.c.project_id.is_(None)
|
||||
stmt = stmt.where(scope)
|
||||
for term in terms:
|
||||
pattern = f"%{term}%"
|
||||
stmt = stmt.where(
|
||||
memory_notes.c.title.ilike(pattern)
|
||||
| memory_notes.c.body.ilike(pattern)
|
||||
| memory_notes.c.kind.ilike(pattern)
|
||||
)
|
||||
rows = conn.execute(stmt.order_by(memory_notes.c.id.desc()).limit(limit)).all()
|
||||
return [dict(r._mapping) for r in rows]
|
||||
|
||||
|
||||
def create_memory_note(
|
||||
conn: Connection,
|
||||
title: str,
|
||||
body: str,
|
||||
kind: str = "fact",
|
||||
project_id: str | None = None,
|
||||
agent_id: int | None = None,
|
||||
tags: list[str] | None = None,
|
||||
) -> dict:
|
||||
now = _now()
|
||||
result = conn.execute(
|
||||
memory_notes.insert().values(
|
||||
project_id=project_id,
|
||||
agent_id=agent_id,
|
||||
title=title,
|
||||
body=body,
|
||||
kind=kind,
|
||||
tags=tags,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
)
|
||||
return get_memory_note(conn, result.inserted_primary_key[0])
|
||||
|
||||
|
||||
def update_memory_note(conn: Connection, note_id: int, **fields: Any) -> dict | None:
|
||||
allowed = {"title", "body", "kind", "tags", "project_id"}
|
||||
values = {k: v for k, v in fields.items() if k in allowed}
|
||||
if values:
|
||||
values["updated_at"] = _now()
|
||||
conn.execute(memory_notes.update().where(memory_notes.c.id == note_id).values(**values))
|
||||
return get_memory_note(conn, note_id)
|
||||
|
||||
|
||||
def delete_memory_note(conn: Connection, note_id: int) -> bool:
|
||||
"""Remove a note and its edges (both directions — explicit, not trusting CASCADE
|
||||
on SQLite)."""
|
||||
conn.execute(
|
||||
memory_links.delete().where(
|
||||
(memory_links.c.src_note_id == note_id) | (memory_links.c.dst_note_id == note_id)
|
||||
)
|
||||
)
|
||||
result = conn.execute(memory_notes.delete().where(memory_notes.c.id == note_id))
|
||||
return result.rowcount > 0
|
||||
|
||||
|
||||
def list_memory_links(conn: Connection, note_ids: list[int] | None = None) -> list[dict]:
|
||||
"""All edges, or only those touching the given notes (either endpoint)."""
|
||||
stmt = select(memory_links)
|
||||
if note_ids is not None:
|
||||
if not note_ids:
|
||||
return []
|
||||
stmt = stmt.where(
|
||||
memory_links.c.src_note_id.in_(note_ids) | memory_links.c.dst_note_id.in_(note_ids)
|
||||
)
|
||||
rows = conn.execute(stmt.order_by(memory_links.c.id)).all()
|
||||
return [dict(r._mapping) for r in rows]
|
||||
|
||||
|
||||
def get_memory_link(conn: Connection, link_id: int) -> dict | None:
|
||||
row = conn.execute(select(memory_links).where(memory_links.c.id == link_id)).first()
|
||||
return _row_to_dict(row)
|
||||
|
||||
|
||||
def create_memory_link(
|
||||
conn: Connection,
|
||||
src_note_id: int,
|
||||
dst_note_id: int,
|
||||
relation: str = "relates_to",
|
||||
agent_id: int | None = None,
|
||||
) -> dict:
|
||||
"""Create an edge; saying the same thing twice returns the existing edge instead of
|
||||
erroring — agents re-assert connections and that must stay cheap and idempotent."""
|
||||
existing = conn.execute(
|
||||
select(memory_links).where(
|
||||
memory_links.c.src_note_id == src_note_id,
|
||||
memory_links.c.dst_note_id == dst_note_id,
|
||||
memory_links.c.relation == relation,
|
||||
)
|
||||
).first()
|
||||
if existing is not None:
|
||||
return dict(existing._mapping)
|
||||
result = conn.execute(
|
||||
memory_links.insert().values(
|
||||
src_note_id=src_note_id,
|
||||
dst_note_id=dst_note_id,
|
||||
relation=relation,
|
||||
created_by_agent_id=agent_id,
|
||||
created_at=_now(),
|
||||
)
|
||||
)
|
||||
return get_memory_link(conn, result.inserted_primary_key[0])
|
||||
|
||||
|
||||
def delete_memory_link(conn: Connection, link_id: int) -> bool:
|
||||
result = conn.execute(memory_links.delete().where(memory_links.c.id == link_id))
|
||||
return result.rowcount > 0
|
||||
|
||||
|
||||
def memory_graph(conn: Connection, project_id: str | None = None) -> dict:
|
||||
"""The whole graph in one read — what the /memory page draws. Scoping to a project
|
||||
keeps its notes plus the global ones, and only edges with both endpoints in scope."""
|
||||
notes = list_memory_notes(conn, project_id=project_id, limit=1000)
|
||||
ids = [n["id"] for n in notes]
|
||||
links = list_memory_links(conn, note_ids=ids)
|
||||
in_scope = set(ids)
|
||||
links = [
|
||||
ln for ln in links if ln["src_note_id"] in in_scope and ln["dst_note_id"] in in_scope
|
||||
]
|
||||
return {"notes": notes, "links": links}
|
||||
|
||||
@@ -440,6 +440,57 @@ claude_config = Table(
|
||||
Column("updated_at", PortableTimestamp, nullable=False, server_default=func.now()),
|
||||
)
|
||||
|
||||
# ---- Agent memory (the distilled, linked layer over the raw transcript/log history).
|
||||
# Notes are the durable knowledge agents and operators leave behind — facts, decisions,
|
||||
# gotchas, runbooks — scoped to a project or global, written by agents through the
|
||||
# bundled handler-memory MCP server (or by the operator from the dashboard) and read
|
||||
# back at the next launch. Links make the notes a graph the /memory page can draw.
|
||||
NOTE_KINDS = ("fact", "decision", "gotcha", "runbook")
|
||||
|
||||
memory_notes = Table(
|
||||
"memory_notes",
|
||||
metadata,
|
||||
Column("id", PortableBigInt, primary_key=True, autoincrement=True),
|
||||
Column("project_id", String, ForeignKey("projects.id")), # null = global note
|
||||
# The authoring agent, when an agent wrote it; null = the operator (dashboard).
|
||||
# Nulled (not cascaded) when the agent is deleted — memory outlives its author.
|
||||
Column("agent_id", BigInteger, ForeignKey("agents.id")),
|
||||
Column("title", String, nullable=False),
|
||||
Column("body", String, nullable=False),
|
||||
Column("kind", String, nullable=False, server_default="fact"),
|
||||
Column("tags", PortableJSON), # list of strings, for lightweight filtering
|
||||
Column("created_at", PortableTimestamp, nullable=False, server_default=func.now()),
|
||||
Column("updated_at", PortableTimestamp, nullable=False, server_default=func.now()),
|
||||
CheckConstraint(_in("kind", NOTE_KINDS), name="ck_memory_notes_kind"),
|
||||
Index("ix_memory_notes_project_id", "project_id", "id"),
|
||||
)
|
||||
|
||||
# Note-to-note edges. ``relation`` is a free label ("relates_to", "supersedes",
|
||||
# "caused_by", …) — kept as TEXT, not a vocabulary, because agents coin them.
|
||||
memory_links = Table(
|
||||
"memory_links",
|
||||
metadata,
|
||||
Column("id", PortableBigInt, primary_key=True, autoincrement=True),
|
||||
Column(
|
||||
"src_note_id",
|
||||
BigInteger,
|
||||
ForeignKey("memory_notes.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
),
|
||||
Column(
|
||||
"dst_note_id",
|
||||
BigInteger,
|
||||
ForeignKey("memory_notes.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
),
|
||||
Column("relation", String, nullable=False, server_default="relates_to"),
|
||||
Column("created_by_agent_id", BigInteger, ForeignKey("agents.id")),
|
||||
Column("created_at", PortableTimestamp, nullable=False, server_default=func.now()),
|
||||
UniqueConstraint("src_note_id", "dst_note_id", "relation", name="uq_memory_links_edge"),
|
||||
Index("ix_memory_links_src", "src_note_id"),
|
||||
Index("ix_memory_links_dst", "dst_note_id"),
|
||||
)
|
||||
|
||||
# Recurring agent spawns. The worker checks for due rows on every loop pass and enqueues
|
||||
# an ordinary ``spawn`` command per firing (so scheduled runs show up in the Activity
|
||||
# audit trail like any other control action). Agent names must be unique per project, so
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Hook dispatch: ``python -m handler.hooks <event>``.
|
||||
|
||||
Events: ``stop``, ``session_end``, ``pre_tool_use``, ``notification``. Reads the event
|
||||
Events: ``stop``, ``session_end``, ``session_start``, ``pre_tool_use``,
|
||||
``notification``. Reads the event
|
||||
JSON on stdin, resolves the acting agent, dispatches, and exits 0. A resolution failure
|
||||
or unexpected error exits nonzero with a stderr message but never crashes the agent's
|
||||
turn in a way that loses data.
|
||||
@@ -11,10 +12,10 @@ from __future__ import annotations
|
||||
import sys
|
||||
|
||||
from ..db.engine import connection
|
||||
from . import checkpoint, gate, notify
|
||||
from . import checkpoint, gate, memory_ctx, notify
|
||||
from .context import read_input, resolve_identity
|
||||
|
||||
_EVENTS = {"stop", "session_end", "pre_tool_use", "notification"}
|
||||
_EVENTS = {"stop", "session_end", "session_start", "pre_tool_use", "notification"}
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
@@ -34,6 +35,8 @@ def main(argv: list[str] | None = None) -> int:
|
||||
|
||||
if event in ("stop", "session_end"):
|
||||
checkpoint.handle(conn, ident, hook_input)
|
||||
elif event == "session_start":
|
||||
memory_ctx.handle(conn, ident, hook_input)
|
||||
elif event == "pre_tool_use":
|
||||
gate.handle(conn, ident, hook_input)
|
||||
elif event == "notification":
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
"""SessionStart — memory recall.
|
||||
|
||||
Injects the most recent memory notes in the agent's scope (its project + global) as
|
||||
additional context, so knowledge distilled by earlier runs arrives without the agent
|
||||
having to ask. Deterministic and cheap: titles + truncated bodies of the newest few
|
||||
notes, plus a pointer at the handler-memory MCP tools for deeper search and for
|
||||
writing new notes. Never blocks the session — a memory failure must not stop work.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlalchemy import Connection
|
||||
|
||||
from ..db import repository as repo
|
||||
from .context import HookInput, Identity, emit
|
||||
|
||||
_NOTE_LIMIT = 8
|
||||
_BODY_CHARS = 400
|
||||
|
||||
|
||||
def _render(notes: list[dict]) -> str:
|
||||
lines = [
|
||||
"## Team memory",
|
||||
"Knowledge left by earlier agent runs and the operator. Search it with the "
|
||||
"`memory_search` tool before re-deriving how something works; save durable "
|
||||
"findings (facts, decisions, gotchas, runbooks) with `memory_save`, and "
|
||||
"connect related notes with `memory_link`.",
|
||||
]
|
||||
if notes:
|
||||
lines.append("")
|
||||
lines.append("Most recent notes in scope (of the ones you can search):")
|
||||
for n in notes:
|
||||
scope = n["project_id"] or "global"
|
||||
body = " ".join(n["body"].split())
|
||||
if len(body) > _BODY_CHARS:
|
||||
body = body[:_BODY_CHARS] + "…"
|
||||
lines.append(f"- [#{n['id']} · {n['kind']} · {scope}] {n['title']}: {body}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def handle(conn: Connection, ident: Identity, hook_input: HookInput) -> dict:
|
||||
try:
|
||||
notes = repo.list_memory_notes(conn, project_id=ident.project_id, limit=_NOTE_LIMIT)
|
||||
except Exception:
|
||||
notes = [] # recall is best-effort; the session must start regardless
|
||||
result = {
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "SessionStart",
|
||||
"additionalContext": _render(notes),
|
||||
}
|
||||
}
|
||||
emit(result)
|
||||
return result
|
||||
@@ -0,0 +1,305 @@
|
||||
"""The bundled ``handler-memory`` MCP server — the agents' read/write path to memory.
|
||||
|
||||
A deliberately dependency-free stdio MCP server (newline-delimited JSON-RPC 2.0, the
|
||||
same wire shape as any ``.mcp.json`` stdio entry): ``claude_gen`` injects it into every
|
||||
launch's ``--mcp-config`` as ``python -m handler.mcpserver``, and the subprocess
|
||||
inherits the agent's spawn environment, so identity (``HANDLER_AGENT_ID`` /
|
||||
``HANDLER_PROJECT_ID``) and ``DATABASE_URL`` arrive exactly the way they do for hooks.
|
||||
It talks straight to the database — memory is rows, workers stay stateless.
|
||||
|
||||
Tools: ``memory_search`` (substring search over the agent's project + global notes;
|
||||
empty query = most recent), ``memory_get`` (one note with its links), ``memory_save``
|
||||
(create, or update with ``note_id``), ``memory_link`` (connect two notes, idempotent).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
PROTOCOL_VERSION = "2025-06-18"
|
||||
SERVER_INFO = {"name": "handler-memory", "version": "0.1.0"}
|
||||
|
||||
_NOTE_KINDS = ["fact", "decision", "gotcha", "runbook"]
|
||||
|
||||
TOOLS: list[dict] = [
|
||||
{
|
||||
"name": "memory_search",
|
||||
"description": (
|
||||
"Search the team memory store (notes left by earlier agent runs and the "
|
||||
"operator) for your project plus global notes. Every whitespace-separated "
|
||||
"term must match the title, body, or kind (case-insensitive). An empty "
|
||||
"query returns the most recent notes. Use this BEFORE re-deriving how "
|
||||
"something works — an earlier run may have written it down."
|
||||
),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": "string", "description": "Search terms; empty = recent notes"},
|
||||
"limit": {"type": "integer", "minimum": 1, "maximum": 50, "default": 10},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "memory_get",
|
||||
"description": "Fetch one memory note in full, including its links to other notes.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {"note_id": {"type": "integer"}},
|
||||
"required": ["note_id"],
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "memory_save",
|
||||
"description": (
|
||||
"Save durable knowledge for future agent runs: a fact about the system, a "
|
||||
"decision and its rationale, a gotcha that cost you time, or a runbook. "
|
||||
"Write it for a reader with no context from this session. Pass note_id to "
|
||||
"update an existing note instead of creating a new one; pass global=true "
|
||||
"only for knowledge that applies across every project."
|
||||
),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {"type": "string", "description": "Short, searchable headline"},
|
||||
"body": {"type": "string", "description": "The knowledge itself, markdown ok"},
|
||||
"kind": {"type": "string", "enum": _NOTE_KINDS, "default": "fact"},
|
||||
"tags": {"type": "array", "items": {"type": "string"}},
|
||||
"note_id": {"type": "integer", "description": "Update this note instead"},
|
||||
"global": {
|
||||
"type": "boolean",
|
||||
"description": "Store unscoped (visible to every project)",
|
||||
"default": False,
|
||||
},
|
||||
},
|
||||
"required": ["title", "body"],
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "memory_link",
|
||||
"description": (
|
||||
"Connect two memory notes so the knowledge graph shows how they relate "
|
||||
"(e.g. a gotcha caused_by a decision, a runbook supersedes an older one). "
|
||||
"Idempotent: repeating an existing link is fine."
|
||||
),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"src_note_id": {"type": "integer"},
|
||||
"dst_note_id": {"type": "integer"},
|
||||
"relation": {"type": "string", "default": "relates_to"},
|
||||
},
|
||||
"required": ["src_note_id", "dst_note_id"],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def _iso(v: Any) -> Any:
|
||||
return v.isoformat() if isinstance(v, datetime) else v
|
||||
|
||||
|
||||
def _note_json(note: dict, snippet: bool = False) -> dict:
|
||||
out = {k: _iso(v) for k, v in note.items()}
|
||||
if snippet and isinstance(out.get("body"), str) and len(out["body"]) > 300:
|
||||
out["body"] = out["body"][:300] + "…"
|
||||
return out
|
||||
|
||||
|
||||
class MemoryServer:
|
||||
"""Tool dispatch against the handler database. One short connection per call —
|
||||
the process lives as long as the agent's session, but holds nothing in memory."""
|
||||
|
||||
def __init__(self, agent_id: int | None, project_id: str | None):
|
||||
self.agent_id = agent_id
|
||||
self.project_id = project_id
|
||||
|
||||
def _connection(self):
|
||||
from ..db.engine import connection
|
||||
|
||||
return connection()
|
||||
|
||||
# ---- tool implementations ----
|
||||
|
||||
def memory_search(self, args: dict) -> dict:
|
||||
from ..db import repository as repo
|
||||
|
||||
query = (args.get("query") or "").strip()
|
||||
limit = int(args.get("limit") or 10)
|
||||
with self._connection() as conn:
|
||||
notes = repo.search_memory_notes(
|
||||
conn, query, project_id=self.project_id, limit=limit
|
||||
)
|
||||
return {
|
||||
"count": len(notes),
|
||||
"notes": [_note_json(n, snippet=True) for n in notes],
|
||||
}
|
||||
|
||||
def memory_get(self, args: dict) -> dict:
|
||||
from ..db import repository as repo
|
||||
|
||||
note_id = int(args["note_id"])
|
||||
with self._connection() as conn:
|
||||
note = repo.get_memory_note(conn, note_id)
|
||||
if note is None:
|
||||
raise ValueError(f"note {note_id} not found")
|
||||
links = repo.list_memory_links(conn, note_ids=[note_id])
|
||||
other_ids = {
|
||||
(ln["dst_note_id"] if ln["src_note_id"] == note_id else ln["src_note_id"])
|
||||
for ln in links
|
||||
}
|
||||
titles = {
|
||||
n["id"]: n["title"]
|
||||
for n in (repo.get_memory_note(conn, i) for i in other_ids)
|
||||
if n is not None
|
||||
}
|
||||
return {
|
||||
"note": _note_json(note),
|
||||
"links": [
|
||||
{
|
||||
"id": ln["id"],
|
||||
"src_note_id": ln["src_note_id"],
|
||||
"dst_note_id": ln["dst_note_id"],
|
||||
"relation": ln["relation"],
|
||||
"other_title": titles.get(
|
||||
ln["dst_note_id"] if ln["src_note_id"] == note_id else ln["src_note_id"]
|
||||
),
|
||||
}
|
||||
for ln in links
|
||||
],
|
||||
}
|
||||
|
||||
def memory_save(self, args: dict) -> dict:
|
||||
from ..db import repository as repo
|
||||
|
||||
title = (args.get("title") or "").strip()
|
||||
body = (args.get("body") or "").strip()
|
||||
if not title or not body:
|
||||
raise ValueError("title and body are required")
|
||||
kind = args.get("kind") or "fact"
|
||||
if kind not in _NOTE_KINDS:
|
||||
raise ValueError(f"kind must be one of {_NOTE_KINDS}")
|
||||
tags = args.get("tags")
|
||||
with self._connection() as conn:
|
||||
if args.get("note_id"):
|
||||
note_id = int(args["note_id"])
|
||||
if repo.get_memory_note(conn, note_id) is None:
|
||||
raise ValueError(f"note {note_id} not found")
|
||||
note = repo.update_memory_note(
|
||||
conn, note_id, title=title, body=body, kind=kind, tags=tags
|
||||
)
|
||||
return {"updated": True, "note": _note_json(note)}
|
||||
project_id = None if args.get("global") else self.project_id
|
||||
note = repo.create_memory_note(
|
||||
conn,
|
||||
title=title,
|
||||
body=body,
|
||||
kind=kind,
|
||||
project_id=project_id,
|
||||
agent_id=self.agent_id,
|
||||
tags=tags,
|
||||
)
|
||||
return {"created": True, "note": _note_json(note)}
|
||||
|
||||
def memory_link(self, args: dict) -> dict:
|
||||
from ..db import repository as repo
|
||||
|
||||
src, dst = int(args["src_note_id"]), int(args["dst_note_id"])
|
||||
if src == dst:
|
||||
raise ValueError("a note cannot link to itself")
|
||||
relation = (args.get("relation") or "relates_to").strip() or "relates_to"
|
||||
with self._connection() as conn:
|
||||
for note_id in (src, dst):
|
||||
if repo.get_memory_note(conn, note_id) is None:
|
||||
raise ValueError(f"note {note_id} not found")
|
||||
link = repo.create_memory_link(
|
||||
conn, src, dst, relation=relation, agent_id=self.agent_id
|
||||
)
|
||||
return {"link": {k: _iso(v) for k, v in link.items()}}
|
||||
|
||||
def call_tool(self, name: str, args: dict) -> dict:
|
||||
handlers = {
|
||||
"memory_search": self.memory_search,
|
||||
"memory_get": self.memory_get,
|
||||
"memory_save": self.memory_save,
|
||||
"memory_link": self.memory_link,
|
||||
}
|
||||
if name not in handlers:
|
||||
raise ValueError(f"unknown tool '{name}'")
|
||||
return handlers[name](args)
|
||||
|
||||
|
||||
def handle_message(server: MemoryServer, msg: dict) -> dict | None:
|
||||
"""One JSON-RPC message in, one response out (None for notifications)."""
|
||||
method = msg.get("method")
|
||||
msg_id = msg.get("id")
|
||||
|
||||
if msg_id is None:
|
||||
return None # a notification (initialized, cancelled, …) — nothing to answer
|
||||
if not method:
|
||||
return _error(msg_id, -32600, "invalid request: no method")
|
||||
|
||||
if method == "initialize":
|
||||
client_version = (msg.get("params") or {}).get("protocolVersion") or PROTOCOL_VERSION
|
||||
return _result(
|
||||
msg_id,
|
||||
{
|
||||
"protocolVersion": client_version,
|
||||
"capabilities": {"tools": {}},
|
||||
"serverInfo": SERVER_INFO,
|
||||
},
|
||||
)
|
||||
if method == "ping":
|
||||
return _result(msg_id, {})
|
||||
if method == "tools/list":
|
||||
return _result(msg_id, {"tools": TOOLS})
|
||||
if method == "tools/call":
|
||||
params = msg.get("params") or {}
|
||||
name = params.get("name") or ""
|
||||
args = params.get("arguments") or {}
|
||||
try:
|
||||
payload = server.call_tool(name, args)
|
||||
content = [{"type": "text", "text": json.dumps(payload, ensure_ascii=False)}]
|
||||
return _result(msg_id, {"content": content, "isError": False})
|
||||
except Exception as exc: # tool errors go back in-band, per MCP
|
||||
content = [{"type": "text", "text": f"error: {exc}"}]
|
||||
return _result(msg_id, {"content": content, "isError": True})
|
||||
return _error(msg_id, -32601, f"method '{method}' not supported")
|
||||
|
||||
|
||||
def _result(msg_id: Any, result: dict) -> dict:
|
||||
return {"jsonrpc": "2.0", "id": msg_id, "result": result}
|
||||
|
||||
|
||||
def _error(msg_id: Any, code: int, message: str) -> dict:
|
||||
return {"jsonrpc": "2.0", "id": msg_id, "error": {"code": code, "message": message}}
|
||||
|
||||
|
||||
def serve(stdin=None, stdout=None) -> int:
|
||||
"""The stdio loop: one JSON-RPC message per line, responses flushed immediately."""
|
||||
import os
|
||||
|
||||
stdin = stdin or sys.stdin
|
||||
stdout = stdout or sys.stdout
|
||||
agent_id_raw = os.environ.get("HANDLER_AGENT_ID")
|
||||
server = MemoryServer(
|
||||
agent_id=int(agent_id_raw) if agent_id_raw else None,
|
||||
project_id=os.environ.get("HANDLER_PROJECT_ID") or None,
|
||||
)
|
||||
for line in stdin:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
msg = json.loads(line)
|
||||
except ValueError:
|
||||
print(
|
||||
json.dumps(_error(None, -32700, "parse error")), file=stdout, flush=True
|
||||
)
|
||||
continue
|
||||
response = handle_message(server, msg)
|
||||
if response is not None:
|
||||
print(json.dumps(response, ensure_ascii=False), file=stdout, flush=True)
|
||||
return 0
|
||||
@@ -0,0 +1,10 @@
|
||||
"""``python -m handler.mcpserver`` — run the bundled handler-memory MCP server."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
from . import serve
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(serve())
|
||||
@@ -0,0 +1,73 @@
|
||||
"""agent memory: notes + links
|
||||
|
||||
Revision ID: 0014_agent_memory
|
||||
Revises: 0013_schedule_model
|
||||
Create Date: 2026-08-04
|
||||
|
||||
The distilled, linked knowledge layer over the raw transcript/log history. Agents write
|
||||
notes (facts, decisions, gotchas, runbooks) through the bundled handler-memory MCP
|
||||
server; operators write them from the dashboard's Memory page. Links make the notes a
|
||||
graph — the web of "how everything is connected" the /memory page draws. Scoped per
|
||||
project or global; state lives only here, so it survives disposable workers by
|
||||
construction.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
from handler.db.types import PortableBigInt, PortableJSON, PortableTimestamp
|
||||
|
||||
revision: str = "0014_agent_memory"
|
||||
down_revision: str | None = "0013_schedule_model"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"memory_notes",
|
||||
sa.Column("id", PortableBigInt, primary_key=True, autoincrement=True),
|
||||
sa.Column("project_id", sa.String(), sa.ForeignKey("projects.id")),
|
||||
sa.Column("agent_id", sa.BigInteger(), sa.ForeignKey("agents.id")),
|
||||
sa.Column("title", sa.String(), nullable=False),
|
||||
sa.Column("body", sa.String(), nullable=False),
|
||||
sa.Column("kind", sa.String(), nullable=False, server_default="fact"),
|
||||
sa.Column("tags", PortableJSON),
|
||||
sa.Column("created_at", PortableTimestamp, nullable=False, server_default=sa.func.now()),
|
||||
sa.Column("updated_at", PortableTimestamp, nullable=False, server_default=sa.func.now()),
|
||||
sa.CheckConstraint(
|
||||
"kind IN ('fact', 'decision', 'gotcha', 'runbook')", name="ck_memory_notes_kind"
|
||||
),
|
||||
)
|
||||
op.create_index("ix_memory_notes_project_id", "memory_notes", ["project_id", "id"])
|
||||
op.create_table(
|
||||
"memory_links",
|
||||
sa.Column("id", PortableBigInt, primary_key=True, autoincrement=True),
|
||||
sa.Column(
|
||||
"src_note_id",
|
||||
sa.BigInteger(),
|
||||
sa.ForeignKey("memory_notes.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"dst_note_id",
|
||||
sa.BigInteger(),
|
||||
sa.ForeignKey("memory_notes.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("relation", sa.String(), nullable=False, server_default="relates_to"),
|
||||
sa.Column("created_by_agent_id", sa.BigInteger(), sa.ForeignKey("agents.id")),
|
||||
sa.Column("created_at", PortableTimestamp, nullable=False, server_default=sa.func.now()),
|
||||
sa.UniqueConstraint("src_note_id", "dst_note_id", "relation", name="uq_memory_links_edge"),
|
||||
)
|
||||
op.create_index("ix_memory_links_src", "memory_links", ["src_note_id"])
|
||||
op.create_index("ix_memory_links_dst", "memory_links", ["dst_note_id"])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("memory_links")
|
||||
op.drop_table("memory_notes")
|
||||
@@ -434,9 +434,11 @@ def test_spawn_applies_managed_config(env, fake_launch):
|
||||
agent = spawn.spawn("proj", "api", task="do it")
|
||||
wd = agent["working_dir"]
|
||||
|
||||
# Disabled connectors are excluded from the generated --mcp-config file.
|
||||
# Disabled connectors are excluded from the generated --mcp-config file; the
|
||||
# built-in handler-memory server is always injected ahead of the DB connectors.
|
||||
mcp = json.loads(open(claude_gen.mcp_config_path(wd)).read())
|
||||
assert list(mcp["mcpServers"]) == ["github"]
|
||||
assert list(mcp["mcpServers"]) == ["handler-memory", "github"]
|
||||
assert mcp["mcpServers"]["handler-memory"]["args"] == ["-m", "handler.mcpserver"]
|
||||
# Skills synced to the (test-scoped) user-level dir.
|
||||
skill = os.path.join(str(env["tmp"]), ".claude", "skills", "deploy", "SKILL.md")
|
||||
assert os.path.exists(skill)
|
||||
|
||||
@@ -79,11 +79,17 @@ def test_spawn_creates_agent_settings_and_run(env, fake_launch):
|
||||
with get_engine().begin() as conn:
|
||||
assert repo.get_agent_by_name(conn, "proj", "api")["id"] == agent["id"]
|
||||
|
||||
# settings.json wires all four hook events AND the headless permission allowlist
|
||||
# settings.json wires all five hook events AND the headless permission allowlist
|
||||
# (claude -p auto-denies anything that would prompt; the allowlist is what lets
|
||||
# normal work proceed — the hooks stay the hard gate).
|
||||
settings = json.loads((root / ".claude" / "settings.json").read_text())
|
||||
assert set(settings["hooks"]) == {"Stop", "SessionEnd", "PreToolUse", "Notification"}
|
||||
assert set(settings["hooks"]) == {
|
||||
"Stop",
|
||||
"SessionEnd",
|
||||
"SessionStart",
|
||||
"PreToolUse",
|
||||
"Notification",
|
||||
}
|
||||
pre = settings["hooks"]["PreToolUse"][0]
|
||||
assert pre["matcher"] == "AskUserQuestion|Bash"
|
||||
assert "handler.hooks pre_tool_use" in pre["hooks"][0]["command"]
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
"""Agent memory: the notes/links DAL, the /memory API, the bundled handler-memory MCP
|
||||
server, and the SessionStart recall hook."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from handler.db import repository as repo
|
||||
from handler.db.engine import get_engine
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def seeded(env):
|
||||
"""Two projects, one agent, and a small note graph."""
|
||||
with get_engine().begin() as conn:
|
||||
repo.create_project(conn, "proj", "/tmp/proj")
|
||||
repo.create_project(conn, "other", "/tmp/other")
|
||||
agent = repo.create_agent(conn, "proj", "api", "/tmp/proj", "working")
|
||||
n1 = repo.create_memory_note(
|
||||
conn, "Auth flow", "Tokens are minted in deps.py", "fact",
|
||||
project_id="proj", agent_id=agent["id"], tags=["auth"],
|
||||
)
|
||||
n2 = repo.create_memory_note(
|
||||
conn, "Use SQLite fallback", "Postgres is default, SQLite for tests",
|
||||
"decision", project_id="proj",
|
||||
)
|
||||
n3 = repo.create_memory_note(
|
||||
conn, "Global runbook", "How to rotate tokens", "runbook",
|
||||
)
|
||||
n4 = repo.create_memory_note(
|
||||
conn, "Other-project note", "Not visible from proj scope", "fact",
|
||||
project_id="other",
|
||||
)
|
||||
link = repo.create_memory_link(
|
||||
conn, n1["id"], n2["id"], relation="caused_by", agent_id=agent["id"]
|
||||
)
|
||||
return {"agent": agent, "n1": n1, "n2": n2, "n3": n3, "n4": n4, "link": link}
|
||||
|
||||
|
||||
# --- DAL -------------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_scoped_listing_and_search(seeded):
|
||||
with get_engine().begin() as conn:
|
||||
# Project scope = its notes + global, newest first; never another project's.
|
||||
notes = repo.list_memory_notes(conn, project_id="proj")
|
||||
ids = [n["id"] for n in notes]
|
||||
assert seeded["n4"]["id"] not in ids
|
||||
assert {seeded["n1"]["id"], seeded["n2"]["id"], seeded["n3"]["id"]} <= set(ids)
|
||||
|
||||
# Every term must match; case-insensitive over title/body/kind.
|
||||
hits = repo.search_memory_notes(conn, "auth tokens", project_id="proj")
|
||||
assert [h["id"] for h in hits] == [seeded["n1"]["id"]]
|
||||
# Empty query = recent notes in scope.
|
||||
assert repo.search_memory_notes(conn, "", project_id="proj")
|
||||
# kind matches too.
|
||||
assert any(
|
||||
h["id"] == seeded["n3"]["id"]
|
||||
for h in repo.search_memory_notes(conn, "runbook", project_id="proj")
|
||||
)
|
||||
|
||||
|
||||
def test_link_idempotent_and_graph_scope(seeded):
|
||||
with get_engine().begin() as conn:
|
||||
again = repo.create_memory_link(
|
||||
conn, seeded["n1"]["id"], seeded["n2"]["id"], relation="caused_by"
|
||||
)
|
||||
assert again["id"] == seeded["link"]["id"] # re-asserting is not a new edge
|
||||
|
||||
# A cross-scope link's edge drops out of a scoped graph when one endpoint is out.
|
||||
repo.create_memory_link(conn, seeded["n1"]["id"], seeded["n4"]["id"])
|
||||
graph = repo.memory_graph(conn, project_id="proj")
|
||||
graph_ids = {n["id"] for n in graph["notes"]}
|
||||
assert seeded["n4"]["id"] not in graph_ids
|
||||
assert [ln["id"] for ln in graph["links"]] == [seeded["link"]["id"]]
|
||||
|
||||
|
||||
def test_delete_note_removes_edges(seeded):
|
||||
with get_engine().begin() as conn:
|
||||
assert repo.delete_memory_note(conn, seeded["n2"]["id"])
|
||||
assert repo.get_memory_note(conn, seeded["n2"]["id"]) is None
|
||||
assert repo.list_memory_links(conn, note_ids=[seeded["n1"]["id"]]) == []
|
||||
|
||||
|
||||
def test_agent_delete_keeps_notes_project_delete_removes_them(seeded):
|
||||
with get_engine().begin() as conn:
|
||||
# Deleting the authoring agent orphans the attribution, never the knowledge.
|
||||
assert repo.delete_agent(conn, "proj", "api")
|
||||
note = repo.get_memory_note(conn, seeded["n1"]["id"])
|
||||
assert note is not None and note["agent_id"] is None
|
||||
link = repo.get_memory_link(conn, seeded["link"]["id"])
|
||||
assert link is not None and link["created_by_agent_id"] is None
|
||||
|
||||
# Deleting the project takes its notes (and their edges); global notes stay.
|
||||
assert repo.delete_project(conn, "proj")
|
||||
assert repo.get_memory_note(conn, seeded["n1"]["id"]) is None
|
||||
assert repo.get_memory_note(conn, seeded["n2"]["id"]) is None
|
||||
assert repo.get_memory_note(conn, seeded["n3"]["id"]) is not None
|
||||
|
||||
|
||||
# --- API -------------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_api_note_crud_and_graph(client, auth, seeded):
|
||||
r = client.post(
|
||||
"/memory/notes",
|
||||
json={"title": "From the dashboard", "body": "operator wisdom", "kind": "gotcha"},
|
||||
headers=auth,
|
||||
)
|
||||
assert r.status_code == 201
|
||||
note = r.json()
|
||||
assert note["agent_id"] is None and note["project_id"] is None
|
||||
|
||||
r = client.patch(f"/memory/notes/{note['id']}", json={"kind": "runbook"}, headers=auth)
|
||||
assert r.status_code == 200 and r.json()["kind"] == "runbook"
|
||||
|
||||
r = client.get("/memory/notes?q=wisdom", headers=auth)
|
||||
assert [n["id"] for n in r.json()] == [note["id"]]
|
||||
|
||||
r = client.get("/memory/graph?project_id=proj", headers=auth)
|
||||
graph = r.json()
|
||||
assert {n["id"] for n in graph["notes"]} >= {seeded["n1"]["id"], seeded["n3"]["id"]}
|
||||
assert graph["links"][0]["relation"] == "caused_by"
|
||||
|
||||
assert client.delete(f"/memory/notes/{note['id']}", headers=auth).status_code == 200
|
||||
assert client.get(f"/memory/notes/{note['id']}", headers=auth).status_code == 404
|
||||
|
||||
|
||||
def test_api_validation(client, auth, seeded):
|
||||
n1 = seeded["n1"]["id"]
|
||||
# Unknown project scope on create; self-links; missing endpoints.
|
||||
r = client.post(
|
||||
"/memory/notes",
|
||||
json={"title": "x", "body": "y", "project_id": "nope"},
|
||||
headers=auth,
|
||||
)
|
||||
assert r.status_code == 400
|
||||
r = client.post(
|
||||
"/memory/links", json={"src_note_id": n1, "dst_note_id": n1}, headers=auth
|
||||
)
|
||||
assert r.status_code == 400
|
||||
r = client.post(
|
||||
"/memory/links", json={"src_note_id": n1, "dst_note_id": 999999}, headers=auth
|
||||
)
|
||||
assert r.status_code == 404
|
||||
# Bad kind is a 422 straight from the schema.
|
||||
r = client.post("/memory/notes", json={"title": "x", "body": "y", "kind": "poem"}, headers=auth)
|
||||
assert r.status_code == 422
|
||||
# No token, no memory.
|
||||
assert client.get("/memory/notes").status_code == 401
|
||||
|
||||
|
||||
# --- MCP server ------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _rpc(server, method, params=None, msg_id=1):
|
||||
from handler.mcpserver import handle_message
|
||||
|
||||
return handle_message(server, {"jsonrpc": "2.0", "id": msg_id, "method": method,
|
||||
"params": params or {}})
|
||||
|
||||
|
||||
def _call_tool(server, name, args):
|
||||
resp = _rpc(server, "tools/call", {"name": name, "arguments": args})
|
||||
result = resp["result"]
|
||||
return result["isError"], json.loads(result["content"][0]["text"]) if not result[
|
||||
"isError"
|
||||
] else result["content"][0]["text"]
|
||||
|
||||
|
||||
def test_mcp_protocol_basics(seeded):
|
||||
from handler.mcpserver import MemoryServer, handle_message
|
||||
|
||||
server = MemoryServer(agent_id=seeded["agent"]["id"], project_id="proj")
|
||||
init = _rpc(server, "initialize", {"protocolVersion": "2025-06-18"})
|
||||
assert init["result"]["serverInfo"]["name"] == "handler-memory"
|
||||
assert init["result"]["protocolVersion"] == "2025-06-18"
|
||||
# Notifications get no response; unknown methods error.
|
||||
assert handle_message(server, {"jsonrpc": "2.0", "method": "notifications/initialized"}) is None
|
||||
assert _rpc(server, "resources/list")["error"]["code"] == -32601
|
||||
tools = _rpc(server, "tools/list")["result"]["tools"]
|
||||
assert {t["name"] for t in tools} == {
|
||||
"memory_search", "memory_get", "memory_save", "memory_link",
|
||||
}
|
||||
|
||||
|
||||
def test_mcp_tools_roundtrip(seeded):
|
||||
from handler.mcpserver import MemoryServer
|
||||
|
||||
server = MemoryServer(agent_id=seeded["agent"]["id"], project_id="proj")
|
||||
|
||||
err, saved = _call_tool(server, "memory_save", {
|
||||
"title": "Worker heartbeats", "body": "Reaper marks stale workers crashed",
|
||||
"kind": "fact", "tags": ["workers"],
|
||||
})
|
||||
assert not err and saved["created"]
|
||||
assert saved["note"]["project_id"] == "proj"
|
||||
assert saved["note"]["agent_id"] == seeded["agent"]["id"]
|
||||
|
||||
err, found = _call_tool(server, "memory_search", {"query": "heartbeats"})
|
||||
assert not err and found["count"] == 1
|
||||
|
||||
# Search sees global notes; never the other project's.
|
||||
err, found = _call_tool(server, "memory_search", {"query": "rotate tokens"})
|
||||
assert not err and found["count"] == 1
|
||||
err, found = _call_tool(server, "memory_search", {"query": "Other-project"})
|
||||
assert not err and found["count"] == 0
|
||||
|
||||
err, linked = _call_tool(server, "memory_link", {
|
||||
"src_note_id": saved["note"]["id"], "dst_note_id": seeded["n1"]["id"],
|
||||
"relation": "relates_to",
|
||||
})
|
||||
assert not err and linked["link"]["created_by_agent_id"] == seeded["agent"]["id"]
|
||||
|
||||
err, got = _call_tool(server, "memory_get", {"note_id": saved["note"]["id"]})
|
||||
assert not err
|
||||
assert got["links"][0]["other_title"] == "Auth flow"
|
||||
|
||||
# Update in place via note_id.
|
||||
err, updated = _call_tool(server, "memory_save", {
|
||||
"note_id": saved["note"]["id"], "title": "Worker heartbeats",
|
||||
"body": "expanded", "kind": "gotcha",
|
||||
})
|
||||
assert not err and updated["updated"]
|
||||
|
||||
# Tool errors come back in-band, not as protocol errors.
|
||||
err, msg = _call_tool(server, "memory_get", {"note_id": 424242})
|
||||
assert err and "not found" in msg
|
||||
|
||||
|
||||
def test_mcp_global_save(seeded):
|
||||
from handler.mcpserver import MemoryServer
|
||||
|
||||
server = MemoryServer(agent_id=None, project_id="proj")
|
||||
err, saved = _call_tool(server, "memory_save", {
|
||||
"title": "For everyone", "body": "x", "global": True,
|
||||
})
|
||||
assert not err and saved["note"]["project_id"] is None
|
||||
|
||||
|
||||
# --- SessionStart recall hook + launch wiring ------------------------------------------
|
||||
|
||||
|
||||
def test_session_start_hook_injects_notes(seeded, capsys):
|
||||
from handler.hooks import memory_ctx
|
||||
from handler.hooks.context import HookInput, Identity
|
||||
|
||||
ident = Identity(seeded["agent"]["id"], "proj", "api", "/tmp/proj")
|
||||
with get_engine().begin() as conn:
|
||||
result = memory_ctx.handle(conn, ident, HookInput(raw={}, event="session_start"))
|
||||
ctx = result["hookSpecificOutput"]["additionalContext"]
|
||||
assert result["hookSpecificOutput"]["hookEventName"] == "SessionStart"
|
||||
assert "Auth flow" in ctx and "Global runbook" in ctx
|
||||
assert "Other-project note" not in ctx
|
||||
assert "memory_search" in ctx # the pointer at the tools
|
||||
# The hook wrote its JSON response to stdout for claude to consume.
|
||||
assert json.loads(capsys.readouterr().out)["hookSpecificOutput"]
|
||||
|
||||
|
||||
def test_settings_wire_session_start_and_memory_allow(env):
|
||||
from handler.control import settings_gen
|
||||
|
||||
settings = settings_gen.build_settings()
|
||||
hook = settings["hooks"]["SessionStart"][0]["hooks"][0]["command"]
|
||||
assert hook.endswith("-m handler.hooks session_start")
|
||||
assert "mcp__handler-memory" in settings["permissions"]["allow"]
|
||||
Reference in New Issue
Block a user