@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/src/projects.ts DELETED
@@ -1,192 +0,0 @@
1
- import { spawnSync } from 'node:child_process'
2
- import { existsSync } from 'node:fs'
3
- import { basename, resolve } from 'node:path'
4
-
5
- import type { LlmIwikiDatabase } from './db'
6
-
7
- export interface ProjectRecord {
8
- id: string
9
- canonicalName: string
10
- displayName: string | null
11
- slug: string
12
- canonicalRepoUrl: string | null
13
- identitySource: string
14
- }
15
-
16
- export function canonicalizeRemoteUrl(remoteUrl: string): string {
17
- return remoteUrl
18
- .trim()
19
- .replace(/\/+$/, '')
20
- .replace(/^ssh:\/\/git@([^/]+)\//, '$1/')
21
- .replace(/^git@([^:]+):/, '$1/')
22
- .replace(/^https?:\/\//, '')
23
- .replace(/\.git$/, '')
24
- }
25
-
26
- export function slugifyProjectName(name: string): string {
27
- const ascii = name
28
- .normalize('NFKD')
29
- .replace(/[^\w\s./-]/g, '')
30
- .trim()
31
- .toLowerCase()
32
- .replace(/[\s._/]+/g, '-')
33
- .replace(/-+/g, '-')
34
- .replace(/^-|-$/g, '')
35
-
36
- return ascii || 'project'
37
- }
38
-
39
- function git(cwd: string, args: string[]): string | null {
40
- const result = spawnSync('git', args, { cwd, encoding: 'utf8' })
41
- if (result.status !== 0) return null
42
- return result.stdout.trim() || null
43
- }
44
-
45
- interface ProjectIdentity {
46
- id: string
47
- canonicalName: string
48
- slug: string
49
- canonicalRepoUrl: string | null
50
- identitySource: string
51
- }
52
-
53
- function computeProjectIdentity(localPath: string): ProjectIdentity {
54
- const gitRoot = git(localPath, ['rev-parse', '--show-toplevel'])
55
- const remote = git(localPath, ['config', '--get', 'remote.origin.url'])
56
- const canonicalRepoUrl = remote ? canonicalizeRemoteUrl(remote) : null
57
- const canonicalName = canonicalRepoUrl ?? basename(gitRoot ?? localPath)
58
- const slug = slugifyProjectName(canonicalName)
59
- const id = `proj_${Bun.hash(canonicalRepoUrl ?? gitRoot ?? localPath).toString(16)}`
60
-
61
- return {
62
- id,
63
- canonicalName,
64
- slug,
65
- canonicalRepoUrl,
66
- identitySource: canonicalRepoUrl ? 'git_remote' : 'path',
67
- }
68
- }
69
-
70
- function upsertProjectIdentity(db: LlmIwikiDatabase, identity: ProjectIdentity): ProjectRecord {
71
- const now = new Date().toISOString()
72
- db.query(`
73
- INSERT INTO projects (id, canonical_name, display_name, slug, canonical_repo_url, provider, identity_source, created_at, updated_at)
74
- VALUES ($id, $canonicalName, NULL, $slug, $canonicalRepoUrl, NULL, $identitySource, $now, $now)
75
- ON CONFLICT(id) DO UPDATE SET
76
- canonical_name = excluded.canonical_name,
77
- slug = excluded.slug,
78
- canonical_repo_url = excluded.canonical_repo_url,
79
- identity_source = excluded.identity_source,
80
- updated_at = excluded.updated_at
81
- `).run({
82
- $id: identity.id,
83
- $canonicalName: identity.canonicalName,
84
- $slug: identity.slug,
85
- $canonicalRepoUrl: identity.canonicalRepoUrl,
86
- $identitySource: identity.identitySource,
87
- $now: now,
88
- })
89
-
90
- return getProject(db, identity.id)
91
- }
92
-
93
- export function resolveProject(db: LlmIwikiDatabase, checkoutPath: string): ProjectRecord {
94
- const localPath = resolve(checkoutPath)
95
- if (!existsSync(localPath)) throw new Error(`Path does not exist: ${localPath}`)
96
- return upsertProjectIdentity(db, computeProjectIdentity(localPath))
97
- }
98
-
99
- export function resolveProjectByPath(db: LlmIwikiDatabase, rawPath: string): ProjectRecord {
100
- const localPath = resolve(rawPath)
101
- return upsertProjectIdentity(db, computeProjectIdentity(localPath))
102
- }
103
-
104
- export function renameProject(db: LlmIwikiDatabase, projectId: string, displayName: string): ProjectRecord {
105
- const now = new Date().toISOString()
106
- const changes = db
107
- .query('UPDATE projects SET display_name = $displayName, updated_at = $now WHERE id = $projectId')
108
- .run({
109
- $displayName: displayName,
110
- $projectId: projectId,
111
- $now: now,
112
- })
113
-
114
- if (changes.changes === 0) throw new Error(`Project not found: ${projectId}`)
115
-
116
- return getProject(db, projectId)
117
- }
118
-
119
- export interface ProjectSummaryRow extends ProjectRecord {
120
- sessionCount: number
121
- lastSeenAt: string | null
122
- }
123
-
124
- export function listProjects(db: LlmIwikiDatabase): ProjectSummaryRow[] {
125
- const rows = db
126
- .query<
127
- {
128
- id: string
129
- canonical_name: string
130
- display_name: string | null
131
- slug: string
132
- canonical_repo_url: string | null
133
- identity_source: string
134
- session_count: number
135
- last_seen_at: string | null
136
- },
137
- []
138
- >(`
139
- SELECT
140
- p.id,
141
- p.canonical_name,
142
- p.display_name,
143
- p.slug,
144
- p.canonical_repo_url,
145
- p.identity_source,
146
- COUNT(s.id) AS session_count,
147
- MAX(s.last_seen_at) AS last_seen_at
148
- FROM projects p
149
- LEFT JOIN sessions s ON s.project_id = p.id
150
- GROUP BY p.id
151
- ORDER BY session_count DESC, p.canonical_name ASC
152
- `)
153
- .all()
154
-
155
- return rows.map((row) => ({
156
- id: row.id,
157
- canonicalName: row.canonical_name,
158
- displayName: row.display_name,
159
- slug: row.slug,
160
- canonicalRepoUrl: row.canonical_repo_url,
161
- identitySource: row.identity_source,
162
- sessionCount: row.session_count,
163
- lastSeenAt: row.last_seen_at,
164
- }))
165
- }
166
-
167
- export function getProject(db: LlmIwikiDatabase, projectId: string): ProjectRecord {
168
- const row = db
169
- .query<
170
- {
171
- id: string
172
- canonical_name: string
173
- display_name: string | null
174
- slug: string
175
- canonical_repo_url: string | null
176
- identity_source: string
177
- },
178
- [string]
179
- >('SELECT * FROM projects WHERE id = ?')
180
- .get(projectId)
181
-
182
- if (!row) throw new Error(`Project not found: ${projectId}`)
183
-
184
- return {
185
- id: row.id,
186
- canonicalName: row.canonical_name,
187
- displayName: row.display_name,
188
- slug: row.slug,
189
- canonicalRepoUrl: row.canonical_repo_url,
190
- identitySource: row.identity_source,
191
- }
192
- }
package/src/sessions.ts DELETED
@@ -1,119 +0,0 @@
1
- import type { LlmIwikiDatabase } from './db'
2
-
3
- export interface SessionRow {
4
- id: string
5
- sourceId: string
6
- sourceSessionId: string
7
- title: string | null
8
- messageCount: number
9
- status: string
10
- rawProjectPath: string | null
11
- updatedAt: string | null
12
- lastSeenAt: string
13
- }
14
-
15
- export interface SourceBreakdown {
16
- source: string
17
- sessionCount: number
18
- }
19
-
20
- export interface ProjectInspection {
21
- sources: SourceBreakdown[]
22
- sessions: SessionRow[]
23
- }
24
-
25
- function mapRow(row: {
26
- id: string
27
- source_id: string
28
- source_session_id: string
29
- title: string | null
30
- message_count: number
31
- status: string
32
- raw_project_path: string | null
33
- updated_at: string | null
34
- last_seen_at: string
35
- }): SessionRow {
36
- return {
37
- id: row.id,
38
- sourceId: row.source_id,
39
- sourceSessionId: row.source_session_id,
40
- title: row.title,
41
- messageCount: row.message_count,
42
- status: row.status,
43
- rawProjectPath: row.raw_project_path,
44
- updatedAt: row.updated_at,
45
- lastSeenAt: row.last_seen_at,
46
- }
47
- }
48
-
49
- export function listSessionsByProject(db: LlmIwikiDatabase, projectId: string, limit = 100): SessionRow[] {
50
- return db
51
- .query<Parameters<typeof mapRow>[0], [string, number]>(`
52
- SELECT id, source_id, source_session_id, title, message_count, status, raw_project_path, updated_at, last_seen_at
53
- FROM sessions
54
- WHERE project_id = ?
55
- ORDER BY COALESCE(updated_at, last_seen_at) DESC
56
- LIMIT ?
57
- `)
58
- .all(projectId, limit)
59
- .map(mapRow)
60
- }
61
-
62
- export interface StoredMessage {
63
- role: string
64
- content: string
65
- }
66
-
67
- export function getSessionMessages(db: LlmIwikiDatabase, sessionId: string): StoredMessage[] {
68
- return db
69
- .query<{ role: string; content: string }, [string]>(
70
- 'SELECT role, content FROM messages WHERE session_id = ? ORDER BY seq_order ASC',
71
- )
72
- .all(sessionId)
73
- }
74
-
75
- export function getSession(db: LlmIwikiDatabase, sessionId: string): SessionRow | null {
76
- const row = db
77
- .query<Parameters<typeof mapRow>[0], [string]>(`
78
- SELECT id, source_id, source_session_id, title, message_count, status, raw_project_path, updated_at, last_seen_at
79
- FROM sessions
80
- WHERE id = ?
81
- `)
82
- .get(sessionId)
83
- return row ? mapRow(row) : null
84
- }
85
-
86
- export function listSessionsToSummarize(
87
- db: LlmIwikiDatabase,
88
- projectId: string,
89
- scope: 'changed' | 'all',
90
- ): SessionRow[] {
91
- const statusClause = scope === 'changed' ? "AND status IN ('new', 'changed')" : ''
92
- return db
93
- .query<Parameters<typeof mapRow>[0], [string]>(`
94
- SELECT id, source_id, source_session_id, title, message_count, status, raw_project_path, updated_at, last_seen_at
95
- FROM sessions
96
- WHERE project_id = ? ${statusClause}
97
- ORDER BY COALESCE(updated_at, last_seen_at) DESC
98
- `)
99
- .all(projectId)
100
- .map(mapRow)
101
- }
102
-
103
- export function inspectProject(db: LlmIwikiDatabase, projectId: string): ProjectInspection {
104
- const sources = db
105
- .query<{ source_id: string; session_count: number }, [string]>(`
106
- SELECT source_id, COUNT(*) AS session_count
107
- FROM sessions
108
- WHERE project_id = ?
109
- GROUP BY source_id
110
- ORDER BY session_count DESC
111
- `)
112
- .all(projectId)
113
- .map((row) => ({ source: row.source_id, sessionCount: row.session_count }))
114
-
115
- return {
116
- sources,
117
- sessions: listSessionsByProject(db, projectId),
118
- }
119
- }
package/src/skills.ts DELETED
@@ -1,168 +0,0 @@
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 DELETED
@@ -1,122 +0,0 @@
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
- }