litclaude-ai 0.3.3 → 0.3.6

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 (54) hide show
  1. package/CHANGELOG.md +54 -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 +34 -4
  10. package/plugins/litclaude/commands/dynamic-workflow.md +6 -6
  11. package/plugins/litclaude/commands/review-work.md +5 -5
  12. package/plugins/litclaude/hooks/hooks.json +12 -0
  13. package/plugins/litclaude/lib/litgoal/autoloop.mjs +78 -0
  14. package/plugins/litclaude/lib/litgoal/cli.mjs +8 -0
  15. package/plugins/litclaude/skills/lit-loop/SKILL.md +12 -10
  16. package/plugins/litclaude/skills/lit-plan/SKILL.md +9 -10
  17. package/plugins/litclaude/skills/litgoal/SKILL.md +41 -5
  18. package/plugins/litclaude/skills/lsp-setup/SKILL.md +151 -0
  19. package/plugins/litclaude/skills/lsp-setup/references/bash/README.md +63 -0
  20. package/plugins/litclaude/skills/lsp-setup/references/c-cpp/README.md +72 -0
  21. package/plugins/litclaude/skills/lsp-setup/references/csharp/README.md +81 -0
  22. package/plugins/litclaude/skills/lsp-setup/references/dart/README.md +53 -0
  23. package/plugins/litclaude/skills/lsp-setup/references/elixir/README.md +57 -0
  24. package/plugins/litclaude/skills/lsp-setup/references/go/README.md +59 -0
  25. package/plugins/litclaude/skills/lsp-setup/references/haskell/README.md +64 -0
  26. package/plugins/litclaude/skills/lsp-setup/references/java/README.md +66 -0
  27. package/plugins/litclaude/skills/lsp-setup/references/julia/README.md +62 -0
  28. package/plugins/litclaude/skills/lsp-setup/references/kotlin/README.md +68 -0
  29. package/plugins/litclaude/skills/lsp-setup/references/lua/README.md +64 -0
  30. package/plugins/litclaude/skills/lsp-setup/references/php/README.md +56 -0
  31. package/plugins/litclaude/skills/lsp-setup/references/python/README.md +68 -0
  32. package/plugins/litclaude/skills/lsp-setup/references/ruby/README.md +62 -0
  33. package/plugins/litclaude/skills/lsp-setup/references/rust/README.md +61 -0
  34. package/plugins/litclaude/skills/lsp-setup/references/swift/README.md +60 -0
  35. package/plugins/litclaude/skills/lsp-setup/references/terraform/README.md +61 -0
  36. package/plugins/litclaude/skills/lsp-setup/references/typescript/README.md +77 -0
  37. package/plugins/litclaude/skills/lsp-setup/references/yaml/README.md +60 -0
  38. package/plugins/litclaude/skills/lsp-setup/references/zig/README.md +55 -0
  39. package/plugins/litclaude/skills/lsp-setup/scripts/detect-lsp.ts +220 -0
  40. package/plugins/litclaude/skills/lsp-setup/scripts/lsp-server-table.ts +146 -0
  41. package/plugins/litclaude/skills/lsp-setup/scripts/tsconfig.json +18 -0
  42. package/plugins/litclaude/skills/lsp-setup/scripts/verify-lsp.ts +242 -0
  43. package/plugins/litclaude/skills/start-work/SKILL.md +12 -13
  44. package/plugins/litclaude/skills/visual-qa/SKILL.md +259 -0
  45. package/plugins/litclaude/skills/visual-qa/scripts/ansi.ts +17 -0
  46. package/plugins/litclaude/skills/visual-qa/scripts/cli.ts +96 -0
  47. package/plugins/litclaude/skills/visual-qa/scripts/east-asian-width.ts +72 -0
  48. package/plugins/litclaude/skills/visual-qa/scripts/image-diff.ts +109 -0
  49. package/plugins/litclaude/skills/visual-qa/scripts/png-crc.ts +27 -0
  50. package/plugins/litclaude/skills/visual-qa/scripts/png-decode.ts +206 -0
  51. package/plugins/litclaude/skills/visual-qa/scripts/png-synth.ts +57 -0
  52. package/plugins/litclaude/skills/visual-qa/scripts/tui-grid.ts +88 -0
  53. package/plugins/litclaude/skills/visual-qa/scripts/types.ts +54 -0
  54. package/scripts/qa-portable-install.sh +1 -1
