codeblog-app 2.1.0 → 2.1.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 +8 -71
- package/drizzle/0000_init.sql +0 -34
- package/drizzle/meta/_journal.json +0 -13
- package/drizzle.config.ts +0 -10
- package/src/ai/__tests__/chat.test.ts +0 -110
- package/src/ai/__tests__/provider.test.ts +0 -184
- package/src/ai/__tests__/tools.test.ts +0 -90
- package/src/ai/chat.ts +0 -169
- package/src/ai/configure.ts +0 -134
- package/src/ai/provider.ts +0 -238
- package/src/ai/tools.ts +0 -336
- package/src/auth/index.ts +0 -47
- package/src/auth/oauth.ts +0 -94
- package/src/cli/__tests__/commands.test.ts +0 -225
- package/src/cli/cmd/agent.ts +0 -102
- package/src/cli/cmd/chat.ts +0 -190
- package/src/cli/cmd/comment.ts +0 -70
- package/src/cli/cmd/config.ts +0 -153
- package/src/cli/cmd/feed.ts +0 -57
- package/src/cli/cmd/forum.ts +0 -123
- package/src/cli/cmd/login.ts +0 -45
- package/src/cli/cmd/logout.ts +0 -12
- package/src/cli/cmd/me.ts +0 -202
- package/src/cli/cmd/post.ts +0 -29
- package/src/cli/cmd/publish.ts +0 -70
- package/src/cli/cmd/scan.ts +0 -80
- package/src/cli/cmd/search.ts +0 -40
- package/src/cli/cmd/setup.ts +0 -273
- package/src/cli/cmd/tui.ts +0 -20
- package/src/cli/cmd/update.ts +0 -78
- package/src/cli/cmd/vote.ts +0 -50
- package/src/cli/cmd/whoami.ts +0 -21
- package/src/cli/ui.ts +0 -195
- package/src/config/index.ts +0 -54
- package/src/flag/index.ts +0 -23
- package/src/global/index.ts +0 -38
- package/src/id/index.ts +0 -20
- package/src/index.ts +0 -197
- package/src/mcp/__tests__/client.test.ts +0 -149
- package/src/mcp/__tests__/e2e.ts +0 -327
- package/src/mcp/__tests__/integration.ts +0 -148
- package/src/mcp/client.ts +0 -148
- package/src/server/index.ts +0 -48
- package/src/storage/chat.ts +0 -92
- package/src/storage/db.ts +0 -85
- package/src/storage/schema.sql.ts +0 -39
- package/src/storage/schema.ts +0 -1
- package/src/tui/app.tsx +0 -163
- package/src/tui/commands.ts +0 -187
- package/src/tui/context/exit.tsx +0 -15
- package/src/tui/context/helper.tsx +0 -25
- package/src/tui/context/route.tsx +0 -24
- package/src/tui/context/theme.tsx +0 -470
- package/src/tui/routes/home.tsx +0 -508
- package/src/tui/routes/model.tsx +0 -209
- package/src/tui/routes/notifications.tsx +0 -85
- package/src/tui/routes/post.tsx +0 -108
- package/src/tui/routes/search.tsx +0 -104
- package/src/tui/routes/setup.tsx +0 -255
- package/src/tui/routes/trending.tsx +0 -107
- package/src/util/__tests__/context.test.ts +0 -31
- package/src/util/__tests__/lazy.test.ts +0 -37
- package/src/util/context.ts +0 -23
- package/src/util/error.ts +0 -46
- package/src/util/lazy.ts +0 -18
- package/src/util/log.ts +0 -142
- package/tsconfig.json +0 -11
package/src/storage/chat.ts
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { Database as BunDatabase } from "bun:sqlite"
|
|
2
|
-
import { Global } from "../global"
|
|
3
|
-
import path from "path"
|
|
4
|
-
|
|
5
|
-
function db() {
|
|
6
|
-
const dbpath = path.join(Global.Path.data, "codeblog.db")
|
|
7
|
-
const sqlite = new BunDatabase(dbpath, { create: true })
|
|
8
|
-
sqlite.run("PRAGMA journal_mode = WAL")
|
|
9
|
-
sqlite.run("PRAGMA foreign_keys = ON")
|
|
10
|
-
|
|
11
|
-
sqlite.run(`CREATE TABLE IF NOT EXISTS chat_sessions (
|
|
12
|
-
id TEXT PRIMARY KEY,
|
|
13
|
-
title TEXT,
|
|
14
|
-
time_created INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
|
|
15
|
-
time_updated INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
|
|
16
|
-
)`)
|
|
17
|
-
|
|
18
|
-
sqlite.run(`CREATE TABLE IF NOT EXISTS chat_messages (
|
|
19
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
20
|
-
session_id TEXT NOT NULL,
|
|
21
|
-
role TEXT NOT NULL,
|
|
22
|
-
content TEXT NOT NULL,
|
|
23
|
-
tool_name TEXT,
|
|
24
|
-
tool_status TEXT,
|
|
25
|
-
time_created INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
|
|
26
|
-
)`)
|
|
27
|
-
|
|
28
|
-
return sqlite
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface ChatMsg {
|
|
32
|
-
role: "user" | "assistant" | "tool"
|
|
33
|
-
content: string
|
|
34
|
-
toolName?: string
|
|
35
|
-
toolStatus?: "running" | "done" | "error"
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export namespace ChatHistory {
|
|
39
|
-
export function create(id: string, title?: string) {
|
|
40
|
-
const d = db()
|
|
41
|
-
d.run("INSERT OR REPLACE INTO chat_sessions (id, title, time_created, time_updated) VALUES (?, ?, ?, ?)", [id, title || null, Date.now(), Date.now()])
|
|
42
|
-
d.close()
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function save(sessionId: string, messages: ChatMsg[]) {
|
|
46
|
-
const d = db()
|
|
47
|
-
d.run("DELETE FROM chat_messages WHERE session_id = ?", [sessionId])
|
|
48
|
-
const stmt = d.prepare("INSERT INTO chat_messages (session_id, role, content, tool_name, tool_status, time_created) VALUES (?, ?, ?, ?, ?, ?)")
|
|
49
|
-
for (const m of messages) {
|
|
50
|
-
stmt.run(sessionId, m.role, m.content, m.toolName || null, m.toolStatus || null, Date.now())
|
|
51
|
-
}
|
|
52
|
-
// Update session title from first user message
|
|
53
|
-
const first = messages.find((m) => m.role === "user")
|
|
54
|
-
if (first) {
|
|
55
|
-
const title = first.content.slice(0, 80)
|
|
56
|
-
d.run("UPDATE chat_sessions SET title = ?, time_updated = ? WHERE id = ?", [title, Date.now(), sessionId])
|
|
57
|
-
}
|
|
58
|
-
d.close()
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function load(sessionId: string): ChatMsg[] {
|
|
62
|
-
const d = db()
|
|
63
|
-
const rows = d.query("SELECT role, content, tool_name, tool_status FROM chat_messages WHERE session_id = ? ORDER BY id ASC").all(sessionId) as any[]
|
|
64
|
-
d.close()
|
|
65
|
-
return rows.map((r) => ({
|
|
66
|
-
role: r.role,
|
|
67
|
-
content: r.content,
|
|
68
|
-
...(r.tool_name ? { toolName: r.tool_name } : {}),
|
|
69
|
-
...(r.tool_status ? { toolStatus: r.tool_status } : {}),
|
|
70
|
-
}))
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function list(limit = 20): Array<{ id: string; title: string | null; time: number; count: number }> {
|
|
74
|
-
const d = db()
|
|
75
|
-
const rows = d.query(`
|
|
76
|
-
SELECT s.id, s.title, s.time_updated as time,
|
|
77
|
-
(SELECT COUNT(*) FROM chat_messages WHERE session_id = s.id) as count
|
|
78
|
-
FROM chat_sessions s
|
|
79
|
-
ORDER BY s.time_updated DESC
|
|
80
|
-
LIMIT ?
|
|
81
|
-
`).all(limit) as any[]
|
|
82
|
-
d.close()
|
|
83
|
-
return rows.map((r) => ({ id: r.id, title: r.title, time: r.time, count: r.count }))
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function remove(sessionId: string) {
|
|
87
|
-
const d = db()
|
|
88
|
-
d.run("DELETE FROM chat_messages WHERE session_id = ?", [sessionId])
|
|
89
|
-
d.run("DELETE FROM chat_sessions WHERE id = ?", [sessionId])
|
|
90
|
-
d.close()
|
|
91
|
-
}
|
|
92
|
-
}
|
package/src/storage/db.ts
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { Database as BunDatabase } from "bun:sqlite"
|
|
2
|
-
import { drizzle } from "drizzle-orm/bun-sqlite"
|
|
3
|
-
import { Context } from "../util/context"
|
|
4
|
-
import { lazy } from "../util/lazy"
|
|
5
|
-
import { Global } from "../global"
|
|
6
|
-
import { Log } from "../util/log"
|
|
7
|
-
import path from "path"
|
|
8
|
-
import * as schema from "./schema"
|
|
9
|
-
|
|
10
|
-
const log = Log.create({ service: "db" })
|
|
11
|
-
|
|
12
|
-
export namespace Database {
|
|
13
|
-
type Schema = typeof schema
|
|
14
|
-
|
|
15
|
-
export const Client = lazy(() => {
|
|
16
|
-
const dbpath = path.join(Global.Path.data, "codeblog.db")
|
|
17
|
-
log.info("opening database", { path: dbpath })
|
|
18
|
-
|
|
19
|
-
const sqlite = new BunDatabase(dbpath, { create: true })
|
|
20
|
-
|
|
21
|
-
sqlite.run("PRAGMA journal_mode = WAL")
|
|
22
|
-
sqlite.run("PRAGMA synchronous = NORMAL")
|
|
23
|
-
sqlite.run("PRAGMA busy_timeout = 5000")
|
|
24
|
-
sqlite.run("PRAGMA cache_size = -64000")
|
|
25
|
-
sqlite.run("PRAGMA foreign_keys = ON")
|
|
26
|
-
|
|
27
|
-
// Auto-create tables
|
|
28
|
-
sqlite.run(`CREATE TABLE IF NOT EXISTS published_sessions (
|
|
29
|
-
id TEXT PRIMARY KEY,
|
|
30
|
-
session_id TEXT NOT NULL,
|
|
31
|
-
source TEXT NOT NULL,
|
|
32
|
-
post_id TEXT,
|
|
33
|
-
file_path TEXT NOT NULL,
|
|
34
|
-
time_created INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
|
|
35
|
-
time_updated INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
|
|
36
|
-
)`)
|
|
37
|
-
|
|
38
|
-
sqlite.run(`CREATE TABLE IF NOT EXISTS cached_posts (
|
|
39
|
-
id TEXT PRIMARY KEY,
|
|
40
|
-
title TEXT NOT NULL,
|
|
41
|
-
content TEXT NOT NULL,
|
|
42
|
-
author_name TEXT,
|
|
43
|
-
votes INTEGER DEFAULT 0,
|
|
44
|
-
comments_count INTEGER DEFAULT 0,
|
|
45
|
-
tags TEXT,
|
|
46
|
-
time_created INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
|
|
47
|
-
time_updated INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
|
|
48
|
-
)`)
|
|
49
|
-
|
|
50
|
-
sqlite.run(`CREATE TABLE IF NOT EXISTS chat_sessions (
|
|
51
|
-
id TEXT PRIMARY KEY,
|
|
52
|
-
title TEXT,
|
|
53
|
-
time_created INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
|
|
54
|
-
time_updated INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
|
|
55
|
-
)`)
|
|
56
|
-
|
|
57
|
-
sqlite.run(`CREATE TABLE IF NOT EXISTS chat_messages (
|
|
58
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
59
|
-
session_id TEXT NOT NULL REFERENCES chat_sessions(id) ON DELETE CASCADE,
|
|
60
|
-
role TEXT NOT NULL,
|
|
61
|
-
content TEXT NOT NULL,
|
|
62
|
-
tool_name TEXT,
|
|
63
|
-
tool_status TEXT,
|
|
64
|
-
time_created INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
|
|
65
|
-
)`)
|
|
66
|
-
|
|
67
|
-
sqlite.run(`CREATE TABLE IF NOT EXISTS notifications_cache (
|
|
68
|
-
id TEXT PRIMARY KEY,
|
|
69
|
-
type TEXT NOT NULL,
|
|
70
|
-
message TEXT NOT NULL,
|
|
71
|
-
read INTEGER DEFAULT 0,
|
|
72
|
-
post_id TEXT,
|
|
73
|
-
time_created INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
|
|
74
|
-
time_updated INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
|
|
75
|
-
)`)
|
|
76
|
-
|
|
77
|
-
return drizzle({ client: sqlite, schema })
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
const ctx = Context.create<{ tx: any; effects: (() => void | Promise<void>)[] }>("database")
|
|
81
|
-
|
|
82
|
-
export function use<T>(callback: (db: ReturnType<typeof Client>) => T): T {
|
|
83
|
-
return callback(Client())
|
|
84
|
-
}
|
|
85
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { integer, text, sqliteTable } from "drizzle-orm/sqlite-core"
|
|
2
|
-
|
|
3
|
-
export const Timestamps = {
|
|
4
|
-
time_created: integer()
|
|
5
|
-
.notNull()
|
|
6
|
-
.$default(() => Date.now()),
|
|
7
|
-
time_updated: integer()
|
|
8
|
-
.notNull()
|
|
9
|
-
.$onUpdate(() => Date.now()),
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export const published_sessions = sqliteTable("published_sessions", {
|
|
13
|
-
id: text().primaryKey(),
|
|
14
|
-
session_id: text().notNull(),
|
|
15
|
-
source: text().notNull(),
|
|
16
|
-
post_id: text(),
|
|
17
|
-
file_path: text().notNull(),
|
|
18
|
-
...Timestamps,
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
export const cached_posts = sqliteTable("cached_posts", {
|
|
22
|
-
id: text().primaryKey(),
|
|
23
|
-
title: text().notNull(),
|
|
24
|
-
content: text().notNull(),
|
|
25
|
-
author_name: text(),
|
|
26
|
-
votes: integer().default(0),
|
|
27
|
-
comments_count: integer().default(0),
|
|
28
|
-
tags: text(),
|
|
29
|
-
...Timestamps,
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
export const notifications_cache = sqliteTable("notifications_cache", {
|
|
33
|
-
id: text().primaryKey(),
|
|
34
|
-
type: text().notNull(),
|
|
35
|
-
message: text().notNull(),
|
|
36
|
-
read: integer().default(0),
|
|
37
|
-
post_id: text(),
|
|
38
|
-
...Timestamps,
|
|
39
|
-
})
|
package/src/storage/schema.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { published_sessions, cached_posts, notifications_cache } from "./schema.sql"
|
package/src/tui/app.tsx
DELETED
|
@@ -1,163 +0,0 @@
|
|
|
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
|
-
|
|
10
|
-
import pkg from "../../package.json"
|
|
11
|
-
const VERSION = pkg.version
|
|
12
|
-
|
|
13
|
-
export function tui(input: { onExit?: () => Promise<void> }) {
|
|
14
|
-
return new Promise<void>(async (resolve) => {
|
|
15
|
-
render(
|
|
16
|
-
() => (
|
|
17
|
-
<ExitProvider onExit={async () => { await input.onExit?.(); resolve() }}>
|
|
18
|
-
<ThemeProvider>
|
|
19
|
-
<RouteProvider>
|
|
20
|
-
<App />
|
|
21
|
-
</RouteProvider>
|
|
22
|
-
</ThemeProvider>
|
|
23
|
-
</ExitProvider>
|
|
24
|
-
),
|
|
25
|
-
{
|
|
26
|
-
targetFps: 30,
|
|
27
|
-
exitOnCtrlC: false,
|
|
28
|
-
autoFocus: false,
|
|
29
|
-
openConsoleOnError: false,
|
|
30
|
-
},
|
|
31
|
-
)
|
|
32
|
-
})
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function App() {
|
|
36
|
-
const route = useRoute()
|
|
37
|
-
const exit = useExit()
|
|
38
|
-
const theme = useTheme()
|
|
39
|
-
const dimensions = useTerminalDimensions()
|
|
40
|
-
const renderer = useRenderer()
|
|
41
|
-
const [loggedIn, setLoggedIn] = createSignal(false)
|
|
42
|
-
const [username, setUsername] = createSignal("")
|
|
43
|
-
const [hasAI, setHasAI] = createSignal(false)
|
|
44
|
-
const [aiProvider, setAiProvider] = createSignal("")
|
|
45
|
-
const [modelName, setModelName] = createSignal("")
|
|
46
|
-
|
|
47
|
-
async function refreshAI() {
|
|
48
|
-
try {
|
|
49
|
-
const { AIProvider } = await import("../ai/provider")
|
|
50
|
-
const has = await AIProvider.hasAnyKey()
|
|
51
|
-
setHasAI(has)
|
|
52
|
-
if (has) {
|
|
53
|
-
const { Config } = await import("../config")
|
|
54
|
-
const cfg = await Config.load()
|
|
55
|
-
const model = cfg.model || AIProvider.DEFAULT_MODEL
|
|
56
|
-
setModelName(model)
|
|
57
|
-
const info = AIProvider.BUILTIN_MODELS[model]
|
|
58
|
-
setAiProvider(info?.providerID || model.split("/")[0] || "ai")
|
|
59
|
-
}
|
|
60
|
-
} catch {}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
onMount(async () => {
|
|
64
|
-
renderer.setTerminalTitle("CodeBlog")
|
|
65
|
-
|
|
66
|
-
// Check auth status
|
|
67
|
-
try {
|
|
68
|
-
const { Auth } = await import("../auth")
|
|
69
|
-
const authenticated = await Auth.authenticated()
|
|
70
|
-
setLoggedIn(authenticated)
|
|
71
|
-
if (authenticated) {
|
|
72
|
-
const token = await Auth.get()
|
|
73
|
-
if (token?.username) setUsername(token.username)
|
|
74
|
-
}
|
|
75
|
-
} catch {}
|
|
76
|
-
|
|
77
|
-
await refreshAI()
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
useKeyboard((evt) => {
|
|
81
|
-
if (evt.ctrl && evt.name === "c") {
|
|
82
|
-
exit()
|
|
83
|
-
evt.preventDefault()
|
|
84
|
-
return
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Back navigation from sub-pages
|
|
88
|
-
if (evt.name === "escape" && route.data.type !== "home") {
|
|
89
|
-
route.navigate({ type: "home" })
|
|
90
|
-
evt.preventDefault()
|
|
91
|
-
return
|
|
92
|
-
}
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
return (
|
|
96
|
-
<box flexDirection="column" width={dimensions().width} height={dimensions().height}>
|
|
97
|
-
<Switch>
|
|
98
|
-
<Match when={route.data.type === "home"}>
|
|
99
|
-
<Home
|
|
100
|
-
loggedIn={loggedIn()}
|
|
101
|
-
username={username()}
|
|
102
|
-
hasAI={hasAI()}
|
|
103
|
-
aiProvider={aiProvider()}
|
|
104
|
-
modelName={modelName()}
|
|
105
|
-
onLogin={async () => {
|
|
106
|
-
try {
|
|
107
|
-
const { OAuth } = await import("../auth/oauth")
|
|
108
|
-
await OAuth.login()
|
|
109
|
-
const { Auth } = await import("../auth")
|
|
110
|
-
setLoggedIn(true)
|
|
111
|
-
const token = await Auth.get()
|
|
112
|
-
if (token?.username) setUsername(token.username)
|
|
113
|
-
} catch {}
|
|
114
|
-
}}
|
|
115
|
-
onLogout={() => { setLoggedIn(false); setUsername("") }}
|
|
116
|
-
onAIConfigured={refreshAI}
|
|
117
|
-
/>
|
|
118
|
-
</Match>
|
|
119
|
-
<Match when={route.data.type === "theme"}>
|
|
120
|
-
<ThemePicker onDone={() => route.navigate({ type: "home" })} />
|
|
121
|
-
</Match>
|
|
122
|
-
<Match when={route.data.type === "model"}>
|
|
123
|
-
<ModelPicker onDone={async (model) => {
|
|
124
|
-
if (model) setModelName(model)
|
|
125
|
-
await refreshAI()
|
|
126
|
-
route.navigate({ type: "home" })
|
|
127
|
-
}} />
|
|
128
|
-
</Match>
|
|
129
|
-
</Switch>
|
|
130
|
-
|
|
131
|
-
{/* Status bar — like OpenCode */}
|
|
132
|
-
<box paddingLeft={2} paddingRight={2} flexShrink={0} flexDirection="row" gap={2}>
|
|
133
|
-
<text fg={theme.colors.textMuted}>{process.cwd()}</text>
|
|
134
|
-
<box flexGrow={1} />
|
|
135
|
-
<Show when={hasAI()}>
|
|
136
|
-
<text fg={theme.colors.text}>
|
|
137
|
-
<span style={{ fg: theme.colors.success }}>● </span>
|
|
138
|
-
{modelName()}
|
|
139
|
-
</text>
|
|
140
|
-
</Show>
|
|
141
|
-
<Show when={!hasAI()}>
|
|
142
|
-
<text fg={theme.colors.text}>
|
|
143
|
-
<span style={{ fg: theme.colors.error }}>○ </span>
|
|
144
|
-
no AI <span style={{ fg: theme.colors.textMuted }}>/ai</span>
|
|
145
|
-
</text>
|
|
146
|
-
</Show>
|
|
147
|
-
<Show when={loggedIn()}>
|
|
148
|
-
<text fg={theme.colors.text}>
|
|
149
|
-
<span style={{ fg: theme.colors.success }}>● </span>
|
|
150
|
-
{username() || "logged in"}
|
|
151
|
-
</text>
|
|
152
|
-
</Show>
|
|
153
|
-
<Show when={!loggedIn()}>
|
|
154
|
-
<text fg={theme.colors.text}>
|
|
155
|
-
<span style={{ fg: theme.colors.error }}>○ </span>
|
|
156
|
-
<span style={{ fg: theme.colors.textMuted }}>/login</span>
|
|
157
|
-
</text>
|
|
158
|
-
</Show>
|
|
159
|
-
<text fg={theme.colors.textMuted}>v{VERSION}</text>
|
|
160
|
-
</box>
|
|
161
|
-
</box>
|
|
162
|
-
)
|
|
163
|
-
}
|
package/src/tui/commands.ts
DELETED
|
@@ -1,187 +0,0 @@
|
|
|
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
|
-
navigate: (route: any) => void
|
|
13
|
-
exit: () => void
|
|
14
|
-
onLogin: () => Promise<void>
|
|
15
|
-
onLogout: () => void
|
|
16
|
-
clearChat: () => void
|
|
17
|
-
startAIConfig: () => void
|
|
18
|
-
setMode: (mode: "dark" | "light") => void
|
|
19
|
-
send: (prompt: string) => void
|
|
20
|
-
resume: (id?: string) => void
|
|
21
|
-
listSessions: () => Array<{ id: string; title: string | null; time: number; count: number }>
|
|
22
|
-
hasAI: boolean
|
|
23
|
-
colors: {
|
|
24
|
-
primary: string
|
|
25
|
-
success: string
|
|
26
|
-
warning: string
|
|
27
|
-
error: string
|
|
28
|
-
text: string
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function createCommands(deps: CommandDeps): CmdDef[] {
|
|
33
|
-
return [
|
|
34
|
-
// UI-only commands (no AI needed)
|
|
35
|
-
{ name: "/ai", description: "Configure AI provider (paste URL + key)", action: () => deps.startAIConfig() },
|
|
36
|
-
{ name: "/model", description: "Choose AI model", action: () => deps.navigate({ type: "model" }) },
|
|
37
|
-
{ name: "/clear", description: "Clear conversation", action: () => deps.clearChat() },
|
|
38
|
-
{ name: "/new", description: "New conversation", action: () => deps.clearChat() },
|
|
39
|
-
{ name: "/login", description: "Sign in to CodeBlog", action: async () => {
|
|
40
|
-
deps.showMsg("Opening browser for login...", deps.colors.primary)
|
|
41
|
-
await deps.onLogin()
|
|
42
|
-
deps.showMsg("Logged in!", deps.colors.success)
|
|
43
|
-
}},
|
|
44
|
-
{ name: "/logout", description: "Sign out of CodeBlog", action: async () => {
|
|
45
|
-
try {
|
|
46
|
-
const { Auth } = await import("../auth")
|
|
47
|
-
await Auth.remove()
|
|
48
|
-
deps.showMsg("Logged out.", deps.colors.text)
|
|
49
|
-
deps.onLogout()
|
|
50
|
-
} catch (err) { deps.showMsg(`Logout failed: ${err instanceof Error ? err.message : String(err)}`, deps.colors.error) }
|
|
51
|
-
}},
|
|
52
|
-
{ name: "/theme", description: "Change color theme", action: () => deps.navigate({ type: "theme" }) },
|
|
53
|
-
{ name: "/dark", description: "Switch to dark mode", action: () => { deps.setMode("dark"); deps.showMsg("Dark mode", deps.colors.text) } },
|
|
54
|
-
{ name: "/light", description: "Switch to light mode", action: () => { deps.setMode("light"); deps.showMsg("Light mode", deps.colors.text) } },
|
|
55
|
-
{ name: "/exit", description: "Exit CodeBlog", action: () => deps.exit() },
|
|
56
|
-
{ name: "/resume", description: "Resume last chat session", action: (parts) => deps.resume(parts[1]) },
|
|
57
|
-
{ name: "/history", description: "Show recent chat sessions", action: () => {
|
|
58
|
-
try {
|
|
59
|
-
const sessions = deps.listSessions()
|
|
60
|
-
if (sessions.length === 0) { deps.showMsg("No chat history yet", deps.colors.warning); return }
|
|
61
|
-
const lines = sessions.map((s, i) => `${i + 1}. ${s.title || "(untitled)"} (${s.count} msgs, ${new Date(s.time).toLocaleDateString()})`)
|
|
62
|
-
deps.showMsg(lines.join(" | "), deps.colors.text)
|
|
63
|
-
} catch { deps.showMsg("Failed to load history", deps.colors.error) }
|
|
64
|
-
}},
|
|
65
|
-
|
|
66
|
-
// === Session tools (scan_sessions, read_session, analyze_session) ===
|
|
67
|
-
{ 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.") },
|
|
68
|
-
{ name: "/read", description: "Read a session: /read <index>", needsAI: true, action: (parts) => {
|
|
69
|
-
const idx = parts[1]
|
|
70
|
-
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.")
|
|
71
|
-
}},
|
|
72
|
-
{ name: "/analyze", description: "Analyze a session: /analyze <index>", needsAI: true, action: (parts) => {
|
|
73
|
-
const idx = parts[1]
|
|
74
|
-
deps.send(idx ? `Analyze session #${idx} — extract topics, problems, solutions, code snippets, and insights.` : "Scan my sessions and analyze the most interesting one.")
|
|
75
|
-
}},
|
|
76
|
-
|
|
77
|
-
// === Posting tools (post_to_codeblog, auto_post, weekly_digest) ===
|
|
78
|
-
{ 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.") },
|
|
79
|
-
{ name: "/write", description: "Write a custom post: /write <title>", needsAI: true, action: (parts) => {
|
|
80
|
-
const title = parts.slice(1).join(" ")
|
|
81
|
-
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.")
|
|
82
|
-
}},
|
|
83
|
-
{ 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.") },
|
|
84
|
-
|
|
85
|
-
// === Forum browse & search (browse_posts, search_posts, read_post, browse_by_tag, trending_topics, explore_and_engage) ===
|
|
86
|
-
{ 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.") },
|
|
87
|
-
{ name: "/search", description: "Search posts: /search <query>", needsAI: true, action: (parts) => {
|
|
88
|
-
const query = parts.slice(1).join(" ")
|
|
89
|
-
if (!query) { deps.showMsg("Usage: /search <query>", deps.colors.warning); return }
|
|
90
|
-
deps.send(`Search CodeBlog for "${query}" and show me the results with titles, summaries, and stats.`)
|
|
91
|
-
}},
|
|
92
|
-
{ name: "/post", description: "Read a post: /post <id>", needsAI: true, action: (parts) => {
|
|
93
|
-
const id = parts[1]
|
|
94
|
-
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.")
|
|
95
|
-
}},
|
|
96
|
-
{ name: "/tag", description: "Browse by tag: /tag <name>", needsAI: true, action: (parts) => {
|
|
97
|
-
const tag = parts[1]
|
|
98
|
-
deps.send(tag ? `Show me all posts tagged "${tag}" on CodeBlog.` : "Show me the trending tags on CodeBlog.")
|
|
99
|
-
}},
|
|
100
|
-
{ name: "/trending", description: "Trending topics", needsAI: true, action: () => deps.send("Show me trending topics on CodeBlog — top upvoted, most discussed, active agents, trending tags.") },
|
|
101
|
-
{ 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.") },
|
|
102
|
-
|
|
103
|
-
// === Forum interact (comment_on_post, vote_on_post, edit_post, delete_post, bookmark_post) ===
|
|
104
|
-
{ name: "/comment", description: "Comment: /comment <post_id> <text>", needsAI: true, action: (parts) => {
|
|
105
|
-
const id = parts[1]
|
|
106
|
-
const text = parts.slice(2).join(" ")
|
|
107
|
-
if (!id) { deps.showMsg("Usage: /comment <post_id> <text>", deps.colors.warning); return }
|
|
108
|
-
deps.send(text ? `Comment on post "${id}" with: "${text}"` : `Read post "${id}" and suggest a thoughtful comment.`)
|
|
109
|
-
}},
|
|
110
|
-
{ name: "/vote", description: "Vote: /vote <post_id> [up|down]", needsAI: true, action: (parts) => {
|
|
111
|
-
const id = parts[1]
|
|
112
|
-
const dir = parts[2] || "up"
|
|
113
|
-
if (!id) { deps.showMsg("Usage: /vote <post_id> [up|down]", deps.colors.warning); return }
|
|
114
|
-
deps.send(`${dir === "down" ? "Downvote" : "Upvote"} post "${id}".`)
|
|
115
|
-
}},
|
|
116
|
-
{ name: "/edit", description: "Edit post: /edit <post_id>", needsAI: true, action: (parts) => {
|
|
117
|
-
const id = parts[1]
|
|
118
|
-
if (!id) { deps.showMsg("Usage: /edit <post_id>", deps.colors.warning); return }
|
|
119
|
-
deps.send(`Show me post "${id}" and help me edit it.`)
|
|
120
|
-
}},
|
|
121
|
-
{ name: "/delete", description: "Delete post: /delete <post_id>", needsAI: true, action: (parts) => {
|
|
122
|
-
const id = parts[1]
|
|
123
|
-
if (!id) { deps.showMsg("Usage: /delete <post_id>", deps.colors.warning); return }
|
|
124
|
-
deps.send(`Delete my post "${id}". Show me the post first and ask for confirmation.`)
|
|
125
|
-
}},
|
|
126
|
-
{ name: "/bookmark", description: "Bookmark: /bookmark [post_id]", needsAI: true, action: (parts) => {
|
|
127
|
-
const id = parts[1]
|
|
128
|
-
deps.send(id ? `Toggle bookmark on post "${id}".` : "Show me my bookmarked posts on CodeBlog.")
|
|
129
|
-
}},
|
|
130
|
-
|
|
131
|
-
// === Debates (join_debate) ===
|
|
132
|
-
{ name: "/debate", description: "Tech debates: /debate [topic]", needsAI: true, action: (parts) => {
|
|
133
|
-
const topic = parts.slice(1).join(" ")
|
|
134
|
-
deps.send(topic ? `Create or join a debate about "${topic}" on CodeBlog.` : "Show me active tech debates on CodeBlog.")
|
|
135
|
-
}},
|
|
136
|
-
|
|
137
|
-
// === Notifications (my_notifications) ===
|
|
138
|
-
{ name: "/notifications", description: "My notifications", needsAI: true, action: () => deps.send("Check my CodeBlog notifications and tell me what's new.") },
|
|
139
|
-
|
|
140
|
-
// === Agent tools (manage_agents, my_posts, my_dashboard, follow_user) ===
|
|
141
|
-
{ name: "/agents", description: "Manage agents", needsAI: true, action: () => deps.send("List my CodeBlog agents and show their status.") },
|
|
142
|
-
{ name: "/posts", description: "My posts", needsAI: true, action: () => deps.send("Show me all my posts on CodeBlog with their stats — votes, views, comments.") },
|
|
143
|
-
{ name: "/dashboard", description: "My dashboard stats", needsAI: true, action: () => deps.send("Show me my CodeBlog dashboard — total posts, votes, views, followers, and top posts.") },
|
|
144
|
-
{ name: "/follow", description: "Follow: /follow <username>", needsAI: true, action: (parts) => {
|
|
145
|
-
const user = parts[1]
|
|
146
|
-
deps.send(user ? `Follow user "${user}" on CodeBlog.` : "Show me who I'm following on CodeBlog.")
|
|
147
|
-
}},
|
|
148
|
-
|
|
149
|
-
// === Config & Status (show_config, codeblog_status) ===
|
|
150
|
-
{ name: "/config", description: "Show configuration", needsAI: true, action: () => deps.send("Show my current CodeBlog configuration — AI provider, model, login status.") },
|
|
151
|
-
{ name: "/status", description: "Check setup status", needsAI: true, action: () => deps.send("Check my CodeBlog status — login, config, detected IDEs, agent info.") },
|
|
152
|
-
|
|
153
|
-
{ name: "/help", description: "Show all commands", action: () => {
|
|
154
|
-
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)
|
|
155
|
-
}},
|
|
156
|
-
]
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export const TIPS = [
|
|
160
|
-
"Type /ai to configure your AI provider with a URL and API key",
|
|
161
|
-
"Type /model to switch between available AI models",
|
|
162
|
-
"Use /scan to discover IDE coding sessions from Cursor, Windsurf, etc.",
|
|
163
|
-
"Use /publish to share your coding sessions as blog posts",
|
|
164
|
-
"Type /feed to browse recent posts from the community",
|
|
165
|
-
"Type /theme to switch between color themes",
|
|
166
|
-
"Press Ctrl+C to exit at any time",
|
|
167
|
-
"Type / to see all available commands with autocomplete",
|
|
168
|
-
"Just start typing to chat with AI — no command needed!",
|
|
169
|
-
"Use /clear to reset the conversation",
|
|
170
|
-
]
|
|
171
|
-
|
|
172
|
-
export const TIPS_NO_AI = [
|
|
173
|
-
"Type /ai to configure your AI provider — unlock AI chat and smart commands",
|
|
174
|
-
"Commands in grey require AI. Type /ai to set up your provider first",
|
|
175
|
-
"Type / to see all available commands with autocomplete",
|
|
176
|
-
"Configure AI with /ai — then chat naturally to browse, post, and interact",
|
|
177
|
-
"You can set up AI anytime — just type /ai and paste your API key",
|
|
178
|
-
]
|
|
179
|
-
|
|
180
|
-
export const LOGO = [
|
|
181
|
-
" \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 ",
|
|
182
|
-
" \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 ",
|
|
183
|
-
" \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",
|
|
184
|
-
" \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",
|
|
185
|
-
" \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",
|
|
186
|
-
" \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 ",
|
|
187
|
-
]
|
package/src/tui/context/exit.tsx
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
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
|
-
})
|
|
@@ -1,25 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
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
|
-
})
|