@codehourra/llm-iwiki 0.1.0 → 0.1.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/dist/index.js +514 -0
- package/package.json +7 -8
- package/src/ai-yaml.ts +0 -82
- package/src/cli.ts +0 -485
- package/src/collectors/claude-code.ts +0 -120
- package/src/collectors/codebuddy.ts +0 -164
- package/src/collectors/codex.ts +0 -130
- package/src/collectors/cursor.ts +0 -178
- package/src/collectors/gemini.ts +0 -141
- package/src/collectors/index.ts +0 -20
- package/src/collectors/types.ts +0 -22
- package/src/collectors/util.ts +0 -58
- package/src/compaction.ts +0 -63
- package/src/config.ts +0 -57
- package/src/db.ts +0 -152
- package/src/experiences.ts +0 -235
- package/src/index.ts +0 -10
- package/src/obsidian.ts +0 -345
- package/src/paths.ts +0 -23
- package/src/projects.ts +0 -192
- package/src/sessions.ts +0 -119
- package/src/skills.ts +0 -168
- package/src/summarize.ts +0 -122
- package/src/sync.ts +0 -207
- package/src/types.ts +0 -26
package/src/collectors/gemini.ts
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from 'node:fs'
|
|
2
|
-
import { basename, dirname, join } from 'node:path'
|
|
3
|
-
|
|
4
|
-
import type { Collector, RawMessage, RawSession } from './types'
|
|
5
|
-
import { deriveTitle, isEphemeralPath } from './util'
|
|
6
|
-
|
|
7
|
-
const INTERNAL_ROOTS = ['.gemini-internal', '.gemini']
|
|
8
|
-
|
|
9
|
-
function loadProjectNameMap(geminiRoot: string): Map<string, string> {
|
|
10
|
-
const nameToPath = new Map<string, string>()
|
|
11
|
-
const projectsFile = join(geminiRoot, 'projects.json')
|
|
12
|
-
if (!existsSync(projectsFile)) return nameToPath
|
|
13
|
-
try {
|
|
14
|
-
const parsed = JSON.parse(readFileSync(projectsFile, 'utf8')) as { projects?: Record<string, string> }
|
|
15
|
-
for (const [path, name] of Object.entries(parsed.projects ?? {})) {
|
|
16
|
-
if (typeof name === 'string' && !nameToPath.has(name)) nameToPath.set(name, path)
|
|
17
|
-
}
|
|
18
|
-
} catch {
|
|
19
|
-
// ignore malformed projects.json
|
|
20
|
-
}
|
|
21
|
-
return nameToPath
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function looksLikeAbsolutePath(value: string): boolean {
|
|
25
|
-
const trimmed = value.trim()
|
|
26
|
-
return trimmed.startsWith('/') && !trimmed.includes('\n') && trimmed.length < 256
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function parseChatFile(filePath: string, projectDir: string, nameToPath: Map<string, string>): RawSession | null {
|
|
30
|
-
let parsed: {
|
|
31
|
-
sessionId?: string
|
|
32
|
-
startTime?: string
|
|
33
|
-
lastUpdated?: string
|
|
34
|
-
messages?: Array<{ type?: string; content?: unknown; timestamp?: string }>
|
|
35
|
-
}
|
|
36
|
-
try {
|
|
37
|
-
parsed = JSON.parse(readFileSync(filePath, 'utf8'))
|
|
38
|
-
} catch {
|
|
39
|
-
return null
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const messages: RawMessage[] = []
|
|
43
|
-
for (const message of parsed.messages ?? []) {
|
|
44
|
-
if (message.type !== 'user' && message.type !== 'gemini') continue
|
|
45
|
-
const content = typeof message.content === 'string' ? message.content : ''
|
|
46
|
-
if (content.trim() === '') continue
|
|
47
|
-
messages.push({
|
|
48
|
-
role: message.type === 'user' ? 'user' : 'assistant',
|
|
49
|
-
content,
|
|
50
|
-
timestamp: typeof message.timestamp === 'string' ? message.timestamp : null,
|
|
51
|
-
})
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (messages.length === 0) return null
|
|
55
|
-
|
|
56
|
-
let rawProjectPath = nameToPath.get(projectDir) ?? null
|
|
57
|
-
if (!rawProjectPath) {
|
|
58
|
-
const firstUser = messages.find((message) => message.role === 'user')
|
|
59
|
-
if (firstUser && looksLikeAbsolutePath(firstUser.content)) {
|
|
60
|
-
rawProjectPath = firstUser.content.trim().replace(/\/+$/, '')
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
if (isEphemeralPath(rawProjectPath)) return null
|
|
64
|
-
|
|
65
|
-
const timestamps = messages.map((message) => message.timestamp).filter((value): value is string => value != null)
|
|
66
|
-
|
|
67
|
-
return {
|
|
68
|
-
sourceSessionId: parsed.sessionId ?? basename(filePath).replace(/\.json$/, ''),
|
|
69
|
-
rawPath: filePath,
|
|
70
|
-
rawProjectPath,
|
|
71
|
-
title: deriveTitle(messages),
|
|
72
|
-
createdAt: parsed.startTime ?? timestamps[0] ?? null,
|
|
73
|
-
updatedAt: parsed.lastUpdated ?? timestamps[timestamps.length - 1] ?? null,
|
|
74
|
-
messages,
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function resolveGeminiRoot(homeDir: string): string | null {
|
|
79
|
-
const seen = new Set<string>()
|
|
80
|
-
for (const root of INTERNAL_ROOTS) {
|
|
81
|
-
const dir = join(homeDir, root)
|
|
82
|
-
if (!existsSync(dir)) continue
|
|
83
|
-
let canonical: string
|
|
84
|
-
try {
|
|
85
|
-
canonical = realpathSync(dir)
|
|
86
|
-
} catch {
|
|
87
|
-
canonical = dir
|
|
88
|
-
}
|
|
89
|
-
if (seen.has(canonical)) continue
|
|
90
|
-
seen.add(canonical)
|
|
91
|
-
return dir
|
|
92
|
-
}
|
|
93
|
-
return null
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function listChatFiles(geminiRoot: string): string[] {
|
|
97
|
-
const tmpDir = join(geminiRoot, 'tmp')
|
|
98
|
-
if (!existsSync(tmpDir)) return []
|
|
99
|
-
|
|
100
|
-
const files: string[] = []
|
|
101
|
-
for (const projectEntry of readdirSync(tmpDir, { withFileTypes: true })) {
|
|
102
|
-
if (!projectEntry.isDirectory()) continue
|
|
103
|
-
const chatsDir = join(tmpDir, projectEntry.name, 'chats')
|
|
104
|
-
if (!existsSync(chatsDir)) continue
|
|
105
|
-
for (const fileEntry of readdirSync(chatsDir, { withFileTypes: true })) {
|
|
106
|
-
if (fileEntry.isFile() && fileEntry.name.startsWith('session-') && fileEntry.name.endsWith('.json')) {
|
|
107
|
-
files.push(join(chatsDir, fileEntry.name))
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return files
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export const geminiCollector: Collector = {
|
|
115
|
-
id: 'gemini',
|
|
116
|
-
name: 'Gemini',
|
|
117
|
-
|
|
118
|
-
detect(homeDir: string): boolean {
|
|
119
|
-
const root = resolveGeminiRoot(homeDir)
|
|
120
|
-
return root != null && existsSync(join(root, 'tmp'))
|
|
121
|
-
},
|
|
122
|
-
|
|
123
|
-
collect(homeDir: string): RawSession[] {
|
|
124
|
-
const geminiRoot = resolveGeminiRoot(homeDir)
|
|
125
|
-
if (!geminiRoot) return []
|
|
126
|
-
const nameToPath = loadProjectNameMap(geminiRoot)
|
|
127
|
-
|
|
128
|
-
const sessions: RawSession[] = []
|
|
129
|
-
for (const filePath of listChatFiles(geminiRoot)) {
|
|
130
|
-
try {
|
|
131
|
-
if (!statSync(filePath).isFile()) continue
|
|
132
|
-
const projectDir = basename(dirname(dirname(filePath)))
|
|
133
|
-
const session = parseChatFile(filePath, projectDir, nameToPath)
|
|
134
|
-
if (session) sessions.push(session)
|
|
135
|
-
} catch {
|
|
136
|
-
continue
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
return sessions
|
|
140
|
-
},
|
|
141
|
-
}
|
package/src/collectors/index.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { claudeCodeCollector } from './claude-code'
|
|
2
|
-
import { codebuddyCollector } from './codebuddy'
|
|
3
|
-
import { codexCollector } from './codex'
|
|
4
|
-
import { cursorCollector } from './cursor'
|
|
5
|
-
import { geminiCollector } from './gemini'
|
|
6
|
-
import type { Collector } from './types'
|
|
7
|
-
|
|
8
|
-
export const COLLECTORS: Collector[] = [
|
|
9
|
-
claudeCodeCollector,
|
|
10
|
-
codexCollector,
|
|
11
|
-
cursorCollector,
|
|
12
|
-
geminiCollector,
|
|
13
|
-
codebuddyCollector,
|
|
14
|
-
]
|
|
15
|
-
|
|
16
|
-
export function getCollector(id: string): Collector | null {
|
|
17
|
-
return COLLECTORS.find((collector) => collector.id === id) ?? null
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export type { Collector, RawMessage, RawSession } from './types'
|
package/src/collectors/types.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
export interface RawMessage {
|
|
2
|
-
role: string
|
|
3
|
-
content: string
|
|
4
|
-
timestamp: string | null
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export interface RawSession {
|
|
8
|
-
sourceSessionId: string
|
|
9
|
-
rawPath: string
|
|
10
|
-
rawProjectPath: string | null
|
|
11
|
-
title: string | null
|
|
12
|
-
createdAt: string | null
|
|
13
|
-
updatedAt: string | null
|
|
14
|
-
messages: RawMessage[]
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface Collector {
|
|
18
|
-
id: string
|
|
19
|
-
name: string
|
|
20
|
-
detect(homeDir: string): boolean
|
|
21
|
-
collect(homeDir: string): RawSession[]
|
|
22
|
-
}
|
package/src/collectors/util.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { tmpdir } from 'node:os'
|
|
2
|
-
|
|
3
|
-
import type { RawMessage } from './types'
|
|
4
|
-
|
|
5
|
-
const TITLE_MAX_LENGTH = 120
|
|
6
|
-
|
|
7
|
-
const EPHEMERAL_PREFIXES = ['/tmp/', '/private/tmp/', '/var/folders/', '/private/var/folders/']
|
|
8
|
-
|
|
9
|
-
export function isEphemeralPath(path: string | null): boolean {
|
|
10
|
-
if (!path) return false
|
|
11
|
-
const temp = tmpdir()
|
|
12
|
-
if (path === temp) return true
|
|
13
|
-
const normalized = path.endsWith('/') ? path : `${path}/`
|
|
14
|
-
const prefixes = [...EPHEMERAL_PREFIXES, `${temp}/`]
|
|
15
|
-
return prefixes.some((prefix) => normalized.startsWith(prefix))
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function clampTitle(value: string): string {
|
|
19
|
-
return value.length > TITLE_MAX_LENGTH ? `${value.slice(0, TITLE_MAX_LENGTH)}…` : value
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function firstMeaningfulLine(content: string): string | null {
|
|
23
|
-
for (const line of content.split('\n')) {
|
|
24
|
-
const trimmed = line.trim()
|
|
25
|
-
if (trimmed === '' || /^[-=*#>`~]+$/.test(trimmed)) continue
|
|
26
|
-
return trimmed
|
|
27
|
-
}
|
|
28
|
-
return null
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function deriveTitle(messages: RawMessage[]): string | null {
|
|
32
|
-
for (const message of messages) {
|
|
33
|
-
if (message.role !== 'user') continue
|
|
34
|
-
const line = firstMeaningfulLine(message.content)
|
|
35
|
-
if (line) return clampTitle(line)
|
|
36
|
-
}
|
|
37
|
-
return null
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function normalizeContentParts(content: unknown): string {
|
|
41
|
-
if (typeof content === 'string') return content
|
|
42
|
-
if (Array.isArray(content)) {
|
|
43
|
-
return content
|
|
44
|
-
.map((part) => {
|
|
45
|
-
if (typeof part === 'string') return part
|
|
46
|
-
if (part && typeof part === 'object') {
|
|
47
|
-
const record = part as Record<string, unknown>
|
|
48
|
-
if (typeof record.text === 'string') return record.text
|
|
49
|
-
return JSON.stringify(record)
|
|
50
|
-
}
|
|
51
|
-
return String(part)
|
|
52
|
-
})
|
|
53
|
-
.join('\n')
|
|
54
|
-
.trim()
|
|
55
|
-
}
|
|
56
|
-
if (content == null) return ''
|
|
57
|
-
return JSON.stringify(content)
|
|
58
|
-
}
|
package/src/compaction.ts
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import type { RawMessage } from './collectors'
|
|
2
|
-
|
|
3
|
-
export interface CompactionOptions {
|
|
4
|
-
maxPerMessage: number
|
|
5
|
-
maxMessages: number
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
const DEFAULTS: CompactionOptions = {
|
|
9
|
-
maxPerMessage: 1200,
|
|
10
|
-
maxMessages: 60,
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function squashWhitespace(text: string): string {
|
|
14
|
-
return text.replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim()
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function truncate(text: string, max: number): string {
|
|
18
|
-
const cleaned = squashWhitespace(text)
|
|
19
|
-
if (cleaned.length <= max) return cleaned
|
|
20
|
-
return `${cleaned.slice(0, max)}\n…[truncated ${cleaned.length - max} chars]`
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface CompactMessageInput {
|
|
24
|
-
role: string
|
|
25
|
-
content: string
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Deterministic, dependency-free compaction. Keeps the head and tail of long
|
|
30
|
-
* conversations and truncates oversized individual messages so the result is a
|
|
31
|
-
* readable transcript that fits a token budget without calling any model.
|
|
32
|
-
*/
|
|
33
|
-
export function compactTranscript(
|
|
34
|
-
messages: CompactMessageInput[],
|
|
35
|
-
options: Partial<CompactionOptions> = {},
|
|
36
|
-
): string {
|
|
37
|
-
const opts = { ...DEFAULTS, ...options }
|
|
38
|
-
const meaningful = messages.filter((message) => message.content.trim() !== '')
|
|
39
|
-
|
|
40
|
-
let kept = meaningful
|
|
41
|
-
let omittedNote = ''
|
|
42
|
-
if (meaningful.length > opts.maxMessages) {
|
|
43
|
-
const head = Math.ceil(opts.maxMessages * 0.6)
|
|
44
|
-
const tail = opts.maxMessages - head
|
|
45
|
-
kept = [...meaningful.slice(0, head), ...meaningful.slice(meaningful.length - tail)]
|
|
46
|
-
omittedNote = `\n…[omitted ${meaningful.length - opts.maxMessages} middle messages]\n`
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const lines: string[] = []
|
|
50
|
-
kept.forEach((message, index) => {
|
|
51
|
-
if (omittedNote && index === Math.ceil(opts.maxMessages * 0.6)) {
|
|
52
|
-
lines.push(omittedNote)
|
|
53
|
-
}
|
|
54
|
-
const label = message.role === 'user' ? 'User' : message.role === 'assistant' ? 'Assistant' : message.role
|
|
55
|
-
lines.push(`**${label}:** ${truncate(message.content, opts.maxPerMessage)}`)
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
return lines.join('\n\n')
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function compactRawMessages(messages: RawMessage[], options?: Partial<CompactionOptions>): string {
|
|
62
|
-
return compactTranscript(messages, options)
|
|
63
|
-
}
|
package/src/config.ts
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
-
|
|
3
|
-
export interface LlmIwikiConfig {
|
|
4
|
-
obsidianVault: string | null
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
const KEY_ALIASES: Record<string, string> = {
|
|
8
|
-
'obsidian.vault': 'obsidian_vault',
|
|
9
|
-
obsidian_vault: 'obsidian_vault',
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function parseToml(source: string): Record<string, string> {
|
|
13
|
-
const config: Record<string, string> = {}
|
|
14
|
-
for (const line of source.split('\n')) {
|
|
15
|
-
const trimmed = line.trim()
|
|
16
|
-
if (trimmed === '' || trimmed.startsWith('#') || trimmed.startsWith('[')) continue
|
|
17
|
-
const match = trimmed.match(/^([\w.]+)\s*=\s*(.*)$/)
|
|
18
|
-
if (!match) continue
|
|
19
|
-
const key = match[1]!
|
|
20
|
-
let value = match[2]!.trim()
|
|
21
|
-
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
22
|
-
value = value.slice(1, -1)
|
|
23
|
-
}
|
|
24
|
-
config[key] = value
|
|
25
|
-
}
|
|
26
|
-
return config
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function readConfig(configFile: string): LlmIwikiConfig {
|
|
30
|
-
if (!existsSync(configFile)) return { obsidianVault: null }
|
|
31
|
-
const raw = parseToml(readFileSync(configFile, 'utf8'))
|
|
32
|
-
const vault = raw.obsidian_vault
|
|
33
|
-
return { obsidianVault: vault && vault.trim() !== '' ? vault : null }
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function setConfigValue(configFile: string, key: string, value: string): string {
|
|
37
|
-
const normalizedKey = KEY_ALIASES[key]
|
|
38
|
-
if (!normalizedKey) {
|
|
39
|
-
throw new Error(`Unknown config key: ${key}. Supported: obsidian.vault`)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const existing = existsSync(configFile) ? readFileSync(configFile, 'utf8') : ''
|
|
43
|
-
const lines = existing.split('\n')
|
|
44
|
-
const line = `${normalizedKey} = "${value}"`
|
|
45
|
-
let replaced = false
|
|
46
|
-
const next = lines.map((entry) => {
|
|
47
|
-
if (entry.trim().startsWith(`${normalizedKey} `) || entry.trim().startsWith(`${normalizedKey}=`)) {
|
|
48
|
-
replaced = true
|
|
49
|
-
return line
|
|
50
|
-
}
|
|
51
|
-
return entry
|
|
52
|
-
})
|
|
53
|
-
if (!replaced) next.push(line)
|
|
54
|
-
|
|
55
|
-
writeFileSync(configFile, `${next.filter((entry, index) => !(entry === '' && index === next.length - 1)).join('\n')}\n`)
|
|
56
|
-
return normalizedKey
|
|
57
|
-
}
|
package/src/db.ts
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import { Database } from 'bun:sqlite'
|
|
2
|
-
import { mkdirSync } from 'node:fs'
|
|
3
|
-
import { dirname } from 'node:path'
|
|
4
|
-
|
|
5
|
-
export type LlmIwikiDatabase = Database
|
|
6
|
-
|
|
7
|
-
export function openDatabase(databaseFile: string): LlmIwikiDatabase {
|
|
8
|
-
mkdirSync(dirname(databaseFile), { recursive: true })
|
|
9
|
-
return new Database(databaseFile)
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function runMigrations(db: LlmIwikiDatabase): void {
|
|
13
|
-
db.exec(`
|
|
14
|
-
PRAGMA journal_mode = WAL;
|
|
15
|
-
|
|
16
|
-
CREATE TABLE IF NOT EXISTS projects (
|
|
17
|
-
id TEXT PRIMARY KEY,
|
|
18
|
-
canonical_name TEXT NOT NULL,
|
|
19
|
-
display_name TEXT,
|
|
20
|
-
slug TEXT NOT NULL,
|
|
21
|
-
canonical_repo_url TEXT,
|
|
22
|
-
provider TEXT,
|
|
23
|
-
identity_source TEXT NOT NULL,
|
|
24
|
-
created_at TEXT NOT NULL,
|
|
25
|
-
updated_at TEXT NOT NULL
|
|
26
|
-
);
|
|
27
|
-
|
|
28
|
-
CREATE TABLE IF NOT EXISTS project_checkouts (
|
|
29
|
-
id TEXT PRIMARY KEY,
|
|
30
|
-
project_id TEXT NOT NULL,
|
|
31
|
-
local_path TEXT NOT NULL,
|
|
32
|
-
git_root TEXT,
|
|
33
|
-
remote_url TEXT,
|
|
34
|
-
canonical_remote_url TEXT,
|
|
35
|
-
current_branch TEXT,
|
|
36
|
-
first_seen_at TEXT NOT NULL,
|
|
37
|
-
last_seen_at TEXT NOT NULL
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
CREATE TABLE IF NOT EXISTS project_aliases (
|
|
41
|
-
id TEXT PRIMARY KEY,
|
|
42
|
-
project_id TEXT NOT NULL,
|
|
43
|
-
alias_type TEXT NOT NULL,
|
|
44
|
-
alias_value TEXT NOT NULL
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
CREATE TABLE IF NOT EXISTS sources (
|
|
48
|
-
id TEXT PRIMARY KEY,
|
|
49
|
-
name TEXT NOT NULL,
|
|
50
|
-
enabled INTEGER NOT NULL,
|
|
51
|
-
scan_paths TEXT,
|
|
52
|
-
config_json TEXT,
|
|
53
|
-
last_sync_at TEXT
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
CREATE TABLE IF NOT EXISTS sessions (
|
|
57
|
-
id TEXT PRIMARY KEY,
|
|
58
|
-
source_id TEXT NOT NULL,
|
|
59
|
-
source_session_id TEXT NOT NULL,
|
|
60
|
-
project_id TEXT,
|
|
61
|
-
checkout_id TEXT,
|
|
62
|
-
raw_project_path TEXT,
|
|
63
|
-
raw_path TEXT,
|
|
64
|
-
title TEXT,
|
|
65
|
-
message_count INTEGER NOT NULL,
|
|
66
|
-
content_hash TEXT NOT NULL,
|
|
67
|
-
status TEXT NOT NULL,
|
|
68
|
-
created_at TEXT,
|
|
69
|
-
updated_at TEXT,
|
|
70
|
-
first_seen_at TEXT NOT NULL,
|
|
71
|
-
last_seen_at TEXT NOT NULL,
|
|
72
|
-
UNIQUE(source_id, source_session_id, raw_path)
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions (project_id);
|
|
76
|
-
|
|
77
|
-
CREATE TABLE IF NOT EXISTS messages (
|
|
78
|
-
id TEXT PRIMARY KEY,
|
|
79
|
-
session_id TEXT NOT NULL,
|
|
80
|
-
role TEXT NOT NULL,
|
|
81
|
-
content TEXT NOT NULL,
|
|
82
|
-
timestamp TEXT,
|
|
83
|
-
tokens_in INTEGER DEFAULT 0,
|
|
84
|
-
tokens_out INTEGER DEFAULT 0,
|
|
85
|
-
seq_order INTEGER NOT NULL,
|
|
86
|
-
content_hash TEXT NOT NULL
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages (session_id);
|
|
90
|
-
|
|
91
|
-
CREATE TABLE IF NOT EXISTS session_summaries (
|
|
92
|
-
id TEXT PRIMARY KEY,
|
|
93
|
-
session_id TEXT NOT NULL,
|
|
94
|
-
project_id TEXT NOT NULL,
|
|
95
|
-
title TEXT NOT NULL,
|
|
96
|
-
value TEXT NOT NULL,
|
|
97
|
-
summary_markdown TEXT NOT NULL,
|
|
98
|
-
metadata_json TEXT,
|
|
99
|
-
created_at TEXT NOT NULL,
|
|
100
|
-
updated_at TEXT NOT NULL
|
|
101
|
-
);
|
|
102
|
-
|
|
103
|
-
CREATE TABLE IF NOT EXISTS experience_candidates (
|
|
104
|
-
id TEXT PRIMARY KEY,
|
|
105
|
-
project_id TEXT NOT NULL,
|
|
106
|
-
proposed_title TEXT NOT NULL,
|
|
107
|
-
proposed_slug TEXT NOT NULL,
|
|
108
|
-
proposed_body_markdown TEXT NOT NULL,
|
|
109
|
-
source_sessions_json TEXT NOT NULL,
|
|
110
|
-
confidence TEXT,
|
|
111
|
-
status TEXT NOT NULL,
|
|
112
|
-
created_at TEXT NOT NULL
|
|
113
|
-
);
|
|
114
|
-
|
|
115
|
-
CREATE TABLE IF NOT EXISTS experiences (
|
|
116
|
-
id TEXT PRIMARY KEY,
|
|
117
|
-
project_id TEXT,
|
|
118
|
-
title TEXT NOT NULL,
|
|
119
|
-
slug TEXT NOT NULL,
|
|
120
|
-
problem_type TEXT,
|
|
121
|
-
solution_type TEXT,
|
|
122
|
-
tech_stack_json TEXT,
|
|
123
|
-
summary TEXT,
|
|
124
|
-
body_markdown TEXT NOT NULL,
|
|
125
|
-
confidence TEXT,
|
|
126
|
-
status TEXT NOT NULL,
|
|
127
|
-
created_at TEXT NOT NULL,
|
|
128
|
-
updated_at TEXT NOT NULL,
|
|
129
|
-
UNIQUE(project_id, slug)
|
|
130
|
-
);
|
|
131
|
-
|
|
132
|
-
CREATE TABLE IF NOT EXISTS session_experience_links (
|
|
133
|
-
session_id TEXT NOT NULL,
|
|
134
|
-
experience_id TEXT NOT NULL,
|
|
135
|
-
relation TEXT NOT NULL,
|
|
136
|
-
PRIMARY KEY (session_id, experience_id)
|
|
137
|
-
);
|
|
138
|
-
|
|
139
|
-
CREATE TABLE IF NOT EXISTS obsidian_notes (
|
|
140
|
-
id TEXT PRIMARY KEY,
|
|
141
|
-
note_type TEXT NOT NULL,
|
|
142
|
-
entity_id TEXT NOT NULL,
|
|
143
|
-
file_path TEXT NOT NULL,
|
|
144
|
-
managed_hash TEXT,
|
|
145
|
-
frontmatter_hash TEXT,
|
|
146
|
-
last_exported_at TEXT,
|
|
147
|
-
last_seen_mtime TEXT,
|
|
148
|
-
conflict_status TEXT NOT NULL,
|
|
149
|
-
UNIQUE(note_type, entity_id)
|
|
150
|
-
);
|
|
151
|
-
`)
|
|
152
|
-
}
|