litclaude-ai 0.3.3 → 0.3.7

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 (56) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +5 -5
  3. package/README_ko-KR.md +5 -5
  4. package/RELEASE_CHECKLIST.md +8 -6
  5. package/cover.png +0 -0
  6. package/generate_cover.py +1 -1
  7. package/package.json +1 -1
  8. package/plugins/litclaude/.claude-plugin/plugin.json +1 -1
  9. package/plugins/litclaude/bin/litclaude-hook.js +42 -6
  10. package/plugins/litclaude/commands/dynamic-workflow.md +6 -6
  11. package/plugins/litclaude/commands/litresearch.md +37 -0
  12. package/plugins/litclaude/commands/review-work.md +5 -5
  13. package/plugins/litclaude/hooks/hooks.json +12 -0
  14. package/plugins/litclaude/lib/litgoal/autoloop.mjs +78 -0
  15. package/plugins/litclaude/lib/litgoal/cli.mjs +8 -0
  16. package/plugins/litclaude/skills/lit-loop/SKILL.md +12 -10
  17. package/plugins/litclaude/skills/lit-plan/SKILL.md +9 -10
  18. package/plugins/litclaude/skills/litgoal/SKILL.md +41 -5
  19. package/plugins/litclaude/skills/litresearch/SKILL.md +148 -0
  20. package/plugins/litclaude/skills/lsp-setup/SKILL.md +151 -0
  21. package/plugins/litclaude/skills/lsp-setup/references/bash/README.md +63 -0
  22. package/plugins/litclaude/skills/lsp-setup/references/c-cpp/README.md +72 -0
  23. package/plugins/litclaude/skills/lsp-setup/references/csharp/README.md +81 -0
  24. package/plugins/litclaude/skills/lsp-setup/references/dart/README.md +53 -0
  25. package/plugins/litclaude/skills/lsp-setup/references/elixir/README.md +57 -0
  26. package/plugins/litclaude/skills/lsp-setup/references/go/README.md +59 -0
  27. package/plugins/litclaude/skills/lsp-setup/references/haskell/README.md +64 -0
  28. package/plugins/litclaude/skills/lsp-setup/references/java/README.md +66 -0
  29. package/plugins/litclaude/skills/lsp-setup/references/julia/README.md +62 -0
  30. package/plugins/litclaude/skills/lsp-setup/references/kotlin/README.md +68 -0
  31. package/plugins/litclaude/skills/lsp-setup/references/lua/README.md +64 -0
  32. package/plugins/litclaude/skills/lsp-setup/references/php/README.md +56 -0
  33. package/plugins/litclaude/skills/lsp-setup/references/python/README.md +68 -0
  34. package/plugins/litclaude/skills/lsp-setup/references/ruby/README.md +62 -0
  35. package/plugins/litclaude/skills/lsp-setup/references/rust/README.md +61 -0
  36. package/plugins/litclaude/skills/lsp-setup/references/swift/README.md +60 -0
  37. package/plugins/litclaude/skills/lsp-setup/references/terraform/README.md +61 -0
  38. package/plugins/litclaude/skills/lsp-setup/references/typescript/README.md +77 -0
  39. package/plugins/litclaude/skills/lsp-setup/references/yaml/README.md +60 -0
  40. package/plugins/litclaude/skills/lsp-setup/references/zig/README.md +55 -0
  41. package/plugins/litclaude/skills/lsp-setup/scripts/detect-lsp.ts +220 -0
  42. package/plugins/litclaude/skills/lsp-setup/scripts/lsp-server-table.ts +146 -0
  43. package/plugins/litclaude/skills/lsp-setup/scripts/tsconfig.json +18 -0
  44. package/plugins/litclaude/skills/lsp-setup/scripts/verify-lsp.ts +242 -0
  45. package/plugins/litclaude/skills/start-work/SKILL.md +12 -13
  46. package/plugins/litclaude/skills/visual-qa/SKILL.md +259 -0
  47. package/plugins/litclaude/skills/visual-qa/scripts/ansi.ts +17 -0
  48. package/plugins/litclaude/skills/visual-qa/scripts/cli.ts +96 -0
  49. package/plugins/litclaude/skills/visual-qa/scripts/east-asian-width.ts +72 -0
  50. package/plugins/litclaude/skills/visual-qa/scripts/image-diff.ts +109 -0
  51. package/plugins/litclaude/skills/visual-qa/scripts/png-crc.ts +27 -0
  52. package/plugins/litclaude/skills/visual-qa/scripts/png-decode.ts +206 -0
  53. package/plugins/litclaude/skills/visual-qa/scripts/png-synth.ts +57 -0
  54. package/plugins/litclaude/skills/visual-qa/scripts/tui-grid.ts +88 -0
  55. package/plugins/litclaude/skills/visual-qa/scripts/types.ts +54 -0
  56. package/scripts/qa-portable-install.sh +1 -1
