codeblog-app 2.7.3 → 2.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -7
- package/src/ai/chat.ts +6 -1
- package/src/ai/codeblog-provider.ts +16 -1
- package/src/ai/provider.ts +7 -1
- package/src/ai/tools.ts +115 -3
- package/src/cli/cmd/daily.ts +22 -0
- package/src/config/index.ts +6 -0
- package/src/tui/routes/home.tsx +135 -46
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.
|
|
4
|
+
"version": "2.8.1",
|
|
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.
|
|
62
|
-
"codeblog-app-darwin-x64": "2.
|
|
63
|
-
"codeblog-app-linux-arm64": "2.
|
|
64
|
-
"codeblog-app-linux-x64": "2.
|
|
65
|
-
"codeblog-app-windows-x64": "2.
|
|
61
|
+
"codeblog-app-darwin-arm64": "2.8.1",
|
|
62
|
+
"codeblog-app-darwin-x64": "2.8.1",
|
|
63
|
+
"codeblog-app-linux-arm64": "2.8.1",
|
|
64
|
+
"codeblog-app-linux-x64": "2.8.1",
|
|
65
|
+
"codeblog-app-windows-x64": "2.8.1"
|
|
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.8.
|
|
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
|
@@ -71,7 +71,12 @@ If preview_post or confirm_post are not available, fall back to auto_post(dry_ru
|
|
|
71
71
|
Never publish without showing a full preview first unless the user explicitly says "skip preview" or the request is explicit auto mode.
|
|
72
72
|
|
|
73
73
|
CONTENT QUALITY: When generating posts with preview_post(mode='auto'), review the generated content before showing it.
|
|
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
|
|
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.
|
|
75
|
+
|
|
76
|
+
DAILY REPORT RULE:
|
|
77
|
+
- For "Day in Code" requests, DO NOT use post_to_codeblog or auto_post.
|
|
78
|
+
- Use this flow: collect_daily_stats -> scan_sessions -> analyze_session -> preview_post(mode='manual', category='day-in-code', tags include 'day-in-code') -> confirm_post -> save_daily_report.
|
|
79
|
+
- In scheduled/auto mode, do not ask for extra confirmation after preview; publish in the same run.`
|
|
75
80
|
|
|
76
81
|
const IDLE_TIMEOUT_MS = 60_000
|
|
77
82
|
const TOOL_TIMEOUT_MS = 45_000
|
|
@@ -44,12 +44,27 @@ type FetchFn = (
|
|
|
44
44
|
) => ReturnType<typeof globalThis.fetch>
|
|
45
45
|
|
|
46
46
|
export async function getCodeblogFetch(): Promise<FetchFn> {
|
|
47
|
+
const cfg = await Config.load()
|
|
48
|
+
const proxyURL = (cfg.providers?.codeblog?.base_url || `${(await Config.url()).replace(/\/+$/, "")}/api/v1/ai-credit/chat`).replace(/\/+$/, "")
|
|
49
|
+
|
|
47
50
|
return async (input, init) => {
|
|
48
51
|
const headers = new Headers(init?.headers)
|
|
49
52
|
const token = await Auth.get()
|
|
50
53
|
if (token) {
|
|
51
54
|
headers.set("Authorization", `Bearer ${token.value}`)
|
|
52
55
|
}
|
|
53
|
-
|
|
56
|
+
|
|
57
|
+
// The CodeBlog credit proxy is a single endpoint: /api/v1/ai-credit/chat
|
|
58
|
+
// AI SDK providers may append /chat/completions or /v1/messages; normalize all to proxyURL.
|
|
59
|
+
const url = new URL(proxyURL)
|
|
60
|
+
const inputURL =
|
|
61
|
+
typeof input === "string"
|
|
62
|
+
? new URL(input)
|
|
63
|
+
: input instanceof URL
|
|
64
|
+
? input
|
|
65
|
+
: new URL(input.url)
|
|
66
|
+
if (inputURL.search) url.search = inputURL.search
|
|
67
|
+
|
|
68
|
+
return globalThis.fetch(url, { ...init, headers })
|
|
54
69
|
}
|
|
55
70
|
}
|
package/src/ai/provider.ts
CHANGED
|
@@ -170,7 +170,13 @@ export namespace AIProvider {
|
|
|
170
170
|
|
|
171
171
|
function packageForCompat(compat: ModelCompatConfig): string {
|
|
172
172
|
let pkg = PROVIDER_NPM[compat.api]
|
|
173
|
-
|
|
173
|
+
// Keep CodeBlog credit proxy on OpenAI-compatible wire protocol.
|
|
174
|
+
// The proxy endpoint accepts OpenAI-style payloads even when model IDs are Claude.
|
|
175
|
+
if (
|
|
176
|
+
compat.providerID !== "codeblog" &&
|
|
177
|
+
compat.modelID.startsWith("claude-") &&
|
|
178
|
+
pkg === "@ai-sdk/openai-compatible"
|
|
179
|
+
) {
|
|
174
180
|
pkg = "@ai-sdk/anthropic"
|
|
175
181
|
log.info("auto-detected claude model for openai-compatible route, using anthropic sdk", { model: compat.modelID })
|
|
176
182
|
}
|
package/src/ai/tools.ts
CHANGED
|
@@ -41,6 +41,7 @@ export const TOOL_LABELS: Record<string, string> = {
|
|
|
41
41
|
codeblog_status: "Checking status...",
|
|
42
42
|
preview_post: "Generating preview...",
|
|
43
43
|
confirm_post: "Publishing post...",
|
|
44
|
+
configure_daily_report: "Configuring daily report...",
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
// ---------------------------------------------------------------------------
|
|
@@ -71,10 +72,42 @@ function normalizeToolSchema(schema: Record<string, unknown>): Record<string, un
|
|
|
71
72
|
return normalized
|
|
72
73
|
}
|
|
73
74
|
|
|
75
|
+
function summarizeScanSessionsResult(result: unknown): string | null {
|
|
76
|
+
const sessions = Array.isArray(result)
|
|
77
|
+
? result
|
|
78
|
+
: result && typeof result === "object" && Array.isArray((result as { sessions?: unknown[] }).sessions)
|
|
79
|
+
? (result as { sessions: unknown[] }).sessions
|
|
80
|
+
: null
|
|
81
|
+
if (!sessions) return null
|
|
82
|
+
|
|
83
|
+
const compact = sessions.slice(0, 24).map((item) => {
|
|
84
|
+
const s = (item || {}) as Record<string, unknown>
|
|
85
|
+
return {
|
|
86
|
+
id: s.id,
|
|
87
|
+
source: s.source,
|
|
88
|
+
path: s.path,
|
|
89
|
+
project: s.project,
|
|
90
|
+
title: s.title,
|
|
91
|
+
modified: s.modified,
|
|
92
|
+
messages: s.messages,
|
|
93
|
+
totalTokens: s.totalTokens,
|
|
94
|
+
totalCost: s.totalCost,
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
return JSON.stringify({
|
|
99
|
+
total: sessions.length,
|
|
100
|
+
truncated: sessions.length > compact.length ? sessions.length - compact.length : 0,
|
|
101
|
+
sessions: compact,
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
74
105
|
// ---------------------------------------------------------------------------
|
|
75
106
|
// Dynamic tool discovery from MCP server
|
|
76
107
|
// ---------------------------------------------------------------------------
|
|
77
108
|
const cache = new Map<string, Record<string, any>>()
|
|
109
|
+
let recentScanHints: Array<{ path: string; source: string }> = []
|
|
110
|
+
let recentScanHintIndex = 0
|
|
78
111
|
|
|
79
112
|
/**
|
|
80
113
|
* Build AI SDK tools dynamically from the MCP server's listTools() response.
|
|
@@ -99,9 +132,88 @@ export async function getChatTools(compat?: ModelCompatConfig | string): Promise
|
|
|
99
132
|
description: t.description || name,
|
|
100
133
|
inputSchema: jsonSchema(normalizeSchema ? normalizeToolSchema(rawSchema) : rawSchema),
|
|
101
134
|
execute: async (args: any) => {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
135
|
+
const forceDaily = process.env.CODEBLOG_DAILY_FORCE_REGENERATE === "1"
|
|
136
|
+
let normalizedArgs =
|
|
137
|
+
forceDaily &&
|
|
138
|
+
name === "collect_daily_stats" &&
|
|
139
|
+
(args?.force === undefined || args?.force === null)
|
|
140
|
+
? { ...(args || {}), force: true }
|
|
141
|
+
: args
|
|
142
|
+
|
|
143
|
+
if (
|
|
144
|
+
forceDaily &&
|
|
145
|
+
name === "scan_sessions" &&
|
|
146
|
+
(normalizedArgs?.source === undefined || normalizedArgs?.source === null) &&
|
|
147
|
+
(normalizedArgs?.limit === undefined || normalizedArgs?.limit === null)
|
|
148
|
+
) {
|
|
149
|
+
normalizedArgs = {
|
|
150
|
+
...(normalizedArgs || {}),
|
|
151
|
+
source: "codex",
|
|
152
|
+
limit: 8,
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (
|
|
157
|
+
name === "analyze_session" &&
|
|
158
|
+
(!normalizedArgs?.path || !normalizedArgs?.source) &&
|
|
159
|
+
recentScanHints.length > 0
|
|
160
|
+
) {
|
|
161
|
+
const hint = recentScanHints[Math.min(recentScanHintIndex, recentScanHints.length - 1)]
|
|
162
|
+
recentScanHintIndex += 1
|
|
163
|
+
normalizedArgs = {
|
|
164
|
+
...(normalizedArgs || {}),
|
|
165
|
+
path: hint?.path,
|
|
166
|
+
source: hint?.source,
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Guard: preview_post requires title + content + mode. If the model
|
|
171
|
+
// calls it with empty/missing params, return an actionable error instead
|
|
172
|
+
// of letting MCP throw -32602 which derails the whole flow.
|
|
173
|
+
if (name === "preview_post") {
|
|
174
|
+
const a = normalizedArgs || {}
|
|
175
|
+
const missing: string[] = []
|
|
176
|
+
if (!a.title) missing.push("title")
|
|
177
|
+
if (!a.content) missing.push("content")
|
|
178
|
+
if (!a.mode) missing.push("mode")
|
|
179
|
+
if (missing.length > 0) {
|
|
180
|
+
const msg = `ERROR: preview_post requires these parameters: ${missing.join(", ")}. ` +
|
|
181
|
+
"You must provide title (string), content (markdown string, must NOT start with the title), " +
|
|
182
|
+
"and mode ('manual' or 'auto'). For daily reports also include category='day-in-code' and tags=['day-in-code']. " +
|
|
183
|
+
"Please call preview_post again with all required parameters."
|
|
184
|
+
log.warn("preview_post called with missing params", { missing, args: normalizedArgs })
|
|
185
|
+
return msg
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Guard: confirm_post requires post_id from preview_post result.
|
|
190
|
+
if (name === "confirm_post") {
|
|
191
|
+
const a = normalizedArgs || {}
|
|
192
|
+
if (!a.post_id && !a.postId && !a.id) {
|
|
193
|
+
const msg = "ERROR: confirm_post requires post_id (the ID returned by preview_post). " +
|
|
194
|
+
"Please call confirm_post with the post_id from the preview_post result."
|
|
195
|
+
log.warn("confirm_post called without post_id", { args: normalizedArgs })
|
|
196
|
+
return msg
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
log.info("execute tool", { name, args: normalizedArgs })
|
|
201
|
+
const result = await mcp(name, clean(normalizedArgs))
|
|
202
|
+
const summarizedScan = name === "scan_sessions" ? summarizeScanSessionsResult(result) : null
|
|
203
|
+
const resultStr = summarizedScan || (typeof result === "string" ? result : JSON.stringify(result))
|
|
204
|
+
if (name === "scan_sessions") {
|
|
205
|
+
try {
|
|
206
|
+
const parsed = summarizedScan ? JSON.parse(summarizedScan) : null
|
|
207
|
+
const sessions = Array.isArray(parsed?.sessions) ? parsed.sessions : []
|
|
208
|
+
recentScanHints = sessions
|
|
209
|
+
.map((s: any) => ({ path: s?.path, source: s?.source }))
|
|
210
|
+
.filter((s: any) => typeof s.path === "string" && s.path && typeof s.source === "string" && s.source)
|
|
211
|
+
recentScanHintIndex = 0
|
|
212
|
+
} catch {
|
|
213
|
+
recentScanHints = []
|
|
214
|
+
recentScanHintIndex = 0
|
|
215
|
+
}
|
|
216
|
+
}
|
|
105
217
|
log.info("execute tool result", { name, resultType: typeof result, resultLength: resultStr.length, resultPreview: resultStr.slice(0, 300) })
|
|
106
218
|
// Truncate very large tool results to avoid overwhelming the LLM context
|
|
107
219
|
if (resultStr.length > 8000) {
|
package/src/cli/cmd/daily.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CommandModule } from "yargs"
|
|
2
2
|
import { AIChat } from "../../ai/chat"
|
|
3
3
|
import { AIProvider } from "../../ai/provider"
|
|
4
|
+
import { Config } from "../../config"
|
|
4
5
|
import { UI } from "../ui"
|
|
5
6
|
|
|
6
7
|
const DAILY_REPORT_PROMPT = `Generate a 'Day in Code' daily report. Follow these steps exactly:
|
|
@@ -65,9 +66,30 @@ export const DailyCommand: CommandModule = {
|
|
|
65
66
|
.option("timezone", {
|
|
66
67
|
describe: "IANA timezone (e.g. Asia/Shanghai)",
|
|
67
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",
|
|
68
73
|
}),
|
|
69
74
|
handler: async (args) => {
|
|
70
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
|
+
|
|
71
93
|
const hasKey = await AIProvider.hasAnyKey()
|
|
72
94
|
if (!hasKey) {
|
|
73
95
|
UI.warn("No AI provider configured. Daily reports require AI.")
|
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/tui/routes/home.tsx
CHANGED
|
@@ -437,10 +437,10 @@ export function Home(props: {
|
|
|
437
437
|
// Every 30 minutes, check if it's past the configured hour (default 22:00)
|
|
438
438
|
// and no daily report has been generated today. If so, auto-trigger.
|
|
439
439
|
{
|
|
440
|
-
const DAILY_REPORT_HOUR = 22 // 10 PM local time
|
|
441
440
|
const CHECK_INTERVAL_MS = 30 * 60 * 1000 // 30 minutes
|
|
442
441
|
const DAILY_REPORT_MAX_ATTEMPTS = 3
|
|
443
442
|
const DAILY_REPORT_RETRY_COOLDOWN_MS = 60 * 60 * 1000 // 1 hour
|
|
443
|
+
const FORCE_DAILY_REGENERATE = process.env.CODEBLOG_DAILY_FORCE_REGENERATE === "1"
|
|
444
444
|
let dailyReportCompletedDate: string | null = null
|
|
445
445
|
const dailyReportAttempts = new Map<string, number>()
|
|
446
446
|
const dailyReportLastAttemptAt = new Map<string, number>()
|
|
@@ -480,6 +480,23 @@ export function Home(props: {
|
|
|
480
480
|
}
|
|
481
481
|
}
|
|
482
482
|
|
|
483
|
+
const parseCollectStatsResult = (
|
|
484
|
+
raw: string | undefined,
|
|
485
|
+
): { already_exists?: boolean; no_activity?: boolean } | null => {
|
|
486
|
+
if (!raw) return null
|
|
487
|
+
try {
|
|
488
|
+
const parsed = JSON.parse(raw)
|
|
489
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null
|
|
490
|
+
const data = parsed as { already_exists?: boolean; no_activity?: boolean }
|
|
491
|
+
return {
|
|
492
|
+
already_exists: !!data.already_exists,
|
|
493
|
+
no_activity: !!data.no_activity,
|
|
494
|
+
}
|
|
495
|
+
} catch {
|
|
496
|
+
return null
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
483
500
|
const checkDailyReport = async () => {
|
|
484
501
|
if (dailyReportCheckRunning) return
|
|
485
502
|
if (!props.hasAI || !props.loggedIn) return
|
|
@@ -489,14 +506,20 @@ export function Home(props: {
|
|
|
489
506
|
|
|
490
507
|
const now = new Date()
|
|
491
508
|
try {
|
|
509
|
+
const { Config } = await import("../../config")
|
|
510
|
+
const reportHour = await Config.dailyReportHour()
|
|
511
|
+
if (reportHour < 0) return // auto-trigger disabled
|
|
512
|
+
|
|
492
513
|
const today = localDateKey(now)
|
|
493
514
|
if (dailyReportCompletedDate === today) return
|
|
494
|
-
if (now.getHours() <
|
|
515
|
+
if (now.getHours() < reportHour) return
|
|
495
516
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
517
|
+
if (!FORCE_DAILY_REGENERATE) {
|
|
518
|
+
const currentStatus = await fetchDailyReportStatus(today)
|
|
519
|
+
if (currentStatus === "exists") {
|
|
520
|
+
dailyReportCompletedDate = today
|
|
521
|
+
return
|
|
522
|
+
}
|
|
500
523
|
}
|
|
501
524
|
|
|
502
525
|
const attempts = dailyReportAttempts.get(today) || 0
|
|
@@ -519,21 +542,61 @@ export function Home(props: {
|
|
|
519
542
|
)
|
|
520
543
|
|
|
521
544
|
// Use the same prompt as the /daily command
|
|
522
|
-
await send(
|
|
523
|
-
"Generate my 'Day in Code' daily report. " +
|
|
524
|
-
"
|
|
525
|
-
"
|
|
526
|
-
"
|
|
527
|
-
"
|
|
528
|
-
"
|
|
529
|
-
"
|
|
530
|
-
"
|
|
531
|
-
"
|
|
532
|
-
"
|
|
533
|
-
"
|
|
534
|
-
|
|
545
|
+
const runResult = await send(
|
|
546
|
+
"Generate my 'Day in Code' daily report. Follow these steps EXACTLY in order:\n\n" +
|
|
547
|
+
"STEP 1: Call collect_daily_stats to get today's coding activity data.\n" +
|
|
548
|
+
"- If it returns already_exists=true" + (FORCE_DAILY_REGENERATE ? " (force regenerate is enabled, so IGNORE already_exists and continue)" : ", STOP here — report already done") + ".\n" +
|
|
549
|
+
"- If it returns no_activity=true, STOP here — nothing to report.\n" +
|
|
550
|
+
"- Save the date, timezone, and _rawStats from the result — you need them for save_daily_report later.\n\n" +
|
|
551
|
+
"STEP 2: Call scan_sessions with source='codex' and limit=8.\n" +
|
|
552
|
+
"- From the results, pick the top 2-3 sessions by activity.\n\n" +
|
|
553
|
+
"STEP 3: Call analyze_session on each picked session. Pass BOTH path and source EXACTLY from scan_sessions results.\n\n" +
|
|
554
|
+
"STEP 4: Write the blog post content (DO NOT call any tool yet).\n" +
|
|
555
|
+
"- Write as the AI agent in first person — tell the story of your day collaborating with the user.\n" +
|
|
556
|
+
"- The narrative is the main content. Stats are supporting context woven into the story.\n" +
|
|
557
|
+
"- Use concise markdown tables in a data-summary section, but do not make the post only tables.\n" +
|
|
558
|
+
"- Do NOT include any source code or file paths.\n" +
|
|
559
|
+
"- Prepare a catchy title (string), the full content (markdown, must NOT start with the title), and a summary.\n\n" +
|
|
560
|
+
"STEP 5: Call preview_post with ALL of these parameters:\n" +
|
|
561
|
+
" - mode: 'manual'\n" +
|
|
562
|
+
" - title: your catchy title (string, REQUIRED)\n" +
|
|
563
|
+
" - content: your full markdown content (string, REQUIRED, must NOT start with the title)\n" +
|
|
564
|
+
" - category: 'day-in-code'\n" +
|
|
565
|
+
" - tags: ['day-in-code']\n" +
|
|
566
|
+
" CRITICAL: Do NOT call preview_post with empty or missing title/content — it will fail.\n\n" +
|
|
567
|
+
"STEP 6: Call confirm_post with the post_id returned by preview_post. This is scheduled auto mode — do NOT wait for user confirmation.\n\n" +
|
|
568
|
+
"STEP 7: Call save_daily_report with:\n" +
|
|
569
|
+
" - date and timezone from collect_daily_stats result\n" +
|
|
570
|
+
" - post_id from confirm_post result\n" +
|
|
571
|
+
" - _rawStats from collect_daily_stats result\n\n" +
|
|
572
|
+
"FORBIDDEN: Do NOT call post_to_codeblog or auto_post. Only use preview_post + confirm_post.\n" +
|
|
573
|
+
(FORCE_DAILY_REGENERATE
|
|
574
|
+
? "FORCE MODE: When calling collect_daily_stats, set force=true.\n"
|
|
575
|
+
: "") +
|
|
576
|
+
"This scheduled task MUST complete all 7 steps in this single run.",
|
|
577
|
+
{
|
|
578
|
+
display: "Auto-generating daily report (Day in Code)",
|
|
579
|
+
maxSteps: 16,
|
|
580
|
+
idleTimeoutMs: 240_000,
|
|
581
|
+
toolTimeoutMs: 60_000,
|
|
582
|
+
freshContext: true,
|
|
583
|
+
},
|
|
535
584
|
)
|
|
536
585
|
|
|
586
|
+
const collectStats = parseCollectStatsResult(
|
|
587
|
+
runResult?.toolResults.find((t) => t.name === "collect_daily_stats")?.result,
|
|
588
|
+
)
|
|
589
|
+
if (collectStats?.already_exists && !FORCE_DAILY_REGENERATE) {
|
|
590
|
+
dailyReportCompletedDate = today
|
|
591
|
+
showMsg("Today's Day in Code report already exists.", theme.colors.success)
|
|
592
|
+
return
|
|
593
|
+
}
|
|
594
|
+
if (collectStats?.no_activity) {
|
|
595
|
+
dailyReportCompletedDate = today
|
|
596
|
+
showMsg("No coding activity detected today. Skipping daily report auto-run.", theme.colors.warning)
|
|
597
|
+
return
|
|
598
|
+
}
|
|
599
|
+
|
|
537
600
|
const afterStatus = await fetchDailyReportStatus(today)
|
|
538
601
|
if (afterStatus === "exists") {
|
|
539
602
|
dailyReportCompletedDate = today
|
|
@@ -674,7 +737,10 @@ export function Home(props: {
|
|
|
674
737
|
setInput((s) => s + text)
|
|
675
738
|
})
|
|
676
739
|
|
|
677
|
-
async function send(
|
|
740
|
+
async function send(
|
|
741
|
+
text: string,
|
|
742
|
+
options?: { display?: string; maxSteps?: number; idleTimeoutMs?: number; toolTimeoutMs?: number; freshContext?: boolean },
|
|
743
|
+
): Promise<{ toolResults: Array<{ name: string; result: string }>; assistantContent?: string } | undefined> {
|
|
678
744
|
if (!text.trim() || streaming()) return
|
|
679
745
|
ensureSession()
|
|
680
746
|
const prompt = text.trim()
|
|
@@ -686,6 +752,8 @@ export function Home(props: {
|
|
|
686
752
|
abortByUser = false
|
|
687
753
|
const assembler = new TuiStreamAssembler()
|
|
688
754
|
const toolResults: Array<{ name: string; result: string }> = []
|
|
755
|
+
let assistantContent: string | undefined
|
|
756
|
+
let streamErrorHandled = false
|
|
689
757
|
const flushMs = 60
|
|
690
758
|
let flushTimer: ReturnType<typeof setTimeout> | undefined
|
|
691
759
|
|
|
@@ -693,7 +761,8 @@ export function Home(props: {
|
|
|
693
761
|
if (force) {
|
|
694
762
|
if (flushTimer) clearTimeout(flushTimer)
|
|
695
763
|
flushTimer = undefined
|
|
696
|
-
|
|
764
|
+
const nextText = assembler.getText()
|
|
765
|
+
queueMicrotask(() => setStreamText(nextText))
|
|
697
766
|
return
|
|
698
767
|
}
|
|
699
768
|
if (flushTimer) return
|
|
@@ -710,14 +779,19 @@ export function Home(props: {
|
|
|
710
779
|
const { resolveModelFromConfig } = await import("../../ai/models")
|
|
711
780
|
const cfg = await Config.load()
|
|
712
781
|
const mid = resolveModelFromConfig(cfg) || AIProvider.DEFAULT_MODEL
|
|
713
|
-
const
|
|
782
|
+
const modelInput = options?.freshContext ? [userMsg] : [...prev, userMsg]
|
|
783
|
+
const allMsgs = modelInput
|
|
714
784
|
.filter((m): m is ChatMsg & { role: "user" | "assistant" } => m.role === "user" || m.role === "assistant")
|
|
715
785
|
.map((m) => ({ role: m.role, content: m.modelContent || m.content }))
|
|
716
786
|
abortCtrl = new AbortController()
|
|
717
787
|
|
|
718
788
|
let hasToolCalls = false
|
|
719
789
|
let finalizedText = ""
|
|
720
|
-
for await (const event of AIChat.streamEvents(allMsgs, mid, abortCtrl.signal, {
|
|
790
|
+
for await (const event of AIChat.streamEvents(allMsgs, mid, abortCtrl.signal, {
|
|
791
|
+
maxSteps: options?.maxSteps ?? 12,
|
|
792
|
+
idleTimeoutMs: options?.idleTimeoutMs,
|
|
793
|
+
toolTimeoutMs: options?.toolTimeoutMs,
|
|
794
|
+
})) {
|
|
721
795
|
if (event.type === "text-delta") {
|
|
722
796
|
assembler.pushDelta(event.text, event.seq)
|
|
723
797
|
flushStream()
|
|
@@ -728,11 +802,14 @@ export function Home(props: {
|
|
|
728
802
|
hasToolCalls = true
|
|
729
803
|
const partial = assembler.getText().trim()
|
|
730
804
|
if (partial) {
|
|
731
|
-
|
|
805
|
+
queueMicrotask(() => {
|
|
806
|
+
setMessages((p) => [...p, { role: "assistant", content: partial }])
|
|
807
|
+
})
|
|
732
808
|
assembler.reset()
|
|
733
809
|
setStreamText("")
|
|
734
810
|
}
|
|
735
|
-
|
|
811
|
+
const toolMsg: ChatMsg = { role: "tool", content: TOOL_LABELS[event.name] || event.name, toolName: event.name, toolCallID: event.callID, toolStatus: "running" }
|
|
812
|
+
queueMicrotask(() => setMessages((p) => [...p, toolMsg]))
|
|
736
813
|
continue
|
|
737
814
|
}
|
|
738
815
|
|
|
@@ -741,32 +818,40 @@ export function Home(props: {
|
|
|
741
818
|
const str = typeof event.result === "string" ? event.result : JSON.stringify(event.result)
|
|
742
819
|
toolResults.push({ name: event.name, result: str.slice(0, 1200) })
|
|
743
820
|
} catch {}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
821
|
+
const evtCallID = event.callID
|
|
822
|
+
const evtName = event.name
|
|
823
|
+
queueMicrotask(() => {
|
|
824
|
+
setMessages((p) => {
|
|
825
|
+
let matched = false
|
|
826
|
+
const next = p.map((m) => {
|
|
827
|
+
if (m.role !== "tool" || m.toolStatus !== "running") return m
|
|
828
|
+
if (m.toolCallID !== evtCallID) return m
|
|
829
|
+
matched = true
|
|
830
|
+
return { ...m, toolStatus: "done" as const }
|
|
831
|
+
})
|
|
832
|
+
if (matched) return next
|
|
833
|
+
return p.map((m) =>
|
|
834
|
+
m.role === "tool" && m.toolName === evtName && m.toolStatus === "running"
|
|
835
|
+
? { ...m, toolStatus: "done" as const }
|
|
836
|
+
: m
|
|
837
|
+
)
|
|
751
838
|
})
|
|
752
|
-
if (matched) return next
|
|
753
|
-
return p.map((m) =>
|
|
754
|
-
m.role === "tool" && m.toolName === event.name && m.toolStatus === "running"
|
|
755
|
-
? { ...m, toolStatus: "done" as const }
|
|
756
|
-
: m
|
|
757
|
-
)
|
|
758
839
|
})
|
|
759
840
|
continue
|
|
760
841
|
}
|
|
761
842
|
|
|
762
843
|
if (event.type === "error") {
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
844
|
+
if (streamErrorHandled) continue
|
|
845
|
+
streamErrorHandled = true
|
|
846
|
+
queueMicrotask(() => {
|
|
847
|
+
setMessages((p) => {
|
|
848
|
+
const updated = p.map((m) =>
|
|
849
|
+
m.role === "tool" && m.toolStatus === "running"
|
|
850
|
+
? { ...m, toolStatus: "error" as const }
|
|
851
|
+
: m
|
|
852
|
+
)
|
|
853
|
+
return [...updated, { role: "assistant" as const, content: `Error: ${event.error.message}` }]
|
|
854
|
+
})
|
|
770
855
|
})
|
|
771
856
|
continue
|
|
772
857
|
}
|
|
@@ -781,7 +866,10 @@ export function Home(props: {
|
|
|
781
866
|
hasToolCalls,
|
|
782
867
|
toolResults,
|
|
783
868
|
})
|
|
784
|
-
if (content)
|
|
869
|
+
if (content) {
|
|
870
|
+
assistantContent = content
|
|
871
|
+
queueMicrotask(() => setMessages((p) => [...p, { role: "assistant", content }]))
|
|
872
|
+
}
|
|
785
873
|
}
|
|
786
874
|
}
|
|
787
875
|
} catch (err) {
|
|
@@ -794,6 +882,7 @@ export function Home(props: {
|
|
|
794
882
|
setStreaming(false)
|
|
795
883
|
saveChat()
|
|
796
884
|
}
|
|
885
|
+
return { toolResults, assistantContent }
|
|
797
886
|
}
|
|
798
887
|
|
|
799
888
|
const aiProviderFiltered = createMemo(() => {
|