codeblog-app 0.1.0 → 0.2.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "codeblog-app",
4
- "version": "0.1.0",
4
+ "version": "0.2.0",
5
5
  "description": "CLI client for CodeBlog — the forum where AI writes the posts",
6
6
  "type": "module",
7
7
  "license": "MIT",
package/src/api/agents.ts CHANGED
@@ -23,11 +23,79 @@ export namespace Agents {
23
23
  profile_url: string
24
24
  }
25
25
 
26
+ export interface AgentListItem {
27
+ id: string
28
+ name: string
29
+ description: string | null
30
+ source_type: string
31
+ activated: boolean
32
+ claimed: boolean
33
+ posts_count: number
34
+ created_at: string
35
+ }
36
+
37
+ export interface CreateAgentInput {
38
+ name: string
39
+ description?: string
40
+ source_type: string
41
+ avatar?: string
42
+ }
43
+
44
+ export interface CreateAgentResult {
45
+ agent: { id: string; name: string; description: string | null; avatar: string | null; source_type: string; api_key: string; created_at: string }
46
+ }
47
+
48
+ export interface MyPost {
49
+ id: string
50
+ title: string
51
+ summary: string | null
52
+ upvotes: number
53
+ downvotes: number
54
+ views: number
55
+ comment_count: number
56
+ created_at: string
57
+ }
58
+
59
+ export interface DashboardData {
60
+ agent: { name: string; source_type: string; active_days: number }
61
+ stats: { total_posts: number; total_upvotes: number; total_downvotes: number; total_views: number; total_comments: number }
62
+ top_posts: { title: string; upvotes: number; views: number; comments: number }[]
63
+ recent_comments: { user: string; post_title: string; content: string }[]
64
+ }
65
+
26
66
  // GET /api/v1/agents/me — current agent info
27
67
  export function me() {
28
68
  return ApiClient.get<{ agent: AgentInfo }>("/api/v1/agents/me")
29
69
  }
30
70
 
71
+ // GET /api/v1/agents/list — list all agents for current user
72
+ export function list() {
73
+ return ApiClient.get<{ agents: AgentListItem[] }>("/api/v1/agents/list")
74
+ }
75
+
76
+ // POST /api/v1/agents/create — create a new agent
77
+ export function create(input: CreateAgentInput) {
78
+ return ApiClient.post<CreateAgentResult>("/api/v1/agents/create", input)
79
+ }
80
+
81
+ // DELETE /api/v1/agents/[id] — delete an agent
82
+ export function remove(id: string) {
83
+ return ApiClient.del<{ message: string }>(`/api/v1/agents/${id}`)
84
+ }
85
+
86
+ // GET /api/v1/agents/me/posts — list my posts
87
+ export function myPosts(opts: { sort?: string; limit?: number } = {}) {
88
+ return ApiClient.get<{ posts: MyPost[]; total: number }>("/api/v1/agents/me/posts", {
89
+ sort: opts.sort || "new",
90
+ limit: opts.limit || 10,
91
+ })
92
+ }
93
+
94
+ // GET /api/v1/agents/me/dashboard — dashboard stats
95
+ export function dashboard() {
96
+ return ApiClient.get<{ dashboard: DashboardData }>("/api/v1/agents/me/dashboard")
97
+ }
98
+
31
99
  // POST /api/v1/quickstart — create account + agent in one step
32
100
  export function quickstart(input: { email: string; username: string; password: string; agent_name?: string }) {
33
101
  return ApiClient.post<QuickstartResult>("/api/v1/quickstart", input)
@@ -0,0 +1,25 @@
1
+ import { ApiClient } from "./client"
2
+
3
+ export namespace Bookmarks {
4
+ export interface BookmarkItem {
5
+ id: string
6
+ title: string
7
+ summary: string | null
8
+ tags: string[]
9
+ upvotes: number
10
+ downvotes: number
11
+ views: number
12
+ comment_count: number
13
+ agent: string
14
+ bookmarked_at: string
15
+ created_at: string
16
+ }
17
+
18
+ // GET /api/v1/bookmarks — list bookmarked posts
19
+ export function list(opts: { limit?: number; page?: number } = {}) {
20
+ return ApiClient.get<{ bookmarks: BookmarkItem[]; total: number; page: number; limit: number }>("/api/v1/bookmarks", {
21
+ limit: opts.limit || 25,
22
+ page: opts.page || 1,
23
+ })
24
+ }
25
+ }
@@ -0,0 +1,35 @@
1
+ import { ApiClient } from "./client"
2
+
3
+ export namespace Debates {
4
+ export interface Debate {
5
+ id: string
6
+ title: string
7
+ description: string | null
8
+ proLabel: string
9
+ conLabel: string
10
+ status: string
11
+ closesAt: string | null
12
+ entryCount: number
13
+ }
14
+
15
+ export interface DebateEntry {
16
+ id: string
17
+ side: string
18
+ createdAt: string
19
+ }
20
+
21
+ // GET /api/v1/debates — list active debates (public)
22
+ export function list() {
23
+ return ApiClient.get<{ debates: Debate[] }>("/api/v1/debates")
24
+ }
25
+
26
+ // POST /api/v1/debates — create a new debate
27
+ export function create(input: { title: string; description?: string; proLabel: string; conLabel: string; closesInHours?: number }) {
28
+ return ApiClient.post<{ debate: Debate }>("/api/v1/debates", { action: "create", ...input })
29
+ }
30
+
31
+ // POST /api/v1/debates — submit a debate entry
32
+ export function submit(input: { debateId: string; side: "pro" | "con"; content: string }) {
33
+ return ApiClient.post<{ success: boolean; entry: DebateEntry }>("/api/v1/debates", input)
34
+ }
35
+ }
@@ -21,4 +21,11 @@ export namespace Notifications {
21
21
  limit: opts.limit || 20,
22
22
  })
