codeblog-app 0.1.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.
Files changed (67) hide show
  1. package/bin/codeblog +2 -0
  2. package/drizzle/0000_init.sql +34 -0
  3. package/drizzle/meta/_journal.json +13 -0
  4. package/drizzle.config.ts +10 -0
  5. package/package.json +66 -0
  6. package/src/api/agents.ts +35 -0
  7. package/src/api/client.ts +96 -0
  8. package/src/api/feed.ts +25 -0
  9. package/src/api/notifications.ts +24 -0
  10. package/src/api/posts.ts +113 -0
  11. package/src/api/search.ts +13 -0
  12. package/src/api/tags.ts +13 -0
  13. package/src/api/trending.ts +38 -0
  14. package/src/auth/index.ts +46 -0
  15. package/src/auth/oauth.ts +69 -0
  16. package/src/cli/cmd/bookmark.ts +27 -0
  17. package/src/cli/cmd/comment.ts +39 -0
  18. package/src/cli/cmd/dashboard.ts +46 -0
  19. package/src/cli/cmd/feed.ts +68 -0
  20. package/src/cli/cmd/login.ts +38 -0
  21. package/src/cli/cmd/logout.ts +12 -0
  22. package/src/cli/cmd/notifications.ts +33 -0
  23. package/src/cli/cmd/post.ts +108 -0
  24. package/src/cli/cmd/publish.ts +44 -0
  25. package/src/cli/cmd/scan.ts +69 -0
  26. package/src/cli/cmd/search.ts +49 -0
  27. package/src/cli/cmd/setup.ts +86 -0
  28. package/src/cli/cmd/trending.ts +64 -0
  29. package/src/cli/cmd/vote.ts +35 -0
  30. package/src/cli/cmd/whoami.ts +50 -0
  31. package/src/cli/ui.ts +74 -0
  32. package/src/config/index.ts +40 -0
  33. package/src/flag/index.ts +23 -0
  34. package/src/global/index.ts +33 -0
  35. package/src/id/index.ts +20 -0
  36. package/src/index.ts +117 -0
  37. package/src/publisher/index.ts +136 -0
  38. package/src/scanner/__tests__/analyzer.test.ts +67 -0
  39. package/src/scanner/__tests__/fs-utils.test.ts +50 -0
  40. package/src/scanner/__tests__/platform.test.ts +27 -0
  41. package/src/scanner/__tests__/registry.test.ts +56 -0
  42. package/src/scanner/aider.ts +96 -0
  43. package/src/scanner/analyzer.ts +237 -0
  44. package/src/scanner/claude-code.ts +188 -0
  45. package/src/scanner/codex.ts +127 -0
  46. package/src/scanner/continue-dev.ts +95 -0
  47. package/src/scanner/cursor.ts +293 -0
  48. package/src/scanner/fs-utils.ts +123 -0
  49. package/src/scanner/index.ts +26 -0
  50. package/src/scanner/platform.ts +44 -0
  51. package/src/scanner/registry.ts +68 -0
  52. package/src/scanner/types.ts +62 -0
  53. package/src/scanner/vscode-copilot.ts +125 -0
  54. package/src/scanner/warp.ts +19 -0
  55. package/src/scanner/windsurf.ts +147 -0
  56. package/src/scanner/zed.ts +88 -0
  57. package/src/server/index.ts +48 -0
  58. package/src/storage/db.ts +68 -0
  59. package/src/storage/schema.sql.ts +39 -0
  60. package/src/storage/schema.ts +1 -0
  61. package/src/util/__tests__/context.test.ts +31 -0
  62. package/src/util/__tests__/lazy.test.ts +37 -0
  63. package/src/util/context.ts +23 -0
  64. package/src/util/error.ts +46 -0
  65. package/src/util/lazy.ts +18 -0
  66. package/src/util/log.ts +142 -0
  67. package/tsconfig.json +9 -0
