@codehourra/llm-iwiki 0.1.0
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/LICENSE +21 -0
- package/README.md +67 -0
- package/package.json +53 -0
- package/src/ai-yaml.ts +82 -0
- package/src/cli.ts +485 -0
- package/src/collectors/claude-code.ts +120 -0
- package/src/collectors/codebuddy.ts +164 -0
- package/src/collectors/codex.ts +130 -0
- package/src/collectors/cursor.ts +178 -0
- package/src/collectors/gemini.ts +141 -0
- package/src/collectors/index.ts +20 -0
- package/src/collectors/types.ts +22 -0
- package/src/collectors/util.ts +58 -0
- package/src/compaction.ts +63 -0
- package/src/config.ts +57 -0
- package/src/db.ts +152 -0
- package/src/experiences.ts +235 -0
- package/src/index.ts +10 -0
- package/src/obsidian.ts +345 -0
- package/src/paths.ts +23 -0
- package/src/projects.ts +192 -0
- package/src/sessions.ts +119 -0
- package/src/skills.ts +168 -0
- package/src/summarize.ts +122 -0
- package/src/sync.ts +207 -0
- package/src/types.ts +26 -0
package/src/skills.ts
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { dirname, join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
export type SkillTarget = 'codex' | 'claude-code' | 'cursor'
|
|
5
|
+
|
|
6
|
+
export const SKILL_TARGETS = ['codex', 'claude-code', 'cursor'] as const satisfies readonly SkillTarget[]
|
|
7
|
+
|
|
8
|
+
export interface InitSkillsOptions {
|
|
9
|
+
cwd: string
|
|
10
|
+
target: SkillTarget | null
|
|
11
|
+
force: boolean
|
|
12
|
+
dryRun: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface InitSkillsResult {
|
|
16
|
+
written: string[]
|
|
17
|
+
skipped: string[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface SkillTemplate {
|
|
21
|
+
directory: string
|
|
22
|
+
content: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const TARGET_NAMES: Record<SkillTarget, string> = {
|
|
26
|
+
codex: 'Codex',
|
|
27
|
+
'claude-code': 'Claude Code',
|
|
28
|
+
cursor: 'Cursor',
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const SKILL_TEMPLATES: SkillTemplate[] = [
|
|
32
|
+
{
|
|
33
|
+
directory: 'aiwiki-after-session',
|
|
34
|
+
content: `---
|
|
35
|
+
name: aiwiki-after-session
|
|
36
|
+
description: Capture session knowledge into llm-iwiki summaries, experiences, and Obsidian export after project work.
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
# AIWiki After Session
|
|
40
|
+
|
|
41
|
+
Use this skill after a meaningful coding or design session in the current project.
|
|
42
|
+
|
|
43
|
+
## Steps
|
|
44
|
+
|
|
45
|
+
1. Sync the current project:
|
|
46
|
+
\`\`\`bash
|
|
47
|
+
llm-iwiki sync --project .
|
|
48
|
+
\`\`\`
|
|
49
|
+
2. Prepare changed summaries:
|
|
50
|
+
\`\`\`bash
|
|
51
|
+
llm-iwiki summarize prepare changed --project . --out .llm-iwiki/tasks/summaries-task.md
|
|
52
|
+
\`\`\`
|
|
53
|
+
3. Generate \`.llm-iwiki/tasks/summaries.yaml\` from the prepared task.
|
|
54
|
+
4. Apply the summaries:
|
|
55
|
+
\`\`\`bash
|
|
56
|
+
llm-iwiki summarize apply --project . --file .llm-iwiki/tasks/summaries.yaml
|
|
57
|
+
\`\`\`
|
|
58
|
+
5. Prepare experience proposals from changed summaries:
|
|
59
|
+
\`\`\`bash
|
|
60
|
+
llm-iwiki experiences prepare --project . --from changed-summaries --out .llm-iwiki/tasks/experiences-task.md
|
|
61
|
+
\`\`\`
|
|
62
|
+
6. Generate \`.llm-iwiki/tasks/experiences.yaml\` from the prepared task.
|
|
63
|
+
7. Propose the experiences:
|
|
64
|
+
\`\`\`bash
|
|
65
|
+
llm-iwiki experiences propose --project . --file .llm-iwiki/tasks/experiences.yaml
|
|
66
|
+
\`\`\`
|
|
67
|
+
8. Export project knowledge to Obsidian:
|
|
68
|
+
\`\`\`bash
|
|
69
|
+
llm-iwiki obsidian export --project .
|
|
70
|
+
\`\`\`
|
|
71
|
+
`,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
directory: 'aiwiki-before-debug',
|
|
75
|
+
content: `---
|
|
76
|
+
name: aiwiki-before-debug
|
|
77
|
+
description: Search project memory before debugging so related history is visible before changing code.
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
# AIWiki Before Debug
|
|
81
|
+
|
|
82
|
+
Use this skill before investigating a bug, failure, error message, or confusing behavior.
|
|
83
|
+
|
|
84
|
+
## Steps
|
|
85
|
+
|
|
86
|
+
1. Search the full project knowledge index:
|
|
87
|
+
\`\`\`bash
|
|
88
|
+
llm-iwiki search "<error or topic>" --project . --index all
|
|
89
|
+
\`\`\`
|
|
90
|
+
2. Read any related summaries, experiences, or prior decisions before editing code.
|
|
91
|
+
3. Report whether related history was found, and name the most relevant item when it was found.
|
|
92
|
+
`,
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
directory: 'aiwiki-project-retrospective',
|
|
96
|
+
content: `---
|
|
97
|
+
name: aiwiki-project-retrospective
|
|
98
|
+
description: Review recent project knowledge and extract retrospective themes from llm-iwiki.
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
# AIWiki Project Retrospective
|
|
102
|
+
|
|
103
|
+
Use this skill when preparing a project retrospective or looking for recent repeated lessons.
|
|
104
|
+
|
|
105
|
+
## Steps
|
|
106
|
+
|
|
107
|
+
1. Prepare a retrospective from recent project history:
|
|
108
|
+
\`\`\`bash
|
|
109
|
+
llm-iwiki experiences prepare --project . --from all-recent --since 30d --out .llm-iwiki/tasks/retrospective-task.md
|
|
110
|
+
\`\`\`
|
|
111
|
+
2. Review the prepared task for repeated themes, unresolved questions, and process improvements.
|
|
112
|
+
3. Capture any accepted learnings through the normal llm-iwiki experience proposal flow.
|
|
113
|
+
`,
|
|
114
|
+
},
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
function appendTargetGuidance(content: string, target: SkillTarget | null): string {
|
|
118
|
+
if (!target) return content
|
|
119
|
+
|
|
120
|
+
return `${content}
|
|
121
|
+
## Tool Target
|
|
122
|
+
|
|
123
|
+
This skill is initialized for ${TARGET_NAMES[target]}.
|
|
124
|
+
`
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function isFileExistsError(error: unknown): boolean {
|
|
128
|
+
return error instanceof Error && 'code' in error && error.code === 'EEXIST'
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function initSkills(options: InitSkillsOptions): InitSkillsResult {
|
|
132
|
+
const written: string[] = []
|
|
133
|
+
const skipped: string[] = []
|
|
134
|
+
|
|
135
|
+
for (const template of SKILL_TEMPLATES) {
|
|
136
|
+
const filePath = join(options.cwd, '.agents', 'skills', template.directory, 'SKILL.md')
|
|
137
|
+
|
|
138
|
+
if (existsSync(filePath) && !options.force) {
|
|
139
|
+
skipped.push(filePath)
|
|
140
|
+
continue
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
written.push(filePath)
|
|
144
|
+
|
|
145
|
+
if (options.dryRun) {
|
|
146
|
+
continue
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
mkdirSync(dirname(filePath), { recursive: true })
|
|
150
|
+
const content = appendTargetGuidance(template.content, options.target)
|
|
151
|
+
if (options.force) {
|
|
152
|
+
writeFileSync(filePath, content)
|
|
153
|
+
continue
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
writeFileSync(filePath, content, { flag: 'wx' })
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if (!isFileExistsError(error)) {
|
|
160
|
+
throw error
|
|
161
|
+
}
|
|
162
|
+
written.pop()
|
|
163
|
+
skipped.push(filePath)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return { written, skipped }
|
|
168
|
+
}
|
package/src/summarize.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { ParsedSummariesYaml } from './types'
|
|
2
|
+
import type { LlmIwikiDatabase } from './db'
|
|
3
|
+
import { compactTranscript } from './compaction'
|
|
4
|
+
import { getSessionMessages, listSessionsToSummarize } from './sessions'
|
|
5
|
+
|
|
6
|
+
export interface PrepareSummariesResult {
|
|
7
|
+
markdown: string
|
|
8
|
+
sessionCount: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const SUMMARIES_FORMAT = `## 输出格式
|
|
12
|
+
|
|
13
|
+
请阅读下面每个会话的压缩记录,为有价值的会话生成一份 \`summaries.yaml\`,写入
|
|
14
|
+
\`.llm-iwiki/tasks/summaries.yaml\`,再运行 \`llm-iwiki summarize apply\`。
|
|
15
|
+
|
|
16
|
+
\`\`\`yaml
|
|
17
|
+
project_id: <见下方 project_id>
|
|
18
|
+
summaries:
|
|
19
|
+
- session_id: <会话的 session_id,原样照抄>
|
|
20
|
+
title: <一句话标题>
|
|
21
|
+
value: none | low | medium | high
|
|
22
|
+
summary_markdown: |
|
|
23
|
+
用 Markdown 概括这次会话解决了什么问题、关键决策、结论。
|
|
24
|
+
\`\`\`
|
|
25
|
+
|
|
26
|
+
要求:
|
|
27
|
+
- \`value\` 表示这次会话的沉淀价值,\`medium\` / \`high\` 会进入经验提取。
|
|
28
|
+
- \`session_id\` 必须与下面给出的完全一致。
|
|
29
|
+
- 没有价值的会话可以省略,不必每个都写。`
|
|
30
|
+
|
|
31
|
+
export function prepareSummariesTask(
|
|
32
|
+
db: LlmIwikiDatabase,
|
|
33
|
+
projectId: string,
|
|
34
|
+
scope: 'changed' | 'all',
|
|
35
|
+
): PrepareSummariesResult {
|
|
36
|
+
const sessions = listSessionsToSummarize(db, projectId, scope)
|
|
37
|
+
|
|
38
|
+
const blocks: string[] = []
|
|
39
|
+
for (const session of sessions) {
|
|
40
|
+
const messages = getSessionMessages(db, session.id)
|
|
41
|
+
if (messages.length === 0) continue
|
|
42
|
+
const transcript = compactTranscript(messages)
|
|
43
|
+
blocks.push(
|
|
44
|
+
[
|
|
45
|
+
`### ${session.id}`,
|
|
46
|
+
`- source: ${session.sourceId}`,
|
|
47
|
+
`- title: ${session.title ?? '(无标题)'}`,
|
|
48
|
+
`- messages: ${session.messageCount}`,
|
|
49
|
+
'',
|
|
50
|
+
transcript,
|
|
51
|
+
].join('\n'),
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const markdown = [
|
|
56
|
+
`# Summaries Task`,
|
|
57
|
+
'',
|
|
58
|
+
`project_id: ${projectId}`,
|
|
59
|
+
`scope: ${scope}`,
|
|
60
|
+
`sessions: ${blocks.length}`,
|
|
61
|
+
'',
|
|
62
|
+
SUMMARIES_FORMAT,
|
|
63
|
+
'',
|
|
64
|
+
'---',
|
|
65
|
+
'',
|
|
66
|
+
blocks.join('\n\n---\n\n'),
|
|
67
|
+
'',
|
|
68
|
+
].join('\n')
|
|
69
|
+
|
|
70
|
+
return { markdown, sessionCount: blocks.length }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface ApplySummariesResult {
|
|
74
|
+
written: number
|
|
75
|
+
skipped: string[]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function hash(value: string): string {
|
|
79
|
+
return Bun.hash(value).toString(16)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function applySummaries(db: LlmIwikiDatabase, parsed: ParsedSummariesYaml): ApplySummariesResult {
|
|
83
|
+
const now = new Date().toISOString()
|
|
84
|
+
let written = 0
|
|
85
|
+
const skipped: string[] = []
|
|
86
|
+
|
|
87
|
+
const apply = db.transaction(() => {
|
|
88
|
+
for (const summary of parsed.summaries) {
|
|
89
|
+
const session = db
|
|
90
|
+
.query<{ project_id: string | null }, [string]>('SELECT project_id FROM sessions WHERE id = ?')
|
|
91
|
+
.get(summary.sessionId)
|
|
92
|
+
if (!session) {
|
|
93
|
+
skipped.push(summary.sessionId)
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
db.query(`
|
|
98
|
+
INSERT INTO session_summaries (id, session_id, project_id, title, value, summary_markdown, metadata_json, created_at, updated_at)
|
|
99
|
+
VALUES ($id, $sessionId, $projectId, $title, $value, $summaryMarkdown, $metadata, $now, $now)
|
|
100
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
101
|
+
title = excluded.title,
|
|
102
|
+
value = excluded.value,
|
|
103
|
+
summary_markdown = excluded.summary_markdown,
|
|
104
|
+
metadata_json = excluded.metadata_json,
|
|
105
|
+
updated_at = excluded.updated_at
|
|
106
|
+
`).run({
|
|
107
|
+
$id: `sum_${hash(summary.sessionId)}`,
|
|
108
|
+
$sessionId: summary.sessionId,
|
|
109
|
+
$projectId: session.project_id ?? parsed.projectId,
|
|
110
|
+
$title: summary.title,
|
|
111
|
+
$value: summary.value,
|
|
112
|
+
$summaryMarkdown: summary.summaryMarkdown,
|
|
113
|
+
$metadata: JSON.stringify(summary.metadata),
|
|
114
|
+
$now: now,
|
|
115
|
+
})
|
|
116
|
+
written += 1
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
apply()
|
|
120
|
+
|
|
121
|
+
return { written, skipped }
|
|
122
|
+
}
|
package/src/sync.ts
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import type { LlmIwikiDatabase } from './db'
|
|
2
|
+
import { resolveProjectByPath } from './projects'
|
|
3
|
+
import { COLLECTORS, type Collector, type RawSession } from './collectors'
|
|
4
|
+
|
|
5
|
+
export interface SourceSyncReport {
|
|
6
|
+
source: string
|
|
7
|
+
new: number
|
|
8
|
+
changed: number
|
|
9
|
+
unchanged: number
|
|
10
|
+
sourceMissing: number
|
|
11
|
+
total: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SyncReport {
|
|
15
|
+
bySource: SourceSyncReport[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface SyncOptions {
|
|
19
|
+
homeDir: string
|
|
20
|
+
projectFilter?: string | null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function hash(value: string): string {
|
|
24
|
+
return Bun.hash(value).toString(16)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function sessionPrimaryId(sourceId: string, session: RawSession): string {
|
|
28
|
+
return `ses_${hash(`${sourceId}\u0000${session.sourceSessionId}\u0000${session.rawPath}`)}`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function messageContentHash(role: string, content: string): string {
|
|
32
|
+
return hash(`${role}\u0000${content}`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function sessionContentHash(messageHashes: string[]): string {
|
|
36
|
+
return hash(`${messageHashes.length}\u0000${messageHashes.join('\u0000')}`)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function upsertSource(db: LlmIwikiDatabase, collector: Collector, now: string): void {
|
|
40
|
+
db.query(`
|
|
41
|
+
INSERT INTO sources (id, name, enabled, scan_paths, config_json, last_sync_at)
|
|
42
|
+
VALUES ($id, $name, 1, NULL, NULL, $now)
|
|
43
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
44
|
+
name = excluded.name,
|
|
45
|
+
last_sync_at = excluded.last_sync_at
|
|
46
|
+
`).run({ $id: collector.id, $name: collector.name, $now: now })
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function resolveProjectId(db: LlmIwikiDatabase, rawProjectPath: string | null): string | null {
|
|
50
|
+
if (!rawProjectPath) return null
|
|
51
|
+
try {
|
|
52
|
+
return resolveProjectByPath(db, rawProjectPath).id
|
|
53
|
+
} catch {
|
|
54
|
+
return null
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function writeSession(
|
|
59
|
+
db: LlmIwikiDatabase,
|
|
60
|
+
collector: Collector,
|
|
61
|
+
session: RawSession,
|
|
62
|
+
now: string,
|
|
63
|
+
report: SourceSyncReport,
|
|
64
|
+
): string {
|
|
65
|
+
const id = sessionPrimaryId(collector.id, session)
|
|
66
|
+
const messageHashes = session.messages.map((message) => messageContentHash(message.role, message.content))
|
|
67
|
+
const contentHash = sessionContentHash(messageHashes)
|
|
68
|
+
const projectId = resolveProjectId(db, session.rawProjectPath)
|
|
69
|
+
|
|
70
|
+
const existing = db
|
|
71
|
+
.query<{ content_hash: string; first_seen_at: string }, [string]>(
|
|
72
|
+
'SELECT content_hash, first_seen_at FROM sessions WHERE id = ?',
|
|
73
|
+
)
|
|
74
|
+
.get(id)
|
|
75
|
+
|
|
76
|
+
if (existing && existing.content_hash === contentHash) {
|
|
77
|
+
db.query('UPDATE sessions SET project_id = $projectId, status = $status, last_seen_at = $now WHERE id = $id').run({
|
|
78
|
+
$projectId: projectId,
|
|
79
|
+
$status: 'unchanged',
|
|
80
|
+
$now: now,
|
|
81
|
+
$id: id,
|
|
82
|
+
})
|
|
83
|
+
report.unchanged += 1
|
|
84
|
+
return id
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const status = existing ? 'changed' : 'new'
|
|
88
|
+
const firstSeenAt = existing?.first_seen_at ?? now
|
|
89
|
+
|
|
90
|
+
db.query(`
|
|
91
|
+
INSERT INTO sessions (
|
|
92
|
+
id, source_id, source_session_id, project_id, checkout_id, raw_project_path, raw_path,
|
|
93
|
+
title, message_count, content_hash, status, created_at, updated_at, first_seen_at, last_seen_at
|
|
94
|
+
) VALUES (
|
|
95
|
+
$id, $sourceId, $sourceSessionId, $projectId, NULL, $rawProjectPath, $rawPath,
|
|
96
|
+
$title, $messageCount, $contentHash, $status, $createdAt, $updatedAt, $firstSeenAt, $now
|
|
97
|
+
)
|
|
98
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
99
|
+
project_id = excluded.project_id,
|
|
100
|
+
raw_project_path = excluded.raw_project_path,
|
|
101
|
+
title = excluded.title,
|
|
102
|
+
message_count = excluded.message_count,
|
|
103
|
+
content_hash = excluded.content_hash,
|
|
104
|
+
status = excluded.status,
|
|
105
|
+
created_at = excluded.created_at,
|
|
106
|
+
updated_at = excluded.updated_at,
|
|
107
|
+
last_seen_at = excluded.last_seen_at
|
|
108
|
+
`).run({
|
|
109
|
+
$id: id,
|
|
110
|
+
$sourceId: collector.id,
|
|
111
|
+
$sourceSessionId: session.sourceSessionId,
|
|
112
|
+
$projectId: projectId,
|
|
113
|
+
$rawProjectPath: session.rawProjectPath,
|
|
114
|
+
$rawPath: session.rawPath,
|
|
115
|
+
$title: session.title,
|
|
116
|
+
$messageCount: session.messages.length,
|
|
117
|
+
$contentHash: contentHash,
|
|
118
|
+
$status: status,
|
|
119
|
+
$createdAt: session.createdAt,
|
|
120
|
+
$updatedAt: session.updatedAt,
|
|
121
|
+
$firstSeenAt: firstSeenAt,
|
|
122
|
+
$now: now,
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
db.query('DELETE FROM messages WHERE session_id = ?').run(id)
|
|
126
|
+
session.messages.forEach((message, index) => {
|
|
127
|
+
db.query(`
|
|
128
|
+
INSERT INTO messages (id, session_id, role, content, timestamp, seq_order, content_hash)
|
|
129
|
+
VALUES ($id, $sessionId, $role, $content, $timestamp, $seqOrder, $contentHash)
|
|
130
|
+
`).run({
|
|
131
|
+
$id: `msg_${id}_${index}`,
|
|
132
|
+
$sessionId: id,
|
|
133
|
+
$role: message.role,
|
|
134
|
+
$content: message.content,
|
|
135
|
+
$timestamp: message.timestamp,
|
|
136
|
+
$seqOrder: index,
|
|
137
|
+
$contentHash: messageHashes[index]!,
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
if (status === 'new') report.new += 1
|
|
142
|
+
else report.changed += 1
|
|
143
|
+
return id
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function markMissingSessions(
|
|
147
|
+
db: LlmIwikiDatabase,
|
|
148
|
+
sourceId: string,
|
|
149
|
+
seenIds: Set<string>,
|
|
150
|
+
now: string,
|
|
151
|
+
report: SourceSyncReport,
|
|
152
|
+
): void {
|
|
153
|
+
const rows = db
|
|
154
|
+
.query<{ id: string }, [string]>(
|
|
155
|
+
"SELECT id FROM sessions WHERE source_id = ? AND status != 'source_missing'",
|
|
156
|
+
)
|
|
157
|
+
.all(sourceId)
|
|
158
|
+
|
|
159
|
+
for (const row of rows) {
|
|
160
|
+
if (seenIds.has(row.id)) continue
|
|
161
|
+
db.query('UPDATE sessions SET status = $status, last_seen_at = $now WHERE id = $id').run({
|
|
162
|
+
$status: 'source_missing',
|
|
163
|
+
$now: now,
|
|
164
|
+
$id: row.id,
|
|
165
|
+
})
|
|
166
|
+
report.sourceMissing += 1
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function runSync(db: LlmIwikiDatabase, options: SyncOptions): SyncReport {
|
|
171
|
+
const now = new Date().toISOString()
|
|
172
|
+
const bySource: SourceSyncReport[] = []
|
|
173
|
+
|
|
174
|
+
for (const collector of COLLECTORS) {
|
|
175
|
+
if (!collector.detect(options.homeDir)) continue
|
|
176
|
+
|
|
177
|
+
upsertSource(db, collector, now)
|
|
178
|
+
const report: SourceSyncReport = {
|
|
179
|
+
source: collector.id,
|
|
180
|
+
new: 0,
|
|
181
|
+
changed: 0,
|
|
182
|
+
unchanged: 0,
|
|
183
|
+
sourceMissing: 0,
|
|
184
|
+
total: 0,
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const sessions = collector.collect(options.homeDir)
|
|
188
|
+
const seenIds = new Set<string>()
|
|
189
|
+
|
|
190
|
+
const writeAll = db.transaction(() => {
|
|
191
|
+
for (const session of sessions) {
|
|
192
|
+
if (options.projectFilter && session.rawProjectPath !== options.projectFilter) continue
|
|
193
|
+
const id = writeSession(db, collector, session, now, report)
|
|
194
|
+
seenIds.add(id)
|
|
195
|
+
report.total += 1
|
|
196
|
+
}
|
|
197
|
+
if (!options.projectFilter) {
|
|
198
|
+
markMissingSessions(db, collector.id, seenIds, now, report)
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
writeAll()
|
|
202
|
+
|
|
203
|
+
bySource.push(report)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return { bySource }
|
|
207
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type SummaryValue = 'none' | 'low' | 'medium' | 'high'
|
|
2
|
+
export type Confidence = 'low' | 'medium' | 'high'
|
|
3
|
+
|
|
4
|
+
export interface ParsedSummariesYaml {
|
|
5
|
+
projectId: string
|
|
6
|
+
summaries: Array<{
|
|
7
|
+
sessionId: string
|
|
8
|
+
title: string
|
|
9
|
+
value: SummaryValue
|
|
10
|
+
summaryMarkdown: string
|
|
11
|
+
metadata: Record<string, unknown>
|
|
12
|
+
}>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ParsedExperiencesYaml {
|
|
16
|
+
projectId: string
|
|
17
|
+
experiences: Array<{
|
|
18
|
+
title: string
|
|
19
|
+
slug: string | null
|
|
20
|
+
summary: string
|
|
21
|
+
bodyMarkdown: string
|
|
22
|
+
sourceSessions: string[]
|
|
23
|
+
confidence: Confidence | null
|
|
24
|
+
metadata: Record<string, unknown>
|
|
25
|
+
}>
|
|
26
|
+
}
|