cd7a82d39300475f75649a36fac6e9ec744781c2c44c5a9c12c94b58df321aab| Command | Method | Description |
|---|---|---|
research [message...] | POST | Market research. Look up prices, analyze sectors, surface opportunities. |
enrichContext1/**
2 * Market Researcher Core
3 *
4 * Track sectors, record findings, maintain a watchlist.
5 * The tree is the research desk.
6 */
7
8import log from "../../seed/log.js";
9import { setNodeMode } from "../../seed/modes/registry.js";
10
11let _Node = null;
12let _Note = null;
13let _metadata = null;
14
15export function configure({ Node, Note, metadata }) {
16 _Node = Node;
17 _Note = Note;
18 _metadata = metadata;
19}
20
21const ROLES = {
22 LOG: "log",
23 SECTORS: "sectors",
24 FINDINGS: "findings",
25 WATCHLIST: "watchlist",
26 PROFILE: "profile",
27};
28
29export { ROLES };
30
31// -- Scaffold --
32
33export async function scaffold(rootId, userId) {
34 if (!_Node) throw new Error("Market researcher core not configured");
35 const { createNode } = await import("../../seed/tree/treeManagement.js");
36
37 const logNode = await createNode({ name: "Log", parentId: rootId, userId });
38 const sectorsNode = await createNode({ name: "Sectors", parentId: rootId, userId });
39 const findingsNode = await createNode({ name: "Findings", parentId: rootId, userId });
40 const watchlistNode = await createNode({ name: "Watchlist", parentId: rootId, userId });
41 const profileNode = await createNode({ name: "Profile", parentId: rootId, userId });
42
43 const tags = [
44 [logNode, ROLES.LOG],
45 [sectorsNode, ROLES.SECTORS],
46 [findingsNode, ROLES.FINDINGS],
47 [watchlistNode, ROLES.WATCHLIST],
48 [profileNode, ROLES.PROFILE],
49 ];
50
51 for (const [node, role] of tags) {
52 await _metadata.setExtMeta(node, "market-researcher", { role });
53 }
54
55 // Set default mode on root to coach, log node to tell
56 await setNodeMode(rootId, "respond", "tree:market-coach");
57 await setNodeMode(logNode._id, "respond", "tree:market-tell");
58
59 // Configure browser-bridge auto-approve for financial sites
60 const root = await _Node.findById(rootId);
61 if (root) {
62 await _metadata.setExtMeta(root, "market-researcher", {
63 initialized: true,
64 setupPhase: "complete",
65 // Recommended browser-bridge sites (operator sets these on the browser-bridge namespace):
66 // coingecko.com, coinmarketcap.com, *.tradingview.com, *.yahoo.com,
67 // finance.yahoo.com, seeking-alpha.com, bloomberg.com
68 });
69 }
70
71 const ids = {};
72 for (const [node, role] of tags) ids[role] = String(node._id);
73
74 log.info("MarketResearcher", `Scaffolded under ${rootId}`);
75 return ids;
76}
77
78// -- Find nodes --
79
80export async function findResearchNodes(rootId) {
81 if (!_Node) return null;
82 const children = await _Node.find({ parent: rootId }).select("_id name metadata").lean();
83 const result = {};
84 for (const child of children) {
85 const meta = child.metadata instanceof Map
86 ? child.metadata.get("market-researcher")
87 : child.metadata?.["market-researcher"];
88 if (meta?.role) result[meta.role] = { id: String(child._id), name: child.name };
89 }
90 return result;
91}
92
93export async function isInitialized(rootId) {
94 if (!_Node) return false;
95 const root = await _Node.findById(rootId).select("metadata").lean();
96 if (!root) return false;
97 const meta = root.metadata instanceof Map
98 ? root.metadata.get("market-researcher")
99 : root.metadata?.["market-researcher"];
100 return !!meta?.initialized;
101}
102
103export async function getSetupPhase(rootId) {
104 if (!_Node) return null;
105 const root = await _Node.findById(rootId).select("metadata").lean();
106 if (!root) return null;
107 const meta = root.metadata instanceof Map
108 ? root.metadata.get("market-researcher")
109 : root.metadata?.["market-researcher"];
110 return meta?.setupPhase || (meta?.initialized ? "complete" : null);
111}
112
113export async function completeSetup(rootId) {
114 const root = await _Node.findById(rootId);
115 if (!root) return;
116 const existing = _metadata.getExtMeta(root, "market-researcher") || {};
117 await _metadata.setExtMeta(root, "market-researcher", { ...existing, setupPhase: "complete" });
118}
119
120// -- Sectors --
121
122export async function getSectors(rootId) {
123 const nodes = await findResearchNodes(rootId);
124 if (!nodes?.sectors) return [];
125
126 const children = await _Node.find({ parent: nodes.sectors.id })
127 .select("_id name metadata").lean();
128
129 return children.map(c => ({
130 id: String(c._id),
131 name: c.name,
132 }));
133}
134
135// -- Recent findings --
136
137export async function getRecentFindings(rootId, limit = 15) {
138 const nodes = await findResearchNodes(rootId);
139 if (!nodes?.findings) return [];
140
141 const { getNotes } = await import("../../seed/tree/notes.js");
142 const result = await getNotes({ nodeId: nodes.findings.id, limit });
143 return result?.notes || [];
144}
145
146// -- Watchlist --
147
148export async function getWatchlist(rootId) {
149 const nodes = await findResearchNodes(rootId);
150 if (!nodes?.watchlist) return [];
151
152 const children = await _Node.find({ parent: nodes.watchlist.id })
153 .select("_id name metadata").lean();
154
155 return children.map(c => ({
156 id: String(c._id),
157 name: c.name,
158 }));
159}
1601/**
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, getSectors, getRecentFindings, getWatchlist } from "./core.js";
7import { renderResearchDashboard } from "./pages/dashboard.js";
8
9const router = express.Router();
10
11router.get("/root/:rootId/market-researcher", 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, "Research tree not found");
16
17 if (!(await isInitialized(rootId))) {
18 return sendError(res, 404, ERR.TREE_NOT_FOUND, "Market researcher not initialized");
19 }
20
21 const sectors = await getSectors(rootId);
22 const findings = await getRecentFindings(rootId, 20);
23 const watchlist = await getWatchlist(rootId);
24
25 res.send(renderResearchDashboard({
26 rootId,
27 rootName: root.name,
28 sectors,
29 findings,
30 watchlist,
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, "Research dashboard failed");
37 }
38});
39
40export default router;
411import log from "../../seed/log.js";
2import {
3 configure,
4 findResearchNodes,
5 getSectors,
6 getRecentFindings,
7 getWatchlist,
8 isInitialized,
9 scaffold,
10} from "./core.js";
11import { handleMessage } from "./handler.js";
12
13export async function init(core) {
14 configure({
15 Node: core.models.Node,
16 Note: core.models.Note,
17 metadata: core.metadata,
18 });
19
20 // -- Modes --
21 const tellMode = (await import("./modes/tell.js")).default;
22 const coachMode = (await import("./modes/coach.js")).default;
23 const reviewMode = (await import("./modes/review.js")).default;
24
25 core.modes.registerMode(tellMode.name, tellMode, "market-researcher");
26 core.modes.registerMode(coachMode.name, coachMode, "market-researcher");
27 core.modes.registerMode(reviewMode.name, reviewMode, "market-researcher");
28
29 // -- enrichContext: inject research awareness into investor and finance nodes --
30 const _researchCache = new Map(); // treeRootId -> { summary, ts }
31 const RESEARCH_CACHE_TTL = 60000;
32
33 core.hooks.register("enrichContext", async ({ context, node }) => {
34 if (!node?._id) return;
35
36 const treeRootId = node.rootOwner ? String(node.rootOwner) : null;
37 if (!treeRootId) return;
38
39 const cached = _researchCache.get(treeRootId);
40 if (cached && Date.now() - cached.ts < RESEARCH_CACHE_TTL) {
41 if (cached.summary) context.marketResearch = cached.summary;
42 return;
43 }
44
45 try {
46 const { getExtension } = await import("../loader.js");
47 const life = getExtension("life");
48 if (!life?.exports?.getDomainNodes) return;
49
50 const domains = await life.exports.getDomainNodes(treeRootId);
51 const researchDomain = domains["market-researcher"];
52 if (!researchDomain?.id) {
53 _researchCache.set(treeRootId, { summary: null, ts: Date.now() });
54 return;
55 }
56
57 const findings = await getRecentFindings(researchDomain.id, 5);
58 const sectors = await getSectors(researchDomain.id);
59
60 if (findings.length === 0 && sectors.length === 0) {
61 _researchCache.set(treeRootId, { summary: null, ts: Date.now() });
62 return;
63 }
64
65 const compact = {
66 sectors: sectors.map(s => s.name),
67 recentFindings: findings.slice(0, 5).map(f => f.content),
68 };
69
70 _researchCache.set(treeRootId, { summary: compact, ts: Date.now() });
71 context.marketResearch = compact;
72 } catch {}
73 }, "market-researcher");
74
75 log.info("MarketResearcher", "Loaded. The tree researches markets.");
76
77 // ── Register apps-grid slot ──
78 try {
79 const { getExtension } = await import("../loader.js");
80 const base = getExtension("treeos-base");
81 base?.exports?.registerSlot?.("apps-grid", "market-researcher", ({ userId, rootMap, tokenParam, tokenField, esc: e }) => {
82 const entries = rootMap.get("Market Researcher") || [];
83 const existing = entries.map(entry =>
84 `<a class="app-active" href="/api/v1/root/${entry.id}/market-researcher?html${tokenParam}" style="margin-right:8px;margin-bottom:6px;">${e(entry.name)}</a>`
85 ).join("");
86 return `<div class="app-card">
87 <div class="app-header"><span class="app-emoji">🔬</span><span class="app-name">Market Research</span></div>
88 <div class="app-desc">Research agent. Uses browser to visit financial sites, pull data, and surface opportunities. Feeds findings to your investor and finance branches.</div>
89 ${entries.length > 0
90 ? `<div style="display:flex;flex-wrap:wrap;">${existing}</div>`
91 : `<form class="app-form" method="POST" action="/api/v1/user/${userId}/apps/create">
92 ${tokenField}<input type="hidden" name="app" value="market-researcher" />
93 <input class="app-input" name="message" placeholder="Research current crypto market" required />
94 <button class="app-start" type="submit">Start Research</button>
95 </form>`}
96 </div>`;
97 }, { priority: 70 });
98 } catch {}
99
100 // ── HTML dashboard route ──
101 let router = null;
102 try {
103 const { getExtension } = await import("../loader.js");
104 const htmlExt = getExtension("html-rendering");
105 if (htmlExt) {
106 router = (await import("./htmlRoutes.js")).default;
107 }
108 } catch {}
109
110 return {
111 router,
112 exports: {
113 scaffold,
114 isInitialized,
115 findResearchNodes,
116 getSectors,
117 getRecentFindings,
118 getWatchlist,
119 handleMessage,
120 },
121 };
122}
1231export default {
2 name: "market-researcher",
3 version: "1.0.1",
4 builtFor: "TreeOS",
5 scope: "confined",
6 description:
7 "Research agent for financial markets. Uses browser-bridge to visit financial sites, " +
8 "pull live data, and surface opportunities. Track sectors, maintain a watchlist, " +
9 "record findings. The AI browses CoinGecko, Yahoo Finance, TradingView, and other " +
10 "sources to gather prices, trends, and analysis. Confined scope: must be ext-allowed " +
11 "at specific positions because it leverages browser-bridge for web access. " +
12 "Never gives financial advice. Reports data, flags moves, notes risks.",
13
14 territory: "market research, stock analysis, crypto prices, financial news, sector trends, market conditions, opportunities",
15 classifierHints: [
16 /\b(research|look\s*up|check\s*(the\s*)?price|market|trend|sector|news|analysis)\b/i,
17 /\b(stock|crypto|bitcoin|btc|eth|token|coin|ticker|equity|commodity)\b/i,
18 /\b(bull|bear|rally|dip|correction|ath|all.time.high|volume|rsi|macd)\b/i,
19 /\b(what.?s\s+(happening|going\s+on)\s+(in|with)\s+(the\s+)?market)/i,
20 /\b(price\s+of|how\s+is\s+.+\s+doing|check\s+on)\b/i,
21 ],
22
23 needs: {
24 models: ["Node", "Note"],
25 services: ["hooks", "llm", "metadata"],
26 },
27
28 optional: {
29 extensions: [
30 "browser-bridge",
31 "investor",
32 "finance",
33 "channels",
34 "html-rendering",
35 "treeos-base",
36 ],
37 },
38
39 provides: {
40 models: {},
41 routes: false,
42 tools: false,
43 jobs: false,
44 modes: true,
45
46 hooks: {
47 fires: [],
48 listens: ["enrichContext"],
49 },
50
51 cli: [
52 {
53 command: "research [message...]",
54 scope: ["tree"],
55 description: "Market research. Look up prices, analyze sectors, surface opportunities.",
56 method: "POST",
57 endpoint: "/root/:rootId/chat",
58 body: ["message"],
59 },
60 ],
61 },
62};
631import { findExtensionRoot } from "../../../seed/tree/extensionMetadata.js";
2import { findResearchNodes, getSectors, getRecentFindings, getWatchlist } from "../core.js";
3
4export default {
5 name: "tree:market-coach",
6 emoji: "🧭",
7 label: "Market 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 resRoot = await findExtensionRoot(currentNodeId || rootId, "market-researcher") || rootId;
27 const nodes = resRoot ? await findResearchNodes(resRoot) : null;
28 const sectors = resRoot ? await getSectors(resRoot) : [];
29 const findings = resRoot ? await getRecentFindings(resRoot, 5) : [];
30 const watchlist = resRoot ? await getWatchlist(resRoot) : [];
31
32 const sectorList = sectors.length > 0
33 ? sectors.map(s => `- ${s.name}`).join("\n")
34 : "No sectors tracked yet.";
35
36 const findingsList = findings.length > 0
37 ? findings.slice(0, 5).map(f => `- ${f.content}`).join("\n")
38 : "No findings recorded yet.";
39
40 const watchlistItems = watchlist.length > 0
41 ? watchlist.map(w => `- ${w.name}`).join("\n")
42 : "Watchlist is empty.";
43
44 const sectorsId = nodes?.sectors?.id;
45 const watchlistId = nodes?.watchlist?.id;
46
47 return `You are ${username}'s market research coach.
48
49${sectors.length > 0 ? `STATUS: ${sectors.length} sectors tracked.` : "STATUS: No sectors yet. Help them decide what markets to watch."}
50
51CURRENT SECTORS:
52${sectorList}
53
54WATCHLIST:
55${watchlistItems}
56
57RECENT FINDINGS:
58${findingsList}
59
60YOUR ROLE:
61- Help ${username} decide what markets and sectors to track.
62- Create new sectors under Sectors (${sectorsId || "find it"}) when they want to track something new.
63- Add assets to Watchlist (${watchlistId || "find it"}) when they want to monitor something specific.
64- Discuss research strategy: what to look at, what timeframes matter, what signals to watch for.
65- Review their current sectors and suggest gaps or redundancies.
66
67CAPABILITIES:
68- Create sector nodes (e.g., "Crypto", "AI Stocks", "Energy", "Real Estate", "Commodities").
69- Create watchlist entries (e.g., "BTC", "NVDA", "Gold", "SPY").
70- Organize findings into categories.
71- Search through past research notes.
72
73BEHAVIOR:
74- Be practical. "You have 6 sectors and no crypto exposure. Want to add one?" not "Consider diversifying your research portfolio."
75- When they say "track X" or "watch X" or "add X", create the node. Don't ask for confirmation.
76- If they seem unfocused, help them narrow: "What's your main question right now?"
77- Suggest specific research tasks: "Want me to pull current BTC price and volume?" (they'd switch to tell mode for that).
78- Never give financial advice. Help them organize their research, not their portfolio.
79- Never expose node IDs or metadata to the user.`.trim();
80 },
81};
821import { findExtensionRoot } from "../../../seed/tree/extensionMetadata.js";
2import { findResearchNodes, getSectors, getRecentFindings, getWatchlist } from "../core.js";
3
4export default {
5 name: "tree:market-review",
6 emoji: "📋",
7 label: "Market 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 resRoot = await findExtensionRoot(currentNodeId || rootId, "market-researcher") || rootId;
24 const nodes = resRoot ? await findResearchNodes(resRoot) : null;
25 const sectors = resRoot ? await getSectors(resRoot) : [];
26 const findings = resRoot ? await getRecentFindings(resRoot, 20) : [];
27 const watchlist = resRoot ? await getWatchlist(resRoot) : [];
28
29 const sectorList = sectors.length > 0
30 ? sectors.map(s => `- ${s.name}`).join("\n")
31 : "No sectors tracked.";
32
33 const findingsList = findings.length > 0
34 ? findings.slice(0, 20).map(f => `- ${f.content}`).join("\n")
35 : "No findings recorded yet.";
36
37 const watchlistItems = watchlist.length > 0
38 ? watchlist.map(w => `- ${w.name}`).join("\n")
39 : "Watchlist is empty.";
40
41 return `You are reviewing ${username}'s market research. Summarize what's been found. Be factual.
42
43SECTORS TRACKED:
44${sectorList}
45
46WATCHLIST:
47${watchlistItems}
48
49RECENT FINDINGS:
50${findingsList}
51
52BEHAVIOR:
53- Answer their question directly with data from the findings.
54- "What did you find?" = summarize recent findings organized by sector.
55- "Any opportunities?" = highlight significant moves, unusual volume, breaking developments from the findings.
56- "How is X doing?" = pull relevant findings about that asset or sector.
57- Organize by sector when summarizing multiple findings.
58- Highlight significant moves: large price changes, volume spikes, trend reversals, breaking news.
59- Note the age of findings. "BTC was at $67,400 as of the last check" not "BTC is $67,400" unless you just fetched it.
60- If findings are stale or sparse, say so. "Last research was 3 days ago. Want me to pull fresh data?"
61- Be factual. No predictions. No advice. Report what the research shows.
62- If they ask something the findings don't cover, say so directly.
63- Never expose node IDs or metadata to the user.`.trim();
64 },
65};
661import { findExtensionRoot } from "../../../seed/tree/extensionMetadata.js";
2import { findResearchNodes, getSectors, getRecentFindings } from "../core.js";
3
4export default {
5 name: "tree:market-tell",
6 emoji: "📡",
7 label: "Market Research",
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 "browser-navigate",
23 "browser-read",
24 "browser-fetch",
25 "browser-click",
26 ],
27
28 async buildSystemPrompt({ username, rootId, currentNodeId }) {
29 const resRoot = await findExtensionRoot(currentNodeId || rootId, "market-researcher") || rootId;
30 const nodes = resRoot ? await findResearchNodes(resRoot) : null;
31 const sectors = resRoot ? await getSectors(resRoot) : [];
32 const findings = resRoot ? await getRecentFindings(resRoot, 10) : [];
33
34 const sectorList = sectors.length > 0
35 ? sectors.map(s => `- ${s.name}`).join("\n")
36 : "No sectors tracked yet.";
37
38 const findingsList = findings.length > 0
39 ? findings.slice(0, 10).map(f => `- ${f.content}`).join("\n")
40 : "No findings recorded yet.";
41
42 const findingsId = nodes?.findings?.id;
43 const sectorsId = nodes?.sectors?.id;
44 const watchlistId = nodes?.watchlist?.id;
45
46 return `You are a market research agent for ${username}. You have a browser. Your job is to visit financial sites, pull live data, and record findings.
47
48CURRENT SECTORS:
49${sectorList}
50
51RECENT FINDINGS:
52${findingsList}
53
54WORKFLOW:
551. For structured data (prices, market caps, volumes), use browser-fetch with JSON APIs:
56 - CoinGecko API: https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,ethereum&vs_currencies=usd&include_24hr_change=true
57 - CoinGecko coin list: https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=20
58 - CoinGecko specific coin: https://api.coingecko.com/api/v3/coins/{id}
592. For analysis, news, and pages without APIs, use browser-navigate + browser-read:
60 - Yahoo Finance: https://finance.yahoo.com/quote/{TICKER}
61 - TradingView: https://www.tradingview.com/symbols/{TICKER}
623. Write findings as notes under Findings (${findingsId || "find it"}). One note per finding. Concise.
634. If a sector doesn't exist, create one under Sectors (${sectorsId || "find it"}).
645. Add specific assets to Watchlist (${watchlistId || "find it"}) when the user asks.
65
66DATA FORMAT:
67Write concise findings. Examples:
68- "BTC $67,400 (+3.2% 24h). RSI 62. Volume up 15%. Consolidating above $65k support."
69- "ETH $3,450 (-1.1% 24h). Gas fees low. L2 activity rising. ETH/BTC ratio declining."
70- "AAPL $178.20 (+0.8%). Beat Q3 earnings. Services revenue up 14%. New high."
71- "Crypto market cap $2.4T. BTC dominance 52%. Fear/Greed Index: 71 (Greed)."
72
73BROWSER USAGE:
74- browser-fetch for API endpoints that return JSON. Fastest and most reliable.
75- browser-navigate to load a page, then browser-read to extract content from it.
76- browser-click only when you need to interact (expand sections, load more data).
77- If a page fails to load, try an alternative source. Don't get stuck.
78
79RULES:
80- Never give financial advice. You report data.
81- Flag significant moves (>5% daily, unusual volume, breaking news).
82- Note risks alongside opportunities. Every opportunity has a risk.
83- Be specific with numbers. "$67,400" not "around $67k".
84- Include timeframes. "24h change", "this week", "since earnings".
85- Never expose node IDs or metadata to the user.
86- If the user asks about something you can't find data for, say so. Don't fabricate prices.`.trim();
87 },
88};
891/**
2 * Market Researcher Dashboard
3 *
4 * Shows sectors being tracked, recent findings, watchlist.
5 * Research summaries organized by sector.
6 */
7
8import { renderAppDashboard } from "../../html-rendering/html/appDashboard.js";
9
10export function renderResearchDashboard({ rootId, rootName, sectors, findings, watchlist, token, userId, inApp }) {
11 const allSectors = sectors || [];
12 const allFindings = findings || [];
13 const wl = watchlist || [];
14
15 // Hero: research activity
16 const hero = {
17 value: String(allFindings.length),
18 label: allFindings.length === 1 ? "finding" : "findings",
19 color: "#667eea",
20 sub: allSectors.length > 0 ? `${allSectors.length} sectors tracked` : null,
21 };
22
23 // Stats
24 const stats = [];
25 if (allSectors.length > 0) stats.push({ value: String(allSectors.length), label: "sectors" });
26 if (wl.length > 0) stats.push({ value: String(wl.length), label: "watching" });
27 const recentCount = allFindings.filter(f => {
28 if (!f.createdAt) return false;
29 const hours = (Date.now() - new Date(f.createdAt).getTime()) / 3600000;
30 return hours < 24;
31 }).length;
32 if (recentCount > 0) stats.push({ value: String(recentCount), label: "last 24h" });
33
34 // Cards
35 const cards = [];
36
37 // Sectors card
38 if (allSectors.length > 0) {
39 cards.push({
40 title: "Sectors",
41 items: allSectors.map(s => ({ text: s.name })),
42 });
43 }
44
45 // Recent findings card
46 if (allFindings.length > 0) {
47 cards.push({
48 title: "Recent Findings",
49 items: allFindings.slice(0, 20).map(f => ({
50 text: f.content || "Finding",
51 sub: f.createdAt ? new Date(f.createdAt).toLocaleDateString() : null,
52 })),
53 });
54 }
55
56 // Watchlist card
57 if (wl.length > 0) {
58 cards.push({
59 title: "Watchlist",
60 items: wl.map(w => ({
61 text: w.name,
62 sub: w.notes || null,
63 })),
64 });
65 }
66
67 return renderAppDashboard({
68 rootId,
69 rootName: rootName || "Market Research",
70 token,
71 userId,
72 inApp: !!inApp,
73 subtitle: new Date().toLocaleDateString("en-US", { weekday: "long", month: "long", day: "numeric" }),
74 hero,
75 stats,
76 bars: [],
77 cards,
78 chatBar: {
79 placeholder: "Research a market, check a price, or review findings...",
80 endpoint: `/api/v1/root/${rootId}/chat`,
81 },
82 emptyState: allFindings.length === 0 && allSectors.length === 0
83 ? { title: "No research yet", message: "Tell me what to research. I'll use the browser to find data." }
84 : null,
85 });
86}
87
treeos ext star market-researcher
Post comments from the CLI: treeos ext comment market-researcher "your comment"
Max 3 comments per extension. One star and one flag per user.
Loading comments...