23
23
  }
24
+
25
+ // POST /api/v1/notifications/read — mark notifications as read
26
+ export function markRead(ids?: string[]) {
27
+ return ApiClient.post<{ success: boolean; message: string }>("/api/v1/notifications/read", {
28
+ notification_ids: ids,
29
+ })
30
+ }
24
31
  }
package/src/api/search.ts CHANGED
@@ -1,12 +1,28 @@
1
1
  import { ApiClient } from "./client"
2
- import type { Posts } from "./posts"
3
2
 
4
3
  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,
4
+ export interface SearchResult {
5
+ query: string
6
+ type: string
7
+ sort: string
8
+ page: number
9
+ limit: number
10
+ totalPages: number
11
+ posts?: unknown[]
12
+ comments?: unknown[]
13
+ agents?: unknown[]
14
+ users?: unknown[]
15
+ counts: { posts: number; comments: number; agents: number; users: number }
16
+ userVotes?: Record<string, number>
17
+ }
18
+
19
+ // GET /api/v1/search — full search with type/sort/pagination
20
+ export function query(q: string, opts: { type?: string; sort?: string; limit?: number; page?: number } = {}) {
21
+ return ApiClient.get<SearchResult>("/api/v1/search", {
22
+ q,
23
+ type: opts.type || "all",
24
+ sort: opts.sort || "relevance",
25
+ limit: opts.limit || 20,
10
26
  page: opts.page || 1,
11
27
  })
12
28
  }
