codeblog-app 1.6.0 → 1.6.1

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": "1.6.0",
4
+ "version": "1.6.1",
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.5.2",
60
- "codeblog-app-darwin-x64": "1.5.2",
61
- "codeblog-app-linux-arm64": "1.5.2",
62
- "codeblog-app-linux-x64": "1.5.2",
63
- "codeblog-app-windows-x64": "1.5.2"
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 coreMessages: CoreMessage[] = messages.map((m) => ({
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
- try {
58
- for await (const part of result.fullStream) {
59
- switch (part.type) {
60
- case "text-delta": {
61
- // AI SDK v6 uses .text, older versions use .textDelta
62
- const delta = (part as any).text ?? (part as any).textDelta ?? ""
63
- if (delta) {
64
- full += delta
65
- callbacks.onToken?.(delta)
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
- // If fullStream text-delta didn't capture text, try result.text as fallback
84
- if (!full.trim()) {
85
- try { full = await result.text } catch {}
86
- }
87
- callbacks.onFinish?.(full || "(No response)")
88
- } catch (err) {
89
- const error = err instanceof Error ? err : new Error(String(err))
90
- log.error("stream error", { error: error.message })
91
- if (callbacks.onError) {
92
- callbacks.onError(error)
93
- } else {
94
- throw error
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
 
@@ -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: "/scan", description: "Scan IDE coding sessions", action: async () => {
50
- deps.showMsg("Scanning IDE sessions...", deps.colors.primary)
51
- try {
52
- const { registerAllScanners, scanAll } = await import("../scanner")
53
- registerAllScanners()
54
- const sessions = scanAll(10)
55
- if (sessions.length === 0) deps.showMsg("No IDE sessions found.", deps.colors.warning)
56
- else deps.showMsg(`Found ${sessions.length} sessions: ${sessions.slice(0, 3).map((s) => `[${s.source}] ${s.project}`).join(" | ")}`, deps.colors.success)
57
- } catch (err) { deps.showMsg(`Scan failed: ${err instanceof Error ? err.message : String(err)}`, deps.colors.error) }
58
- }},
59
- { name: "/publish", description: "Publish sessions as blog posts", action: async () => {
60
- deps.showMsg("Publishing sessions...", deps.colors.primary)
61
- try {
62
- const { Publisher } = await import("../publisher")
63
- const results = await Publisher.scanAndPublish({ limit: 1 })
64
- const ok = results.filter((r) => r.postId)
65
- deps.showMsg(ok.length > 0 ? `Published ${ok.length} post(s)!` : "No sessions to publish.", ok.length > 0 ? deps.colors.success : deps.colors.warning)
66
- } catch (err) { deps.showMsg(`Publish failed: ${err instanceof Error ? err.message : String(err)}`, deps.colors.error) }
67
- }},
68
- { name: "/feed", description: "Browse recent posts", action: async () => {
69
- deps.showMsg("Loading feed...", deps.colors.primary)
70
- try {
71
- const { Feed } = await import("../api/feed")
72
- const result = await Feed.list()
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
- try {
82
- const { Search } = await import("../api/search")
83
- const result = await Search.query(query)
84
- const count = result.counts?.posts || 0
85
- const posts = result.posts || []
86
- deps.showMsg(count > 0 ? `${count} results for "${query}": ${posts.slice(0, 3).map((p: any) => p.title?.slice(0, 30)).join(" | ")}` : `No results for "${query}"`, count > 0 ? deps.colors.success : deps.colors.warning)
87
- } catch (err) { deps.showMsg(`Search failed: ${err instanceof Error ? err.message : String(err)}`, deps.colors.error) }
88
- }},
89
- { name: "/config", description: "Show current configuration", action: async () => {
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("Commands: /ai /model /scan /publish /feed /search /config /clear /theme /login /logout /exit", deps.colors.text)
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
 
@@ -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