@skillrecordings/cli 0.1.0 → 0.2.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.
Files changed (136) hide show
  1. package/bin/skill.mjs +27 -0
  2. package/dist/chunk-2NCCVTEE.js +22342 -0
  3. package/dist/chunk-2NCCVTEE.js.map +1 -0
  4. package/dist/chunk-3E3GYSZR.js +7071 -0
  5. package/dist/chunk-3E3GYSZR.js.map +1 -0
  6. package/dist/chunk-F4EM72IH.js +86 -0
  7. package/dist/chunk-F4EM72IH.js.map +1 -0
  8. package/dist/chunk-FGP7KUQW.js +432 -0
  9. package/dist/chunk-FGP7KUQW.js.map +1 -0
  10. package/dist/chunk-H3D6VCME.js +55 -0
  11. package/dist/chunk-H3D6VCME.js.map +1 -0
  12. package/dist/chunk-HK3PEWFD.js +208 -0
  13. package/dist/chunk-HK3PEWFD.js.map +1 -0
  14. package/dist/chunk-KEV3QKXP.js +4495 -0
  15. package/dist/chunk-KEV3QKXP.js.map +1 -0
  16. package/dist/chunk-MG37YDAK.js +882 -0
  17. package/dist/chunk-MG37YDAK.js.map +1 -0
  18. package/dist/chunk-MLNDSBZ4.js +482 -0
  19. package/dist/chunk-MLNDSBZ4.js.map +1 -0
  20. package/dist/chunk-N2WIV2JV.js +22 -0
  21. package/dist/chunk-N2WIV2JV.js.map +1 -0
  22. package/dist/chunk-PWWRCN5W.js +2067 -0
  23. package/dist/chunk-PWWRCN5W.js.map +1 -0
  24. package/dist/chunk-SKHBM3XP.js +7746 -0
  25. package/dist/chunk-SKHBM3XP.js.map +1 -0
  26. package/dist/chunk-WFANXVQG.js +64 -0
  27. package/dist/chunk-WFANXVQG.js.map +1 -0
  28. package/dist/chunk-WYKL32C3.js +275 -0
  29. package/dist/chunk-WYKL32C3.js.map +1 -0
  30. package/dist/chunk-ZNF7XD2S.js +134 -0
  31. package/dist/chunk-ZNF7XD2S.js.map +1 -0
  32. package/dist/config-AUAIYDSI.js +20 -0
  33. package/dist/config-AUAIYDSI.js.map +1 -0
  34. package/dist/fileFromPath-XN7LXIBI.js +134 -0
  35. package/dist/fileFromPath-XN7LXIBI.js.map +1 -0
  36. package/dist/getMachineId-bsd-KW2E7VK3.js +42 -0
  37. package/dist/getMachineId-bsd-KW2E7VK3.js.map +1 -0
  38. package/dist/getMachineId-darwin-ROXJUJX5.js +42 -0
  39. package/dist/getMachineId-darwin-ROXJUJX5.js.map +1 -0
  40. package/dist/getMachineId-linux-KVZEHQSU.js +34 -0
  41. package/dist/getMachineId-linux-KVZEHQSU.js.map +1 -0
  42. package/dist/getMachineId-unsupported-PPRILPPA.js +25 -0
  43. package/dist/getMachineId-unsupported-PPRILPPA.js.map +1 -0
  44. package/dist/getMachineId-win-IIF36LEJ.js +44 -0
  45. package/dist/getMachineId-win-IIF36LEJ.js.map +1 -0
  46. package/dist/index.js +112703 -0
  47. package/dist/index.js.map +1 -0
  48. package/dist/lib-R6DEEJCP.js +7623 -0
  49. package/dist/lib-R6DEEJCP.js.map +1 -0
  50. package/dist/pipeline-IAVVAKTU.js +120 -0
  51. package/dist/pipeline-IAVVAKTU.js.map +1 -0
  52. package/dist/query-NTP5NVXN.js +25 -0
  53. package/dist/query-NTP5NVXN.js.map +1 -0
  54. package/dist/routing-BAEPFB7V.js +390 -0
  55. package/dist/routing-BAEPFB7V.js.map +1 -0
  56. package/dist/stripe-lookup-charge-EPRUMZDL.js +56 -0
  57. package/dist/stripe-lookup-charge-EPRUMZDL.js.map +1 -0
  58. package/dist/stripe-payment-history-SJPKA63N.js +67 -0
  59. package/dist/stripe-payment-history-SJPKA63N.js.map +1 -0
  60. package/dist/stripe-subscription-status-L4Z65GB3.js +58 -0
  61. package/dist/stripe-subscription-status-L4Z65GB3.js.map +1 -0
  62. package/dist/stripe-verify-refund-FZDKCIUQ.js +54 -0
  63. package/dist/stripe-verify-refund-FZDKCIUQ.js.map +1 -0
  64. package/dist/support-memory-WSG7SDKG.js +10 -0
  65. package/dist/support-memory-WSG7SDKG.js.map +1 -0
  66. package/package.json +10 -7
  67. package/.env.encrypted +0 -0
  68. package/CHANGELOG.md +0 -35
  69. package/data/tt-archive-dataset.json +0 -1
  70. package/data/validate-test-dataset.json +0 -97
  71. package/docs/CLI-AUTH.md +0 -504
  72. package/preload.ts +0 -18
  73. package/src/__tests__/init.test.ts +0 -74
  74. package/src/alignment-test.ts +0 -64
  75. package/src/check-apps.ts +0 -16
  76. package/src/commands/auth/decrypt.ts +0 -123
  77. package/src/commands/auth/encrypt.ts +0 -81
  78. package/src/commands/auth/index.ts +0 -50
  79. package/src/commands/auth/keygen.ts +0 -41
  80. package/src/commands/auth/status.ts +0 -164
  81. package/src/commands/axiom/forensic.ts +0 -868
  82. package/src/commands/axiom/index.ts +0 -697
  83. package/src/commands/build-dataset.ts +0 -311
  84. package/src/commands/db-status.ts +0 -47
  85. package/src/commands/deploys.ts +0 -219
  86. package/src/commands/eval-local/compare.ts +0 -171
  87. package/src/commands/eval-local/health.ts +0 -212
  88. package/src/commands/eval-local/index.ts +0 -76
  89. package/src/commands/eval-local/real-tools.ts +0 -416
  90. package/src/commands/eval-local/run.ts +0 -1168
  91. package/src/commands/eval-local/score-production.ts +0 -256
  92. package/src/commands/eval-local/seed.ts +0 -276
  93. package/src/commands/eval-pipeline/index.ts +0 -53
  94. package/src/commands/eval-pipeline/real-tools.ts +0 -492
  95. package/src/commands/eval-pipeline/run.ts +0 -1316
  96. package/src/commands/eval-pipeline/seed.ts +0 -395
  97. package/src/commands/eval-prompt.ts +0 -496
  98. package/src/commands/eval.test.ts +0 -253
  99. package/src/commands/eval.ts +0 -108
  100. package/src/commands/faq-classify.ts +0 -460
  101. package/src/commands/faq-cluster.ts +0 -135
  102. package/src/commands/faq-extract.ts +0 -249
  103. package/src/commands/faq-mine.ts +0 -432
  104. package/src/commands/faq-review.ts +0 -426
  105. package/src/commands/front/index.ts +0 -351
  106. package/src/commands/front/pull-conversations.ts +0 -275
  107. package/src/commands/front/tags.ts +0 -825
  108. package/src/commands/front-cache.ts +0 -1277
  109. package/src/commands/front-stats.ts +0 -75
  110. package/src/commands/health.test.ts +0 -82
  111. package/src/commands/health.ts +0 -362
  112. package/src/commands/init.test.ts +0 -89
  113. package/src/commands/init.ts +0 -106
  114. package/src/commands/inngest/client.ts +0 -294
  115. package/src/commands/inngest/events.ts +0 -296
  116. package/src/commands/inngest/investigate.ts +0 -382
  117. package/src/commands/inngest/runs.ts +0 -149
  118. package/src/commands/inngest/signal.ts +0 -143
  119. package/src/commands/kb-sync.ts +0 -498
  120. package/src/commands/memory/find.ts +0 -135
  121. package/src/commands/memory/get.ts +0 -87
  122. package/src/commands/memory/index.ts +0 -97
  123. package/src/commands/memory/stats.ts +0 -163
  124. package/src/commands/memory/store.ts +0 -49
  125. package/src/commands/memory/vote.ts +0 -159
  126. package/src/commands/pipeline.ts +0 -127
  127. package/src/commands/responses.ts +0 -856
  128. package/src/commands/tools.ts +0 -293
  129. package/src/commands/wizard.ts +0 -319
  130. package/src/index.ts +0 -172
  131. package/src/lib/crypto.ts +0 -56
  132. package/src/lib/env-loader.ts +0 -206
  133. package/src/lib/onepassword.ts +0 -137
  134. package/src/test-agent-local.ts +0 -115
  135. package/tsconfig.json +0 -11
  136. package/vitest.config.ts +0 -10
