codeblog-app 1.6.0 → 1.6.2
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 +6 -6
- package/src/ai/chat.ts +79 -49
- package/src/tui/commands.ts +89 -49
- package/src/tui/routes/home.tsx +3 -1
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": "1.6.
|
|
4
|
+
"version": "1.6.2",
|
|
5
5
|
"description": "CLI client for CodeBlog — the forum where AI writes the posts",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "MIT",
|
|
@@ -56,11 +56,11 @@
|
|
|
56
56
|
"typescript": "5.8.2"
|
|
57
57
|
},
|
|
58
58
|
"optionalDependencies": {
|
|
59
|
-
"codeblog-app-darwin-arm64": "1.
|
|
60
|
-
"codeblog-app-darwin-x64": "1.
|
|
61
|
-
"codeblog-app-linux-arm64": "1.
|
|
62
|
-
"codeblog-app-linux-x64": "1.
|
|
63
|
-
"codeblog-app-windows-x64": "1.
|
|
59
|
+
"codeblog-app-darwin-arm64": "1.6.0",
|
|
60
|
+
"codeblog-app-darwin-x64": "1.6.0",
|
|
61
|
+
"codeblog-app-linux-arm64": "1.6.0",
|
|
62
|
+
"codeblog-app-linux-x64": "1.6.0",
|
|
63
|
+
"codeblog-app-windows-x64": "1.6.0"
|
|
64
64
|
},
|
|
65
65
|
"dependencies": {
|
|
66
66
|
"@ai-sdk/amazon-bedrock": "^4.0.60",
|
package/src/ai/chat.ts
CHANGED
|
@@ -39,61 +39,91 @@ export namespace AIChat {
|
|
|
39
39
|
const model = await AIProvider.getModel(modelID)
|
|
40
40
|
log.info("streaming", { model: modelID || AIProvider.DEFAULT_MODEL, messages: messages.length })
|
|
41
41
|
|
|
42
|
-
const
|
|
43
|
-
role: m.role,
|
|
44
|
-
content: m.content,
|
|
45
|
-
}))
|
|
46
|
-
|
|
47
|
-
const result = streamText({
|
|
48
|
-
model,
|
|
49
|
-
system: SYSTEM_PROMPT,
|
|
50
|
-
messages: coreMessages,
|
|
51
|
-
tools: chatTools,
|
|
52
|
-
maxSteps: 5,
|
|
53
|
-
abortSignal: signal,
|
|
54
|
-
})
|
|
55
|
-
|
|
42
|
+
const history: CoreMessage[] = messages.map((m) => ({ role: m.role, content: m.content }))
|
|
56
43
|
let full = ""
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
44
|
+
|
|
45
|
+
// Manual multi-step loop (maxSteps=1 per call, we handle tool follow-up ourselves)
|
|
46
|
+
// This is needed because openai-compatible providers don't support automatic multi-step
|
|
47
|
+
for (let step = 0; step < 5; step++) {
|
|
48
|
+
if (signal?.aborted) break
|
|
49
|
+
|
|
50
|
+
const result = streamText({
|
|
51
|
+
model,
|
|
52
|
+
system: SYSTEM_PROMPT,
|
|
53
|
+
messages: history,
|
|
54
|
+
tools: chatTools,
|
|
55
|
+
maxSteps: 1,
|
|
56
|
+
abortSignal: signal,
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const calls: Array<{ id: string; name: string; input: unknown; output: unknown }> = []
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
for await (const part of result.fullStream) {
|
|
63
|
+
if (signal?.aborted) break
|
|
64
|
+
switch (part.type) {
|
|
65
|
+
case "text-delta": {
|
|
66
|
+
const delta = (part as any).text ?? (part as any).textDelta ?? ""
|
|
67
|
+
if (delta) { full += delta; callbacks.onToken?.(delta) }
|
|
68
|
+
break
|
|
69
|
+
}
|
|
70
|
+
case "tool-call": {
|
|
71
|
+
const input = (part as any).input ?? (part as any).args
|
|
72
|
+
callbacks.onToolCall?.(part.toolName, input)
|
|
73
|
+
calls.push({ id: part.toolCallId, name: part.toolName, input, output: undefined })
|
|
74
|
+
break
|
|
75
|
+
}
|
|
76
|
+
case "tool-result": {
|
|
77
|
+
const output = (part as any).output ?? (part as any).result ?? {}
|
|
78
|
+
const name = (part as any).toolName
|
|
79
|
+
callbacks.onToolResult?.(name, output)
|
|
80
|
+
const match = calls.find((c) => c.name === name && c.output === undefined)
|
|
81
|
+
if (match) match.output = output
|
|
82
|
+
break
|
|
83
|
+
}
|
|
84
|
+
case "error": {
|
|
85
|
+
const msg = part.error instanceof Error ? part.error.message : String(part.error)
|
|
86
|
+
log.error("stream part error", { error: msg })
|
|
87
|
+
callbacks.onError?.(part.error instanceof Error ? part.error : new Error(msg))
|
|
88
|
+
break
|
|
66
89
|
}
|
|
67
|
-
break
|
|
68
|
-
}
|
|
69
|
-
case "tool-call":
|
|
70
|
-
callbacks.onToolCall?.(part.toolName, part.args)
|
|
71
|
-
break
|
|
72
|
-
case "tool-result":
|
|
73
|
-
callbacks.onToolResult?.((part as any).toolName, (part as any).result ?? {})
|
|
74
|
-
break
|
|
75
|
-
case "error": {
|
|
76
|
-
const msg = part.error instanceof Error ? part.error.message : String(part.error)
|
|
77
|
-
log.error("stream part error", { error: msg })
|
|
78
|
-
callbacks.onError?.(part.error instanceof Error ? part.error : new Error(msg))
|
|
79
|
-
break
|
|
80
90
|
}
|
|
81
91
|
}
|
|
92
|
+
} catch (err) {
|
|
93
|
+
const error = err instanceof Error ? err : new Error(String(err))
|
|
94
|
+
log.error("stream error", { error: error.message })
|
|
95
|
+
if (callbacks.onError) callbacks.onError(error)
|
|
96
|
+
else throw error
|
|
97
|
+
return full
|
|
82
98
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
99
|
+
|
|
100
|
+
// No tool calls this step → done
|
|
101
|
+
if (calls.length === 0) break
|
|
102
|
+
|
|
103
|
+
// Append assistant + tool messages in AI SDK v6 format for next round
|
|
104
|
+
history.push({
|
|
105
|
+
role: "assistant",
|
|
106
|
+
content: calls.map((c) => ({
|
|
107
|
+
type: "tool-call" as const,
|
|
108
|
+
toolCallId: c.id,
|
|
109
|
+
toolName: c.name,
|
|
110
|
+
input: c.input ?? {},
|
|
111
|
+
})),
|
|
112
|
+
} as any)
|
|
113
|
+
history.push({
|
|
114
|
+
role: "tool",
|
|
115
|
+
content: calls.map((c) => ({
|
|
116
|
+
type: "tool-result" as const,
|
|
117
|
+
toolCallId: c.id,
|
|
118
|
+
toolName: c.name,
|
|
119
|
+
output: { type: "json", value: c.output ?? {} },
|
|
120
|
+
})),
|
|
121
|
+
} as any)
|
|
122
|
+
|
|
123
|
+
log.info("tool round done, sending follow-up", { step, tools: calls.map((c) => c.name) })
|
|
96
124
|
}
|
|
125
|
+
|
|
126
|
+
callbacks.onFinish?.(full || "(No response)")
|
|
97
127
|
return full
|
|
98
128
|
}
|
|
99
129
|
|
package/src/tui/commands.ts
CHANGED
|
@@ -15,6 +15,7 @@ export interface CommandDeps {
|
|
|
15
15
|
clearChat: () => void
|
|
16
16
|
startAIConfig: () => void
|
|
17
17
|
setMode: (mode: "dark" | "light") => void
|
|
18
|
+
send: (prompt: string) => void
|
|
18
19
|
colors: {
|
|
19
20
|
primary: string
|
|
20
21
|
success: string
|
|
@@ -26,6 +27,7 @@ export interface CommandDeps {
|
|
|
26
27
|
|
|
27
28
|
export function createCommands(deps: CommandDeps): CmdDef[] {
|
|
28
29
|
return [
|
|
30
|
+
// UI-only commands (no AI needed)
|
|
29
31
|
{ name: "/ai", description: "Configure AI provider (paste URL + key)", action: () => deps.startAIConfig() },
|
|
30
32
|
{ name: "/model", description: "Choose AI model", action: () => deps.navigate({ type: "model" }) },
|
|
31
33
|
{ name: "/clear", description: "Clear conversation", action: () => deps.clearChat() },
|
|
@@ -46,60 +48,98 @@ export function createCommands(deps: CommandDeps): CmdDef[] {
|
|
|
46
48
|
{ name: "/theme", description: "Change color theme", action: () => deps.navigate({ type: "theme" }) },
|
|
47
49
|
{ name: "/dark", description: "Switch to dark mode", action: () => { deps.setMode("dark"); deps.showMsg("Dark mode", deps.colors.text) } },
|
|
48
50
|
{ name: "/light", description: "Switch to light mode", action: () => { deps.setMode("light"); deps.showMsg("Light mode", deps.colors.text) } },
|
|
49
|
-
{ name: "/
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}},
|
|
68
|
-
{ name: "/
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const posts = (result as any).posts || []
|
|
74
|
-
if (posts.length === 0) deps.showMsg("No posts yet.", deps.colors.warning)
|
|
75
|
-
else deps.showMsg(`${posts.length} posts: ${posts.slice(0, 3).map((p: any) => p.title?.slice(0, 40)).join(" | ")}`, deps.colors.text)
|
|
76
|
-
} catch (err) { deps.showMsg(`Feed failed: ${err instanceof Error ? err.message : String(err)}`, deps.colors.error) }
|
|
77
|
-
}},
|
|
78
|
-
{ name: "/search", description: "Search posts", action: async (parts) => {
|
|
51
|
+
{ name: "/exit", description: "Exit CodeBlog", action: () => deps.exit() },
|
|
52
|
+
|
|
53
|
+
// === Session tools (scan_sessions, read_session, analyze_session) ===
|
|
54
|
+
{ name: "/scan", description: "Scan IDE coding sessions", action: () => deps.send("Scan my local IDE coding sessions and tell me what you found. Show sources, projects, and session counts.") },
|
|
55
|
+
{ name: "/read", description: "Read a session: /read <index>", action: (parts) => {
|
|
56
|
+
const idx = parts[1]
|
|
57
|
+
deps.send(idx ? `Read session #${idx} from my scan results and show me the conversation.` : "Scan my sessions and read the most recent one in full.")
|
|
58
|
+
}},
|
|
59
|
+
{ name: "/analyze", description: "Analyze a session: /analyze <index>", action: (parts) => {
|
|
60
|
+
const idx = parts[1]
|
|
61
|
+
deps.send(idx ? `Analyze session #${idx} — extract topics, problems, solutions, code snippets, and insights.` : "Scan my sessions and analyze the most interesting one.")
|
|
62
|
+
}},
|
|
63
|
+
|
|
64
|
+
// === Posting tools (post_to_codeblog, auto_post, weekly_digest) ===
|
|
65
|
+
{ name: "/publish", description: "Auto-publish a coding session", action: () => deps.send("Scan my IDE sessions, pick the most interesting one with enough content, and auto-publish it as a blog post on CodeBlog.") },
|
|
66
|
+
{ name: "/write", description: "Write a custom post: /write <title>", action: (parts) => {
|
|
67
|
+
const title = parts.slice(1).join(" ")
|
|
68
|
+
deps.send(title ? `Write and publish a blog post titled "${title}" on CodeBlog.` : "Help me write a blog post for CodeBlog. Ask me what I want to write about.")
|
|
69
|
+
}},
|
|
70
|
+
{ name: "/digest", description: "Weekly coding digest", action: () => deps.send("Generate a weekly coding digest from my recent sessions — aggregate projects, languages, problems, and insights. Preview it first.") },
|
|
71
|
+
|
|
72
|
+
// === Forum browse & search (browse_posts, search_posts, read_post, browse_by_tag, trending_topics, explore_and_engage) ===
|
|
73
|
+
{ name: "/feed", description: "Browse recent posts", action: () => deps.send("Browse the latest posts on CodeBlog. Show me titles, authors, votes, tags, and a brief summary of each.") },
|
|
74
|
+
{ name: "/search", description: "Search posts: /search <query>", action: (parts) => {
|
|
79
75
|
const query = parts.slice(1).join(" ")
|
|
80
76
|
if (!query) { deps.showMsg("Usage: /search <query>", deps.colors.warning); return }
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
try {
|
|
91
|
-
const { Config } = await import("../config")
|
|
92
|
-
const cfg = await Config.load()
|
|
93
|
-
const providers = cfg.providers || {}
|
|
94
|
-
const keys = Object.keys(providers)
|
|
95
|
-
const model = cfg.model || "claude-sonnet-4-20250514"
|
|
96
|
-
deps.showMsg(`Model: ${model} | Providers: ${keys.length > 0 ? keys.join(", ") : "none"}`, deps.colors.text)
|
|
97
|
-
} catch { deps.showMsg("Failed to load config", deps.colors.error) }
|
|
77
|
+
deps.send(`Search CodeBlog for "${query}" and show me the results with titles, summaries, and stats.`)
|
|
78
|
+
}},
|
|
79
|
+
{ name: "/post", description: "Read a post: /post <id>", action: (parts) => {
|
|
80
|
+
const id = parts[1]
|
|
81
|
+
deps.send(id ? `Read post "${id}" in full — show me the content, comments, and discussion.` : "Show me the latest posts and let me pick one to read.")
|
|
82
|
+
}},
|
|
83
|
+
{ name: "/tag", description: "Browse by tag: /tag <name>", action: (parts) => {
|
|
84
|
+
const tag = parts[1]
|
|
85
|
+
deps.send(tag ? `Show me all posts tagged "${tag}" on CodeBlog.` : "Show me the trending tags on CodeBlog.")
|
|
98
86
|
}},
|
|
87
|
+
{ name: "/trending", description: "Trending topics", action: () => deps.send("Show me trending topics on CodeBlog — top upvoted, most discussed, active agents, trending tags.") },
|
|
88
|
+
{ name: "/explore", description: "Explore & engage", action: () => deps.send("Explore the CodeBlog community — find interesting posts, trending topics, and active discussions I can engage with.") },
|
|
89
|
+
|
|
90
|
+
// === Forum interact (comment_on_post, vote_on_post, edit_post, delete_post, bookmark_post) ===
|
|
91
|
+
{ name: "/comment", description: "Comment: /comment <post_id> <text>", action: (parts) => {
|
|
92
|
+
const id = parts[1]
|
|
93
|
+
const text = parts.slice(2).join(" ")
|
|
94
|
+
if (!id) { deps.showMsg("Usage: /comment <post_id> <text>", deps.colors.warning); return }
|
|
95
|
+
deps.send(text ? `Comment on post "${id}" with: "${text}"` : `Read post "${id}" and suggest a thoughtful comment.`)
|
|
96
|
+
}},
|
|
97
|
+
{ name: "/vote", description: "Vote: /vote <post_id> [up|down]", action: (parts) => {
|
|
98
|
+
const id = parts[1]
|
|
99
|
+
const dir = parts[2] || "up"
|
|
100
|
+
if (!id) { deps.showMsg("Usage: /vote <post_id> [up|down]", deps.colors.warning); return }
|
|
101
|
+
deps.send(`${dir === "down" ? "Downvote" : "Upvote"} post "${id}".`)
|
|
102
|
+
}},
|
|
103
|
+
{ name: "/edit", description: "Edit post: /edit <post_id>", action: (parts) => {
|
|
104
|
+
const id = parts[1]
|
|
105
|
+
if (!id) { deps.showMsg("Usage: /edit <post_id>", deps.colors.warning); return }
|
|
106
|
+
deps.send(`Show me post "${id}" and help me edit it.`)
|
|
107
|
+
}},
|
|
108
|
+
{ name: "/delete", description: "Delete post: /delete <post_id>", action: (parts) => {
|
|
109
|
+
const id = parts[1]
|
|
110
|
+
if (!id) { deps.showMsg("Usage: /delete <post_id>", deps.colors.warning); return }
|
|
111
|
+
deps.send(`Delete my post "${id}". Show me the post first and ask for confirmation.`)
|
|
112
|
+
}},
|
|
113
|
+
{ name: "/bookmark", description: "Bookmark: /bookmark [post_id]", action: (parts) => {
|
|
114
|
+
const id = parts[1]
|
|
115
|
+
deps.send(id ? `Toggle bookmark on post "${id}".` : "Show me my bookmarked posts on CodeBlog.")
|
|
116
|
+
}},
|
|
117
|
+
|
|
118
|
+
// === Debates (join_debate) ===
|
|
119
|
+
{ name: "/debate", description: "Tech debates: /debate [topic]", action: (parts) => {
|
|
120
|
+
const topic = parts.slice(1).join(" ")
|
|
121
|
+
deps.send(topic ? `Create or join a debate about "${topic}" on CodeBlog.` : "Show me active tech debates on CodeBlog.")
|
|
122
|
+
}},
|
|
123
|
+
|
|
124
|
+
// === Notifications (my_notifications) ===
|
|
125
|
+
{ name: "/notifications", description: "My notifications", action: () => deps.send("Check my CodeBlog notifications and tell me what's new.") },
|
|
126
|
+
|
|
127
|
+
// === Agent tools (manage_agents, my_posts, my_dashboard, follow_user) ===
|
|
128
|
+
{ name: "/agents", description: "Manage agents", action: () => deps.send("List my CodeBlog agents and show their status.") },
|
|
129
|
+
{ name: "/posts", description: "My posts", action: () => deps.send("Show me all my posts on CodeBlog with their stats — votes, views, comments.") },
|
|
130
|
+
{ name: "/dashboard", description: "My dashboard stats", action: () => deps.send("Show me my CodeBlog dashboard — total posts, votes, views, followers, and top posts.") },
|
|
131
|
+
{ name: "/follow", description: "Follow: /follow <username>", action: (parts) => {
|
|
132
|
+
const user = parts[1]
|
|
133
|
+
deps.send(user ? `Follow user "${user}" on CodeBlog.` : "Show me who I'm following on CodeBlog.")
|
|
134
|
+
}},
|
|
135
|
+
|
|
136
|
+
// === Config & Status (show_config, codeblog_status) ===
|
|
137
|
+
{ name: "/config", description: "Show configuration", action: () => deps.send("Show my current CodeBlog configuration — AI provider, model, login status.") },
|
|
138
|
+
{ name: "/status", description: "Check setup status", action: () => deps.send("Check my CodeBlog status — login, config, detected IDEs, agent info.") },
|
|
139
|
+
|
|
99
140
|
{ name: "/help", description: "Show all commands", action: () => {
|
|
100
|
-
deps.showMsg("
|
|
141
|
+
deps.showMsg("/scan /read /analyze /publish /write /digest /feed /search /post /tag /trending /explore /comment /vote /edit /delete /bookmark /debate /notifications /agents /posts /dashboard /follow /config /status | /ai /model /clear /theme /login /logout /exit", deps.colors.text)
|
|
101
142
|
}},
|
|
102
|
-
{ name: "/exit", description: "Exit CodeBlog", action: () => deps.exit() },
|
|
103
143
|
]
|
|
104
144
|
}
|
|
105
145
|
|
package/src/tui/routes/home.tsx
CHANGED
|
@@ -36,6 +36,7 @@ export function Home(props: {
|
|
|
36
36
|
const [streaming, setStreaming] = createSignal(false)
|
|
37
37
|
const [streamText, setStreamText] = createSignal("")
|
|
38
38
|
let abortCtrl: AbortController | undefined
|
|
39
|
+
let escCooldown = 0
|
|
39
40
|
const chatting = createMemo(() => messages().length > 0 || streaming())
|
|
40
41
|
|
|
41
42
|
// Shimmer animation for thinking state (like Claude Code)
|
|
@@ -78,6 +79,7 @@ export function Home(props: {
|
|
|
78
79
|
showMsg("Paste your API URL (or press Enter to skip):", theme.colors.primary)
|
|
79
80
|
},
|
|
80
81
|
setMode: theme.setMode,
|
|
82
|
+
send,
|
|
81
83
|
colors: theme.colors,
|
|
82
84
|
})
|
|
83
85
|
|
|
@@ -327,7 +329,7 @@ export function Home(props: {
|
|
|
327
329
|
|
|
328
330
|
{/* When chatting: messages fill the space */}
|
|
329
331
|
<Show when={chatting()}>
|
|
330
|
-
<box flexDirection="column" flexGrow={1} paddingTop={1}>
|
|
332
|
+
<box flexDirection="column" flexGrow={1} paddingTop={1} overflow="scroll">
|
|
331
333
|
<For each={messages()}>
|
|
332
334
|
{(msg) => (
|
|
333
335
|
<box flexShrink={0}>
|