feat: ship v0.6.0 with cockpit, knowledge base, and chapter bridge

Deliver P5 writer workflow: writing cockpit, manual knowledge CRUD with review, chapter bridge with optional AI, publish status, and stock buffer reminders.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-06 23:03:11 +08:00
parent 78f046890d
commit b33d2e7b34
45 changed files with 2389 additions and 29 deletions
@@ -0,0 +1,21 @@
import type { SqliteDb } from '../types'
export class BookPrefsRepository {
constructor(private db: SqliteDb) {}
get(key: string): string | null {
const row = this.db.prepare('SELECT value FROM book_preferences WHERE key = ?').get(key) as
| { value: string }
| undefined
return row?.value ?? null
}
set(key: string, value: string): void {
this.db
.prepare(
`INSERT INTO book_preferences (key, value) VALUES (?, ?)
ON CONFLICT(key) DO UPDATE SET value = excluded.value`
)
.run(key, value)
}
}
@@ -0,0 +1,21 @@
import type { SqliteDb } from '../types'
export class BridgeDismissRepository {
constructor(private db: SqliteDb) {}
isDismissed(chapterId: string): boolean {
const row = this.db
.prepare('SELECT chapter_id FROM chapter_bridge_dismiss WHERE chapter_id = ?')
.get(chapterId)
return Boolean(row)
}
dismiss(chapterId: string): void {
this.db
.prepare(
`INSERT INTO chapter_bridge_dismiss (chapter_id) VALUES (?)
ON CONFLICT(chapter_id) DO UPDATE SET dismissed_at = datetime('now')`
)
.run(chapterId)
}
}
+36 -4
View File
@@ -1,6 +1,6 @@
import { randomUUID } from 'crypto'
import { countWords } from '../../services/word-count'
import type { Chapter, ChapterOrigin, ChapterStatus } from '../../../shared/types'
import type { Chapter, ChapterOrigin, ChapterStatus, PublishStatus } from '../../../shared/types'
import type { SqliteDb } from '../types'
function mapRow(row: Record<string, unknown>): Chapter {
return {
@@ -12,7 +12,8 @@ function mapRow(row: Record<string, unknown>): Chapter {
wordCount: row.word_count as number,
sortOrder: row.sort_order as number,
cursorOffset: (row.cursor_offset as number) ?? 0,
origin: ((row.origin as ChapterOrigin) ?? 'manual') as ChapterOrigin
origin: ((row.origin as ChapterOrigin) ?? 'manual') as ChapterOrigin,
publishStatus: ((row.publish_status as PublishStatus) ?? 'draft') as PublishStatus
}
}
@@ -59,7 +60,13 @@ export class ChapterRepository {
update(
id: string,
patch: Partial<{ title: string; content: string; status: ChapterStatus; cursorOffset: number }>
patch: Partial<{
title: string
content: string
status: ChapterStatus
cursorOffset: number
publishStatus: PublishStatus
}>
): Chapter {
const existing = this.get(id)
if (!existing) throw new Error('Chapter not found')
@@ -71,7 +78,7 @@ export class ChapterRepository {
.prepare(
`UPDATE chapters SET
title = ?, content = ?, status = ?, cursor_offset = ?,
word_count = ?, updated_at = datetime('now')
publish_status = ?, word_count = ?, updated_at = datetime('now')
WHERE id = ?`
)
.run(
@@ -79,12 +86,37 @@ export class ChapterRepository {
content,
patch.status ?? existing.status,
patch.cursorOffset ?? existing.cursorOffset,
patch.publishStatus ?? existing.publishStatus ?? 'draft',
wordCount,
id
)
return this.get(id)!
}
setPublishStatus(id: string, publishStatus: PublishStatus): Chapter {
return this.update(id, { publishStatus })
}
countByPublishStatus(status: PublishStatus): number {
const row = this.db
.prepare('SELECT COUNT(*) AS c FROM chapters WHERE publish_status = ?')
.get(status) as { c: number }
return row.c
}
getPreviousChapter(chapterId: string): Chapter | null {
const current = this.get(chapterId)
if (!current) return null
const row = this.db
.prepare(
`SELECT * FROM chapters
WHERE sort_order < ?
ORDER BY sort_order DESC LIMIT 1`
)
.get(current.sortOrder) as Record<string, unknown> | undefined
return row ? mapRow(row) : null
}
delete(id: string): void {
this.db.prepare('DELETE FROM chapters WHERE id = ?').run(id)
}
+114
View File
@@ -0,0 +1,114 @@
import { randomUUID } from 'crypto'
import type {
CreateKnowledgeInput,
ForeshadowStatus,
KnowledgeEntry,
KnowledgeStatus,
KnowledgeType
} from '../../../shared/types'
import type { SqliteDb } from '../types'
function mapRow(row: Record<string, unknown>): KnowledgeEntry {
return {
id: row.id as string,
type: row.type as KnowledgeType,
title: row.title as string,
content: row.content as string,
importance: row.importance as number,
status: row.status as KnowledgeStatus,
foreshadowStatus: (row.foreshadow_status as ForeshadowStatus) || undefined,
sourceChapterId: (row.source_chapter_id as string) || undefined,
lastMentionChapterId: (row.last_mention_chapter_id as string) || undefined,
linkedBookmarkId: (row.linked_bookmark_id as string) || undefined,
createdAt: row.created_at as string,
updatedAt: row.updated_at as string
}
}
export class KnowledgeRepository {
constructor(private db: SqliteDb) {}
create(input: CreateKnowledgeInput): KnowledgeEntry {
const id = randomUUID()
const status = input.status ?? 'pending'
this.db
.prepare(
`INSERT INTO knowledge_entries (
id, type, title, content, importance, status, foreshadow_status,
source_chapter_id, last_mention_chapter_id, linked_bookmark_id
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
)
.run(
id,
input.type,
input.title,
input.content ?? '',
input.importance ?? 3,
status,
input.foreshadowStatus ?? null,
input.sourceChapterId ?? null,
input.lastMentionChapterId ?? null,
input.linkedBookmarkId ?? null
)
return this.get(id)!
}
get(id: string): KnowledgeEntry | null {
const row = this.db.prepare('SELECT * FROM knowledge_entries WHERE id = ?').get(id) as
| Record<string, unknown>
| undefined
return row ? mapRow(row) : null
}
list(filter?: { status?: KnowledgeStatus; type?: KnowledgeType }): KnowledgeEntry[] {
let sql = 'SELECT * FROM knowledge_entries WHERE 1=1'
const params: unknown[] = []
if (filter?.status) {
sql += ' AND status = ?'
params.push(filter.status)
}
if (filter?.type) {
sql += ' AND type = ?'
params.push(filter.type)
}
sql += ' ORDER BY updated_at DESC'
return (this.db.prepare(sql).all(...params) as Record<string, unknown>[]).map(mapRow)
}
update(id: string, patch: Partial<CreateKnowledgeInput & { status: KnowledgeStatus }>): KnowledgeEntry {
const existing = this.get(id)
if (!existing) throw new Error('Knowledge entry not found')
this.db
.prepare(
`UPDATE knowledge_entries SET
type = ?, title = ?, content = ?, importance = ?, status = ?,
foreshadow_status = ?, source_chapter_id = ?, last_mention_chapter_id = ?,
linked_bookmark_id = ?, updated_at = datetime('now')
WHERE id = ?`
)
.run(
patch.type ?? existing.type,
patch.title ?? existing.title,
patch.content ?? existing.content,
patch.importance ?? existing.importance,
patch.status ?? existing.status,
patch.foreshadowStatus ?? existing.foreshadowStatus ?? null,
patch.sourceChapterId ?? existing.sourceChapterId ?? null,
patch.lastMentionChapterId ?? existing.lastMentionChapterId ?? null,
patch.linkedBookmarkId ?? existing.linkedBookmarkId ?? null,
id
)
return this.get(id)!
}
delete(id: string): void {
this.db.prepare('DELETE FROM knowledge_entries WHERE id = ?').run(id)
}
countByStatus(status: KnowledgeStatus): number {
const row = this.db
.prepare('SELECT COUNT(*) AS c FROM knowledge_entries WHERE status = ?')
.get(status) as { c: number }
return row.c
}
}