codeblog-app 2.3.1 → 2.3.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.
Files changed (83) hide show
  1. package/package.json +8 -73
  2. package/drizzle/0000_init.sql +0 -34
  3. package/drizzle/meta/_journal.json +0 -13
  4. package/drizzle.config.ts +0 -10
  5. package/src/ai/__tests__/chat.test.ts +0 -188
  6. package/src/ai/__tests__/compat.test.ts +0 -46
  7. package/src/ai/__tests__/home.ai-stream.integration.test.ts +0 -77
  8. package/src/ai/__tests__/provider-registry.test.ts +0 -61
  9. package/src/ai/__tests__/provider.test.ts +0 -238
  10. package/src/ai/__tests__/stream-events.test.ts +0 -152
  11. package/src/ai/__tests__/tools.test.ts +0 -93
  12. package/src/ai/chat.ts +0 -336
  13. package/src/ai/configure.ts +0 -143
  14. package/src/ai/models.ts +0 -26
  15. package/src/ai/provider-registry.ts +0 -150
  16. package/src/ai/provider.ts +0 -264
  17. package/src/ai/stream-events.ts +0 -64
  18. package/src/ai/tools.ts +0 -118
  19. package/src/ai/types.ts +0 -105
  20. package/src/auth/index.ts +0 -49
  21. package/src/auth/oauth.ts +0 -123
  22. package/src/cli/__tests__/commands.test.ts +0 -229
  23. package/src/cli/cmd/agent.ts +0 -97
  24. package/src/cli/cmd/ai.ts +0 -10
  25. package/src/cli/cmd/chat.ts +0 -190
  26. package/src/cli/cmd/comment.ts +0 -67
  27. package/src/cli/cmd/config.ts +0 -153
  28. package/src/cli/cmd/feed.ts +0 -53
  29. package/src/cli/cmd/forum.ts +0 -106
  30. package/src/cli/cmd/login.ts +0 -45
  31. package/src/cli/cmd/logout.ts +0 -12
  32. package/src/cli/cmd/me.ts +0 -188
  33. package/src/cli/cmd/post.ts +0 -25
  34. package/src/cli/cmd/publish.ts +0 -64
  35. package/src/cli/cmd/scan.ts +0 -78
  36. package/src/cli/cmd/search.ts +0 -35
  37. package/src/cli/cmd/setup.ts +0 -622
  38. package/src/cli/cmd/tui.ts +0 -20
  39. package/src/cli/cmd/uninstall.ts +0 -281
  40. package/src/cli/cmd/update.ts +0 -123
  41. package/src/cli/cmd/vote.ts +0 -50
  42. package/src/cli/cmd/whoami.ts +0 -18
  43. package/src/cli/mcp-print.ts +0 -6
  44. package/src/cli/ui.ts +0 -357
  45. package/src/config/index.ts +0 -92
  46. package/src/flag/index.ts +0 -23
  47. package/src/global/index.ts +0 -38
  48. package/src/id/index.ts +0 -20
  49. package/src/index.ts +0 -203
  50. package/src/mcp/__tests__/client.test.ts +0 -149
  51. package/src/mcp/__tests__/e2e.ts +0 -331
  52. package/src/mcp/__tests__/integration.ts +0 -148
  53. package/src/mcp/client.ts +0 -118
  54. package/src/server/index.ts +0 -48
  55. package/src/storage/chat.ts +0 -73
  56. package/src/storage/db.ts +0 -85
  57. package/src/storage/schema.sql.ts +0 -39
  58. package/src/storage/schema.ts +0 -1
  59. package/src/tui/__tests__/input-intent.test.ts +0 -27
  60. package/src/tui/__tests__/stream-assembler.test.ts +0 -33
  61. package/src/tui/ai-stream.ts +0 -28
  62. package/src/tui/app.tsx +0 -210
  63. package/src/tui/commands.ts +0 -220
  64. package/src/tui/context/exit.tsx +0 -15
  65. package/src/tui/context/helper.tsx +0 -25
  66. package/src/tui/context/route.tsx +0 -24
  67. package/src/tui/context/theme.tsx +0 -471
  68. package/src/tui/input-intent.ts +0 -26
  69. package/src/tui/routes/home.tsx +0 -1060
  70. package/src/tui/routes/model.tsx +0 -210
  71. package/src/tui/routes/notifications.tsx +0 -87
  72. package/src/tui/routes/post.tsx +0 -102
  73. package/src/tui/routes/search.tsx +0 -105
  74. package/src/tui/routes/setup.tsx +0 -267
  75. package/src/tui/routes/trending.tsx +0 -107
  76. package/src/tui/stream-assembler.ts +0 -49
  77. package/src/util/__tests__/context.test.ts +0 -31
  78. package/src/util/__tests__/lazy.test.ts +0 -37
  79. package/src/util/context.ts +0 -23
  80. package/src/util/error.ts +0 -46
  81. package/src/util/lazy.ts +0 -18
  82. package/src/util/log.ts +0 -144
  83. package/tsconfig.json +0 -11
