@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/cli.ts
ADDED
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { parseExperiencesYaml, parseSummariesYaml } from './ai-yaml'
|
|
5
|
+
import { readConfig, setConfigValue } from './config'
|
|
6
|
+
import { openDatabase, runMigrations } from './db'
|
|
7
|
+
import {
|
|
8
|
+
acceptExperience,
|
|
9
|
+
type ExperienceScope,
|
|
10
|
+
listCandidates,
|
|
11
|
+
prepareExperiencesTask,
|
|
12
|
+
proposeExperiences,
|
|
13
|
+
rejectExperience,
|
|
14
|
+
} from './experiences'
|
|
15
|
+
import { checkVault, exportProject } from './obsidian'
|
|
16
|
+
import { getAppPaths, getProjectTaskDir } from './paths'
|
|
17
|
+
import { getProject, listProjects, renameProject, resolveProject } from './projects'
|
|
18
|
+
import { inspectProject } from './sessions'
|
|
19
|
+
import { initSkills, SKILL_TARGETS, type SkillTarget } from './skills'
|
|
20
|
+
import { applySummaries, prepareSummariesTask } from './summarize'
|
|
21
|
+
import { runSync } from './sync'
|
|
22
|
+
|
|
23
|
+
export interface CliRuntime {
|
|
24
|
+
cwd: string
|
|
25
|
+
homeDir?: string
|
|
26
|
+
stdout: (message: string) => void
|
|
27
|
+
stderr: (message: string) => void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const HELP = `llm-iwiki
|
|
31
|
+
|
|
32
|
+
Usage:
|
|
33
|
+
llm-iwiki init
|
|
34
|
+
llm-iwiki doctor
|
|
35
|
+
llm-iwiki sync [--project <path>]
|
|
36
|
+
llm-iwiki projects list
|
|
37
|
+
llm-iwiki projects resolve <path>
|
|
38
|
+
llm-iwiki projects inspect <path-or-project-id>
|
|
39
|
+
llm-iwiki projects rename <path-or-project-id> <display-name>
|
|
40
|
+
llm-iwiki summarize prepare [changed|all] --project <path> [--out <file>]
|
|
41
|
+
llm-iwiki summarize apply --project <path> --file <summaries.yaml>
|
|
42
|
+
llm-iwiki experiences prepare --project <path> [--from changed-summaries|all-recent] [--out <file>]
|
|
43
|
+
llm-iwiki experiences propose --project <path> --file <experiences.yaml>
|
|
44
|
+
llm-iwiki experiences candidates [--project <path>]
|
|
45
|
+
llm-iwiki experiences accept <candidate-id>
|
|
46
|
+
llm-iwiki experiences reject <candidate-id>
|
|
47
|
+
llm-iwiki obsidian export [--project <path>] [--vault <dir>] [--force]
|
|
48
|
+
llm-iwiki obsidian check
|
|
49
|
+
llm-iwiki config show
|
|
50
|
+
llm-iwiki config set <key> <value>
|
|
51
|
+
llm-iwiki skills init [--target codex|claude-code|cursor] [--force] [--dry-run]
|
|
52
|
+
`
|
|
53
|
+
|
|
54
|
+
function resolveCliPath(cwd: string, targetPath: string): string {
|
|
55
|
+
return isAbsolute(targetPath) ? targetPath : resolve(cwd, targetPath)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readFlag(args: string[], name: string): string | null {
|
|
59
|
+
const index = args.indexOf(name)
|
|
60
|
+
if (index === -1) return null
|
|
61
|
+
const value = args[index + 1]
|
|
62
|
+
if (!value || value.startsWith('--')) return null
|
|
63
|
+
return value
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function runCli(args: string[], runtime: CliRuntime): Promise<number> {
|
|
67
|
+
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
68
|
+
runtime.stdout(HELP)
|
|
69
|
+
return 0
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (args[0] === 'init') {
|
|
73
|
+
const paths = getAppPaths(runtime.homeDir)
|
|
74
|
+
mkdirSync(paths.configDir, { recursive: true })
|
|
75
|
+
if (!existsSync(paths.configFile)) {
|
|
76
|
+
writeFileSync(paths.configFile, 'obsidian_vault = ""\n')
|
|
77
|
+
}
|
|
78
|
+
const db = openDatabase(paths.databaseFile)
|
|
79
|
+
try {
|
|
80
|
+
runMigrations(db)
|
|
81
|
+
} finally {
|
|
82
|
+
db.close()
|
|
83
|
+
}
|
|
84
|
+
runtime.stdout(`Initialized llm-iwiki at ${paths.configDir}`)
|
|
85
|
+
return 0
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (args[0] === 'doctor') {
|
|
89
|
+
const paths = getAppPaths(runtime.homeDir)
|
|
90
|
+
if (!existsSync(paths.configFile)) {
|
|
91
|
+
runtime.stderr('llm-iwiki is not initialized. Run: llm-iwiki init')
|
|
92
|
+
return 1
|
|
93
|
+
}
|
|
94
|
+
if (!existsSync(paths.databaseFile)) {
|
|
95
|
+
runtime.stderr('llm-iwiki database is missing. Run: llm-iwiki init')
|
|
96
|
+
return 1
|
|
97
|
+
}
|
|
98
|
+
const db = openDatabase(paths.databaseFile)
|
|
99
|
+
try {
|
|
100
|
+
runMigrations(db)
|
|
101
|
+
} finally {
|
|
102
|
+
db.close()
|
|
103
|
+
}
|
|
104
|
+
runtime.stdout(`config: ${paths.configFile}`)
|
|
105
|
+
runtime.stdout(`database: ${paths.databaseFile}`)
|
|
106
|
+
runtime.stdout('status: ok')
|
|
107
|
+
return 0
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (args[0] === 'sync') {
|
|
111
|
+
const projectFlag = readFlag(args, '--project')
|
|
112
|
+
const paths = getAppPaths(runtime.homeDir)
|
|
113
|
+
const db = openDatabase(paths.databaseFile)
|
|
114
|
+
try {
|
|
115
|
+
runMigrations(db)
|
|
116
|
+
const report = runSync(db, {
|
|
117
|
+
homeDir: runtime.homeDir ?? paths.homeDir,
|
|
118
|
+
projectFilter: projectFlag ? resolveCliPath(runtime.cwd, projectFlag) : null,
|
|
119
|
+
})
|
|
120
|
+
if (report.bySource.length === 0) {
|
|
121
|
+
runtime.stdout('No collectors detected on this machine.')
|
|
122
|
+
return 0
|
|
123
|
+
}
|
|
124
|
+
for (const source of report.bySource) {
|
|
125
|
+
runtime.stdout(
|
|
126
|
+
`${source.source}: ${source.total} sessions (new ${source.new}, changed ${source.changed}, unchanged ${source.unchanged}, missing ${source.sourceMissing})`,
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
return 0
|
|
130
|
+
} catch (error) {
|
|
131
|
+
runtime.stderr(error instanceof Error ? error.message : String(error))
|
|
132
|
+
return 1
|
|
133
|
+
} finally {
|
|
134
|
+
db.close()
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (args[0] === 'projects' && args[1] === 'list') {
|
|
139
|
+
const paths = getAppPaths(runtime.homeDir)
|
|
140
|
+
const db = openDatabase(paths.databaseFile)
|
|
141
|
+
try {
|
|
142
|
+
runMigrations(db)
|
|
143
|
+
const projects = listProjects(db)
|
|
144
|
+
if (projects.length === 0) {
|
|
145
|
+
runtime.stdout('No projects yet. Run: llm-iwiki sync')
|
|
146
|
+
return 0
|
|
147
|
+
}
|
|
148
|
+
for (const project of projects) {
|
|
149
|
+
const name = project.displayName ?? project.canonicalName
|
|
150
|
+
runtime.stdout(`${project.id} ${project.sessionCount} sessions ${name}`)
|
|
151
|
+
}
|
|
152
|
+
return 0
|
|
153
|
+
} finally {
|
|
154
|
+
db.close()
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (args[0] === 'projects' && args[1] === 'inspect') {
|
|
159
|
+
const target = args[2]
|
|
160
|
+
if (!target) {
|
|
161
|
+
runtime.stderr('Usage: llm-iwiki projects inspect <path-or-project-id>')
|
|
162
|
+
return 1
|
|
163
|
+
}
|
|
164
|
+
const paths = getAppPaths(runtime.homeDir)
|
|
165
|
+
const db = openDatabase(paths.databaseFile)
|
|
166
|
+
try {
|
|
167
|
+
runMigrations(db)
|
|
168
|
+
const project = target.startsWith('proj_')
|
|
169
|
+
? getProject(db, target)
|
|
170
|
+
: resolveProject(db, resolveCliPath(runtime.cwd, target))
|
|
171
|
+
const inspection = inspectProject(db, project.id)
|
|
172
|
+
runtime.stdout(`project: ${project.displayName ?? project.canonicalName}`)
|
|
173
|
+
runtime.stdout(`id: ${project.id}`)
|
|
174
|
+
if (project.canonicalRepoUrl) runtime.stdout(`repo: ${project.canonicalRepoUrl}`)
|
|
175
|
+
runtime.stdout(`sources: ${inspection.sources.map((s) => `${s.source}(${s.sessionCount})`).join(', ') || 'none'}`)
|
|
176
|
+
runtime.stdout(`sessions: ${inspection.sessions.length}`)
|
|
177
|
+
for (const session of inspection.sessions) {
|
|
178
|
+
runtime.stdout(` [${session.sourceId}] ${session.title ?? session.sourceSessionId} (${session.messageCount} msgs, ${session.status})`)
|
|
179
|
+
}
|
|
180
|
+
return 0
|
|
181
|
+
} catch (error) {
|
|
182
|
+
runtime.stderr(error instanceof Error ? error.message : String(error))
|
|
183
|
+
return 1
|
|
184
|
+
} finally {
|
|
185
|
+
db.close()
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (args[0] === 'projects' && args[1] === 'resolve') {
|
|
190
|
+
const targetPath = args[2] ?? runtime.cwd
|
|
191
|
+
const paths = getAppPaths(runtime.homeDir)
|
|
192
|
+
const db = openDatabase(paths.databaseFile)
|
|
193
|
+
try {
|
|
194
|
+
runMigrations(db)
|
|
195
|
+
const project = resolveProject(db, resolveCliPath(runtime.cwd, targetPath))
|
|
196
|
+
runtime.stdout(JSON.stringify(project, null, 2))
|
|
197
|
+
return 0
|
|
198
|
+
} catch (error) {
|
|
199
|
+
runtime.stderr(error instanceof Error ? error.message : String(error))
|
|
200
|
+
return 1
|
|
201
|
+
} finally {
|
|
202
|
+
db.close()
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (args[0] === 'projects' && args[1] === 'rename') {
|
|
207
|
+
const target = args[2]
|
|
208
|
+
const displayName = args[3]
|
|
209
|
+
if (!target || !displayName) {
|
|
210
|
+
runtime.stderr('Usage: llm-iwiki projects rename <path-or-project-id> <display-name>')
|
|
211
|
+
return 1
|
|
212
|
+
}
|
|
213
|
+
const paths = getAppPaths(runtime.homeDir)
|
|
214
|
+
const db = openDatabase(paths.databaseFile)
|
|
215
|
+
try {
|
|
216
|
+
runMigrations(db)
|
|
217
|
+
const project = target.startsWith('proj_')
|
|
218
|
+
? renameProject(db, target, displayName)
|
|
219
|
+
: renameProject(db, resolveProject(db, resolveCliPath(runtime.cwd, target)).id, displayName)
|
|
220
|
+
runtime.stdout(JSON.stringify(project, null, 2))
|
|
221
|
+
return 0
|
|
222
|
+
} catch (error) {
|
|
223
|
+
runtime.stderr(error instanceof Error ? error.message : String(error))
|
|
224
|
+
return 1
|
|
225
|
+
} finally {
|
|
226
|
+
db.close()
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (args[0] === 'summarize' && args[1] === 'prepare') {
|
|
231
|
+
const scopeArg = args[2] && !args[2].startsWith('--') ? args[2] : 'changed'
|
|
232
|
+
if (scopeArg !== 'changed' && scopeArg !== 'all') {
|
|
233
|
+
runtime.stderr('Usage: llm-iwiki summarize prepare [changed|all] --project <path> [--out <file>]')
|
|
234
|
+
return 1
|
|
235
|
+
}
|
|
236
|
+
const projectPath = resolveCliPath(runtime.cwd, readFlag(args, '--project') ?? runtime.cwd)
|
|
237
|
+
const outFile = readFlag(args, '--out') ?? join(getProjectTaskDir(projectPath), 'summaries-task.md')
|
|
238
|
+
const paths = getAppPaths(runtime.homeDir)
|
|
239
|
+
const db = openDatabase(paths.databaseFile)
|
|
240
|
+
try {
|
|
241
|
+
runMigrations(db)
|
|
242
|
+
const project = resolveProject(db, projectPath)
|
|
243
|
+
const result = prepareSummariesTask(db, project.id, scopeArg)
|
|
244
|
+
mkdirSync(dirname(resolveCliPath(runtime.cwd, outFile)), { recursive: true })
|
|
245
|
+
writeFileSync(resolveCliPath(runtime.cwd, outFile), result.markdown)
|
|
246
|
+
runtime.stdout(`prepared summaries task: ${result.sessionCount} sessions -> ${outFile}`)
|
|
247
|
+
return 0
|
|
248
|
+
} catch (error) {
|
|
249
|
+
runtime.stderr(error instanceof Error ? error.message : String(error))
|
|
250
|
+
return 1
|
|
251
|
+
} finally {
|
|
252
|
+
db.close()
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (args[0] === 'summarize' && args[1] === 'apply') {
|
|
257
|
+
const file = readFlag(args, '--file')
|
|
258
|
+
if (!file) {
|
|
259
|
+
runtime.stderr('Usage: llm-iwiki summarize apply --project <path> --file <summaries.yaml>')
|
|
260
|
+
return 1
|
|
261
|
+
}
|
|
262
|
+
const paths = getAppPaths(runtime.homeDir)
|
|
263
|
+
const db = openDatabase(paths.databaseFile)
|
|
264
|
+
try {
|
|
265
|
+
runMigrations(db)
|
|
266
|
+
const parsed = parseSummariesYaml(readFileSync(resolveCliPath(runtime.cwd, file), 'utf8'))
|
|
267
|
+
const result = applySummaries(db, parsed)
|
|
268
|
+
runtime.stdout(`applied summaries: ${result.written}`)
|
|
269
|
+
if (result.skipped.length > 0) {
|
|
270
|
+
runtime.stdout(`skipped (unknown session): ${result.skipped.length}`)
|
|
271
|
+
}
|
|
272
|
+
return 0
|
|
273
|
+
} catch (error) {
|
|
274
|
+
runtime.stderr(error instanceof Error ? error.message : String(error))
|
|
275
|
+
return 1
|
|
276
|
+
} finally {
|
|
277
|
+
db.close()
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (args[0] === 'experiences' && args[1] === 'prepare') {
|
|
282
|
+
const fromArg = (readFlag(args, '--from') ?? 'changed-summaries') as ExperienceScope
|
|
283
|
+
if (fromArg !== 'changed-summaries' && fromArg !== 'all-recent') {
|
|
284
|
+
runtime.stderr('Invalid --from. Use changed-summaries or all-recent.')
|
|
285
|
+
return 1
|
|
286
|
+
}
|
|
287
|
+
const projectPath = resolveCliPath(runtime.cwd, readFlag(args, '--project') ?? runtime.cwd)
|
|
288
|
+
const outFile = readFlag(args, '--out') ?? join(getProjectTaskDir(projectPath), 'experiences-task.md')
|
|
289
|
+
const paths = getAppPaths(runtime.homeDir)
|
|
290
|
+
const db = openDatabase(paths.databaseFile)
|
|
291
|
+
try {
|
|
292
|
+
runMigrations(db)
|
|
293
|
+
const project = resolveProject(db, projectPath)
|
|
294
|
+
const result = prepareExperiencesTask(db, project.id, fromArg)
|
|
295
|
+
mkdirSync(dirname(resolveCliPath(runtime.cwd, outFile)), { recursive: true })
|
|
296
|
+
writeFileSync(resolveCliPath(runtime.cwd, outFile), result.markdown)
|
|
297
|
+
runtime.stdout(`prepared experiences task: ${result.summaryCount} summaries -> ${outFile}`)
|
|
298
|
+
return 0
|
|
299
|
+
} catch (error) {
|
|
300
|
+
runtime.stderr(error instanceof Error ? error.message : String(error))
|
|
301
|
+
return 1
|
|
302
|
+
} finally {
|
|
303
|
+
db.close()
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (args[0] === 'experiences' && args[1] === 'propose') {
|
|
308
|
+
const file = readFlag(args, '--file')
|
|
309
|
+
if (!file) {
|
|
310
|
+
runtime.stderr('Usage: llm-iwiki experiences propose --project <path> --file <experiences.yaml>')
|
|
311
|
+
return 1
|
|
312
|
+
}
|
|
313
|
+
const paths = getAppPaths(runtime.homeDir)
|
|
314
|
+
const db = openDatabase(paths.databaseFile)
|
|
315
|
+
try {
|
|
316
|
+
runMigrations(db)
|
|
317
|
+
const parsed = parseExperiencesYaml(readFileSync(resolveCliPath(runtime.cwd, file), 'utf8'))
|
|
318
|
+
const result = proposeExperiences(db, parsed)
|
|
319
|
+
runtime.stdout(`proposed experiences: ${result.written}`)
|
|
320
|
+
return 0
|
|
321
|
+
} catch (error) {
|
|
322
|
+
runtime.stderr(error instanceof Error ? error.message : String(error))
|
|
323
|
+
return 1
|
|
324
|
+
} finally {
|
|
325
|
+
db.close()
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (args[0] === 'experiences' && args[1] === 'candidates') {
|
|
330
|
+
const projectFlag = readFlag(args, '--project')
|
|
331
|
+
const paths = getAppPaths(runtime.homeDir)
|
|
332
|
+
const db = openDatabase(paths.databaseFile)
|
|
333
|
+
try {
|
|
334
|
+
runMigrations(db)
|
|
335
|
+
const projectId = projectFlag ? resolveProject(db, resolveCliPath(runtime.cwd, projectFlag)).id : null
|
|
336
|
+
const candidates = listCandidates(db, projectId)
|
|
337
|
+
if (candidates.length === 0) {
|
|
338
|
+
runtime.stdout('No experience candidates. Run: llm-iwiki experiences propose')
|
|
339
|
+
return 0
|
|
340
|
+
}
|
|
341
|
+
for (const candidate of candidates) {
|
|
342
|
+
runtime.stdout(
|
|
343
|
+
`${candidate.id} [${candidate.status}] ${candidate.confidence ?? '-'} ${candidate.proposed_title}`,
|
|
344
|
+
)
|
|
345
|
+
}
|
|
346
|
+
return 0
|
|
347
|
+
} catch (error) {
|
|
348
|
+
runtime.stderr(error instanceof Error ? error.message : String(error))
|
|
349
|
+
return 1
|
|
350
|
+
} finally {
|
|
351
|
+
db.close()
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (args[0] === 'experiences' && (args[1] === 'accept' || args[1] === 'reject')) {
|
|
356
|
+
const candidateId = args[2]
|
|
357
|
+
if (!candidateId) {
|
|
358
|
+
runtime.stderr(`Usage: llm-iwiki experiences ${args[1]} <candidate-id>`)
|
|
359
|
+
return 1
|
|
360
|
+
}
|
|
361
|
+
const paths = getAppPaths(runtime.homeDir)
|
|
362
|
+
const db = openDatabase(paths.databaseFile)
|
|
363
|
+
try {
|
|
364
|
+
runMigrations(db)
|
|
365
|
+
if (args[1] === 'accept') {
|
|
366
|
+
const result = acceptExperience(db, candidateId)
|
|
367
|
+
runtime.stdout(`accepted: ${result.experienceId} (${result.slug}), linked sessions: ${result.linkedSessions}`)
|
|
368
|
+
} else {
|
|
369
|
+
rejectExperience(db, candidateId)
|
|
370
|
+
runtime.stdout(`rejected: ${candidateId}`)
|
|
371
|
+
}
|
|
372
|
+
return 0
|
|
373
|
+
} catch (error) {
|
|
374
|
+
runtime.stderr(error instanceof Error ? error.message : String(error))
|
|
375
|
+
return 1
|
|
376
|
+
} finally {
|
|
377
|
+
db.close()
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (args[0] === 'config' && args[1] === 'show') {
|
|
382
|
+
const paths = getAppPaths(runtime.homeDir)
|
|
383
|
+
const config = readConfig(paths.configFile)
|
|
384
|
+
runtime.stdout(`config: ${paths.configFile}`)
|
|
385
|
+
runtime.stdout(`obsidian.vault: ${config.obsidianVault ?? '(unset)'}`)
|
|
386
|
+
return 0
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (args[0] === 'config' && args[1] === 'set') {
|
|
390
|
+
const key = args[2]
|
|
391
|
+
const value = args[3]
|
|
392
|
+
if (!key || value === undefined) {
|
|
393
|
+
runtime.stderr('Usage: llm-iwiki config set <key> <value>')
|
|
394
|
+
return 1
|
|
395
|
+
}
|
|
396
|
+
const paths = getAppPaths(runtime.homeDir)
|
|
397
|
+
mkdirSync(paths.configDir, { recursive: true })
|
|
398
|
+
try {
|
|
399
|
+
const normalizedKey = setConfigValue(paths.configFile, key, value)
|
|
400
|
+
runtime.stdout(`set ${normalizedKey} = ${value}`)
|
|
401
|
+
return 0
|
|
402
|
+
} catch (error) {
|
|
403
|
+
runtime.stderr(error instanceof Error ? error.message : String(error))
|
|
404
|
+
return 1
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (args[0] === 'obsidian' && args[1] === 'export') {
|
|
409
|
+
const paths = getAppPaths(runtime.homeDir)
|
|
410
|
+
const vaultFlag = readFlag(args, '--vault')
|
|
411
|
+
const vault = vaultFlag
|
|
412
|
+
? resolveCliPath(runtime.cwd, vaultFlag)
|
|
413
|
+
: readConfig(paths.configFile).obsidianVault
|
|
414
|
+
if (!vault) {
|
|
415
|
+
runtime.stderr('No Obsidian vault configured. Run: llm-iwiki config set obsidian.vault <dir>')
|
|
416
|
+
return 1
|
|
417
|
+
}
|
|
418
|
+
const projectPath = resolveCliPath(runtime.cwd, readFlag(args, '--project') ?? runtime.cwd)
|
|
419
|
+
const force = args.includes('--force')
|
|
420
|
+
const db = openDatabase(paths.databaseFile)
|
|
421
|
+
try {
|
|
422
|
+
runMigrations(db)
|
|
423
|
+
const project = resolveProject(db, projectPath)
|
|
424
|
+
const report = exportProject(db, vault, project, { force })
|
|
425
|
+
runtime.stdout(`vault: ${vault}`)
|
|
426
|
+
runtime.stdout(
|
|
427
|
+
`exported: created ${report.created}, updated ${report.updated}, forced ${report.forced}, conflicts ${report.conflicts.length}`,
|
|
428
|
+
)
|
|
429
|
+
for (const conflict of report.conflicts) {
|
|
430
|
+
runtime.stdout(` conflict (skipped): ${conflict}`)
|
|
431
|
+
}
|
|
432
|
+
if (report.conflicts.length > 0 && !force) {
|
|
433
|
+
runtime.stdout('Re-run with --force to overwrite managed blocks of conflicting notes.')
|
|
434
|
+
}
|
|
435
|
+
return 0
|
|
436
|
+
} catch (error) {
|
|
437
|
+
runtime.stderr(error instanceof Error ? error.message : String(error))
|
|
438
|
+
return 1
|
|
439
|
+
} finally {
|
|
440
|
+
db.close()
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (args[0] === 'obsidian' && args[1] === 'check') {
|
|
445
|
+
const paths = getAppPaths(runtime.homeDir)
|
|
446
|
+
const db = openDatabase(paths.databaseFile)
|
|
447
|
+
try {
|
|
448
|
+
runMigrations(db)
|
|
449
|
+
const report = checkVault(db)
|
|
450
|
+
runtime.stdout(`notes: ${report.total}, clean: ${report.clean}, needs attention: ${report.entries.length}`)
|
|
451
|
+
for (const entry of report.entries) {
|
|
452
|
+
runtime.stdout(` ${entry.status}: ${entry.filePath}`)
|
|
453
|
+
}
|
|
454
|
+
return 0
|
|
455
|
+
} finally {
|
|
456
|
+
db.close()
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (args[0] === 'skills' && args[1] === 'init') {
|
|
461
|
+
const targetFlag = readFlag(args, '--target')
|
|
462
|
+
if (args.includes('--target') && !targetFlag) {
|
|
463
|
+
runtime.stderr('Invalid --target. Use codex, claude-code, or cursor.')
|
|
464
|
+
return 1
|
|
465
|
+
}
|
|
466
|
+
if (targetFlag && !(SKILL_TARGETS as readonly string[]).includes(targetFlag)) {
|
|
467
|
+
runtime.stderr('Invalid --target. Use codex, claude-code, or cursor.')
|
|
468
|
+
return 1
|
|
469
|
+
}
|
|
470
|
+
const target = targetFlag as SkillTarget | null
|
|
471
|
+
const result = initSkills({
|
|
472
|
+
cwd: runtime.cwd,
|
|
473
|
+
target,
|
|
474
|
+
force: args.includes('--force'),
|
|
475
|
+
dryRun: args.includes('--dry-run'),
|
|
476
|
+
})
|
|
477
|
+
runtime.stdout(`skills written: ${result.written.length}`)
|
|
478
|
+
runtime.stdout(`skills skipped: ${result.skipped.length}`)
|
|
479
|
+
return 0
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
runtime.stderr(`Unknown command: ${args.join(' ')}`)
|
|
483
|
+
runtime.stderr(HELP)
|
|
484
|
+
return 1
|
|
485
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from 'node:fs'
|
|
2
|
+
import { basename, join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import type { Collector, RawMessage, RawSession } from './types'
|
|
5
|
+
import { deriveTitle, isEphemeralPath, normalizeContentParts } from './util'
|
|
6
|
+
|
|
7
|
+
const PROJECT_ROOTS = ['.claude/projects', '.claude-internal/projects']
|
|
8
|
+
|
|
9
|
+
function parseSessionFile(filePath: string): RawSession | null {
|
|
10
|
+
const raw = readFileSync(filePath, 'utf8')
|
|
11
|
+
const lines = raw.split('\n')
|
|
12
|
+
|
|
13
|
+
const messages: RawMessage[] = []
|
|
14
|
+
let rawProjectPath: string | null = null
|
|
15
|
+
let sourceSessionId: string | null = null
|
|
16
|
+
|
|
17
|
+
for (const line of lines) {
|
|
18
|
+
const trimmed = line.trim()
|
|
19
|
+
if (trimmed === '') continue
|
|
20
|
+
|
|
21
|
+
let parsed: Record<string, unknown>
|
|
22
|
+
try {
|
|
23
|
+
parsed = JSON.parse(trimmed) as Record<string, unknown>
|
|
24
|
+
} catch {
|
|
25
|
+
continue
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (typeof parsed.sessionId === 'string' && !sourceSessionId) {
|
|
29
|
+
sourceSessionId = parsed.sessionId
|
|
30
|
+
}
|
|
31
|
+
if (typeof parsed.cwd === 'string' && !rawProjectPath) {
|
|
32
|
+
rawProjectPath = parsed.cwd
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const type = parsed.type
|
|
36
|
+
if (type !== 'user' && type !== 'assistant') continue
|
|
37
|
+
if (parsed.isSidechain === true) continue
|
|
38
|
+
|
|
39
|
+
const message = parsed.message
|
|
40
|
+
if (!message || typeof message !== 'object') continue
|
|
41
|
+
const messageRecord = message as Record<string, unknown>
|
|
42
|
+
const role = typeof messageRecord.role === 'string' ? messageRecord.role : type
|
|
43
|
+
const content = normalizeContentParts(messageRecord.content)
|
|
44
|
+
if (content.trim() === '') continue
|
|
45
|
+
|
|
46
|
+
messages.push({
|
|
47
|
+
role,
|
|
48
|
+
content,
|
|
49
|
+
timestamp: typeof parsed.timestamp === 'string' ? parsed.timestamp : null,
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (messages.length === 0) return null
|
|
54
|
+
if (isEphemeralPath(rawProjectPath)) return null
|
|
55
|
+
|
|
56
|
+
sourceSessionId ??= basename(filePath).replace(/\.jsonl$/, '')
|
|
57
|
+
const timestamps = messages.map((message) => message.timestamp).filter((value): value is string => value != null)
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
sourceSessionId,
|
|
61
|
+
rawPath: filePath,
|
|
62
|
+
rawProjectPath,
|
|
63
|
+
title: deriveTitle(messages),
|
|
64
|
+
createdAt: timestamps[0] ?? null,
|
|
65
|
+
updatedAt: timestamps[timestamps.length - 1] ?? null,
|
|
66
|
+
messages,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function listSessionFiles(homeDir: string): string[] {
|
|
71
|
+
const files: string[] = []
|
|
72
|
+
const seenRoots = new Set<string>()
|
|
73
|
+
for (const root of PROJECT_ROOTS) {
|
|
74
|
+
const projectsDir = join(homeDir, root)
|
|
75
|
+
if (!existsSync(projectsDir)) continue
|
|
76
|
+
|
|
77
|
+
let canonicalRoot: string
|
|
78
|
+
try {
|
|
79
|
+
canonicalRoot = realpathSync(projectsDir)
|
|
80
|
+
} catch {
|
|
81
|
+
canonicalRoot = projectsDir
|
|
82
|
+
}
|
|
83
|
+
if (seenRoots.has(canonicalRoot)) continue
|
|
84
|
+
seenRoots.add(canonicalRoot)
|
|
85
|
+
|
|
86
|
+
for (const projectEntry of readdirSync(projectsDir, { withFileTypes: true })) {
|
|
87
|
+
if (!projectEntry.isDirectory()) continue
|
|
88
|
+
const projectDir = join(projectsDir, projectEntry.name)
|
|
89
|
+
for (const fileEntry of readdirSync(projectDir, { withFileTypes: true })) {
|
|
90
|
+
if (fileEntry.isFile() && fileEntry.name.endsWith('.jsonl')) {
|
|
91
|
+
files.push(join(projectDir, fileEntry.name))
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return files
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const claudeCodeCollector: Collector = {
|
|
100
|
+
id: 'claude-code',
|
|
101
|
+
name: 'Claude Code',
|
|
102
|
+
|
|
103
|
+
detect(homeDir: string): boolean {
|
|
104
|
+
return PROJECT_ROOTS.some((root) => existsSync(join(homeDir, root)))
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
collect(homeDir: string): RawSession[] {
|
|
108
|
+
const sessions: RawSession[] = []
|
|
109
|
+
for (const filePath of listSessionFiles(homeDir)) {
|
|
110
|
+
try {
|
|
111
|
+
if (!statSync(filePath).isFile()) continue
|
|
112
|
+
const session = parseSessionFile(filePath)
|
|
113
|
+
if (session) sessions.push(session)
|
|
114
|
+
} catch {
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return sessions
|
|
119
|
+
},
|
|
120
|
+
}
|