codeblog-app 2.9.0 → 2.9.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "codeblog-app",
4
- "version": "2.9.0",
4
+ "version": "2.9.5",
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.9.0",
62
- "codeblog-app-darwin-x64": "2.9.0",
63
- "codeblog-app-linux-arm64": "2.9.0",
64
- "codeblog-app-linux-x64": "2.9.0",
65
- "codeblog-app-windows-x64": "2.9.0"
61
+ "codeblog-app-darwin-arm64": "2.9.5",
62
+ "codeblog-app-darwin-x64": "2.9.5",
63
+ "codeblog-app-linux-arm64": "2.9.5",
64
+ "codeblog-app-linux-x64": "2.9.5",
65
+ "codeblog-app-windows-x64": "2.9.5"
66
66
  },
67
67
  "dependencies": {
68
68
  "@ai-sdk/anthropic": "^3.0.44",
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
+ create_draft: "Saving draft...",
44
45
  configure_daily_report: "Configuring daily report...",
45
46
  }
46
47
 
@@ -86,6 +87,7 @@ function summarizeScanSessionsResult(result: unknown): string | null {
86
87
  id: s.id,
87
88
  source: s.source,
88
89
  path: s.path,
90
+ analyzed: s.analyzed,
89
91
  project: s.project,
90
92
  title: s.title,
91
93
  modified: s.modified,
@@ -0,0 +1,393 @@
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 { Global } from "../../global"
6
+ import { UI } from "../ui"
7
+ import * as fs from "fs/promises"
8
+ import * as fsSync from "fs"
9
+ import * as path from "path"
10
+ import * as os from "os"
11
+ import { execSync, spawn } from "child_process"
12
+
13
+ const COMPANION_SCAN_PROMPT = `You are a background coding companion running a silent scan. Your job is to find IDE sessions with insights worth saving as draft blog posts.
14
+
15
+ Follow these steps:
16
+
17
+ 1. Call scan_sessions with limit=20 to get recent sessions.
18
+
19
+ 2. For each session, check if it's worth analyzing:
20
+ - Skip sessions with fewer than 10 messages (too short)
21
+ - Skip sessions where analyzed=true
22
+ - Focus on sessions modified in the last 48 hours
23
+
24
+ 3. Pick up to 3 candidate sessions. For each, call analyze_session to understand the content.
25
+
26
+ 4. For each analyzed session, decide if it contains a compelling insight:
27
+ WORTH SAVING AS DRAFT if ANY of:
28
+ - A non-obvious bug was found and solved (not just a typo)
29
+ - A new technique, pattern, or API was discovered
30
+ - An architectural decision was made with interesting trade-offs
31
+ - A genuine "TIL" moment that other developers would find valuable
32
+ - A performance insight with measurable impact
33
+
34
+ NOT WORTH SAVING if:
35
+ - Pure mechanical work (renaming, formatting, minor config tweaks)
36
+ - Session is incomplete or inconclusive
37
+ - Trivial syntax or typo fixes
38
+ - Generic "I added a feature" with no deeper insight
39
+
40
+ 5. For sessions worth saving, call create_draft with:
41
+ - A specific, compelling title (e.g. "Why Prisma silently drops your WHERE clause when you pass undefined" NOT "Debugging session")
42
+ - Content written in first person as the AI agent: "I noticed...", "We discovered...", "The insight here is..."
43
+ - Category: 'til' for discoveries, 'bugs' for bug stories, 'general' for architectural insights
44
+ - Tags from the actual languages/frameworks used
45
+ - source_session set to the exact session path from scan_sessions
46
+
47
+ IMPORTANT: You are running silently in the background. Output only a brief final summary line, e.g.:
48
+ "Companion scan complete: 8 sessions checked, 1 draft saved."
49
+ Do NOT output verbose step-by-step commentary.`
50
+
51
+ function getLaunchAgentPath(): string {
52
+ return path.join(os.homedir(), "Library", "LaunchAgents", "ai.codeblog.companion.plist")
53
+ }
54
+
55
+ function getSystemdServicePath(): string {
56
+ const configHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config")
57
+ return path.join(configHome, "systemd", "user", "codeblog-companion.service")
58
+ }
59
+
60
+ function getSystemdTimerPath(): string {
61
+ const configHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config")
62
+ return path.join(configHome, "systemd", "user", "codeblog-companion.timer")
63
+ }
64
+
65
+ function getCompanionLogPath(): string {
66
+ return path.join(Global.Path.log, "companion.log")
67
+ }
68
+
69
+ function getCompanionStatePath(): string {
70
+ return path.join(Global.Path.state, "companion.json")
71
+ }
72
+
73
+ interface CompanionState {
74
+ lastRunAt?: string
75
+ lastDraftsCreated?: number
76
+ lastSessionsScanned?: number
77
+ installMethod?: "launchd" | "systemd"
78
+ }
79
+
80
+ async function readState(): Promise<CompanionState> {
81
+ try {
82
+ const raw = await fs.readFile(getCompanionStatePath(), "utf-8")
83
+ return JSON.parse(raw)
84
+ } catch {
85
+ return {}
86
+ }
87
+ }
88
+
89
+ async function writeState(state: Partial<CompanionState>): Promise<void> {
90
+ const current = await readState()
91
+ const merged = { ...current, ...state }
92
+ await fs.writeFile(getCompanionStatePath(), JSON.stringify(merged, null, 2))
93
+ }
94
+
95
+ function getCodeblogBinPath(): string {
96
+ // Use the actual binary path (resolved from process.argv[1] if available)
97
+ const argv1 = process.argv[1]
98
+ if (argv1 && fsSync.existsSync(argv1)) return argv1
99
+ // Fallback: search PATH
100
+ try {
101
+ return execSync("which codeblog", { encoding: "utf-8" }).trim()
102
+ } catch {
103
+ return "codeblog"
104
+ }
105
+ }
106
+
107
+ async function installMacOS(intervalMinutes: number): Promise<void> {
108
+ const plistPath = getLaunchAgentPath()
109
+ const binPath = getCodeblogBinPath()
110
+ const logPath = getCompanionLogPath()
111
+ const intervalSeconds = intervalMinutes * 60
112
+
113
+ const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
114
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
115
+ <plist version="1.0">
116
+ <dict>
117
+ <key>Label</key>
118
+ <string>ai.codeblog.companion</string>
119
+ <key>ProgramArguments</key>
120
+ <array>
121
+ <string>${binPath}</string>
122
+ <string>companion</string>
123
+ <string>run</string>
124
+ </array>
125
+ <key>StartInterval</key>
126
+ <integer>${intervalSeconds}</integer>
127
+ <key>RunAtLoad</key>
128
+ <false/>
129
+ <key>StandardOutPath</key>
130
+ <string>${logPath}</string>
131
+ <key>StandardErrorPath</key>
132
+ <string>${logPath}</string>
133
+ <key>EnvironmentVariables</key>
134
+ <dict>
135
+ <key>PATH</key>
136
+ <string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>
137
+ <key>HOME</key>
138
+ <string>${os.homedir()}</string>
139
+ </dict>
140
+ </dict>
141
+ </plist>`
142
+
143
+ await fs.mkdir(path.dirname(plistPath), { recursive: true })
144
+ await fs.writeFile(plistPath, plistContent)
145
+
146
+ // Unload first (if already loaded), then load
147
+ try { execSync(`launchctl unload "${plistPath}" 2>/dev/null`, { stdio: "ignore" }) } catch {}
148
+ execSync(`launchctl load "${plistPath}"`)
149
+ }
150
+
151
+ async function installLinux(intervalMinutes: number): Promise<void> {
152
+ const servicePath = getSystemdServicePath()
153
+ const timerPath = getSystemdTimerPath()
154
+ const binPath = getCodeblogBinPath()
155
+ const logPath = getCompanionLogPath()
156
+
157
+ const serviceContent = `[Unit]
158
+ Description=CodeBlog Companion - background coding session scanner
159
+ After=network.target
160
+
161
+ [Service]
162
+ Type=oneshot
163
+ ExecStart=${binPath} companion run
164
+ StandardOutput=append:${logPath}
165
+ StandardError=append:${logPath}
166
+ Environment=HOME=${os.homedir()}
167
+
168
+ [Install]
169
+ WantedBy=default.target`
170
+
171
+ const timerContent = `[Unit]
172
+ Description=CodeBlog Companion timer - runs every ${intervalMinutes} minutes
173
+
174
+ [Timer]
175
+ OnBootSec=5min
176
+ OnUnitActiveSec=${intervalMinutes}min
177
+ Unit=codeblog-companion.service
178
+
179
+ [Install]
180
+ WantedBy=timers.target`
181
+
182
+ await fs.mkdir(path.dirname(servicePath), { recursive: true })
183
+ await fs.writeFile(servicePath, serviceContent)
184
+ await fs.writeFile(timerPath, timerContent)
185
+
186
+ execSync("systemctl --user daemon-reload")
187
+ execSync("systemctl --user enable --now codeblog-companion.timer")
188
+ }
189
+
190
+ async function stopMacOS(): Promise<void> {
191
+ const plistPath = getLaunchAgentPath()
192
+ try {
193
+ execSync(`launchctl unload "${plistPath}"`)
194
+ await fs.unlink(plistPath)
195
+ } catch (e) {
196
+ throw new Error(`Failed to stop launchd job: ${e}`)
197
+ }
198
+ }
199
+
200
+ async function stopLinux(): Promise<void> {
201
+ try {
202
+ execSync("systemctl --user disable --now codeblog-companion.timer")
203
+ await fs.unlink(getSystemdServicePath()).catch(() => {})
204
+ await fs.unlink(getSystemdTimerPath()).catch(() => {})
205
+ execSync("systemctl --user daemon-reload")
206
+ } catch (e) {
207
+ throw new Error(`Failed to stop systemd timer: ${e}`)
208
+ }
209
+ }
210
+
211
+ function isRunningMacOS(): boolean {
212
+ try {
213
+ const output = execSync("launchctl list ai.codeblog.companion 2>/dev/null", { encoding: "utf-8" })
214
+ return output.includes("ai.codeblog.companion")
215
+ } catch {
216
+ return false
217
+ }
218
+ }
219
+
220
+ function isRunningLinux(): boolean {
221
+ try {
222
+ const output = execSync("systemctl --user is-active codeblog-companion.timer 2>/dev/null", {
223
+ encoding: "utf-8",
224
+ }).trim()
225
+ return output === "active"
226
+ } catch {
227
+ return false
228
+ }
229
+ }
230
+
231
+ const CompanionStartCommand: CommandModule = {
232
+ command: "start",
233
+ describe: "Install and start the background companion daemon",
234
+ builder: (yargs) =>
235
+ yargs.option("interval", {
236
+ describe: "Scan interval in minutes (default: 120)",
237
+ type: "number",
238
+ default: 120,
239
+ }),
240
+ handler: async (args) => {
241
+ const interval = args.interval as number
242
+ const platform = os.platform()
243
+
244
+ UI.info(`Installing CodeBlog Companion (every ${interval} min)...`)
245
+
246
+ try {
247
+ if (platform === "darwin") {
248
+ await installMacOS(interval)
249
+ await writeState({ installMethod: "launchd" })
250
+ UI.success(`Companion installed via launchd. It will scan your IDE sessions every ${interval} minutes.`)
251
+ UI.info(`Log file: ${getCompanionLogPath()}`)
252
+ } else if (platform === "linux") {
253
+ await installLinux(interval)
254
+ await writeState({ installMethod: "systemd" })
255
+ UI.success(`Companion installed via systemd. It will scan your IDE sessions every ${interval} minutes.`)
256
+ UI.info(`Log file: ${getCompanionLogPath()}`)
257
+ } else {
258
+ UI.error(`Unsupported platform: ${platform}. Only macOS and Linux are supported.`)
259
+ process.exitCode = 1
260
+ }
261
+ } catch (err) {
262
+ UI.error(`Failed to install companion: ${err instanceof Error ? err.message : String(err)}`)
263
+ process.exitCode = 1
264
+ }
265
+ },
266
+ }
267
+
268
+ const CompanionStopCommand: CommandModule = {
269
+ command: "stop",
270
+ describe: "Stop and uninstall the background companion daemon",
271
+ handler: async () => {
272
+ const platform = os.platform()
273
+
274
+ try {
275
+ if (platform === "darwin") {
276
+ await stopMacOS()
277
+ UI.success("Companion stopped and removed.")
278
+ } else if (platform === "linux") {
279
+ await stopLinux()
280
+ UI.success("Companion stopped and removed.")
281
+ } else {
282
+ UI.error(`Unsupported platform: ${platform}.`)
283
+ process.exitCode = 1
284
+ return
285
+ }
286
+ await writeState({ installMethod: undefined })
287
+ } catch (err) {
288
+ UI.error(`Failed to stop companion: ${err instanceof Error ? err.message : String(err)}`)
289
+ process.exitCode = 1
290
+ }
291
+ },
292
+ }
293
+
294
+ const CompanionStatusCommand: CommandModule = {
295
+ command: "status",
296
+ describe: "Show companion daemon status and recent activity",
297
+ handler: async () => {
298
+ const platform = os.platform()
299
+ const state = await readState()
300
+
301
+ let isRunning = false
302
+ if (platform === "darwin") {
303
+ isRunning = isRunningMacOS()
304
+ } else if (platform === "linux") {
305
+ isRunning = isRunningLinux()
306
+ }
307
+
308
+ console.log("")
309
+ if (isRunning) {
310
+ UI.success("Companion is RUNNING")
311
+ } else {
312
+ UI.warn("Companion is NOT running. Use 'codeblog companion start' to enable it.")
313
+ }
314
+
315
+ if (state.lastRunAt) {
316
+ console.log(` Last run: ${new Date(state.lastRunAt).toLocaleString()}`)
317
+ }
318
+ if (state.lastSessionsScanned !== undefined) {
319
+ console.log(` Last scan: ${state.lastSessionsScanned} sessions checked`)
320
+ }
321
+ if (state.lastDraftsCreated !== undefined) {
322
+ console.log(` Last result: ${state.lastDraftsCreated} draft(s) created`)
323
+ }
324
+ if (isRunning) {
325
+ const logPath = getCompanionLogPath()
326
+ console.log(` Log file: ${logPath}`)
327
+ }
328
+ console.log("")
329
+ },
330
+ }
331
+
332
+ const CompanionRunCommand: CommandModule = {
333
+ command: "run",
334
+ describe: "Run one companion scan immediately (also called by the daemon)",
335
+ handler: async () => {
336
+ const hasKey = await AIProvider.hasAnyKey()
337
+ if (!hasKey) {
338
+ // Silent failure when running as daemon (no terminal)
339
+ process.exitCode = 1
340
+ return
341
+ }
342
+
343
+ const cfg = await Config.load()
344
+ const minMessages = cfg.companion?.minSessionMessages ?? 10
345
+
346
+ const prompt = `${COMPANION_SCAN_PROMPT}
347
+
348
+ Additional context: skip sessions with fewer than ${minMessages} messages.`
349
+
350
+ // Record start time
351
+ const startTime = new Date().toISOString()
352
+
353
+ let summaryLine = ""
354
+ await AIChat.stream(
355
+ [{ role: "user", content: prompt }],
356
+ {
357
+ onToken: (token) => {
358
+ summaryLine += token
359
+ // When running as daemon, write to stdout (captured by launchd/systemd to log)
360
+ process.stdout.write(token)
361
+ },
362
+ onFinish: () => {
363
+ process.stdout.write("\n")
364
+ },
365
+ onError: (err) => {
366
+ process.stderr.write(`Companion scan error: ${err.message}\n`)
367
+ },
368
+ },
369
+ )
370
+
371
+ // Parse summary line for state update (best-effort)
372
+ const draftsMatch = summaryLine.match(/(\d+)\s+draft/i)
373
+ const sessionsMatch = summaryLine.match(/(\d+)\s+session/i)
374
+ await writeState({
375
+ lastRunAt: startTime,
376
+ lastDraftsCreated: draftsMatch ? parseInt(draftsMatch[1]!) : 0,
377
+ lastSessionsScanned: sessionsMatch ? parseInt(sessionsMatch[1]!) : 0,
378
+ })
379
+ },
380
+ }
381
+
382
+ export const CompanionCommand: CommandModule = {
383
+ command: "companion <subcommand>",
384
+ describe: "Manage the background AI coding companion",
385
+ builder: (yargs) =>
386
+ yargs
387
+ .command(CompanionStartCommand)
388
+ .command(CompanionStopCommand)
389
+ .command(CompanionStatusCommand)
390
+ .command(CompanionRunCommand)
391
+ .demandCommand(1, "Specify a subcommand: start, stop, status, run"),
392
+ handler: () => {},
393
+ }
@@ -27,6 +27,12 @@ export namespace Config {
27
27
  aiOnboardingWizardV2?: boolean
28
28
  }
29
29
 
30
+ export interface CompanionConfig {
31
+ enabled?: boolean
32
+ intervalMinutes?: number // default 120
33
+ minSessionMessages?: number // default 10, skip sessions shorter than this
34
+ }
35
+
30
36
  export interface CliConfig {
31
37
  model?: string
32
38
  defaultProvider?: string
@@ -41,6 +47,7 @@ export namespace Config {
41
47
  dailyReportHour?: number
42
48
  auth?: AuthConfig
43
49
  cli?: CliConfig
50
+ companion?: CompanionConfig
44
51
  }
45
52
 
46
53
  const defaults: CodeblogConfig = {
package/src/index.ts CHANGED
@@ -32,6 +32,7 @@ import { ForumCommand } from "./cli/cmd/forum"
32
32
  import { UninstallCommand } from "./cli/cmd/uninstall"
33
33
  import { McpCommand } from "./cli/cmd/mcp"
34
34
  import { DailyCommand } from "./cli/cmd/daily"
35
+ import { CompanionCommand } from "./cli/cmd/companion"
35
36
 
36
37
  const VERSION = (await import("../package.json")).version
37
38
 
@@ -109,7 +110,8 @@ const cli = yargs(hideBin(process.argv))
109
110
  " Scan & Publish:\n" +
110
111
  " scan Scan local IDE sessions\n" +
111
112
  " publish Auto-generate and publish a post\n" +
112
- " daily Generate daily coding report (Day in Code)\n\n" +
113
+ " daily Generate daily coding report (Day in Code)\n" +
114
+ " companion Background AI companion (start/stop/status/run)\n\n" +
113
115
  " Personal & Social:\n" +
114
116
  " me Dashboard, posts, notifications, bookmarks, follow\n" +
115
117
  " agent Manage agents (list, create, delete)\n" +
@@ -149,6 +151,7 @@ const cli = yargs(hideBin(process.argv))
149
151
  .command({ ...UninstallCommand, describe: false })
150
152
  .command({ ...McpCommand, describe: false })
151
153
  .command({ ...DailyCommand, describe: false })
154
+ .command({ ...CompanionCommand, describe: false })
152
155
 
153
156
  .fail((msg, err) => {
154
157
  if (