heartbeads 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/BUILD_ID +1 -0
- package/.next/app-build-manifest.json +49 -0
- package/.next/app-path-routes-manifest.json +1 -0
- package/.next/build-manifest.json +32 -0
- package/.next/export-marker.json +1 -0
- package/.next/images-manifest.json +1 -0
- package/.next/next-minimal-server.js.nft.json +1 -0
- package/.next/next-server.js.nft.json +1 -0
- package/.next/package.json +1 -0
- package/.next/prerender-manifest.json +1 -0
- package/.next/react-loadable-manifest.json +8 -0
- package/.next/required-server-files.json +1 -0
- package/.next/routes-manifest.json +1 -0
- package/.next/server/app/_not-found/page.js +1 -0
- package/.next/server/app/_not-found/page.js.nft.json +1 -0
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
- package/.next/server/app/_not-found.html +1 -0
- package/.next/server/app/_not-found.meta +6 -0
- package/.next/server/app/_not-found.rsc +9 -0
- package/.next/server/app/api/auth/route.js +1 -0
- package/.next/server/app/api/auth/route.js.nft.json +1 -0
- package/.next/server/app/api/beads/route.js +8 -0
- package/.next/server/app/api/beads/route.js.nft.json +1 -0
- package/.next/server/app/api/beads/stream/route.js +10 -0
- package/.next/server/app/api/beads/stream/route.js.nft.json +1 -0
- package/.next/server/app/api/beads.body +1 -0
- package/.next/server/app/api/beads.meta +1 -0
- package/.next/server/app/api/config/route.js +8 -0
- package/.next/server/app/api/config/route.js.nft.json +1 -0
- package/.next/server/app/api/docs/page.js +120 -0
- package/.next/server/app/api/docs/page.js.nft.json +1 -0
- package/.next/server/app/api/docs/page_client-reference-manifest.js +1 -0
- package/.next/server/app/api/docs.html +120 -0
- package/.next/server/app/api/docs.meta +5 -0
- package/.next/server/app/api/docs.rsc +70 -0
- package/.next/server/app/api/login/route.js +1 -0
- package/.next/server/app/api/login/route.js.nft.json +1 -0
- package/.next/server/app/api/logout/route.js +1 -0
- package/.next/server/app/api/logout/route.js.nft.json +1 -0
- package/.next/server/app/api/oauth/callback/route.js +1 -0
- package/.next/server/app/api/oauth/callback/route.js.nft.json +1 -0
- package/.next/server/app/api/oauth/client-metadata.json/route.js +1 -0
- package/.next/server/app/api/oauth/client-metadata.json/route.js.nft.json +1 -0
- package/.next/server/app/api/oauth/jwks.json/route.js +1 -0
- package/.next/server/app/api/oauth/jwks.json/route.js.nft.json +1 -0
- package/.next/server/app/api/records/route.js +1 -0
- package/.next/server/app/api/records/route.js.nft.json +1 -0
- package/.next/server/app/api/status/route.js +1 -0
- package/.next/server/app/api/status/route.js.nft.json +1 -0
- package/.next/server/app/api/v1/graph/route.js +1 -0
- package/.next/server/app/api/v1/graph/route.js.nft.json +1 -0
- package/.next/server/app/api/v1/issues/[id]/route.js +1 -0
- package/.next/server/app/api/v1/issues/[id]/route.js.nft.json +1 -0
- package/.next/server/app/api/v1/ready/route.js +1 -0
- package/.next/server/app/api/v1/ready/route.js.nft.json +1 -0
- package/.next/server/app/index.html +1 -0
- package/.next/server/app/index.meta +5 -0
- package/.next/server/app/index.rsc +9 -0
- package/.next/server/app/login/page.js +1 -0
- package/.next/server/app/login/page.js.nft.json +1 -0
- package/.next/server/app/login/page_client-reference-manifest.js +1 -0
- package/.next/server/app/login.html +1 -0
- package/.next/server/app/login.meta +5 -0
- package/.next/server/app/login.rsc +9 -0
- package/.next/server/app/opengraph-image.png/route.js +1 -0
- package/.next/server/app/opengraph-image.png/route.js.nft.json +1 -0
- package/.next/server/app/opengraph-image.png.body +0 -0
- package/.next/server/app/opengraph-image.png.meta +1 -0
- package/.next/server/app/page.js +24 -0
- package/.next/server/app/page.js.nft.json +1 -0
- package/.next/server/app/page_client-reference-manifest.js +1 -0
- package/.next/server/app/twitter-image.png/route.js +1 -0
- package/.next/server/app/twitter-image.png/route.js.nft.json +1 -0
- package/.next/server/app/twitter-image.png.body +0 -0
- package/.next/server/app/twitter-image.png.meta +1 -0
- package/.next/server/app-paths-manifest.json +22 -0
- package/.next/server/chunks/247.js +12 -0
- package/.next/server/chunks/29.js +1 -0
- package/.next/server/chunks/343.js +1 -0
- package/.next/server/chunks/460.js +12 -0
- package/.next/server/chunks/533.js +38 -0
- package/.next/server/chunks/542.js +27 -0
- package/.next/server/chunks/590.js +6 -0
- package/.next/server/chunks/615.js +15 -0
- package/.next/server/chunks/696.js +25 -0
- package/.next/server/chunks/719.js +2 -0
- package/.next/server/chunks/739.js +1 -0
- package/.next/server/chunks/950.js +2 -0
- package/.next/server/chunks/font-manifest.json +1 -0
- package/.next/server/edge-runtime-webpack.js +2 -0
- package/.next/server/edge-runtime-webpack.js.map +1 -0
- package/.next/server/font-manifest.json +1 -0
- package/.next/server/functions-config-manifest.json +1 -0
- package/.next/server/interception-route-rewrite-manifest.js +1 -0
- package/.next/server/middleware-build-manifest.js +1 -0
- package/.next/server/middleware-manifest.json +32 -0
- package/.next/server/middleware-react-loadable-manifest.js +1 -0
- package/.next/server/middleware.js +14 -0
- package/.next/server/middleware.js.map +1 -0
- package/.next/server/next-font-manifest.js +1 -0
- package/.next/server/next-font-manifest.json +1 -0
- package/.next/server/pages/404.html +1 -0
- package/.next/server/pages/500.html +1 -0
- package/.next/server/pages/_app.js +1 -0
- package/.next/server/pages/_app.js.nft.json +1 -0
- package/.next/server/pages/_document.js +1 -0
- package/.next/server/pages/_document.js.nft.json +1 -0
- package/.next/server/pages/_error.js +1 -0
- package/.next/server/pages/_error.js.nft.json +1 -0
- package/.next/server/pages-manifest.json +1 -0
- package/.next/server/server-reference-manifest.js +1 -0
- package/.next/server/server-reference-manifest.json +1 -0
- package/.next/server/webpack-runtime.js +1 -0
- package/.next/static/chunks/149.a3e3a5dc03e21086.js +1 -0
- package/.next/static/chunks/2200cc46-7c93a0e00b0bb825.js +1 -0
- package/.next/static/chunks/788-aa413085174e935a.js +1 -0
- package/.next/static/chunks/945-3ff1d381a0af1ecd.js +2 -0
- package/.next/static/chunks/971-bb44d52bcd9ee2a9.js +1 -0
- package/.next/static/chunks/app/_not-found/page-200b7a7a6cfc29df.js +1 -0
- package/.next/static/chunks/app/api/docs/page-1dc18f40154cdce6.js +1 -0
- package/.next/static/chunks/app/layout-13e3cdaaa416edb6.js +1 -0
- package/.next/static/chunks/app/login/page-60d930d64f021753.js +1 -0
- package/.next/static/chunks/app/not-found-ae1139bed2018dd8.js +1 -0
- package/.next/static/chunks/app/page-583300dd8af66e5a.js +1 -0
- package/.next/static/chunks/framework-6e06c675866dc992.js +1 -0
- package/.next/static/chunks/main-app-8b0c4a1007dbb7f4.js +1 -0
- package/.next/static/chunks/main-e680fb049d7426e1.js +1 -0
- package/.next/static/chunks/pages/_app-0c3037849002a4aa.js +1 -0
- package/.next/static/chunks/pages/_error-a647cd2c75dc4dc7.js +1 -0
- package/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/.next/static/chunks/webpack-117444a4bfe51057.js +1 -0
- package/.next/static/css/8c1b520a38ba4ccd.css +3 -0
- package/.next/static/vFM69sDrBUf_9ULwPmVAE/_buildManifest.js +1 -0
- package/.next/static/vFM69sDrBUf_9ULwPmVAE/_ssgManifest.js +1 -0
- package/README.md +389 -0
- package/app/api/auth/route.ts +103 -0
- package/app/api/beads/route.ts +27 -0
- package/app/api/beads/stream/route.ts +83 -0
- package/app/api/config/route.ts +48 -0
- package/app/api/docs/page.tsx +497 -0
- package/app/api/login/route.ts +42 -0
- package/app/api/logout/route.ts +14 -0
- package/app/api/oauth/callback/route.ts +97 -0
- package/app/api/oauth/client-metadata.json/route.ts +33 -0
- package/app/api/oauth/jwks.json/route.ts +32 -0
- package/app/api/records/route.ts +168 -0
- package/app/api/status/route.ts +25 -0
- package/app/api/v1/graph/route.ts +251 -0
- package/app/api/v1/issues/[id]/route.ts +158 -0
- package/app/api/v1/ready/route.ts +229 -0
- package/app/globals.css +230 -0
- package/app/layout.tsx +51 -0
- package/app/login/page.tsx +164 -0
- package/app/not-found.tsx +91 -0
- package/app/opengraph-image.png +0 -0
- package/app/page.tsx +2041 -0
- package/app/twitter-image.png +0 -0
- package/bin/heartbeads.mjs +225 -0
- package/components/ActivityItem.tsx +326 -0
- package/components/ActivityOverlay.tsx +125 -0
- package/components/ActivityPanel.tsx +345 -0
- package/components/AllCommentsPanel.tsx +270 -0
- package/components/AuthButton.tsx +202 -0
- package/components/BeadTooltip.tsx +246 -0
- package/components/BeadsGraph.tsx +2493 -0
- package/components/BeadsLogo.tsx +94 -0
- package/components/CommentTooltip.tsx +338 -0
- package/components/ContextMenu.tsx +272 -0
- package/components/DescriptionModal.tsx +595 -0
- package/components/GraphStats.tsx +121 -0
- package/components/HeartIcon.tsx +33 -0
- package/components/HelpPanel.tsx +339 -0
- package/components/MobileActionSheet.tsx +255 -0
- package/components/NodeDetail.tsx +793 -0
- package/components/SettingsModal.tsx +315 -0
- package/components/StatusLegend.tsx +99 -0
- package/components/TimelineBar.tsx +116 -0
- package/components/TutorialOverlay.tsx +235 -0
- package/hooks/useBeadsComments.ts +81 -0
- package/hooks/useIsMobile.ts +19 -0
- package/lib/activity.ts +377 -0
- package/lib/agent.ts +29 -0
- package/lib/api-helpers.ts +46 -0
- package/lib/auth/client.ts +221 -0
- package/lib/auth.tsx +159 -0
- package/lib/comments.ts +413 -0
- package/lib/diff-beads.ts +128 -0
- package/lib/discover.ts +228 -0
- package/lib/env.ts +33 -0
- package/lib/gate.ts +55 -0
- package/lib/parse-beads.ts +234 -0
- package/lib/session.ts +52 -0
- package/lib/settings.ts +42 -0
- package/lib/timeline.ts +138 -0
- package/lib/tts.ts +397 -0
- package/lib/types.ts +271 -0
- package/lib/utils.ts +48 -0
- package/lib/watch-beads.ts +97 -0
- package/next.config.mjs +4 -0
- package/package.json +81 -0
- package/postcss.config.mjs +9 -0
- package/public/image.png +0 -0
- package/scripts/generate-jwk.js +38 -0
- package/tailwind.config.ts +41 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { env } from "@/lib/env";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
|
|
6
|
+
export async function GET() {
|
|
7
|
+
const publicUrl = env.PUBLIC_URL;
|
|
8
|
+
const url = publicUrl || `http://127.0.0.1:${env.PORT}`;
|
|
9
|
+
const isConfidential = !!publicUrl && !!env.ATPROTO_JWK_PRIVATE;
|
|
10
|
+
|
|
11
|
+
const metadata: Record<string, unknown> = {
|
|
12
|
+
client_name: "Beads Map",
|
|
13
|
+
client_uri: url,
|
|
14
|
+
dpop_bound_access_tokens: true,
|
|
15
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
16
|
+
response_types: ["code"],
|
|
17
|
+
scope: "atproto transition:generic",
|
|
18
|
+
application_type: "web",
|
|
19
|
+
redirect_uris: [`${url}/api/oauth/callback`],
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
if (isConfidential) {
|
|
23
|
+
metadata.client_id = `${publicUrl}/api/oauth/client-metadata.json`;
|
|
24
|
+
metadata.token_endpoint_auth_method = "private_key_jwt";
|
|
25
|
+
metadata.token_endpoint_auth_signing_alg = "ES256";
|
|
26
|
+
metadata.jwks_uri = `${publicUrl}/api/oauth/jwks.json`;
|
|
27
|
+
} else {
|
|
28
|
+
metadata.client_id = `http://localhost?redirect_uri=${encodeURIComponent(`${url}/api/oauth/callback`)}&scope=${encodeURIComponent("atproto transition:generic")}`;
|
|
29
|
+
metadata.token_endpoint_auth_method = "none";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return NextResponse.json(metadata);
|
|
33
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { getJwks } from "@/lib/auth/client";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
|
|
6
|
+
export async function GET() {
|
|
7
|
+
try {
|
|
8
|
+
const jwks = await getJwks();
|
|
9
|
+
|
|
10
|
+
if (!jwks) {
|
|
11
|
+
return NextResponse.json(
|
|
12
|
+
{ keys: [] },
|
|
13
|
+
{
|
|
14
|
+
headers: {
|
|
15
|
+
"Content-Type": "application/json",
|
|
16
|
+
"Cache-Control": "public, max-age=3600",
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return NextResponse.json(jwks, {
|
|
23
|
+
headers: {
|
|
24
|
+
"Content-Type": "application/json",
|
|
25
|
+
"Cache-Control": "public, max-age=3600",
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error("Failed to get JWKS:", error);
|
|
30
|
+
return NextResponse.json({ keys: [] }, { status: 500 });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { getAuthenticatedAgent } from "@/lib/agent";
|
|
3
|
+
import { getSession } from "@/lib/session";
|
|
4
|
+
|
|
5
|
+
export const dynamic = "force-dynamic";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* POST /api/records — Create a new record
|
|
9
|
+
* Body: { collection, rkey?, record }
|
|
10
|
+
*/
|
|
11
|
+
export async function POST(request: NextRequest) {
|
|
12
|
+
try {
|
|
13
|
+
const session = await getSession();
|
|
14
|
+
if (!session.did) {
|
|
15
|
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const agent = await getAuthenticatedAgent();
|
|
19
|
+
if (!agent) {
|
|
20
|
+
return NextResponse.json(
|
|
21
|
+
{ error: "Failed to authenticate" },
|
|
22
|
+
{ status: 401 }
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const body = await request.json();
|
|
27
|
+
const { collection, rkey, record } = body;
|
|
28
|
+
|
|
29
|
+
if (!collection || typeof collection !== "string") {
|
|
30
|
+
return NextResponse.json(
|
|
31
|
+
{ error: "Collection is required" },
|
|
32
|
+
{ status: 400 }
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
if (!record || typeof record !== "object") {
|
|
36
|
+
return NextResponse.json(
|
|
37
|
+
{ error: "Record is required" },
|
|
38
|
+
{ status: 400 }
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const res = await agent.com.atproto.repo.createRecord({
|
|
43
|
+
repo: session.did,
|
|
44
|
+
collection,
|
|
45
|
+
rkey: rkey || undefined,
|
|
46
|
+
record,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return NextResponse.json({
|
|
50
|
+
success: true,
|
|
51
|
+
uri: res.data.uri,
|
|
52
|
+
cid: res.data.cid,
|
|
53
|
+
});
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error("Failed to create record:", error);
|
|
56
|
+
const message =
|
|
57
|
+
error instanceof Error ? error.message : "Failed to create record";
|
|
58
|
+
return NextResponse.json({ error: message }, { status: 500 });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* PUT /api/records — Update an existing record
|
|
64
|
+
* Body: { collection, rkey, record }
|
|
65
|
+
*/
|
|
66
|
+
export async function PUT(request: NextRequest) {
|
|
67
|
+
try {
|
|
68
|
+
const session = await getSession();
|
|
69
|
+
if (!session.did) {
|
|
70
|
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const agent = await getAuthenticatedAgent();
|
|
74
|
+
if (!agent) {
|
|
75
|
+
return NextResponse.json(
|
|
76
|
+
{ error: "Failed to authenticate" },
|
|
77
|
+
{ status: 401 }
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const body = await request.json();
|
|
82
|
+
const { collection, rkey, record } = body;
|
|
83
|
+
|
|
84
|
+
if (!collection || typeof collection !== "string") {
|
|
85
|
+
return NextResponse.json(
|
|
86
|
+
{ error: "Collection is required" },
|
|
87
|
+
{ status: 400 }
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
if (!rkey || typeof rkey !== "string") {
|
|
91
|
+
return NextResponse.json(
|
|
92
|
+
{ error: "Record key is required" },
|
|
93
|
+
{ status: 400 }
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
if (!record || typeof record !== "object") {
|
|
97
|
+
return NextResponse.json(
|
|
98
|
+
{ error: "Record is required" },
|
|
99
|
+
{ status: 400 }
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
await agent.com.atproto.repo.putRecord({
|
|
104
|
+
repo: session.did,
|
|
105
|
+
collection,
|
|
106
|
+
rkey,
|
|
107
|
+
record,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return NextResponse.json({ success: true });
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.error("Failed to update record:", error);
|
|
113
|
+
const message =
|
|
114
|
+
error instanceof Error ? error.message : "Failed to update record";
|
|
115
|
+
return NextResponse.json({ error: message }, { status: 500 });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* DELETE /api/records?collection=...&rkey=...
|
|
121
|
+
* Deletes a record from the authenticated user's repo.
|
|
122
|
+
*/
|
|
123
|
+
export async function DELETE(request: NextRequest) {
|
|
124
|
+
try {
|
|
125
|
+
const session = await getSession();
|
|
126
|
+
if (!session.did) {
|
|
127
|
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const agent = await getAuthenticatedAgent();
|
|
131
|
+
if (!agent) {
|
|
132
|
+
return NextResponse.json(
|
|
133
|
+
{ error: "Failed to authenticate" },
|
|
134
|
+
{ status: 401 }
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const { searchParams } = new URL(request.url);
|
|
139
|
+
const collection = searchParams.get("collection");
|
|
140
|
+
const rkey = searchParams.get("rkey");
|
|
141
|
+
|
|
142
|
+
if (!collection) {
|
|
143
|
+
return NextResponse.json(
|
|
144
|
+
{ error: "Collection is required" },
|
|
145
|
+
{ status: 400 }
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
if (!rkey) {
|
|
149
|
+
return NextResponse.json(
|
|
150
|
+
{ error: "Record key is required" },
|
|
151
|
+
{ status: 400 }
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
await agent.com.atproto.repo.deleteRecord({
|
|
156
|
+
repo: session.did,
|
|
157
|
+
collection,
|
|
158
|
+
rkey,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
return NextResponse.json({ success: true });
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.error("Failed to delete record:", error);
|
|
164
|
+
const message =
|
|
165
|
+
error instanceof Error ? error.message : "Failed to delete record";
|
|
166
|
+
return NextResponse.json({ error: message }, { status: 500 });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { getSession } from "@/lib/session";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
|
|
6
|
+
export async function GET() {
|
|
7
|
+
try {
|
|
8
|
+
const session = await getSession();
|
|
9
|
+
|
|
10
|
+
if (!session.did) {
|
|
11
|
+
return NextResponse.json({ authenticated: false });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return NextResponse.json({
|
|
15
|
+
authenticated: true,
|
|
16
|
+
did: session.did,
|
|
17
|
+
handle: session.handle,
|
|
18
|
+
displayName: session.displayName,
|
|
19
|
+
avatar: session.avatar,
|
|
20
|
+
});
|
|
21
|
+
} catch (error) {
|
|
22
|
+
console.error("Status check failed:", error);
|
|
23
|
+
return NextResponse.json({ authenticated: false });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/v1/graph — Full project snapshot for AI agents.
|
|
3
|
+
*
|
|
4
|
+
* Returns the entire beads graph with issues, dependencies, comments,
|
|
5
|
+
* claims, and activity in a single JSON response. No auth required.
|
|
6
|
+
*
|
|
7
|
+
* Query params:
|
|
8
|
+
* ?status=open,in_progress Filter issues by status (comma-separated)
|
|
9
|
+
* ?priority=0,1 Filter by priority (comma-separated)
|
|
10
|
+
* ?prefix=beads-map Filter by repo prefix
|
|
11
|
+
* ?include=comments,activity Opt-in fields (default: all)
|
|
12
|
+
* ?limit=50 Activity feed cap (default 50, max 200)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { loadBeadsData } from "@/lib/parse-beads";
|
|
16
|
+
import { discoverBeadsDir, getRepoUrls } from "@/lib/discover";
|
|
17
|
+
import { fetchBeadsComments, getClaimedNodes } from "@/lib/comments";
|
|
18
|
+
import type { BeadsComment, ClaimInfo } from "@/lib/comments";
|
|
19
|
+
import { buildHistoricalFeed } from "@/lib/activity";
|
|
20
|
+
import type { GraphNode } from "@/lib/types";
|
|
21
|
+
import { jsonResponse, errorResponse, OPTIONS } from "@/lib/api-helpers";
|
|
22
|
+
import { readFileSync, existsSync } from "fs";
|
|
23
|
+
import { join } from "path";
|
|
24
|
+
import { parse as parseYaml } from "yaml";
|
|
25
|
+
|
|
26
|
+
export const dynamic = "force-dynamic";
|
|
27
|
+
export { OPTIONS };
|
|
28
|
+
|
|
29
|
+
// Read version from package.json at module load time
|
|
30
|
+
let heartbeadsVersion = "0.0.0";
|
|
31
|
+
try {
|
|
32
|
+
const pkg = JSON.parse(
|
|
33
|
+
readFileSync(join(process.cwd(), "package.json"), "utf-8")
|
|
34
|
+
);
|
|
35
|
+
heartbeadsVersion = pkg.version || heartbeadsVersion;
|
|
36
|
+
} catch {
|
|
37
|
+
// ignore
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Serialize a BeadsComment tree for API output */
|
|
41
|
+
function serializeComment(c: BeadsComment): Record<string, unknown> {
|
|
42
|
+
return {
|
|
43
|
+
author: {
|
|
44
|
+
handle: c.handle,
|
|
45
|
+
did: c.did,
|
|
46
|
+
...(c.displayName ? { displayName: c.displayName } : {}),
|
|
47
|
+
},
|
|
48
|
+
text: c.text,
|
|
49
|
+
createdAt: c.createdAt,
|
|
50
|
+
likes: c.likes.length,
|
|
51
|
+
replies: c.replies.map(serializeComment),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Serialize a claim for API output */
|
|
56
|
+
function serializeClaim(claim: ClaimInfo) {
|
|
57
|
+
return {
|
|
58
|
+
handle: claim.handle,
|
|
59
|
+
did: claim.did,
|
|
60
|
+
...(claim.displayName ? { displayName: claim.displayName } : {}),
|
|
61
|
+
claimed_at: claim.claimedAt,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function GET(request: Request) {
|
|
66
|
+
try {
|
|
67
|
+
const discovery = discoverBeadsDir();
|
|
68
|
+
const url = new URL(request.url);
|
|
69
|
+
const params = url.searchParams;
|
|
70
|
+
|
|
71
|
+
// Parse query params
|
|
72
|
+
const statusFilter = params.get("status")?.split(",").filter(Boolean);
|
|
73
|
+
const priorityFilter = params
|
|
74
|
+
.get("priority")
|
|
75
|
+
?.split(",")
|
|
76
|
+
.map(Number)
|
|
77
|
+
.filter((n) => !isNaN(n));
|
|
78
|
+
const prefixFilter = params.get("prefix") || null;
|
|
79
|
+
const includeParam = params.get("include");
|
|
80
|
+
const limit = Math.min(
|
|
81
|
+
Math.max(parseInt(params.get("limit") || "50") || 50, 1),
|
|
82
|
+
200
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// Determine what to include (default: everything)
|
|
86
|
+
let includeComments = true;
|
|
87
|
+
let includeActivity = true;
|
|
88
|
+
if (includeParam !== null) {
|
|
89
|
+
const includes = includeParam.split(",").filter(Boolean);
|
|
90
|
+
includeComments = includes.includes("comments");
|
|
91
|
+
includeActivity = includes.includes("activity");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Load beads data + config in parallel with optional comments
|
|
95
|
+
const beadsPromise = Promise.resolve(loadBeadsData(discovery.beadsDir));
|
|
96
|
+
const commentsPromise = includeComments
|
|
97
|
+
? fetchBeadsComments().catch((err) => {
|
|
98
|
+
console.error("[api/v1/graph] Failed to fetch comments:", err);
|
|
99
|
+
return null;
|
|
100
|
+
})
|
|
101
|
+
: Promise.resolve(null);
|
|
102
|
+
|
|
103
|
+
const [beadsData, commentsResult] = await Promise.all([
|
|
104
|
+
beadsPromise,
|
|
105
|
+
commentsPromise,
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
// Build project metadata
|
|
109
|
+
let repos: string[] = ["."];
|
|
110
|
+
const configPath = join(discovery.beadsDir, "config.yaml");
|
|
111
|
+
try {
|
|
112
|
+
if (existsSync(configPath)) {
|
|
113
|
+
const content = readFileSync(configPath, "utf-8");
|
|
114
|
+
const config = parseYaml(content);
|
|
115
|
+
const additional = config?.repos?.additional;
|
|
116
|
+
if (Array.isArray(additional)) {
|
|
117
|
+
repos = [config?.repos?.primary || ".", ...additional];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} catch {
|
|
121
|
+
// ignore
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const repoUrls = getRepoUrls(discovery.beadsDir);
|
|
125
|
+
const claims = commentsResult
|
|
126
|
+
? getClaimedNodes(commentsResult.allComments)
|
|
127
|
+
: new Map<string, ClaimInfo>();
|
|
128
|
+
|
|
129
|
+
// Build node map for enrichment
|
|
130
|
+
const nodeMap = new Map<string, GraphNode>(
|
|
131
|
+
beadsData.graphData.nodes.map((n) => [n.id, n])
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// Filter and enrich issues
|
|
135
|
+
let issues = beadsData.graphData.nodes;
|
|
136
|
+
|
|
137
|
+
if (statusFilter) {
|
|
138
|
+
issues = issues.filter((n) => statusFilter.includes(n.status));
|
|
139
|
+
}
|
|
140
|
+
if (priorityFilter) {
|
|
141
|
+
issues = issues.filter((n) => priorityFilter.includes(n.priority));
|
|
142
|
+
}
|
|
143
|
+
if (prefixFilter) {
|
|
144
|
+
issues = issues.filter((n) => n.prefix === prefixFilter);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const enrichedIssues = issues.map((node) => {
|
|
148
|
+
const nodeComments =
|
|
149
|
+
commentsResult?.commentsByNode.get(node.id) || [];
|
|
150
|
+
const claim = claims.get(node.id) || null;
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
id: node.id,
|
|
154
|
+
title: node.title,
|
|
155
|
+
description: node.description || null,
|
|
156
|
+
status: node.status,
|
|
157
|
+
priority: node.priority,
|
|
158
|
+
issue_type: node.issueType,
|
|
159
|
+
owner: node.owner || null,
|
|
160
|
+
assignee: node.assignee || null,
|
|
161
|
+
labels: [] as string[], // labels not on GraphNode, could extend later
|
|
162
|
+
created_at: node.createdAt,
|
|
163
|
+
updated_at: node.updatedAt,
|
|
164
|
+
closed_at: node.closedAt || null,
|
|
165
|
+
close_reason: node.closeReason || null,
|
|
166
|
+
prefix: node.prefix,
|
|
167
|
+
blockers: node.dependentIds, // issues that block this one (upstream)
|
|
168
|
+
dependents: node.blockerIds, // issues this one blocks (downstream)
|
|
169
|
+
...(includeComments
|
|
170
|
+
? { comments: nodeComments.map(serializeComment) }
|
|
171
|
+
: {}),
|
|
172
|
+
claimed_by: claim ? serializeClaim(claim) : null,
|
|
173
|
+
};
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Build dependencies
|
|
177
|
+
const dependencies = beadsData.graphData.links.map((link) => {
|
|
178
|
+
const src =
|
|
179
|
+
typeof link.source === "object"
|
|
180
|
+
? (link.source as { id: string }).id
|
|
181
|
+
: link.source;
|
|
182
|
+
const tgt =
|
|
183
|
+
typeof link.target === "object"
|
|
184
|
+
? (link.target as { id: string }).id
|
|
185
|
+
: link.target;
|
|
186
|
+
return { from: src, to: tgt, type: link.type };
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Build activity feed
|
|
190
|
+
let activity: Record<string, unknown>[] = [];
|
|
191
|
+
if (includeActivity) {
|
|
192
|
+
const feed = buildHistoricalFeed(
|
|
193
|
+
beadsData.graphData.nodes,
|
|
194
|
+
beadsData.graphData.links,
|
|
195
|
+
commentsResult?.allComments || null
|
|
196
|
+
);
|
|
197
|
+
activity = feed.slice(0, limit).map((event) => ({
|
|
198
|
+
type: event.type,
|
|
199
|
+
time: new Date(event.time).toISOString(),
|
|
200
|
+
issue_id: event.nodeId,
|
|
201
|
+
...(event.nodeTitle ? { issue_title: event.nodeTitle } : {}),
|
|
202
|
+
...(event.actor ? { actor: { handle: event.actor.handle } } : {}),
|
|
203
|
+
...(event.detail ? { detail: event.detail } : {}),
|
|
204
|
+
}));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Warnings for partial data
|
|
208
|
+
const warnings: string[] = [];
|
|
209
|
+
if (includeComments && !commentsResult) {
|
|
210
|
+
warnings.push("Failed to fetch comments from indexer");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return jsonResponse({
|
|
214
|
+
project: {
|
|
215
|
+
name: discovery.issuePrefix || discovery.repoName,
|
|
216
|
+
prefix: discovery.issuePrefix || null,
|
|
217
|
+
repos,
|
|
218
|
+
repoUrls,
|
|
219
|
+
},
|
|
220
|
+
issues: enrichedIssues,
|
|
221
|
+
dependencies,
|
|
222
|
+
stats: {
|
|
223
|
+
total: beadsData.stats.total,
|
|
224
|
+
open: beadsData.stats.open,
|
|
225
|
+
in_progress: beadsData.stats.inProgress,
|
|
226
|
+
blocked: beadsData.stats.blocked,
|
|
227
|
+
closed: beadsData.stats.closed,
|
|
228
|
+
actionable: beadsData.stats.actionable,
|
|
229
|
+
},
|
|
230
|
+
...(includeActivity ? { activity } : {}),
|
|
231
|
+
_meta: {
|
|
232
|
+
generated_at: new Date().toISOString(),
|
|
233
|
+
api_version: "v1" as const,
|
|
234
|
+
heartbeads_version: heartbeadsVersion,
|
|
235
|
+
...(warnings.length ? { warnings } : {}),
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
} catch (error: unknown) {
|
|
239
|
+
const message =
|
|
240
|
+
error instanceof Error ? error.message : "Unknown error";
|
|
241
|
+
if (message.includes("No .beads/ directory found")) {
|
|
242
|
+
return errorResponse(
|
|
243
|
+
"No .beads directory found",
|
|
244
|
+
404,
|
|
245
|
+
"Run bd init in your project first, or set BEADS_DIR."
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
console.error("[api/v1/graph] Error:", error);
|
|
249
|
+
return errorResponse("Internal server error", 500);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/v1/issues/:id — Single issue detail for AI agents.
|
|
3
|
+
*
|
|
4
|
+
* Returns one issue with full description, threaded comments,
|
|
5
|
+
* claim info, and enriched blockers/dependents (with title + status).
|
|
6
|
+
* No auth required.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { loadBeadsData } from "@/lib/parse-beads";
|
|
10
|
+
import { discoverBeadsDir } from "@/lib/discover";
|
|
11
|
+
import { fetchBeadsComments, getClaimedNodes } from "@/lib/comments";
|
|
12
|
+
import type { BeadsComment, ClaimInfo } from "@/lib/comments";
|
|
13
|
+
import type { GraphNode } from "@/lib/types";
|
|
14
|
+
import { jsonResponse, errorResponse, OPTIONS } from "@/lib/api-helpers";
|
|
15
|
+
import { readFileSync } from "fs";
|
|
16
|
+
import { join } from "path";
|
|
17
|
+
|
|
18
|
+
export const dynamic = "force-dynamic";
|
|
19
|
+
export { OPTIONS };
|
|
20
|
+
|
|
21
|
+
let heartbeadsVersion = "0.0.0";
|
|
22
|
+
try {
|
|
23
|
+
const pkg = JSON.parse(
|
|
24
|
+
readFileSync(join(process.cwd(), "package.json"), "utf-8")
|
|
25
|
+
);
|
|
26
|
+
heartbeadsVersion = pkg.version || heartbeadsVersion;
|
|
27
|
+
} catch {
|
|
28
|
+
// ignore
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function serializeComment(c: BeadsComment): Record<string, unknown> {
|
|
32
|
+
return {
|
|
33
|
+
author: {
|
|
34
|
+
handle: c.handle,
|
|
35
|
+
did: c.did,
|
|
36
|
+
...(c.displayName ? { displayName: c.displayName } : {}),
|
|
37
|
+
},
|
|
38
|
+
text: c.text,
|
|
39
|
+
createdAt: c.createdAt,
|
|
40
|
+
likes: c.likes.length,
|
|
41
|
+
replies: c.replies.map(serializeComment),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function GET(
|
|
46
|
+
request: Request,
|
|
47
|
+
{ params }: { params: { id: string } }
|
|
48
|
+
) {
|
|
49
|
+
try {
|
|
50
|
+
const discovery = discoverBeadsDir();
|
|
51
|
+
const issueId = params.id;
|
|
52
|
+
|
|
53
|
+
// Load beads data + comments in parallel
|
|
54
|
+
const [beadsData, commentsResult] = await Promise.all([
|
|
55
|
+
Promise.resolve(loadBeadsData(discovery.beadsDir)),
|
|
56
|
+
fetchBeadsComments().catch((err) => {
|
|
57
|
+
console.error("[api/v1/issues] Failed to fetch comments:", err);
|
|
58
|
+
return null;
|
|
59
|
+
}),
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
// Find the issue
|
|
63
|
+
const nodeMap = new Map<string, GraphNode>(
|
|
64
|
+
beadsData.graphData.nodes.map((n) => [n.id, n])
|
|
65
|
+
);
|
|
66
|
+
const node = nodeMap.get(issueId);
|
|
67
|
+
|
|
68
|
+
if (!node) {
|
|
69
|
+
return errorResponse(
|
|
70
|
+
"Issue not found",
|
|
71
|
+
404,
|
|
72
|
+
`No issue with id "${issueId}". Use GET /api/v1/graph to list all issues.`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Build enriched blockers/dependents with title + status
|
|
77
|
+
// dependentIds = issues that block this one (upstream)
|
|
78
|
+
// blockerIds = issues this one blocks (downstream)
|
|
79
|
+
const enrichedBlockers = node.dependentIds
|
|
80
|
+
.map((id) => {
|
|
81
|
+
const n = nodeMap.get(id);
|
|
82
|
+
return n
|
|
83
|
+
? { id: n.id, title: n.title, status: n.status }
|
|
84
|
+
: { id, title: null, status: null };
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const enrichedDependents = node.blockerIds
|
|
88
|
+
.map((id) => {
|
|
89
|
+
const n = nodeMap.get(id);
|
|
90
|
+
return n
|
|
91
|
+
? { id: n.id, title: n.title, status: n.status }
|
|
92
|
+
: { id, title: null, status: null };
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Comments and claims
|
|
96
|
+
const nodeComments =
|
|
97
|
+
commentsResult?.commentsByNode.get(issueId) || [];
|
|
98
|
+
const claims = commentsResult
|
|
99
|
+
? getClaimedNodes(commentsResult.allComments)
|
|
100
|
+
: new Map<string, ClaimInfo>();
|
|
101
|
+
const claim = claims.get(issueId) || null;
|
|
102
|
+
|
|
103
|
+
const warnings: string[] = [];
|
|
104
|
+
if (!commentsResult) {
|
|
105
|
+
warnings.push("Failed to fetch comments from indexer");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return jsonResponse({
|
|
109
|
+
issue: {
|
|
110
|
+
id: node.id,
|
|
111
|
+
title: node.title,
|
|
112
|
+
description: node.description || null,
|
|
113
|
+
status: node.status,
|
|
114
|
+
priority: node.priority,
|
|
115
|
+
issue_type: node.issueType,
|
|
116
|
+
owner: node.owner || null,
|
|
117
|
+
assignee: node.assignee || null,
|
|
118
|
+
labels: [] as string[],
|
|
119
|
+
created_at: node.createdAt,
|
|
120
|
+
updated_at: node.updatedAt,
|
|
121
|
+
closed_at: node.closedAt || null,
|
|
122
|
+
close_reason: node.closeReason || null,
|
|
123
|
+
prefix: node.prefix,
|
|
124
|
+
blockers: enrichedBlockers,
|
|
125
|
+
dependents: enrichedDependents,
|
|
126
|
+
comments: nodeComments.map(serializeComment),
|
|
127
|
+
claimed_by: claim
|
|
128
|
+
? {
|
|
129
|
+
handle: claim.handle,
|
|
130
|
+
did: claim.did,
|
|
131
|
+
...(claim.displayName
|
|
132
|
+
? { displayName: claim.displayName }
|
|
133
|
+
: {}),
|
|
134
|
+
claimed_at: claim.claimedAt,
|
|
135
|
+
}
|
|
136
|
+
: null,
|
|
137
|
+
},
|
|
138
|
+
_meta: {
|
|
139
|
+
generated_at: new Date().toISOString(),
|
|
140
|
+
api_version: "v1" as const,
|
|
141
|
+
heartbeads_version: heartbeadsVersion,
|
|
142
|
+
...(warnings.length ? { warnings } : {}),
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
} catch (error: unknown) {
|
|
146
|
+
const message =
|
|
147
|
+
error instanceof Error ? error.message : "Unknown error";
|
|
148
|
+
if (message.includes("No .beads/ directory found")) {
|
|
149
|
+
return errorResponse(
|
|
150
|
+
"No .beads directory found",
|
|
151
|
+
404,
|
|
152
|
+
"Run bd init in your project first, or set BEADS_DIR."
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
console.error("[api/v1/issues] Error:", error);
|
|
156
|
+
return errorResponse("Internal server error", 500);
|
|
157
|
+
}
|
|
158
|
+
}
|