[TERMINAL · SKILLS]
> mounting /skills...
> indexing 295 manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Terminal.skills
Use Cases/Build a Knowledge Graph Builder

Build a Knowledge Graph Builder

Build a knowledge graph builder with entity extraction, relationship mapping, graph queries, visualization, and incremental updates for connecting organizational knowledge.

Development#redis#caching#database#pub-sub#queues
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 4 skills

Avg quality 93/100·All SAFE
>

redis

v1.0.0

Build applications with Redis — caching, session storage, pub/sub, streams, rate limiting, leaderboards, and queues. Use when tasks involve in-memory data storage, real-time messaging, distributed locking, or performance optimization with caching layers.

93/100 quality
1.81× impact
SAFE
View skill
>

postgresql

v1.0.0

Assists with designing schemas, writing performant queries, managing indexes, and operating PostgreSQL databases. Use when working with JSONB, full-text search, window functions, CTEs, row-level security, replication, or performance tuning. Trigger words: postgresql, postgres, sql, database, jsonb, rls, window functions, cte.

87/100 quality
1.53× impact
SAFE
View skill
>

hono

v1.0.0

You are an expert in Hono, the ultrafast web framework for the edge. You help developers build APIs and web applications that run on Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda, and Vercel Edge — with a tiny footprint (~14KB), middleware ecosystem, JSX support, RPC client, and Web Standards API compatibility that makes code truly portable across runtimes.

93/100 quality
3.00× impact
SAFE
View skill
>

zod

v1.0.0

You are an expert in Zod, the TypeScript-first schema declaration and validation library. You help developers define schemas that validate data at runtime AND infer TypeScript types at compile time — eliminating the need to write types and validators separately. Used for API input validation, form validation, environment variables, config files, and any data boundary.

100/100 quality
1.21× impact
SAFE
View skill
$

The Problem

Lena leads knowledge management at a 25-person consultancy. Information is trapped in 5,000 documents, 10K emails, and 500 Slack channels. Finding "which clients use technology X" or "who is our expert on topic Y" requires asking 5 people. New hires take 3 months to learn who knows what. CRM, project management, and email are disconnected — no tool shows the relationship between people, projects, technologies, and clients. They need a knowledge graph: extract entities from all sources, map relationships, query connections, and visualize the organizational brain.

Step 1: Build the Knowledge Graph

typescript
import { pool } from "../db";
import { Redis } from "ioredis";
import { randomBytes, createHash } from "node:crypto";
const redis = new Redis(process.env.REDIS_URL!);

interface Entity { id: string; type: "person" | "company" | "technology" | "project" | "topic" | "document"; name: string; properties: Record<string, any>; sources: string[]; updatedAt: string; }
interface Relationship { id: string; sourceId: string; targetId: string; type: string; weight: number; properties: Record<string, any>; sources: string[]; updatedAt: string; }
interface GraphQuery { startEntity: string; relationshipTypes?: string[]; maxDepth: number; }
interface GraphResult { nodes: Entity[]; edges: Relationship[]; paths: Array<{ entities: string[]; relationships: string[] }>; }

// Add or update entity
export async function upsertEntity(entity: Omit<Entity, "id" | "updatedAt">): Promise<Entity> {
  const id = createHash("md5").update(`${entity.type}:${entity.name.toLowerCase()}`).digest("hex").slice(0, 16);
  const existing = await getEntity(id);

  if (existing) {
    const mergedSources = [...new Set([...existing.sources, ...entity.sources])];
    const mergedProperties = { ...existing.properties, ...entity.properties };
    await pool.query(
      "UPDATE graph_entities SET properties = $2, sources = $3, updated_at = NOW() WHERE id = $1",
      [id, JSON.stringify(mergedProperties), JSON.stringify(mergedSources)]
    );
    return { ...existing, properties: mergedProperties, sources: mergedSources, updatedAt: new Date().toISOString() };
  }

  const full: Entity = { ...entity, id, updatedAt: new Date().toISOString() };
  await pool.query(
    "INSERT INTO graph_entities (id, type, name, properties, sources, updated_at) VALUES ($1, $2, $3, $4, $5, NOW())",
    [id, entity.type, entity.name, JSON.stringify(entity.properties), JSON.stringify(entity.sources)]
  );
  return full;
}

