Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ jobs:

- name: Typecheck
run: bun run typecheck
working-directory: apps/web

- name: Build
run: bun run build
Expand Down
65 changes: 65 additions & 0 deletions apps/web/__tests__/lib/strip-markdown.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { describe, it, expect } from "vitest";
import { stripMarkdown } from "@/lib/utils/strip-markdown";

describe("stripMarkdown", () => {
it("strips heading markers", () => {
expect(stripMarkdown("# Hello World")).toBe("Hello World");
});

it("strips bold (**) markers", () => {
expect(stripMarkdown("This is **bold** text")).toBe("This is bold text");
});

it("strips italic asterisk markers", () => {
expect(stripMarkdown("This is *italic* text")).toBe("This is italic text");
});

it("strips italic underscore markers", () => {
expect(stripMarkdown("This is _italic_ text")).toBe("This is italic text");
});

it("strips strikethrough markers", () => {
expect(stripMarkdown("This is ~~struck~~ text")).toBe(
"This is struck text",
);
});

it("strips inline code markers", () => {
expect(stripMarkdown("Use `code` here")).toBe("Use code here");
});

it("strips link syntax keeping the label", () => {
expect(stripMarkdown("Visit [Straude](https://straude.com) today")).toBe(
"Visit Straude today",
);
});

it("strips image syntax (link rule consumes the body, leaving the bang)", () => {
// The link rule runs before the image rule, so [alt](url) is captured
// first — only the leading "!" survives. Locked in to preserve current
// OG-card output byte-for-byte.
expect(stripMarkdown("Look ![alt](https://x.com/img.png) here")).toBe(
"Look !alt here",
);
});

it("strips unordered list bullet markers", () => {
expect(stripMarkdown("- first item")).toBe("first item");
});

it("strips ordered list markers", () => {
expect(stripMarkdown("1. first item")).toBe("first item");
});

it("strips blockquote markers", () => {
expect(stripMarkdown("> a quoted line")).toBe("a quoted line");
});

it("collapses double newlines to a single space", () => {
expect(stripMarkdown("para one\n\npara two")).toBe("para one para two");
});

it("collapses single newlines to a single space", () => {
expect(stripMarkdown("line one\nline two")).toBe("line one line two");
});
});
6 changes: 1 addition & 5 deletions apps/web/app/admin/components/CodexGrowthCharts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
ReferenceLine,
} from "recharts";
import { useAdminTheme } from "./AdminShell";
import { formatDateMonDay as formatDate } from "@/lib/utils/dates";

