[TERMINAL · SKILLS]
> mounting /skills...
> indexing skill manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Use Cases/Build an AI Meeting Summarizer with Action Items

Build an AI Meeting Summarizer with Action Items

Auto-generate meeting summaries, extract action items with owners and deadlines, and sync to project management tools — saving 5 hours/week per team and ensuring nothing falls through the cracks.

Development#typescript#tsconfig#type-checking#type-safety#javascript-migration
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 7 skills

Avg quality 94/100·All SAFE
>

typescript

v1.0.0

Sets up and repairs TypeScript projects: picks tsconfig.json settings for a Node.js service, an npm library or a bundled web app, runs type-checking in CI, and replaces `any` and unsafe casts with types the compiler can verify. Use when someone asks to "set up TypeScript", "fix this tsconfig", "fix a TypeScript error", "upgrade to TypeScript 6 or 7", "migrate JavaScript to TypeScript", "publish types for a package", "run .ts files in Node" or "make this type-safe". Covers the changed defaults in TypeScript 6.0 and 7.0, module and moduleResolution, strict mode, project references, declaration output, narrowing, satisfies and common compiler errors.

93/100 quality
7.30× impact
SAFE· stale score
View skill
>

ai-sdk

v1.1.0

The Vercel AI SDK is a TypeScript toolkit for calling LLMs from Node.js, Next.js, Nuxt, SvelteKit and edge runtimes through one API. It covers text generation, streaming, structured output with Zod, tool calling, agents (ToolLoopAgent) and chat UIs with useChat. Use when asked to "add AI to my Next.js app", "stream a chat response", "get typed JSON from an LLM", "switch between OpenAI, Anthropic and Gemini", or "build a tool-calling agent".

77/100 quality
5.47× impact
SAFE· stale score
View skill
>

bull-mq

v1.1.0

Runs background jobs in Node.js with BullMQ, a job queue library backed by Redis (or PostgreSQL since v6): delayed and prioritized jobs, retries with backoff, rate limiting, cron-style job schedulers, parent-child flows and concurrency control. Use when a user asks to add a background worker, queue emails or webhooks, schedule recurring jobs, retry failed jobs, rate limit an API client, or migrate from Bull or from BullMQ 5 repeatable jobs.

100/100 quality
1.19× impact
SAFE· stale score
View skill
>

redis

v1.1.0

Redis is an in-memory data store used as a cache, session store, message broker and counter service. Use when adding a cache layer, storing sessions, rate limiting an API, running pub/sub or Streams with consumer groups, taking a distributed lock, building a leaderboard, or configuring Redis for production (persistence, maxmemory, eviction).

93/100 quality
1.79× impact
SAFE· stale score
View skill
>

postgresql

v1.1.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.

100/100 quality
5.15× impact
SAFE· stale score
View skill
>

zod

v1.1.0

Zod is a TypeScript-first schema library that validates data at runtime and infers static types from the same schema. Use when validating API request bodies, form input, environment variables or config files, parsing untrusted JSON, or migrating from Zod 3 to Zod 4. Covers z.object, safeParse, z.infer, coercion, discriminated unions, refinements, JSON Schema output and the v4 API changes.

93/100 quality
3.87× impact
SAFE· stale score
View skill
>

hono

v1.1.0

Hono is a small web framework built on Web Standards (Request/Response) that runs unchanged on Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda and Vercel. Use it when the user asks to build a Hono API, add Zod validation, JWT auth, CORS or middleware, use the type-safe RPC client (hc), render JSX, stream responses, or deploy one codebase to several runtimes.

100/100 quality
6.20× impact
SAFE· stale score
View skill
$

The Problem

A 50-person company has 200 meetings/week. Meeting notes are either not taken, taken poorly, or taken by someone who then can't participate. Action items live in people's heads — 40% are forgotten. "Didn't we discuss this last week?" is said daily. The PM spends 5 hours/week manually writing summaries and chasing action items. When someone misses a meeting, they have no way to catch up except asking a colleague to rehash it.

Step 1: Transcript Processor

typescript
// src/meetings/processor.ts
import { generateObject } from 'ai';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';

const MeetingSummary = z.object({
  title: z.string(),
  date: z.string(),
  duration: z.string(),
  participants: z.array(z.string()),
  summary: z.string().max(500),
  keyTopics: z.array(z.object({
    topic: z.string(),
    discussion: z.string(),
    outcome: z.enum(['decided', 'needs_followup', 'informational', 'tabled']),
    decision: z.string().optional(),
  })),
  actionItems: z.array(z.object({
    description: z.string(),
    owner: z.string(),
    deadline: z.string().optional(),
    priority: z.enum(['high', 'medium', 'low']),
    context: z.string(), // why this action item exists
  })),
  openQuestions: z.array(z.string()),
  nextSteps: z.array(z.string()),
  sentiment: z.enum(['productive', 'neutral', 'contentious', 'unfocused']),
});

export async function summarizeMeeting(transcript: string, metadata: {
  meetingTitle?: string;
  participants: string[];
  scheduledDuration: string;
}): Promise<z.infer<typeof MeetingSummary>> {
  const { object } = await generateObject({
    model: openai('gpt-4o'),
    schema: MeetingSummary,
    prompt: `Summarize this meeting transcript. Extract every action item with a clear owner.

Meeting: ${metadata.meetingTitle ?? 'Untitled Meeting'}
Scheduled duration: ${metadata.scheduledDuration}
Participants: ${metadata.participants.join(', ')}

Transcript:
${transcript}

Rules:
- Summary should be 2-3 sentences, high-level
- Each topic should capture the key discussion points and outcome
- Action items MUST have an owner (the person who said "I'll do X" or was assigned)
- If a deadline was mentioned, include it
- Open questions are things raised but not resolved
- Be objective — don't editorialize
- If people talked over each other or went in circles, mark sentiment as "unfocused"`,
  });

  return object;
}

