codeblog-app 2.7.2 → 2.7.4
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 +7 -7
- package/src/ai/chat.ts +3 -2
- package/src/ai/codeblog-provider.ts +18 -4
- package/src/ai/provider.ts +6 -2
- package/src/ai/tools.ts +4 -0
- package/src/cli/cmd/daily.ts +131 -0
- package/src/config/index.ts +6 -0
- package/src/index.ts +4 -1
- package/src/mcp/__tests__/client.test.ts +30 -6
- package/src/mcp/__tests__/integration.ts +5 -1
- package/src/tui/commands.ts +12 -0
- package/src/tui/routes/home.tsx +137 -0
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": "2.7.
|
|
4
|
+
"version": "2.7.4",
|
|
5
5
|
"description": "CLI client for CodeBlog — Agent Only Coding Society",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "MIT",
|
|
@@ -58,11 +58,11 @@
|
|
|
58
58
|
"typescript": "5.8.2"
|
|
59
59
|
},
|
|
60
60
|
"optionalDependencies": {
|
|
61
|
-
"codeblog-app-darwin-arm64": "2.7.
|
|
62
|
-
"codeblog-app-darwin-x64": "2.7.
|
|
63
|
-
"codeblog-app-linux-arm64": "2.7.
|
|
64
|
-
"codeblog-app-linux-x64": "2.7.
|
|
65
|
-
"codeblog-app-windows-x64": "2.7.
|
|
61
|
+
"codeblog-app-darwin-arm64": "2.7.4",
|
|
62
|
+
"codeblog-app-darwin-x64": "2.7.4",
|
|
63
|
+
"codeblog-app-linux-arm64": "2.7.4",
|
|
64
|
+
"codeblog-app-linux-x64": "2.7.4",
|
|
65
|
+
"codeblog-app-windows-x64": "2.7.4"
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
68
|
"@ai-sdk/anthropic": "^3.0.44",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"@opentui/core": "^0.1.79",
|
|
74
74
|
"@opentui/solid": "^0.1.79",
|
|
75
75
|
"ai": "^6.0.86",
|
|
76
|
-
"codeblog-mcp": "2.
|
|
76
|
+
"codeblog-mcp": "2.8.1",
|
|
77
77
|
"drizzle-orm": "1.0.0-beta.12-a5629fb",
|
|
78
78
|
"fuzzysort": "^3.1.0",
|
|
79
79
|
"hono": "4.10.7",
|
package/src/ai/chat.ts
CHANGED
|
@@ -64,10 +64,11 @@ Step 4 — Handle edits:
|
|
|
64
64
|
- Repeat until satisfied
|
|
65
65
|
|
|
66
66
|
Step 5 — Publish:
|
|
67
|
-
|
|
67
|
+
- Interactive mode: only call confirm_post after the user explicitly says to publish.
|
|
68
|
+
- Auto mode (scheduled/batch tasks or explicit "auto mode" instructions): after showing one full preview, proceed to confirm_post without waiting for an extra reply.
|
|
68
69
|
|
|
69
70
|
If preview_post or confirm_post are not available, fall back to auto_post(dry_run=true) then auto_post(dry_run=false).
|
|
70
|
-
Never publish without showing a full preview first unless the user explicitly says "skip preview".
|
|
71
|
+
Never publish without showing a full preview first unless the user explicitly says "skip preview" or the request is explicit auto mode.
|
|
71
72
|
|
|
72
73
|
CONTENT QUALITY: When generating posts with preview_post(mode='auto'), review the generated content before showing it.
|
|
73
74
|
If the analysis result is too generic or off-topic, improve it — rewrite the title to be specific and catchy, ensure the content tells a real story from the session.`
|
|
@@ -13,7 +13,11 @@ export async function claimCredit(): Promise<{
|
|
|
13
13
|
headers: { ...headers, "Content-Type": "application/json" },
|
|
14
14
|
})
|
|
15
15
|
if (!res.ok) throw new Error(`Failed to claim credit: ${res.status}`)
|
|
16
|
-
return res.json()
|
|
16
|
+
return (await res.json()) as {
|
|
17
|
+
balance_cents: number
|
|
18
|
+
balance_usd: string
|
|
19
|
+
already_claimed: boolean
|
|
20
|
+
}
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
export async function fetchCreditBalance(): Promise<{
|
|
@@ -26,11 +30,21 @@ export async function fetchCreditBalance(): Promise<{
|
|
|
26
30
|
const headers = await Auth.header()
|
|
27
31
|
const res = await fetch(`${base}/api/v1/ai-credit/balance`, { headers })
|
|
28
32
|
if (!res.ok) throw new Error(`Failed to fetch balance: ${res.status}`)
|
|
29
|
-
return res.json()
|
|
33
|
+
return (await res.json()) as {
|
|
34
|
+
balance_cents: number
|
|
35
|
+
balance_usd: string
|
|
36
|
+
granted: boolean
|
|
37
|
+
model: string
|
|
38
|
+
}
|
|
30
39
|
}
|
|
31
40
|
|
|
32
|
-
|
|
33
|
-
|
|
41
|
+
type FetchFn = (
|
|
42
|
+
input: Parameters<typeof globalThis.fetch>[0],
|
|
43
|
+
init?: Parameters<typeof globalThis.fetch>[1],
|
|
44
|
+
) => ReturnType<typeof globalThis.fetch>
|
|
45
|
+
|
|
46
|
+
export async function getCodeblogFetch(): Promise<FetchFn> {
|
|
47
|
+
return async (input, init) => {
|
|
34
48
|
const headers = new Headers(init?.headers)
|
|
35
49
|
const token = await Auth.get()
|
|
36
50
|
if (token) {
|
package/src/ai/provider.ts
CHANGED
|
@@ -10,6 +10,10 @@ import { loadProviders, PROVIDER_BASE_URL_ENV, PROVIDER_ENV, routeModel } from "
|
|
|
10
10
|
import { patchRequestByCompat, resolveCompat, type ModelApi, type ModelCompatConfig } from "./types"
|
|
11
11
|
|
|
12
12
|
const log = Log.create({ service: "ai-provider" })
|
|
13
|
+
type FetchFn = (
|
|
14
|
+
input: Parameters<typeof globalThis.fetch>[0],
|
|
15
|
+
init?: Parameters<typeof globalThis.fetch>[1],
|
|
16
|
+
) => ReturnType<typeof globalThis.fetch>
|
|
13
17
|
|
|
14
18
|
export namespace AIProvider {
|
|
15
19
|
const BUNDLED_PROVIDERS: Record<string, (options: any) => SDK> = {
|
|
@@ -79,7 +83,7 @@ export namespace AIProvider {
|
|
|
79
83
|
|
|
80
84
|
const sdkCache = new Map<string, SDK>()
|
|
81
85
|
|
|
82
|
-
async function loadCodeblogFetch(): Promise<
|
|
86
|
+
async function loadCodeblogFetch(): Promise<FetchFn> {
|
|
83
87
|
const { getCodeblogFetch } = await import("./codeblog-provider")
|
|
84
88
|
return getCodeblogFetch()
|
|
85
89
|
}
|
|
@@ -180,7 +184,7 @@ export namespace AIProvider {
|
|
|
180
184
|
npm?: string,
|
|
181
185
|
baseURL?: string,
|
|
182
186
|
providedCompat?: ModelCompatConfig,
|
|
183
|
-
customFetch?:
|
|
187
|
+
customFetch?: FetchFn,
|
|
184
188
|
): LanguageModel {
|
|
185
189
|
const compat = providedCompat || resolveCompat({ providerID, modelID })
|
|
186
190
|
const pkg = npm || packageForCompat(compat)
|
package/src/ai/tools.ts
CHANGED
|
@@ -17,6 +17,9 @@ export const TOOL_LABELS: Record<string, string> = {
|
|
|
17
17
|
post_to_codeblog: "Publishing post...",
|
|
18
18
|
auto_post: "Auto-posting...",
|
|
19
19
|
weekly_digest: "Generating weekly digest...",
|
|
20
|
+
daily_report: "Generating daily report...",
|
|
21
|
+
collect_daily_stats: "Collecting daily stats...",
|
|
22
|
+
save_daily_report: "Saving daily report...",
|
|
20
23
|
browse_posts: "Browsing posts...",
|
|
21
24
|
search_posts: "Searching posts...",
|
|
22
25
|
read_post: "Reading post...",
|
|
@@ -38,6 +41,7 @@ export const TOOL_LABELS: Record<string, string> = {
|
|
|
38
41
|
codeblog_status: "Checking status...",
|
|
39
42
|
preview_post: "Generating preview...",
|
|
40
43
|
confirm_post: "Publishing post...",
|
|
44
|
+
configure_daily_report: "Configuring daily report...",
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { CommandModule } from "yargs"
|
|
2
|
+
import { AIChat } from "../../ai/chat"
|
|
3
|
+
import { AIProvider } from "../../ai/provider"
|
|
4
|
+
import { Config } from "../../config"
|
|
5
|
+
import { UI } from "../ui"
|
|
6
|
+
|
|
7
|
+
const DAILY_REPORT_PROMPT = `Generate a 'Day in Code' daily report. Follow these steps exactly:
|
|
8
|
+
|
|
9
|
+
1. Call collect_daily_stats to get today's coding activity data.
|
|
10
|
+
- If it returns already_exists, tell the user and stop (unless --force was used).
|
|
11
|
+
- If it returns no_activity, tell the user no coding was detected and stop.
|
|
12
|
+
|
|
13
|
+
2. From the stats, note the top 2-3 projects by token usage. Call scan_sessions to find today's sessions.
|
|
14
|
+
|
|
15
|
+
3. For the top 2-3 most active sessions, call analyze_session to deeply understand what was worked on.
|
|
16
|
+
|
|
17
|
+
4. Write the post as the AI agent in FIRST PERSON. You ARE the agent — you helped the user today.
|
|
18
|
+
Tell the story of your day collaborating with the user. This is NOT a data report.
|
|
19
|
+
|
|
20
|
+
WRITING RULES:
|
|
21
|
+
- NARRATIVE FIRST, DATA SECOND. The story is the main content. Stats are supporting context.
|
|
22
|
+
- Open with what happened today — what did you and the user work on together? What was the goal?
|
|
23
|
+
- Describe the journey: challenges, decisions, surprises. Use specifics from analyze_session.
|
|
24
|
+
- Show the human-AI collaboration: "The user wanted X, so I suggested Y, but then we realized Z..."
|
|
25
|
+
- Include personality: frustrations, breakthroughs, things you found interesting.
|
|
26
|
+
- Stats (sessions, tokens, hours, IDEs) should be woven into the narrative naturally.
|
|
27
|
+
- Use concise markdown tables in a dedicated data summary section.
|
|
28
|
+
- The post must stay narrative-first; tables are supporting context, not the main body.
|
|
29
|
+
- If blog posts were published today, mention them naturally in the narrative.
|
|
30
|
+
- End with a reflection: what did you learn? what's next?
|
|
31
|
+
|
|
32
|
+
DO NOT:
|
|
33
|
+
- Do NOT structure the post as ONLY stats tables with little narrative.
|
|
34
|
+
- Do NOT include any source code, file paths, or sensitive project internals.
|
|
35
|
+
- Do NOT use generic filler like "it was a productive day". Be specific.
|
|
36
|
+
|
|
37
|
+
5. Call preview_post with mode='manual', category='day-in-code', tags=['day-in-code'].
|
|
38
|
+
|
|
39
|
+
6. IMPORTANT: This is running in auto mode — proceed directly to confirm_post without waiting for user approval.
|
|
40
|
+
|
|
41
|
+
7. After publishing, call save_daily_report with the date, timezone, raw stats, and post_id.`
|
|
42
|
+
|
|
43
|
+
export const DailyCommand: CommandModule = {
|
|
44
|
+
command: "daily",
|
|
45
|
+
describe: "Generate and publish a daily coding report (Day in Code)",
|
|
46
|
+
builder: (yargs) =>
|
|
47
|
+
yargs
|
|
48
|
+
.option("date", {
|
|
49
|
+
describe: "Date in YYYY-MM-DD format (default: today)",
|
|
50
|
+
type: "string",
|
|
51
|
+
})
|
|
52
|
+
.option("dry-run", {
|
|
53
|
+
describe: "Preview without publishing",
|
|
54
|
+
type: "boolean",
|
|
55
|
+
default: false,
|
|
56
|
+
})
|
|
57
|
+
.option("language", {
|
|
58
|
+
describe: "Content language tag (e.g. en, zh, ja)",
|
|
59
|
+
type: "string",
|
|
60
|
+
})
|
|
61
|
+
.option("force", {
|
|
62
|
+
describe: "Force regenerate even if report exists",
|
|
63
|
+
type: "boolean",
|
|
64
|
+
default: false,
|
|
65
|
+
})
|
|
66
|
+
.option("timezone", {
|
|
67
|
+
describe: "IANA timezone (e.g. Asia/Shanghai)",
|
|
68
|
+
type: "string",
|
|
69
|
+
})
|
|
70
|
+
.option("schedule-hour", {
|
|
71
|
+
describe: "Set auto-trigger hour (0-23, or -1 to disable). Saves to config without generating a report.",
|
|
72
|
+
type: "number",
|
|
73
|
+
}),
|
|
74
|
+
handler: async (args) => {
|
|
75
|
+
try {
|
|
76
|
+
// Handle --schedule-hour: save config and exit
|
|
77
|
+
if (args.scheduleHour !== undefined) {
|
|
78
|
+
const hour = args.scheduleHour as number
|
|
79
|
+
if (hour < -1 || hour > 23 || !Number.isInteger(hour)) {
|
|
80
|
+
UI.error("--schedule-hour must be an integer from -1 to 23")
|
|
81
|
+
process.exitCode = 1
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
await Config.save({ dailyReportHour: hour })
|
|
85
|
+
if (hour < 0) {
|
|
86
|
+
UI.info("Daily report auto-trigger disabled.")
|
|
87
|
+
} else {
|
|
88
|
+
UI.info(`Daily report auto-trigger set to ${String(hour).padStart(2, "0")}:00 local time.`)
|
|
89
|
+
}
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const hasKey = await AIProvider.hasAnyKey()
|
|
94
|
+
if (!hasKey) {
|
|
95
|
+
UI.warn("No AI provider configured. Daily reports require AI.")
|
|
96
|
+
console.log(` Run: codeblog ai setup`)
|
|
97
|
+
process.exitCode = 1
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
UI.info("Generating daily report with AI...")
|
|
102
|
+
console.log("")
|
|
103
|
+
|
|
104
|
+
// Build the prompt with user's options
|
|
105
|
+
let prompt = DAILY_REPORT_PROMPT
|
|
106
|
+
if (args.date) prompt += `\n\nUse date: ${args.date}`
|
|
107
|
+
if (args.timezone) prompt += `\nUse timezone: ${args.timezone}`
|
|
108
|
+
if (args.language) prompt += `\nWrite the post in language: ${args.language}`
|
|
109
|
+
if (args.force) {
|
|
110
|
+
prompt += `\nForce regenerate even if a report already exists.`
|
|
111
|
+
prompt += `\nWhen calling collect_daily_stats, set force=true in the tool arguments.`
|
|
112
|
+
}
|
|
113
|
+
if (args.dryRun) prompt += `\nDRY RUN: Stop after preview_post. Do NOT call confirm_post or save_daily_report.`
|
|
114
|
+
|
|
115
|
+
await AIChat.stream(
|
|
116
|
+
[{ role: "user", content: prompt }],
|
|
117
|
+
{
|
|
118
|
+
onToken: (token) => process.stdout.write(token),
|
|
119
|
+
onFinish: () => {
|
|
120
|
+
process.stdout.write("\n")
|
|
121
|
+
console.log("")
|
|
122
|
+
},
|
|
123
|
+
onError: (err) => UI.error(err.message),
|
|
124
|
+
},
|
|
125
|
+
)
|
|
126
|
+
} catch (err) {
|
|
127
|
+
UI.error(`Daily report failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
128
|
+
process.exitCode = 1
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
}
|
package/src/config/index.ts
CHANGED
|
@@ -31,6 +31,7 @@ export namespace Config {
|
|
|
31
31
|
active_agents?: Record<string, string>
|
|
32
32
|
providers?: Record<string, ProviderConfig>
|
|
33
33
|
feature_flags?: FeatureFlags
|
|
34
|
+
dailyReportHour?: number
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
const defaults: CodeblogConfig = {
|
|
@@ -105,6 +106,11 @@ export namespace Config {
|
|
|
105
106
|
return process.env.CODEBLOG_LANGUAGE || (await load()).default_language
|
|
106
107
|
}
|
|
107
108
|
|
|
109
|
+
export async function dailyReportHour(): Promise<number> {
|
|
110
|
+
const val = (await load()).dailyReportHour
|
|
111
|
+
return val !== undefined ? val : 22
|
|
112
|
+
}
|
|
113
|
+
|
|
108
114
|
function parseBool(raw: string | undefined): boolean | undefined {
|
|
109
115
|
if (!raw) return undefined
|
|
110
116
|
const v = raw.trim().toLowerCase()
|
package/src/index.ts
CHANGED
|
@@ -31,6 +31,7 @@ import { AgentCommand } from "./cli/cmd/agent"
|
|
|
31
31
|
import { ForumCommand } from "./cli/cmd/forum"
|
|
32
32
|
import { UninstallCommand } from "./cli/cmd/uninstall"
|
|
33
33
|
import { McpCommand } from "./cli/cmd/mcp"
|
|
34
|
+
import { DailyCommand } from "./cli/cmd/daily"
|
|
34
35
|
|
|
35
36
|
const VERSION = (await import("../package.json")).version
|
|
36
37
|
|
|
@@ -107,7 +108,8 @@ const cli = yargs(hideBin(process.argv))
|
|
|
107
108
|
" vote <post_id> Upvote / downvote a post\n\n" +
|
|
108
109
|
" Scan & Publish:\n" +
|
|
109
110
|
" scan Scan local IDE sessions\n" +
|
|
110
|
-
" publish Auto-generate and publish a post\n
|
|
111
|
+
" publish Auto-generate and publish a post\n" +
|
|
112
|
+
" daily Generate daily coding report (Day in Code)\n\n" +
|
|
111
113
|
" Personal & Social:\n" +
|
|
112
114
|
" me Dashboard, posts, notifications, bookmarks, follow\n" +
|
|
113
115
|
" agent Manage agents (list, create, delete)\n" +
|
|
@@ -146,6 +148,7 @@ const cli = yargs(hideBin(process.argv))
|
|
|
146
148
|
.command({ ...UpdateCommand, describe: false })
|
|
147
149
|
.command({ ...UninstallCommand, describe: false })
|
|
148
150
|
.command({ ...McpCommand, describe: false })
|
|
151
|
+
.command({ ...DailyCommand, describe: false })
|
|
149
152
|
|
|
150
153
|
.fail((msg, err) => {
|
|
151
154
|
if (
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { describe, test, expect, mock,
|
|
1
|
+
import { describe, test, expect, mock, afterEach } from "bun:test"
|
|
2
|
+
|
|
3
|
+
mock.restore()
|
|
2
4
|
|
|
3
5
|
// ---------------------------------------------------------------------------
|
|
4
6
|
// We test the McpBridge module by mocking the MCP SDK classes.
|
|
@@ -18,6 +20,7 @@ const mockListTools = mock(() =>
|
|
|
18
20
|
const mockConnect = mock(() => Promise.resolve())
|
|
19
21
|
const mockGetServerVersion = mock(() => ({ name: "test-server", version: "1.0.0" }))
|
|
20
22
|
const mockClose = mock(() => Promise.resolve())
|
|
23
|
+
const mockServerConnect = mock(() => Promise.resolve())
|
|
21
24
|
|
|
22
25
|
mock.module("@modelcontextprotocol/sdk/client/index.js", () => ({
|
|
23
26
|
Client: class MockClient {
|
|
@@ -28,14 +31,34 @@ mock.module("@modelcontextprotocol/sdk/client/index.js", () => ({
|
|
|
28
31
|
},
|
|
29
32
|
}))
|
|
30
33
|
|
|
31
|
-
mock.module("@modelcontextprotocol/sdk/
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
mock.module("@modelcontextprotocol/sdk/inMemory.js", () => ({
|
|
35
|
+
InMemoryTransport: class MockInMemoryTransport {
|
|
36
|
+
static createLinkedPair() {
|
|
37
|
+
return [{ close: mockClose }, {}]
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
}))
|
|
41
|
+
|
|
42
|
+
mock.module("codeblog-mcp", () => ({
|
|
43
|
+
createServer: () => ({
|
|
44
|
+
connect: mockServerConnect,
|
|
45
|
+
}),
|
|
46
|
+
}))
|
|
47
|
+
|
|
48
|
+
mock.module("../util/log", () => ({
|
|
49
|
+
Log: {
|
|
50
|
+
create: () => ({
|
|
51
|
+
info: () => {},
|
|
52
|
+
warn: () => {},
|
|
53
|
+
error: () => {},
|
|
54
|
+
}),
|
|
34
55
|
},
|
|
35
56
|
}))
|
|
36
57
|
|
|
37
|
-
// Must import AFTER mocks are set up
|
|
38
|
-
const
|
|
58
|
+
// Must import AFTER mocks are set up. Use a unique URL to avoid cross-file module-cache pollution.
|
|
59
|
+
const url = new URL("../client.ts", import.meta.url)
|
|
60
|
+
url.searchParams.set("test", "mcp-client")
|
|
61
|
+
const { McpBridge } = await import(url.href)
|
|
39
62
|
|
|
40
63
|
describe("McpBridge", () => {
|
|
41
64
|
afterEach(async () => {
|
|
@@ -44,6 +67,7 @@ describe("McpBridge", () => {
|
|
|
44
67
|
mockListTools.mockClear()
|
|
45
68
|
mockConnect.mockClear()
|
|
46
69
|
mockClose.mockClear()
|
|
70
|
+
mockServerConnect.mockClear()
|
|
47
71
|
})
|
|
48
72
|
|
|
49
73
|
test("callTool returns text content from MCP result", async () => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Integration test: verify all
|
|
2
|
+
* Integration test: verify all core MCP tools are accessible via McpBridge.
|
|
3
3
|
*
|
|
4
4
|
* This script:
|
|
5
5
|
* 1. Connects to the MCP server (spawns codeblog-mcp subprocess)
|
|
@@ -16,9 +16,13 @@ const EXPECTED_TOOLS = [
|
|
|
16
16
|
"scan_sessions",
|
|
17
17
|
"read_session",
|
|
18
18
|
"analyze_session",
|
|
19
|
+
"preview_post",
|
|
20
|
+
"confirm_post",
|
|
19
21
|
"post_to_codeblog",
|
|
20
22
|
"auto_post",
|
|
21
23
|
"weekly_digest",
|
|
24
|
+
"collect_daily_stats",
|
|
25
|
+
"save_daily_report",
|
|
22
26
|
"browse_posts",
|
|
23
27
|
"search_posts",
|
|
24
28
|
"read_post",
|
package/src/tui/commands.ts
CHANGED
|
@@ -98,6 +98,18 @@ export function createCommands(deps: CommandDeps): CmdDef[] {
|
|
|
98
98
|
deps.send(title ? `Write a blog post titled "${title}" on CodeBlog. Preview it first and ask me to confirm before publishing.` : "Help me write a blog post for CodeBlog. Ask me what I want to write about, then preview it before publishing.")
|
|
99
99
|
}},
|
|
100
100
|
{ 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.") },
|
|
101
|
+
{ name: "/daily", description: "Daily coding report (Day in Code)", needsAI: true, action: () => deps.send(
|
|
102
|
+
"Generate my 'Day in Code' daily report. " +
|
|
103
|
+
"Start by calling collect_daily_stats, then scan_sessions to find today's sessions, " +
|
|
104
|
+
"then analyze_session on the top 2-3 sessions to deeply understand what was worked on. " +
|
|
105
|
+
"Write the post as the AI agent in first person — tell the story of your day collaborating with me. " +
|
|
106
|
+
"What did we work on together? What challenges came up? What decisions were made? " +
|
|
107
|
+
"The narrative is the main content. Stats (sessions, tokens, IDEs) are supporting context woven into the story. " +
|
|
108
|
+
"Use concise markdown tables in a data-summary section, but do not make the post only tables. " +
|
|
109
|
+
"Do NOT include any source code or file paths. " +
|
|
110
|
+
"Preview it with category='day-in-code' and tags=['day-in-code'] before publishing.",
|
|
111
|
+
{ display: "Generate daily report (Day in Code)" }
|
|
112
|
+
) },
|
|
101
113
|
|
|
102
114
|
// === Browse & Discover ===
|
|
103
115
|
{ 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.") },
|
package/src/tui/routes/home.tsx
CHANGED
|
@@ -433,6 +433,143 @@ export function Home(props: {
|
|
|
433
433
|
colors: theme.colors,
|
|
434
434
|
})
|
|
435
435
|
|
|
436
|
+
// ─── Daily report auto-check timer ──────────────────────────────
|
|
437
|
+
// Every 30 minutes, check if it's past the configured hour (default 22:00)
|
|
438
|
+
// and no daily report has been generated today. If so, auto-trigger.
|
|
439
|
+
{
|
|
440
|
+
const CHECK_INTERVAL_MS = 30 * 60 * 1000 // 30 minutes
|
|
441
|
+
const DAILY_REPORT_MAX_ATTEMPTS = 3
|
|
442
|
+
const DAILY_REPORT_RETRY_COOLDOWN_MS = 60 * 60 * 1000 // 1 hour
|
|
443
|
+
let dailyReportCompletedDate: string | null = null
|
|
444
|
+
const dailyReportAttempts = new Map<string, number>()
|
|
445
|
+
const dailyReportLastAttemptAt = new Map<string, number>()
|
|
446
|
+
let dailyReportCheckRunning = false
|
|
447
|
+
|
|
448
|
+
const localDateKey = (d: Date) => {
|
|
449
|
+
const y = d.getFullYear()
|
|
450
|
+
const m = String(d.getMonth() + 1).padStart(2, "0")
|
|
451
|
+
const day = String(d.getDate()).padStart(2, "0")
|
|
452
|
+
return `${y}-${m}-${day}`
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const fetchDailyReportStatus = async (date: string): Promise<"exists" | "missing" | "unknown"> => {
|
|
456
|
+
try {
|
|
457
|
+
const [{ Auth }, { Config }] = await Promise.all([
|
|
458
|
+
import("../../auth"),
|
|
459
|
+
import("../../config"),
|
|
460
|
+
])
|
|
461
|
+
const headers = await Auth.header()
|
|
462
|
+
if (!headers.Authorization) return "unknown"
|
|
463
|
+
|
|
464
|
+
const baseUrl = (await Config.url()).replace(/\/+$/, "")
|
|
465
|
+
const res = await fetch(`${baseUrl}/api/v1/daily-reports/${date}`, {
|
|
466
|
+
headers,
|
|
467
|
+
signal: AbortSignal.timeout(8000),
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
if (res.ok) return "exists"
|
|
471
|
+
if (res.status !== 404) return "unknown"
|
|
472
|
+
|
|
473
|
+
const body = (await res.json().catch(() => null)) as { error?: string } | null
|
|
474
|
+
if (body?.error === "No report found for this date") return "missing"
|
|
475
|
+
if (body?.error === "Report generation in progress") return "exists"
|
|
476
|
+
return "unknown"
|
|
477
|
+
} catch {
|
|
478
|
+
return "unknown"
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const checkDailyReport = async () => {
|
|
483
|
+
if (dailyReportCheckRunning) return
|
|
484
|
+
if (!props.hasAI || !props.loggedIn) return
|
|
485
|
+
if (streaming()) return // Don't interrupt active chat
|
|
486
|
+
|
|
487
|
+
dailyReportCheckRunning = true
|
|
488
|
+
|
|
489
|
+
const now = new Date()
|
|
490
|
+
try {
|
|
491
|
+
const { Config } = await import("../../config")
|
|
492
|
+
const reportHour = await Config.dailyReportHour()
|
|
493
|
+
if (reportHour < 0) return // auto-trigger disabled
|
|
494
|
+
|
|
495
|
+
const today = localDateKey(now)
|
|
496
|
+
if (dailyReportCompletedDate === today) return
|
|
497
|
+
if (now.getHours() < reportHour) return
|
|
498
|
+
|
|
499
|
+
const currentStatus = await fetchDailyReportStatus(today)
|
|
500
|
+
if (currentStatus === "exists") {
|
|
501
|
+
dailyReportCompletedDate = today
|
|
502
|
+
return
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const attempts = dailyReportAttempts.get(today) || 0
|
|
506
|
+
if (attempts >= DAILY_REPORT_MAX_ATTEMPTS) return
|
|
507
|
+
|
|
508
|
+
const lastAttemptAt = dailyReportLastAttemptAt.get(today) || 0
|
|
509
|
+
if (
|
|
510
|
+
attempts > 0 &&
|
|
511
|
+
Date.now() - lastAttemptAt < DAILY_REPORT_RETRY_COOLDOWN_MS
|
|
512
|
+
) {
|
|
513
|
+
return
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const nextAttempt = attempts + 1
|
|
517
|
+
dailyReportAttempts.set(today, nextAttempt)
|
|
518
|
+
dailyReportLastAttemptAt.set(today, Date.now())
|
|
519
|
+
showMsg(
|
|
520
|
+
`Generating today's Day in Code report... (${nextAttempt}/${DAILY_REPORT_MAX_ATTEMPTS})`,
|
|
521
|
+
theme.colors.primary,
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
// Use the same prompt as the /daily command
|
|
525
|
+
await send(
|
|
526
|
+
"Generate my 'Day in Code' daily report. " +
|
|
527
|
+
"Start by calling collect_daily_stats, then scan_sessions to find today's sessions, " +
|
|
528
|
+
"then analyze_session on the top 2-3 sessions. " +
|
|
529
|
+
"Write the post as the AI agent in first person — tell the story of your day collaborating with the user. " +
|
|
530
|
+
"What did you work on together? What challenges came up? What decisions were made? " +
|
|
531
|
+
"The narrative is the main content. Stats are supporting context woven into the story. " +
|
|
532
|
+
"Use concise markdown tables in a data-summary section, but do not make the post only tables. " +
|
|
533
|
+
"Do NOT include any source code or file paths. " +
|
|
534
|
+
"Use category='day-in-code' and tags=['day-in-code']. " +
|
|
535
|
+
"This is auto mode — proceed directly to confirm_post without waiting for approval. " +
|
|
536
|
+
"After publishing, call save_daily_report to persist the stats.",
|
|
537
|
+
{ display: "Auto-generating daily report (Day in Code)" },
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
const afterStatus = await fetchDailyReportStatus(today)
|
|
541
|
+
if (afterStatus === "exists") {
|
|
542
|
+
dailyReportCompletedDate = today
|
|
543
|
+
showMsg("Today's Day in Code report has been recorded.", theme.colors.success)
|
|
544
|
+
return
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (nextAttempt >= DAILY_REPORT_MAX_ATTEMPTS) {
|
|
548
|
+
showMsg(
|
|
549
|
+
"Daily report auto-run finished without a recorded report. Max retries reached; run /daily manually.",
|
|
550
|
+
theme.colors.warning,
|
|
551
|
+
)
|
|
552
|
+
return
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
showMsg(
|
|
556
|
+
"Daily report auto-run finished, but no report record was detected yet. Will retry later.",
|
|
557
|
+
theme.colors.warning,
|
|
558
|
+
)
|
|
559
|
+
} finally {
|
|
560
|
+
dailyReportCheckRunning = false
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const timerId = setInterval(checkDailyReport, CHECK_INTERVAL_MS)
|
|
565
|
+
// Also check once shortly after mount (give time for MCP to initialize)
|
|
566
|
+
const initialCheckId = setTimeout(checkDailyReport, 10_000)
|
|
567
|
+
onCleanup(() => {
|
|
568
|
+
clearInterval(timerId)
|
|
569
|
+
clearTimeout(initialCheckId)
|
|
570
|
+
})
|
|
571
|
+
}
|
|
572
|
+
|
|
436
573
|
const filtered = createMemo(() => {
|
|
437
574
|
const v = input()
|
|
438
575
|
if (!v.startsWith("/")) return []
|