interface ModelUsageRow {
date: string;
Expand All @@ -29,11 +30,6 @@ const RANGES = [
{ key: "all", label: "All", days: Infinity },
] as const;

function formatDate(dateStr: string) {
const d = new Date(dateStr + "T00:00:00");
return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
}

function useChartTheme() {
const { theme } = useAdminTheme();
const isDark = theme === "dark";
Expand Down
6 changes: 1 addition & 5 deletions apps/web/app/admin/components/GrowthMetrics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,14 @@ import {
ResponsiveContainer,
} from "recharts";
import { useAdminTheme } from "./AdminShell";
import { formatDateMonDay as formatDate } from "@/lib/utils/dates";

interface GrowthRow {
date: string;
signups: number;
cumulative_users: number;
}

function formatDate(dateStr: string) {
const d = new Date(dateStr + "T00:00:00");
return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
}

export function GrowthMetrics({ data }: { data: GrowthRow[] }) {
const { theme } = useAdminTheme();
const isDark = theme === "dark";
Expand Down
6 changes: 1 addition & 5 deletions apps/web/app/admin/components/ModelShareChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
Legend,
} from "recharts";
import { useAdminTheme } from "./AdminShell";
import { formatDateMonDay as formatDate } from "@/lib/utils/dates";

interface RpcRow {
date: string;
Expand Down Expand Up @@ -40,11 +41,6 @@ const RANGES = [

type DayRow = Record<string, string | number>;

function formatDate(dateStr: string) {
const d = new Date(dateStr + "T00:00:00");
return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
}

function Skeleton() {
return (
<div className="admin-card">
Expand Down
6 changes: 1 addition & 5 deletions apps/web/app/admin/components/ModelUsageChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
Legend,
} from "recharts";
import { useAdminTheme } from "./AdminShell";
import { formatDateMonDay as formatDate } from "@/lib/utils/dates";

interface ModelUsageRow {
date: string;
Expand All @@ -35,11 +36,6 @@ const VIEW_MODES: { key: ViewMode; label: string }[] = [
{ key: "daily", label: "Daily" },
];

function formatDate(dateStr: string) {
const d = new Date(dateStr + "T00:00:00");
return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
}

function formatUsd(value: number) {
return `$${value.toLocaleString("en-US", { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`;
}
Expand Down
6 changes: 1 addition & 5 deletions apps/web/app/admin/components/NorthStarChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
ResponsiveContainer,
} from "recharts";
import { useAdminTheme } from "./AdminShell";
import { formatDateMonDay as formatDate } from "@/lib/utils/dates";

interface SpendRow {
date: string;
Expand All @@ -25,11 +26,6 @@ const RANGES = [
{ key: "all", label: "All", days: Infinity },
] as const;

function formatDate(dateStr: string) {
const d = new Date(dateStr + "T00:00:00");
return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
}

function formatUsd(value: number) {
return `$${value.toLocaleString("en-US", { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`;
}
Expand Down
6 changes: 1 addition & 5 deletions apps/web/app/admin/components/UserSignupsChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
ResponsiveContainer,
} from "recharts";
import { useAdminTheme } from "./AdminShell";
import { formatDateMonDay as formatDate } from "@/lib/utils/dates";

interface SignupRow {
date: string;
Expand All @@ -23,11 +24,6 @@ const RANGES = [
{ key: "all", label: "All", days: Infinity },
] as const;

function formatDate(dateStr: string) {
const d = new Date(dateStr + "T00:00:00");
return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
}

function Skeleton() {
return (
<div className="admin-card">
Expand Down
8 changes: 1 addition & 7 deletions apps/web/app/api/embed/[username]/svg/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { NextRequest } from "next/server";
import { formatTokens } from "@straude/shared/format";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check packages/shared/package.json for exports/types configuration
fd -g 'package.json' packages/shared --exec cat {}

# Verify TypeScript compiler can resolve subpath from apps/web tsconfig
fd -g 'tsconfig*.json' apps/web --exec cat {}

# Check if any declarations are already generated
fd -e d.ts packages/shared

Repository: ohong/straude

Length of output: 1929


🏁 Script executed:

# Check if dist/format.d.ts exists
fd -type f packages/shared/dist

# Check if source files exist
fd -type f packages/shared/src

# Look at the directory structure of packages/shared
tree -L 3 packages/shared 2>/dev/null || find packages/shared -type f -o -type d | head -30

# Check if there's a build output or tsconfig in packages/shared
cat packages/shared/tsconfig.json 2>/dev/null || echo "No tsconfig.json found"

Repository: ohong/straude

Length of output: 846


🏁 Script executed:

# Check if dist is in .gitignore
cat packages/shared/.gitignore 2>/dev/null || echo "No .gitignore in packages/shared"
cat .gitignore 2>/dev/null | grep -E "dist|node_modules" || echo "Checking root .gitignore"

# Check git status to see if dist is untracked or ignored
git -C packages/shared status 2>/dev/null || git status packages/shared

# Verify the exports field one more time from the initial results is accurate
cat packages/shared/package.json | grep -A 15 '"exports"'

Repository: ohong/straude

Length of output: 720


🏁 Script executed:

cd packages/shared && npm run build

Repository: ohong/straude

Length of output: 637


Actual blocker: packages/shared/tsconfig.json is missing rootDir configuration.

The build fails with TS5011:

The 'rootDir' setting must be explicitly set to this or another path to adjust your output's file layout.

The exports field in package.json is already correctly configured. The issue is that packages/shared/tsconfig.json needs an explicit rootDir setting:

  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "isolatedModules": true,
    "declaration": true,
    "outDir": "./dist",
+   "rootDir": "./src"
  },

Without this, the TypeScript compiler cannot build the package, so the declaration files are never generated, causing the TS2307 error in consuming packages.

🧰 Tools
🪛 GitHub Actions: CI

[error] 2-2: TypeScript (TS2307): Cannot find module '@straude/shared/format' or its corresponding type declarations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/api/embed/`[username]/svg/route.ts at line 2, Add an explicit
rootDir to the shared package TS config so tsc can compute output layout: open
the packages/shared/tsconfig.json and set "rootDir" to the source root (e.g.,
"src") alongside existing compilerOptions so declaration files are emitted and
imports like formatTokens (imported in
apps/web/app/api/embed/[username]/svg/route.ts) resolve; after adding rootDir,
rebuild to ensure TS5011 and downstream TS2307 errors are fixed.

import { getServiceClient } from "@/lib/supabase/service";
import { getProfileShareCardData } from "@/lib/share-assets/profile-card-data";
import {
Expand All @@ -20,13 +21,6 @@ function esc(str: string): string {
.replace(/"/g, "&quot;");
}

function formatTokens(n: number): string {
if (n >= 1_000_000_000) return `${(n / 1_000_000_000).toFixed(1)}B`;
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`;
return String(n);
}

// ── SVG gradient workaround ────────────────────────────────────────
// SVG <rect> doesn't support CSS `linear-gradient()`. We use a
// <linearGradient> def for the warm background matching the stats card.
Expand Down
4 changes: 2 additions & 2 deletions apps/web/app/api/users/me/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ function normalizeProfileLink(value: unknown): string | null {
let parsed: URL;
try {
parsed = new URL(link);
} catch {
throw new Error("Profile link must be a valid URL");
} catch (err) {
throw new Error("Profile link must be a valid URL", { cause: err });
}

if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
Expand Down
41 changes: 6 additions & 35 deletions apps/web/app/recap/[username]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { notFound } from "next/navigation";
import { createClient } from "@/lib/supabase/server";
import { getServiceClient } from "@/lib/supabase/service";
import { getRecapData } from "@/lib/utils/recap";
import { fillContributionDays, getRecapData } from "@/lib/utils/recap";
import { formatCurrency, formatTokens, getCellColor } from "@/lib/utils/format";
import type { Metadata } from "next";
import Link from "next/link";
Expand Down Expand Up @@ -61,7 +61,11 @@ export default async function PublicRecapPage({
profile.streak_freezes ?? 0
);

const allDays = fillDays(data.contribution_data, data.total_days, data.period);
const allDays = fillContributionDays(
data.contribution_data,
data.total_days,
data.period,
);

return (
<div className="flex min-h-screen flex-col items-center justify-center bg-background p-4">
Expand Down Expand Up @@ -228,36 +232,3 @@ export default async function PublicRecapPage({
);
}

function fillDays(
data: { date: string; cost_usd: number }[],
totalDays: number,
period: "week" | "month"
): { date: string; cost_usd: number }[] {
const lookup = new Map(data.map((d) => [d.date, d.cost_usd]));
const now = new Date();
let start: Date;

if (period === "week") {
const day = now.getDay();
const mondayOffset = day === 0 ? -6 : 1 - day;
start = new Date(now);
start.setDate(now.getDate() + mondayOffset);
} else {
start = new Date(now.getFullYear(), now.getMonth(), 1);
}

// Cap at today — don't render future days
const msPerDay = 86400000;
const daysSinceStart =
Math.floor((now.getTime() - start.getTime()) / msPerDay) + 1;
const cappedDays = Math.min(totalDays, daysSinceStart);

const result: { date: string; cost_usd: number }[] = [];
for (let i = 0; i < cappedDays; i++) {
const d = new Date(start);
d.setDate(start.getDate() + i);
const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
result.push({ date: key, cost_usd: lookup.get(key) ?? 0 });
}
return result;
}
22 changes: 2 additions & 20 deletions apps/web/components/app/feed/ActivityCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { Avatar } from "@/components/ui/Avatar";
import { ImageGrid } from "@/components/app/shared/ImageGrid";
import { ImageLightbox } from "@/components/app/shared/ImageLightbox";
import { ShareMenu } from "./ShareMenu";
import { prettifyModel } from "@straude/shared/models";
export { prettifyModel } from "@straude/shared/models";
import { cn } from "@/lib/utils/cn";
import { formatCurrency, formatTokens } from "@/lib/utils/format";
import { mentionsToMarkdownLinks } from "@/lib/utils/mentions";
Expand Down Expand Up @@ -54,26 +56,6 @@ function timeAgo(dateStr: string, usageDate?: string | null) {
return createdAt.toLocaleDateString("en-US", { month: "short", day: "numeric" });
}

export function prettifyModel(model: string): string {
const normalized = model.trim();
if (/claude-opus-4/i.test(normalized)) return "Claude Opus";
if (/claude-sonnet-4/i.test(normalized)) return "Claude Sonnet";
if (/claude-haiku-4/i.test(normalized)) return "Claude Haiku";
// Preserve full OpenAI model names (e.g. gpt-5.3-codex -> GPT-5.3-Codex)
if (/^gpt-/i.test(normalized)) {
return normalized
.replace(/^gpt/i, "GPT")
.replace(/-codex$/i, "-Codex");
}
if (/^o4/i.test(normalized)) return "o4";
if (/^o3/i.test(normalized)) return "o3";
// Legacy: broader Claude matching
if (normalized.includes("opus")) return "Claude Opus";
if (normalized.includes("sonnet")) return "Claude Sonnet";
if (normalized.includes("haiku")) return "Claude Haiku";
return normalized;
}

function formatModels(
models: string[] | undefined,
breakdown: ModelBreakdownEntry[] | null | undefined,
Expand Down
8 changes: 1 addition & 7 deletions apps/web/components/app/profile/ContributionGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { useState, useCallback } from "react";
import { cn } from "@/lib/utils/cn";
import { formatCurrency, getCellColor } from "@/lib/utils/format";
import { formatDateKey } from "@/lib/utils/dates";
import { getHeatmapLegend } from "@/lib/share-assets/heatmap";
import type { ContributionDay } from "@/types";

Expand All @@ -14,13 +15,6 @@ const STEP = CELL_SIZE + GAP;
const DAYS = 7;
const MONTH_LABEL_HEIGHT = 16;

function formatDateKey(d: Date): string {
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${y}-${m}-${day}`;
}

function formatDateLabel(d: Date): string {
return d.toLocaleDateString("en-US", {
month: "long",
Expand Down
8 changes: 3 additions & 5 deletions apps/web/components/app/profile/ProfileSharePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@
import Image from "next/image";
import { useEffect, useMemo, useState } from "react";
import { Check, Copy, Download, ImageIcon } from "lucide-react";
import {
buildProfileIntentUrl,
buildProfileShareUrl,
} from "@/lib/utils/profile-share";
import { buildProfileIntentUrl } from "@/lib/utils/profile-share";

export function ProfileSharePanel({
username,
Expand Down Expand Up @@ -43,7 +40,8 @@ export function ProfileSharePanel({

useEffect(() => {
setPublicUrl(
shareUrlOverride ?? buildProfileShareUrl(window.location.origin, username)
shareUrlOverride ??
new URL(`/stats/${username}`, window.location.origin).toString()
);
setSupportsClipboardImage(
typeof ClipboardItem !== "undefined" &&
Expand Down
Loading
Loading