EXTENSION for TreeOS
relationships
People in your life. Track who matters, what you did for them, what you can do. Anytime you mention someone by name, the tree notices. Each person gets a node. Interactions log what happened. Ideas capture things you want to do for people. The AI builds awareness of your social world over time. Type 'be' for a guided check-in: who haven't you reached out to lately?
v1.0.1 by TreeOS Site 0 downloads 9 files 752 lines 24.6 KB published 38d ago
treeos ext install relationships
View changelog

Manifest

Provides

  • 1 CLI commands

Requires

  • services: hooks, llm, metadata
  • models: Node, Note

Optional

  • extensions: channels, html-rendering, treeos-base
SHA256: 24bfdcf92adb74e5305a3dee491bb15ac292be5d8065227cf8ed1c19a7beccb7

CLI Commands

CommandMethodDescription
rel [message...]POSTRelationships. Talk about people.

Hooks

Listens To

  • enrichContext

Source Code

1/**
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}
188
1/**
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}
9
1import 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;
41
1import 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}
123
1export 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};
57
1import { 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};
74
1import { 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};
66
1import { 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};
65
1/**
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
0 stars
0 flags
React from the CLI: treeos ext star relationships

Comments

Loading comments...

Post comments from the CLI: treeos ext comment relationships "your comment"
Max 3 comments per extension. One star and one flag per user.