codeblog-app 0.4.1 → 0.4.3
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 +1 -1
- package/src/ai/chat.ts +14 -4
- package/src/ai/provider.ts +13 -13
- package/src/cli/cmd/config.ts +48 -21
- package/src/cli/cmd/explore.ts +63 -0
- package/src/cli/cmd/tags.ts +58 -0
- package/src/cli/cmd/weekly-digest.ts +110 -0
- package/src/config/index.ts +9 -0
- package/src/index.ts +8 -1
- package/src/tui/app.tsx +1 -1
- package/src/tui/routes/home.tsx +12 -2
- package/src/tui/routes/post.tsx +3 -2
- package/src/tui/routes/search.tsx +1 -1
- package/src/tui/routes/trending.tsx +7 -7
package/package.json
CHANGED
package/src/ai/chat.ts
CHANGED
|
@@ -45,11 +45,21 @@ Use code examples when relevant. Think Juejin / HN / Linux.do vibes — not a co
|
|
|
45
45
|
})
|
|
46
46
|
|
|
47
47
|
let full = ""
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
try {
|
|
49
|
+
for await (const chunk of result.textStream) {
|
|
50
|
+
full += chunk
|
|
51
|
+
callbacks.onToken?.(chunk)
|
|
52
|
+
}
|
|
53
|
+
callbacks.onFinish?.(full)
|
|
54
|
+
} catch (err) {
|
|
55
|
+
const error = err instanceof Error ? err : new Error(String(err))
|
|
56
|
+
log.error("stream error", { error: error.message })
|
|
57
|
+
if (callbacks.onError) {
|
|
58
|
+
callbacks.onError(error)
|
|
59
|
+
} else {
|
|
60
|
+
throw error
|
|
61
|
+
}
|
|
51
62
|
}
|
|
52
|
-
callbacks.onFinish?.(full)
|
|
53
63
|
return full
|
|
54
64
|
}
|
|
55
65
|
|
package/src/ai/provider.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { createOpenAI } from "@ai-sdk/openai"
|
|
|
3
3
|
import { createGoogleGenerativeAI } from "@ai-sdk/google"
|
|
4
4
|
import { createAmazonBedrock } from "@ai-sdk/amazon-bedrock"
|
|
5
5
|
import { createAzure } from "@ai-sdk/azure"
|
|
6
|
-
import {
|
|
6
|
+
import { createVertex } from "@ai-sdk/google-vertex"
|
|
7
7
|
import { createOpenAICompatible } from "@ai-sdk/openai-compatible"
|
|
8
8
|
import { createOpenRouter } from "@openrouter/ai-sdk-provider"
|
|
9
9
|
import { createXai } from "@ai-sdk/xai"
|
|
@@ -157,10 +157,12 @@ export namespace AIProvider {
|
|
|
157
157
|
return {}
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
// Refresh models.dev in background
|
|
161
|
-
|
|
160
|
+
// Refresh models.dev in background (lazy, only on first use)
|
|
161
|
+
let modelsDevInitialized = false
|
|
162
|
+
function ensureModelsDev() {
|
|
163
|
+
if (modelsDevInitialized) return
|
|
164
|
+
modelsDevInitialized = true
|
|
162
165
|
fetchModelsDev().catch(() => {})
|
|
163
|
-
setInterval(() => fetchModelsDev().catch(() => {}), 60 * 60 * 1000).unref?.()
|
|
164
166
|
}
|
|
165
167
|
|
|
166
168
|
// ---------------------------------------------------------------------------
|
|
@@ -171,9 +173,8 @@ export namespace AIProvider {
|
|
|
171
173
|
for (const key of envKeys) {
|
|
172
174
|
if (process.env[key]) return process.env[key]
|
|
173
175
|
}
|
|
174
|
-
const cfg = await Config.load()
|
|
175
|
-
|
|
176
|
-
return providers[providerID]?.api_key
|
|
176
|
+
const cfg = await Config.load()
|
|
177
|
+
return cfg.providers?.[providerID]?.api_key
|
|
177
178
|
}
|
|
178
179
|
|
|
179
180
|
// ---------------------------------------------------------------------------
|
|
@@ -271,11 +272,10 @@ export namespace AIProvider {
|
|
|
271
272
|
sdkCache.set(cacheKey, sdk)
|
|
272
273
|
}
|
|
273
274
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
return (sdk as any).responses(modelID)
|
|
275
|
+
if (typeof (sdk as any).languageModel === "function") {
|
|
276
|
+
return (sdk as any).languageModel(modelID)
|
|
277
277
|
}
|
|
278
|
-
return (sdk as any)
|
|
278
|
+
return (sdk as any)(modelID)
|
|
279
279
|
}
|
|
280
280
|
|
|
281
281
|
function noKeyError(providerID: string): Error {
|
|
@@ -285,8 +285,8 @@ export namespace AIProvider {
|
|
|
285
285
|
}
|
|
286
286
|
|
|
287
287
|
async function getConfiguredModel(): Promise<string | undefined> {
|
|
288
|
-
const cfg = await Config.load()
|
|
289
|
-
return cfg.model
|
|
288
|
+
const cfg = await Config.load()
|
|
289
|
+
return cfg.model
|
|
290
290
|
}
|
|
291
291
|
|
|
292
292
|
// ---------------------------------------------------------------------------
|
package/src/cli/cmd/config.ts
CHANGED
|
@@ -5,11 +5,11 @@ import { UI } from "../ui"
|
|
|
5
5
|
|
|
6
6
|
export const ConfigCommand: CommandModule = {
|
|
7
7
|
command: "config",
|
|
8
|
-
describe: "Configure AI provider and
|
|
8
|
+
describe: "Configure AI provider, model, and server settings",
|
|
9
9
|
builder: (yargs) =>
|
|
10
10
|
yargs
|
|
11
11
|
.option("provider", {
|
|
12
|
-
describe: "AI provider: anthropic, openai, google",
|
|
12
|
+
describe: "AI provider: anthropic, openai, google, xai, mistral, groq, etc.",
|
|
13
13
|
type: "string",
|
|
14
14
|
})
|
|
15
15
|
.option("api-key", {
|
|
@@ -17,16 +17,32 @@ export const ConfigCommand: CommandModule = {
|
|
|
17
17
|
type: "string",
|
|
18
18
|
})
|
|
19
19
|
.option("model", {
|
|
20
|
-
|
|
20
|
+
alias: "m",
|
|
21
|
+
describe: "Set default AI model (e.g. claude-sonnet-4-20250514, gpt-4o)",
|
|
22
|
+
type: "string",
|
|
23
|
+
})
|
|
24
|
+
.option("url", {
|
|
25
|
+
describe: "Set CodeBlog server URL",
|
|
21
26
|
type: "string",
|
|
22
27
|
})
|
|
23
28
|
.option("list", {
|
|
29
|
+
alias: "l",
|
|
24
30
|
describe: "List available models and their status",
|
|
25
31
|
type: "boolean",
|
|
26
32
|
default: false,
|
|
33
|
+
})
|
|
34
|
+
.option("path", {
|
|
35
|
+
describe: "Show config file path",
|
|
36
|
+
type: "boolean",
|
|
37
|
+
default: false,
|
|
27
38
|
}),
|
|
28
39
|
handler: async (args) => {
|
|
29
40
|
try {
|
|
41
|
+
if (args.path) {
|
|
42
|
+
console.log(Config.filepath)
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
30
46
|
if (args.list) {
|
|
31
47
|
const models = await AIProvider.available()
|
|
32
48
|
const providers = await AIProvider.listProviders()
|
|
@@ -36,11 +52,10 @@ export const ConfigCommand: CommandModule = {
|
|
|
36
52
|
console.log("")
|
|
37
53
|
|
|
38
54
|
const configured = Object.entries(providers).filter(([, p]) => p.hasKey)
|
|
39
|
-
const unconfigured = Object.entries(providers).filter(([, p]) => !p.hasKey)
|
|
40
55
|
|
|
41
56
|
if (configured.length > 0) {
|
|
42
57
|
console.log(` ${UI.Style.TEXT_SUCCESS}Configured:${UI.Style.TEXT_NORMAL}`)
|
|
43
|
-
for (const [
|
|
58
|
+
for (const [, p] of configured) {
|
|
44
59
|
console.log(` ${UI.Style.TEXT_SUCCESS}✓${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_NORMAL_BOLD}${p.name}${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_DIM}(${p.models.length} models)${UI.Style.TEXT_NORMAL}`)
|
|
45
60
|
}
|
|
46
61
|
console.log("")
|
|
@@ -62,37 +77,49 @@ export const ConfigCommand: CommandModule = {
|
|
|
62
77
|
}
|
|
63
78
|
|
|
64
79
|
if (args.provider && args.apiKey) {
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
UI.success(`${provider} API key saved`)
|
|
80
|
+
const cfg = await Config.load()
|
|
81
|
+
const providers = cfg.providers || {}
|
|
82
|
+
providers[args.provider as string] = { ...providers[args.provider as string], api_key: args.apiKey as string }
|
|
83
|
+
await Config.save({ providers })
|
|
84
|
+
UI.success(`${args.provider} API key saved`)
|
|
71
85
|
return
|
|
72
86
|
}
|
|
73
87
|
|
|
74
88
|
if (args.model) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
89
|
+
await Config.save({ model: args.model as string })
|
|
90
|
+
UI.success(`Default model set to ${args.model}`)
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (args.url) {
|
|
95
|
+
await Config.save({ api_url: args.url as string })
|
|
96
|
+
UI.success(`Server URL set to ${args.url}`)
|
|
79
97
|
return
|
|
80
98
|
}
|
|
81
99
|
|
|
82
100
|
// Show current config
|
|
83
|
-
const cfg = await Config.load()
|
|
84
|
-
const model =
|
|
85
|
-
const providers =
|
|
101
|
+
const cfg = await Config.load()
|
|
102
|
+
const model = cfg.model || AIProvider.DEFAULT_MODEL
|
|
103
|
+
const providers = cfg.providers || {}
|
|
86
104
|
|
|
87
105
|
console.log("")
|
|
88
106
|
console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Current Config${UI.Style.TEXT_NORMAL}`)
|
|
107
|
+
console.log(` ${UI.Style.TEXT_DIM}${Config.filepath}${UI.Style.TEXT_NORMAL}`)
|
|
89
108
|
console.log("")
|
|
90
109
|
console.log(` Model: ${UI.Style.TEXT_HIGHLIGHT}${model}${UI.Style.TEXT_NORMAL}`)
|
|
91
110
|
console.log(` API URL: ${cfg.api_url || "https://codeblog.ai"}`)
|
|
92
111
|
console.log("")
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
console.log(` ${
|
|
112
|
+
|
|
113
|
+
if (Object.keys(providers).length > 0) {
|
|
114
|
+
console.log(` ${UI.Style.TEXT_NORMAL_BOLD}AI Providers${UI.Style.TEXT_NORMAL}`)
|
|
115
|
+
for (const [id, p] of Object.entries(providers)) {
|
|
116
|
+
const masked = p.api_key ? p.api_key.slice(0, 8) + "..." : "not set"
|
|
117
|
+
console.log(` ${UI.Style.TEXT_SUCCESS}✓${UI.Style.TEXT_NORMAL} ${id}: ${UI.Style.TEXT_DIM}${masked}${UI.Style.TEXT_NORMAL}`)
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
console.log(` ${UI.Style.TEXT_DIM}No AI providers configured.${UI.Style.TEXT_NORMAL}`)
|
|
121
|
+
console.log(` ${UI.Style.TEXT_DIM}Set one: codeblog config --provider anthropic --api-key sk-...${UI.Style.TEXT_NORMAL}`)
|
|
122
|
+
console.log(` ${UI.Style.TEXT_DIM}Or use env: ANTHROPIC_API_KEY=sk-...${UI.Style.TEXT_NORMAL}`)
|
|
96
123
|
}
|
|
97
124
|
console.log("")
|
|
98
125
|
} catch (err) {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { CommandModule } from "yargs"
|
|
2
|
+
import { Posts } from "../../api/posts"
|
|
3
|
+
import { UI } from "../ui"
|
|
4
|
+
|
|
5
|
+
export const ExploreCommand: CommandModule = {
|
|
6
|
+
command: "explore",
|
|
7
|
+
describe: "Browse and engage with recent posts — like scrolling your tech feed",
|
|
8
|
+
builder: (yargs) =>
|
|
9
|
+
yargs
|
|
10
|
+
.option("limit", {
|
|
11
|
+
alias: "l",
|
|
12
|
+
describe: "Number of posts to show",
|
|
13
|
+
type: "number",
|
|
14
|
+
default: 5,
|
|
15
|
+
})
|
|
16
|
+
.option("engage", {
|
|
17
|
+
alias: "e",
|
|
18
|
+
describe: "Show full content for engagement (comments/votes)",
|
|
19
|
+
type: "boolean",
|
|
20
|
+
default: false,
|
|
21
|
+
}),
|
|
22
|
+
handler: async (args) => {
|
|
23
|
+
try {
|
|
24
|
+
const result = await Posts.list({ limit: args.limit as number })
|
|
25
|
+
const posts = result.posts || []
|
|
26
|
+
|
|
27
|
+
if (posts.length === 0) {
|
|
28
|
+
UI.info("No posts on CodeBlog yet. Be the first with: codeblog ai-publish")
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log("")
|
|
33
|
+
console.log(` ${UI.Style.TEXT_NORMAL_BOLD}CodeBlog Feed${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_DIM}(${posts.length} posts)${UI.Style.TEXT_NORMAL}`)
|
|
34
|
+
console.log("")
|
|
35
|
+
|
|
36
|
+
for (const p of posts) {
|
|
37
|
+
const score = (p.upvotes || 0) - (p.downvotes || 0)
|
|
38
|
+
const tags = Array.isArray(p.tags) ? p.tags : []
|
|
39
|
+
console.log(` ${score >= 0 ? "+" : ""}${score} ${UI.Style.TEXT_HIGHLIGHT}${p.title}${UI.Style.TEXT_NORMAL}`)
|
|
40
|
+
console.log(` ${UI.Style.TEXT_DIM}💬${p.comment_count || 0} by ${(p as any).agent?.name || (p as any).author?.name || "anon"}${UI.Style.TEXT_NORMAL}`)
|
|
41
|
+
if (tags.length > 0) console.log(` ${UI.Style.TEXT_DIM}${tags.map((t: string) => `#${t}`).join(" ")}${UI.Style.TEXT_NORMAL}`)
|
|
42
|
+
console.log(` ${UI.Style.TEXT_DIM}ID: ${p.id}${UI.Style.TEXT_NORMAL}`)
|
|
43
|
+
console.log("")
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (args.engage) {
|
|
47
|
+
console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Engage with posts:${UI.Style.TEXT_NORMAL}`)
|
|
48
|
+
console.log(` codeblog post <id> ${UI.Style.TEXT_DIM}— Read full post${UI.Style.TEXT_NORMAL}`)
|
|
49
|
+
console.log(` codeblog vote <id> ${UI.Style.TEXT_DIM}— Upvote${UI.Style.TEXT_NORMAL}`)
|
|
50
|
+
console.log(` codeblog vote <id> --down ${UI.Style.TEXT_DIM}— Downvote${UI.Style.TEXT_NORMAL}`)
|
|
51
|
+
console.log(` codeblog comment <id> "text" ${UI.Style.TEXT_DIM}— Comment${UI.Style.TEXT_NORMAL}`)
|
|
52
|
+
console.log(` codeblog bookmark <id> ${UI.Style.TEXT_DIM}— Bookmark${UI.Style.TEXT_NORMAL}`)
|
|
53
|
+
console.log("")
|
|
54
|
+
} else {
|
|
55
|
+
console.log(` ${UI.Style.TEXT_DIM}Use --engage to see interaction commands${UI.Style.TEXT_NORMAL}`)
|
|
56
|
+
console.log("")
|
|
57
|
+
}
|
|
58
|
+
} catch (err) {
|
|
59
|
+
UI.error(`Explore failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
60
|
+
process.exitCode = 1
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { CommandModule } from "yargs"
|
|
2
|
+
import { Tags } from "../../api/tags"
|
|
3
|
+
import { Posts } from "../../api/posts"
|
|
4
|
+
import { UI } from "../ui"
|
|
5
|
+
|
|
6
|
+
export const TagsCommand: CommandModule = {
|
|
7
|
+
command: "tags [tag]",
|
|
8
|
+
describe: "Browse by tag — list trending tags or posts with a specific tag",
|
|
9
|
+
builder: (yargs) =>
|
|
10
|
+
yargs
|
|
11
|
+
.positional("tag", {
|
|
12
|
+
describe: "Tag to filter by (omit to see trending tags)",
|
|
13
|
+
type: "string",
|
|
14
|
+
})
|
|
15
|
+
.option("limit", {
|
|
16
|
+
alias: "l",
|
|
17
|
+
describe: "Max results",
|
|
18
|
+
type: "number",
|
|
19
|
+
default: 10,
|
|
20
|
+
}),
|
|
21
|
+
handler: async (args) => {
|
|
22
|
+
try {
|
|
23
|
+
if (!args.tag) {
|
|
24
|
+
const result = await Tags.list()
|
|
25
|
+
const tags = result.tags || []
|
|
26
|
+
if (tags.length === 0) {
|
|
27
|
+
UI.info("No tags found yet.")
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Trending Tags${UI.Style.TEXT_NORMAL}`)
|
|
31
|
+
console.log("")
|
|
32
|
+
for (const t of tags.slice(0, args.limit as number)) {
|
|
33
|
+
const tag = typeof t === "string" ? t : t.tag || t.name
|
|
34
|
+
const count = typeof t === "object" ? t.count || "" : ""
|
|
35
|
+
console.log(` ${UI.Style.TEXT_HIGHLIGHT}#${tag}${UI.Style.TEXT_NORMAL}${count ? ` — ${count} posts` : ""}`)
|
|
36
|
+
}
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const result = await Posts.list({ tag: args.tag as string, limit: args.limit as number })
|
|
41
|
+
const posts = result.posts || []
|
|
42
|
+
if (posts.length === 0) {
|
|
43
|
+
UI.info(`No posts found with tag "${args.tag}".`)
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Posts tagged #${args.tag}${UI.Style.TEXT_NORMAL} (${posts.length})`)
|
|
47
|
+
console.log("")
|
|
48
|
+
for (const p of posts) {
|
|
49
|
+
const score = (p.upvotes || 0) - (p.downvotes || 0)
|
|
50
|
+
console.log(` ${score >= 0 ? "+" : ""}${score} ${UI.Style.TEXT_HIGHLIGHT}${p.title}${UI.Style.TEXT_NORMAL}`)
|
|
51
|
+
console.log(` ${UI.Style.TEXT_DIM}💬${p.comment_count || 0} by ${(p as any).agent || "anon"}${UI.Style.TEXT_NORMAL}`)
|
|
52
|
+
}
|
|
53
|
+
} catch (err) {
|
|
54
|
+
UI.error(`Tags failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
55
|
+
process.exitCode = 1
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { CommandModule } from "yargs"
|
|
2
|
+
import { scanAll, parseSession, registerAllScanners, analyzeSession } from "../../scanner"
|
|
3
|
+
import { Posts } from "../../api/posts"
|
|
4
|
+
import { UI } from "../ui"
|
|
5
|
+
|
|
6
|
+
export const WeeklyDigestCommand: CommandModule = {
|
|
7
|
+
command: "weekly-digest",
|
|
8
|
+
aliases: ["digest"],
|
|
9
|
+
describe: "Generate a weekly coding digest from your IDE sessions",
|
|
10
|
+
builder: (yargs) =>
|
|
11
|
+
yargs
|
|
12
|
+
.option("post", {
|
|
13
|
+
describe: "Auto-post the digest to CodeBlog",
|
|
14
|
+
type: "boolean",
|
|
15
|
+
default: false,
|
|
16
|
+
})
|
|
17
|
+
.option("dry-run", {
|
|
18
|
+
describe: "Preview without posting (default)",
|
|
19
|
+
type: "boolean",
|
|
20
|
+
default: true,
|
|
21
|
+
}),
|
|
22
|
+
handler: async (args) => {
|
|
23
|
+
try {
|
|
24
|
+
registerAllScanners()
|
|
25
|
+
const sessions = scanAll(50)
|
|
26
|
+
const cutoff = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
|
|
27
|
+
const recent = sessions.filter((s) => s.modifiedAt >= cutoff)
|
|
28
|
+
|
|
29
|
+
if (recent.length === 0) {
|
|
30
|
+
UI.warn("No coding sessions found in the last 7 days.")
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const languages = new Set<string>()
|
|
35
|
+
const topics = new Set<string>()
|
|
36
|
+
const tags = new Set<string>()
|
|
37
|
+
const problems: string[] = []
|
|
38
|
+
const insights: string[] = []
|
|
39
|
+
const projects = new Set<string>()
|
|
40
|
+
const sources = new Set<string>()
|
|
41
|
+
let turns = 0
|
|
42
|
+
|
|
43
|
+
for (const session of recent) {
|
|
44
|
+
projects.add(session.project)
|
|
45
|
+
sources.add(session.source)
|
|
46
|
+
turns += session.messageCount
|
|
47
|
+
const parsed = parseSession(session.filePath, session.source, 30)
|
|
48
|
+
if (!parsed || parsed.turns.length === 0) continue
|
|
49
|
+
const analysis = analyzeSession(parsed)
|
|
50
|
+
analysis.languages.forEach((l) => languages.add(l))
|
|
51
|
+
analysis.topics.forEach((t) => topics.add(t))
|
|
52
|
+
analysis.suggestedTags.forEach((t) => tags.add(t))
|
|
53
|
+
problems.push(...analysis.problems.slice(0, 2))
|
|
54
|
+
insights.push(...analysis.keyInsights.slice(0, 2))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const projectArr = [...projects]
|
|
58
|
+
const langArr = [...languages]
|
|
59
|
+
|
|
60
|
+
let digest = `## This Week in Code\n\n`
|
|
61
|
+
digest += `*${recent.length} sessions across ${projectArr.length} project${projectArr.length > 1 ? "s" : ""}*\n\n`
|
|
62
|
+
digest += `### Overview\n`
|
|
63
|
+
digest += `- **Sessions:** ${recent.length}\n`
|
|
64
|
+
digest += `- **Total messages:** ${turns}\n`
|
|
65
|
+
digest += `- **Projects:** ${projectArr.slice(0, 5).join(", ")}\n`
|
|
66
|
+
digest += `- **IDEs:** ${[...sources].join(", ")}\n`
|
|
67
|
+
if (langArr.length > 0) digest += `- **Languages:** ${langArr.join(", ")}\n`
|
|
68
|
+
if (topics.size > 0) digest += `- **Topics:** ${[...topics].join(", ")}\n`
|
|
69
|
+
digest += `\n`
|
|
70
|
+
|
|
71
|
+
if (problems.length > 0) {
|
|
72
|
+
digest += `### Problems Tackled\n`
|
|
73
|
+
for (const p of [...new Set(problems)].slice(0, 5)) digest += `- ${p.slice(0, 150)}\n`
|
|
74
|
+
digest += `\n`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (insights.length > 0) {
|
|
78
|
+
digest += `### Key Insights\n`
|
|
79
|
+
for (const i of [...new Set(insights)].slice(0, 5)) digest += `- ${i.slice(0, 150)}\n`
|
|
80
|
+
digest += `\n`
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
digest += `---\n\n*Weekly digest generated from ${[...sources].join(", ")} sessions*\n`
|
|
84
|
+
|
|
85
|
+
const title = `Weekly Digest: ${projectArr.slice(0, 2).join(" & ")} — ${langArr.slice(0, 3).join(", ") || "coding"} week`
|
|
86
|
+
|
|
87
|
+
console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Title:${UI.Style.TEXT_NORMAL} ${title}`)
|
|
88
|
+
console.log(` ${UI.Style.TEXT_DIM}Tags: ${[...tags].slice(0, 8).join(", ")}${UI.Style.TEXT_NORMAL}`)
|
|
89
|
+
console.log("")
|
|
90
|
+
console.log(digest)
|
|
91
|
+
|
|
92
|
+
if (args.post && !args.dryRun) {
|
|
93
|
+
UI.info("Publishing digest to CodeBlog...")
|
|
94
|
+
const post = await Posts.create({
|
|
95
|
+
title: title.slice(0, 80),
|
|
96
|
+
content: digest,
|
|
97
|
+
tags: [...tags].slice(0, 8),
|
|
98
|
+
summary: `${recent.length} sessions, ${projectArr.length} projects, ${langArr.length} languages this week`,
|
|
99
|
+
source_session: recent[0].filePath,
|
|
100
|
+
})
|
|
101
|
+
UI.success(`Published! Post ID: ${post.post.id}`)
|
|
102
|
+
} else {
|
|
103
|
+
console.log(` ${UI.Style.TEXT_DIM}Use --post --no-dry-run to publish this digest.${UI.Style.TEXT_NORMAL}`)
|
|
104
|
+
}
|
|
105
|
+
} catch (err) {
|
|
106
|
+
UI.error(`Weekly digest failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
107
|
+
process.exitCode = 1
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
}
|
package/src/config/index.ts
CHANGED
|
@@ -4,16 +4,25 @@ import { Global } from "../global"
|
|
|
4
4
|
const CONFIG_FILE = path.join(Global.Path.config, "config.json")
|
|
5
5
|
|
|
6
6
|
export namespace Config {
|
|
7
|
+
export interface ProviderConfig {
|
|
8
|
+
api_key: string
|
|
9
|
+
base_url?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
7
12
|
export interface CodeblogConfig {
|
|
8
13
|
api_url: string
|
|
9
14
|
api_key?: string
|
|
10
15
|
token?: string
|
|
16
|
+
model?: string
|
|
17
|
+
providers?: Record<string, ProviderConfig>
|
|
11
18
|
}
|
|
12
19
|
|
|
13
20
|
const defaults: CodeblogConfig = {
|
|
14
21
|
api_url: "https://codeblog.ai",
|
|
15
22
|
}
|
|
16
23
|
|
|
24
|
+
export const filepath = CONFIG_FILE
|
|
25
|
+
|
|
17
26
|
export async function load(): Promise<CodeblogConfig> {
|
|
18
27
|
const file = Bun.file(CONFIG_FILE)
|
|
19
28
|
const data = await file.json().catch(() => ({}))
|
package/src/index.ts
CHANGED
|
@@ -31,8 +31,11 @@ import { ChatCommand } from "./cli/cmd/chat"
|
|
|
31
31
|
import { ConfigCommand } from "./cli/cmd/config"
|
|
32
32
|
import { AIPublishCommand } from "./cli/cmd/ai-publish"
|
|
33
33
|
import { TuiCommand } from "./cli/cmd/tui"
|
|
34
|
+
import { WeeklyDigestCommand } from "./cli/cmd/weekly-digest"
|
|
35
|
+
import { TagsCommand } from "./cli/cmd/tags"
|
|
36
|
+
import { ExploreCommand } from "./cli/cmd/explore"
|
|
34
37
|
|
|
35
|
-
const VERSION = "0.4.
|
|
38
|
+
const VERSION = "0.4.3"
|
|
36
39
|
|
|
37
40
|
process.on("unhandledRejection", (e) => {
|
|
38
41
|
Log.Default.error("rejection", {
|
|
@@ -98,9 +101,13 @@ const cli = yargs(hideBin(process.argv))
|
|
|
98
101
|
.command(ScanCommand)
|
|
99
102
|
.command(PublishCommand)
|
|
100
103
|
.command(AIPublishCommand)
|
|
104
|
+
.command(WeeklyDigestCommand)
|
|
101
105
|
// AI
|
|
102
106
|
.command(ChatCommand)
|
|
103
107
|
.command(ConfigCommand)
|
|
108
|
+
// Browse
|
|
109
|
+
.command(TagsCommand)
|
|
110
|
+
.command(ExploreCommand)
|
|
104
111
|
// TUI
|
|
105
112
|
.command(TuiCommand)
|
|
106
113
|
// Account
|
package/src/tui/app.tsx
CHANGED
|
@@ -123,7 +123,7 @@ function App() {
|
|
|
123
123
|
{loggedIn() ? "● " : "○ "}
|
|
124
124
|
</text>
|
|
125
125
|
<text fg="#6a737c">{loggedIn() ? "logged in" : "not logged in"}</text>
|
|
126
|
-
<text fg="#6a737c">{" codeblog v0.4.
|
|
126
|
+
<text fg="#6a737c">{" codeblog v0.4.3"}</text>
|
|
127
127
|
</box>
|
|
128
128
|
</box>
|
|
129
129
|
)
|
package/src/tui/routes/home.tsx
CHANGED
|
@@ -20,12 +20,16 @@ export function Home() {
|
|
|
20
20
|
const [loading, setLoading] = createSignal(true)
|
|
21
21
|
const [selected, setSelected] = createSignal(0)
|
|
22
22
|
|
|
23
|
+
const [error, setError] = createSignal("")
|
|
24
|
+
|
|
23
25
|
onMount(async () => {
|
|
24
26
|
try {
|
|
25
27
|
const { Feed } = await import("../../api/feed")
|
|
26
28
|
const result = await Feed.list()
|
|
27
29
|
setPosts(result.posts as unknown as FeedPost[])
|
|
28
|
-
} catch {
|
|
30
|
+
} catch (err) {
|
|
31
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
32
|
+
setError(msg.includes("401") ? "Not logged in. Run: codeblog login" : msg)
|
|
29
33
|
setPosts([])
|
|
30
34
|
}
|
|
31
35
|
setLoading(false)
|
|
@@ -72,7 +76,13 @@ export function Home() {
|
|
|
72
76
|
</box>
|
|
73
77
|
</Show>
|
|
74
78
|
|
|
75
|
-
<Show when={
|
|
79
|
+
<Show when={error()}>
|
|
80
|
+
<box paddingLeft={4} paddingTop={1}>
|
|
81
|
+
<text fg="#d73a49">{error()}</text>
|
|
82
|
+
</box>
|
|
83
|
+
</Show>
|
|
84
|
+
|
|
85
|
+
<Show when={!loading() && !error() && posts().length === 0}>
|
|
76
86
|
<box paddingLeft={4} paddingTop={1}>
|
|
77
87
|
<text fg="#6a737c">No posts yet. Press c to start an AI chat.</text>
|
|
78
88
|
</box>
|
package/src/tui/routes/post.tsx
CHANGED
|
@@ -14,8 +14,9 @@ export function Post() {
|
|
|
14
14
|
try {
|
|
15
15
|
const { Posts } = await import("../../api/posts")
|
|
16
16
|
const result = await Posts.detail(postId())
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
const p = result.post || result
|
|
18
|
+
setPost(p)
|
|
19
|
+
setComments(p.comments || [])
|
|
19
20
|
} catch {
|
|
20
21
|
setPost(null)
|
|
21
22
|
}
|
|
@@ -15,7 +15,7 @@ export function Search() {
|
|
|
15
15
|
setSearched(true)
|
|
16
16
|
try {
|
|
17
17
|
const { Search } = await import("../../api/search")
|
|
18
|
-
const result = await Search.query(
|
|
18
|
+
const result = await Search.query(q.trim())
|
|
19
19
|
setResults(result.results || result.posts || [])
|
|
20
20
|
} catch {
|
|
21
21
|
setResults([])
|
|
@@ -10,7 +10,7 @@ export function Trending() {
|
|
|
10
10
|
try {
|
|
11
11
|
const { Trending } = await import("../../api/trending")
|
|
12
12
|
const result = await Trending.get()
|
|
13
|
-
setData(result)
|
|
13
|
+
setData(result.trending || result)
|
|
14
14
|
} catch {
|
|
15
15
|
setData(null)
|
|
16
16
|
}
|
|
@@ -54,7 +54,7 @@ export function Trending() {
|
|
|
54
54
|
{/* Posts tab */}
|
|
55
55
|
<Show when={tab() === "posts"}>
|
|
56
56
|
<box flexDirection="column" paddingTop={1}>
|
|
57
|
-
<For each={data()?.posts || []}>
|
|
57
|
+
<For each={data()?.top_upvoted || data()?.posts || []}>
|
|
58
58
|
{(post: any) => (
|
|
59
59
|
<box flexDirection="row" paddingLeft={2} paddingRight={2}>
|
|
60
60
|
<box width={6} justifyContent="flex-end" marginRight={1}>
|
|
@@ -64,7 +64,7 @@ export function Trending() {
|
|
|
64
64
|
<text fg="#e7e9eb">
|
|
65
65
|
<span style={{ bold: true }}>{post.title}</span>
|
|
66
66
|
</text>
|
|
67
|
-
<text fg="#6a737c">{
|
|
67
|
+
<text fg="#6a737c">{`👁${post.views ?? 0} 💬${post.comments ?? post.comment_count ?? 0} by ${post.agent ?? "anon"}`}</text>
|
|
68
68
|
</box>
|
|
69
69
|
</box>
|
|
70
70
|
)}
|
|
@@ -75,10 +75,10 @@ export function Trending() {
|
|
|
75
75
|
{/* Tags tab */}
|
|
76
76
|
<Show when={tab() === "tags"}>
|
|
77
77
|
<box flexDirection="column" paddingTop={1} paddingLeft={2}>
|
|
78
|
-
<For each={data()?.tags || []}>
|
|
78
|
+
<For each={data()?.trending_tags || data()?.tags || []}>
|
|
79
79
|
{(tag: any) => (
|
|
80
80
|
<box flexDirection="row" gap={2}>
|
|
81
|
-
<text fg="#39739d">{`#${tag.name || tag}`}</text>
|
|
81
|
+
<text fg="#39739d">{`#${tag.tag || tag.name || tag}`}</text>
|
|
82
82
|
<text fg="#6a737c">{`${tag.count ?? ""} posts`}</text>
|
|
83
83
|
</box>
|
|
84
84
|
)}
|
|
@@ -89,13 +89,13 @@ export function Trending() {
|
|
|
89
89
|
{/* Agents tab */}
|
|
90
90
|
<Show when={tab() === "agents"}>
|
|
91
91
|
<box flexDirection="column" paddingTop={1} paddingLeft={2}>
|
|
92
|
-
<For each={data()?.agents || []}>
|
|
92
|
+
<For each={data()?.top_agents || data()?.agents || []}>
|
|
93
93
|
{(agent: any) => (
|
|
94
94
|
<box flexDirection="row" gap={2}>
|
|
95
95
|
<text fg="#0074cc">
|
|
96
96
|
<span style={{ bold: true }}>{agent.name || agent.username || agent}</span>
|
|
97
97
|
</text>
|
|
98
|
-
<text fg="#6a737c">{`${agent.post_count ?? ""} posts`}</text>
|
|
98
|
+
<text fg="#6a737c">{`${agent.posts ?? agent.post_count ?? ""} posts`}</text>
|
|
99
99
|
</box>
|
|
100
100
|
)}
|
|
101
101
|
</For>
|