// Add relationship
export async function addRelationship(rel: Omit<Relationship, "id" | "updatedAt">): Promise<Relationship> {
  const id = createHash("md5").update(`${rel.sourceId}:${rel.type}:${rel.targetId}`).digest("hex").slice(0, 16);

  await pool.query(
    `INSERT INTO graph_relationships (id, source_id, target_id, type, weight, properties, sources, updated_at)
     VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())
     ON CONFLICT (id) DO UPDATE SET weight = graph_relationships.weight + $5, sources = $7, updated_at = NOW()`,
    [id, rel.sourceId, rel.targetId, rel.type, rel.weight, JSON.stringify(rel.properties), JSON.stringify(rel.sources)]
  );

  return { ...rel, id, updatedAt: new Date().toISOString() };
}

// Traverse graph
export async function queryGraph(query: GraphQuery): Promise<GraphResult> {
  const visited = new Set<string>();
  const nodes: Entity[] = [];
  const edges: Relationship[] = [];
  const paths: GraphResult["paths"] = [];

  async function traverse(entityId: string, depth: number, currentPath: { entities: string[]; relationships: string[] }): Promise<void> {
    if (depth > query.maxDepth || visited.has(entityId)) return;
    visited.add(entityId);

    const entity = await getEntity(entityId);
    if (!entity) return;
    nodes.push(entity);

    let sql = "SELECT * FROM graph_relationships WHERE source_id = $1 OR target_id = $1";
    const params: any[] = [entityId];
    if (query.relationshipTypes?.length) {
      sql += " AND type = ANY($2)";
      params.push(query.relationshipTypes);
    }

    const { rows: rels } = await pool.query(sql, params);

    for (const rel of rels) {
      const r: Relationship = { ...rel, properties: JSON.parse(rel.properties), sources: JSON.parse(rel.sources) };
      edges.push(r);
      const nextId = rel.source_id === entityId ? rel.target_id : rel.source_id;
      const newPath = { entities: [...currentPath.entities, nextId], relationships: [...currentPath.relationships, rel.id] };

      if (depth === query.maxDepth - 1) paths.push(newPath);
      await traverse(nextId, depth + 1, newPath);
    }
  }

  await traverse(query.startEntity, 0, { entities: [query.startEntity], relationships: [] });

  // Deduplicate
  const uniqueNodes = [...new Map(nodes.map((n) => [n.id, n])).values()];
  const uniqueEdges = [...new Map(edges.map((e) => [e.id, e])).values()];

  return { nodes: uniqueNodes, edges: uniqueEdges, paths };
}

// Find shortest path between two entities
export async function findPath(fromId: string, toId: string, maxDepth: number = 5): Promise<{ found: boolean; path: string[]; relationships: string[] }> {
  const visited = new Set<string>();
  const queue: Array<{ entityId: string; path: string[]; rels: string[] }> = [{ entityId: fromId, path: [fromId], rels: [] }];

  while (queue.length > 0) {
    const { entityId, path, rels } = queue.shift()!;
    if (entityId === toId) return { found: true, path, relationships: rels };
    if (path.length > maxDepth) continue;
    if (visited.has(entityId)) continue;
    visited.add(entityId);

    const { rows } = await pool.query(
      "SELECT id, source_id, target_id, type FROM graph_relationships WHERE source_id = $1 OR target_id = $1",
      [entityId]
    );

    for (const rel of rows) {
      const nextId = rel.source_id === entityId ? rel.target_id : rel.source_id;
      if (!visited.has(nextId)) {
        queue.push({ entityId: nextId, path: [...path, nextId], rels: [...rels, `${rel.type}(${rel.id})`] });
      }
    }
  }

  return { found: false, path: [], relationships: [] };
}