@@ -1,210 +0,0 @@
1
- import { createSignal, createMemo, onMount, Show, For } from "solid-js"
2
- import { useKeyboard, usePaste } from "@opentui/solid"
3
- import { useTheme } from "../context/theme"
4
-
5
- interface ModelItem {
6
- id: string
7
- name: string
8
- provider: string
9
- }
10
-
11
- export function ModelPicker(props: { onDone: (model?: string) => void }) {
12
- const theme = useTheme()
13
- const [models, setModels] = createSignal<ModelItem[]>([])
14
- const [idx, setIdx] = createSignal(0)
15
- const [current, setCurrent] = createSignal("")
16
- const [loading, setLoading] = createSignal(true)
17
- const [filter, setFilter] = createSignal("")
18
- const [status, setStatus] = createSignal("")
19
-
20
- // Visible height for scrolling
21
- const maxVisible = 15
22
-
23
- const filtered = createMemo(() => {
24
- const q = filter().toLowerCase()
25
- if (!q) return models()
26
- return models().filter((m) => m.id.toLowerCase().includes(q) || m.name.toLowerCase().includes(q))
27
- })
28
-
29
- // Scroll offset
30
- const scrollTop = createMemo(() => {
31
- const i = idx()
32
- const len = filtered().length
33
- if (len <= maxVisible) return 0
34
- if (i < maxVisible - 2) return 0
35
- if (i > len - 3) return Math.max(0, len - maxVisible)
36
- return i - maxVisible + 3
37
- })
38
-
39
- const visible = createMemo(() => filtered().slice(scrollTop(), scrollTop() + maxVisible))
40
-
41
- onMount(async () => {
42
- try {
43
- const { AIProvider } = await import("../../ai/provider")
44
- const { Config } = await import("../../config")
45
- const cfg = await Config.load()
46
- setCurrent(cfg.model || AIProvider.DEFAULT_MODEL)
47
-
48
- setStatus("Fetching models from API...")
49
- const all = await AIProvider.available()
50
- const items = all.filter((m) => m.hasKey).map((m) => ({
51
- id: m.model.id,
52
- name: m.model.name,
53
- provider: m.model.providerID,
54
- }))
55
- if (items.length > 0) {
56
- setModels(items)
57
- const modelId = cfg.model || AIProvider.DEFAULT_MODEL
58
- const curIdx = items.findIndex((m) => m.id === modelId || `${m.provider}/${m.id}` === modelId)
59
- if (curIdx >= 0) setIdx(curIdx)
60
- setStatus(`${items.length} models loaded`)
61
- } else {
62
- setModels([])
63
- setStatus("No models with API keys configured")
64
- }
65
- } catch (err) {
66
- setStatus(`Error: ${err instanceof Error ? err.message : String(err)}`)
67
- }
68
- setLoading(false)
69
- })
70
-
71
- usePaste((evt) => {
72
- const text = evt.text.replace(/[\n\r]/g, "").trim()
73
- if (text) {
74
- setFilter((s) => s + text)
75
- setIdx(0)
76
- evt.preventDefault()
77
- }
78
- })
79
-
80
- useKeyboard((evt) => {
81
- if (evt.name === "up") {
82
- setIdx((i) => (i - 1 + filtered().length) % filtered().length)
83
- evt.preventDefault()
84
- return
85
- }
86
- if (evt.name === "down") {
87
- setIdx((i) => (i + 1) % filtered().length)
88
- evt.preventDefault()
89
- return
90
- }
91
- if (evt.name === "return") {
92
- const m = filtered()[idx()]
93
- if (m) save(m.id)
94
- evt.preventDefault()
95
- return
96
- }
97
- if (evt.name === "escape") {
98
- if (filter()) { setFilter(""); setIdx(0) }
99
- else props.onDone()
100
- evt.preventDefault()
101
- return
102
- }
103
- if (evt.name === "backspace") {
104
- setFilter((s) => s.slice(0, -1))
105
- setIdx(0)
106
- evt.preventDefault()
107
- return
108
- }
109
- if (evt.sequence && evt.sequence.length >= 1 && !evt.ctrl && !evt.meta) {
110
- const clean = evt.sequence.replace(/[\x00-\x1f\x7f]/g, "")
111
- if (clean) {
112
- setFilter((s) => s + clean)
113
- setIdx(0)
114
- evt.preventDefault()
115
- return
116
- }
117
- }
118
- if (evt.name === "space") {
119
- setFilter((s) => s + " ")
120
- evt.preventDefault()
121
- return
122
- }
123
- })
124
-
125
- async function save(id: string) {
126
- try {
127
- const item = filtered().find((m) => m.id === id)
128
- const saveId = item && item.provider === "openai-compatible" ? `openai-compatible/${id}` : id
129
- const { Config } = await import("../../config")
130
- await Config.save({ model: saveId })
131
- props.onDone(saveId)
132
- } catch {
133
- props.onDone()
134
- }
135
- }
136
-
137
- return (
138
- <box flexDirection="column" flexGrow={1} paddingLeft={2} paddingRight={2} paddingTop={1}>
139
- <box flexDirection="row" flexShrink={0}>
140
- <text fg={theme.colors.primary}>
141
- <span style={{ bold: true }}>Select Model</span>
142
- </text>
143
- <box flexGrow={1} />
144
- <text fg={theme.colors.textMuted}>
145
- ↑↓ select · Enter confirm · Type to filter · Esc back
146
- </text>
147
- </box>
148
-
149
- <box flexShrink={0} flexDirection="row" paddingTop={1}>
150
- <text fg={theme.colors.textMuted}>Current: </text>
151
- <text fg={theme.colors.success}>{current()}</text>
152
- <text fg={theme.colors.textMuted}>{" · " + status()}</text>
153
- </box>
154
-
155
- {/* Filter input */}
156
- <box flexShrink={0} paddingTop={1} flexDirection="row">
157
- <text fg={theme.colors.primary}><span style={{ bold: true }}>{"❯ "}</span></text>
158
- <text fg={theme.colors.input}>{filter()}</text>
159
- <text fg={theme.colors.cursor}>{"█"}</text>
160
- <Show when={!filter()}>
161
- <text fg={theme.colors.textMuted}> type to filter...</text>
162
- </Show>
163
- </box>
164
-
165
- <box height={1} />
166
-
167
- <Show when={loading()}>
168
- <text fg={theme.colors.textMuted}>Fetching models from API...</text>
169
- </Show>
170
-
171
- <Show when={!loading()}>
172
- <Show when={filtered().length === 0}>
173
- <text fg={theme.colors.warning}>No models match "{filter()}"</text>
174
- </Show>
175
- <Show when={filtered().length > 0}>
176
- <box flexDirection="column" flexShrink={0}>
177
- <For each={visible()}>
178
- {(m, i) => {
179
- const selected = () => scrollTop() + i() === idx()
180
- return (
181
- <box flexDirection="row" backgroundColor={selected() ? theme.colors.primary : undefined}>
182
- <text fg={selected() ? "#ffffff" : theme.colors.textMuted}>
183
- {selected() ? "❯ " : " "}
184
- </text>
185
- <text fg={selected() ? "#ffffff" : theme.colors.text}>
186
- <span style={{ bold: selected() }}>
187
- {m.id.length > 40 ? m.id.slice(0, 40) + "…" : m.id.padEnd(42)}
188
- </span>
189
- </text>
190
- <text fg={selected() ? "#ffffff" : theme.colors.textMuted}>
191
- {" " + m.provider}
192
- </text>
193
- {(m.id === current() || `${m.provider}/${m.id}` === current()) ? (
194
- <text fg={selected() ? "#ffffff" : theme.colors.success}>{" ← current"}</text>
195
- ) : null}
196
- </box>
197
- )
198
- }}
199
- </For>
200
- </box>
201
- <Show when={filtered().length > maxVisible}>
202
- <text fg={theme.colors.textMuted}>
203
- {" ↑↓ " + filtered().length + " models total · showing " + (scrollTop() + 1) + "-" + Math.min(scrollTop() + maxVisible, filtered().length)}
204
- </text>
205
- </Show>
206
- </Show>
207
- </Show>
208
- </box>
209
- )
210
- }
@@ -1,87 +0,0 @@
1
- import { createSignal, onMount, For, Show } from "solid-js"
2
- import { useKeyboard } from "@opentui/solid"
3
- import { useTheme } from "../context/theme"
4
- import { McpBridge } from "../../mcp/client"
5
-
6
- export function Notifications() {
7
- const theme = useTheme()
8
- const [items, setItems] = createSignal<any[]>([])
9
- const [loading, setLoading] = createSignal(true)
10
- const [selected, setSelected] = createSignal(0)
11
-
12
- onMount(async () => {
13
- try {
14
- const result = await McpBridge.callToolJSON<any>("my_notifications", { action: "list" })
15
- setItems(result.notifications || result || [])
16
- } catch {
17
- setItems([])
18
- }
19
- setLoading(false)
20
- })
21
-
22
- useKeyboard((evt) => {
23
- const n = items()
24
- if (evt.name === "up" || evt.name === "k") {
25
- setSelected((s) => Math.max(0, s - 1))
26
- evt.preventDefault()
27
- }
28
- if (evt.name === "down" || evt.name === "j") {
29
- setSelected((s) => Math.min(n.length - 1, s + 1))
30
- evt.preventDefault()
31
- }
32
- })
33
-
34
- return (
35
- <box flexDirection="column" flexGrow={1}>
36
- <box paddingLeft={2} paddingRight={2} paddingTop={1} flexShrink={0} flexDirection="row" gap={1}>
37
- <text fg={theme.colors.accent}>
38
- <span style={{ bold: true }}>Notifications</span>
39
- </text>
40
- <text fg={theme.colors.textMuted}>{`(${items().length})`}</text>
41
- <box flexGrow={1} />
42
- <text fg={theme.colors.textMuted}>esc:back j/k:navigate</text>
43
- </box>
44
-
45
- <Show when={loading()}>
46
- <box paddingLeft={4} paddingTop={1}>
47
- <text fg={theme.colors.textMuted}>Loading notifications...</text>
48
- </box>
49
- </Show>
50
-
51
- <Show when={!loading() && items().length === 0}>
52
- <box paddingLeft={4} paddingTop={1}>
53
- <text fg={theme.colors.textMuted}>No notifications.</text>
54
- </box>
55
- </Show>
56
-
57
- <box flexDirection="column" paddingTop={1} flexGrow={1}>
58
- <For each={items()}>
59
- {(item: any, i) => {
60
- const isSelected = () => i() === selected()
61
- const isRead = item.read || item.is_read
62
- return (
63
- <box flexDirection="row" paddingLeft={2} paddingRight={2}>
64
- <box width={3}>
65
- <text fg={isRead ? theme.colors.textMuted : theme.colors.primary}>
66
- {isRead ? " " : "● "}
67
- </text>
68
- </box>
69
- <box flexDirection="column" flexGrow={1}>
70
- <text fg={isSelected() ? theme.colors.primary : theme.colors.text}>
71
- <span style={{ bold: isSelected() }}>
72
- {item.message || item.content || item.type || "Notification"}
73
- </span>
74
- </text>
75
- <box flexDirection="row" gap={1}>
76
- <text fg={theme.colors.textMuted}>{item.from_user || item.actor || ""}</text>
77
- <text fg={theme.colors.textMuted}>{item.created_at || ""}</text>
78
- </box>
79
- </box>
80
- </box>
81
- )
82
- }}
83
- </For>
84
- </box>
85
- </box>
86
- )
87
- }
@@ -1,102 +0,0 @@
1
- import { createSignal, onMount, For, Show } from "solid-js"
2
- import { useKeyboard } from "@opentui/solid"
3
- import { useRoute } from "../context/route"
4
- import { useTheme } from "../context/theme"
5
- import { McpBridge } from "../../mcp/client"
6
-
7
- export function Post() {
8
- const route = useRoute()
9
- const theme = useTheme()
10
- const postId = () => route.data.type === "post" ? route.data.postId : ""
11
- const [post, setPost] = createSignal<any>(null)
12
- const [comments, setComments] = createSignal<any[]>([])
13
- const [loading, setLoading] = createSignal(true)
14
-
15
- onMount(async () => {
16
- try {
17
- const result = await McpBridge.callToolJSON<any>("read_post", { post_id: postId() })
18
- const p = result.post || result
19
- setPost(p)
20
- setComments(p.comments || [])
21
- } catch {
22
- setPost(null)
23
- }
24
- setLoading(false)
25
- })
26
-
27
- useKeyboard((evt) => {
28
- if (evt.name === "up" || evt.name === "k") {
29
- evt.preventDefault()
30
- }
31
- if (evt.name === "down" || evt.name === "j") {
32
- evt.preventDefault()
33
- }
34
- })
35
-
36
- return (
37
- <box flexDirection="column" flexGrow={1}>
38
- <Show when={loading()}>
39
- <box paddingLeft={4} paddingTop={2}>
40
- <text fg={theme.colors.textMuted}>Loading post...</text>
41
- </box>
42
- </Show>
43
-
44
- <Show when={!loading() && !post()}>
45
- <box paddingLeft={4} paddingTop={2}>
46
- <text fg={theme.colors.error}>Post not found.</text>
47
- </box>
48
- </Show>
49
-
50
- <Show when={!loading() && post()}>
51
- <box paddingLeft={2} paddingRight={2} paddingTop={1} flexShrink={0}>
52
- <text fg={theme.colors.text}>
53
- <span style={{ bold: true }}>{post()?.title}</span>
54
- </text>
55
- </box>
56
-
57
- <box paddingLeft={2} paddingTop={0} flexShrink={0} flexDirection="row" gap={2}>
58
- <text fg={theme.colors.success}>{`▲${(post()?.upvotes ?? 0) - (post()?.downvotes ?? 0)}`}</text>
59
- <text fg={theme.colors.textMuted}>{`💬${post()?.comment_count ?? 0} 👁${post()?.views ?? 0}`}</text>
60
- <text fg={theme.colors.textMuted}>{`by ${post()?.agent?.name || "anon"}`}</text>
61
- </box>
62
-
63
- <Show when={(post()?.tags || []).length > 0}>
64
- <box paddingLeft={2} paddingTop={0} flexShrink={0} flexDirection="row" gap={1}>
65
- <For each={post()?.tags || []}>
66
- {(tag: string) => <text fg={theme.colors.primary}>{`#${tag}`}</text>}
67
- </For>
68
- </box>
69
- </Show>
70
-
71
- <box paddingLeft={2} paddingRight={2} paddingTop={1} flexGrow={1}>
72
- <text fg={theme.colors.text}>{post()?.content?.slice(0, 2000) || post()?.summary || ""}</text>
73
- </box>
74
-
75
- <Show when={comments().length > 0}>
76
- <box paddingLeft={2} paddingTop={1} flexShrink={0}>
77
- <text fg={theme.colors.accent}>
78
- <span style={{ bold: true }}>{`Comments (${comments().length})`}</span>
79
- </text>
80
- </box>
81
- <box flexDirection="column" paddingLeft={2} paddingRight={2}>
82
- <For each={comments()}>
83
- {(comment: any) => (
84
- <box flexDirection="column" paddingTop={1}>
85
- <box flexDirection="row" gap={1}>
86
- <text fg={theme.colors.primary}>
87
- <span style={{ bold: true }}>{comment.user?.username || comment.agent || "anon"}</span>
88
- </text>
89
- <text fg={theme.colors.textMuted}>{comment.createdAt || comment.created_at || ""}</text>
90
- </box>
91
- <box paddingLeft={2}>
92
- <text fg={theme.colors.text}>{comment.content || comment.body || ""}</text>
93
- </box>
94
- </box>
95
- )}
96
- </For>
97
- </box>
98
- </Show>
99
- </Show>
100
- </box>
101
- )
102
- }
@@ -1,105 +0,0 @@
1
- import { createSignal, For, Show } from "solid-js"
2
- import { useKeyboard } from "@opentui/solid"
3
- import { useRoute } from "../context/route"
4
- import { useTheme } from "../context/theme"
5
- import { McpBridge } from "../../mcp/client"
6
-
7
- export function Search() {
8
- const route = useRoute()
9
- const theme = useTheme()
10
- const [query, setQuery] = createSignal(route.data.type === "search" ? route.data.query : "")
11
- const [results, setResults] = createSignal<any[]>([])
12
- const [loading, setLoading] = createSignal(false)
13
- const [searched, setSearched] = createSignal(false)
14
-
15
- async function doSearch(q: string) {
16
- if (!q.trim()) return
17
- setLoading(true)
18
- setSearched(true)
19
- try {
20
- const result = await McpBridge.callToolJSON<any>("search_posts", { query: q.trim() })
21
- setResults(result.results || result.posts || [])
22
- } catch {
23
- setResults([])
24
- }
25
- setLoading(false)
26
- }
27
-
28
- useKeyboard((evt) => {
29
- if (evt.name === "return" && !evt.shift) {
30
- doSearch(query())
31
- evt.preventDefault()
32
- return
33
- }
34
- if (evt.name === "backspace") {
35
- setQuery((s) => s.slice(0, -1))
36
- evt.preventDefault()
37
- return
38
- }
39
- if (evt.sequence && evt.sequence.length === 1 && !evt.ctrl && !evt.meta) {
40
- setQuery((s) => s + evt.sequence)
41
- evt.preventDefault()
42
- return
43
- }
44
- if (evt.name === "space") {
45
- setQuery((s) => s + " ")
46
- evt.preventDefault()
47
- return
48
- }
49
- })
50
-
51
- return (
52
- <box flexDirection="column" flexGrow={1}>
53
- <box paddingLeft={2} paddingRight={2} paddingTop={1} flexShrink={0} flexDirection="row" gap={1}>
54
- <text fg={theme.colors.accent}>
55
- <span style={{ bold: true }}>Search</span>
56
- </text>
57
- <box flexGrow={1} />
58
- <text fg={theme.colors.textMuted}>esc:back</text>
59
- </box>
60
-
61
- <box paddingLeft={2} paddingRight={2} paddingTop={1} flexShrink={0} flexDirection="row">
62
- <text fg={theme.colors.primary}>
63
- <span style={{ bold: true }}>{"🔍 "}</span>
64
- </text>
65
- <text fg={theme.colors.input}>{query()}</text>
66
- <text fg={theme.colors.cursor}>{"█"}</text>
67
- </box>
68
-
69
- <Show when={loading()}>
70
- <box paddingLeft={4} paddingTop={1}>
71
- <text fg={theme.colors.textMuted}>Searching...</text>
72
- </box>
73
- </Show>
74
-
75
- <Show when={!loading() && searched() && results().length === 0}>
76
- <box paddingLeft={4} paddingTop={1}>
77
- <text fg={theme.colors.textMuted}>No results found.</text>
78
- </box>
79
- </Show>
80
-
81
- <box flexDirection="column" paddingTop={1} flexGrow={1}>
82
- <For each={results()}>
83
- {(item: any) => (
84
- <box flexDirection="row" paddingLeft={2} paddingRight={2}>
85
- <box width={6} justifyContent="flex-end" marginRight={1}>
86
- <text fg={theme.colors.success}>{`▲${item.score ?? item.upvotes ?? 0}`}</text>
87
- </box>
88
- <box flexDirection="column" flexGrow={1}>
89
- <text fg={theme.colors.text}>
90
- <span style={{ bold: true }}>{item.title}</span>
91
- </text>
92
- <box flexDirection="row" gap={1}>
93
- <text fg={theme.colors.textMuted}>{`💬${item.comment_count ?? 0}`}</text>
94
- <For each={(item.tags || []).slice(0, 3)}>
95
- {(tag: string) => <text fg={theme.colors.primary}>{`#${tag}`}</text>}
96
- </For>
97
- </box>
98
- </box>
99
- </box>
100
- )}
101
- </For>
102
- </box>
103
- </box>
104
- )
105
- }