@@ -1,825 +0,0 @@
1
- /**
2
- * Front CLI tag management commands
3
- *
4
- * Provides commands for:
5
- * - Listing tags with conversation counts
6
- * - Filtering unused tags
7
- * - Deleting tags
8
- * - Renaming tags
9
- * - Cleanup (duplicates, case variants, obsolete tags)
10
- */
11
-
12
- import { confirm } from '@inquirer/prompts'
13
- import { createInstrumentedFrontClient } from '@skillrecordings/core/front/instrumented-client'
14
- import {
15
- type TagWithConversationCount,
16
- findCaseVariants,
17
- findExactDuplicates,
18
- } from '@skillrecordings/core/tags/audit'
19
- import { DEFAULT_CATEGORY_TAG_MAPPING } from '@skillrecordings/core/tags/registry'
20
- import type { Command } from 'commander'
21
-
22
- /**
23
- * Get Front SDK client from environment
24
- */
25
- function getFrontSdkClient() {
26
- const apiToken = process.env.FRONT_API_TOKEN
27
- if (!apiToken) {
28
- throw new Error('FRONT_API_TOKEN environment variable is required')
29
- }
30
- return createInstrumentedFrontClient({ apiToken })
31
- }
32
-
33
- /**
34
- * Get Front API token
35
- */
36
- function getFrontApiToken() {
37
- const apiToken = process.env.FRONT_API_TOKEN
38
- if (!apiToken) {
39
- throw new Error('FRONT_API_TOKEN environment variable is required')
40
- }
41
- return apiToken
42
- }
43
-
44
- /**
45
- * Raw fetch for tags - bypasses SDK validation which chokes on Front's messy data
46
- */
47
- async function fetchTagsRaw(): Promise<
48
- Array<{
49
- id: string
50
- name: string
51
- highlight?: string | null
52
- is_private: boolean
53
- description?: string | null
54
- }>
55
- > {
56
- const apiToken = getFrontApiToken()
57
- const allTags: Array<{
58
- id: string
59
- name: string
60
- highlight?: string | null
61
- is_private: boolean
62
- description?: string | null
63
- }> = []
64
-
65
- let nextUrl: string | null = 'https://api2.frontapp.com/tags'
66
- const front = createInstrumentedFrontClient({ apiToken })
67
-
68
- while (nextUrl) {
69
- const data = (await front.raw.get(nextUrl)) as {
70
- _results: Array<{
71
- id: string
72
- name: string
73
- highlight?: string | null
74
- is_private: boolean
75
- description?: string | null
76
- }>
77
- _pagination?: { next?: string }
78
- }
79
-
80
- allTags.push(...data._results)
81
- nextUrl = data._pagination?.next ?? null
82
- }
83
-
84
- return allTags
85
- }
86
-
87
- /**
88
- * Raw create tag - bypasses SDK response validation
89
- */
90
- async function createTagRaw(params: {
91
- name: string
92
- highlight?: string | null
93
- description?: string | null
94
- }): Promise<void> {
95
- const apiToken = getFrontApiToken()
96
- const body: Record<string, unknown> = { name: params.name }
97
- if (params.highlight) body.highlight = params.highlight
98
- if (params.description) body.description = params.description
99
-
100
- const front = createInstrumentedFrontClient({ apiToken })
101
- await front.raw.post('/tags', body)
102
- }
103
-
104
- /**
105
- * Truncate string with ellipsis
106
- */
107
- function truncate(str: string, len: number): string {
108
- if (str.length <= len) return str
109
- return str.slice(0, len - 3) + '...'
110
- }
111
-
112
- interface TagWithCount {
113
- id: string
114
- name: string
115
- highlight: string | null
116
- is_private: boolean
117
- description: string | null
118
- conversation_count: number
119
- }
120
-
121
- /**
122
- * Sleep for a given number of milliseconds
123
- */
124
- function sleep(ms: number): Promise<void> {
125
- return new Promise((resolve) => setTimeout(resolve, ms))
126
- }
127
-
128
- /**
129
- * Get conversation count for a tag
130
- * Uses the conversations endpoint and checks pagination
131
- */
132
- async function getConversationCount(
133
- front: ReturnType<typeof createInstrumentedFrontClient>,
134
- tagId: string
135
- ): Promise<number> {
136
- try {
137
- const result = (await front.tags.listConversations(tagId)) as {
138
- _results?: unknown[]
139
- _pagination?: { total?: number }
140
- }
141
- // Use pagination total if available, otherwise count results
142
- return result._pagination?.total ?? result._results?.length ?? 0
143
- } catch {
144
- return 0
145
- }
146
- }
147
-
148
- /**
149
- * Fetch conversation counts for tags with rate limiting
150
- * @param tags - Array of tags to fetch counts for
151
- * @param front - Front SDK client
152
- * @param delayMs - Delay between API calls (default 100ms)
153
- * @param batchSize - Number of concurrent requests (default 5)
154
- * @param onProgress - Callback for progress updates
155
- */
156
- async function fetchConversationCountsRateLimited<
157
- T extends { id: string; name: string },
158
- >(
159
- tags: T[],
160
- front: ReturnType<typeof createInstrumentedFrontClient>,
161
- options: {
162
- delayMs?: number
163
- batchSize?: number
164
- onProgress?: (completed: number, total: number) => void
165
- } = {}
166
- ): Promise<Map<string, number>> {
167
- const { delayMs = 100, batchSize = 5, onProgress } = options
168
- const counts = new Map<string, number>()
169
-
170
- // Process in batches
171
- for (let i = 0; i < tags.length; i += batchSize) {
172
- const batch = tags.slice(i, i + batchSize)
173
-
174
- // Fetch batch concurrently
175
- const results = await Promise.all(
176
- batch.map(async (tag) => {
177
- const count = await getConversationCount(front, tag.id)
178
- return { id: tag.id, count }
179
- })
180
- )
181
-
182
- // Store results
183
- for (const { id, count } of results) {
184
- counts.set(id, count)
185
- }
186
-
187
- // Progress update
188
- const completed = Math.min(i + batchSize, tags.length)
189
- onProgress?.(completed, tags.length)
190
-
191
- // Rate limit delay between batches (not after last batch)
192
- if (i + batchSize < tags.length) {
193
- await sleep(delayMs)
194
- }
195
- }
196
-
197
- return counts
198
- }
199
-
200
- /**
201
- * Command: skill front tags list
202
- * List all tags with conversation counts
203
- */
204
- async function listTags(options: {
205
- json?: boolean
206
- unused?: boolean
207
- }): Promise<void> {
208
- try {
209
- const front = getFrontSdkClient()
210
- const tags = await fetchTagsRaw()
211
-
212
- // Fetch conversation counts for each tag
213
- const tagsWithCounts: TagWithCount[] = await Promise.all(
214
- tags.map(async (tag) => {
215
- const count = await getConversationCount(front, tag.id)
216
- return {
217
- id: tag.id,
218
- name: tag.name,
219
- highlight: tag.highlight ?? null,
220
- is_private: tag.is_private,
221
- description: tag.description ?? null,
222
- conversation_count: count,
223
- }
224
- })
225
- )
226
-
227
- // Filter to unused if requested
228
- const filteredTags = options.unused
229
- ? tagsWithCounts.filter((t) => t.conversation_count === 0)
230
- : tagsWithCounts
231
-
232
- if (options.json) {
233
- console.log(JSON.stringify(filteredTags, null, 2))
234
- return
235
- }
236
-
237
- if (filteredTags.length === 0) {
238
- if (options.unused) {
239
- console.log('\n✨ No unused tags found!\n')
240
- } else {
241
- console.log('\n📭 No tags found.\n')
242
- }
243
- return
244
- }
245
-
246
- const header = options.unused ? '🏷️ Unused Tags' : '🏷️ All Tags'
247
- console.log(`\n${header} (${filteredTags.length}):`)
248
- console.log('-'.repeat(80))
249
-
250
- // Table header
251
- console.log(
252
- `${'ID'.padEnd(20)} ${'Name'.padEnd(30)} ${'Color'.padEnd(10)} ${'Convos'.padEnd(8)}`
253
- )
254
- console.log('-'.repeat(80))
255
-
256
- for (const tag of filteredTags) {
257
- const highlight = tag.highlight || '-'
258
- const countStr =
259
- tag.conversation_count === 0 ? '0 ⚠️' : tag.conversation_count.toString()
260
-
261
- console.log(
262
- `${truncate(tag.id, 20).padEnd(20)} ${truncate(tag.name, 30).padEnd(30)} ${highlight.padEnd(10)} ${countStr.padEnd(8)}`
263
- )
264
- }
265
-
266
- console.log('')
267
-
268
- if (!options.unused) {
269
- const unusedCount = tagsWithCounts.filter(
270
- (t) => t.conversation_count === 0
271
- ).length
272
- if (unusedCount > 0) {
273
- console.log(
274
- `💡 Found ${unusedCount} unused tag(s). Use --unused to filter.\n`
275
- )
276
- }
277
- }
278
- } catch (error) {
279
- if (options.json) {
280
- console.error(
281
- JSON.stringify({
282
- error: error instanceof Error ? error.message : 'Unknown error',
283
- })
284
- )
285
- } else {
286
- console.error(
287
- 'Error:',
288
- error instanceof Error ? error.message : 'Unknown error'
289
- )
290
- }
291
- process.exit(1)
292
- }
293
- }
294
-
295
- /**
296
- * Command: skill front tags delete <id>
297
- * Delete a tag by ID
298
- */
299
- async function deleteTag(
300
- id: string,
301
- options: { force?: boolean }
302
- ): Promise<void> {
303
- try {
304
- const front = getFrontSdkClient()
305
-
306
- // Fetch tag details first
307
- const tag = await front.tags.get(id)
308
- const convCount = await getConversationCount(front, id)
309
-
310
- if (!options.force) {
311
- console.log(`\n🏷️ Tag: ${tag.name}`)
312
- console.log(` ID: ${tag.id}`)
313
- console.log(` Conversations: ${convCount}`)
314
-
315
- if (convCount > 0) {
316
- console.log(
317
- `\n⚠️ Warning: This tag is used in ${convCount} conversation(s).`
318
- )
319
- console.log(
320
- ' Deleting it will remove the tag from those conversations.'
321
- )
322
- }
323
-
324
- const confirmed = await confirm({
325
- message: `Are you sure you want to delete tag "${tag.name}"?`,
326
- default: false,
327
- })
328
-
329
- if (!confirmed) {
330
- console.log('\n❌ Cancelled.\n')
331
- return
332
- }
333
- }
334
-
335
- await front.tags.delete(id)
336
- console.log(`\n✅ Deleted tag "${tag.name}" (${id})\n`)
337
- } catch (error) {
338
- console.error(
339
- 'Error:',
340
- error instanceof Error ? error.message : 'Unknown error'
341
- )
342
- process.exit(1)
343
- }
344
- }
345
-
346
- /**
347
- * Command: skill front tags rename <id> <name>
348
- * Rename a tag
349
- */
350
- async function renameTag(id: string, newName: string): Promise<void> {
351
- try {
352
- const front = getFrontSdkClient()
353
-
354
- // Fetch current tag details
355
- const oldTag = await front.tags.get(id)
356
- const oldName = oldTag.name
357
-
358
- // Update the tag
359
- const updatedTag = await front.tags.update(id, { name: newName })
360
-
361
- console.log(`\n✅ Renamed tag:`)
362
- console.log(` "${oldName}" → "${updatedTag.name}"`)
363
- console.log(` ID: ${id}\n`)
364
- } catch (error) {
365
- console.error(
366
- 'Error:',
367
- error instanceof Error ? error.message : 'Unknown error'
368
- )
369
- process.exit(1)
370
- }
371
- }
372
-
373
- // ============================================================================
374
- // Cleanup Command
375
- // ============================================================================
376
-
377
- /**
378
- * Patterns for obsolete tags that should be deleted
379
- */
380
- const OBSOLETE_TAG_PATTERNS = [
381
- /^giftmas$/i,
382
- /^jan-2022$/i,
383
- /^feb-2022$/i,
384
- /^mar-2022$/i,
385
- /^apr-2022$/i,
386
- /^may-2022$/i,
387
- /^jun-2022$/i,
388
- /^jul-2022$/i,
389
- /^aug-2022$/i,
390
- /^sep-2022$/i,
391
- /^oct-2022$/i,
392
- /^nov-2022$/i,
393
- /^dec-2022$/i,
394
- // Gmail import artifacts (e.g., "INBOX", "STARRED", etc.)
395
- /^INBOX$/,
396
- /^STARRED$/,
397
- /^IMPORTANT$/,
398
- /^SENT$/,
399
- /^DRAFT$/,
400
- /^CATEGORY_/,
401
- /^UNREAD$/,
402
- ]
403
-
404
- /**
405
- * Convert name to canonical lowercase-hyphenated form
406
- */
407
- function toCanonicalForm(name: string): string {
408
- return name
409
- .toLowerCase()
410
- .replace(/[_\s]+/g, '-')
411
- .replace(/[^a-z0-9-]/g, '')
412
- .replace(/-+/g, '-')
413
- .replace(/^-|-$/g, '')
414
- }
415
-
416
- /**
417
- * Check if tag name matches obsolete patterns
418
- */
419
- function isObsoleteTag(name: string): boolean {
420
- return OBSOLETE_TAG_PATTERNS.some((pattern) => pattern.test(name))
421
- }
422
-
423
- interface CleanupPlan {
424
- duplicatesToDelete: Array<{
425
- tag: TagWithConversationCount
426
- keepTag: TagWithConversationCount
427
- reason: string
428
- }>
429
- caseVariantsToRename: Array<{
430
- tag: TagWithConversationCount
431
- newName: string
432
- canonical: TagWithConversationCount
433
- }>
434
- obsoleteToDelete: TagWithConversationCount[]
435
- missingToCreate: Array<{
436
- name: string
437
- highlight: string
438
- description: string
439
- }>
440
- }
441
-
442
- /**
443
- * Build cleanup plan from current tags
444
- */
445
- async function buildCleanupPlan(
446
- front: ReturnType<typeof createInstrumentedFrontClient>,
447
- tagsWithCounts: TagWithConversationCount[]
448
- ): Promise<CleanupPlan> {
449
- const plan: CleanupPlan = {
450
- duplicatesToDelete: [],
451
- caseVariantsToRename: [],
452
- obsoleteToDelete: [],
453
- missingToCreate: [],
454
- }
455
-
456
- // 1. Find exact duplicates
457
- const exactDuplicates = findExactDuplicates(tagsWithCounts)
458
- for (const group of exactDuplicates) {
459
- // Sort by conversation count descending - keep the one with most conversations
460
- const sorted = [...group.tags].sort(
461
- (a, b) => b.conversationCount - a.conversationCount
462
- )
463
- const keep = sorted[0]!
464
- for (const tag of sorted.slice(1)) {
465
- plan.duplicatesToDelete.push({
466
- tag,
467
- keepTag: keep,
468
- reason: `Exact duplicate of "${keep.name}" (keeping ${keep.conversationCount} convos)`,
469
- })
470
- }
471
- }
472
-
473
- // 2. Find case variants (but exclude tags already marked for deletion)
474
- const tagsToDeleteIds = new Set(plan.duplicatesToDelete.map((d) => d.tag.id))
475
- const remainingTags = tagsWithCounts.filter((t) => !tagsToDeleteIds.has(t.id))
476
- const caseVariants = findCaseVariants(remainingTags)
477
-
478
- for (const group of caseVariants) {
479
- // Sort by conversation count descending
480
- const sorted = [...group.variants].sort(
481
- (a, b) => b.conversationCount - a.conversationCount
482
- )
483
- const canonical = sorted[0]!
484
- const canonicalForm = toCanonicalForm(canonical.name)
485
-
486
- for (const variant of sorted.slice(1)) {
487
- // Don't rename if already in canonical form
488
- if (variant.name === canonicalForm) continue
489
-
490
- plan.caseVariantsToRename.push({
491
- tag: variant,
492
- newName: canonicalForm,
493
- canonical,
494
- })
495
- }
496
-
497
- // If the "canonical" tag (most convos) isn't in canonical form, rename it too
498
- if (canonical.name !== canonicalForm) {
499
- // Check if there's already a tag with the canonical form
500
- const existingCanonical = sorted.find((t) => t.name === canonicalForm)
501
- if (!existingCanonical) {
502
- plan.caseVariantsToRename.push({
503
- tag: canonical,
504
- newName: canonicalForm,
505
- canonical,
506
- })
507
- }
508
- }
509
- }
510
-
511
- // 3. Find obsolete tags
512
- for (const tag of tagsWithCounts) {
513
- if (isObsoleteTag(tag.name) && !tagsToDeleteIds.has(tag.id)) {
514
- plan.obsoleteToDelete.push(tag)
515
- }
516
- }
517
-
518
- // 4. Find missing standard tags
519
- const existingTagNames = new Set(
520
- tagsWithCounts.map((t) => t.name.toLowerCase())
521
- )
522
- const categoryConfigs = Object.values(DEFAULT_CATEGORY_TAG_MAPPING)
523
- for (const config of categoryConfigs) {
524
- if (!existingTagNames.has(config.tagName.toLowerCase())) {
525
- plan.missingToCreate.push({
526
- name: config.tagName,
527
- highlight: config.highlight,
528
- description: config.description ?? '',
529
- })
530
- }
531
- }
532
-
533
- return plan
534
- }
535
-
536
- /**
537
- * Print cleanup plan summary
538
- */
539
- function printCleanupPlan(plan: CleanupPlan): void {
540
- console.log('\n📋 Tag Cleanup Plan')
541
- console.log('='.repeat(60))
542
-
543
- // Duplicates to delete
544
- if (plan.duplicatesToDelete.length > 0) {
545
- console.log(
546
- `\n🔴 Duplicates to DELETE (${plan.duplicatesToDelete.length}):`
547
- )
548
- for (const item of plan.duplicatesToDelete) {
549
- console.log(
550
- ` - "${item.tag.name}" (${item.tag.conversationCount} convos) → ${item.reason}`
551
- )
552
- }
553
- }
554
-
555
- // Case variants to rename
556
- if (plan.caseVariantsToRename.length > 0) {
557
- console.log(
558
- `\n🟡 Case variants to RENAME (${plan.caseVariantsToRename.length}):`
559
- )
560
- for (const item of plan.caseVariantsToRename) {
561
- console.log(
562
- ` - "${item.tag.name}" → "${item.newName}" (merge with ${item.canonical.conversationCount} convos)`
563
- )
564
- }
565
- }
566
-
567
- // Obsolete to delete
568
- if (plan.obsoleteToDelete.length > 0) {
569
- console.log(
570
- `\n🗑️ Obsolete tags to DELETE (${plan.obsoleteToDelete.length}):`
571
- )
572
- for (const tag of plan.obsoleteToDelete) {
573
- console.log(` - "${tag.name}" (${tag.conversationCount} convos)`)
574
- }
575
- }
576
-
577
- // Missing to create
578
- if (plan.missingToCreate.length > 0) {
579
- console.log(
580
- `\n🟢 Missing standard tags to CREATE (${plan.missingToCreate.length}):`
581
- )
582
- for (const item of plan.missingToCreate) {
583
- console.log(` - "${item.name}" (${item.highlight})`)
584
- }
585
- }
586
-
587
- // Summary totals
588
- const totalChanges =
589
- plan.duplicatesToDelete.length +
590
- plan.caseVariantsToRename.length +
591
- plan.obsoleteToDelete.length +
592
- plan.missingToCreate.length
593
-
594
- console.log('\n' + '='.repeat(60))
595
- console.log(`📊 Total changes: ${totalChanges}`)
596
- console.log(` - Delete duplicates: ${plan.duplicatesToDelete.length}`)
597
- console.log(` - Rename variants: ${plan.caseVariantsToRename.length}`)
598
- console.log(` - Delete obsolete: ${plan.obsoleteToDelete.length}`)
599
- console.log(` - Create missing: ${plan.missingToCreate.length}`)
600
-
601
- if (totalChanges === 0) {
602
- console.log('\n✨ No cleanup needed - tags are in good shape!')
603
- }
604
- }
605
-
606
- /**
607
- * Execute cleanup plan
608
- */
609
- async function executeCleanupPlan(
610
- front: ReturnType<typeof createInstrumentedFrontClient>,
611
- plan: CleanupPlan
612
- ): Promise<{ success: number; failed: number }> {
613
- const results = { success: 0, failed: 0 }
614
-
615
- // 1. Delete duplicates
616
- for (const item of plan.duplicatesToDelete) {
617
- try {
618
- console.log(` Deleting duplicate "${item.tag.name}"...`)
619
- await front.tags.delete(item.tag.id)
620
- console.log(` ✅ Deleted "${item.tag.name}"`)
621
- results.success++
622
- } catch (error) {
623
- console.log(
624
- ` ❌ Failed to delete "${item.tag.name}": ${error instanceof Error ? error.message : 'Unknown error'}`
625
- )
626
- results.failed++
627
- }
628
- }
629
-
630
- // 2. Rename case variants (we need to merge - can't just rename if target exists)
631
- for (const item of plan.caseVariantsToRename) {
632
- try {
633
- console.log(` Renaming "${item.tag.name}" → "${item.newName}"...`)
634
- await front.tags.update(item.tag.id, { name: item.newName })
635
- console.log(` ✅ Renamed "${item.tag.name}" → "${item.newName}"`)
636
- results.success++
637
- } catch (error) {
638
- // If rename fails (maybe tag with that name exists), try to delete instead
639
- const errMsg = error instanceof Error ? error.message : 'Unknown error'
640
- if (errMsg.includes('already exists') || errMsg.includes('duplicate')) {
641
- try {
642
- console.log(` Name exists, deleting "${item.tag.name}" instead...`)
643
- await front.tags.delete(item.tag.id)
644
- console.log(` ✅ Deleted "${item.tag.name}" (merged into existing)`)
645
- results.success++
646
- } catch (delError) {
647
- console.log(
648
- ` ❌ Failed to delete "${item.tag.name}": ${delError instanceof Error ? delError.message : 'Unknown error'}`
649
- )
650
- results.failed++
651
- }
652
- } else {
653
- console.log(` ❌ Failed to rename "${item.tag.name}": ${errMsg}`)
654
- results.failed++
655
- }
656
- }
657
- }
658
-
659
- // 3. Delete obsolete tags
660
- for (const tag of plan.obsoleteToDelete) {
661
- try {
662
- console.log(` Deleting obsolete "${tag.name}"...`)
663
- await front.tags.delete(tag.id)
664
- console.log(` ✅ Deleted "${tag.name}"`)
665
- results.success++
666
- } catch (error) {
667
- console.log(
668
- ` ❌ Failed to delete "${tag.name}": ${error instanceof Error ? error.message : 'Unknown error'}`
669
- )
670
- results.failed++
671
- }
672
- }
673
-
674
- // 4. Create missing standard tags
675
- for (const item of plan.missingToCreate) {
676
- try {
677
- console.log(` Creating "${item.name}"...`)
678
- await createTagRaw({
679
- name: item.name,
680
- highlight: item.highlight,
681
- description: item.description,
682
- })
683
- console.log(` ✅ Created "${item.name}"`)
684
- results.success++
685
- } catch (error) {
686
- console.log(
687
- ` ❌ Failed to create "${item.name}": ${error instanceof Error ? error.message : 'Unknown error'}`
688
- )
689
- results.failed++
690
- }
691
- }
692
-
693
- return results
694
- }
695
-
696
- /**
697
- * Command: skill front tags cleanup
698
- * Clean up tag issues: duplicates, case variants, obsolete tags
699
- */
700
- async function cleanupTags(options: { execute?: boolean }): Promise<void> {
701
- try {
702
- const front = getFrontSdkClient()
703
-
704
- console.log('\n🔍 Analyzing tags...')
705
-
706
- // Fetch all tags (raw fetch to avoid SDK validation issues)
707
- const tags = await fetchTagsRaw()
708
- console.log(` Found ${tags.length} tags`)
709
- console.log(' Fetching conversation counts (rate-limited)...')
710
-
711
- // Fetch conversation counts with rate limiting and progress
712
- const counts = await fetchConversationCountsRateLimited(tags, front, {
713
- delayMs: 150, // 150ms between batches
714
- batchSize: 5, // 5 concurrent requests per batch
715
- onProgress: (completed, total) => {
716
- const pct = Math.round((completed / total) * 100)
717
- process.stdout.write(`\r Progress: ${completed}/${total} (${pct}%)`)
718
- },
719
- })
720
- console.log('') // newline after progress
721
-
722
- // Build tag objects with counts
723
- const tagsWithCounts: TagWithConversationCount[] = tags.map((tag) => ({
724
- id: tag.id,
725
- name: tag.name,
726
- highlight: tag.highlight ?? null,
727
- is_private: tag.is_private,
728
- description: tag.description ?? null,
729
- conversationCount: counts.get(tag.id) ?? 0,
730
- _links: {
731
- self: '',
732
- related: { owner: '', children: '', conversations: '' },
733
- },
734
- })) as unknown as TagWithConversationCount[]
735
-
736
- // Build cleanup plan
737
- const plan = await buildCleanupPlan(front, tagsWithCounts)
738
-
739
- // Print the plan
740
- printCleanupPlan(plan)
741
-
742
- const totalChanges =
743
- plan.duplicatesToDelete.length +
744
- plan.caseVariantsToRename.length +
745
- plan.obsoleteToDelete.length +
746
- plan.missingToCreate.length
747
-
748
- if (totalChanges === 0) {
749
- console.log('')
750
- return
751
- }
752
-
753
- // If not executing, show dry-run notice
754
- if (!options.execute) {
755
- console.log('\n⚠️ DRY RUN - No changes made')
756
- console.log(' Use --execute to apply these changes\n')
757
- return
758
- }
759
-
760
- // Confirm before executing
761
- console.log('')
762
- const confirmed = await confirm({
763
- message: `Apply ${totalChanges} change(s)?`,
764
- default: false,
765
- })
766
-
767
- if (!confirmed) {
768
- console.log('\n❌ Cancelled.\n')
769
- return
770
- }
771
-
772
- // Execute the plan
773
- console.log('\n🚀 Executing cleanup...\n')
774
- const results = await executeCleanupPlan(front, plan)
775
-
776
- // Final summary
777
- console.log('\n' + '='.repeat(60))
778
- console.log('📊 Cleanup Complete')
779
- console.log(` ✅ Successful: ${results.success}`)
780
- console.log(` ❌ Failed: ${results.failed}`)
781
- console.log('')
782
- } catch (error) {
783
- console.error(
784
- 'Error:',
785
- error instanceof Error ? error.message : 'Unknown error'
786
- )
787
- process.exit(1)
788
- }
789
- }
790
-
791
- /**
792
- * Register tag commands with Commander
793
- */
794
- export function registerTagCommands(frontCommand: Command): void {
795
- const tags = frontCommand.command('tags').description('Manage Front tags')
796
-
797
- tags
798
- .command('list')
799
- .description('List all tags with conversation counts')
800
- .option('--json', 'Output as JSON')
801
- .option('--unused', 'Show only tags with 0 conversations')
802
- .action(listTags)
803
-
804
- tags
805
- .command('delete')
806
- .description('Delete a tag by ID')
807
- .argument('<id>', 'Tag ID (e.g., tag_xxx)')
808
- .option('-f, --force', 'Skip confirmation prompt')
809
- .action(deleteTag)
810
-
811
- tags
812
- .command('rename')
813
- .description('Rename a tag')
814
- .argument('<id>', 'Tag ID (e.g., tag_xxx)')
815
- .argument('<name>', 'New tag name')
816
- .action(renameTag)
817
-
818
- tags
819
- .command('cleanup')
820
- .description(
821
- 'Clean up tags: delete duplicates, merge case variants, remove obsolete, create missing standard tags'
822
- )
823
- .option('--execute', 'Actually apply changes (default is dry-run)', false)
824
- .action(cleanupTags)
825
- }