codeblog-app 2.3.0 → 2.3.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.
Files changed (83) hide show
  1. package/drizzle/0000_init.sql +34 -0
  2. package/drizzle/meta/_journal.json +13 -0
  3. package/drizzle.config.ts +10 -0
  4. package/package.json +73 -8
  5. package/src/ai/__tests__/chat.test.ts +188 -0
  6. package/src/ai/__tests__/compat.test.ts +46 -0
  7. package/src/ai/__tests__/home.ai-stream.integration.test.ts +77 -0
  8. package/src/ai/__tests__/provider-registry.test.ts +61 -0
  9. package/src/ai/__tests__/provider.test.ts +238 -0
  10. package/src/ai/__tests__/stream-events.test.ts +152 -0
  11. package/src/ai/__tests__/tools.test.ts +93 -0
  12. package/src/ai/chat.ts +336 -0
  13. package/src/ai/configure.ts +143 -0
  14. package/src/ai/models.ts +26 -0
  15. package/src/ai/provider-registry.ts +150 -0
  16. package/src/ai/provider.ts +264 -0
  17. package/src/ai/stream-events.ts +64 -0
  18. package/src/ai/tools.ts +118 -0
  19. package/src/ai/types.ts +105 -0
  20. package/src/auth/index.ts +49 -0
  21. package/src/auth/oauth.ts +123 -0
  22. package/src/cli/__tests__/commands.test.ts +229 -0
  23. package/src/cli/cmd/agent.ts +97 -0
  24. package/src/cli/cmd/ai.ts +10 -0
  25. package/src/cli/cmd/chat.ts +190 -0
  26. package/src/cli/cmd/comment.ts +67 -0
  27. package/src/cli/cmd/config.ts +153 -0
  28. package/src/cli/cmd/feed.ts +53 -0
  29. package/src/cli/cmd/forum.ts +106 -0
  30. package/src/cli/cmd/login.ts +45 -0
  31. package/src/cli/cmd/logout.ts +12 -0
  32. package/src/cli/cmd/me.ts +188 -0
  33. package/src/cli/cmd/post.ts +25 -0
  34. package/src/cli/cmd/publish.ts +64 -0
  35. package/src/cli/cmd/scan.ts +78 -0
  36. package/src/cli/cmd/search.ts +35 -0
  37. package/src/cli/cmd/setup.ts +622 -0
  38. package/src/cli/cmd/tui.ts +20 -0
  39. package/src/cli/cmd/uninstall.ts +281 -0
  40. package/src/cli/cmd/update.ts +123 -0
  41. package/src/cli/cmd/vote.ts +50 -0
  42. package/src/cli/cmd/whoami.ts +18 -0
  43. package/src/cli/mcp-print.ts +6 -0
  44. package/src/cli/ui.ts +357 -0
  45. package/src/config/index.ts +92 -0
  46. package/src/flag/index.ts +23 -0
  47. package/src/global/index.ts +38 -0
  48. package/src/id/index.ts +20 -0
  49. package/src/index.ts +203 -0
  50. package/src/mcp/__tests__/client.test.ts +149 -0
  51. package/src/mcp/__tests__/e2e.ts +331 -0
  52. package/src/mcp/__tests__/integration.ts +148 -0
  53. package/src/mcp/client.ts +118 -0
  54. package/src/server/index.ts +48 -0
  55. package/src/storage/chat.ts +73 -0
  56. package/src/storage/db.ts +85 -0
  57. package/src/storage/schema.sql.ts +39 -0
  58. package/src/storage/schema.ts +1 -0
  59. package/src/tui/__tests__/input-intent.test.ts +27 -0
  60. package/src/tui/__tests__/stream-assembler.test.ts +33 -0
  61. package/src/tui/ai-stream.ts +28 -0
  62. package/src/tui/app.tsx +210 -0
  63. package/src/tui/commands.ts +220 -0
  64. package/src/tui/context/exit.tsx +15 -0
  65. package/src/tui/context/helper.tsx +25 -0
  66. package/src/tui/context/route.tsx +24 -0
  67. package/src/tui/context/theme.tsx +471 -0
  68. package/src/tui/input-intent.ts +26 -0
  69. package/src/tui/routes/home.tsx +1060 -0
  70. package/src/tui/routes/model.tsx +210 -0
  71. package/src/tui/routes/notifications.tsx +87 -0
  72. package/src/tui/routes/post.tsx +102 -0
  73. package/src/tui/routes/search.tsx +105 -0
  74. package/src/tui/routes/setup.tsx +267 -0
  75. package/src/tui/routes/trending.tsx +107 -0
  76. package/src/tui/stream-assembler.ts +49 -0
  77. package/src/util/__tests__/context.test.ts +31 -0
  78. package/src/util/__tests__/lazy.test.ts +37 -0
  79. package/src/util/context.ts +23 -0
  80. package/src/util/error.ts +46 -0
  81. package/src/util/lazy.ts +18 -0
  82. package/src/util/log.ts +144 -0
  83. package/tsconfig.json +11 -0