@@ -0,0 +1,206 @@
1
+ import { Buffer } from "node:buffer"
2
+ import { inflateSync } from "node:zlib"
3
+
4
+ import { PNG_SIGNATURE } from "./png-crc.ts"
5
+ import type { DecodedImage } from "./types.ts"
6
+
7
+ export class PngDecodeError extends Error {
8
+ readonly name = "PngDecodeError"
9
+ }
10
+
11
+ interface PngHeader {
12
+ readonly width: number
13
+ readonly height: number
14
+ readonly bitDepth: number
15
+ readonly colorType: number
16
+ readonly channels: number
17
+ }
18
+
19
+ interface PngChunk {
20
+ readonly type: string
21
+ readonly data: Buffer
22
+ }
23
+
24
+ function readChunks(buffer: Buffer): readonly PngChunk[] {
25
+ const chunks: PngChunk[] = []
26
+ let offset = 8
27
+ while (offset + 8 <= buffer.length) {
28
+ const length = buffer.readUInt32BE(offset)
29
+ const type = buffer.toString("ascii", offset + 4, offset + 8)
30
+ const dataStart = offset + 8
31
+ const dataEnd = dataStart + length
32
+ if (dataEnd + 4 > buffer.length) break
33
+ chunks.push({ type, data: buffer.subarray(dataStart, dataEnd) })
34
+ offset = dataEnd + 4
35
+ }
36
+ return chunks
37
+ }
38
+
39
+ function channelsForColorType(colorType: number): number {
40
+ switch (colorType) {
41
+ case 0:
42
+ return 1
43
+ case 2:
44
+ return 3
45
+ case 4:
46
+ return 2
47
+ case 6:
48
+ return 4
49
+ default:
50
+ throw new PngDecodeError(`unsupported color type ${colorType}`)
51
+ }
52
+ }
53
+
54
+ function parseHeader(data: Buffer): PngHeader {
55
+ if (data.length < 13) {
56
+ throw new PngDecodeError("invalid IHDR chunk length")
57
+ }
58
+ const colorType = data[9] ?? 0
59
+ return {
60
+ width: data.readUInt32BE(0),
61
+ height: data.readUInt32BE(4),
62
+ bitDepth: data[8] ?? 0,
63
+ colorType,
64
+ channels: channelsForColorType(colorType),
65
+ }
66
+ }
67
+
68
+ function paeth(a: number, b: number, c: number): number {
69
+ const p = a + b - c
70
+ const pa = Math.abs(p - a)
71
+ const pb = Math.abs(p - b)
72
+ const pc = Math.abs(p - c)
73
+ if (pa <= pb && pa <= pc) return a
74
+ if (pb <= pc) return b
75
+ return c
76
+ }
77
+
78
+ function unfilterRow(filterType: number, row: Buffer, prev: Buffer | null, bpp: number): Buffer {
79
+ const out = Buffer.alloc(row.length)
80
+ for (let i = 0; i < row.length; i++) {
81
+ const raw = row[i] ?? 0
82
+ const a = i >= bpp ? (out[i - bpp] ?? 0) : 0
83
+ const b = prev ? (prev[i] ?? 0) : 0
84
+ const c = i >= bpp && prev ? (prev[i - bpp] ?? 0) : 0
85
+ switch (filterType) {
86
+ case 0:
87
+ out[i] = raw
88
+ break
89
+ case 1:
90
+ out[i] = (raw + a) & 0xff
91
+ break
92
+ case 2:
93
+ out[i] = (raw + b) & 0xff
94
+ break
95
+ case 3:
96
+ out[i] = (raw + ((a + b) >> 1)) & 0xff
97
+ break
98
+ case 4:
99
+ out[i] = (raw + paeth(a, b, c)) & 0xff
100
+ break
101
+ default:
102
+ throw new PngDecodeError(`unsupported filter type ${filterType}`)
103
+ }
104
+ }
105
+ return out
106
+ }
107
+
108
+ function decodePixels(idat: Buffer, width: number, height: number, bpp: number): Buffer {
109
+ const inflated = inflateSync(idat)
110
+ const rowBytes = width * bpp
111
+ if (inflated.length < height * (rowBytes + 1)) {
112
+ throw new PngDecodeError("truncated image data")
113
+ }
114
+ const pixels = Buffer.alloc(width * height * bpp)
115
+ let prev: Buffer | null = null
116
+ for (let y = 0; y < height; y++) {
117
+ const rowStart = y * (rowBytes + 1)
118
+ const filterType = inflated[rowStart] ?? 0
119
+ const filtered = inflated.subarray(rowStart + 1, rowStart + 1 + rowBytes)
120
+ const row = unfilterRow(filterType, filtered, prev, bpp)
121
+ row.copy(pixels, y * rowBytes)
122
+ prev = row
123
+ }
124
+ return pixels
125
+ }
126
+
127
+ function normalizeToRgba(
128
+ pixels: Buffer,
129
+ pixelCount: number,
130
+ channels: number,
131
+ ): { readonly rgba: Uint8Array; readonly hasTransparent: boolean } {
132
+ const rgba = new Uint8Array(pixelCount * 4)
133
+ let hasTransparent = false
134
+ for (let i = 0; i < pixelCount; i++) {
135
+ const src = i * channels
136
+ let r = 0
137
+ let g = 0
138
+ let b = 0
139
+ let a = 255
140
+ switch (channels) {
141
+ case 1: {
142
+ const v = pixels[src] ?? 0
143
+ r = v
144
+ g = v
145
+ b = v
146
+ break
147
+ }
148
+ case 2: {
149
+ const v = pixels[src] ?? 0
150
+ r = v
151
+ g = v
152
+ b = v
153
+ a = pixels[src + 1] ?? 255
154
+ break
155
+ }
156
+ case 3: {
157
+ r = pixels[src] ?? 0
158
+ g = pixels[src + 1] ?? 0
159
+ b = pixels[src + 2] ?? 0
160
+ break
161
+ }
162
+ default: {
163
+ r = pixels[src] ?? 0
164
+ g = pixels[src + 1] ?? 0
165
+ b = pixels[src + 2] ?? 0
166
+ a = pixels[src + 3] ?? 255
167
+ }
168
+ }
169
+ const dst = i * 4
170
+ rgba[dst] = r
171
+ rgba[dst + 1] = g
172
+ rgba[dst + 2] = b
173
+ rgba[dst + 3] = a
174
+ if (a < 255) hasTransparent = true
175
+ }
176
+ return { rgba, hasTransparent }
177
+ }
178
+
179
+ export function decodePng(buffer: Buffer): DecodedImage {
180
+ if (buffer.length < 8 || !buffer.subarray(0, 8).equals(PNG_SIGNATURE)) {
181
+ throw new PngDecodeError("not a PNG file (bad signature)")
182
+ }
183
+ const chunks = readChunks(buffer)
184
+ const ihdr = chunks.find((chunk) => chunk.type === "IHDR")
185
+ if (ihdr === undefined) {
186
+ throw new PngDecodeError("missing IHDR chunk")
187
+ }
188
+ const header = parseHeader(ihdr.data)
189
+ if (header.bitDepth !== 8) {
190
+ throw new PngDecodeError(`unsupported bit depth ${header.bitDepth}`)
191
+ }
192
+ const idatChunks = chunks.filter((chunk) => chunk.type === "IDAT")
193
+ if (idatChunks.length === 0) {
194
+ throw new PngDecodeError("missing IDAT chunk")
195
+ }
196
+ const idat = Buffer.concat(idatChunks.map((chunk) => chunk.data))
197
+ const pixels = decodePixels(idat, header.width, header.height, header.channels)
198
+ const normalized = normalizeToRgba(pixels, header.width * header.height, header.channels)
199
+ return {
200
+ width: header.width,
201
+ height: header.height,
202
+ rgba: normalized.rgba,
203
+ hasAlphaChannel: header.colorType === 4 || header.colorType === 6,
204
+ hasTransparentPixels: normalized.hasTransparent,
205
+ }
206
+ }
@@ -0,0 +1,57 @@
1
+ import { Buffer } from "node:buffer"
2
+ import { deflateSync } from "node:zlib"
3
+
4
+ import { crc32, PNG_SIGNATURE } from "./png-crc.ts"
5
+
6
+ const BIT_DEPTH_8 = 8
7
+ const COLOR_TYPE_RGBA = 6
8
+ const FILTER_NONE = 0
9
+ const RGBA_CHANNELS = 4
10
+
11
+ function pngChunk(type: string, data: Buffer): Buffer {
12
+ const typeBuffer = Buffer.from(type, "ascii")
13
+ const length = Buffer.alloc(4)
14
+ length.writeUInt32BE(data.length, 0)
15
+ const crcBuffer = Buffer.alloc(4)
16
+ crcBuffer.writeUInt32BE(crc32(Buffer.concat([typeBuffer, data])), 0)
17
+ return Buffer.concat([length, typeBuffer, data, crcBuffer])
18
+ }
19
+
20
+ export function encodeRgbaPng(width: number, height: number, rgba: Uint8Array): Buffer {
21
+ const rowBytes = width * RGBA_CHANNELS
22
+ const raw = Buffer.alloc(height * (rowBytes + 1))
23
+ for (let y = 0; y < height; y++) {
24
+ const rowStart = y * (rowBytes + 1)
25
+ raw[rowStart] = FILTER_NONE
26
+ for (let x = 0; x < rowBytes; x++) {
27
+ raw[rowStart + 1 + x] = rgba[y * rowBytes + x] ?? 0
28
+ }
29
+ }
30
+ const header = Buffer.alloc(13)
31
+ header.writeUInt32BE(width, 0)
32
+ header.writeUInt32BE(height, 4)
33
+ header[8] = BIT_DEPTH_8
34
+ header[9] = COLOR_TYPE_RGBA
35
+ return Buffer.concat([
36
+ PNG_SIGNATURE,
37
+ pngChunk("IHDR", header),
38
+ pngChunk("IDAT", deflateSync(raw)),
39
+ pngChunk("IEND", Buffer.alloc(0)),
40
+ ])
41
+ }
42
+
43
+ export function solidRgba(
44
+ width: number,
45
+ height: number,
46
+ color: readonly [number, number, number, number],
47
+ ): Uint8Array {
48
+ const rgba = new Uint8Array(width * height * RGBA_CHANNELS)
49
+ for (let pixel = 0; pixel < width * height; pixel++) {
50
+ const offset = pixel * RGBA_CHANNELS
51
+ rgba[offset] = color[0]
52
+ rgba[offset + 1] = color[1]
53
+ rgba[offset + 2] = color[2]
54
+ rgba[offset + 3] = color[3]
55
+ }
56
+ return rgba
57
+ }
@@ -0,0 +1,88 @@
1
+ import { hasAnsi, stripAnsi } from "./ansi.ts"
2
+ import { charWidth, stringWidth } from "./east-asian-width.ts"
3
+ import type { OverflowLine, TuiCheckResult } from "./types.ts"
4
+
5
+ const BOX_DRAWING_START = 0x2500
6
+ const BOX_DRAWING_END = 0x257f
7
+ const MAX_WIDE_COLUMNS = 64
8
+
9
+ function splitLines(text: string): string[] {
10
+ const lines = text.split(/\r?\n/)
11
+ if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop()
12
+ return lines
13
+ }
14
+
15
+ function isFrameLine(plain: string): boolean {
16
+ for (const char of plain) {
17
+ const codePoint = char.codePointAt(0)
18
+ if (codePoint !== undefined && codePoint >= BOX_DRAWING_START && codePoint <= BOX_DRAWING_END) {
19
+ return true
20
+ }
21
+ }
22
+ return false
23
+ }
24
+
25
+ function wideStartColumns(plain: string): number[] {
26
+ const columns: number[] = []
27
+ let column = 0
28
+ for (const char of plain) {
29
+ const codePoint = char.codePointAt(0)
30
+ if (codePoint === undefined) continue
31
+ const width = charWidth(codePoint)
32
+ if (width === 2) columns.push(column)
33
+ column += width
34
+ }
35
+ return columns
36
+ }
37
+
38
+ function summarize(
39
+ lineCount: number,
40
+ maxWidth: number,
41
+ expectedColumns: number,
42
+ overflowCount: number,
43
+ borderMisaligned: boolean,
44
+ containsAnsi: boolean,
45
+ ): string {
46
+ const parts = [`${lineCount} line(s)`, `max width ${maxWidth}/${expectedColumns}`]
47
+ if (overflowCount > 0) parts.push(`${overflowCount} overflow line(s)`)
48
+ if (borderMisaligned) parts.push("borders misaligned")
49
+ if (containsAnsi) parts.push("contains ANSI")
50
+ return `${parts.join("; ")}.`
51
+ }
52
+
53
+ export function checkTui(text: string, expectedColumns: number): TuiCheckResult {
54
+ const lines = splitLines(text)
55
+ const lineWidths: number[] = []
56
+ const overflowLines: OverflowLine[] = []
57
+ const wideColumns = new Set<number>()
58
+ const frameWidths = new Set<number>()
59
+
60
+ for (let index = 0; index < lines.length; index++) {
61
+ const plain = stripAnsi(lines[index] ?? "")
62
+ const width = stringWidth(plain)
63
+ lineWidths.push(width)
64
+ if (expectedColumns > 0 && width > expectedColumns) {
65
+ overflowLines.push({ line: index + 1, width })
66
+ }
67
+ if (isFrameLine(plain)) frameWidths.add(width)
68
+ for (const column of wideStartColumns(plain)) {
69
+ if (wideColumns.size < MAX_WIDE_COLUMNS) wideColumns.add(column)
70
+ }
71
+ }
72
+
73
+ const maxWidth = lineWidths.reduce((max, width) => (width > max ? width : max), 0)
74
+ const borderMisaligned = frameWidths.size > 1
75
+ const containsAnsi = hasAnsi(text)
76
+ return {
77
+ command: "tui-check",
78
+ expectedColumns,
79
+ lineCount: lines.length,
80
+ lineWidths,
81
+ maxWidth,
82
+ overflowLines,
83
+ borderMisaligned,
84
+ wideCharColumns: [...wideColumns].sort((a, b) => a - b),
85
+ hasAnsi: containsAnsi,
86
+ summary: summarize(lines.length, maxWidth, expectedColumns, overflowLines.length, borderMisaligned, containsAnsi),
87
+ }
88
+ }
@@ -0,0 +1,54 @@
1
+ export interface DecodedImage {
2
+ readonly width: number
3
+ readonly height: number
4
+ readonly rgba: Uint8Array
5
+ readonly hasAlphaChannel: boolean
6
+ readonly hasTransparentPixels: boolean
7
+ }
8
+
9
+ export interface Hotspot {
10
+ readonly gridX: number
11
+ readonly gridY: number
12
+ readonly x: number
13
+ readonly y: number
14
+ readonly width: number
15
+ readonly height: number
16
+ readonly diffRatio: number
17
+ }
18
+
19
+ export interface ImageDimensions {
20
+ readonly width: number
21
+ readonly height: number
22
+ }
23
+
24
+ export interface ImageDiffResult {
25
+ readonly command: "image-diff"
26
+ readonly dimensionsMatch: boolean
27
+ readonly reference: ImageDimensions
28
+ readonly actual: ImageDimensions
29
+ readonly totalPixels: number
30
+ readonly diffPixels: number
31
+ readonly diffRatio: number
32
+ readonly similarityScore: number
33
+ readonly alphaChannelIntact: boolean
34
+ readonly hotspots: readonly Hotspot[]
35
+ readonly summary: string
36
+ }
37
+
38
+ export interface OverflowLine {
39
+ readonly line: number
40
+ readonly width: number
41
+ }
42
+
43
+ export interface TuiCheckResult {
44
+ readonly command: "tui-check"
45
+ readonly expectedColumns: number
46
+ readonly lineCount: number
47
+ readonly lineWidths: readonly number[]
48
+ readonly maxWidth: number
49
+ readonly overflowLines: readonly OverflowLine[]
50
+ readonly borderMisaligned: boolean
51
+ readonly wideCharColumns: readonly number[]
52
+ readonly hasAnsi: boolean
53
+ readonly summary: string
54
+ }
@@ -104,7 +104,7 @@ echo "DOCTOR_PASS" >> "$EVIDENCE"
104
104
 
105
105
  if command -v claude >/dev/null 2>&1; then
106
106
  run_step CLAUDE_PLUGIN_DETAILS claude plugin details litclaude@litclaude-ai
107
- if ! grep -Eq "Skills \\([0-9]+\\)" "$EVIDENCE" || ! grep -q "lit-loop" "$EVIDENCE" || ! grep -q "lit-plan" "$EVIDENCE" || ! grep -q "Hooks (4)" "$EVIDENCE"; then
107
+ if ! grep -Eq "Skills \\([0-9]+\\)" "$EVIDENCE" || ! grep -q "lit-loop" "$EVIDENCE" || ! grep -q "lit-plan" "$EVIDENCE" || ! grep -q "Hooks (5)" "$EVIDENCE"; then
108
108
  echo "CLAUDE_PLUGIN_DETAILS_FAIL: expected LitClaude skills/hooks inventory" >> "$EVIDENCE"
109
109
  echo "CLAUDE_PLUGIN_DETAILS_FAIL"
110
110
  exit 1