EXTENSION for TreeOS
life
Scaffolding library for domain trees. Creates Life roots, group nodes, and domain scaffolds. Pure machinery. Sprout is the user-facing entry point. Operators can use `life add <domain>` as an admin shortcut.
v1.0.1 by TreeOS Site 0 downloads 4 files 584 lines 18.5 KB published 38d ago
treeos ext install life
View changelog

Manifest

Provides

  • routes
  • 1 CLI commands

Requires

  • services: hooks, metadata
  • models: Node

Optional

  • extensions: food, fitness, study, recovery, kb, channels
SHA256: f49f8a6e5becab1e42ed2c01d8b068cc8ed1dd423a31ecd13a32c565261ad567

Dependents

1 package depend on this

PackageTypeRelationship
sprout v1.0.1extensionneeds

CLI Commands

CommandMethodDescription
lifeGETLife tree management. Use: life add <domain>, life domains
life addPOSTAdd a domain to your Life tree. e.g. life add food
life domainsGETList available and scaffolded domains

Source Code

1/**
2 * Life Core
3 *
4 * Scaffold domains. Wire channels. Hand off.
5 * Life doesn't manage extensions after setup. It plants seeds.
6 */
7
8import log from "../../seed/log.js";
9import { getExtension } from "../loader.js";
10
11// Domains that go under Health vs Learning vs Work
12const DOMAIN_GROUPS = {
13  food: "Health",
14  fitness: "Health",
15  recovery: "Health",
16  study: "Learning",
17  kb: "Work",
18  relationships: "Social",
19  finance: "Finance",
20  investor: "Finance",
21  "market-researcher": "Finance",
22};
23
24// Auto-wire channels between related domains
25const CHANNEL_MAP = [
26  ["food", "fitness"],
27  ["fitness", "recovery"],
28  ["food", "recovery"],
29  ["relationships", "recovery"],
30  ["finance", "food"],
31  ["finance", "fitness"],
32  ["investor", "finance"],
33  ["market-researcher", "investor"],
34  ["market-researcher", "finance"],
35];
36
37import { DELETED } from "../../seed/protocol.js";
38
39/**
40 * Create the Life root node for a user. No groups or domains yet.
41 */
42export async function scaffoldRoot(userId) {
43  const { createNode } = await import("../../seed/tree/treeManagement.js");
44  const { setExtMeta } = await import("../../seed/tree/extensionMetadata.js");
45  const root = await createNode({ name: "Life", isRoot: true, userId });
46  const rootId = String(root._id);
47  await setExtMeta(root, "life", { initialized: true });
48  log.info("Life", `Scaffolded Life root for user ${userId}: ${rootId}`);
49  return { rootId };
50}
51
52/**
53 * Find the Life root node for a user. Returns the ID string or null.
54 */
55export async function findLifeRoot(userId) {
56  const Node = (await import("../../seed/models/node.js")).default;
57  // Check by metadata first, then fall back to name match
58  const root = await Node.findOne({
59    rootOwner: userId,
60    parent: { $nin: [DELETED, null] },
61    "metadata.life.initialized": true,
62  }).select("_id").lean()
63    || await Node.findOne({
64      rootOwner: userId,
65      name: "Life",
66      parent: { $nin: [DELETED, null] },
67    }).select("_id").lean();
68  return root ? String(root._id) : null;
69}
70
71/**
72 * Get domain nodes under a Life root. Walks groups and their children.
73 * Returns { fitness: { id, name, ready }, food: { id, name, ready }, ... }
74 */
75export async function getDomainNodes(rootId) {
76  const Node = (await import("../../seed/models/node.js")).default;
77  const result = {};
78  const domains = ["food", "fitness", "study", "recovery", "kb", "relationships", "finance", "investor", "market-researcher"];
79
80  const children = await Node.find({ parent: rootId }).select("_id name metadata").lean();
81  for (const child of children) {
82    // Check if child itself is a domain
83    for (const d of domains) {
84      const meta = child.metadata instanceof Map ? child.metadata.get(d) : child.metadata?.[d];
85      if (meta?.initialized) {
86        result[d] = { id: String(child._id), name: child.name, ready: meta.setupPhase !== "base", treeRootId: String(rootId) };
87      }
88    }
89    // Check grandchildren (domains under group nodes)
90    const grandchildren = await Node.find({ parent: child._id }).select("_id name metadata").lean();
91    for (const gc of grandchildren) {
92      for (const d of domains) {
93        const meta = gc.metadata instanceof Map ? gc.metadata.get(d) : gc.metadata?.[d];
94        if (meta?.initialized) {
95          result[d] = { id: String(gc._id), name: gc.name, ready: meta.setupPhase !== "base", treeRootId: String(rootId) };
96        }
97      }
98    }
99  }
100  return result;
101}
102
103/**
104 * Get available domains (installed extensions that have scaffold capability).
105 */
106export function getAvailableDomains() {
107  const domains = [];
108  for (const name of ["food", "fitness", "study", "recovery", "kb", "relationships", "finance", "investor", "market-researcher"]) {
109    const ext = getExtension(name);
110    if (ext?.exports?.scaffold || ext?.exports?.isInitialized) {
111      domains.push(name);
112    }
113  }
114  return domains;
115}
116
117/**
118 * Scaffold selected domains under a single tree or as separate trees.
119 */
120export async function scaffold({ selections, singleTree, userId, username }) {
121  const { createNode } = await import("../../seed/tree/treeManagement.js");
122  const Node = (await import("../../seed/models/node.js")).default;
123  const results = [];
124
125  if (singleTree) {
126    // Find existing Life root or create one
127    let rootId = await findLifeRoot(userId);
128    let root;
129    if (rootId) {
130      root = await Node.findById(rootId);
131      // Ensure rootOwner is set (fixes manually created Life nodes)
132      if (root && !root.rootOwner) {
133        root.rootOwner = userId;
134        await root.save();
135      }
136      root = root?.toObject ? root.toObject() : root;
137      log.verbose("Life", `Found existing Life root: ${rootId}`);
138    } else {
139      root = await createNode({ name: "Life", isRoot: true, userId });
140      rootId = String(root._id);
141      const { setExtMeta } = await import("../../seed/tree/extensionMetadata.js");
142      await setExtMeta(root, "life", { initialized: true });
143    }
144
145    // Find or create group nodes
146    const groups = new Set(selections.map(s => DOMAIN_GROUPS[s]).filter(Boolean));
147    const groupNodes = {};
148
149    for (const group of groups) {
150      // Check if group already exists
151      const existing = await Node.findOne({ parent: rootId, name: group }).select("_id").lean();
152      if (existing) {
153        groupNodes[group] = String(existing._id);
154      } else {
155        const node = await createNode({ name: group, parentId: rootId, userId });
156        groupNodes[group] = String(node._id);
157      }
158    }
159
160    // Scaffold each domain under its group (skip if already exists)
161    for (const sel of selections) {
162      const group = DOMAIN_GROUPS[sel];
163      const parentId = groupNodes[group] || rootId;
164      const domainName = sel.charAt(0).toUpperCase() + sel.slice(1);
165
166      // Check if domain node already exists
167      const existing = await Node.findOne({ parent: parentId, name: domainName }).select("_id metadata").lean();
168      if (existing) {
169        const meta = existing.metadata instanceof Map ? existing.metadata.get(sel) : existing.metadata?.[sel];
170        if (meta?.initialized) {
171          log.verbose("Life", `Domain ${sel} already scaffolded, skipping`);
172          results.push({ name: sel, id: String(existing._id), status: "exists" });
173          continue;
174        }
175      }
176
177      try {
178        // Create the domain node (or use existing uninitialized one)
179        let domainId;
180        if (existing) {
181          domainId = String(existing._id);
182        } else {
183          const domainNode = await createNode({ name: domainName, parentId, userId });
184          domainId = String(domainNode._id);
185        }
186
187        // Call the extension's scaffold
188        const ext = getExtension(sel);
189        if (ext?.exports?.scaffold) {
190          await ext.exports.scaffold(domainId, userId);
191
192          // Set modes.respond so the routing index finds this node
193          const DOMAIN_MODES = {
194            food: "tree:food-coach", fitness: "tree:fitness-plan",
195            recovery: "tree:recovery-plan", study: "tree:study-coach", kb: "tree:kb-tell",
196            relationships: "tree:relationships-coach",
197            finance: "tree:finance-coach",
198            investor: "tree:investor-coach",
199            "market-researcher": "tree:market-coach",
200          };
201          if (DOMAIN_MODES[sel]) {
202            const { setNodeMode } = await import("../../seed/modes/registry.js");
203            await setNodeMode(domainId, "respond", DOMAIN_MODES[sel], userId);
204          }
205
206          results.push({ name: sel, id: domainId, status: "ok" });
207        } else {
208          results.push({ name: sel, id: domainId, status: "no-scaffold" });
209        }
210      } catch (err) {
211        log.warn("Life", `Failed to scaffold ${sel}: ${err.message}`);
212        results.push({ name: sel, status: "error", error: err.message });
213      }
214    }
215
216    // Wire channels between related domains
217    await wireChannels(selections, rootId, userId);
218
219    // Rebuild routing index so new domain nodes are immediately routable
220    try {
221      const { rebuildIndexForRoot } = await import("../tree-orchestrator/routingIndex.js");
222      await rebuildIndexForRoot(rootId);
223    } catch {}
224
225    return { rootId, type: "single", results };
226
227  } else {
228    // Separate trees
229    for (const sel of selections) {
230      try {
231        const root = await createNode({
232          name: sel.charAt(0).toUpperCase() + sel.slice(1),
233          isRoot: true,
234          userId,
235        });
236        const rootId = String(root._id);
237
238        const ext = getExtension(sel);
239        if (ext?.exports?.scaffold) {
240          await ext.exports.scaffold(rootId, userId);
241          results.push({ name: sel, rootId, status: "ok" });
242        } else {
243          results.push({ name: sel, rootId, status: "no-scaffold" });
244        }
245      } catch (err) {
246        log.warn("Life", `Failed to scaffold ${sel}: ${err.message}`);
247        results.push({ name: sel, status: "error", error: err.message });
248      }
249    }
250
251    // Wire channels between related separate trees
252    await wireChannelsSeparate(selections, results, userId);
253
254    return { type: "separate", results };
255  }
256}
257
258/**
259 * Add a domain to an existing Life tree.
260 */
261export async function addDomain({ rootId, domain, userId }) {
262  const { createNode } = await import("../../seed/tree/treeManagement.js");
263  const Node = (await import("../../seed/models/node.js")).default;
264
265  // Find the right group node
266  const group = DOMAIN_GROUPS[domain];
267  const children = await Node.find({ parent: rootId }).select("_id name").lean();
268  let groupNode = children.find(c => c.name === group);
269
270  if (!groupNode && group) {
271    const node = await createNode({ name: group, parentId: rootId, userId });
272    groupNode = node;
273  }
274
275  const parentId = groupNode ? String(groupNode._id) : rootId;
276  const domainName = domain.charAt(0).toUpperCase() + domain.slice(1);
277
278  // Check if domain already exists
279  const existing = await Node.findOne({ parent: parentId, name: domainName }).select("_id metadata").lean();
280  if (existing) {
281    const meta = existing.metadata instanceof Map ? existing.metadata.get(domain) : existing.metadata?.[domain];
282    if (meta?.initialized) {
283      return { name: domain, id: String(existing._id), status: "exists" };
284    }
285  }
286
287  let domainId;
288  if (existing) {
289    domainId = String(existing._id);
290  } else {
291    const domainNode = await createNode({ name: domainName, parentId, userId });
292    domainId = String(domainNode._id);
293  }
294
295  const ext = getExtension(domain);
296  if (ext?.exports?.scaffold) {
297    await ext.exports.scaffold(domainId, userId);
298
299    const DOMAIN_MODES = {
300      food: "tree:food-coach", fitness: "tree:fitness-plan",
301      recovery: "tree:recovery-plan", study: "tree:study-coach", kb: "tree:kb-tell",
302    };
303    if (DOMAIN_MODES[domain]) {
304      const { setNodeMode } = await import("../../seed/modes/registry.js");
305      await setNodeMode(domainId, "respond", DOMAIN_MODES[domain], userId);
306    }
307  }
308
309  // Wire any new channels
310  const existingDomains = await getInstalledDomains(rootId);
311  existingDomains.push(domain);
312  await wireChannels(existingDomains, rootId, userId);
313
314  // Ensure Life root is in user's nav list
315  try {
316    const nav = getExtension("navigation");
317    if (nav?.exports?.addRoot) await nav.exports.addRoot(userId, rootId);
318  } catch {}
319
320  return { name: domain, id: domainId, status: "ok" };
321}
322
323/**
324 * Get which domains are already scaffolded under a root.
325 */
326async function getInstalledDomains(rootId) {
327  const Node = (await import("../../seed/models/node.js")).default;
328  const installed = [];
329
330  // Walk one level of children (group nodes) and their children (domain nodes)
331  const children = await Node.find({ parent: rootId }).select("_id name metadata").lean();
332  for (const child of children) {
333    // Check if this child itself is a domain
334    for (const domain of ["food", "fitness", "study", "recovery", "kb", "relationships"]) {
335      if (child.name.toLowerCase() === domain) {
336        const meta = child.metadata instanceof Map ? child.metadata.get(domain) : child.metadata?.[domain];
337        if (meta?.initialized) installed.push(domain);
338      }
339    }
340    // Check grandchildren (under group nodes like Health, Learning)
341    const grandchildren = await Node.find({ parent: child._id }).select("name metadata").lean();
342    for (const gc of grandchildren) {
343      for (const domain of ["food", "fitness", "study", "recovery", "kb", "relationships"]) {
344        if (gc.name.toLowerCase() === domain) {
345          const meta = gc.metadata instanceof Map ? gc.metadata.get(domain) : gc.metadata?.[domain];
346          if (meta?.initialized) installed.push(domain);
347        }
348      }
349    }
350  }
351
352  return installed;
353}
354
355/**
356 * Wire channels between related domains in a single tree.
357 */
358async function wireChannels(selections, rootId, userId) {
359  const ch = getExtension("channels");
360  if (!ch?.exports?.createChannel) return;
361
362  const Node = (await import("../../seed/models/node.js")).default;
363
364  // Find domain nodes by walking the tree
365  const domainNodes = {};
366  const children = await Node.find({ parent: rootId }).select("_id name").lean();
367  for (const child of children) {
368    const name = child.name.toLowerCase();
369    if (selections.includes(name)) {
370      domainNodes[name] = String(child._id);
371    }
372    const grandchildren = await Node.find({ parent: child._id }).select("_id name").lean();
373    for (const gc of grandchildren) {
374      const gcName = gc.name.toLowerCase();
375      if (selections.includes(gcName)) {
376        domainNodes[gcName] = String(gc._id);
377      }
378    }
379  }
380
381  for (const [a, b] of CHANNEL_MAP) {
382    if (domainNodes[a] && domainNodes[b]) {
383      try {
384        await ch.exports.createChannel({
385          sourceNodeId: domainNodes[a],
386          targetNodeId: domainNodes[b],
387          channelName: `${a}-${b}`,
388          direction: "bidirectional",
389          filter: { tags: [a, b] },
390          userId,
391        });
392        log.info("Life", `Channel: ${a} <-> ${b}`);
393      } catch (err) {
394        log.verbose("Life", `Channel ${a}-${b} failed or exists: ${err.message}`);
395      }
396    }
397  }
398}
399
400/**
401 * Wire channels between separate tree roots.
402 */
403async function wireChannelsSeparate(selections, results, userId) {
404  const ch = getExtension("channels");
405  if (!ch?.exports?.createChannel) return;
406
407  const rootMap = {};
408  for (const r of results) {
409    if (r.rootId) rootMap[r.name] = r.rootId;
410  }
411
412  for (const [a, b] of CHANNEL_MAP) {
413    if (rootMap[a] && rootMap[b]) {
414      try {
415        await ch.exports.createChannel({
416          sourceNodeId: rootMap[a],
417          targetNodeId: rootMap[b],
418          channelName: `${a}-${b}`,
419          direction: "bidirectional",
420          filter: { tags: [a, b] },
421          userId,
422        });
423        log.info("Life", `Channel: ${a} <-> ${b} (cross-tree)`);
424      } catch (err) {
425        log.verbose("Life", `Channel ${a}-${b} failed or exists: ${err.message}`);
426      }
427    }
428  }
429}
430
1/**
2 * Life
3 *
4 * Scaffolding library for domain trees. Pure machinery.
5 * Sprout is the user-facing entry point. Life just builds what it's told.
6 * Operators can use `life add <domain>` as an admin shortcut.
7 */
8
9import log from "../../seed/log.js";
10import {
11  scaffoldRoot,
12  findLifeRoot,
13  getDomainNodes,
14  addDomain,
15  getAvailableDomains,
16} from "./core.js";
17
18export async function init(core) {
19  const { default: router } = await import("./routes.js");
20
21  log.info("Life", "Loaded. Scaffolding library ready.");
22
23  return {
24    router,
25    exports: {
26      scaffoldRoot,
27      findLifeRoot,
28      getDomainNodes,
29      addDomain,
30      getAvailableDomains,
31    },
32  };
33}
34
1export default {
2  name: "life",
3  version: "1.0.1",
4  builtFor: "TreeOS",
5  description:
6    "Scaffolding library for domain trees. Creates Life roots, group nodes, " +
7    "and domain scaffolds. Pure machinery. Sprout is the user-facing entry point. " +
8    "Operators can use `life add <domain>` as an admin shortcut.",
9
10  needs: {
11    models: ["Node"],
12    services: ["hooks", "metadata"],
13  },
14
15  optional: {
16    extensions: ["food", "fitness", "study", "recovery", "kb", "channels"],
17  },
18
19  provides: {
20    models: {},
21    routes: "./routes.js",
22    tools: false,
23    jobs: false,
24
25    cli: [
26      {
27        command: "life",
28        scope: ["home"],
29        description: "Life tree management. Use: life add <domain>, life domains",
30        method: "GET",
31        endpoint: "/life/domains",
32        subcommands: {
33          add: {
34            method: "POST",
35            endpoint: "/life/add",
36            description: "Add a domain to your Life tree. e.g. life add food",
37            args: ["domain"],
38          },
39          domains: {
40            method: "GET",
41            endpoint: "/life/domains",
42            description: "List available and scaffolded domains",
43          },
44        },
45      },
46    ],
47  },
48};
49
1import express from "express";
2import authenticate from "../../seed/middleware/authenticate.js";
3import { sendOk, sendError, ERR } from "../../seed/protocol.js";
4import log from "../../seed/log.js";
5import { getAvailableDomains, addDomain, findLifeRoot, getDomainNodes } from "./core.js";
6
7const router = express.Router();
8
9/**
10 * POST /life/add
11 * Operator shortcut: add a single domain to the user's Life tree.
12 * Body: { domain: "food" }
13 */
14router.post("/life/add", authenticate, async (req, res) => {
15  try {
16    const userId = req.userId;
17    const { domain } = req.body;
18
19    if (!domain) {
20      return sendError(res, 400, ERR.INVALID_INPUT, "domain required");
21    }
22
23    const available = new Set(getAvailableDomains());
24    const normalized = domain.toLowerCase();
25    if (!available.has(normalized)) {
26      return sendError(res, 400, ERR.INVALID_INPUT, `Unknown domain "${domain}". Available: ${[...available].join(", ")}`);
27    }
28
29    // Find Life root or create one
30    let rootId = await findLifeRoot(userId);
31    if (!rootId) {
32      const { scaffoldRoot } = await import("./core.js");
33      const result = await scaffoldRoot(userId);
34      rootId = result.rootId;
35    }
36
37    const result = await addDomain({ rootId, domain: normalized, userId });
38
39    // Rebuild routing index
40    try {
41      const { rebuildIndexForRoot } = await import("../tree-orchestrator/routingIndex.js");
42      await rebuildIndexForRoot(rootId);
43    } catch {}
44
45    sendOk(res, result);
46  } catch (err) {
47    log.error("Life", "Add domain error:", err.message);
48    sendError(res, 500, ERR.INTERNAL, err.message);
49  }
50});
51
52/**
53 * GET /life/domains
54 * List available domains (installed extensions with scaffold support).
55 */
56router.get("/life/domains", authenticate, async (req, res) => {
57  const available = getAvailableDomains();
58  const rootId = await findLifeRoot(req.userId);
59  let scaffolded = {};
60  if (rootId) {
61    scaffolded = await getDomainNodes(rootId);
62  }
63  sendOk(res, {
64    available,
65    scaffolded: Object.keys(scaffolded),
66    rootId: rootId || null,
67  });
68});
69
70export default router;
71

Versions

Version Published Downloads
1.0.1 38d ago 0
1.0.0 48d ago 0
0 stars
0 flags
React from the CLI: treeos ext star life

Comments

Loading comments...

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