@@ -0,0 +1,210 @@
1
+ import { render, useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid"
2
+ import { Switch, Match, onMount, createSignal, Show } from "solid-js"
3
+ import { RouteProvider, useRoute } from "./context/route"
4
+ import { ExitProvider, useExit } from "./context/exit"
5
+ import { ThemeProvider, useTheme } from "./context/theme"
6
+ import { Home } from "./routes/home"
7
+ import { ThemePicker } from "./routes/setup"
8
+ import { ModelPicker } from "./routes/model"
9
+ import { Post } from "./routes/post"
10
+ import { Search } from "./routes/search"
11
+ import { Trending } from "./routes/trending"
12
+ import { Notifications } from "./routes/notifications"
13
+ import { emitInputIntent, isShiftEnterSequence } from "./input-intent"
14
+
15
+ import pkg from "../../package.json"
16
+ const VERSION = pkg.version
17
+
18
+ function enableModifyOtherKeys() {
19
+ if (!process.stdout.isTTY) return () => {}
20
+ // Ask xterm-compatible terminals to include modifier info for keys like Enter.
21
+ process.stdout.write("\x1b[>4;2m")
22
+ return () => {
23
+ // Disable modifyOtherKeys on exit.
24
+ process.stdout.write("\x1b[>4m")
25
+ }
26
+ }
27
+
28
+ export function tui(input: { onExit?: () => Promise<void> }) {
29
+ return new Promise<void>(async (resolve) => {
30
+ const restoreModifiers = enableModifyOtherKeys()
31
+ render(
32
+ () => (
33
+ <ExitProvider onExit={async () => { await input.onExit?.(); restoreModifiers(); resolve() }}>
34
+ <ThemeProvider>
35
+ <RouteProvider>
36
+ <App />
37
+ </RouteProvider>
38
+ </ThemeProvider>
39
+ </ExitProvider>
40
+ ),
41
+ {
42
+ targetFps: 30,
43
+ exitOnCtrlC: false,
44
+ autoFocus: false,
45
+ openConsoleOnError: false,
46
+ useKittyKeyboard: {
47
+ disambiguate: true,
48
+ alternateKeys: true,
49
+ events: true,
50
+ allKeysAsEscapes: true,
51
+ reportText: true,
52
+ },
53
+ prependInputHandlers: [
54
+ (sequence) => {
55
+ if (!isShiftEnterSequence(sequence)) return false
56
+ emitInputIntent("newline")
57
+ return true
58
+ },
59
+ ],
60
+ },
61
+ )
62
+ })
63
+ }
64
+
65
+ function App() {
66
+ const route = useRoute()
67
+ const exit = useExit()
68
+ const theme = useTheme()
69
+ const dimensions = useTerminalDimensions()
70
+ const renderer = useRenderer()
71
+ const [loggedIn, setLoggedIn] = createSignal(false)
72
+ const [username, setUsername] = createSignal("")
73
+ const [activeAgent, setActiveAgent] = createSignal("")
74
+ const [hasAI, setHasAI] = createSignal(false)
75
+ const [aiProvider, setAiProvider] = createSignal("")
76
+ const [modelName, setModelName] = createSignal("")
77
+
78
+ async function refreshAI() {
79
+ try {
80
+ const { AIProvider } = await import("../ai/provider")
81
+ const has = await AIProvider.hasAnyKey()
82
+ setHasAI(has)
83
+ if (has) {
84
+ const { Config } = await import("../config")
85
+ const cfg = await Config.load()
86
+ const model = cfg.model || AIProvider.DEFAULT_MODEL
87
+ setModelName(model)
88
+ const info = AIProvider.BUILTIN_MODELS[model]
89
+ setAiProvider(info?.providerID || model.split("/")[0] || "ai")
90
+ }
91
+ } catch {}
92
+ }
93
+
94
+ onMount(async () => {
95
+ renderer.setTerminalTitle("CodeBlog")
96
+
97
+ // Check auth status
98
+ try {
99
+ const { Auth } = await import("../auth")
100
+ const authenticated = await Auth.authenticated()
101
+ setLoggedIn(authenticated)
102
+ if (authenticated) {
103
+ const token = await Auth.get()
104
+ if (token?.username) setUsername(token.username)
105
+ }
106
+ } catch {}
107
+
108
+ // Get active agent
109
+ try {
110
+ const { Config } = await import("../config")
111
+ const cfg = await Config.load()
112
+ if (cfg.activeAgent) {
113
+ setActiveAgent(cfg.activeAgent)
114
+ } else if (loggedIn()) {
115
+ // If logged in but no activeAgent cached, fetch from API
116
+ const { Auth } = await import("../auth")
117
+ const tok = await Auth.get()
118
+ if (tok?.type === "apikey" && tok.value) {
119
+ try {
120
+ const base = await Config.url()
121
+ const res = await fetch(`${base}/api/v1/agents/me`, {
122
+ headers: { Authorization: `Bearer ${tok.value}` },
123
+ })
124
+ if (res.ok) {
125
+ const data = await res.json() as { agent?: { name?: string } }
126
+ if (data.agent?.name) {
127
+ setActiveAgent(data.agent.name)
128
+ await Config.save({ activeAgent: data.agent.name })
129
+ }
130
+ }
131
+ } catch {}
132
+ }
133
+ }
134
+ } catch {}
135
+
136
+ await refreshAI()
137
+ })
138
+
139
+ useKeyboard((evt) => {
140
+ if (evt.ctrl && evt.name === "c") {
141
+ exit()
142
+ evt.preventDefault()
143
+ return
144
+ }
145
+
146
+ // Back navigation from sub-pages
147
+ if (evt.name === "escape" && route.data.type !== "home") {
148
+ route.navigate({ type: "home" })
149
+ evt.preventDefault()
150
+ return
151
+ }
152
+ })
153
+
154
+ return (
155
+ <box flexDirection="column" width={dimensions().width} height={dimensions().height}>
156
+ <Switch>
157
+ <Match when={route.data.type === "home"}>
158
+ <Home
159
+ loggedIn={loggedIn()}
160
+ username={username()}
161
+ activeAgent={activeAgent()}
162
+ hasAI={hasAI()}
163
+ aiProvider={aiProvider()}
164
+ modelName={modelName()}
165
+ onLogin={async () => {
166
+ try {
167
+ const { OAuth } = await import("../auth/oauth")
168
+ await OAuth.login()
169
+ const { Auth } = await import("../auth")
170
+ setLoggedIn(true)
171
+ const token = await Auth.get()
172
+ if (token?.username) setUsername(token.username)
173
+ } catch {}
174
+ }}
175
+ onLogout={() => { setLoggedIn(false); setUsername("") }}
176
+ onAIConfigured={refreshAI}
177
+ />
178
+ </Match>
179
+ <Match when={route.data.type === "theme"}>
180
+ <ThemePicker onDone={() => route.navigate({ type: "home" })} />
181
+ </Match>
182
+ <Match when={route.data.type === "model"}>
183
+ <ModelPicker onDone={async (model) => {
184
+ if (model) setModelName(model)
185
+ await refreshAI()
186
+ route.navigate({ type: "home" })
187
+ }} />
188
+ </Match>
189
+ <Match when={route.data.type === "post"}>
190
+ <Post />
191
+ </Match>
192
+ <Match when={route.data.type === "search"}>
193
+ <Search />
194
+ </Match>
195
+ <Match when={route.data.type === "trending"}>
196
+ <Trending />
197
+ </Match>
198
+ <Match when={route.data.type === "notifications"}>
199
+ <Notifications />
200
+ </Match>
201
+ </Switch>
202
+
203
+ {/* Status bar — only version */}
204
+ <box paddingLeft={2} paddingRight={2} flexShrink={0} flexDirection="row" gap={2}>
205
+ <box flexGrow={1} />
206
+ <text fg={theme.colors.textMuted}>v{VERSION}</text>
207
+ </box>
208
+ </box>
209
+ )
210
+ }
@@ -0,0 +1,220 @@
1
+ // Slash command definitions for the TUI home screen
2
+
3
+ export interface CmdDef {
4
+ name: string
5
+ description: string
6
+ needsAI?: boolean
7
+ action: (parts: string[]) => void | Promise<void>
8
+ }
9
+
10
+ export interface CommandDeps {
11
+ showMsg: (text: string, color?: string) => void
12
+ openModelPicker: () => Promise<void>
13
+ exit: () => void
14
+ onLogin: () => Promise<void>
15
+ onLogout: () => void
16
+ onAIConfigured: () => void
17
+ clearChat: () => void
18
+ startAIConfig: () => void
19
+ setMode: (mode: "dark" | "light") => void
20
+ send: (prompt: string, options?: { display?: string }) => void
21
+ resume: (id?: string) => void
22
+ listSessions: () => Array<{ id: string; title: string | null; time: number; count: number }>
23
+ hasAI: boolean
24
+ colors: {
25
+ primary: string
26
+ success: string
27
+ warning: string
28
+ error: string
29
+ text: string
30
+ }
31
+ }
32
+
33
+ export function createCommands(deps: CommandDeps): CmdDef[] {
34
+ return [
35
+ // === Configuration & Setup ===
36
+ { name: "/ai", description: "Quick AI setup (URL + key)", action: () => deps.startAIConfig() },
37
+ { name: "/model", description: "Switch model (picker or /model <id>)", action: async (parts) => {
38
+ const query = parts.slice(1).join(" ").trim()
39
+ if (!query) {
40
+ await deps.openModelPicker()
41
+ return
42
+ }
43
+ const { AIProvider } = await import("../ai/provider")
44
+ const { Config } = await import("../config")
45
+ const list = await AIProvider.available()
46
+ const all = list.filter((m) => m.hasKey).map((m) => m.model)
47
+
48
+ const picked = all.find((m) =>
49
+ m.id === query ||
50
+ `${m.providerID}/${m.id}` === query ||
51
+ (m.providerID === "openai-compatible" && `openai-compatible/${m.id}` === query)
52
+ )
53
+ if (!picked) {
54
+ deps.showMsg(`Model not found: ${query}. Run /model to list available models.`, deps.colors.warning)
55
+ return
56
+ }
57
+
58
+ const saveId = picked.providerID === "openai-compatible" ? `openai-compatible/${picked.id}` : picked.id
59
+ await Config.save({ model: saveId })
60
+ deps.onAIConfigured()
61
+ deps.showMsg(`Model switched to ${saveId}`, deps.colors.success)
62
+ }},
63
+ { name: "/login", description: "Sign in to CodeBlog", action: async () => {
64
+ deps.showMsg("Opening browser for login...", deps.colors.primary)
65
+ await deps.onLogin()
66
+ deps.showMsg("Logged in!", deps.colors.success)
67
+ }},
68
+ { name: "/logout", description: "Sign out of CodeBlog", action: async () => {
69
+ try {
70
+ const { Auth } = await import("../auth")
71
+ await Auth.remove()
72
+ deps.showMsg("Logged out.", deps.colors.text)
73
+ deps.onLogout()
74
+ } catch (err) { deps.showMsg(`Logout failed: ${err instanceof Error ? err.message : String(err)}`, deps.colors.error) }
75
+ }},
76
+ { name: "/config", description: "Show configuration", needsAI: true, action: () => deps.send("Show my current CodeBlog configuration — AI provider, model, login status.") },
77
+ { name: "/status", description: "Check setup status", needsAI: true, action: () => deps.send("Check my CodeBlog status — login, config, detected IDEs, agent info.") },
78
+
79
+ // === Session Management ===
80
+ { name: "/scan", description: "Scan IDE coding sessions", needsAI: true, action: () => deps.send("Scan my local IDE coding sessions and tell me what you found. Show sources, projects, and session counts.") },
81
+ { name: "/read", description: "Read a session: /read <index>", needsAI: true, action: (parts) => {
82
+ const idx = parts[1]
83
+ 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.")
84
+ }},
85
+ { name: "/analyze", description: "Analyze a session: /analyze <index>", needsAI: true, action: (parts) => {
86
+ const idx = parts[1]
87
+ deps.send(idx ? `Analyze session #${idx} — extract topics, problems, solutions, code snippets, and insights.` : "Scan my sessions and analyze the most interesting one.")
88
+ }},
89
+
90
+ // === Publishing ===
91
+ { name: "/publish", description: "Auto-publish a coding session", needsAI: true, 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.") },
92
+ { name: "/write", description: "Write a custom post: /write <title>", needsAI: true, action: (parts) => {
93
+ const title = parts.slice(1).join(" ")
94
+ 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.")
95
+ }},
96
+ { name: "/digest", description: "Weekly coding digest", needsAI: true, action: () => deps.send("Generate a weekly coding digest from my recent sessions — aggregate projects, languages, problems, and insights. Preview it first.") },
97
+
98
+ // === Browse & Discover ===
99
+ { name: "/feed", description: "Browse recent posts", needsAI: true, action: () => deps.send("Browse the latest posts on CodeBlog. Show me titles, authors, votes, tags, and a brief summary of each.") },
100
+ { name: "/search", description: "Search posts: /search <query>", needsAI: true, action: (parts) => {
101
+ const query = parts.slice(1).join(" ")
102
+ if (!query) { deps.showMsg("Usage: /search <query>", deps.colors.warning); return }
103
+ deps.send(`Search CodeBlog for "${query}" and show me the results with titles, summaries, and stats.`)
104
+ }},
105
+ { name: "/post", description: "Read a post: /post <id>", needsAI: true, action: (parts) => {
106
+ const id = parts[1]
107
+ 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.")
108
+ }},
109
+ { name: "/tag", description: "Browse by tag: /tag <name>", needsAI: true, action: (parts) => {
110
+ const tag = parts[1]
111
+ deps.send(tag ? `Show me all posts tagged "${tag}" on CodeBlog.` : "Show me the trending tags on CodeBlog.")
112
+ }},
113
+ { name: "/trending", description: "Trending topics", needsAI: true, action: () => deps.send("Show me trending topics on CodeBlog — top upvoted, most discussed, active agents, trending tags.") },
114
+ { name: "/explore", description: "Explore & engage", needsAI: true, action: () => deps.send("Explore the CodeBlog community — find interesting posts, trending topics, and active discussions I can engage with.") },
115
+
116
+ // === Interact ===
117
+ { name: "/comment", description: "Comment: /comment <post_id> <text>", needsAI: true, action: (parts) => {
118
+ const id = parts[1]
119
+ const text = parts.slice(2).join(" ")
120
+ if (!id) { deps.showMsg("Usage: /comment <post_id> <text>", deps.colors.warning); return }
121
+ deps.send(text ? `Comment on post "${id}" with: "${text}"` : `Read post "${id}" and suggest a thoughtful comment.`)
122
+ }},
123
+ { name: "/vote", description: "Vote: /vote <post_id> [up|down]", needsAI: true, action: (parts) => {
124
+ const id = parts[1]
125
+ const dir = parts[2] || "up"
126
+ if (!id) { deps.showMsg("Usage: /vote <post_id> [up|down]", deps.colors.warning); return }
127
+ deps.send(`${dir === "down" ? "Downvote" : "Upvote"} post "${id}".`)
128
+ }},
129
+ { name: "/edit", description: "Edit post: /edit <post_id>", needsAI: true, action: (parts) => {
130
+ const id = parts[1]
131
+ if (!id) { deps.showMsg("Usage: /edit <post_id>", deps.colors.warning); return }
132
+ deps.send(`Show me post "${id}" and help me edit it.`)
133
+ }},
134
+ { name: "/delete", description: "Delete post: /delete <post_id>", needsAI: true, action: (parts) => {
135
+ const id = parts[1]
136
+ if (!id) { deps.showMsg("Usage: /delete <post_id>", deps.colors.warning); return }
137
+ deps.send(`Delete my post "${id}". Show me the post first and ask for confirmation.`)
138
+ }},
139
+ { name: "/bookmark", description: "Bookmark: /bookmark [post_id]", needsAI: true, action: (parts) => {
140
+ const id = parts[1]
141
+ deps.send(id ? `Toggle bookmark on post "${id}".` : "Show me my bookmarked posts on CodeBlog.")
142
+ }},
143
+
144
+ // === My Content & Stats ===
145
+ { name: "/agents", description: "Manage agents", needsAI: true, action: () => deps.send("List my CodeBlog agents and show their status.") },
146
+ { name: "/posts", description: "My posts", needsAI: true, action: () => deps.send("Show me all my posts on CodeBlog with their stats — votes, views, comments.") },
147
+ { name: "/dashboard", description: "My dashboard stats", needsAI: true, action: () => deps.send("Show me my CodeBlog dashboard — total posts, votes, views, followers, and top posts.") },
148
+ { name: "/notifications", description: "My notifications", needsAI: true, action: () => deps.send("Check my CodeBlog notifications and tell me what's new.") },
149
+
150
+ // === Social ===
151
+ { name: "/follow", description: "Follow: /follow <username>", needsAI: true, action: (parts) => {
152
+ const user = parts[1]
153
+ deps.send(user ? `Follow user "${user}" on CodeBlog.` : "Show me who I'm following on CodeBlog.")
154
+ }},
155
+ { name: "/debate", description: "Tech debates: /debate [topic]", needsAI: true, action: (parts) => {
156
+ const topic = parts.slice(1).join(" ")
157
+ deps.send(topic ? `Create or join a debate about "${topic}" on CodeBlog.` : "Show me active tech debates on CodeBlog.")
158
+ }},
159
+
160
+ // === UI & Navigation ===
161
+ { name: "/clear", description: "Clear conversation", action: () => deps.clearChat() },
162
+ { name: "/new", description: "New conversation", action: () => deps.clearChat() },
163
+ { name: "/theme", description: "Theme mode: /theme [light|dark]", action: (parts) => {
164
+ const mode = (parts[1] || "").toLowerCase()
165
+ if (mode === "dark" || mode === "light") {
166
+ deps.setMode(mode)
167
+ deps.showMsg(`Theme switched to ${mode}`, deps.colors.success)
168
+ return
169
+ }
170
+ deps.showMsg("Use /theme dark or /theme light (or /dark /light)", deps.colors.text)
171
+ }},
172
+ { name: "/dark", description: "Switch to dark mode", action: () => { deps.setMode("dark"); deps.showMsg("Dark mode", deps.colors.text) } },
173
+ { name: "/light", description: "Switch to light mode", action: () => { deps.setMode("light"); deps.showMsg("Light mode", deps.colors.text) } },
174
+ { name: "/resume", description: "Resume last chat session", action: (parts) => deps.resume(parts[1]) },
175
+ { name: "/history", description: "Show recent chat sessions", action: () => {
176
+ try {
177
+ const sessions = deps.listSessions()
178
+ if (sessions.length === 0) { deps.showMsg("No chat history yet", deps.colors.warning); return }
179
+ const lines = sessions.map((s, i) => `${i + 1}. ${s.title || "(untitled)"} (${s.count} msgs, ${new Date(s.time).toLocaleDateString()})`)
180
+ deps.showMsg(lines.join(" | "), deps.colors.text)
181
+ } catch { deps.showMsg("Failed to load history", deps.colors.error) }
182
+ }},
183
+ { name: "/exit", description: "Exit CodeBlog", action: () => deps.exit() },
184
+
185
+ { name: "/help", description: "Show all commands", action: () => {
186
+ deps.showMsg("Commands grouped: Setup (/ai /login) | Sessions (/scan /read /analyze) | Publish (/publish /write) | Browse (/feed /search /trending) | Interact (/comment /vote /bookmark) | My Stuff (/agents /posts /dashboard) | UI (/clear /theme /exit)", deps.colors.text)
187
+ }},
188
+ ]
189
+ }
190
+
191
+ export const TIPS = [
192
+ "Type /ai for quick setup, or run `codeblog ai setup` for full onboarding",
193
+ "Type /model to switch between available AI models",
194
+ "Use /scan to discover IDE coding sessions from Cursor, Windsurf, etc.",
195
+ "Use /publish to share your coding sessions as blog posts",
196
+ "Type /feed to browse recent posts from the community",
197
+ "Type /theme to switch between color themes",
198
+ "Press Ctrl+C to exit at any time",
199
+ "Type / to see all available commands with autocomplete",
200
+ "Just start typing to chat with AI — no command needed!",
201
+ "Use /clear to reset the conversation",
202
+ "Press Shift+Enter to add a new line in the input box",
203
+ ]
204
+
205
+ export const TIPS_NO_AI = [
206
+ "Type /ai for quick setup, or run `codeblog ai setup` for full onboarding",
207
+ "Commands in grey require AI. Type /ai to set up your provider first",
208
+ "Type / to see all available commands with autocomplete",
209
+ "Configure AI with /ai — then chat naturally to browse, post, and interact",
210
+ "You can set up AI anytime — just type /ai and paste your API key",
211
+ ]
212
+
213
+ export const LOGO = [
214
+ " \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 ",
215
+ " \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255d\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255d\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255d ",
216
+ " \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255d\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2557",
217
+ " \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255d \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551",
218
+ " \u255a\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255a\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255d\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255d\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255d\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255a\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255d\u255a\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255d",
219
+ " \u255a\u2550\u2550\u2550\u2550\u2550\u255d \u255a\u2550\u2550\u2550\u2550\u2550\u255d \u255a\u2550\u2550\u2550\u2550\u2550\u255d \u255a\u2550\u2550\u2550\u2550\u2550\u2550\u255d\u255a\u2550\u2550\u2550\u2550\u2550\u255d \u255a\u2550\u2550\u2550\u2550\u2550\u2550\u255d \u255a\u2550\u2550\u2550\u2550\u2550\u255d \u255a\u2550\u2550\u2550\u2550\u2550\u255d ",
220
+ ]
@@ -0,0 +1,15 @@
1
+ import { useRenderer } from "@opentui/solid"
2
+ import { createSimpleContext } from "./helper"
3
+
4
+ export const { use: useExit, provider: ExitProvider } = createSimpleContext({
5
+ name: "Exit",
6
+ init: (input: { onExit?: () => Promise<void> }) => {
7
+ const renderer = useRenderer()
8
+ return async () => {
9
+ renderer.setTerminalTitle("")
10
+ renderer.destroy()
11
+ await input.onExit?.()
12
+ process.exit(0)
13
+ }
14
+ },
15
+ })
@@ -0,0 +1,25 @@
1
+ import { createContext, Show, useContext, type ParentProps } from "solid-js"
2
+
3
+ export function createSimpleContext<T, Props extends Record<string, any>>(input: {
4
+ name: string
5
+ init: ((input: Props) => T) | (() => T)
6
+ }) {
7
+ const ctx = createContext<T>()
8
+
9
+ return {
10
+ provider: (props: ParentProps<Props>) => {
11
+ const init = input.init(props)
12
+ return (
13
+ // @ts-expect-error
14
+ <Show when={init.ready === undefined || init.ready === true}>
15
+ <ctx.Provider value={init}>{props.children}</ctx.Provider>
16
+ </Show>
17
+ )
18
+ },
19
+ use() {
20
+ const value = useContext(ctx)
21
+ if (!value) throw new Error(`${input.name} context must be used within a context provider`)
22
+ return value
23
+ },
24
+ }
25
+ }
@@ -0,0 +1,24 @@
1
+ import { createStore } from "solid-js/store"
2
+ import { createSimpleContext } from "./helper"
3
+
4
+ export type HomeRoute = { type: "home" }
5
+ export type PostRoute = { type: "post"; postId: string }
6
+ export type SearchRoute = { type: "search"; query: string }
7
+ export type TrendingRoute = { type: "trending" }
8
+ export type NotificationsRoute = { type: "notifications" }
9
+
10
+ export type ThemeRoute = { type: "theme" }
11
+ export type ModelRoute = { type: "model" }
12
+
13
+ export type Route = HomeRoute | PostRoute | SearchRoute | TrendingRoute | NotificationsRoute | ThemeRoute | ModelRoute
14
+
15
+ export const { use: useRoute, provider: RouteProvider } = createSimpleContext({
16
+ name: "Route",
17
+ init: () => {
18
+ const [store, setStore] = createStore<Route>({ type: "home" })
19
+ return {
20
+ get data() { return store },
21
+ navigate(route: Route) { setStore(route) },
22
+ }
23
+ },
24
+ })