24bfdcf92adb74e5305a3dee491bb15ac292be5d8065227cf8ed1c19a7beccb7| Command | Method | Description |
|---|---|---|
rel [message...] | POST | Relationships. Talk about people. |
enrichContext1/**
2 * Relationships Core
3 *
4 * People in your life. Track who matters. The tree notices.
5 */
6
7import log from "../../seed/log.js";
8import { setNodeMode } from "../../seed/modes/registry.js";
9
10let _Node = null;
11let _Note = null;
12let _metadata = null;
13
14export function configure({ Node, Note, metadata }) {
15 _Node = Node;
16 _Note = Note;
17 _metadata = metadata;
18}
19
20const ROLES = {
21 LOG: "log",
22 PEOPLE: "people",
23 IDEAS: "ideas",
24 PROFILE: "profile",
25 HISTORY: "history",
26};
27
28export { ROLES };
29
30// ── Scaffold ──
31
32export async function scaffold(rootId, userId) {
33 if (!_Node) throw new Error("Relationships core not configured");
34 const { createNode } = await import("../../seed/tree/treeManagement.js");
35
36 const logNode = await createNode({ name: "Log", parentId: rootId, userId });
37 const peopleNode = await createNode({ name: "People", parentId: rootId, userId });
38 const ideasNode = await createNode({ name: "Ideas", parentId: rootId, userId });
39 const profileNode = await createNode({ name: "Profile", parentId: rootId, userId });
40 const historyNode = await createNode({ name: "History", parentId: rootId, userId });
41
42 const tags = [
43 [logNode, ROLES.LOG],
44 [peopleNode, ROLES.PEOPLE],
45 [ideasNode, ROLES.IDEAS],
46 [profileNode, ROLES.PROFILE],
47 [historyNode, ROLES.HISTORY],
48 ];
49
50 for (const [node, role] of tags) {
51 await _metadata.setExtMeta(node, "relationships", { role });
52 }
53
54 await setNodeMode(rootId, "respond", "tree:relationships-coach");
55 await setNodeMode(logNode._id, "respond", "tree:relationships-log");
56
57 const root = await _Node.findById(rootId);
58 if (root) {
59 await _metadata.setExtMeta(root, "relationships", {
60 initialized: true,
61 setupPhase: "complete",
62 });
63 }
64
65 const ids = {};
66 for (const [node, role] of tags) ids[role] = String(node._id);
67
68 log.info("Relationships", `Scaffolded under ${rootId}`);
69 return ids;
70}
71
72// ── Find nodes ──
73
74export async function findRelNodes(rootId) {
75 if (!_Node) return null;
76 const children = await _Node.find({ parent: rootId }).select("_id name metadata").lean();
77 const result = {};
78 for (const child of children) {
79 const meta = child.metadata instanceof Map
80 ? child.metadata.get("relationships")
81 : child.metadata?.relationships;
82 if (meta?.role) result[meta.role] = { id: String(child._id), name: child.name };
83 }
84 return result;
85}
86
87export async function isInitialized(rootId) {
88 if (!_Node) return false;
89 const root = await _Node.findById(rootId).select("metadata").lean();
90 if (!root) return false;
91 const meta = root.metadata instanceof Map
92 ? root.metadata.get("relationships")
93 : root.metadata?.relationships;
94 return !!meta?.initialized;
95}
96
97export async function getSetupPhase(rootId) {
98 if (!_Node) return null;
99 const root = await _Node.findById(rootId).select("metadata").lean();
100 if (!root) return null;
101 const meta = root.metadata instanceof Map
102 ? root.metadata.get("relationships")
103 : root.metadata?.relationships;
104 return meta?.setupPhase || (meta?.initialized ? "complete" : null);
105}
106
107export async function completeSetup(rootId) {
108 const root = await _Node.findById(rootId);
109 if (!root) return;
110 const existing = _metadata.getExtMeta(root, "relationships") || {};
111 await _metadata.setExtMeta(root, "relationships", { ...existing, setupPhase: "complete" });
112}
113
114// ── People ──
115
116/**
117 * Find or create a person node under People.
118 * Returns { id, name, isNew }.
119 */
120export async function findOrCreatePerson(rootId, personName, userId) {
121 const nodes = await findRelNodes(rootId);
122 if (!nodes?.people) return null;
123
124 const peopleId = nodes.people.id;
125 const children = await _Node.find({ parent: peopleId }).select("_id name").lean();
126
127 // Case-insensitive match
128 const lower = personName.toLowerCase().trim();
129 const existing = children.find(c => c.name.toLowerCase() === lower);
130 if (existing) return { id: String(existing._id), name: existing.name, isNew: false };
131
132 // Create new person node
133 const { createNode } = await import("../../seed/tree/treeManagement.js");
134 const node = await createNode({ name: personName.trim(), parentId: peopleId, userId });
135 await _metadata.setExtMeta(node, "relationships", { role: "person" });
136 log.verbose("Relationships", `Created person: ${personName}`);
137 return { id: String(node._id), name: node.name, isNew: true };
138}
139
140/**
141 * Get all people under this root.
142 */
143export async function getPeople(rootId) {
144 const nodes = await findRelNodes(rootId);
145 if (!nodes?.people) return [];
146
147 const children = await _Node.find({ parent: nodes.people.id })
148 .select("_id name metadata").lean();
149
150 return children.map(c => {
151 const meta = c.metadata instanceof Map
152 ? c.metadata.get("relationships")
153 : c.metadata?.relationships;
154 return {
155 id: String(c._id),
156 name: c.name,
157 role: meta?.role || "person",
158 relation: meta?.relation || null,
159 lastContact: meta?.lastContact || null,
160 noteCount: meta?.noteCount || 0,
161 };
162 });
163}
164
165/**
166 * Get recent interactions from the log.
167 */
168export async function getRecentInteractions(rootId, limit = 10) {
169 const nodes = await findRelNodes(rootId);
170 if (!nodes?.log) return [];
171
172 const { getNotes } = await import("../../seed/tree/notes.js");
173 const result = await getNotes({ nodeId: nodes.log.id, limit });
174 return result?.notes || [];
175}
176
177/**
178 * Get ideas (things to do for people).
179 */
180export async function getIdeas(rootId) {
181 const nodes = await findRelNodes(rootId);
182 if (!nodes?.ideas) return [];
183
184 const { getNotes } = await import("../../seed/tree/notes.js");
185 const result = await getNotes({ nodeId: nodes.ideas.id, limit: 20 });
186 return result?.notes || [];
187}
1881/**
2 * No data work needed. The AI handles everything via modes.
3 * Returns null so the orchestrator routes to the default mode.
4 */
5
6export async function handleMessage() {
7 return null;
8}
91import express from "express";
2import { sendError, ERR } from "../../seed/protocol.js";
3import urlAuth from "../html-rendering/urlAuth.js";
4import { htmlOnly } from "../html-rendering/htmlHelpers.js";
5import Node from "../../seed/models/node.js";
6import { isInitialized, getPeople, getRecentInteractions, getIdeas } from "./core.js";
7import { renderRelationshipsDashboard } from "./pages/dashboard.js";
8
9const router = express.Router();
10
11router.get("/root/:rootId/relationships", urlAuth, htmlOnly, async (req, res) => {
12 try {
13 const { rootId } = req.params;
14 const root = await Node.findById(rootId).select("name metadata").lean();
15 if (!root) return sendError(res, 404, ERR.TREE_NOT_FOUND, "Relationships tree not found");
16
17 if (!(await isInitialized(rootId))) {
18 return sendError(res, 404, ERR.TREE_NOT_FOUND, "Relationships not initialized");
19 }
20
21 const people = await getPeople(rootId);
22 const recentInteractions = await getRecentInteractions(rootId, 15);
23 const ideas = await getIdeas(rootId);
24
25 res.send(renderRelationshipsDashboard({
26 rootId,
27 rootName: root.name,
28 people,
29 recentInteractions,
30 ideas,
31 token: req.query.token || null,
32 userId: req.user?._id?.toString() || req.user?.id || null,
33 inApp: !!req.query.inApp,
34 }));
35 } catch (err) {
36 sendError(res, 500, ERR.INTERNAL, "Relationships dashboard failed");
37 }
38});
39
40export default router;
411import log from "../../seed/log.js";
2import { configure, findRelNodes, getPeople, isInitialized, scaffold } from "./core.js";
3import { handleMessage } from "./handler.js";
4
5export async function init(core) {
6 configure({
7 Node: core.models.Node,
8 Note: core.models.Note,
9 metadata: core.metadata,
10 });
11
12 // ── Modes ──
13 const logMode = (await import("./modes/log.js")).default;
14 const coachMode = (await import("./modes/coach.js")).default;
15 const reviewMode = (await import("./modes/review.js")).default;
16
17 core.modes.registerMode(logMode.name, logMode, "relationships");
18 core.modes.registerMode(coachMode.name, coachMode, "relationships");
19 core.modes.registerMode(reviewMode.name, reviewMode, "relationships");
20
21 // ── enrichContext: inject people awareness into ALL tree nodes ──
22 // Fires everywhere in the tree, not just relationships nodes.
23 // The AI at any position knows about tracked people.
24 const _peopleCache = new Map(); // treeRootId -> { people, ts }
25 const PEOPLE_CACHE_TTL = 60000; // 60s
26
27 core.hooks.register("enrichContext", async ({ context, node, meta }) => {
28 if (!node?._id) return;
29
30 // At a relationships person node: show full interaction history
31 const relMeta = meta?.relationships;
32 if (relMeta?.role === "person") {
33 const { getNotes } = await import("../../seed/tree/notes.js");
34 const result = await getNotes({ nodeId: String(node._id), limit: 10 });
35 if (result?.notes?.length > 0) {
36 context.personHistory = result.notes.map(n => n.content);
37 }
38 return;
39 }
40
41 // For all other nodes: inject compact people summary from the tree
42 // Find the tree root (rootOwner field on any node)
43 const treeRootId = node.rootOwner ? String(node.rootOwner) : null;
44 if (!treeRootId) return;
45
46 // Check cache
47 const cached = _peopleCache.get(treeRootId);
48 if (cached && Date.now() - cached.ts < PEOPLE_CACHE_TTL) {
49 if (cached.people.length > 0) context.knownPeople = cached.people;
50 return;
51 }
52
53 // Find relationships domain via life extension
54 try {
55 const { getExtension } = await import("../loader.js");
56 const life = getExtension("life");
57 if (!life?.exports?.getDomainNodes) return;
58
59 const domains = await life.exports.getDomainNodes(treeRootId);
60 if (!domains.relationships?.id) {
61 _peopleCache.set(treeRootId, { people: [], ts: Date.now() });
62 return;
63 }
64
65 const people = await getPeople(domains.relationships.id);
66 const compact = people.map(p => {
67 const parts = [p.name];
68 if (p.relation) parts.push(`(${p.relation})`);
69 return parts.join(" ");
70 });
71
72 _peopleCache.set(treeRootId, { people: compact, ts: Date.now() });
73 if (compact.length > 0) context.knownPeople = compact;
74 } catch {}
75 }, "relationships");
76
77 log.info("Relationships", "Loaded. The tree sees people.");
78
79 // ── Register apps-grid slot ──
80 try {
81 const { getExtension } = await import("../loader.js");
82 const base = getExtension("treeos-base");
83 base?.exports?.registerSlot?.("apps-grid", "relationships", ({ userId, rootMap, tokenParam, tokenField, esc: e }) => {
84 const entries = rootMap.get("Relationships") || [];
85 const existing = entries.map(entry =>
86 `<a class="app-active" href="/api/v1/root/${entry.id}/relationships?html${tokenParam}" style="margin-right:8px;margin-bottom:6px;">${e(entry.name)}</a>`
87 ).join("");
88 return `<div class="app-card">
89 <div class="app-header"><span class="app-emoji">👥</span><span class="app-name">Relationships</span></div>
90 <div class="app-desc">People in your life. Track who matters, interactions, ideas for others. The tree notices when you mention someone.</div>
91 ${entries.length > 0
92 ? `<div style="display:flex;flex-wrap:wrap;">${existing}</div>`
93 : `<form class="app-form" method="POST" action="/api/v1/user/${userId}/apps/create">
94 ${tokenField}<input type="hidden" name="app" value="relationships" />
95 <input class="app-input" name="message" placeholder="Tell me about someone in your life" required />
96 <button class="app-start" type="submit">Start Relationships</button>
97 </form>`}
98 </div>`;
99 }, { priority: 55 });
100 } catch {}
101
102 // ── HTML dashboard route ──
103 let router = null;
104 try {
105 const { getExtension } = await import("../loader.js");
106 const htmlExt = getExtension("html-rendering");
107 if (htmlExt) {
108 router = (await import("./htmlRoutes.js")).default;
109 }
110 } catch {}
111
112 return {
113 router,
114 exports: {
115 scaffold,
116 isInitialized,
117 findRelNodes,
118 getPeople,
119 handleMessage,
120 },
121 };
122}
1231export default {
2 name: "relationships",
3 version: "1.0.1",
4 builtFor: "TreeOS",
5 description:
6 "People in your life. Track who matters, what you did for them, what you " +
7 "can do. Anytime you mention someone by name, the tree notices. Each person " +
8 "gets a node. Interactions log what happened. Ideas capture things you want " +
9 "to do for people. The AI builds awareness of your social world over time. " +
10 "Type 'be' for a guided check-in: who haven't you reached out to lately?",
11
12 territory: "people, friends, family, relationships, social interactions, someone, they, them",
13 classifierHints: [
14 /\b(my (mom|dad|brother|sister|wife|husband|partner|friend|boss|coworker|son|daughter|uncle|aunt|grandma|grandpa))\b/i,
15 /\b(talked to|hung out|met with|called|texted|visited|saw|ran into)\b/i,
16 /\b(birthday|anniversary|gift|favor|help them|check on|reach out|catch up)\b/i,
17 /\b(relationships?|people|person|friends?|family|colleague)\b/i,
18 ],
19
20 needs: {
21 models: ["Node", "Note"],
22 services: ["hooks", "llm", "metadata"],
23 },
24
25 optional: {
26 extensions: [
27 "channels",
28 "html-rendering",
29 "treeos-base",
30 ],
31 },
32
33 provides: {
34 models: {},
35 routes: false,
36 tools: false,
37 jobs: false,
38 modes: true,
39
40 hooks: {
41 fires: [],
42 listens: ["enrichContext"],
43 },
44
45 cli: [
46 {
47 command: "rel [message...]",
48 scope: ["tree"],
49 description: "Relationships. Talk about people.",
50 method: "POST",
51 endpoint: "/root/:rootId/chat",
52 body: ["message"],
53 },
54 ],
55 },
56};
571import { findExtensionRoot } from "../../../seed/tree/extensionMetadata.js";
2import { findRelNodes, getPeople, getIdeas } from "../core.js";
3
4export default {
5 name: "tree:relationships-coach",
6 emoji: "💬",
7 label: "Relationships Coach",
8 bigMode: "tree",
9 hidden: true,
10
11 maxMessagesBeforeLoop: 6,
12 preserveContextOnLoop: true,
13
14 toolNames: [
15 "navigate-tree",
16 "get-tree-context",
17 "get-tree",
18 "get-node-notes",
19 "create-new-node",
20 "create-node-note",
21 "edit-node-note",
22 "get-searched-notes-by-user",
23 ],
24
25 async buildSystemPrompt({ username, rootId, currentNodeId }) {
26 const relRoot = await findExtensionRoot(currentNodeId || rootId, "relationships") || rootId;
27 const nodes = relRoot ? await findRelNodes(relRoot) : null;
28 const people = relRoot ? await getPeople(relRoot) : [];
29 const ideas = relRoot ? await getIdeas(relRoot) : [];
30
31 const peopleList = people.length > 0
32 ? people.map(p => {
33 const parts = [p.name];
34 if (p.relation) parts.push(`(${p.relation})`);
35 if (p.lastContact) {
36 const days = Math.floor((Date.now() - new Date(p.lastContact).getTime()) / 86400000);
37 parts.push(`${days}d ago`);
38 }
39 return `- ${parts.join(" ")}`;
40 }).join("\n")
41 : "No people tracked yet.";
42
43 const ideasList = ideas.length > 0
44 ? ideas.slice(0, 5).map(i => `- ${i.content}`).join("\n")
45 : "";
46
47 const peopleId = nodes?.people?.id;
48 const ideasId = nodes?.ideas?.id;
49
50 return `You are ${username}'s relationship coach.
51
52${people.length > 0 ? `STATUS: ${people.length} people tracked.` : "STATUS: No people tracked yet. Ask who matters to them."}
53
54PEOPLE:
55${peopleList}
56${ideasList ? `\nPENDING IDEAS:\n${ideasList}` : ""}
57
58Your role: help ${username} think about their relationships. Who to reach out to. What to do for people. How to be a better friend, family member, colleague.
59
60CAPABILITIES:
61${peopleId ? `- Create new people under People (${peopleId})` : "- Track new people"}
62${ideasId ? `- Log ideas under Ideas (${ideasId})` : "- Track ideas for people"}
63- Write notes on any person's node
64
65BEHAVIOR:
66- Be warm but not pushy. Relationships are personal.
67- When they mention someone new, offer to add them.
68- When they ask "who should I reach out to?", check lastContact dates and suggest people they haven't talked to.
69- When they have an idea for someone (gift, activity, help), log it under Ideas.
70- Keep it conversational. This isn't a CRM. It's awareness.
71- Never expose node IDs or metadata to the user.`.trim();
72 },
73};
741import { findExtensionRoot } from "../../../seed/tree/extensionMetadata.js";
2import { findRelNodes, getPeople } from "../core.js";
3
4export default {
5 name: "tree:relationships-log",
6 emoji: "👥",
7 label: "Relationships Log",
8 bigMode: "tree",
9 hidden: true,
10
11 maxMessagesBeforeLoop: 6,
12 preserveContextOnLoop: true,
13
14 toolNames: [
15 "navigate-tree",
16 "get-tree-context",
17 "get-tree",
18 "get-node-notes",
19 "create-new-node",
20 "create-node-note",
21 "edit-node-note",
22 "edit-node-name",
23 "get-searched-notes-by-user",
24 ],
25
26 async buildSystemPrompt({ username, rootId, currentNodeId }) {
27 const relRoot = await findExtensionRoot(currentNodeId || rootId, "relationships") || rootId;
28 const nodes = relRoot ? await findRelNodes(relRoot) : null;
29 const people = relRoot ? await getPeople(relRoot) : [];
30
31 const peopleList = people.length > 0
32 ? people.map(p => {
33 const parts = [p.name];
34 if (p.relation) parts.push(`(${p.relation})`);
35 if (p.lastContact) parts.push(`last: ${new Date(p.lastContact).toLocaleDateString()}`);
36 return `- ${parts.join(" ")}`;
37 }).join("\n")
38 : "No people tracked yet.";
39
40 const peopleId = nodes?.people?.id;
41 const logId = nodes?.log?.id;
42
43 return `You are tracking relationships for ${username}. People they know, interactions, patterns.
44
45PEOPLE:
46${peopleList}
47
48The user is telling you about an interaction with someone or about a person in their life.
49
50WORKFLOW:
511. Identify who they're talking about.
522. ${peopleId ? `Check if this person exists under People (${peopleId}). If not, create a node for them there.` : "Find or create a node for this person."}
533. Write ONE short note on the person's node. Just the facts. No dates (the system timestamps it). No filler.
544. If they mention a relationship type (friend, coworker, family, pet), set metadata.relationships.relation on the person's node.
55
56RULES:
57- One note per interaction. Never write duplicates.
58- Notes are terse: "Coffee at Blue Bottle. Talked about job change." Not "On April 5, 2026, Tabor and Jake met..."
59- Do NOT write to the Log node. The person's node IS the log for that person.
60- Use their exact name. Don't rename people.
61- If the user mentions multiple people, create/update each.
62- Confirm in one sentence. "Noted. Coffee with Jake."
63- Never expose node IDs or metadata to the user.`.trim();
64 },
65};
661import { findExtensionRoot } from "../../../seed/tree/extensionMetadata.js";
2import { findRelNodes, getPeople, getRecentInteractions } from "../core.js";
3
4export default {
5 name: "tree:relationships-review",
6 emoji: "🔍",
7 label: "Relationships Review",
8 bigMode: "tree",
9 hidden: true,
10
11 maxMessagesBeforeLoop: 6,
12 preserveContextOnLoop: true,
13
14 toolNames: [
15 "navigate-tree",
16 "get-tree-context",
17 "get-tree",
18 "get-node-notes",
19 "get-searched-notes-by-user",
20 ],
21
22 async buildSystemPrompt({ username, rootId, currentNodeId }) {
23 const relRoot = await findExtensionRoot(currentNodeId || rootId, "relationships") || rootId;
24 const people = relRoot ? await getPeople(relRoot) : [];
25 const recent = relRoot ? await getRecentInteractions(relRoot, 15) : [];
26
27 // Sort by lastContact, oldest first (people to reach out to)
28 const sorted = [...people].sort((a, b) => {
29 const aTime = a.lastContact ? new Date(a.lastContact).getTime() : 0;
30 const bTime = b.lastContact ? new Date(b.lastContact).getTime() : 0;
31 return aTime - bTime;
32 });
33
34 const overdueList = sorted
35 .filter(p => {
36 if (!p.lastContact) return true;
37 const days = Math.floor((Date.now() - new Date(p.lastContact).getTime()) / 86400000);
38 return days > 14;
39 })
40 .map(p => {
41 const days = p.lastContact
42 ? Math.floor((Date.now() - new Date(p.lastContact).getTime()) / 86400000)
43 : "never";
44 return `- ${p.name}${p.relation ? ` (${p.relation})` : ""}: ${days === "never" ? "never contacted" : `${days} days ago`}`;
45 }).join("\n");
46
47 const recentList = recent.slice(0, 10).map(n => `- ${n.content}`).join("\n");
48
49 return `You are reviewing ${username}'s relationships. Show patterns, suggest who to reach out to, highlight what's going well.
50
51${overdueList ? `PEOPLE TO REACH OUT TO (14+ days):\n${overdueList}\n` : "Everyone is recently contacted."}
52${recentList ? `RECENT INTERACTIONS:\n${recentList}\n` : "No recent interactions logged."}
53
54TOTAL PEOPLE TRACKED: ${people.length}
55
56BEHAVIOR:
57- Highlight who they haven't talked to in a while.
58- Note positive patterns (regular meetups, consistent check-ins).
59- Note gaps (family members not contacted, old friends fading).
60- Be honest but gentle. Don't guilt-trip.
61- If they ask about a specific person, read that person's notes and summarize.
62- Never expose node IDs or metadata to the user.`.trim();
63 },
64};
651/**
2 * Relationships Dashboard
3 *
4 * Shows people tracked, recent interactions, ideas for others.
5 * Highlights who you haven't contacted in a while.
6 */
7
8import { renderAppDashboard } from "../../html-rendering/html/appDashboard.js";
9
10export function renderRelationshipsDashboard({ rootId, rootName, people, recentInteractions, ideas, token, userId, inApp }) {
11 const allPeople = people || [];
12 const recent = recentInteractions || [];
13 const allIdeas = ideas || [];
14
15 // Sort people by lastContact (oldest first for overdue detection)
16 const sorted = [...allPeople].sort((a, b) => {
17 const aTime = a.lastContact ? new Date(a.lastContact).getTime() : 0;
18 const bTime = b.lastContact ? new Date(b.lastContact).getTime() : 0;
19 return aTime - bTime;
20 });
21
22 const overdue = sorted.filter(p => {
23 if (!p.lastContact) return true;
24 const days = Math.floor((Date.now() - new Date(p.lastContact).getTime()) / 86400000);
25 return days > 14;
26 });
27
28 // Hero: total people
29 const hero = {
30 value: String(allPeople.length),
31 label: allPeople.length === 1 ? "person tracked" : "people tracked",
32 color: "#a78bfa",
33 sub: overdue.length > 0 ? `${overdue.length} overdue for contact` : null,
34 };
35
36 // Stats
37 const stats = [];
38 if (recent.length > 0) stats.push({ value: String(recent.length), label: "recent interactions" });
39 if (allIdeas.length > 0) stats.push({ value: String(allIdeas.length), label: "ideas pending" });
40 const recentCount = allPeople.filter(p => {
41 if (!p.lastContact) return false;
42 const days = Math.floor((Date.now() - new Date(p.lastContact).getTime()) / 86400000);
43 return days <= 7;
44 }).length;
45 if (recentCount > 0) stats.push({ value: String(recentCount), label: "contacted this week" });
46
47 // Cards
48 const cards = [];
49
50 // People card
51 if (allPeople.length > 0) {
52 cards.push({
53 title: "People",
54 items: allPeople.map(p => {
55 const parts = [];
56 if (p.relation) parts.push(p.relation);
57 if (p.lastContact) {
58 const days = Math.floor((Date.now() - new Date(p.lastContact).getTime()) / 86400000);
59 parts.push(days === 0 ? "today" : days === 1 ? "yesterday" : `${days}d ago`);
60 } else {
61 parts.push("never contacted");
62 }
63 return {
64 text: p.name,
65 sub: parts.join(" . "),
66 };
67 }),
68 });
69 }
70
71 // Overdue card
72 if (overdue.length > 0) {
73 cards.push({
74 title: "Reach Out To",
75 items: overdue.slice(0, 10).map(p => {
76 const days = p.lastContact
77 ? Math.floor((Date.now() - new Date(p.lastContact).getTime()) / 86400000)
78 : null;
79 return {
80 text: p.name,
81 sub: p.relation ? `${p.relation} . ${days != null ? `${days} days` : "never"}` : (days != null ? `${days} days` : "never contacted"),
82 };
83 }),
84 });
85 }
86
87 // Recent interactions card
88 if (recent.length > 0) {
89 cards.push({
90 title: "Recent Interactions",
91 items: recent.slice(0, 15).map(n => ({
92 text: n.content || "Interaction",
93 sub: n.createdAt ? new Date(n.createdAt).toLocaleDateString() : null,
94 })),
95 });
96 }
97
98 // Ideas card
99 if (allIdeas.length > 0) {
100 cards.push({
101 title: "Ideas for People",
102 items: allIdeas.slice(0, 10).map(n => ({
103 text: n.content || "Idea",
104 sub: n.createdAt ? new Date(n.createdAt).toLocaleDateString() : null,
105 })),
106 });
107 }
108
109 return renderAppDashboard({
110 rootId,
111 rootName: rootName || "Relationships",
112 token,
113 userId,
114 inApp: !!inApp,
115 subtitle: new Date().toLocaleDateString("en-US", { weekday: "long", month: "long", day: "numeric" }),
116 hero,
117 stats,
118 bars: [],
119 cards,
120 chatBar: {
121 placeholder: "Tell me about someone or ask who to reach out to...",
122 endpoint: `/api/v1/root/${rootId}/chat`,
123 },
124 emptyState: allPeople.length === 0
125 ? { title: "No people tracked yet", message: "Tell me about someone in your life. The tree remembers." }
126 : null,
127 });
128}
129
treeos ext star relationships
Post comments from the CLI: treeos ext comment relationships "your comment"
Max 3 comments per extension. One star and one flag per user.
Loading comments...