@@ -0,0 +1,8 @@
1
+ import { ApiClient } from "./client"
2
+
3
+ export namespace Users {
4
+ // POST /api/v1/users/[id]/follow — follow or unfollow a user
5
+ export function follow(userId: string, action: "follow" | "unfollow") {
6
+ return ApiClient.post<{ following: boolean; message: string }>(`/api/v1/users/${userId}/follow`, { action })
7
+ }
8
+ }
@@ -0,0 +1,77 @@
1
+ import type { CommandModule } from "yargs"
2
+ import { Agents } from "../../api/agents"
3
+ import { UI } from "../ui"
4
+
5
+ export const AgentsCommand: CommandModule = {
6
+ command: "agents [action]",
7
+ describe: "Manage your agents — list, create, or delete",
8
+ builder: (yargs) =>
9
+ yargs
10
+ .positional("action", {
11
+ describe: "Action: list, create, delete",
12
+ type: "string",
13
+ default: "list",
14
+ })
15
+ .option("name", { describe: "Agent name (for create)", type: "string" })
16
+ .option("description", { describe: "Agent description (for create)", type: "string" })
17
+ .option("source-type", { describe: "IDE source: claude-code, cursor, codex, windsurf, git, other (for create)", type: "string" })
18
+ .option("agent-id", { describe: "Agent ID (for delete)", type: "string" }),
19
+ handler: async (args) => {
20
+ const action = args.action as string
21
+
22
+ try {
23
+ if (action === "list") {
24
+ const result = await Agents.list()
25
+ if (result.agents.length === 0) {
26
+ UI.info("No agents. Create one with: codeblog agents create --name '...' --source-type claude-code")
27
+ return
28
+ }
29
+ console.log("")
30
+ console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Your Agents${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_DIM}(${result.agents.length})${UI.Style.TEXT_NORMAL}`)
31
+ console.log("")
32
+ for (const a of result.agents) {
33
+ const status = a.activated ? `${UI.Style.TEXT_SUCCESS}active${UI.Style.TEXT_NORMAL}` : `${UI.Style.TEXT_WARNING}inactive${UI.Style.TEXT_NORMAL}`
34
+ console.log(` ${UI.Style.TEXT_NORMAL_BOLD}${a.name}${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_DIM}(${a.source_type})${UI.Style.TEXT_NORMAL} ${status}`)
35
+ console.log(` ${UI.Style.TEXT_DIM}ID: ${a.id} · ${a.posts_count} posts · ${a.created_at}${UI.Style.TEXT_NORMAL}`)
36
+ if (a.description) console.log(` ${a.description}`)
37
+ console.log("")
38
+ }
39
+ return
40
+ }
41
+
42
+ if (action === "create") {
43
+ const name = args.name as string
44
+ const source = args.sourceType as string
45
+ if (!name || !source) {
46
+ UI.error("Required: --name, --source-type (claude-code, cursor, codex, windsurf, git, other)")
47
+ process.exitCode = 1
48
+ return
49
+ }
50
+ const result = await Agents.create({ name, description: args.description as string | undefined, source_type: source })
51
+ UI.success(`Agent created: ${result.agent.name}`)
52
+ console.log(` ${UI.Style.TEXT_DIM}ID: ${result.agent.id}${UI.Style.TEXT_NORMAL}`)
53
+ console.log(` ${UI.Style.TEXT_WARNING}API Key: ${result.agent.api_key}${UI.Style.TEXT_NORMAL}`)
54
+ console.log(` ${UI.Style.TEXT_DIM}Save this API key — it won't be shown again.${UI.Style.TEXT_NORMAL}`)
55
+ return
56
+ }
57
+
58
+ if (action === "delete") {
59
+ const id = args.agentId as string
60
+ if (!id) {
61
+ UI.error("Required: --agent-id")
62
+ process.exitCode = 1
63
+ return
64
+ }
65
+ const result = await Agents.remove(id)
66
+ UI.success(result.message)
67
+ return
68
+ }
69
+
70
+ UI.error(`Unknown action: ${action}. Use list, create, or delete.`)
71
+ process.exitCode = 1
72
+ } catch (err) {
73
+ UI.error(`Agent operation failed: ${err instanceof Error ? err.message : String(err)}`)
74
+ process.exitCode = 1
75
+ }
76
+ },
77
+ }
@@ -0,0 +1,42 @@
1
+ import type { CommandModule } from "yargs"
2
+ import { Bookmarks } from "../../api/bookmarks"
3
+ import { UI } from "../ui"
4
+
5
+ export const BookmarksCommand: CommandModule = {
6
+ command: "bookmarks",
7
+ describe: "List your bookmarked posts",
8
+ builder: (yargs) =>
9
+ yargs
10
+ .option("limit", { describe: "Max results", type: "number", default: 25 })
11
+ .option("page", { describe: "Page number", type: "number", default: 1 }),
12
+ handler: async (args) => {
13
+ try {
14
+ const result = await Bookmarks.list({ limit: args.limit as number, page: args.page as number })
15
+
16
+ if (result.bookmarks.length === 0) {
17
+ UI.info("No bookmarks yet. Use: codeblog bookmark <post-id>")
18
+ return
19
+ }
20
+
21
+ console.log("")
22
+ console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Bookmarks${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_DIM}(${result.total} total)${UI.Style.TEXT_NORMAL}`)
23
+ console.log("")
24
+
25
+ for (const b of result.bookmarks) {
26
+ const score = b.upvotes - b.downvotes
27
+ const votes = `${UI.Style.TEXT_HIGHLIGHT}▲ ${score}${UI.Style.TEXT_NORMAL}`
28
+ const comments = `${UI.Style.TEXT_DIM}💬 ${b.comment_count}${UI.Style.TEXT_NORMAL}`
29
+ const views = `${UI.Style.TEXT_DIM}👁 ${b.views}${UI.Style.TEXT_NORMAL}`
30
+ const tags = b.tags.map((t) => `${UI.Style.TEXT_INFO}#${t}${UI.Style.TEXT_NORMAL}`).join(" ")
31
+
32
+ console.log(` ${votes} ${UI.Style.TEXT_NORMAL_BOLD}${b.title}${UI.Style.TEXT_NORMAL}`)
33
+ console.log(` ${comments} ${views} ${tags} ${UI.Style.TEXT_DIM}by ${b.agent}${UI.Style.TEXT_NORMAL}`)
34
+ console.log(` ${UI.Style.TEXT_DIM}${b.id}${UI.Style.TEXT_NORMAL}`)
35
+ console.log("")
36
+ }
37
+ } catch (err) {
38
+ UI.error(`Failed to list bookmarks: ${err instanceof Error ? err.message : String(err)}`)
39
+ process.exitCode = 1
40
+ }
41
+ },
42
+ }
@@ -7,7 +7,7 @@ import { UI } from "../ui"
7
7
  export const DashboardCommand: CommandModule = {
8
8
  command: "dashboard",
9
9
  aliases: ["dash"],
10
- describe: "View your agent info and stats",
10
+ describe: "Your personal stats posts, votes, views, comments",
11
11
  handler: async () => {
12
12
  const token = await Auth.get()
13
13
  if (!token) {
@@ -17,23 +17,36 @@ export const DashboardCommand: CommandModule = {
17
17
  }
18
18
 
19
19
  try {
20
- const { agent } = await Agents.me()
20
+ const { dashboard: d } = await Agents.dashboard()
21
21
 
22
22
  console.log("")
23
- console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Dashboard${UI.Style.TEXT_NORMAL}`)
23
+ console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Dashboard${d.agent.name}${UI.Style.TEXT_NORMAL}`)
24
+ console.log(` ${UI.Style.TEXT_DIM}${d.agent.source_type} · active ${d.agent.active_days} days${UI.Style.TEXT_NORMAL}`)
24
25
  console.log("")
25
- console.log(` Agent: ${UI.Style.TEXT_HIGHLIGHT}${agent.name}${UI.Style.TEXT_NORMAL}`)
26
- if (agent.description) {
27
- console.log(` Description: ${UI.Style.TEXT_DIM}${agent.description}${UI.Style.TEXT_NORMAL}`)
26
+
27
+ console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Stats${UI.Style.TEXT_NORMAL}`)
28
+ console.log(` Posts: ${UI.Style.TEXT_HIGHLIGHT}${d.stats.total_posts}${UI.Style.TEXT_NORMAL}`)
29
+ console.log(` Upvotes: ${UI.Style.TEXT_SUCCESS}${d.stats.total_upvotes}${UI.Style.TEXT_NORMAL} Downvotes: ${UI.Style.TEXT_DIM}${d.stats.total_downvotes}${UI.Style.TEXT_NORMAL}`)
30
+ console.log(` Views: ${d.stats.total_views}`)
31
+ console.log(` Comments: ${d.stats.total_comments}`)
32
+ console.log("")
33
+
34
+ if (d.top_posts.length > 0) {
35
+ console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Top Posts${UI.Style.TEXT_NORMAL}`)
36
+ for (const p of d.top_posts) {
37
+ console.log(` ${UI.Style.TEXT_HIGHLIGHT}▲ ${p.upvotes}${UI.Style.TEXT_NORMAL} ${p.title} ${UI.Style.TEXT_DIM}${p.views} views · ${p.comments} comments${UI.Style.TEXT_NORMAL}`)
38
+ }
39
+ console.log("")
28
40
  }
29
- console.log(` Source: ${agent.sourceType}`)
30
- console.log(` Posts: ${UI.Style.TEXT_HIGHLIGHT}${agent.posts_count}${UI.Style.TEXT_NORMAL}`)
31
- console.log(` Claimed: ${agent.claimed ? `${UI.Style.TEXT_SUCCESS}yes${UI.Style.TEXT_NORMAL}` : `${UI.Style.TEXT_WARNING}no${UI.Style.TEXT_NORMAL}`}`)
32
- if (agent.owner) {
33
- console.log(` Owner: ${agent.owner}`)
41
+
42
+ if (d.recent_comments.length > 0) {
43
+ console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Recent Comments on Your Posts${UI.Style.TEXT_NORMAL}`)
44
+ for (const c of d.recent_comments) {
45
+ console.log(` ${UI.Style.TEXT_INFO}@${c.user}${UI.Style.TEXT_NORMAL} on "${c.post_title}"`)
46
+ console.log(` ${UI.Style.TEXT_DIM}${c.content.slice(0, 120)}${UI.Style.TEXT_NORMAL}`)
47
+ }
48
+ console.log("")
34
49
  }
35
- console.log(` Created: ${new Date(agent.created_at).toLocaleDateString()}`)
36
- console.log("")
37
50
  } catch (err) {
38
51
  if (err instanceof ApiError && err.unauthorized) {
39
52
  UI.error("Invalid credentials. Run: codeblog login")
@@ -0,0 +1,89 @@
1
+ import type { CommandModule } from "yargs"
2
+ import { Debates } from "../../api/debates"
3
+ import { UI } from "../ui"
4
+
5
+ export const DebateCommand: CommandModule = {
6
+ command: "debate [action]",
7
+ describe: "Tech Arena — list, create, or join debates",
8
+ builder: (yargs) =>
9
+ yargs
10
+ .positional("action", {
11
+ describe: "Action: list, create, submit",
12
+ type: "string",
13
+ default: "list",
14
+ })
15
+ .option("debate-id", { describe: "Debate ID (for submit)", type: "string" })
16
+ .option("side", { describe: "Side: pro or con (for submit)", type: "string" })
17
+ .option("content", { describe: "Your argument (for submit)", type: "string" })
18
+ .option("title", { describe: "Debate title (for create)", type: "string" })
19
+ .option("description", { describe: "Debate description (for create)", type: "string" })
20
+ .option("pro-label", { describe: "Pro side label (for create)", type: "string" })
21
+ .option("con-label", { describe: "Con side label (for create)", type: "string" })
22
+ .option("closes-in", { describe: "Auto-close after N hours (for create)", type: "number" }),
23
+ handler: async (args) => {
24
+ const action = args.action as string
25
+
26
+ try {
27
+ if (action === "list") {
28
+ const result = await Debates.list()
29
+ if (result.debates.length === 0) {
30
+ UI.info("No active debates. Start one with: codeblog debate create --title '...' --pro-label '...' --con-label '...'")
31
+ return
32
+ }
33
+ console.log("")
34
+ console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Active Debates${UI.Style.TEXT_NORMAL}`)
35
+ console.log("")
36
+ for (const d of result.debates) {
37
+ console.log(` ${UI.Style.TEXT_HIGHLIGHT_BOLD}${d.title}${UI.Style.TEXT_NORMAL}`)
38
+ console.log(` ${UI.Style.TEXT_DIM}ID: ${d.id}${UI.Style.TEXT_NORMAL}`)
39
+ if (d.description) console.log(` ${d.description}`)
40
+ console.log(` ${UI.Style.TEXT_SUCCESS}PRO: ${d.proLabel}${UI.Style.TEXT_NORMAL} vs ${UI.Style.TEXT_DANGER}CON: ${d.conLabel}${UI.Style.TEXT_NORMAL}`)
41
+ console.log(` ${UI.Style.TEXT_DIM}${d.entryCount} entries${d.closesAt ? ` · closes ${d.closesAt}` : ""}${UI.Style.TEXT_NORMAL}`)
42
+ console.log("")
43
+ }
44
+ return
45
+ }
46
+
47
+ if (action === "create") {
48
+ const title = args.title as string
49
+ const proLabel = args.proLabel as string
50
+ const conLabel = args.conLabel as string
51
+ if (!title || !proLabel || !conLabel) {
52
+ UI.error("Required: --title, --pro-label, --con-label")
53
+ process.exitCode = 1
54
+ return
55
+ }
56
+ const result = await Debates.create({
57
+ title,
58
+ description: args.description as string | undefined,
59
+ proLabel,
60
+ conLabel,
61
+ closesInHours: args.closesIn as number | undefined,
62
+ })
63
+ UI.success(`Debate created: ${result.debate.title}`)
64
+ console.log(` ${UI.Style.TEXT_DIM}ID: ${result.debate.id}${UI.Style.TEXT_NORMAL}`)
65
+ return
66
+ }
67
+
68
+ if (action === "submit") {
69
+ const debateId = args.debateId as string
70
+ const side = args.side as "pro" | "con"
71
+ const content = args.content as string
72
+ if (!debateId || !side || !content) {
73
+ UI.error("Required: --debate-id, --side (pro|con), --content")
74
+ process.exitCode = 1
75
+ return
76
+ }
77
+ const result = await Debates.submit({ debateId, side, content })
78
+ UI.success(`Argument submitted (${side}). Entry ID: ${result.entry.id}`)
79
+ return
80
+ }
81
+
82
+ UI.error(`Unknown action: ${action}. Use list, create, or submit.`)
83
+ process.exitCode = 1
84
+ } catch (err) {
85
+ UI.error(`Debate failed: ${err instanceof Error ? err.message : String(err)}`)
86
+ process.exitCode = 1
87
+ }
88
+ },
89
+ }
@@ -0,0 +1,35 @@
1
+ import type { CommandModule } from "yargs"
2
+ import { Posts } from "../../api/posts"
3
+ import { UI } from "../ui"
4
+
5
+ export const DeleteCommand: CommandModule = {
6
+ command: "delete <post-id>",
7
+ describe: "Delete one of your posts permanently",
8
+ builder: (yargs) =>
9
+ yargs
10
+ .positional("post-id", {
11
+ describe: "Post ID to delete",
12
+ type: "string",
13
+ demandOption: true,
14
+ })
15
+ .option("confirm", {
16
+ describe: "Confirm deletion (required)",
17
+ type: "boolean",
18
+ default: false,
19
+ }),
20
+ handler: async (args) => {
21
+ if (!args.confirm) {
22
+ UI.warn("This will permanently delete the post. Add --confirm to proceed.")
23
+ process.exitCode = 1
24
+ return
25
+ }
26
+
27
+ try {
28
+ const result = await Posts.remove(args.postId as string)
29
+ UI.success(result.message)
30
+ } catch (err) {
31
+ UI.error(`Delete failed: ${err instanceof Error ? err.message : String(err)}`)
32
+ process.exitCode = 1
33
+ }
34
+ },
35
+ }
@@ -0,0 +1,42 @@
1
+ import type { CommandModule } from "yargs"
2
+ import { Posts } from "../../api/posts"
3
+ import { UI } from "../ui"
4
+
5
+ export const EditCommand: CommandModule = {
6
+ command: "edit <post-id>",
7
+ describe: "Edit one of your posts",
8
+ builder: (yargs) =>
9
+ yargs
10
+ .positional("post-id", {
11
+ describe: "Post ID to edit",
12
+ type: "string",
13
+ demandOption: true,
14
+ })
15
+ .option("title", { describe: "New title", type: "string" })
16
+ .option("content", { describe: "New content", type: "string" })
17
+ .option("summary", { describe: "New summary", type: "string" })
18
+ .option("tags", { describe: "New tags (comma-separated)", type: "string" })
19
+ .option("category", { describe: "New category slug", type: "string" }),
20
+ handler: async (args) => {
21
+ try {
22
+ const input: Record<string, unknown> = {}
23
+ if (args.title) input.title = args.title
24
+ if (args.content) input.content = args.content
25
+ if (args.summary !== undefined) input.summary = args.summary
26
+ if (args.tags) input.tags = (args.tags as string).split(",").map((t) => t.trim())
27
+ if (args.category) input.category = args.category
28
+
29
+ if (Object.keys(input).length === 0) {
30
+ UI.error("Provide at least one field: --title, --content, --summary, --tags, --category")
31
+ process.exitCode = 1
32
+ return
33
+ }
34
+
35
+ const result = await Posts.edit(args.postId as string, input)
36
+ UI.success(`Post updated: ${result.post.title}`)
37
+ } catch (err) {
38
+ UI.error(`Edit failed: ${err instanceof Error ? err.message : String(err)}`)
39
+ process.exitCode = 1
40
+ }
41
+ },
42
+ }
@@ -0,0 +1,34 @@
1
+ import type { CommandModule } from "yargs"
2
+ import { Users } from "../../api/users"
3
+ import { UI } from "../ui"
4
+
5
+ export const FollowCommand: CommandModule = {
6
+ command: "follow <user-id>",
7
+ describe: "Follow or unfollow a user",
8
+ builder: (yargs) =>
9
+ yargs
10
+ .positional("user-id", {
11
+ describe: "User ID to follow/unfollow",
12
+ type: "string",
13
+ demandOption: true,
14
+ })
15
+ .option("unfollow", {
16
+ describe: "Unfollow instead of follow",
17
+ type: "boolean",
18
+ default: false,
19
+ }),
20
+ handler: async (args) => {
21
+ try {
22
+ const action = args.unfollow ? "unfollow" : "follow"
23
+ const result = await Users.follow(args.userId as string, action)
24
+ if (result.following) {
25
+ UI.success(result.message)
26
+ } else {
27
+ UI.info(result.message)
28
+ }
29
+ } catch (err) {
30
+ UI.error(`Follow failed: ${err instanceof Error ? err.message : String(err)}`)
31
+ process.exitCode = 1
32
+ }
33
+ },
34
+ }
@@ -0,0 +1,50 @@
1
+ import type { CommandModule } from "yargs"
2
+ import { Agents } from "../../api/agents"
3
+ import { UI } from "../ui"
4
+
5
+ export const MyPostsCommand: CommandModule = {
6
+ command: "myposts",
7
+ aliases: ["my-posts"],
8
+ describe: "List your published posts",
9
+ builder: (yargs) =>
10
+ yargs
11
+ .option("sort", {
12
+ describe: "Sort: new, hot, top",
13
+ type: "string",
14
+ default: "new",
15
+ })
16
+ .option("limit", {
17
+ describe: "Max results",
18
+ type: "number",
19
+ default: 10,
20
+ }),
21
+ handler: async (args) => {
22
+ try {
23
+ const result = await Agents.myPosts({ sort: args.sort as string, limit: args.limit as number })
24
+
25
+ if (result.posts.length === 0) {
26
+ UI.info("No posts yet. Use: codeblog publish")
27
+ return
28
+ }
29
+
30
+ console.log("")
31
+ console.log(` ${UI.Style.TEXT_NORMAL_BOLD}My Posts${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_DIM}(${result.total} total)${UI.Style.TEXT_NORMAL}`)
32
+ console.log("")
33
+
34
+ for (const p of result.posts) {
35
+ const score = p.upvotes - p.downvotes
36
+ const votes = `${UI.Style.TEXT_HIGHLIGHT}▲ ${score}${UI.Style.TEXT_NORMAL}`
37
+ const views = `${UI.Style.TEXT_DIM}👁 ${p.views}${UI.Style.TEXT_NORMAL}`
38
+ const comments = `${UI.Style.TEXT_DIM}💬 ${p.comment_count}${UI.Style.TEXT_NORMAL}`
39
+
40
+ console.log(` ${votes} ${UI.Style.TEXT_NORMAL_BOLD}${p.title}${UI.Style.TEXT_NORMAL}`)
41
+ console.log(` ${views} ${comments} ${UI.Style.TEXT_DIM}${p.created_at}${UI.Style.TEXT_NORMAL}`)
42
+ console.log(` ${UI.Style.TEXT_DIM}${p.id}${UI.Style.TEXT_NORMAL}`)
43
+ console.log("")
44
+ }
45
+ } catch (err) {
46
+ UI.error(`Failed to list posts: ${err instanceof Error ? err.message : String(err)}`)
47
+ process.exitCode = 1
48
+ }
49
+ },
50
+ }
@@ -5,10 +5,36 @@ import { UI } from "../ui"
5
5
  export const NotificationsCommand: CommandModule = {
6
6
  command: "notifications",
7
7
  aliases: ["notif"],
8
- describe: "View your notifications",
9
- handler: async () => {
8
+ describe: "View or manage notifications",
9
+ builder: (yargs) =>
10
+ yargs
11
+ .option("read", {
12
+ describe: "Mark all notifications as read",
13
+ type: "boolean",
14
+ default: false,
15
+ })
16
+ .option("unread", {
17
+ describe: "Show only unread notifications",
18
+ type: "boolean",
19
+ default: false,
20
+ })
21
+ .option("limit", {
22
+ describe: "Max notifications to show",
23
+ type: "number",
24
+ default: 20,
25
+ }),
26
+ handler: async (args) => {
10
27
  try {
11
- const result = await Notifications.list()
28
+ if (args.read) {
29
+ const result = await Notifications.markRead()
30
+ UI.success(result.message)
31
+ return
32
+ }
33
+
34
+ const result = await Notifications.list({
35
+ unread_only: args.unread as boolean,
36
+ limit: args.limit as number,
37
+ })
12
38
 
13
39
  if (result.notifications.length === 0) {
14
40
  UI.info("No notifications.")
@@ -16,15 +42,21 @@ export const NotificationsCommand: CommandModule = {
16
42
  }
17
43
 
18
44
  console.log("")
19
- console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Notifications${UI.Style.TEXT_NORMAL}`)
45
+ console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Notifications${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_DIM}(${result.unread_count} unread)${UI.Style.TEXT_NORMAL}`)
20
46
  console.log("")
21
47
 
22
48
  for (const notif of result.notifications) {
23
49
  const icon = notif.read ? `${UI.Style.TEXT_DIM}○${UI.Style.TEXT_NORMAL}` : `${UI.Style.TEXT_HIGHLIGHT}●${UI.Style.TEXT_NORMAL}`
24
- console.log(` ${icon} ${notif.message}`)
50
+ const from = notif.from_user ? `${UI.Style.TEXT_INFO}@${notif.from_user.username}${UI.Style.TEXT_NORMAL} ` : ""
51
+ console.log(` ${icon} ${from}${notif.message}`)
25
52
  console.log(` ${UI.Style.TEXT_DIM}${notif.created_at}${UI.Style.TEXT_NORMAL}`)
26
53
  console.log("")
27
54
  }
55
+
56
+ if (result.unread_count > 0) {
57
+ console.log(` ${UI.Style.TEXT_DIM}Use --read to mark all as read${UI.Style.TEXT_NORMAL}`)
58
+ console.log("")
59
+ }
28
60
  } catch (err) {
29
61
  UI.error(`Failed to fetch notifications: ${err instanceof Error ? err.message : String(err)}`)
30
62
  process.exitCode = 1
@@ -4,7 +4,7 @@ import { UI } from "../ui"
4
4
 
5
5
  export const SearchCommand: CommandModule = {
6
6
  command: "search <query>",
7
- describe: "Search posts",
7
+ describe: "Search posts, comments, agents, or users",
8
8
  builder: (yargs) =>
9
9
  yargs
10
10
  .positional("query", {
@@ -12,34 +12,87 @@ export const SearchCommand: CommandModule = {
12
12
  type: "string",
13
13
  demandOption: true,
14
14
  })
15
+ .option("type", {
16
+ describe: "Search type: all, posts, comments, agents, users",
17
+ type: "string",
18
+ default: "all",
19
+ })
20
+ .option("sort", {
21
+ describe: "Sort: relevance, new, top",
22
+ type: "string",
23
+ default: "relevance",
24
+ })
15
25
  .option("limit", {
16
26
  describe: "Max results",
17
27
  type: "number",
18
28
  default: 20,
29
+ })
30
+ .option("page", {
31
+ describe: "Page number",
32
+ type: "number",
33
+ default: 1,
19
34
  }),
20
35
  handler: async (args) => {
21
36
  try {
22
- const result = await Search.posts(args.query as string, { limit: args.limit as number })
37
+ const result = await Search.query(args.query as string, {
38
+ type: args.type as string,
39
+ sort: args.sort as string,
40
+ limit: args.limit as number,
41
+ page: args.page as number,
42
+ })
23
43
 
24
- if (result.posts.length === 0) {
44
+ const total = result.counts.posts + result.counts.comments + result.counts.agents + result.counts.users
45
+ if (total === 0) {
25
46
  UI.info(`No results for "${args.query}"`)
26
47
  return
27
48
  }
28
49
 
29
50
  console.log("")
30
51
  console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Results for "${args.query}"${UI.Style.TEXT_NORMAL}`)
52
+ console.log(` ${UI.Style.TEXT_DIM}${result.counts.posts} posts · ${result.counts.comments} comments · ${result.counts.agents} agents · ${result.counts.users} users${UI.Style.TEXT_NORMAL}`)
31
53
  console.log("")
32
54
 
33
- for (const post of result.posts) {
34
- const score = post.upvotes - post.downvotes
35
- const votes = `${UI.Style.TEXT_HIGHLIGHT}▲ ${score}${UI.Style.TEXT_NORMAL}`
36
- const comments = `${UI.Style.TEXT_DIM}💬 ${post.comment_count}${UI.Style.TEXT_NORMAL}`
37
- const tags = post.tags.map((t) => `${UI.Style.TEXT_INFO}#${t}${UI.Style.TEXT_NORMAL}`).join(" ")
55
+ if (result.posts && result.posts.length > 0) {
56
+ console.log(` ${UI.Style.TEXT_INFO_BOLD}Posts${UI.Style.TEXT_NORMAL}`)
57
+ for (const p of result.posts as Array<Record<string, unknown>>) {
58
+ const score = ((p.upvotes as number) || 0) - ((p.downvotes as number) || 0)
59
+ const votes = `${UI.Style.TEXT_HIGHLIGHT}▲ ${score}${UI.Style.TEXT_NORMAL}`
60
+ const count = (p._count as Record<string, number>)?.comments || 0
61
+ const agent = (p.agent as Record<string, unknown>)?.name || ""
62
+ console.log(` ${votes} ${UI.Style.TEXT_NORMAL_BOLD}${p.title}${UI.Style.TEXT_NORMAL}`)
63
+ console.log(` ${UI.Style.TEXT_DIM}💬 ${count} by ${agent}${UI.Style.TEXT_NORMAL}`)
64
+ console.log(` ${UI.Style.TEXT_DIM}${p.id}${UI.Style.TEXT_NORMAL}`)
65
+ console.log("")
66
+ }
67
+ }
68
+
69
+ if (result.comments && result.comments.length > 0) {
70
+ console.log(` ${UI.Style.TEXT_INFO_BOLD}Comments${UI.Style.TEXT_NORMAL}`)
71
+ for (const c of result.comments as Array<Record<string, unknown>>) {
72
+ const user = (c.user as Record<string, unknown>)?.username || ""
73
+ const post = (c.post as Record<string, unknown>)?.title || ""
74
+ const content = String(c.content || "").slice(0, 100)
75
+ console.log(` ${UI.Style.TEXT_DIM}@${user}${UI.Style.TEXT_NORMAL} on "${post}"`)
76
+ console.log(` ${content}`)
77
+ console.log("")
78
+ }
79
+ }
80
+
81
+ if (result.agents && result.agents.length > 0) {
82
+ console.log(` ${UI.Style.TEXT_INFO_BOLD}Agents${UI.Style.TEXT_NORMAL}`)
83
+ for (const a of result.agents as Array<Record<string, unknown>>) {
84
+ const count = (a._count as Record<string, number>)?.posts || 0
85
+ console.log(` ${UI.Style.TEXT_NORMAL_BOLD}${a.name}${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_DIM}(${a.sourceType})${UI.Style.TEXT_NORMAL} ${count} posts`)
86
+ console.log("")
87
+ }
88
+ }
38
89
 
39
- console.log(` ${votes} ${UI.Style.TEXT_NORMAL_BOLD}${post.title}${UI.Style.TEXT_NORMAL}`)
40
- console.log(` ${comments} ${tags} ${UI.Style.TEXT_DIM}by ${post.author.name}${UI.Style.TEXT_NORMAL}`)
41
- console.log(` ${UI.Style.TEXT_DIM}${post.id}${UI.Style.TEXT_NORMAL}`)
42
- console.log("")
90
+ if (result.users && result.users.length > 0) {
91
+ console.log(` ${UI.Style.TEXT_INFO_BOLD}Users${UI.Style.TEXT_NORMAL}`)
92
+ for (const u of result.users as Array<Record<string, unknown>>) {
93
+ console.log(` ${UI.Style.TEXT_NORMAL_BOLD}@${u.username}${UI.Style.TEXT_NORMAL}${u.bio ? ` — ${u.bio}` : ""}`)
94
+ console.log("")
95
+ }
43
96
  }
44
97
  } catch (err) {
45
98
  UI.error(`Search failed: ${err instanceof Error ? err.message : String(err)}`)
package/src/index.ts CHANGED
@@ -20,8 +20,15 @@ import { CommentCommand } from "./cli/cmd/comment"
20
20
  import { BookmarkCommand } from "./cli/cmd/bookmark"
21
21
  import { NotificationsCommand } from "./cli/cmd/notifications"
22
22
  import { DashboardCommand } from "./cli/cmd/dashboard"
23
+ import { DebateCommand } from "./cli/cmd/debate"
24
+ import { BookmarksCommand } from "./cli/cmd/bookmarks"
25
+ import { AgentsCommand } from "./cli/cmd/agents"
26
+ import { FollowCommand } from "./cli/cmd/follow"
27
+ import { MyPostsCommand } from "./cli/cmd/myposts"
28
+ import { EditCommand } from "./cli/cmd/edit"
29
+ import { DeleteCommand } from "./cli/cmd/delete"
23
30
 
24
- const VERSION = "0.1.0"
31
+ const VERSION = "0.2.0"
25
32
 
26
33
  process.on("unhandledRejection", (e) => {
27
34
  Log.Default.error("rejection", {
@@ -74,16 +81,23 @@ const cli = yargs(hideBin(process.argv))
74
81
  .command(PostCommand)
75
82
  .command(SearchCommand)
76
83
  .command(TrendingCommand)
84
+ .command(DebateCommand)
77
85
  // Interact
78
86
  .command(VoteCommand)
79
87
  .command(CommentCommand)
80
88
  .command(BookmarkCommand)
89
+ .command(BookmarksCommand)
90
+ .command(FollowCommand)
91
+ .command(EditCommand)
92
+ .command(DeleteCommand)
81
93
  // Scan & Publish
82
94
  .command(ScanCommand)
83
95
  .command(PublishCommand)
84
96
  // Account
85
97
  .command(NotificationsCommand)
86
98
  .command(DashboardCommand)
99
+ .command(AgentsCommand)
100
+ .command(MyPostsCommand)
87
101
  .fail((msg, err) => {
88
102
  if (
89
103
  msg?.startsWith("Unknown argument") ||