Step 2: Action Item Tracker

typescript
// src/meetings/action-tracker.ts
import { Pool } from 'pg';
import { Redis } from 'ioredis';

const db = new Pool({ connectionString: process.env.DATABASE_URL });
const redis = new Redis(process.env.REDIS_URL!);

export async function saveActionItems(
  meetingId: string,
  actionItems: Array<{
    description: string;
    owner: string;
    deadline?: string;
    priority: string;
    context: string;
  }>
): Promise<void> {
  for (const item of actionItems) {
    const id = crypto.randomUUID();
    await db.query(`
      INSERT INTO action_items (id, meeting_id, description, owner, deadline, priority, context, status, created_at)
      VALUES ($1, $2, $3, $4, $5, $6, $7, 'open', NOW())
    `, [id, meetingId, item.description, item.owner, item.deadline, item.priority, item.context]);
  }
}

// Weekly digest: which action items are overdue?
export async function getOverdueItems(): Promise<Array<{
  owner: string;
  items: Array<{ description: string; meetingTitle: string; deadline: string; daysPast: number }>;
}>> {
  const { rows } = await db.query(`
    SELECT ai.owner, ai.description, ai.deadline, m.title as meeting_title,
           EXTRACT(DAY FROM NOW() - ai.deadline::date) as days_past
    FROM action_items ai
    JOIN meetings m ON ai.meeting_id = m.id
    WHERE ai.status = 'open' AND ai.deadline IS NOT NULL AND ai.deadline::date < NOW()
    ORDER BY ai.owner, days_past DESC
  `);

  const byOwner = new Map<string, any[]>();
  for (const row of rows) {
    if (!byOwner.has(row.owner)) byOwner.set(row.owner, []);
    byOwner.get(row.owner)!.push({
      description: row.description,
      meetingTitle: row.meeting_title,
      deadline: row.deadline,
      daysPast: parseInt(row.days_past),
    });
  }

  return [...byOwner.entries()].map(([owner, items]) => ({ owner, items }));
}

// Sync to Jira/Linear
export async function syncToProjectManagement(
  actionItem: { description: string; owner: string; deadline?: string; priority: string }
): Promise<void> {
  // Create task in Linear/Jira
  if (process.env.LINEAR_API_KEY) {
    await fetch('https://api.linear.app/graphql', {
      method: 'POST',
      headers: {
        Authorization: process.env.LINEAR_API_KEY!,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        query: `mutation { issueCreate(input: {
          title: "${actionItem.description}",
          assigneeId: "${actionItem.owner}",
          priority: ${actionItem.priority === 'high' ? 1 : actionItem.priority === 'medium' ? 2 : 3},
          ${actionItem.deadline ? `dueDate: "${actionItem.deadline}"` : ''}
        }) { success } }`,
      }),
    });
  }
}

Step 3: API and Webhook

typescript
// src/api/meetings.ts
import { Hono } from 'hono';
import { summarizeMeeting } from '../meetings/processor';
import { saveActionItems } from '../meetings/action-tracker';
import { Pool } from 'pg';

const app = new Hono();
const db = new Pool({ connectionString: process.env.DATABASE_URL });

// Webhook from Zoom/Google Meet/Fireflies
app.post('/v1/meetings/transcript', async (c) => {
  const { transcript, meetingTitle, participants, duration, recordingUrl } = await c.req.json();
  const meetingId = crypto.randomUUID();

  // Save raw transcript
  await db.query(`
    INSERT INTO meetings (id, title, participants, duration, transcript, recording_url, created_at)
    VALUES ($1, $2, $3, $4, $5, $6, NOW())
  `, [meetingId, meetingTitle, participants, duration, transcript, recordingUrl]);

  // Generate summary
  const summary = await summarizeMeeting(transcript, {
    meetingTitle,
    participants,
    scheduledDuration: duration,
  });

  await db.query(`UPDATE meetings SET summary = $1 WHERE id = $2`, [JSON.stringify(summary), meetingId]);

  // Save and sync action items
  await saveActionItems(meetingId, summary.actionItems);
  for (const item of summary.actionItems) {
    await syncToProjectManagement(item).catch(() => {});
  }

  return c.json({ meetingId, summary });
});

// Search past meetings
app.get('/v1/meetings/search', async (c) => {
  const query = c.req.query('q');
  const { rows } = await db.query(`
    SELECT id, title, created_at, summary->>'summary' as summary
    FROM meetings
    WHERE to_tsvector('english', transcript) @@ plainto_tsquery('english', $1)
    ORDER BY created_at DESC LIMIT 20
  `, [query]);

  return c.json({ results: rows });
});

export default app;

import { syncToProjectManagement } from '../meetings/action-tracker';

Results

  • Note-taking: fully automated — nobody manually writes meeting notes
  • Action item capture: 95% (was ~60% manually, 40% forgotten)
  • PM time saved: 5 hours/week freed from summary writing
  • Overdue tracking: weekly digest catches forgotten items — completion rate up 35%
  • "What did we decide?": searchable meeting archive answers in seconds
  • Missed meeting catch-up: read summary in 2 minutes instead of 30-minute rehash
  • Project management sync: action items auto-create Linear/Jira tasks

Related use cases