@@ -0,0 +1,60 @@
1
+ # YAML — LSP setup (Claude Code / LitClaude)
2
+
3
+ - **Recommended server:** `yaml-language-server --stdio`
4
+ - **Extensions:** `.yaml .yml`
5
+ - **Install hint:** `npm install -g yaml-language-server`
6
+
7
+ ## Install
8
+
9
+ - **macOS:** `npm install -g yaml-language-server`
10
+ - **Linux:** `npm install -g yaml-language-server`
11
+ - **Windows:** `npm install -g yaml-language-server` (PowerShell)
12
+
13
+ Confirm it resolves:
14
+
15
+ ```bash
16
+ command -v yaml-language-server
17
+ ```
18
+
19
+ ## Configure
20
+
21
+ Add a `yaml` entry to `plugins/litclaude/.lsp.json`:
22
+
23
+ ```json
24
+ {
25
+ "yaml": {
26
+ "command": ["yaml-language-server", "--stdio"],
27
+ "extensionToLanguage": {
28
+ ".yaml": "yaml",
29
+ ".yml": "yaml"
30
+ }
31
+ }
32
+ }
33
+ ```
34
+
35
+ Claude Code routes `.yaml`/`.yml` edits to `yaml-language-server` via the map.
36
+ Schema association is the main reason to tune the server, but those options
37
+ travel through the editor's LSP `initializationOptions` (`yaml.schemas`,
38
+ `yaml.schemaStore`), not `.lsp.json`. An inline
39
+ `# yaml-language-server: $schema=<url>` modeline works without any config:
40
+
41
+ ```yaml
42
+ # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
43
+ ```
44
+
45
+ ## Alternatives
46
+
47
+ - `redhat.vscode-yaml` bundles the same server in editors.
48
+ - `yamllint` standalone for style/lint-only checks.
49
+
50
+ ## Troubleshooting
51
+ - **PATH:** `yaml-language-server` on PATH; reopen shell after `npm -g` install.
52
+ - **No validation:** no schema matched — add a `$schema` modeline or a `yaml.schemas` mapping in your editor's init options.
53
+ - **Wrong schema applied:** SchemaStore guessed by filename; pin explicitly with a modeline.
54
+
55
+ ## Verify
56
+
57
+ ```bash
58
+ node --experimental-strip-types ../../scripts/verify-lsp.ts path/to/file.yaml
59
+ # or: bun ../../scripts/verify-lsp.ts path/to/file.yaml
60
+ ```
@@ -0,0 +1,55 @@
1
+ # Zig — LSP setup (Claude Code / LitClaude)
2
+
3
+ - **Recommended server:** `zls`
4
+ - **Extensions:** `.zig .zon`
5
+ - **Install hint:** `https://github.com/zigtools/zls`
6
+
7
+ ## Install
8
+
9
+ ZLS (the Zig Language Server) must be built against the **same Zig version** you use.
10
+ See `https://github.com/zigtools/zls`.
11
+
12
+ - **macOS:** `brew install zls`
13
+ - **Linux:** download a prebuilt release matching your Zig version, or `zig build -Doptimize=ReleaseSafe` from the zls source
14
+ - **Windows:** download the matching release from the zls GitHub releases, or build from source
15
+
16
+ Confirm it resolves:
17
+
18
+ ```bash
19
+ command -v zls
20
+ ```
21
+
22
+ ## Configure
23
+
24
+ Add a `zig` entry to `plugins/litclaude/.lsp.json`:
25
+
26
+ ```json
27
+ {
28
+ "zig": {
29
+ "command": ["zls"],
30
+ "extensionToLanguage": {
31
+ ".zig": "zig",
32
+ ".zon": "zig"
33
+ }
34
+ }
35
+ }
36
+ ```
37
+
38
+ Claude Code routes `.zig`/`.zon` edits to `zls` via the map. No extra
39
+ configuration is normally required.
40
+
41
+ ## Alternatives
42
+
43
+ None.
44
+
45
+ ## Troubleshooting
46
+ - **VERSION MATCH (critical):** zls version MUST match your zig version — build/install zls against the exact same Zig. A mismatch causes crashes, parse errors, or silent failures. After upgrading Zig, upgrade/rebuild zls too.
47
+ - **PATH:** `zls` must be on PATH; reopen the shell after install.
48
+ - **zig not found:** zls invokes `zig` for builds — make sure `zig` itself is also on PATH.
49
+
50
+ ## Verify
51
+
52
+ ```bash
53
+ node --experimental-strip-types ../../scripts/verify-lsp.ts path/to/file.zig
54
+ # or: bun ../../scripts/verify-lsp.ts path/to/file.zig
55
+ ```
@@ -0,0 +1,220 @@
1
+ #!/usr/bin/env node
2
+ // detect-lsp.ts <targetDir> [--json] [--config=<path>] — scan a directory for
3
+ // source languages and report, per detected language: the recommended language
4
+ // server, whether its executable is on PATH, an install hint, and whether the
5
+ // LitClaude .lsp.json already declares that language.
6
+ //
7
+ // Runtime: Node 22.6+ with `node --experimental-strip-types`, or `bun`.
8
+ // node --experimental-strip-types scripts/detect-lsp.ts <dir>
9
+ // bun scripts/detect-lsp.ts <dir>
10
+
11
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"
12
+ import { delimiter, extname, join, sep } from "node:path"
13
+ import process from "node:process"
14
+
15
+ import { LANGUAGES, type LanguageServer, LSP_CONFIG_FILE } from "./lsp-server-table.ts"
16
+
17
+ const SKIP_DIRECTORIES = new Set<string>([
18
+ "node_modules",
19
+ ".git",
20
+ "dist",
21
+ "build",
22
+ ".next",
23
+ "out",
24
+ "target",
25
+ ".venv",
26
+ "venv",
27
+ "vendor",
28
+ ".cache",
29
+ "__pycache__",
30
+ ".turbo",
31
+ "coverage",
32
+ ])
33
+
34
+ const MAX_FILES = 50_000
35
+
36
+ interface ConfigState {
37
+ readonly path: string
38
+ readonly exists: boolean
39
+ readonly languages: readonly string[]
40
+ }
41
+
42
+ interface DetectionResult {
43
+ readonly server: LanguageServer
44
+ readonly executable: string
45
+ readonly installed: boolean
46
+ readonly resolvedPath: string | null
47
+ readonly configured: boolean
48
+ }
49
+
50
+ function collectExtensions(root: string): ReadonlySet<string> {
51
+ const found = new Set<string>()
52
+ const stack: string[] = [root]
53
+ let visited = 0
54
+
55
+ while (stack.length > 0) {
56
+ const current = stack.pop()
57
+ if (current === undefined) break
58
+
59
+ let entries: string[]
60
+ try {
61
+ entries = readdirSync(current)
62
+ } catch {
63
+ continue
64
+ }
65
+
66
+ for (const entry of entries) {
67
+ if (visited >= MAX_FILES) return found
68
+ const fullPath = join(current, entry)
69
+
70
+ let kind: "dir" | "file" | "other" = "other"
71
+ try {
72
+ const stat = statSync(fullPath)
73
+ kind = stat.isDirectory() ? "dir" : stat.isFile() ? "file" : "other"
74
+ } catch {
75
+ continue
76
+ }
77
+
78
+ if (kind === "dir") {
79
+ if (!SKIP_DIRECTORIES.has(entry)) stack.push(fullPath)
80
+ } else if (kind === "file") {
81
+ visited += 1
82
+ const ext = extname(entry).toLowerCase()
83
+ if (ext.length > 0) found.add(ext)
84
+ }
85
+ }
86
+ }
87
+
88
+ return found
89
+ }
90
+
91
+ function pathDirectories(): readonly string[] {
92
+ return (process.env["PATH"] ?? "").split(delimiter).filter((dir: string) => dir.length > 0)
93
+ }
94
+
95
+ function resolveExecutable(command: string): string | null {
96
+ const extensions =
97
+ process.platform === "win32" ? (process.env["PATHEXT"]?.split(";") ?? [".EXE", ".CMD", ".BAT"]) : [""]
98
+ const bases =
99
+ command.includes("/") || command.includes(sep)
100
+ ? [command]
101
+ : pathDirectories().map((dir: string) => join(dir, command))
102
+
103
+ for (const base of bases) {
104
+ for (const ext of extensions) {
105
+ const candidate = ext.length > 0 ? `${base}${ext}` : base
106
+ try {
107
+ if (existsSync(candidate) && statSync(candidate).isFile()) return candidate
108
+ } catch {
109
+ continue
110
+ }
111
+ }
112
+ }
113
+ return null
114
+ }
115
+
116
+ function isRecord(value: unknown): value is Record<string, unknown> {
117
+ return typeof value === "object" && value !== null && !Array.isArray(value)
118
+ }
119
+
120
+ // LitClaude .lsp.json is keyed by language name:
121
+ // { "typescript": { "command": [...], "extensionToLanguage": {...} } }
122
+ function parseConfiguredLanguages(path: string): readonly string[] {
123
+ let parsed: unknown
124
+ try {
125
+ parsed = JSON.parse(readFileSync(path, "utf-8"))
126
+ } catch {
127
+ return []
128
+ }
129
+ if (!isRecord(parsed)) return []
130
+ return Object.keys(parsed)
131
+ }
132
+
133
+ function readConfigState(configPath: string): ConfigState {
134
+ if (!existsSync(configPath)) return { path: configPath, exists: false, languages: [] }
135
+ return { path: configPath, exists: true, languages: parseConfiguredLanguages(configPath) }
136
+ }
137
+
138
+ function detect(root: string, configured: readonly string[]): readonly DetectionResult[] {
139
+ const extensions = collectExtensions(root)
140
+ const configuredSet = new Set(configured)
141
+ const results: DetectionResult[] = []
142
+
143
+ for (const server of LANGUAGES) {
144
+ if (!server.extensions.some((ext: string) => extensions.has(ext))) continue
145
+
146
+ const executable = server.command[0] ?? server.language
147
+ const resolvedPath = resolveExecutable(executable)
148
+ results.push({
149
+ server,
150
+ executable,
151
+ installed: resolvedPath !== null,
152
+ resolvedPath,
153
+ configured: configuredSet.has(server.language),
154
+ })
155
+ }
156
+
157
+ return results
158
+ }
159
+
160
+ function renderReport(root: string, results: readonly DetectionResult[], config: ConfigState): string {
161
+ const lines: string[] = [`LSP setup scan: ${root}`]
162
+ lines.push(
163
+ `Config file: ${config.path} (${config.exists ? `present: ${config.languages.length} language(s)` : "absent"})`,
164
+ "",
165
+ )
166
+
167
+ if (results.length === 0) {
168
+ lines.push("No languages with a recommended LSP server were detected here.")
169
+ return lines.join("\n")
170
+ }
171
+
172
+ lines.push("DETECTED LANGUAGES (recommended server per language)")
173
+ for (const result of results) {
174
+ const mark = result.installed ? "OK " : "MISS"
175
+ const state = result.installed ? `installed (${result.resolvedPath})` : "NOT installed"
176
+ const config = result.configured ? "declared in .lsp.json" : "NOT in .lsp.json"
177
+ lines.push(
178
+ `[${mark}] ${result.server.language.padEnd(12)} exe=${result.executable} ${state} ${config}`,
179
+ )
180
+ if (!result.installed) lines.push(` install: ${result.server.installHint}`)
181
+ }
182
+
183
+ const missing = results.filter((result) => !result.installed)
184
+ const notConfigured = results.filter((result) => !result.configured)
185
+ lines.push(
186
+ "",
187
+ missing.length === 0
188
+ ? `All ${results.length} detected server(s) installed.`
189
+ : `${missing.length}/${results.length} server(s) NOT installed: ${missing.map((m) => m.server.language).join(", ")}`,
190
+ notConfigured.length === 0
191
+ ? "All detected languages are declared in .lsp.json."
192
+ : `${notConfigured.length} language(s) absent from .lsp.json: ${notConfigured.map((m) => m.server.language).join(", ")}`,
193
+ "Next: read references/<language>/README.md, then add the language to plugins/litclaude/.lsp.json.",
194
+ )
195
+ return lines.join("\n")
196
+ }
197
+
198
+ function main(): void {
199
+ const args = process.argv.slice(2)
200
+ const wantsJson = args.includes("--json")
201
+ const configFlag = args.find((arg: string) => arg.startsWith("--config="))
202
+ const configPath = configFlag ? configFlag.slice("--config=".length) : LSP_CONFIG_FILE
203
+ const root = args.find((arg: string) => !arg.startsWith("--")) ?? process.cwd()
204
+
205
+ if (!existsSync(root)) {
206
+ process.stderr.write(`detect-lsp: target directory does not exist: ${root}\n`)
207
+ process.exit(2)
208
+ }
209
+
210
+ const config = readConfigState(configPath)
211
+ const results = detect(root, config.languages)
212
+
213
+ if (wantsJson) {
214
+ process.stdout.write(`${JSON.stringify({ root, config, results }, null, 2)}\n`)
215
+ return
216
+ }
217
+ process.stdout.write(`${renderReport(root, results, config)}\n`)
218
+ }
219
+
220
+ main()
@@ -0,0 +1,146 @@
1
+ // Standalone snapshot of the primary language server per supported language,
2
+ // embedded so detect-lsp.ts runs without any external dependency in any project.
3
+ // Each entry mirrors the per-language references/<language>/README.md and the
4
+ // LitClaude .lsp.json shape, which is keyed by language name:
5
+ // { "<language>": { "command": [...], "extensionToLanguage": { ".ext": "<lang>" } } }
6
+ // Edit this table when a reference README changes.
7
+
8
+ export interface LanguageServer {
9
+ readonly language: string
10
+ readonly command: readonly string[]
11
+ readonly extensions: readonly string[]
12
+ readonly installHint: string
13
+ }
14
+
15
+ export const LANGUAGES: readonly LanguageServer[] = [
16
+ {
17
+ language: "typescript",
18
+ command: ["typescript-language-server", "--stdio"],
19
+ extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
20
+ installHint: "npm install -g typescript-language-server typescript",
21
+ },
22
+ {
23
+ language: "python",
24
+ command: ["basedpyright-langserver", "--stdio"],
25
+ extensions: [".py", ".pyi"],
26
+ installHint: "pip install basedpyright (or: uv tool install basedpyright)",
27
+ },
28
+ {
29
+ language: "go",
30
+ command: ["gopls"],
31
+ extensions: [".go"],
32
+ installHint: "go install golang.org/x/tools/gopls@latest",
33
+ },
34
+ {
35
+ language: "rust",
36
+ command: ["rust-analyzer"],
37
+ extensions: [".rs"],
38
+ installHint: "rustup component add rust-analyzer",
39
+ },
40
+ {
41
+ language: "c-cpp",
42
+ command: ["clangd", "--background-index", "--clang-tidy"],
43
+ extensions: [".c", ".cpp", ".cc", ".cxx", ".c++", ".h", ".hpp", ".hh", ".hxx", ".h++"],
44
+ installHint: "macOS: brew install llvm | Linux: apt install clangd | https://clangd.llvm.org/installation",
45
+ },
46
+ {
47
+ language: "java",
48
+ command: ["jdtls"],
49
+ extensions: [".java"],
50
+ installHint: "macOS: brew install jdtls | https://github.com/eclipse-jdtls/eclipse.jdt.ls",
51
+ },
52
+ {
53
+ language: "kotlin",
54
+ command: ["kotlin-lsp"],
55
+ extensions: [".kt", ".kts"],
56
+ installHint: "https://github.com/Kotlin/kotlin-lsp",
57
+ },
58
+ {
59
+ language: "csharp",
60
+ command: ["csharp-ls"],
61
+ extensions: [".cs"],
62
+ installHint: "dotnet tool install -g csharp-ls",
63
+ },
64
+ {
65
+ language: "razor",
66
+ command: ["roslyn-language-server", "--stdio"],
67
+ extensions: [".razor", ".cshtml"],
68
+ installHint: "dotnet tool install -g roslyn-language-server --prerelease (see references/csharp/README.md)",
69
+ },
70
+ {
71
+ language: "swift",
72
+ command: ["sourcekit-lsp"],
73
+ extensions: [".swift", ".objc", ".objcpp"],
74
+ installHint: "Included with Xcode (xcode-select --install) or the Swift toolchain",
75
+ },
76
+ {
77
+ language: "ruby",
78
+ command: ["rubocop", "--lsp"],
79
+ extensions: [".rb", ".rake", ".gemspec", ".ru"],
80
+ installHint: "gem install rubocop (the builtin runs `rubocop --lsp`)",
81
+ },
82
+ {
83
+ language: "php",
84
+ command: ["intelephense", "--stdio"],
85
+ extensions: [".php"],
86
+ installHint: "npm install -g intelephense",
87
+ },
88
+ {
89
+ language: "dart",
90
+ command: ["dart", "language-server", "--lsp"],
91
+ extensions: [".dart"],
92
+ installHint: "Included with the Dart/Flutter SDK",
93
+ },
94
+ {
95
+ language: "elixir",
96
+ command: ["elixir-ls"],
97
+ extensions: [".ex", ".exs"],
98
+ installHint: "https://github.com/elixir-lsp/elixir-ls",
99
+ },
100
+ {
101
+ language: "zig",
102
+ command: ["zls"],
103
+ extensions: [".zig", ".zon"],
104
+ installHint: "https://github.com/zigtools/zls (match zls version to your zig version)",
105
+ },
106
+ {
107
+ language: "lua",
108
+ command: ["lua-language-server"],
109
+ extensions: [".lua"],
110
+ installHint: "macOS: brew install lua-language-server | https://github.com/LuaLS/lua-language-server",
111
+ },
112
+ {
113
+ language: "bash",
114
+ command: ["bash-language-server", "start"],
115
+ extensions: [".sh", ".bash", ".zsh", ".ksh"],
116
+ installHint: "npm install -g bash-language-server",
117
+ },
118
+ {
119
+ language: "yaml",
120
+ command: ["yaml-language-server", "--stdio"],
121
+ extensions: [".yaml", ".yml"],
122
+ installHint: "npm install -g yaml-language-server",
123
+ },
124
+ {
125
+ language: "terraform",
126
+ command: ["terraform-ls", "serve"],
127
+ extensions: [".tf", ".tfvars"],
128
+ installHint: "macOS: brew install hashicorp/tap/terraform-ls | https://github.com/hashicorp/terraform-ls",
129
+ },
130
+ {
131
+ language: "haskell",
132
+ command: ["haskell-language-server-wrapper", "--lsp"],
133
+ extensions: [".hs", ".lhs"],
134
+ installHint: "ghcup install hls",
135
+ },
136
+ {
137
+ language: "julia",
138
+ command: ["julia", "--startup-file=no", "--history-file=no", "-e", "using LanguageServer; runserver()"],
139
+ extensions: [".jl"],
140
+ installHint: "julia -e 'using Pkg; Pkg.add(\"LanguageServer\")' (see references/julia/README.md)",
141
+ },
142
+ ] as const
143
+
144
+ // The LitClaude LSP config file, relative to the plugin root. detect-lsp.ts also
145
+ // accepts an explicit path so it can be pointed at a project copy.
146
+ export const LSP_CONFIG_FILE = "plugins/litclaude/.lsp.json"
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "strict": true,
7
+ "noUncheckedIndexedAccess": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "resolveJsonModule": true,
12
+ "noEmit": true,
13
+ "lib": ["ESNext"],
14
+ "types": ["node"],
15
+ "allowImportingTsExtensions": true
16
+ },
17
+ "include": ["*.ts"]
18
+ }