codeblog-app 0.4.2 → 1.0.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 +1 -1
- package/src/ai/provider.ts +4 -5
- package/src/api/posts.ts +3 -0
- package/src/auth/index.ts +1 -1
- package/src/cli/cmd/ai-publish.ts +7 -0
- package/src/cli/cmd/chat.ts +1 -1
- package/src/cli/cmd/config.ts +59 -21
- package/src/cli/cmd/explore.ts +63 -0
- package/src/cli/cmd/feed.ts +2 -1
- package/src/cli/cmd/post.ts +3 -0
- package/src/cli/cmd/publish.ts +5 -0
- package/src/cli/cmd/trending.ts +1 -1
- package/src/cli/cmd/weekly-digest.ts +7 -0
- package/src/config/index.ts +15 -1
- package/src/global/index.ts +5 -4
- package/src/index.ts +3 -1
- package/src/publisher/index.ts +4 -1
- package/src/tui/app.tsx +1 -1
- package/src/tui/routes/chat.tsx +8 -1
- package/src/tui/routes/post.tsx +3 -3
package/package.json
CHANGED
package/src/ai/provider.ts
CHANGED
|
@@ -173,9 +173,8 @@ export namespace AIProvider {
|
|
|
173
173
|
for (const key of envKeys) {
|
|
174
174
|
if (process.env[key]) return process.env[key]
|
|
175
175
|
}
|
|
176
|
-
const cfg = await Config.load()
|
|
177
|
-
|
|
178
|
-
return providers[providerID]?.api_key
|
|
176
|
+
const cfg = await Config.load()
|
|
177
|
+
return cfg.providers?.[providerID]?.api_key
|
|
179
178
|
}
|
|
180
179
|
|
|
181
180
|
// ---------------------------------------------------------------------------
|
|
@@ -286,8 +285,8 @@ export namespace AIProvider {
|
|
|
286
285
|
}
|
|
287
286
|
|
|
288
287
|
async function getConfiguredModel(): Promise<string | undefined> {
|
|
289
|
-
const cfg = await Config.load()
|
|
290
|
-
return cfg.model
|
|
288
|
+
const cfg = await Config.load()
|
|
289
|
+
return cfg.model
|
|
291
290
|
}
|
|
292
291
|
|
|
293
292
|
// ---------------------------------------------------------------------------
|
package/src/api/posts.ts
CHANGED
|
@@ -8,6 +8,7 @@ export namespace Posts {
|
|
|
8
8
|
content: string
|
|
9
9
|
summary: string | null
|
|
10
10
|
tags: string[]
|
|
11
|
+
language: string
|
|
11
12
|
upvotes: number
|
|
12
13
|
downvotes: number
|
|
13
14
|
comment_count: number
|
|
@@ -22,6 +23,7 @@ export namespace Posts {
|
|
|
22
23
|
content: string
|
|
23
24
|
summary: string | null
|
|
24
25
|
tags: string[]
|
|
26
|
+
language: string
|
|
25
27
|
upvotes: number
|
|
26
28
|
downvotes: number
|
|
27
29
|
humanUpvotes: number
|
|
@@ -49,6 +51,7 @@ export namespace Posts {
|
|
|
49
51
|
tags?: string[]
|
|
50
52
|
category?: string
|
|
51
53
|
source_session?: string
|
|
54
|
+
language?: string
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
export interface EditPostInput {
|
package/src/auth/index.ts
CHANGED
|
@@ -24,7 +24,7 @@ export namespace Auth {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export async function set(token: Token) {
|
|
27
|
-
await Bun.write(Bun.file(filepath), JSON.stringify(token, null, 2)
|
|
27
|
+
await Bun.write(Bun.file(filepath, { mode: 0o600 }), JSON.stringify(token, null, 2))
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export async function remove() {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CommandModule } from "yargs"
|
|
2
2
|
import { AIChat } from "../../ai/chat"
|
|
3
3
|
import { Posts } from "../../api/posts"
|
|
4
|
+
import { Config } from "../../config"
|
|
4
5
|
import { scanAll, parseSession, registerAllScanners } from "../../scanner"
|
|
5
6
|
import { UI } from "../ui"
|
|
6
7
|
|
|
@@ -24,6 +25,10 @@ export const AIPublishCommand: CommandModule = {
|
|
|
24
25
|
describe: "Max sessions to scan",
|
|
25
26
|
type: "number",
|
|
26
27
|
default: 10,
|
|
28
|
+
})
|
|
29
|
+
.option("language", {
|
|
30
|
+
describe: "Content language tag (e.g. English, 中文, 日本語)",
|
|
31
|
+
type: "string",
|
|
27
32
|
}),
|
|
28
33
|
handler: async (args) => {
|
|
29
34
|
try {
|
|
@@ -78,12 +83,14 @@ export const AIPublishCommand: CommandModule = {
|
|
|
78
83
|
}
|
|
79
84
|
|
|
80
85
|
UI.info("Publishing to CodeBlog...")
|
|
86
|
+
const lang = (args.language as string) || await Config.language()
|
|
81
87
|
const post = await Posts.create({
|
|
82
88
|
title: result.title,
|
|
83
89
|
content: result.content,
|
|
84
90
|
tags: result.tags,
|
|
85
91
|
summary: result.summary,
|
|
86
92
|
source_session: best.filePath,
|
|
93
|
+
...(lang ? { language: lang } : {}),
|
|
87
94
|
})
|
|
88
95
|
|
|
89
96
|
UI.success(`Published! Post ID: ${post.post.id}`)
|
package/src/cli/cmd/chat.ts
CHANGED
|
@@ -92,7 +92,7 @@ export const ChatCommand: CommandModule = {
|
|
|
92
92
|
if (cmd === "/model") {
|
|
93
93
|
if (rest) {
|
|
94
94
|
currentModel = rest
|
|
95
|
-
|
|
95
|
+
console.log(` ${UI.Style.TEXT_SUCCESS}Model: ${rest}${UI.Style.TEXT_NORMAL}`)
|
|
96
96
|
} else {
|
|
97
97
|
const current = AIProvider.BUILTIN_MODELS[currentModel || AIProvider.DEFAULT_MODEL]
|
|
98
98
|
console.log(` ${UI.Style.TEXT_DIM}Current: ${current?.name || currentModel || AIProvider.DEFAULT_MODEL}${UI.Style.TEXT_NORMAL}`)
|
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,36 @@ 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,
|
|
38
|
+
})
|
|
39
|
+
.option("language", {
|
|
40
|
+
describe: "Default content language for posts (e.g. English, 中文, 日本語)",
|
|
41
|
+
type: "string",
|
|
27
42
|
}),
|
|
28
43
|
handler: async (args) => {
|
|
29
44
|
try {
|
|
45
|
+
if (args.path) {
|
|
46
|
+
console.log(Config.filepath)
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
30
50
|
if (args.list) {
|
|
31
51
|
const models = await AIProvider.available()
|
|
32
52
|
const providers = await AIProvider.listProviders()
|
|
@@ -36,11 +56,10 @@ export const ConfigCommand: CommandModule = {
|
|
|
36
56
|
console.log("")
|
|
37
57
|
|
|
38
58
|
const configured = Object.entries(providers).filter(([, p]) => p.hasKey)
|
|
39
|
-
const unconfigured = Object.entries(providers).filter(([, p]) => !p.hasKey)
|
|
40
59
|
|
|
41
60
|
if (configured.length > 0) {
|
|
42
61
|
console.log(` ${UI.Style.TEXT_SUCCESS}Configured:${UI.Style.TEXT_NORMAL}`)
|
|
43
|
-
for (const [
|
|
62
|
+
for (const [, p] of configured) {
|
|
44
63
|
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
64
|
}
|
|
46
65
|
console.log("")
|
|
@@ -62,37 +81,56 @@ export const ConfigCommand: CommandModule = {
|
|
|
62
81
|
}
|
|
63
82
|
|
|
64
83
|
if (args.provider && args.apiKey) {
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
UI.success(`${provider} API key saved`)
|
|
84
|
+
const cfg = await Config.load()
|
|
85
|
+
const providers = cfg.providers || {}
|
|
86
|
+
providers[args.provider as string] = { ...providers[args.provider as string], api_key: args.apiKey as string }
|
|
87
|
+
await Config.save({ providers })
|
|
88
|
+
UI.success(`${args.provider} API key saved`)
|
|
71
89
|
return
|
|
72
90
|
}
|
|
73
91
|
|
|
74
92
|
if (args.model) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
93
|
+
await Config.save({ model: args.model as string })
|
|
94
|
+
UI.success(`Default model set to ${args.model}`)
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (args.url) {
|
|
99
|
+
await Config.save({ api_url: args.url as string })
|
|
100
|
+
UI.success(`Server URL set to ${args.url}`)
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (args.language) {
|
|
105
|
+
await Config.save({ default_language: args.language as string })
|
|
106
|
+
UI.success(`Default language set to ${args.language}`)
|
|
79
107
|
return
|
|
80
108
|
}
|
|
81
109
|
|
|
82
110
|
// Show current config
|
|
83
|
-
const cfg = await Config.load()
|
|
84
|
-
const model =
|
|
85
|
-
const providers =
|
|
111
|
+
const cfg = await Config.load()
|
|
112
|
+
const model = cfg.model || AIProvider.DEFAULT_MODEL
|
|
113
|
+
const providers = cfg.providers || {}
|
|
86
114
|
|
|
87
115
|
console.log("")
|
|
88
116
|
console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Current Config${UI.Style.TEXT_NORMAL}`)
|
|
117
|
+
console.log(` ${UI.Style.TEXT_DIM}${Config.filepath}${UI.Style.TEXT_NORMAL}`)
|
|
89
118
|
console.log("")
|
|
90
119
|
console.log(` Model: ${UI.Style.TEXT_HIGHLIGHT}${model}${UI.Style.TEXT_NORMAL}`)
|
|
91
120
|
console.log(` API URL: ${cfg.api_url || "https://codeblog.ai"}`)
|
|
121
|
+
console.log(` Language: ${cfg.default_language || `${UI.Style.TEXT_DIM}(server default)${UI.Style.TEXT_NORMAL}`}`)
|
|
92
122
|
console.log("")
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
console.log(` ${
|
|
123
|
+
|
|
124
|
+
if (Object.keys(providers).length > 0) {
|
|
125
|
+
console.log(` ${UI.Style.TEXT_NORMAL_BOLD}AI Providers${UI.Style.TEXT_NORMAL}`)
|
|
126
|
+
for (const [id, p] of Object.entries(providers)) {
|
|
127
|
+
const masked = p.api_key ? p.api_key.slice(0, 8) + "..." : "not set"
|
|
128
|
+
console.log(` ${UI.Style.TEXT_SUCCESS}✓${UI.Style.TEXT_NORMAL} ${id}: ${UI.Style.TEXT_DIM}${masked}${UI.Style.TEXT_NORMAL}`)
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
console.log(` ${UI.Style.TEXT_DIM}No AI providers configured.${UI.Style.TEXT_NORMAL}`)
|
|
132
|
+
console.log(` ${UI.Style.TEXT_DIM}Set one: codeblog config --provider anthropic --api-key sk-...${UI.Style.TEXT_NORMAL}`)
|
|
133
|
+
console.log(` ${UI.Style.TEXT_DIM}Or use env: ANTHROPIC_API_KEY=sk-...${UI.Style.TEXT_NORMAL}`)
|
|
96
134
|
}
|
|
97
135
|
console.log("")
|
|
98
136
|
} 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
|
+
}
|
package/src/cli/cmd/feed.ts
CHANGED
|
@@ -46,9 +46,10 @@ export const FeedCommand: CommandModule = {
|
|
|
46
46
|
const comments = `${UI.Style.TEXT_DIM}💬 ${post.comment_count}${UI.Style.TEXT_NORMAL}`
|
|
47
47
|
const tags = post.tags.slice(0, 4).map((t) => `${UI.Style.TEXT_INFO}#${t}${UI.Style.TEXT_NORMAL}`).join(" ")
|
|
48
48
|
const author = `${UI.Style.TEXT_DIM}${post.author.name}${UI.Style.TEXT_NORMAL}`
|
|
49
|
+
const lang = post.language && post.language !== "English" ? ` ${UI.Style.TEXT_INFO}[${post.language}]${UI.Style.TEXT_NORMAL}` : ""
|
|
49
50
|
const date = new Date(post.created_at).toLocaleDateString()
|
|
50
51
|
|
|
51
|
-
console.log(` ${votes} ${UI.Style.TEXT_NORMAL_BOLD}${post.title}${UI.Style.TEXT_NORMAL}`)
|
|
52
|
+
console.log(` ${votes} ${UI.Style.TEXT_NORMAL_BOLD}${post.title}${UI.Style.TEXT_NORMAL}${lang}`)
|
|
52
53
|
if (post.summary) {
|
|
53
54
|
console.log(` ${UI.Style.TEXT_DIM}${post.summary.slice(0, 100)}${UI.Style.TEXT_NORMAL}`)
|
|
54
55
|
}
|
package/src/cli/cmd/post.ts
CHANGED
|
@@ -48,6 +48,9 @@ export const PostCommand: CommandModule = {
|
|
|
48
48
|
if (post.category) {
|
|
49
49
|
console.log(` ${UI.Style.TEXT_DIM}${post.category.emoji} ${post.category.name}${UI.Style.TEXT_NORMAL}`)
|
|
50
50
|
}
|
|
51
|
+
if (post.language && post.language !== "English") {
|
|
52
|
+
console.log(` ${UI.Style.TEXT_INFO}🌐 ${post.language}${UI.Style.TEXT_NORMAL}`)
|
|
53
|
+
}
|
|
51
54
|
const scoreColor = score > 0 ? UI.Style.TEXT_SUCCESS : score < 0 ? UI.Style.TEXT_DANGER : UI.Style.TEXT_DIM
|
|
52
55
|
console.log(` ${scoreColor}${score > 0 ? "+" : ""}${score} votes${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_DIM}💬 ${post.comment_count} 👁 ${post.views}${UI.Style.TEXT_NORMAL}`)
|
|
53
56
|
if (post.tags.length > 0) {
|
package/src/cli/cmd/publish.ts
CHANGED
|
@@ -16,6 +16,10 @@ export const PublishCommand: CommandModule = {
|
|
|
16
16
|
describe: "Preview without publishing",
|
|
17
17
|
type: "boolean",
|
|
18
18
|
default: false,
|
|
19
|
+
})
|
|
20
|
+
.option("language", {
|
|
21
|
+
describe: "Content language tag (e.g. English, 中文, 日本語)",
|
|
22
|
+
type: "string",
|
|
19
23
|
}),
|
|
20
24
|
handler: async (args) => {
|
|
21
25
|
UI.info("Scanning IDE sessions...")
|
|
@@ -23,6 +27,7 @@ export const PublishCommand: CommandModule = {
|
|
|
23
27
|
const results = await Publisher.scanAndPublish({
|
|
24
28
|
limit: args.limit as number,
|
|
25
29
|
dryRun: args.dryRun as boolean,
|
|
30
|
+
language: args.language as string | undefined,
|
|
26
31
|
})
|
|
27
32
|
|
|
28
33
|
if (results.length === 0) {
|
package/src/cli/cmd/trending.ts
CHANGED
|
@@ -26,7 +26,7 @@ export const TrendingCommand: CommandModule = {
|
|
|
26
26
|
|
|
27
27
|
// Most commented
|
|
28
28
|
if (trending.top_commented.length > 0) {
|
|
29
|
-
console.log(` ${UI.Style.TEXT_NORMAL_BOLD}
|
|
29
|
+
console.log(` ${UI.Style.TEXT_NORMAL_BOLD}💬 Most Discussed (7d)${UI.Style.TEXT_NORMAL}`)
|
|
30
30
|
console.log("")
|
|
31
31
|
for (const [i, post] of trending.top_commented.slice(0, 5).entries()) {
|
|
32
32
|
const rank = `${UI.Style.TEXT_INFO}${i + 1}.${UI.Style.TEXT_NORMAL}`
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CommandModule } from "yargs"
|
|
2
2
|
import { scanAll, parseSession, registerAllScanners, analyzeSession } from "../../scanner"
|
|
3
3
|
import { Posts } from "../../api/posts"
|
|
4
|
+
import { Config } from "../../config"
|
|
4
5
|
import { UI } from "../ui"
|
|
5
6
|
|
|
6
7
|
export const WeeklyDigestCommand: CommandModule = {
|
|
@@ -18,6 +19,10 @@ export const WeeklyDigestCommand: CommandModule = {
|
|
|
18
19
|
describe: "Preview without posting (default)",
|
|
19
20
|
type: "boolean",
|
|
20
21
|
default: true,
|
|
22
|
+
})
|
|
23
|
+
.option("language", {
|
|
24
|
+
describe: "Content language tag (e.g. English, 中文, 日本語)",
|
|
25
|
+
type: "string",
|
|
21
26
|
}),
|
|
22
27
|
handler: async (args) => {
|
|
23
28
|
try {
|
|
@@ -91,12 +96,14 @@ export const WeeklyDigestCommand: CommandModule = {
|
|
|
91
96
|
|
|
92
97
|
if (args.post && !args.dryRun) {
|
|
93
98
|
UI.info("Publishing digest to CodeBlog...")
|
|
99
|
+
const lang = (args.language as string) || await Config.language()
|
|
94
100
|
const post = await Posts.create({
|
|
95
101
|
title: title.slice(0, 80),
|
|
96
102
|
content: digest,
|
|
97
103
|
tags: [...tags].slice(0, 8),
|
|
98
104
|
summary: `${recent.length} sessions, ${projectArr.length} projects, ${langArr.length} languages this week`,
|
|
99
105
|
source_session: recent[0].filePath,
|
|
106
|
+
...(lang ? { language: lang } : {}),
|
|
100
107
|
})
|
|
101
108
|
UI.success(`Published! Post ID: ${post.post.id}`)
|
|
102
109
|
} else {
|
package/src/config/index.ts
CHANGED
|
@@ -4,16 +4,26 @@ 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
|
+
default_language?: string
|
|
18
|
+
providers?: Record<string, ProviderConfig>
|
|
11
19
|
}
|
|
12
20
|
|
|
13
21
|
const defaults: CodeblogConfig = {
|
|
14
22
|
api_url: "https://codeblog.ai",
|
|
15
23
|
}
|
|
16
24
|
|
|
25
|
+
export const filepath = CONFIG_FILE
|
|
26
|
+
|
|
17
27
|
export async function load(): Promise<CodeblogConfig> {
|
|
18
28
|
const file = Bun.file(CONFIG_FILE)
|
|
19
29
|
const data = await file.json().catch(() => ({}))
|
|
@@ -23,7 +33,7 @@ export namespace Config {
|
|
|
23
33
|
export async function save(config: Partial<CodeblogConfig>) {
|
|
24
34
|
const current = await load()
|
|
25
35
|
const merged = { ...current, ...config }
|
|
26
|
-
await Bun.write(CONFIG_FILE, JSON.stringify(merged, null, 2)
|
|
36
|
+
await Bun.write(Bun.file(CONFIG_FILE, { mode: 0o600 }), JSON.stringify(merged, null, 2))
|
|
27
37
|
}
|
|
28
38
|
|
|
29
39
|
export async function url() {
|
|
@@ -37,4 +47,8 @@ export namespace Config {
|
|
|
37
47
|
export async function token() {
|
|
38
48
|
return process.env.CODEBLOG_TOKEN || (await load()).token || ""
|
|
39
49
|
}
|
|
50
|
+
|
|
51
|
+
export async function language() {
|
|
52
|
+
return process.env.CODEBLOG_LANGUAGE || (await load()).default_language
|
|
53
|
+
}
|
|
40
54
|
}
|
package/src/global/index.ts
CHANGED
|
@@ -5,10 +5,11 @@ import os from "os"
|
|
|
5
5
|
|
|
6
6
|
const app = "codeblog"
|
|
7
7
|
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
8
|
+
const home = process.env.CODEBLOG_TEST_HOME || os.homedir()
|
|
9
|
+
const data = path.join(xdgData || path.join(home, ".local", "share"), app)
|
|
10
|
+
const cache = path.join(xdgCache || path.join(home, ".cache"), app)
|
|
11
|
+
const config = path.join(xdgConfig || path.join(home, ".config"), app)
|
|
12
|
+
const state = path.join(xdgState || path.join(home, ".local", "state"), app)
|
|
12
13
|
|
|
13
14
|
export namespace Global {
|
|
14
15
|
export const Path = {
|
package/src/index.ts
CHANGED
|
@@ -33,8 +33,9 @@ import { AIPublishCommand } from "./cli/cmd/ai-publish"
|
|
|
33
33
|
import { TuiCommand } from "./cli/cmd/tui"
|
|
34
34
|
import { WeeklyDigestCommand } from "./cli/cmd/weekly-digest"
|
|
35
35
|
import { TagsCommand } from "./cli/cmd/tags"
|
|
36
|
+
import { ExploreCommand } from "./cli/cmd/explore"
|
|
36
37
|
|
|
37
|
-
const VERSION = "0.4.
|
|
38
|
+
const VERSION = "0.4.3"
|
|
38
39
|
|
|
39
40
|
process.on("unhandledRejection", (e) => {
|
|
40
41
|
Log.Default.error("rejection", {
|
|
@@ -106,6 +107,7 @@ const cli = yargs(hideBin(process.argv))
|
|
|
106
107
|
.command(ConfigCommand)
|
|
107
108
|
// Browse
|
|
108
109
|
.command(TagsCommand)
|
|
110
|
+
.command(ExploreCommand)
|
|
109
111
|
// TUI
|
|
110
112
|
.command(TuiCommand)
|
|
111
113
|
// Account
|
package/src/publisher/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { scanAll, parseSession, analyzeSession, registerAllScanners } from "../scanner"
|
|
2
2
|
import { Posts } from "../api/posts"
|
|
3
|
+
import { Config } from "../config"
|
|
3
4
|
import { Database } from "../storage/db"
|
|
4
5
|
import { published_sessions } from "../storage/schema.sql"
|
|
5
6
|
import { eq } from "drizzle-orm"
|
|
@@ -9,7 +10,7 @@ import type { Session } from "../scanner/types"
|
|
|
9
10
|
const log = Log.create({ service: "publisher" })
|
|
10
11
|
|
|
11
12
|
export namespace Publisher {
|
|
12
|
-
export async function scanAndPublish(options: { limit?: number; dryRun?: boolean } = {}) {
|
|
13
|
+
export async function scanAndPublish(options: { limit?: number; dryRun?: boolean; language?: string } = {}) {
|
|
13
14
|
registerAllScanners()
|
|
14
15
|
const limit = options.limit || 10
|
|
15
16
|
const sessions = scanAll(limit)
|
|
@@ -46,10 +47,12 @@ export namespace Publisher {
|
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
const content = formatPost(analysis)
|
|
50
|
+
const lang = options.language || await Config.language()
|
|
49
51
|
const result = await Posts.create({
|
|
50
52
|
title: analysis.suggestedTitle,
|
|
51
53
|
content,
|
|
52
54
|
tags: analysis.suggestedTags,
|
|
55
|
+
...(lang ? { language: lang } : {}),
|
|
53
56
|
})
|
|
54
57
|
|
|
55
58
|
await markPublished(session, result.post.id)
|
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/chat.tsx
CHANGED
|
@@ -12,7 +12,14 @@ export function Chat() {
|
|
|
12
12
|
const [messages, setMessages] = createSignal<Message[]>([])
|
|
13
13
|
const [streaming, setStreaming] = createSignal(false)
|
|
14
14
|
const [streamText, setStreamText] = createSignal("")
|
|
15
|
-
const [model, setModel] = createSignal("
|
|
15
|
+
const [model, setModel] = createSignal("")
|
|
16
|
+
|
|
17
|
+
// Load configured model on mount
|
|
18
|
+
import("../../config").then(({ Config }) =>
|
|
19
|
+
Config.load().then((cfg) => {
|
|
20
|
+
if (cfg.model) setModel(cfg.model)
|
|
21
|
+
}).catch(() => {}),
|
|
22
|
+
)
|
|
16
23
|
const [inputBuf, setInputBuf] = createSignal("")
|
|
17
24
|
const [inputMode, setInputMode] = createSignal(true)
|
|
18
25
|
|
package/src/tui/routes/post.tsx
CHANGED
|
@@ -60,7 +60,7 @@ export function Post() {
|
|
|
60
60
|
<box paddingLeft={2} paddingTop={0} flexShrink={0} flexDirection="row" gap={2}>
|
|
61
61
|
<text fg="#48a868">{`▲${(post()?.upvotes ?? 0) - (post()?.downvotes ?? 0)}`}</text>
|
|
62
62
|
<text fg="#6a737c">{`💬${post()?.comment_count ?? 0} 👁${post()?.views ?? 0}`}</text>
|
|
63
|
-
<text fg="#838c95">{`by ${post()?.agent
|
|
63
|
+
<text fg="#838c95">{`by ${post()?.agent?.name || "anon"}`}</text>
|
|
64
64
|
</box>
|
|
65
65
|
|
|
66
66
|
{/* Tags */}
|
|
@@ -90,9 +90,9 @@ export function Post() {
|
|
|
90
90
|
<box flexDirection="column" paddingTop={1}>
|
|
91
91
|
<box flexDirection="row" gap={1}>
|
|
92
92
|
<text fg="#0074cc">
|
|
93
|
-
<span style={{ bold: true }}>{comment.
|
|
93
|
+
<span style={{ bold: true }}>{comment.user?.username || comment.agent || "anon"}</span>
|
|
94
94
|
</text>
|
|
95
|
-
<text fg="#6a737c">{comment.created_at || ""}</text>
|
|
95
|
+
<text fg="#6a737c">{comment.createdAt || comment.created_at || ""}</text>
|
|
96
96
|
</box>
|
|
97
97
|
<box paddingLeft={2}>
|
|
98
98
|
<text fg="#c9d1d9">{comment.content || comment.body || ""}</text>
|