repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
The Living OS cockpit
stars
latest
clone command
git clone gitlawb://did:key:z6Mku78K...XywC/living-os-cockp...git clone gitlawb://did:key:z6Mku78K.../living-os-cockp...59751530feat: surface worker supervisor health in live work8h ago| #1 | import { NextResponse } from 'next/server'; |
| #2 | import { readdir, readFile, stat } from 'fs/promises'; |
| #3 | import path from 'path'; |
| #4 | import { getUserContext, AuthMismatchError, AuthRequiredError } from '@/lib/user-context'; |
| #5 | |
| #6 | export const runtime = 'nodejs'; |
| #7 | export const maxDuration = 60; |
| #8 | |
| #9 | type NodeRecord = { id: string; title: string; path: string; folder: string; tags: string[]; preview: string }; |
| #10 | type EdgeRecord = { id: string; source: string; target: string; type: 'wiki' | 'tag' | 'related' | 'folder' | 'backlink'; label: string; context: string }; |
| #11 | |
| #12 | const SKIP_DIRS = new Set(['.git', 'chromadb', 'faiss', 'leann', 'node_modules', '__pycache__', '.obsidian']); |
| #13 | const MAX_FILES = 350; |
| #14 | const MAX_EDGES_PER_TYPE = 500; |
| #15 | |
| #16 | async function walk(dir: string, root: string, out: string[]) { |
| #17 | let entries: any[] = []; |
| #18 | try { entries = await readdir(dir, { withFileTypes: true }); } catch { return; } |
| #19 | for (const entry of entries) { |
| #20 | if (out.length >= MAX_FILES) return; |
| #21 | if (entry.name.startsWith('.') && entry.name !== '.md') continue; |
| #22 | const full = path.join(dir, entry.name); |
| #23 | if (entry.isDirectory()) { |
| #24 | if (!SKIP_DIRS.has(entry.name)) await walk(full, root, out); |
| #25 | } else if (entry.isFile() && entry.name.toLowerCase().endsWith('.md')) { |
| #26 | const s = await stat(full).catch(() => null); |
| #27 | if (s && s.size <= 750_000) out.push(full); |
| #28 | } |
| #29 | } |
| #30 | } |
| #31 | |
| #32 | function titleFor(file: string, text: string) { |
| #33 | const h1 = text.match(/^#\s+(.+)$/m)?.[1]?.trim(); |
| #34 | return h1 || path.basename(file, path.extname(file)).replace(/[_-]+/g, ' '); |
| #35 | } |
| #36 | |
| #37 | function normalizeKey(value: string) { |
| #38 | return value.trim().toLowerCase().replace(/\.md$/i, '').replace(/[_-]+/g, ' '); |
| #39 | } |
| #40 | |
| #41 | function lineContext(text: string, index: number) { |
| #42 | const before = text.lastIndexOf('\n', Math.max(0, index - 1)); |
| #43 | const after = text.indexOf('\n', index); |
| #44 | return text.slice(before + 1, after === -1 ? undefined : after).trim().slice(0, 240); |
| #45 | } |
| #46 | |
| #47 | function edgeId(source: string, target: string, type: string, label: string) { |
| #48 | return `${type}:${source}->${target}:${label}`; |
| #49 | } |
| #50 | |
| #51 | function pushEdge(edges: EdgeRecord[], seen: Set<string>, edge: Omit<EdgeRecord, 'id'>) { |
| #52 | if (edge.source === edge.target) return; |
| #53 | const id = edgeId(edge.source, edge.target, edge.type, edge.label); |
| #54 | if (seen.has(id)) return; |
| #55 | seen.add(id); |
| #56 | edges.push({ id, ...edge }); |
| #57 | } |
| #58 | |
| #59 | function findTarget(label: string, titleIndex: Map<string, string>) { |
| #60 | const clean = label.split('#')[0].trim(); |
| #61 | return titleIndex.get(normalizeKey(clean)) || titleIndex.get(clean.toLowerCase()); |
| #62 | } |
| #63 | |
| #64 | function authErrorResponse(error: unknown) { |
| #65 | if (error instanceof AuthRequiredError || error instanceof AuthMismatchError) { |
| #66 | return NextResponse.json({ error: error.message }, { status: error.status }); |
| #67 | } |
| #68 | throw error; |
| #69 | } |
| #70 | |
| #71 | export async function GET() { |
| #72 | try { |
| #73 | const ctx = await getUserContext(); |
| #74 | const files: string[] = []; |
| #75 | await walk(ctx.vaultPath, ctx.vaultPath, files); |
| #76 | |
| #77 | const nodes: NodeRecord[] = []; |
| #78 | const titleIndex = new Map<string, string>(); |
| #79 | const fileTexts = new Map<string, string>(); |
| #80 | |
| #81 | for (const file of files) { |
| #82 | const text = await readFile(file, 'utf8').catch(() => ''); |
| #83 | const rel = path.relative(ctx.vaultPath, file); |
| #84 | const title = titleFor(file, text); |
| #85 | const tags = Array.from(new Set(Array.from(text.matchAll(/(^|\s)#([A-Za-z0-9_/-]+)/g)).map((m) => m[2]).slice(0, 16))); |
| #86 | const preview = text.replace(/---[\s\S]*?---/, '').replace(/\s+/g, ' ').trim().slice(0, 220); |
| #87 | const folder = path.dirname(rel) === '.' ? '' : path.dirname(rel); |
| #88 | nodes.push({ id: rel, title, path: rel, folder, tags, preview }); |
| #89 | fileTexts.set(rel, text); |
| #90 | titleIndex.set(normalizeKey(title), rel); |
| #91 | titleIndex.set(normalizeKey(path.basename(file, '.md')), rel); |
| #92 | titleIndex.set(normalizeKey(rel), rel); |
| #93 | titleIndex.set(normalizeKey(rel.replace(/\.md$/i, '')), rel); |
| #94 | } |
| #95 | |
| #96 | const edges: EdgeRecord[] = []; |
| #97 | const seen = new Set<string>(); |
| #98 | |
| #99 | for (const node of nodes) { |
| #100 | const text = fileTexts.get(node.id) || ''; |
| #101 | for (const match of text.matchAll(/\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g)) { |
| #102 | const label = match[1].trim(); |
| #103 | const target = findTarget(label, titleIndex); |
| #104 | if (target) { |
| #105 | const context = lineContext(text, match.index || 0); |
| #106 | pushEdge(edges, seen, { source: node.id, target, type: 'wiki', label, context }); |
| #107 | pushEdge(edges, seen, { source: target, target: node.id, type: 'backlink', label: node.title, context }); |
| #108 | } |
| #109 | } |
| #110 | for (const match of text.matchAll(/^(?:see also|related):\s*(.+)$/gim)) { |
| #111 | const context = match[0].trim(); |
| #112 | const labels = match[1].split(/,|;/).map((item) => item.replace(/\[\[|\]\]/g, '').trim()).filter(Boolean); |
| #113 | for (const label of labels) { |
| #114 | const target = findTarget(label, titleIndex); |
| #115 | if (target) pushEdge(edges, seen, { source: node.id, target, type: 'related', label, context }); |
| #116 | } |
| #117 | } |
| #118 | } |
| #119 | |
| #120 | const byTag = new Map<string, NodeRecord[]>(); |
| #121 | for (const node of nodes) { |
| #122 | for (const tag of node.tags) byTag.set(tag, [...(byTag.get(tag) || []), node]); |
| #123 | } |
| #124 | let tagEdges = 0; |
| #125 | for (const [tag, tagged] of byTag) { |
| #126 | for (let i = 0; i < tagged.length && tagEdges < MAX_EDGES_PER_TYPE; i += 1) { |
| #127 | for (let j = i + 1; j < tagged.length && tagEdges < MAX_EDGES_PER_TYPE; j += 1) { |
| #128 | pushEdge(edges, seen, { source: tagged[i].id, target: tagged[j].id, type: 'tag', label: `#${tag}`, context: `Both notes carry #${tag}` }); |
| #129 | tagEdges += 1; |
| #130 | } |
| #131 | } |
| #132 | } |
| #133 | |
| #134 | const byFolder = new Map<string, NodeRecord[]>(); |
| #135 | for (const node of nodes) { |
| #136 | if (node.folder) byFolder.set(node.folder, [...(byFolder.get(node.folder) || []), node]); |
| #137 | } |
| #138 | let folderEdges = 0; |
| #139 | for (const [folder, folderNodes] of byFolder) { |
| #140 | const sorted = [...folderNodes].sort((a, b) => a.path.localeCompare(b.path)); |
| #141 | for (let i = 0; i < sorted.length - 1 && folderEdges < MAX_EDGES_PER_TYPE; i += 1) { |
| #142 | pushEdge(edges, seen, { source: sorted[i].id, target: sorted[i + 1].id, type: 'folder', label: folder, context: `Shared folder: ${folder}` }); |
| #143 | folderEdges += 1; |
| #144 | } |
| #145 | } |
| #146 | |
| #147 | const edge_counts = edges.reduce((acc: Record<string, number>, edge) => { |
| #148 | acc[edge.type] = (acc[edge.type] || 0) + 1; |
| #149 | return acc; |
| #150 | }, {}); |
| #151 | |
| #152 | return NextResponse.json({ user_id: ctx.userId, vault: ctx.vaultPath, nodes, edges, edge_counts, generated_at: new Date().toISOString() }); |
| #153 | } catch (error) { |
| #154 | return authErrorResponse(error); |
| #155 | } |
| #156 | } |
| #157 |