EXTENSION for TreeOS
go
Navigate by intent. Say where, not how. 'go workout' finds fitness across all your trees. 'go food' finds the food node. 'go' with no arguments shows all navigable positions ranked by recency. Reads the routing index from tree-orchestrator. Matches extension names, tree names, node names. Navigates to the best match. No LLM call. Microseconds.
v1.0.3 by TreeOS Site 0 downloads 5 files 362 lines 9.9 KB published 38d ago
treeos ext install go
View changelog

Manifest

Provides

  • routes
  • tools
  • 1 CLI commands

Requires

  • services: hooks
  • models: Node

Optional

  • extensions: tree-orchestrator, navigation
SHA256: 6e6a8fd07c544a4484bf586f7a9151e850acd568504c8e13a3f2f4f0693c6dfa

CLI Commands

CommandMethodDescription
go [destination...]GETNavigate to a position by name or intent. No args = show all positions.

Source Code

1/**
2 * Go Core
3 *
4 * Cross-tree navigation by intent. Searches all user's trees
5 * via the routing index and node names.
6 */
7
8import Node from "../../seed/models/node.js";
9import log from "../../seed/log.js";
10
11let _getExtension = null;
12let _getUserRoots = null;
13
14export function configure({ getExtension }) {
15  _getExtension = getExtension;
16}
17
18function getRoutingExports() {
19  if (!_getExtension) return null;
20  return _getExtension("tree-orchestrator")?.exports || null;
21}
22
23function getNavExports() {
24  if (!_getExtension) return null;
25  return _getExtension("navigation")?.exports || null;
26}
27
28// ─────────────────────────────────────────────────────────────────────────
29// LIST ALL POSITIONS
30// ─────────────────────────────────────────────────────────────────────────
31
32export async function listPositions(userId) {
33  // Tree roots = direct children of land root, excluding system nodes
34  const landRoot = await Node.findOne({ systemRole: "land-root" }).select("_id").lean();
35  if (!landRoot) return { trees: [], extensions: [] };
36
37  const allRoots = await Node.find({
38    parent: landRoot._id,
39    systemRole: null,
40  }).select("_id name dateCreated rootOwner contributors").lean();
41
42  // Filter to roots this user owns or contributes to
43  const userRoots = allRoots.filter(r =>
44    String(r.rootOwner) === String(userId) ||
45    (r.contributors || []).some(c => String(c) === String(userId))
46  );
47
48  // Deduplicate by name (keep newest)
49  const seen = new Map();
50  for (const root of userRoots) {
51    const existing = seen.get(root.name);
52    if (!existing || root.dateCreated > existing.dateCreated) {
53      seen.set(root.name, root);
54    }
55  }
56  const trees = [...seen.values()].map(r => ({
57    nodeId: String(r._id),
58    name: r.name,
59  }));
60
61  // Extension positions from routing index
62  const extensions = [];
63  const routing = getRoutingExports();
64  if (routing?.getIndexForRoot) {
65    for (const root of trees) {
66      const index = routing.getIndexForRoot(root.nodeId);
67      if (!index) continue;
68      for (const [extName, entry] of index) {
69        extensions.push({
70          nodeId: entry.nodeId,
71          name: extName,
72          path: entry.path,
73        });
74      }
75    }
76  }
77
78  return { trees, extensions };
79}
80
81// ─────────────────────────────────────────────────────────────────────────
82// FIND DESTINATION
83// ─────────────────────────────────────────────────────────────────────────
84
85export async function findDestination(query, userId) {
86  const target = query.toLowerCase().trim();
87  if (!target) return listPositions(userId);
88
89  const matches = [];
90
91  // Get user's roots
92  const nav = getNavExports();
93  const roots = nav?.getUserRootsWithNames
94    ? await nav.getUserRootsWithNames(userId)
95    : await Node.find({ rootOwner: userId }).select("_id name").lean();
96
97  const routing = getRoutingExports();
98
99  // Search routing index across all trees
100  for (const root of roots) {
101    const rootId = String(root._id);
102
103    if (routing?.getIndexForRoot) {
104      const index = routing.getIndexForRoot(rootId);
105      if (index) {
106        for (const [extName, entry] of index) {
107          const score = matchScore(target, extName, entry.name, entry.path);
108          if (score > 0) {
109            matches.push({ ...entry, extension: extName, score });
110          }
111        }
112      }
113    }
114
115    // Check root name
116    const rootScore = matchScore(target, null, root.name, "/" + root.name);
117    if (rootScore > 0) {
118      matches.push({
119        nodeId: rootId,
120        name: root.name,
121        path: "/" + root.name,
122        mode: null,
123        score: rootScore,
124      });
125    }
126  }
127
128  // If no matches from index, search node names directly
129  if (matches.length === 0) {
130    const nodeMatch = await searchNodesByName(target, roots.map(r => r._id));
131    if (nodeMatch) {
132      matches.push(nodeMatch);
133    }
134  }
135
136  // Sort by score descending
137  matches.sort((a, b) => b.score - a.score);
138
139  if (matches.length === 0) {
140    return { found: false, query: target };
141  }
142
143  if (matches.length === 1 || matches[0].score > matches[1].score * 1.5) {
144    // Clear winner
145    return { found: true, destination: matches[0] };
146  }
147
148  // Ambiguous
149  return { found: true, ambiguous: true, options: matches.slice(0, 5) };
150}
151
152function matchScore(target, extName, nodeName, path) {
153  const lowerName = (nodeName || "").toLowerCase();
154  const lowerPath = (path || "").toLowerCase();
155  const lowerExt = (extName || "").toLowerCase();
156
157  // Exact extension name match
158  if (lowerExt === target) return 10;
159
160  // Exact node name match
161  if (lowerName === target) return 9;
162
163  // Extension name contains target
164  if (lowerExt && lowerExt.includes(target)) return 7;
165
166  // Node name contains target
167  if (lowerName.includes(target)) return 6;
168
169  // Path contains target
170  if (lowerPath.includes(target)) return 4;
171
172  // Word match in name
173  const targetWords = target.split(/\s+/);
174  const nameWords = lowerName.split(/[\s\-_\/]+/);
175  const pathWords = lowerPath.split(/[\s\-_\/]+/);
176  const allWords = [...nameWords, ...pathWords, lowerExt].filter(Boolean);
177
178  let wordHits = 0;
179  for (const tw of targetWords) {
180    if (allWords.some(w => w.includes(tw) || tw.includes(w))) wordHits++;
181  }
182  if (wordHits > 0) return 2 + wordHits;
183
184  return 0;
185}
186
187async function searchNodesByName(target, rootIds) {
188  try {
189    // Search for nodes whose name matches the target across user's trees
190    const regex = new RegExp(target.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i");
191    const node = await Node.findOne({
192      name: regex,
193      $or: rootIds.map(id => ({ $or: [{ _id: id }, { parent: { $exists: true } }] })),
194    }).select("_id name parent").lean();
195
196    if (!node) return null;
197
198    // Build path
199    const parts = [node.name];
200    let current = node;
201    let depth = 0;
202    while (current.parent && depth < 10) {
203      current = await Node.findById(current.parent).select("name parent rootOwner").lean();
204      if (!current) break;
205      parts.unshift(current.name);
206      if (current.rootOwner) break;
207      depth++;
208    }
209
210    return {
211      nodeId: String(node._id),
212      name: node.name,
213      path: "/" + parts.join("/"),
214      mode: null,
215      score: 3,
216    };
217  } catch {
218    return null;
219  }
220}
221
1import log from "../../seed/log.js";
2import tools from "./tools.js";
3import { configure, findDestination, listPositions } from "./core.js";
4
5export async function init(core) {
6  // Wire dependencies
7  try {
8    const { getExtension } = await import("../loader.js");
9    configure({ getExtension });
10  } catch {}
11
12  const { default: router } = await import("./routes.js");
13
14  // Tool handler
15  const toolHandlers = {
16    "go-to": async ({ destination }, { userId }) => {
17      if (!destination) {
18        const { trees, extensions } = await listPositions(userId);
19        let out = "";
20        if (trees.length > 0) out += "Trees:\n" + trees.map(t => `  ${t.name}`).join("\n");
21        if (extensions.length > 0) out += (out ? "\n\n" : "") + "Extensions:\n" + extensions.map(e => `  ${e.name}  ${e.path}`).join("\n");
22        return out || "No trees found.";
23      }
24
25      const result = await findDestination(destination, userId);
26
27      if (!result.found) {
28        return `No match for "${destination}". Try 'go' with no arguments to see all positions.`;
29      }
30
31      if (result.ambiguous) {
32        return "Multiple matches:\n" + result.options.map(o => `  ${o.path}${o.extension ? ` (${o.extension})` : ""}`).join("\n") + "\nBe more specific.";
33      }
34
35      const dest = result.destination;
36      return JSON.stringify({
37        _navigate: dest.nodeId,
38        answer: `Navigating to ${dest.path}`,
39      });
40    },
41  };
42
43  log.info("Go", "Loaded. Navigate by intent.");
44
45  return {
46    router,
47    tools,
48    toolHandlers,
49    exports: { findDestination, listPositions },
50  };
51}
52
1export default {
2  name: "go",
3  version: "1.0.3",
4  builtFor: "TreeOS",
5  description:
6    "Navigate by intent. Say where, not how. " +
7    "'go workout' finds fitness across all your trees. 'go food' finds the food node. " +
8    "'go' with no arguments shows all navigable positions ranked by recency. " +
9    "Reads the routing index from tree-orchestrator. Matches extension names, tree names, " +
10    "node names. Navigates to the best match. No LLM call. Microseconds.",
11
12  needs: {
13    services: ["hooks"],
14    models: ["Node"],
15  },
16
17  optional: {
18    extensions: ["tree-orchestrator", "navigation"],
19  },
20
21  provides: {
22    models: {},
23    routes: "./routes.js",
24    tools: true,
25    jobs: false,
26    orchestrator: false,
27    energyActions: {},
28    sessionTypes: {},
29    env: [],
30
31    cli: [
32      {
33        command: "go [destination...]",
34        scope: ["tree", "home"],
35        description: "Navigate to a position by name or intent. No args = show all positions.",
36        method: "GET",
37        endpoint: "/go?q=:destination",
38      },
39    ],
40
41    hooks: {
42      fires: [],
43      listens: [],
44    },
45  },
46};
47
1import express from "express";
2import authenticate from "../../seed/middleware/authenticate.js";
3import { sendOk, sendError, ERR } from "../../seed/protocol.js";
4import { findDestination, listPositions } from "./core.js";
5
6const router = express.Router();
7
8router.get("/go", authenticate, async (req, res) => {
9  try {
10    const query = req.query.q || req.query.destination || "";
11    const result = query.trim()
12      ? await findDestination(query, req.userId)
13      : await listPositions(req.userId);
14
15    sendOk(res, result);
16  } catch (err) {
17    sendError(res, 500, ERR.INTERNAL, err.message);
18  }
19});
20
21export default router;
22
1export default [
2  {
3    name: "go-to",
4    description:
5      "Navigate to a node by name or intent. Searches across all trees. " +
6      "Use when the user says 'go to workout', 'take me to food', 'navigate to study'. " +
7      "Returns the destination node ID and path for navigation.",
8    inputSchema: {
9      type: "object",
10      properties: {
11        destination: {
12          type: "string",
13          description: "What to navigate to. Extension name, tree name, or node name.",
14        },
15      },
16      required: ["destination"],
17    },
18  },
19];
20

Versions

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

Comments

Loading comments...

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