1import router from "./routes.js";
2import monitorMode from "./modes/monitor.js";
3
4export async function init(core) {
5 core.modes.registerMode("land:monitor", monitorMode, "monitor");
6
7 if (core.llm?.registerModeAssignment) {
8 core.llm.registerModeAssignment("land:monitor", "monitor");
9 }
10
11 return { router };
12}
13
1export default {
2 name: "monitor",
3 version: "1.0.2",
4 builtFor: "TreeOS",
5 description:
6 "Activity monitoring for the land, accessible through both AI conversation and raw data " +
7 "endpoints. Registers the land:monitor mode, a conversational AI that acts like a talking " +
8 "dashboard. Instead of dumping raw data, the monitor tells a story: '12 AI conversations " +
9 "today, mostly on the Fitness tree. Your bench press had 3 sessions this week, progressing " +
10 "from 130 to 140.' The mode has access to land-status, land-ext-list, land-users, " +
11 "land-peers, land-system-nodes, land-config-read, get-contributions-by-user, and " +
12 "get-root-nodes for gathering data before summarizing.\n\n" +
13 "The POST /land/activity endpoint drives the conversational interface. It accepts a " +
14 "natural language query, runs it through runChat in the land:monitor mode, and returns " +
15 "the AI's narrative summary. The CLI command 'activity' maps to this endpoint, so admins " +
16 "can ask 'what happened today' or 'which trees are busiest' from the command line.\n\n" +
17 "The GET /land/activity endpoint provides structured data without AI involvement, designed " +
18 "for dashboards and health checks. It aggregates contribution counts (today and this week), " +
19 "chat session counts (today and this week), action type breakdowns (top 10 actions today), " +
20 "AI mode usage breakdowns (top 10 mode/zone combinations today), total user count, loaded " +
21 "extension count, and registered hooks. All date ranges are computed server-side (24 hours " +
22 "and 7 days from request time). Both endpoints are admin-only.",
23
24 needs: {
25 services: ["hooks"],
26 models: ["Node", "User", "Contribution"],
27 },
28
29 optional: {
30 services: ["llm"],
31 },
32
33 provides: {
34 models: {},
35 routes: "./routes.js",
36 tools: false,
37 jobs: false,
38 orchestrator: false,
39 energyActions: {},
40 sessionTypes: {},
41
42 hooks: {
43 fires: [],
44 listens: [],
45 },
46
47 cli: [
48 {
49 command: "activity [query...]", scope: ["home","land"],
50 description: "Ask about land activity. What happened today, which trees are busiest, AI usage stats.",
51 method: "POST",
52 endpoint: "/land/activity",
53 body: ["query"],
54 },
55 ],
56 },
57};
58
1export default {
2 emoji: "📊",
3 label: "Monitor",
4 bigMode: "land",
5
6 toolNames: [
7 "land-status",
8 "land-ext-list",
9 "land-users",
10 "land-peers",
11 "land-system-nodes",
12 "land-config-read",
13 "get-contributions-by-user",
14 "get-root-nodes",
15 ],
16
17 buildSystemPrompt({ username }) {
18 return `You are the activity monitor for this TreeOS land. ${username} is asking about what's happening.
19
20YOUR ROLE
21You summarize land activity. You don't dump raw data. You tell a story.
22"12 AI conversations today, mostly on the Fitness tree. Prestige fired 8 times.
23Your bench press had 3 sessions this week, progressing from 130 to 140."
24
25WHAT YOU CAN SEE
26Use your tools to gather data, then summarize concisely:
27- land-status: overview of the land (users, trees, extensions, peers)
28- land-ext-list: which extensions are loaded with versions
29- land-users: user list with profile types
30- land-peers: federation peer status
31- land-system-nodes: system node details
32- land-config-read: read any config value
33- get-contributions-by-user: audit trail for a specific user
34- get-root-nodes: list of trees
35
36HOW TO ANSWER
371. Gather relevant data with tools (usually 1-2 calls)
382. Aggregate mentally: counts, trends, patterns
393. Present as a short narrative, not a table dump
404. Highlight anything unusual: spikes, errors, new activity, quiet periods
41
42EXAMPLES OF GOOD ANSWERS
43"Quiet day. 4 AI chats, all on your Life tree. No new users. All 2 peers alive."
44
45"Busy week. 89 contributions across 3 trees. Fitness tree is the most active with 34 contributions (mostly value edits from workout logging). 2 new users registered. Understanding ran twice on the Life tree."
46
47"Right now: 3 active sessions, 25 extensions loaded, 2 peers connected. No circuit breakers tripped. Last heartbeat was 4 minutes ago."
48
49WHAT NOT TO DO
50- Don't list every contribution individually
51- Don't show raw JSON
52- Don't say "I queried the database and found..."
53- Don't overwhelm with numbers. Pick the 3-5 most interesting facts.
54- If asked about a specific thing, focus on that. Don't give a full report when they asked about one tree.
55
56TONE
57Clear. Concise. Like a dashboard that talks. Not a sysadmin report.`;
58 },
59};
60
1import express from "express";
2import authenticate from "../../seed/middleware/authenticate.js";
3import { sendOk, sendError, ERR } from "../../seed/protocol.js";
4import User from "../../seed/models/user.js";
5import Contribution from "../../seed/models/contribution.js";
6import log from "../../seed/log.js";
7
8const router = express.Router();
9
10// POST /land/activity - ask about land activity
11router.post("/land/activity", authenticate, async (req, res) => {
12 try {
13 const user = await User.findById(req.userId).select("isAdmin username").lean();
14 if (!user || !user.isAdmin) {
15 return sendError(res, 403, ERR.FORBIDDEN, "Requires admin.");
16 }
17
18 const rawQuery = req.body.query;
19 const query = Array.isArray(rawQuery) ? rawQuery.join(" ") : rawQuery;
20 if (!query) return sendError(res, 400, ERR.INVALID_INPUT, "query required");
21
22 const { runChat } = await import("../../seed/llm/conversation.js");
23
24 const { answer, chatId } = await runChat({
25 userId: req.userId,
26 username: user.username,
27 message: query,
28 mode: "land:monitor",
29 res,
30 });
31
32 sendOk(res, { answer, chatId });
33 } catch (err) {
34 log.error("Monitor", "Activity query error:", err.message);
35 sendError(res, 500, ERR.INTERNAL, err.message);
36 }
37});
38
39// GET /land/activity - quick stats without AI (for dashboards, health checks)
40router.get("/land/activity", authenticate, async (req, res) => {
41 try {
42 const user = await User.findById(req.userId).select("isAdmin").lean();
43 if (!user || !user.isAdmin) {
44 return sendError(res, 403, ERR.FORBIDDEN, "Requires god-tier.");
45 }
46
47 const now = new Date();
48 const oneDayAgo = new Date(now - 24 * 60 * 60 * 1000);
49 const oneWeekAgo = new Date(now - 7 * 24 * 60 * 60 * 1000);
50
51 const Chat = (await import("../../seed/models/chat.js")).default;
52 const { getSessionsForUser } = await import("../../seed/ws/sessionRegistry.js");
53 const { hooks } = await import("../../seed/hooks.js");
54 const { getLoadedExtensionNames } = await import("../../extensions/loader.js");
55
56 // Aggregate stats
57 const [
58 contributionsToday,
59 contributionsWeek,
60 chatsToday,
61 chatsWeek,
62 totalUsers,
63 ] = await Promise.all([
64 Contribution.countDocuments({ date: { $gte: oneDayAgo } }),
65 Contribution.countDocuments({ date: { $gte: oneWeekAgo } }),
66 Chat.countDocuments({ "startMessage.time": { $gte: oneDayAgo } }),
67 Chat.countDocuments({ "startMessage.time": { $gte: oneWeekAgo } }),
68 User.countDocuments({}),
69 ]);
70
71 // Action breakdown today
72 const actionBreakdown = await Contribution.aggregate([
73 { $match: { date: { $gte: oneDayAgo } } },
74 { $group: { _id: "$action", count: { $sum: 1 } } },
75 { $sort: { count: -1 } },
76 { $limit: 10 },
77 ]);
78
79 // AI mode breakdown today
80 const modeBreakdown = await Chat.aggregate([
81 { $match: { "startMessage.time": { $gte: oneDayAgo } } },
82 { $group: { _id: { zone: "$aiContext.zone", mode: "$aiContext.mode" }, count: { $sum: 1 } } },
83 { $sort: { count: -1 } },
84 { $limit: 10 },
85 ]);
86
87 sendOk(res, {
88 period: { today: oneDayAgo, week: oneWeekAgo },
89 today: {
90 contributions: contributionsToday,
91 chats: chatsToday,
92 actions: actionBreakdown.map(a => ({ action: a._id, count: a.count })),
93 modes: modeBreakdown.map(m => ({ mode: m._id, count: m.count })),
94 },
95 week: {
96 contributions: contributionsWeek,
97 chats: chatsWeek,
98 },
99 system: {
100 users: totalUsers,
101 extensions: getLoadedExtensionNames().length,
102 hooks: hooks.list(),
103 },
104 });
105 } catch (err) {
106 log.error("Monitor", "Stats error:", err.message);
107 sendError(res, 500, ERR.INTERNAL, err.message);
108 }
109});
110
111export default router;
112
Loading comments...