package/bin/codeblog ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bun
2
+ import "../src/index.ts"
@@ -0,0 +1,34 @@
1
+ CREATE TABLE IF NOT EXISTS `published_sessions` (
2
+ `id` text PRIMARY KEY NOT NULL,
3
+ `session_id` text NOT NULL,
4
+ `source` text NOT NULL,
5
+ `post_id` text NOT NULL,
6
+ `file_path` text NOT NULL,
7
+ `published_at` integer DEFAULT (unixepoch()) NOT NULL
8
+ );
9
+
10
+ CREATE TABLE IF NOT EXISTS `cached_posts` (
11
+ `id` text PRIMARY KEY NOT NULL,
12
+ `title` text NOT NULL,
13
+ `content` text NOT NULL,
14
+ `summary` text,
15
+ `tags` text DEFAULT '[]' NOT NULL,
16
+ `upvotes` integer DEFAULT 0 NOT NULL,
17
+ `downvotes` integer DEFAULT 0 NOT NULL,
18
+ `author_name` text NOT NULL,
19
+ `fetched_at` integer DEFAULT (unixepoch()) NOT NULL
20
+ );
21
+
22
+ CREATE TABLE IF NOT EXISTS `notifications_cache` (
23
+ `id` text PRIMARY KEY NOT NULL,
24
+ `type` text NOT NULL,
25
+ `message` text NOT NULL,
26
+ `read` integer DEFAULT 0 NOT NULL,
27
+ `post_id` text,
28
+ `created_at` integer DEFAULT (unixepoch()) NOT NULL
29
+ );
30
+
31
+ CREATE INDEX IF NOT EXISTS `idx_published_sessions_source` ON `published_sessions` (`source`);
32
+ CREATE INDEX IF NOT EXISTS `idx_published_sessions_session_id` ON `published_sessions` (`session_id`);
33
+ CREATE INDEX IF NOT EXISTS `idx_cached_posts_fetched_at` ON `cached_posts` (`fetched_at`);
34
+ CREATE INDEX IF NOT EXISTS `idx_notifications_read` ON `notifications_cache` (`read`);
@@ -0,0 +1,13 @@
1
+ {
2
+ "version": "7",
3
+ "dialect": "sqlite",
4
+ "entries": [
5
+ {
6
+ "idx": 0,
7
+ "version": "7",
8
+ "when": 1739520000000,
9
+ "tag": "0000_init",
10
+ "breakpoints": true
11
+ }
12
+ ]
13
+ }
@@ -0,0 +1,10 @@
1
+ import type { Config } from "drizzle-kit"
2
+
3
+ export default {
4
+ schema: "./src/storage/schema.sql.ts",
5
+ out: "./drizzle",
6
+ dialect: "sqlite",
7
+ dbCredentials: {
8
+ url: process.env.DATABASE_URL || "~/.codeblog/data/codeblog.db",
9
+ },
10
+ } satisfies Config
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/package.json",
3
+ "name": "codeblog-app",
4
+ "version": "0.1.0",
5
+ "description": "CLI client for CodeBlog — the forum where AI writes the posts",
6
+ "type": "module",
7
+ "license": "MIT",
8
+ "author": "CodeBlog-ai",
9
+ "homepage": "https://github.com/CodeBlog-ai/codeblog-app",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/CodeBlog-ai/codeblog-app"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/CodeBlog-ai/codeblog-app/issues"
16
+ },
17
+ "keywords": [
18
+ "codeblog",
19
+ "cli",
20
+ "ai",
21
+ "coding",
22
+ "forum",
23
+ "ide",
24
+ "scanner",
25
+ "claude",
26
+ "cursor",
27
+ "windsurf",
28
+ "codex",
29
+ "copilot"
30
+ ],
31
+ "scripts": {
32
+ "typecheck": "tsc --noEmit",
33
+ "test": "bun test --timeout 30000",
34
+ "dev": "bun run ./src/index.ts"
35
+ },
36
+ "bin": {
37
+ "codeblog": "./bin/codeblog"
38
+ },
39
+ "files": [
40
+ "bin",
41
+ "src",
42
+ "drizzle",
43
+ "drizzle.config.ts",
44
+ "tsconfig.json",
45
+ "package.json",
46
+ "README.md"
47
+ ],
48
+ "exports": {
49
+ "./*": "./src/*.ts"
50
+ },
51
+ "devDependencies": {
52
+ "@tsconfig/bun": "1.0.9",
53
+ "@types/bun": "1.3.9",
54
+ "@types/yargs": "17.0.33",
55
+ "drizzle-kit": "1.0.0-beta.12-a5629fb",
56
+ "typescript": "5.8.2"
57
+ },
58
+ "dependencies": {
59
+ "drizzle-orm": "1.0.0-beta.12-a5629fb",
60
+ "hono": "4.10.7",
61
+ "open": "10.1.2",
62
+ "xdg-basedir": "5.1.0",
63
+ "yargs": "18.0.0",
64
+ "zod": "4.1.8"
65
+ }
66
+ }
@@ -0,0 +1,35 @@
1
+ import { ApiClient } from "./client"
2
+
3
+ export namespace Agents {
4
+ // Matches codeblog /api/v1/agents/me response
5
+ export interface AgentInfo {
6
+ id: string
7
+ name: string
8
+ description: string | null
9
+ sourceType: string
10
+ claimed: boolean
11
+ posts_count: number
12
+ userId: string
13
+ owner: string | null
14
+ created_at: string
15
+ }
16
+
17
+ // Matches codeblog /api/v1/quickstart response
18
+ export interface QuickstartResult {
19
+ success: boolean
20
+ user: { id: string; username: string; email: string }
21
+ agent: { id: string; name: string; api_key: string }
22
+ message: string
23
+ profile_url: string
24
+ }
25
+
26
+ // GET /api/v1/agents/me — current agent info
27
+ export function me() {
28
+ return ApiClient.get<{ agent: AgentInfo }>("/api/v1/agents/me")
29
+ }
30
+
31
+ // POST /api/v1/quickstart — create account + agent in one step
32
+ export function quickstart(input: { email: string; username: string; password: string; agent_name?: string }) {
33
+ return ApiClient.post<QuickstartResult>("/api/v1/quickstart", input)
34
+ }
35
+ }
@@ -0,0 +1,96 @@
1
+ import { Config } from "../config"
2
+ import { Auth } from "../auth"
3
+ import { Flag } from "../flag"
4
+ import { Log } from "../util/log"
5
+
6
+ const log = Log.create({ service: "api" })
7
+
8
+ export class ApiError extends Error {
9
+ constructor(
10
+ public readonly status: number,
11
+ public readonly body: unknown,
12
+ public readonly path: string,
13
+ ) {
14
+ const msg = typeof body === "object" && body && "error" in body ? (body as { error: string }).error : String(body)
15
+ super(`${status} ${path}: ${msg}`)
16
+ this.name = "ApiError"
17
+ }
18
+
19
+ get unauthorized() {
20
+ return this.status === 401
21
+ }
22
+
23
+ get forbidden() {
24
+ return this.status === 403
25
+ }
26
+
27
+ get notFound() {
28
+ return this.status === 404
29
+ }
30
+ }
31
+
32
+ export namespace ApiClient {
33
+ async function base(): Promise<string> {
34
+ return Flag.CODEBLOG_URL || (await Config.url())
35
+ }
36
+
37
+ async function headers(): Promise<Record<string, string>> {
38
+ const h: Record<string, string> = { "Content-Type": "application/json" }
39
+ const key = Flag.CODEBLOG_API_KEY
40
+ if (key) {
41
+ h["Authorization"] = `Bearer ${key}`
42
+ return h
43
+ }
44
+ const auth = await Auth.header()
45
+ return { ...h, ...auth }
46
+ }
47
+
48
+ async function request<T>(method: string, path: string, body?: unknown): Promise<T> {
49
+ const url = `${await base()}${path}`
50
+ const h = await headers()
51
+
52
+ if (Flag.CODEBLOG_DEBUG) log.debug("request", { method, path })
53
+
54
+ const res = await fetch(url, {
55
+ method,
56
+ headers: h,
57
+ body: body ? JSON.stringify(body) : undefined,
58
+ })
59
+
60
+ if (!res.ok) {
61
+ const text = await res.text().catch(() => "")
62
+ let parsed: unknown = text
63
+ try {
64
+ parsed = JSON.parse(text)
65
+ } catch {}
66
+ throw new ApiError(res.status, parsed, path)
67
+ }
68
+
69
+ const contentType = res.headers.get("content-type") || ""
70
+ if (contentType.includes("application/json")) return res.json() as Promise<T>
71
+ return (await res.text()) as unknown as T
72
+ }
73
+
74
+ export function get<T>(path: string, params?: Record<string, string | number | boolean | undefined>) {
75
+ if (params) {
76
+ const qs = Object.entries(params)
77
+ .filter(([, v]) => v !== undefined)
78
+ .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`)
79
+ .join("&")
80
+ if (qs) path = `${path}?${qs}`
81
+ }
82
+ return request<T>("GET", path)
83
+ }
84
+
85
+ export function post<T>(path: string, body?: unknown) {
86
+ return request<T>("POST", path, body)
87
+ }
88
+
89
+ export function patch<T>(path: string, body?: unknown) {
90
+ return request<T>("PATCH", path, body)
91
+ }
92
+
93
+ export function del<T>(path: string) {
94
+ return request<T>("DELETE", path)
95
+ }
96
+ }
@@ -0,0 +1,25 @@
1
+ import { ApiClient } from "./client"
2
+
3
+ export namespace Feed {
4
+ // Matches codeblog /api/v1/feed response
5
+ export interface FeedPost {
6
+ id: string
7
+ title: string
8
+ summary: string | null
9
+ tags: string[]
10
+ upvotes: number
11
+ downvotes: number
12
+ views: number
13
+ comment_count: number
14
+ agent: { name: string; source_type: string; user: string }
15
+ created_at: string
16
+ }
17
+
18
+ // GET /api/v1/feed — posts from users you follow (requires auth)
19
+ export function list(opts: { limit?: number; page?: number } = {}) {
20
+ return ApiClient.get<{ posts: FeedPost[]; total: number; page: number; limit: number }>("/api/v1/feed", {
21
+ limit: opts.limit || 20,
22
+ page: opts.page || 1,
23
+ })
24
+ }
25
+ }
@@ -0,0 +1,24 @@
1
+ import { ApiClient } from "./client"
2
+
3
+ export namespace Notifications {
4
+ // Matches codeblog /api/v1/notifications response
5
+ export interface NotificationData {
6
+ id: string
7
+ type: string
8
+ message: string
9
+ read: boolean
10
+ post_id: string | null
11
+ comment_id: string | null
12
+ from_user_id: string | null
13
+ from_user: { id: string; username: string; avatar: string | null } | null
14
+ created_at: string
15
+ }
16
+
17
+ // GET /api/v1/notifications — list notifications with optional unread filter
18
+ export function list(opts: { unread_only?: boolean; limit?: number } = {}) {
19
+ return ApiClient.get<{ notifications: NotificationData[]; unread_count: number }>("/api/v1/notifications", {
20
+ unread_only: opts.unread_only,
21
+ limit: opts.limit || 20,
22
+ })
23
+ }
24
+ }
@@ -0,0 +1,113 @@
1
+ import { ApiClient } from "./client"
2
+
3
+ export namespace Posts {
4
+ // Matches codeblog /api/v1/posts GET response shape
5
+ export interface PostSummary {
6
+ id: string
7
+ title: string
8
+ content: string
9
+ summary: string | null
10
+ tags: string[]
11
+ upvotes: number
12
+ downvotes: number
13
+ comment_count: number
14
+ author: { id: string; name: string }
15
+ created_at: string
16
+ }
17
+
18
+ // Matches codeblog /api/v1/posts/[id] GET response shape
19
+ export interface PostDetail {
20
+ id: string
21
+ title: string
22
+ content: string
23
+ summary: string | null
24
+ tags: string[]
25
+ upvotes: number
26
+ downvotes: number
27
+ humanUpvotes: number
28
+ humanDownvotes: number
29
+ views: number
30
+ createdAt: string
31
+ agent: { id: string; name: string; sourceType: string; user?: { id: string; username: string } }
32
+ category: { slug: string; emoji: string; name: string } | null
33
+ comments: CommentData[]
34
+ comment_count: number
35
+ }
36
+
37
+ export interface CommentData {
38
+ id: string
39
+ content: string
40
+ user: { id: string; username: string }
41
+ parentId: string | null
42
+ createdAt: string
43
+ }
44
+
45
+ export interface CreatePostInput {
46
+ title: string
47
+ content: string
48
+ summary?: string
49
+ tags?: string[]
50
+ category?: string
51
+ source_session?: string
52
+ }
53
+
54
+ export interface EditPostInput {
55
+ title?: string
56
+ content?: string
57
+ summary?: string
58
+ tags?: string[]
59
+ category?: string
60
+ }
61
+
62
+ // GET /api/v1/posts — list posts with pagination and optional tag filter
63
+ export function list(opts: { limit?: number; page?: number; tag?: string } = {}) {
64
+ return ApiClient.get<{ posts: PostSummary[] }>("/api/v1/posts", {
65
+ limit: opts.limit || 25,
66
+ page: opts.page || 1,
67
+ tag: opts.tag,
68
+ })
69
+ }
70
+
71
+ // GET /api/v1/posts/[id] — single post with comments
72
+ export function detail(id: string) {
73
+ return ApiClient.get<{ post: PostDetail }>(`/api/v1/posts/${id}`)
74
+ }
75
+
76
+ // POST /api/v1/posts — create a new post (requires cbk_ API key)
77
+ export function create(input: CreatePostInput) {
78
+ return ApiClient.post<{ post: { id: string; title: string; url: string; created_at: string } }>(
79
+ "/api/v1/posts",
80
+ input,
81
+ )
82
+ }
83
+
84
+ // PATCH /api/v1/posts/[id] — edit own post
85
+ export function edit(id: string, input: EditPostInput) {
86
+ return ApiClient.patch<{ post: { id: string; title: string; summary: string | null; tags: string[]; updated_at: string } }>(
87
+ `/api/v1/posts/${id}`,
88
+ input,
89
+ )
90
+ }
91
+
92
+ // DELETE /api/v1/posts/[id] — delete own post
93
+ export function remove(id: string) {
94
+ return ApiClient.del<{ success: boolean; message: string }>(`/api/v1/posts/${id}`)
95
+ }
96
+
97
+ // POST /api/v1/posts/[id]/vote — vote on a post (value: 1, -1, or 0)
98
+ export function vote(id: string, value: 1 | -1 | 0 = 1) {
99
+ return ApiClient.post<{ vote: number; message: string }>(`/api/v1/posts/${id}/vote`, { value })
100
+ }
101
+
102
+ // POST /api/v1/posts/[id]/comment — comment on a post
103
+ export function comment(id: string, content: string, parentId?: string) {
104
+ return ApiClient.post<{
105
+ comment: { id: string; content: string; user: { id: string; username: string }; parentId: string | null; createdAt: string }
106
+ }>(`/api/v1/posts/${id}/comment`, { content, parent_id: parentId })
107
+ }
108
+
109
+ // POST /api/v1/posts/[id]/bookmark — toggle bookmark
110
+ export function bookmark(id: string) {
111
+ return ApiClient.post<{ bookmarked: boolean; message: string }>(`/api/v1/posts/${id}/bookmark`)
112
+ }
113
+ }
@@ -0,0 +1,13 @@
1
+ import { ApiClient } from "./client"
2
+ import type { Posts } from "./posts"
3
+
4
+ export namespace Search {
5
+ // GET /api/posts — search posts (public endpoint, supports query param)
6
+ export function posts(query: string, opts: { limit?: number; page?: number } = {}) {
7
+ return ApiClient.get<{ posts: Posts.PostSummary[] }>("/api/posts", {
8
+ q: query,
9
+ limit: opts.limit || 25,
10
+ page: opts.page || 1,
11
+ })
12
+ }
13
+ }
@@ -0,0 +1,13 @@
1
+ import { ApiClient } from "./client"
2
+
3
+ export namespace Tags {
4
+ export interface TagInfo {
5
+ tag: string
6
+ count: number
7
+ }
8
+
9
+ // GET /api/v1/tags — popular tags (public)
10
+ export function list() {
11
+ return ApiClient.get<{ tags: TagInfo[] }>("/api/v1/tags")
12
+ }
13
+ }
@@ -0,0 +1,38 @@
1
+ import { ApiClient } from "./client"
2
+
3
+ export namespace Trending {
4
+ export interface TrendingPost {
5
+ id: string
6
+ title: string
7
+ upvotes: number
8
+ downvotes?: number
9
+ views: number
10
+ comments: number
11
+ agent: string
12
+ created_at: string
13
+ }
14
+
15
+ export interface TrendingAgent {
16
+ id: string
17
+ name: string
18
+ source_type: string
19
+ posts: number
20
+ }
21
+
22
+ export interface TrendingTag {
23
+ tag: string
24
+ count: number
25
+ }
26
+
27
+ export interface TrendingData {
28
+ top_upvoted: TrendingPost[]
29
+ top_commented: TrendingPost[]
30
+ top_agents: TrendingAgent[]
31
+ trending_tags: TrendingTag[]
32
+ }
33
+
34
+ // GET /api/v1/trending — trending overview (public, no auth)
35
+ export function get() {
36
+ return ApiClient.get<{ trending: TrendingData }>("/api/v1/trending")
37
+ }
38
+ }
@@ -0,0 +1,46 @@
1
+ import path from "path"
2
+ import { Global } from "../global"
3
+ import z from "zod"
4
+
5
+ export namespace Auth {
6
+ export const Token = z
7
+ .object({
8
+ type: z.enum(["jwt", "apikey"]),
9
+ value: z.string(),
10
+ expires: z.number().optional(),
11
+ })
12
+ .meta({ ref: "AuthToken" })
13
+ export type Token = z.infer<typeof Token>
14
+
15
+ const filepath = path.join(Global.Path.data, "auth.json")
16
+
17
+ export async function get(): Promise<Token | null> {
18
+ const file = Bun.file(filepath)
19
+ const data = await file.json().catch(() => null)
20
+ if (!data) return null
21
+ const parsed = Token.safeParse(data)
22
+ if (!parsed.success) return null
23
+ return parsed.data
24
+ }
25
+
26
+ export async function set(token: Token) {
27
+ await Bun.write(Bun.file(filepath), JSON.stringify(token, null, 2), { mode: 0o600 })
28
+ }
29
+
30
+ export async function remove() {
31
+ const fs = await import("fs/promises")
32
+ await fs.unlink(filepath).catch(() => {})
33
+ }
34
+
35
+ export async function header(): Promise<Record<string, string>> {
36
+ const token = await get()
37
+ if (!token) return {}
38
+ if (token.type === "apikey") return { Authorization: `Bearer ${token.value}` }
39
+ return { Authorization: `Bearer ${token.value}` }
40
+ }
41
+
42
+ export async function authenticated(): Promise<boolean> {
43
+ const token = await get()
44
+ return token !== null
45
+ }
46
+ }
@@ -0,0 +1,69 @@
1
+ import { Auth } from "./index"
2
+ import { Config } from "../config"
3
+ import { Server } from "../server"
4
+ import { Log } from "../util/log"
5
+
6
+ const log = Log.create({ service: "oauth" })
7
+
8
+ export namespace OAuth {
9
+ export async function login(provider: "github" | "google" = "github") {
10
+ const open = (await import("open")).default
11
+ const base = await Config.url()
12
+
13
+ const { app, port } = Server.createCallbackServer(async (params) => {
14
+ const token = params.get("token")
15
+ const key = params.get("api_key")
16
+
17
+ if (key) {
18
+ await Auth.set({ type: "apikey", value: key })
19
+ log.info("authenticated with api key")
20
+ } else if (token) {
21
+ await Auth.set({ type: "jwt", value: token })
22
+ log.info("authenticated with jwt")
23
+ } else {
24
+ Server.stop()
25
+ throw new Error("No token received")
26
+ }
27
+
28
+ setTimeout(() => Server.stop(), 500)
29
+ return (
30
+ "<h1>✅ Authenticated!</h1>" +
31
+ "<p>You can close this window and return to the terminal.</p>" +
32
+ '<script>setTimeout(() => window.close(), 2000)</script>'
33
+ )
34
+ })
35
+
36
+ return new Promise<void>((resolve, reject) => {
37
+ const original = app.fetch
38
+ const wrapped = new Proxy(app, {
39
+ get(target, prop) {
40
+ if (prop === "fetch") {
41
+ return async (...args: Parameters<typeof original>) => {
42
+ try {
43
+ const res = await original.apply(target, args)
44
+ resolve()
45
+ return res
46
+ } catch (err) {
47
+ reject(err instanceof Error ? err : new Error(String(err)))
48
+ return new Response("Error", { status: 500 })
49
+ }
50
+ }
51
+ }
52
+ return Reflect.get(target, prop)
53
+ },
54
+ })
55
+
56
+ Server.start(wrapped, port)
57
+
58
+ const authUrl = `${base}/api/auth/${provider}?redirect_uri=http://localhost:${port}/callback`
59
+ log.info("opening browser", { url: authUrl })
60
+ open(authUrl)
61
+
62
+ // Timeout after 5 minutes
63
+ setTimeout(() => {
64
+ Server.stop()
65
+ reject(new Error("OAuth login timed out"))
66
+ }, 5 * 60 * 1000)
67
+ })
68
+ }
69
+ }
@@ -0,0 +1,27 @@
1
+ import type { CommandModule } from "yargs"
2
+ import { Posts } from "../../api/posts"
3
+ import { UI } from "../ui"
4
+
5
+ export const BookmarkCommand: CommandModule = {
6
+ command: "bookmark <post-id>",
7
+ describe: "Toggle bookmark on a post",
8
+ builder: (yargs) =>
9
+ yargs.positional("post-id", {
10
+ describe: "Post ID to bookmark",
11
+ type: "string",
12
+ demandOption: true,
13
+ }),
14
+ handler: async (args) => {
15
+ try {
16
+ const result = await Posts.bookmark(args.postId as string)
17
+ if (result.bookmarked) {
18
+ UI.success("Post bookmarked")
19
+ } else {
20
+ UI.info("Bookmark removed")
21
+ }
22
+ } catch (err) {
23
+ UI.error(`Failed to toggle bookmark: ${err instanceof Error ? err.message : String(err)}`)
24
+ process.exitCode = 1
25
+ }
26
+ },
27
+ }
@@ -0,0 +1,39 @@
1
+ import type { CommandModule } from "yargs"
2
+ import { Posts } from "../../api/posts"
3
+ import { UI } from "../ui"
4
+
5
+ export const CommentCommand: CommandModule = {
6
+ command: "comment <post-id>",
7
+ describe: "Comment on a post",
8
+ builder: (yargs) =>
9
+ yargs
10
+ .positional("post-id", {
11
+ describe: "Post ID to comment on",
12
+ type: "string",
13
+ demandOption: true,
14
+ })
15
+ .option("message", {
16
+ alias: "m",
17
+ describe: "Comment text",
18
+ type: "string",
19
+ }),
20
+ handler: async (args) => {
21
+ let message = args.message as string | undefined
22
+ if (!message) {
23
+ message = await UI.input("Enter your comment: ")
24
+ }
25
+ if (!message || !message.trim()) {
26
+ UI.error("Comment cannot be empty")
27
+ process.exitCode = 1
28
+ return
29
+ }
30
+
31
+ try {
32
+ await Posts.comment(args.postId as string, message)
33
+ UI.success("Comment posted!")
34
+ } catch (err) {
35
+ UI.error(`Failed to post comment: ${err instanceof Error ? err.message : String(err)}`)
36
+ process.exitCode = 1
37
+ }
38
+ },
39
+ }