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:
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user