@codehourra/llm-iwiki 0.1.0 → 0.1.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codehourra/llm-iwiki",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "面向 AI Agent 的本地知识库 CLI:采集 Claude Code / Cursor / Codex / CodeBuddy / Gemini 会话,按项目归一化,生成总结与经验并导出到 Obsidian。",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -24,10 +24,10 @@
24
24
  "bun"
25
25
  ],
26
26
  "bin": {
27
- "llm-iwiki": "./src/index.ts"
27
+ "llm-iwiki": "./dist/index.js"
28
28
  },
29
29
  "files": [
30
- "src",
30
+ "dist",
31
31
  "README.md",
32
32
  "LICENSE"
33
33
  ],
@@ -41,13 +41,12 @@
41
41
  "dev": "bun run src/index.ts",
42
42
  "test": "bun test --pass-with-no-tests",
43
43
  "typecheck": "tsc --noEmit",
44
- "build": "bun build src/index.ts --compile --outfile dist/llm-iwiki"
45
- },
46
- "dependencies": {
47
- "yaml": "^2.7.0"
44
+ "build": "bun build src/index.ts --target bun --minify --outfile dist/index.js",
45
+ "prepublishOnly": "bun run build"
48
46
  },
49
47
  "devDependencies": {
50
48
  "@types/bun": "^1.3.11",
51
- "typescript": "~5.7"
49
+ "typescript": "~5.7",
50
+ "yaml": "^2.7.0"
52
51
  }
53
52
  }
package/src/ai-yaml.ts DELETED
@@ -1,82 +0,0 @@
1
- import { parse } from 'yaml'
2
-
3
- import type { Confidence, ParsedExperiencesYaml, ParsedSummariesYaml, SummaryValue } from './types'
4
-
5
- const SUMMARY_VALUES = new Set<SummaryValue>(['none', 'low', 'medium', 'high'])
6
- const CONFIDENCE_VALUES = new Set<Confidence>(['low', 'medium', 'high'])
7
-
8
- function asRecord(value: unknown, label: string): Record<string, unknown> {
9
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
10
- throw new Error(`${label} must be an object`)
11
- }
12
- return value as Record<string, unknown>
13
- }
14
-
15
- function requiredString(record: Record<string, unknown>, key: string, label = key): string {
16
- const value = record[key]
17
- if (typeof value !== 'string' || value.trim() === '') {
18
- throw new Error(`Missing required string: ${label}`)
19
- }
20
- return value
21
- }
22
-
23
- function optionalConfidence(record: Record<string, unknown>): Confidence | null {
24
- if (!('confidence' in record)) return null
25
- const confidence = record.confidence
26
- if (typeof confidence !== 'string' || !CONFIDENCE_VALUES.has(confidence as Confidence)) {
27
- throw new Error(`Invalid confidence: ${String(confidence)}`)
28
- }
29
- return confidence as Confidence
30
- }
31
-
32
- export function parseSummariesYaml(source: string): ParsedSummariesYaml {
33
- const root = asRecord(parse(source), 'summaries.yaml')
34
- const projectId = requiredString(root, 'project_id')
35
- if (!Array.isArray(root.summaries)) throw new Error('summaries must be an array')
36
-
37
- return {
38
- projectId,
39
- summaries: root.summaries.map((item, index) => {
40
- const itemLabel = `summaries[${index}]`
41
- const record = asRecord(item, `summaries[${index}]`)
42
- const value = requiredString(record, 'value', `${itemLabel}.value`)
43
- if (!SUMMARY_VALUES.has(value as SummaryValue)) throw new Error(`Invalid summary value: ${value}`)
44
- optionalConfidence(record)
45
- return {
46
- sessionId: requiredString(record, 'session_id', `${itemLabel}.session_id`),
47
- title: requiredString(record, 'title', `${itemLabel}.title`),
48
- value: value as SummaryValue,
49
- summaryMarkdown: requiredString(record, 'summary_markdown', `${itemLabel}.summary_markdown`),
50
- metadata: record,
51
- }
52
- }),
53
- }
54
- }
55
-
56
- export function parseExperiencesYaml(source: string): ParsedExperiencesYaml {
57
- const root = asRecord(parse(source), 'experiences.yaml')
58
- const projectId = requiredString(root, 'project_id')
59
- if (!Array.isArray(root.experiences)) throw new Error('experiences must be an array')
60
-
61
- return {
62
- projectId,
63
- experiences: root.experiences.map((item, index) => {
64
- const itemLabel = `experiences[${index}]`
65
- const record = asRecord(item, itemLabel)
66
- const sourceSessions = record.source_sessions
67
- if (!Array.isArray(sourceSessions) || sourceSessions.some((value) => typeof value !== 'string')) {
68
- throw new Error(`experiences[${index}].source_sessions must be a string array`)
69
- }
70
- const confidence = optionalConfidence(record)
71
- return {
72
- title: requiredString(record, 'title', `${itemLabel}.title`),
73
- slug: typeof record.slug === 'string' ? record.slug : null,
74
- summary: requiredString(record, 'summary', `${itemLabel}.summary`),
75
- bodyMarkdown: requiredString(record, 'body_markdown', `${itemLabel}.body_markdown`),
76
- sourceSessions,
77
- confidence: confidence as Confidence | null,
78
- metadata: record,
79
- }
80
- }),
81
- }
82
- }
package/src/cli.ts DELETED
@@ -1,485 +0,0 @@
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
- }