// Ingest document and extract entities/relationships
export async function ingestDocument(content: string, source: string): Promise<{ entities: number; relationships: number }> {
  const entities: Entity[] = [];
  const relationships: Relationship[] = [];

  // Extract people
  const people = content.match(/(?:Mr|Mrs|Ms|Dr)\.?\s+[A-Z][a-z]+\s+[A-Z][a-z]+/g) || [];
  for (const name of people) {
    const entity = await upsertEntity({ type: "person", name: name.replace(/^(Mr|Mrs|Ms|Dr)\.?\s+/, ""), properties: {}, sources: [source] });
    entities.push(entity);
  }

  // Extract technologies
  const techPatterns = /\b(React|TypeScript|Python|Kubernetes|Docker|AWS|GCP|Azure|PostgreSQL|Redis|GraphQL|REST|Node\.js|Go|Rust|Java|Swift)\b/gi;
  const techs = [...new Set((content.match(techPatterns) || []).map((t) => t.toLowerCase()))];
  for (const tech of techs) {
    const entity = await upsertEntity({ type: "technology", name: tech, properties: {}, sources: [source] });
    entities.push(entity);
  }

  // Extract companies
  const companies = content.match(/[A-Z][a-z]+(?:\s+[A-Z][a-z]+)*(?:\s+(?:Inc|LLC|Ltd|Corp))\.?/g) || [];
  for (const company of companies) {
    const entity = await upsertEntity({ type: "company", name: company, properties: {}, sources: [source] });
    entities.push(entity);
  }

  // Create relationships between co-occurring entities
  for (let i = 0; i < entities.length; i++) {
    for (let j = i + 1; j < entities.length; j++) {
      const relType = inferRelationshipType(entities[i].type, entities[j].type);
      const rel = await addRelationship({
        sourceId: entities[i].id, targetId: entities[j].id,
        type: relType, weight: 1, properties: {}, sources: [source],
      });
      relationships.push(rel);
    }
  }

  return { entities: entities.length, relationships: relationships.length };
}

function inferRelationshipType(type1: string, type2: string): string {
  if (type1 === "person" && type2 === "technology") return "uses";
  if (type1 === "person" && type2 === "company") return "works_at";
  if (type1 === "person" && type2 === "project") return "works_on";
  if (type1 === "company" && type2 === "technology") return "uses";
  if (type1 === "technology" && type2 === "project") return "used_in";
  return "related_to";
}

async function getEntity(id: string): Promise<Entity | null> {
  const { rows: [row] } = await pool.query("SELECT * FROM graph_entities WHERE id = $1", [id]);
  return row ? { ...row, properties: JSON.parse(row.properties), sources: JSON.parse(row.sources) } : null;
}

// Search entities
export async function searchEntities(query: string, type?: string): Promise<Entity[]> {
  let sql = "SELECT * FROM graph_entities WHERE name ILIKE $1";
  const params: any[] = [`%${query}%`];
  if (type) { sql += " AND type = $2"; params.push(type); }
  sql += " ORDER BY name LIMIT 20";
  const { rows } = await pool.query(sql, params);
  return rows.map((r: any) => ({ ...r, properties: JSON.parse(r.properties), sources: JSON.parse(r.sources) }));
}

// Graph statistics
export async function getGraphStats(): Promise<{ entities: number; relationships: number; byType: Record<string, number>; mostConnected: Array<{ name: string; connections: number }> }> {
  const { rows: [{ count: entityCount }] } = await pool.query("SELECT COUNT(*) as count FROM graph_entities");
  const { rows: [{ count: relCount }] } = await pool.query("SELECT COUNT(*) as count FROM graph_relationships");
  const { rows: byType } = await pool.query("SELECT type, COUNT(*) as count FROM graph_entities GROUP BY type");
  const { rows: connected } = await pool.query(
    `SELECT e.name, COUNT(r.id) as connections FROM graph_entities e
     LEFT JOIN graph_relationships r ON e.id = r.source_id OR e.id = r.target_id
     GROUP BY e.id, e.name ORDER BY connections DESC LIMIT 10`
  );

  return {
    entities: parseInt(entityCount), relationships: parseInt(relCount),
    byType: Object.fromEntries(byType.map((r: any) => [r.type, parseInt(r.count)])),
    mostConnected: connected.map((r: any) => ({ name: r.name, connections: parseInt(r.connections) })),
  };
}

Results

  • "Who knows Kubernetes?" answered in 1 second — search entity → traverse "uses" relationships → find all people connected to Kubernetes; previously required asking 5 colleagues
  • Hidden connections discovered — Client A uses React; Client B uses React; their project leads both know each other from a conference → cross-sell opportunity found in the graph
  • Onboarding: 3 months → 2 weeks — new hire explores the graph; sees who works on what, which technologies are used where, who the subject matter experts are
  • 5,000 documents ingested — automatic entity extraction; relationships built incrementally; graph grows with every document; no manual tagging
  • Shortest path queries — "How is Person X connected to Company Y?" → X worked on Project Z which was for Company Y; relationship chain visible