@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/experiences.ts
DELETED
|
@@ -1,235 +0,0 @@
|
|
|
1
|
-
import type { ParsedExperiencesYaml } from './types'
|
|
2
|
-
import type { LlmIwikiDatabase } from './db'
|
|
3
|
-
import { slugifyProjectName } from './projects'
|
|
4
|
-
|
|
5
|
-
export type ExperienceScope = 'changed-summaries' | 'all-recent'
|
|
6
|
-
|
|
7
|
-
export interface PrepareExperiencesResult {
|
|
8
|
-
markdown: string
|
|
9
|
-
summaryCount: number
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
interface SummaryRow {
|
|
13
|
-
session_id: string
|
|
14
|
-
title: string
|
|
15
|
-
value: string
|
|
16
|
-
summary_markdown: string
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const EXPERIENCES_FORMAT = `## 输出格式
|
|
20
|
-
|
|
21
|
-
基于下面这些会话摘要,提炼出可复用的项目经验,生成 \`experiences.yaml\` 写入
|
|
22
|
-
\`.llm-iwiki/tasks/experiences.yaml\`,再运行 \`llm-iwiki experiences propose\`。
|
|
23
|
-
|
|
24
|
-
\`\`\`yaml
|
|
25
|
-
project_id: <见下方 project_id>
|
|
26
|
-
experiences:
|
|
27
|
-
- title: <经验标题>
|
|
28
|
-
slug: <可选,稳定短标识>
|
|
29
|
-
summary: |
|
|
30
|
-
一句话说明这条经验。
|
|
31
|
-
body_markdown: |
|
|
32
|
-
## 背景
|
|
33
|
-
## 方案
|
|
34
|
-
## 结论
|
|
35
|
-
source_sessions:
|
|
36
|
-
- <相关 session_id>
|
|
37
|
-
confidence: low | medium | high
|
|
38
|
-
\`\`\`
|
|
39
|
-
|
|
40
|
-
要求:
|
|
41
|
-
- 一条经验可以聚合多个会话,按主题归纳,不要逐会话复述。
|
|
42
|
-
- \`source_sessions\` 填写该经验来自哪些 session_id。`
|
|
43
|
-
|
|
44
|
-
function fetchSummaries(db: LlmIwikiDatabase, projectId: string, scope: ExperienceScope): SummaryRow[] {
|
|
45
|
-
const valueClause = scope === 'changed-summaries' ? "AND value IN ('medium', 'high')" : ''
|
|
46
|
-
return db
|
|
47
|
-
.query<SummaryRow, [string]>(`
|
|
48
|
-
SELECT session_id, title, value, summary_markdown
|
|
49
|
-
FROM session_summaries
|
|
50
|
-
WHERE project_id = ? ${valueClause}
|
|
51
|
-
ORDER BY updated_at DESC
|
|
52
|
-
`)
|
|
53
|
-
.all(projectId)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function prepareExperiencesTask(
|
|
57
|
-
db: LlmIwikiDatabase,
|
|
58
|
-
projectId: string,
|
|
59
|
-
scope: ExperienceScope,
|
|
60
|
-
): PrepareExperiencesResult {
|
|
61
|
-
const summaries = fetchSummaries(db, projectId, scope)
|
|
62
|
-
|
|
63
|
-
const blocks = summaries.map((summary) =>
|
|
64
|
-
[`### ${summary.session_id} — ${summary.title}`, `- value: ${summary.value}`, '', summary.summary_markdown].join('\n'),
|
|
65
|
-
)
|
|
66
|
-
|
|
67
|
-
const markdown = [
|
|
68
|
-
`# Experiences Task`,
|
|
69
|
-
'',
|
|
70
|
-
`project_id: ${projectId}`,
|
|
71
|
-
`scope: ${scope}`,
|
|
72
|
-
`summaries: ${blocks.length}`,
|
|
73
|
-
'',
|
|
74
|
-
EXPERIENCES_FORMAT,
|
|
75
|
-
'',
|
|
76
|
-
'---',
|
|
77
|
-
'',
|
|
78
|
-
blocks.join('\n\n---\n\n'),
|
|
79
|
-
'',
|
|
80
|
-
].join('\n')
|
|
81
|
-
|
|
82
|
-
return { markdown, summaryCount: blocks.length }
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export interface ProposeExperiencesResult {
|
|
86
|
-
written: number
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function hash(value: string): string {
|
|
90
|
-
return Bun.hash(value).toString(16)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function proposeExperiences(db: LlmIwikiDatabase, parsed: ParsedExperiencesYaml): ProposeExperiencesResult {
|
|
94
|
-
const now = new Date().toISOString()
|
|
95
|
-
let written = 0
|
|
96
|
-
|
|
97
|
-
const propose = db.transaction(() => {
|
|
98
|
-
for (const experience of parsed.experiences) {
|
|
99
|
-
const slug = experience.slug && experience.slug.trim() !== '' ? experience.slug : slugifyProjectName(experience.title)
|
|
100
|
-
const id = `cand_${hash(`${parsed.projectId}\u0000${slug}`)}`
|
|
101
|
-
|
|
102
|
-
db.query(`
|
|
103
|
-
INSERT INTO experience_candidates (
|
|
104
|
-
id, project_id, proposed_title, proposed_slug, proposed_body_markdown, source_sessions_json, confidence, status, created_at
|
|
105
|
-
) VALUES ($id, $projectId, $title, $slug, $body, $sources, $confidence, 'proposed', $now)
|
|
106
|
-
ON CONFLICT(id) DO UPDATE SET
|
|
107
|
-
proposed_title = excluded.proposed_title,
|
|
108
|
-
proposed_body_markdown = excluded.proposed_body_markdown,
|
|
109
|
-
source_sessions_json = excluded.source_sessions_json,
|
|
110
|
-
confidence = excluded.confidence
|
|
111
|
-
`).run({
|
|
112
|
-
$id: id,
|
|
113
|
-
$projectId: parsed.projectId,
|
|
114
|
-
$title: experience.title,
|
|
115
|
-
$slug: slug,
|
|
116
|
-
$body: experience.bodyMarkdown,
|
|
117
|
-
$sources: JSON.stringify(experience.sourceSessions),
|
|
118
|
-
$confidence: experience.confidence,
|
|
119
|
-
$now: now,
|
|
120
|
-
})
|
|
121
|
-
written += 1
|
|
122
|
-
}
|
|
123
|
-
})
|
|
124
|
-
propose()
|
|
125
|
-
|
|
126
|
-
return { written }
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export interface CandidateRow {
|
|
130
|
-
id: string
|
|
131
|
-
project_id: string
|
|
132
|
-
proposed_title: string
|
|
133
|
-
proposed_slug: string
|
|
134
|
-
confidence: string | null
|
|
135
|
-
status: string
|
|
136
|
-
source_sessions_json: string
|
|
137
|
-
created_at: string
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export function listCandidates(db: LlmIwikiDatabase, projectId: string | null): CandidateRow[] {
|
|
141
|
-
if (projectId) {
|
|
142
|
-
return db
|
|
143
|
-
.query<CandidateRow, [string]>(
|
|
144
|
-
'SELECT id, project_id, proposed_title, proposed_slug, confidence, status, source_sessions_json, created_at FROM experience_candidates WHERE project_id = ? ORDER BY created_at DESC',
|
|
145
|
-
)
|
|
146
|
-
.all(projectId)
|
|
147
|
-
}
|
|
148
|
-
return db
|
|
149
|
-
.query<CandidateRow, []>(
|
|
150
|
-
'SELECT id, project_id, proposed_title, proposed_slug, confidence, status, source_sessions_json, created_at FROM experience_candidates ORDER BY created_at DESC',
|
|
151
|
-
)
|
|
152
|
-
.all()
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export interface AcceptExperienceResult {
|
|
156
|
-
experienceId: string
|
|
157
|
-
slug: string
|
|
158
|
-
linkedSessions: number
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export function acceptExperience(db: LlmIwikiDatabase, candidateId: string): AcceptExperienceResult {
|
|
162
|
-
const candidate = db
|
|
163
|
-
.query<
|
|
164
|
-
{
|
|
165
|
-
project_id: string
|
|
166
|
-
proposed_title: string
|
|
167
|
-
proposed_slug: string
|
|
168
|
-
proposed_body_markdown: string
|
|
169
|
-
confidence: string | null
|
|
170
|
-
source_sessions_json: string
|
|
171
|
-
},
|
|
172
|
-
[string]
|
|
173
|
-
>(
|
|
174
|
-
'SELECT project_id, proposed_title, proposed_slug, proposed_body_markdown, confidence, source_sessions_json FROM experience_candidates WHERE id = ?',
|
|
175
|
-
)
|
|
176
|
-
.get(candidateId)
|
|
177
|
-
|
|
178
|
-
if (!candidate) throw new Error(`Experience candidate not found: ${candidateId}`)
|
|
179
|
-
|
|
180
|
-
let sourceSessions: string[] = []
|
|
181
|
-
try {
|
|
182
|
-
const parsed = JSON.parse(candidate.source_sessions_json) as unknown
|
|
183
|
-
if (Array.isArray(parsed)) sourceSessions = parsed.filter((value): value is string => typeof value === 'string')
|
|
184
|
-
} catch {
|
|
185
|
-
sourceSessions = []
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const now = new Date().toISOString()
|
|
189
|
-
const experienceId = `exp_${hash(`${candidate.project_id}\u0000${candidate.proposed_slug}`)}`
|
|
190
|
-
|
|
191
|
-
const accept = db.transaction(() => {
|
|
192
|
-
db.query(`
|
|
193
|
-
INSERT INTO experiences (
|
|
194
|
-
id, project_id, title, slug, body_markdown, confidence, status, created_at, updated_at
|
|
195
|
-
) VALUES ($id, $projectId, $title, $slug, $body, $confidence, 'accepted', $now, $now)
|
|
196
|
-
ON CONFLICT(project_id, slug) DO UPDATE SET
|
|
197
|
-
title = excluded.title,
|
|
198
|
-
body_markdown = excluded.body_markdown,
|
|
199
|
-
confidence = excluded.confidence,
|
|
200
|
-
status = 'accepted',
|
|
201
|
-
updated_at = excluded.updated_at
|
|
202
|
-
`).run({
|
|
203
|
-
$id: experienceId,
|
|
204
|
-
$projectId: candidate.project_id,
|
|
205
|
-
$title: candidate.proposed_title,
|
|
206
|
-
$slug: candidate.proposed_slug,
|
|
207
|
-
$body: candidate.proposed_body_markdown,
|
|
208
|
-
$confidence: candidate.confidence,
|
|
209
|
-
$now: now,
|
|
210
|
-
})
|
|
211
|
-
|
|
212
|
-
const resolved = db
|
|
213
|
-
.query<{ id: string }, [string, string]>('SELECT id FROM experiences WHERE project_id = ? AND slug = ?')
|
|
214
|
-
.get(candidate.project_id, candidate.proposed_slug)
|
|
215
|
-
const finalId = resolved?.id ?? experienceId
|
|
216
|
-
|
|
217
|
-
for (const sessionId of sourceSessions) {
|
|
218
|
-
db.query(`
|
|
219
|
-
INSERT INTO session_experience_links (session_id, experience_id, relation)
|
|
220
|
-
VALUES (?, ?, 'source')
|
|
221
|
-
ON CONFLICT(session_id, experience_id) DO NOTHING
|
|
222
|
-
`).run(sessionId, finalId)
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
db.query("UPDATE experience_candidates SET status = 'accepted' WHERE id = ?").run(candidateId)
|
|
226
|
-
})
|
|
227
|
-
accept()
|
|
228
|
-
|
|
229
|
-
return { experienceId, slug: candidate.proposed_slug, linkedSessions: sourceSessions.length }
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
export function rejectExperience(db: LlmIwikiDatabase, candidateId: string): void {
|
|
233
|
-
const changes = db.query("UPDATE experience_candidates SET status = 'rejected' WHERE id = ?").run(candidateId)
|
|
234
|
-
if (changes.changes === 0) throw new Error(`Experience candidate not found: ${candidateId}`)
|
|
235
|
-
}
|
package/src/index.ts
DELETED
package/src/obsidian.ts
DELETED
|
@@ -1,345 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
-
import { dirname, join } from 'node:path'
|
|
3
|
-
|
|
4
|
-
import { stringify as stringifyYaml } from 'yaml'
|
|
5
|
-
|
|
6
|
-
import type { LlmIwikiDatabase } from './db'
|
|
7
|
-
import type { ProjectRecord } from './projects'
|
|
8
|
-
|
|
9
|
-
const MANAGED_START = '<!-- aiwiki:managed:start -->'
|
|
10
|
-
const MANAGED_END = '<!-- aiwiki:managed:end -->'
|
|
11
|
-
|
|
12
|
-
export type NoteWriteStatus = 'created' | 'updated' | 'conflict' | 'forced'
|
|
13
|
-
|
|
14
|
-
export interface NoteSpec {
|
|
15
|
-
noteType: string
|
|
16
|
-
entityId: string
|
|
17
|
-
relPath: string
|
|
18
|
-
frontmatter: Record<string, unknown>
|
|
19
|
-
title: string
|
|
20
|
-
managedBody: string
|
|
21
|
-
userSectionHeading?: string
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface ExportOptions {
|
|
25
|
-
force: boolean
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface ExportReport {
|
|
29
|
-
created: number
|
|
30
|
-
updated: number
|
|
31
|
-
forced: number
|
|
32
|
-
conflicts: string[]
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function hash(value: string): string {
|
|
36
|
-
return Bun.hash(value).toString(16)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function sanitizeFileName(name: string): string {
|
|
40
|
-
const cleaned = name
|
|
41
|
-
.replace(/[\\/:*?"<>|]/g, ' ')
|
|
42
|
-
.replace(/\s+/g, ' ')
|
|
43
|
-
.trim()
|
|
44
|
-
.slice(0, 80)
|
|
45
|
-
return cleaned === '' ? 'untitled' : cleaned
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function buildNote(spec: NoteSpec): string {
|
|
49
|
-
const frontmatter = stringifyYaml(spec.frontmatter).trimEnd()
|
|
50
|
-
const userHeading = spec.userSectionHeading ?? '## 我的补充'
|
|
51
|
-
return [
|
|
52
|
-
'---',
|
|
53
|
-
frontmatter,
|
|
54
|
-
'---',
|
|
55
|
-
'',
|
|
56
|
-
`# ${spec.title}`,
|
|
57
|
-
'',
|
|
58
|
-
MANAGED_START,
|
|
59
|
-
spec.managedBody.trim(),
|
|
60
|
-
MANAGED_END,
|
|
61
|
-
'',
|
|
62
|
-
userHeading,
|
|
63
|
-
'',
|
|
64
|
-
'',
|
|
65
|
-
].join('\n')
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
interface ManagedSplit {
|
|
69
|
-
before: string
|
|
70
|
-
managed: string
|
|
71
|
-
after: string
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function splitManaged(content: string): ManagedSplit | null {
|
|
75
|
-
const startIndex = content.indexOf(MANAGED_START)
|
|
76
|
-
const endIndex = content.indexOf(MANAGED_END)
|
|
77
|
-
if (startIndex === -1 || endIndex === -1 || endIndex < startIndex) return null
|
|
78
|
-
return {
|
|
79
|
-
before: content.slice(0, startIndex + MANAGED_START.length),
|
|
80
|
-
managed: content.slice(startIndex + MANAGED_START.length, endIndex).trim(),
|
|
81
|
-
after: content.slice(endIndex),
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function replaceManaged(split: ManagedSplit, managedBody: string): string {
|
|
86
|
-
return `${split.before}\n${managedBody.trim()}\n${split.after}`
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
interface NoteRecord {
|
|
90
|
-
managed_hash: string | null
|
|
91
|
-
file_path: string
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function getNoteRecord(db: LlmIwikiDatabase, noteType: string, entityId: string): NoteRecord | null {
|
|
95
|
-
return db
|
|
96
|
-
.query<NoteRecord, [string, string]>(
|
|
97
|
-
'SELECT managed_hash, file_path FROM obsidian_notes WHERE note_type = ? AND entity_id = ?',
|
|
98
|
-
)
|
|
99
|
-
.get(noteType, entityId)
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function upsertNoteRecord(
|
|
103
|
-
db: LlmIwikiDatabase,
|
|
104
|
-
spec: NoteSpec,
|
|
105
|
-
filePath: string,
|
|
106
|
-
managedHash: string,
|
|
107
|
-
conflictStatus: string,
|
|
108
|
-
now: string,
|
|
109
|
-
): void {
|
|
110
|
-
db.query(`
|
|
111
|
-
INSERT INTO obsidian_notes (id, note_type, entity_id, file_path, managed_hash, frontmatter_hash, last_exported_at, conflict_status)
|
|
112
|
-
VALUES ($id, $noteType, $entityId, $filePath, $managedHash, NULL, $now, $conflictStatus)
|
|
113
|
-
ON CONFLICT(note_type, entity_id) DO UPDATE SET
|
|
114
|
-
file_path = excluded.file_path,
|
|
115
|
-
managed_hash = excluded.managed_hash,
|
|
116
|
-
last_exported_at = excluded.last_exported_at,
|
|
117
|
-
conflict_status = excluded.conflict_status
|
|
118
|
-
`).run({
|
|
119
|
-
$id: `note_${hash(`${spec.noteType}\u0000${spec.entityId}`)}`,
|
|
120
|
-
$noteType: spec.noteType,
|
|
121
|
-
$entityId: spec.entityId,
|
|
122
|
-
$filePath: filePath,
|
|
123
|
-
$managedHash: managedHash,
|
|
124
|
-
$conflictStatus: conflictStatus,
|
|
125
|
-
$now: now,
|
|
126
|
-
})
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export function writeNote(
|
|
130
|
-
db: LlmIwikiDatabase,
|
|
131
|
-
vault: string,
|
|
132
|
-
spec: NoteSpec,
|
|
133
|
-
options: ExportOptions,
|
|
134
|
-
now: string,
|
|
135
|
-
): NoteWriteStatus {
|
|
136
|
-
const filePath = join(vault, spec.relPath)
|
|
137
|
-
const managedHash = hash(spec.managedBody.trim())
|
|
138
|
-
|
|
139
|
-
if (!existsSync(filePath)) {
|
|
140
|
-
mkdirSync(dirname(filePath), { recursive: true })
|
|
141
|
-
writeFileSync(filePath, buildNote(spec))
|
|
142
|
-
upsertNoteRecord(db, spec, filePath, managedHash, 'clean', now)
|
|
143
|
-
return 'created'
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const existing = readFileSync(filePath, 'utf8')
|
|
147
|
-
const split = splitManaged(existing)
|
|
148
|
-
if (!split) {
|
|
149
|
-
upsertNoteRecord(db, spec, filePath, managedHash, 'no_managed_block', now)
|
|
150
|
-
return 'conflict'
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const currentManagedHash = hash(split.managed)
|
|
154
|
-
const record = getNoteRecord(db, spec.noteType, spec.entityId)
|
|
155
|
-
const userEditedManaged = !record || record.managed_hash !== currentManagedHash
|
|
156
|
-
|
|
157
|
-
if (userEditedManaged && !options.force) {
|
|
158
|
-
upsertNoteRecord(db, spec, filePath, record?.managed_hash ?? currentManagedHash, 'managed_conflict', now)
|
|
159
|
-
return 'conflict'
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
writeFileSync(filePath, replaceManaged(split, spec.managedBody))
|
|
163
|
-
upsertNoteRecord(db, spec, filePath, managedHash, 'clean', now)
|
|
164
|
-
return userEditedManaged ? 'forced' : 'updated'
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function projectDirName(project: ProjectRecord): string {
|
|
168
|
-
return sanitizeFileName(project.displayName ?? project.canonicalName ?? project.slug)
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
interface SummaryRow {
|
|
172
|
-
id: string
|
|
173
|
-
session_id: string
|
|
174
|
-
title: string
|
|
175
|
-
value: string
|
|
176
|
-
summary_markdown: string
|
|
177
|
-
updated_at: string | null
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
interface ExperienceRow {
|
|
181
|
-
id: string
|
|
182
|
-
title: string
|
|
183
|
-
slug: string
|
|
184
|
-
body_markdown: string
|
|
185
|
-
confidence: string | null
|
|
186
|
-
status: string
|
|
187
|
-
updated_at: string
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
export function exportProject(
|
|
191
|
-
db: LlmIwikiDatabase,
|
|
192
|
-
vault: string,
|
|
193
|
-
project: ProjectRecord,
|
|
194
|
-
options: ExportOptions,
|
|
195
|
-
): ExportReport {
|
|
196
|
-
const now = new Date().toISOString()
|
|
197
|
-
const report: ExportReport = { created: 0, updated: 0, forced: 0, conflicts: [] }
|
|
198
|
-
const dirName = projectDirName(project)
|
|
199
|
-
const baseRel = join('LLM-iWiki', 'Projects', dirName)
|
|
200
|
-
const displayName = project.displayName ?? project.canonicalName
|
|
201
|
-
|
|
202
|
-
const track = (status: NoteWriteStatus, relPath: string): void => {
|
|
203
|
-
if (status === 'created') report.created += 1
|
|
204
|
-
else if (status === 'updated') report.updated += 1
|
|
205
|
-
else if (status === 'forced') report.forced += 1
|
|
206
|
-
else report.conflicts.push(relPath)
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const summaries = db
|
|
210
|
-
.query<SummaryRow, [string]>(`
|
|
211
|
-
SELECT id, session_id, title, value, summary_markdown, updated_at
|
|
212
|
-
FROM session_summaries WHERE project_id = ? ORDER BY updated_at DESC
|
|
213
|
-
`)
|
|
214
|
-
.all(project.id)
|
|
215
|
-
|
|
216
|
-
for (const summary of summaries) {
|
|
217
|
-
const relPath = join(baseRel, 'Sessions', `${sanitizeFileName(summary.title)}.md`)
|
|
218
|
-
const spec: NoteSpec = {
|
|
219
|
-
noteType: 'session-summary',
|
|
220
|
-
entityId: summary.id,
|
|
221
|
-
relPath,
|
|
222
|
-
title: summary.title,
|
|
223
|
-
managedBody: summary.summary_markdown,
|
|
224
|
-
frontmatter: {
|
|
225
|
-
type: 'session-summary',
|
|
226
|
-
aiwiki_id: summary.id,
|
|
227
|
-
aiwiki_project_id: project.id,
|
|
228
|
-
project: displayName,
|
|
229
|
-
session_id: summary.session_id,
|
|
230
|
-
value: summary.value,
|
|
231
|
-
updated_at: summary.updated_at ?? now,
|
|
232
|
-
},
|
|
233
|
-
}
|
|
234
|
-
track(writeNote(db, vault, spec, options, now), relPath)
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const experiences = db
|
|
238
|
-
.query<ExperienceRow, [string]>(`
|
|
239
|
-
SELECT id, title, slug, body_markdown, confidence, status, updated_at
|
|
240
|
-
FROM experiences WHERE project_id = ? AND status = 'accepted' ORDER BY updated_at DESC
|
|
241
|
-
`)
|
|
242
|
-
.all(project.id)
|
|
243
|
-
|
|
244
|
-
for (const experience of experiences) {
|
|
245
|
-
const relPath = join(baseRel, 'Experiences', `${sanitizeFileName(experience.slug || experience.title)}.md`)
|
|
246
|
-
const sourceSessions = db
|
|
247
|
-
.query<{ session_id: string }, [string]>(
|
|
248
|
-
'SELECT session_id FROM session_experience_links WHERE experience_id = ?',
|
|
249
|
-
)
|
|
250
|
-
.all(experience.id)
|
|
251
|
-
.map((row) => row.session_id)
|
|
252
|
-
const spec: NoteSpec = {
|
|
253
|
-
noteType: 'experience',
|
|
254
|
-
entityId: experience.id,
|
|
255
|
-
relPath,
|
|
256
|
-
title: experience.title,
|
|
257
|
-
managedBody: experience.body_markdown,
|
|
258
|
-
frontmatter: {
|
|
259
|
-
type: 'experience',
|
|
260
|
-
aiwiki_id: experience.id,
|
|
261
|
-
aiwiki_project_id: project.id,
|
|
262
|
-
project: displayName,
|
|
263
|
-
slug: experience.slug,
|
|
264
|
-
confidence: experience.confidence,
|
|
265
|
-
status: experience.status,
|
|
266
|
-
source_sessions: sourceSessions,
|
|
267
|
-
updated_at: experience.updated_at,
|
|
268
|
-
},
|
|
269
|
-
}
|
|
270
|
-
track(writeNote(db, vault, spec, options, now), relPath)
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
const indexRel = join(baseRel, 'Project Summary.md')
|
|
274
|
-
const indexBody = [
|
|
275
|
-
`- canonical: \`${project.canonicalName}\``,
|
|
276
|
-
project.canonicalRepoUrl ? `- repo: ${project.canonicalRepoUrl}` : null,
|
|
277
|
-
`- session summaries: ${summaries.length}`,
|
|
278
|
-
`- experiences: ${experiences.length}`,
|
|
279
|
-
]
|
|
280
|
-
.filter((line): line is string => line != null)
|
|
281
|
-
.join('\n')
|
|
282
|
-
const indexSpec: NoteSpec = {
|
|
283
|
-
noteType: 'project-summary',
|
|
284
|
-
entityId: project.id,
|
|
285
|
-
relPath: indexRel,
|
|
286
|
-
title: displayName,
|
|
287
|
-
managedBody: indexBody,
|
|
288
|
-
frontmatter: {
|
|
289
|
-
type: 'project-summary',
|
|
290
|
-
aiwiki_project_id: project.id,
|
|
291
|
-
project: displayName,
|
|
292
|
-
canonical_repo_url: project.canonicalRepoUrl,
|
|
293
|
-
updated_at: now,
|
|
294
|
-
},
|
|
295
|
-
}
|
|
296
|
-
track(writeNote(db, vault, indexSpec, options, now), indexRel)
|
|
297
|
-
|
|
298
|
-
return report
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
export type CheckStatus = 'clean' | 'drift' | 'missing' | 'no_managed_block'
|
|
302
|
-
|
|
303
|
-
export interface CheckEntry {
|
|
304
|
-
noteType: string
|
|
305
|
-
entityId: string
|
|
306
|
-
filePath: string
|
|
307
|
-
status: CheckStatus
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
export interface CheckReport {
|
|
311
|
-
total: number
|
|
312
|
-
clean: number
|
|
313
|
-
entries: CheckEntry[]
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
export function checkVault(db: LlmIwikiDatabase): CheckReport {
|
|
317
|
-
const rows = db
|
|
318
|
-
.query<{ note_type: string; entity_id: string; file_path: string; managed_hash: string | null }, []>(
|
|
319
|
-
'SELECT note_type, entity_id, file_path, managed_hash FROM obsidian_notes ORDER BY file_path ASC',
|
|
320
|
-
)
|
|
321
|
-
.all()
|
|
322
|
-
|
|
323
|
-
const entries: CheckEntry[] = []
|
|
324
|
-
let clean = 0
|
|
325
|
-
|
|
326
|
-
for (const row of rows) {
|
|
327
|
-
let status: CheckStatus
|
|
328
|
-
if (!existsSync(row.file_path)) {
|
|
329
|
-
status = 'missing'
|
|
330
|
-
} else {
|
|
331
|
-
const split = splitManaged(readFileSync(row.file_path, 'utf8'))
|
|
332
|
-
if (!split) {
|
|
333
|
-
status = 'no_managed_block'
|
|
334
|
-
} else if (row.managed_hash && hash(split.managed) === row.managed_hash) {
|
|
335
|
-
status = 'clean'
|
|
336
|
-
} else {
|
|
337
|
-
status = 'drift'
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
if (status === 'clean') clean += 1
|
|
341
|
-
else entries.push({ noteType: row.note_type, entityId: row.entity_id, filePath: row.file_path, status })
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
return { total: rows.length, clean, entries }
|
|
345
|
-
}
|
package/src/paths.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { homedir } from 'node:os'
|
|
2
|
-
import { join, resolve } from 'node:path'
|
|
3
|
-
|
|
4
|
-
export interface AppPaths {
|
|
5
|
-
homeDir: string
|
|
6
|
-
configDir: string
|
|
7
|
-
configFile: string
|
|
8
|
-
databaseFile: string
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function getAppPaths(homeDir = homedir()): AppPaths {
|
|
12
|
-
const configDir = join(homeDir, '.llm-iwiki')
|
|
13
|
-
return {
|
|
14
|
-
homeDir,
|
|
15
|
-
configDir,
|
|
16
|
-
configFile: join(configDir, 'config.toml'),
|
|
17
|
-
databaseFile: join(configDir, 'llm-iwiki.db'),
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function getProjectTaskDir(cwd: string): string {
|
|
22
|
-
return resolve(cwd, '.llm-iwiki', 'tasks')
|
|
23
|
-
}
|