claude-opencode-viewer 2.6.46 → 2.6.48

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.
@@ -0,0 +1,74 @@
1
+ # Code Review Expert
2
+
3
+ A comprehensive code review skill for AI agents. Performs structured reviews with a senior engineer lens, covering architecture, security, performance, and code quality.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npx skills add sanyuan0704/sanyuan-skills --path skills/code-review-expert
9
+ ```
10
+
11
+ ## Features
12
+
13
+ - **SOLID Principles** - Detect SRP, OCP, LSP, ISP, DIP violations
14
+ - **Security Scan** - XSS, injection, SSRF, race conditions, auth gaps, secrets leakage
15
+ - **Performance** - N+1 queries, CPU hotspots, missing cache, memory issues
16
+ - **Error Handling** - Swallowed exceptions, async errors, missing boundaries
17
+ - **Boundary Conditions** - Null handling, empty collections, off-by-one, numeric limits
18
+ - **Removal Planning** - Identify dead code with safe deletion plans
19
+
20
+ ## Usage
21
+
22
+ After installation, simply run:
23
+
24
+ ```
25
+ /code-review-expert
26
+ ```
27
+
28
+ The skill will automatically review your current git changes.
29
+
30
+ ## Workflow
31
+
32
+ 1. **Preflight** - Scope changes via `git diff`
33
+ 2. **SOLID + Architecture** - Check design principles
34
+ 3. **Removal Candidates** - Find dead/unused code
35
+ 4. **Security Scan** - Vulnerability detection
36
+ 5. **Code Quality** - Error handling, performance, boundaries
37
+ 6. **Output** - Findings by severity (P0-P3)
38
+ 7. **Confirmation** - Ask user before implementing fixes
39
+
40
+ ## Severity Levels
41
+
42
+ | Level | Name | Action |
43
+ |-------|------|--------|
44
+ | P0 | Critical | Must block merge |
45
+ | P1 | High | Should fix before merge |
46
+ | P2 | Medium | Fix or create follow-up |
47
+ | P3 | Low | Optional improvement |
48
+
49
+ ## Structure
50
+
51
+ ```
52
+ code-review-expert/
53
+ ├── SKILL.md # Main skill definition
54
+ ├── agents/
55
+ │ └── agent.yaml # Agent interface config
56
+ └── references/
57
+ ├── solid-checklist.md # SOLID smell prompts
58
+ ├── security-checklist.md # Security & reliability
59
+ ├── code-quality-checklist.md # Error, perf, boundaries
60
+ └── removal-plan.md # Deletion planning template
61
+ ```
62
+
63
+ ## References
64
+
65
+ Each checklist provides detailed prompts and anti-patterns:
66
+
67
+ - **solid-checklist.md** - SOLID violations + common code smells
68
+ - **security-checklist.md** - OWASP risks, race conditions, crypto, supply chain
69
+ - **code-quality-checklist.md** - Error handling, caching, N+1, null safety
70
+ - **removal-plan.md** - Safe vs deferred deletion with rollback plans
71
+
72
+ ## License
73
+
74
+ MIT
@@ -0,0 +1,156 @@
1
+ ---
2
+ name: code-review-expert
3
+ description: "Expert code review of current git changes with a senior engineer lens. Detects SOLID violations, security risks, and proposes actionable improvements."
4
+ ---
5
+
6
+ # Code Review Expert
7
+
8
+ ## Overview
9
+
10
+ Perform a structured review of the current git changes with focus on SOLID, architecture, removal candidates, and security risks. Default to review-only output unless the user asks to implement changes.
11
+
12
+ ## Severity Levels
13
+
14
+ | Level | Name | Description | Action |
15
+ |-------|------|-------------|--------|
16
+ | **P0** | Critical | Security vulnerability, data loss risk, correctness bug | Must block merge |
17
+ | **P1** | High | Logic error, significant SOLID violation, performance regression | Should fix before merge |
18
+ | **P2** | Medium | Code smell, maintainability concern, minor SOLID violation | Fix in this PR or create follow-up |
19
+ | **P3** | Low | Style, naming, minor suggestion | Optional improvement |
20
+
21
+ ## Workflow
22
+
23
+ ### 1) Preflight context
24
+
25
+ - Use `git status -sb`, `git diff --stat`, and `git diff` to scope changes.
26
+ - If needed, use `rg` or `grep` to find related modules, usages, and contracts.
27
+ - Identify entry points, ownership boundaries, and critical paths (auth, payments, data writes, network).
28
+
29
+ **Edge cases:**
30
+ - **No changes**: If `git diff` is empty, inform user and ask if they want to review staged changes or a specific commit range.
31
+ - **Large diff (>500 lines)**: Summarize by file first, then review in batches by module/feature area.
32
+ - **Mixed concerns**: Group findings by logical feature, not just file order.
33
+
34
+ ### 2) SOLID + architecture smells
35
+
36
+ - Load `references/solid-checklist.md` for specific prompts.
37
+ - Look for:
38
+ - **SRP**: Overloaded modules with unrelated responsibilities.
39
+ - **OCP**: Frequent edits to add behavior instead of extension points.
40
+ - **LSP**: Subclasses that break expectations or require type checks.
41
+ - **ISP**: Wide interfaces with unused methods.
42
+ - **DIP**: High-level logic tied to low-level implementations.
43
+ - When you propose a refactor, explain *why* it improves cohesion/coupling and outline a minimal, safe split.
44
+ - If refactor is non-trivial, propose an incremental plan instead of a large rewrite.
45
+
46
+ ### 3) Removal candidates + iteration plan
47
+
48
+ - Load `references/removal-plan.md` for template.
49
+ - Identify code that is unused, redundant, or feature-flagged off.
50
+ - Distinguish **safe delete now** vs **defer with plan**.
51
+ - Provide a follow-up plan with concrete steps and checkpoints (tests/metrics).
52
+
53
+ ### 4) Security and reliability scan
54
+
55
+ - Load `references/security-checklist.md` for coverage.
56
+ - Check for:
57
+ - XSS, injection (SQL/NoSQL/command), SSRF, path traversal
58
+ - AuthZ/AuthN gaps, missing tenancy checks
59
+ - Secret leakage or API keys in logs/env/files
60
+ - Rate limits, unbounded loops, CPU/memory hotspots
61
+ - Unsafe deserialization, weak crypto, insecure defaults
62
+ - **Race conditions**: concurrent access, check-then-act, TOCTOU, missing locks
63
+ - Call out both **exploitability** and **impact**.
64
+
65
+ ### 5) Code quality scan
66
+
67
+ - Load `references/code-quality-checklist.md` for coverage.
68
+ - Check for:
69
+ - **Error handling**: swallowed exceptions, overly broad catch, missing error handling, async errors
70
+ - **Performance**: N+1 queries, CPU-intensive ops in hot paths, missing cache, unbounded memory
71
+ - **Boundary conditions**: null/undefined handling, empty collections, numeric boundaries, off-by-one
72
+ - Flag issues that may cause silent failures or production incidents.
73
+
74
+ ### 6) Output format
75
+
76
+ Structure your review as follows:
77
+
78
+ ```markdown
79
+ ## Code Review Summary
80
+
81
+ **Files reviewed**: X files, Y lines changed
82
+ **Overall assessment**: [APPROVE / REQUEST_CHANGES / COMMENT]
83
+
84
+ ---
85
+
86
+ ## Findings
87
+
88
+ ### P0 - Critical
89
+ (none or list)
90
+
91
+ ### P1 - High
92
+ 1. **[file:line]** Brief title
93
+ - Description of issue
94
+ - Suggested fix
95
+
96
+ ### P2 - Medium
97
+ 2. (continue numbering across sections)
98
+ - ...
99
+
100
+ ### P3 - Low
101
+ ...
102
+
103
+ ---
104
+
105
+ ## Removal/Iteration Plan
106
+ (if applicable)
107
+
108
+ ## Additional Suggestions
109
+ (optional improvements, not blocking)
110
+ ```
111
+
112
+ **Inline comments**: Use this format for file-specific findings:
113
+ ```
114
+ ::code-comment{file="path/to/file.ts" line="42" severity="P1"}
115
+ Description of the issue and suggested fix.
116
+ ::
117
+ ```
118
+
119
+ **Clean review**: If no issues found, explicitly state:
120
+ - What was checked
121
+ - Any areas not covered (e.g., "Did not verify database migrations")
122
+ - Residual risks or recommended follow-up tests
123
+
124
+ ### 7) Next steps confirmation
125
+
126
+ After presenting findings, ask user how to proceed:
127
+
128
+ ```markdown
129
+ ---
130
+
131
+ ## Next Steps
132
+
133
+ I found X issues (P0: _, P1: _, P2: _, P3: _).
134
+
135
+ **How would you like to proceed?**
136
+
137
+ 1. **Fix all** - I'll implement all suggested fixes
138
+ 2. **Fix P0/P1 only** - Address critical and high priority issues
139
+ 3. **Fix specific items** - Tell me which issues to fix
140
+ 4. **No changes** - Review complete, no implementation needed
141
+
142
+ Please choose an option or provide specific instructions.
143
+ ```
144
+
145
+ **Important**: Do NOT implement any changes until user explicitly confirms. This is a review-first workflow.
146
+
147
+ ## Resources
148
+
149
+ ### references/
150
+
151
+ | File | Purpose |
152
+ |------|---------|
153
+ | `solid-checklist.md` | SOLID smell prompts and refactor heuristics |
154
+ | `security-checklist.md` | Web/app security and runtime risk checklist |
155
+ | `code-quality-checklist.md` | Error handling, performance, boundary conditions |
156
+ | `removal-plan.md` | Template for deletion candidates and follow-up plan |
@@ -0,0 +1,7 @@
1
+ interface:
2
+ display_name: "Code Review Expert"
3
+ short_description: "Senior engineer code review: SOLID, security, performance, error handling"
4
+ default_prompt: "Review current git changes for SOLID violations, security risks, race conditions, error handling issues, performance problems, and boundary condition bugs."
5
+
6
+ # Agent-agnostic skill - works with any LLM provider.
7
+ # No provider-specific configuration required.
@@ -0,0 +1,130 @@
1
+ # Code Quality Checklist
2
+
3
+ ## Error Handling
4
+
5
+ ### Anti-patterns to Flag
6
+
7
+ - **Swallowed exceptions**: Empty catch blocks or catch with only logging
8
+ ```javascript
9
+ try { ... } catch (e) { } // Silent failure
10
+ try { ... } catch (e) { console.log(e) } // Log and forget
11
+ ```
12
+ - **Overly broad catch**: Catching `Exception`/`Error` base class instead of specific types
13
+ - **Error information leakage**: Stack traces or internal details exposed to users
14
+ - **Missing error handling**: No try-catch around fallible operations (I/O, network, parsing)
15
+ - **Async error handling**: Unhandled promise rejections, missing `.catch()`, no error boundary
16
+
17
+ ### Best Practices to Check
18
+
19
+ - [ ] Errors are caught at appropriate boundaries
20
+ - [ ] Error messages are user-friendly (no internal details exposed)
21
+ - [ ] Errors are logged with sufficient context for debugging
22
+ - [ ] Async errors are properly propagated or handled
23
+ - [ ] Fallback behavior is defined for recoverable errors
24
+ - [ ] Critical errors trigger alerts/monitoring
25
+
26
+ ### Questions to Ask
27
+ - "What happens when this operation fails?"
28
+ - "Will the caller know something went wrong?"
29
+ - "Is there enough context to debug this error?"
30
+
31
+ ---
32
+
33
+ ## Performance & Caching
34
+
35
+ ### CPU-Intensive Operations
36
+
37
+ - **Expensive operations in hot paths**: Regex compilation, JSON parsing, crypto in loops
38
+ - **Blocking main thread**: Sync I/O, heavy computation without worker/async
39
+ - **Unnecessary recomputation**: Same calculation done multiple times
40
+ - **Missing memoization**: Pure functions called repeatedly with same inputs
41
+
42
+ ### Database & I/O
43
+
44
+ - **N+1 queries**: Loop that makes a query per item instead of batch
45
+ ```javascript
46
+ // Bad: N+1
47
+ for (const id of ids) {
48
+ const user = await db.query(`SELECT * FROM users WHERE id = ?`, id)
49
+ }
50
+ // Good: Batch
51
+ const users = await db.query(`SELECT * FROM users WHERE id IN (?)`, ids)
52
+ ```
53
+ - **Missing indexes**: Queries on unindexed columns
54
+ - **Over-fetching**: SELECT * when only few columns needed
55
+ - **No pagination**: Loading entire dataset into memory
56
+
57
+ ### Caching Issues
58
+
59
+ - **Missing cache for expensive operations**: Repeated API calls, DB queries, computations
60
+ - **Cache without TTL**: Stale data served indefinitely
61
+ - **Cache without invalidation strategy**: Data updated but cache not cleared
62
+ - **Cache key collisions**: Insufficient key uniqueness
63
+ - **Caching user-specific data globally**: Security/privacy issue
64
+
65
+ ### Memory
66
+
67
+ - **Unbounded collections**: Arrays/maps that grow without limit
68
+ - **Large object retention**: Holding references preventing GC
69
+ - **String concatenation in loops**: Use StringBuilder/join instead
70
+ - **Loading large files entirely**: Use streaming instead
71
+
72
+ ### Questions to Ask
73
+ - "What's the time complexity of this operation?"
74
+ - "How does this behave with 10x/100x data?"
75
+ - "Is this result cacheable? Should it be?"
76
+ - "Can this be batched instead of one-by-one?"
77
+
78
+ ---
79
+
80
+ ## Boundary Conditions
81
+
82
+ ### Null/Undefined Handling
83
+
84
+ - **Missing null checks**: Accessing properties on potentially null objects
85
+ - **Truthy/falsy confusion**: `if (value)` when `0` or `""` are valid
86
+ - **Optional chaining overuse**: `a?.b?.c?.d` hiding structural issues
87
+ - **Null vs undefined inconsistency**: Mixed usage without clear convention
88
+
89
+ ### Empty Collections
90
+
91
+ - **Empty array not handled**: Code assumes array has items
92
+ - **Empty object edge case**: `for...in` or `Object.keys` on empty object
93
+ - **First/last element access**: `arr[0]` or `arr[arr.length-1]` without length check
94
+
95
+ ### Numeric Boundaries
96
+
97
+ - **Division by zero**: Missing check before division
98
+ - **Integer overflow**: Large numbers exceeding safe integer range
99
+ - **Floating point comparison**: Using `===` instead of epsilon comparison
100
+ - **Negative values**: Index or count that shouldn't be negative
101
+ - **Off-by-one errors**: Loop bounds, array slicing, pagination
102
+
103
+ ### String Boundaries
104
+
105
+ - **Empty string**: Not handled as edge case
106
+ - **Whitespace-only string**: Passes truthy check but is effectively empty
107
+ - **Very long strings**: No length limits causing memory/display issues
108
+ - **Unicode edge cases**: Emoji, RTL text, combining characters
109
+
110
+ ### Common Patterns to Flag
111
+
112
+ ```javascript
113
+ // Dangerous: no null check
114
+ const name = user.profile.name
115
+
116
+ // Dangerous: array access without check
117
+ const first = items[0]
118
+
119
+ // Dangerous: division without check
120
+ const avg = total / count
121
+
122
+ // Dangerous: truthy check excludes valid values
123
+ if (value) { ... } // fails for 0, "", false
124
+ ```
125
+
126
+ ### Questions to Ask
127
+ - "What if this is null/undefined?"
128
+ - "What if this collection is empty?"
129
+ - "What's the valid range for this number?"
130
+ - "What happens at the boundaries (0, -1, MAX_INT)?"
@@ -0,0 +1,52 @@
1
+ # Removal and Iteration Plan Template
2
+
3
+ ## Priority Levels
4
+
5
+ - [ ] **P0**: Immediate removal needed (security risk, significant cost, blocking other work)
6
+ - [ ] **P1**: Remove in current sprint
7
+ - [ ] **P2**: Backlog / next iteration
8
+
9
+ ---
10
+
11
+ ## Safe to Remove Now
12
+
13
+ ### Item: [Name/Description]
14
+
15
+ | Field | Details |
16
+ |-------|---------|
17
+ | **Location** | `path/to/file.ts:line` |
18
+ | **Rationale** | Why this should be removed |
19
+ | **Evidence** | Unused (no references), dead feature flag, deprecated API |
20
+ | **Impact** | None / Low - no active consumers |
21
+ | **Deletion steps** | 1. Remove code 2. Remove tests 3. Remove config |
22
+ | **Verification** | Run tests, check no runtime errors, monitor logs |
23
+
24
+ ---
25
+
26
+ ## Defer Removal (Plan Required)
27
+
28
+ ### Item: [Name/Description]
29
+
30
+ | Field | Details |
31
+ |-------|---------|
32
+ | **Location** | `path/to/file.ts:line` |
33
+ | **Why defer** | Active consumers, needs migration, stakeholder sign-off |
34
+ | **Preconditions** | Feature flag off for 2 weeks, telemetry shows 0 usage |
35
+ | **Breaking changes** | List any API/contract changes |
36
+ | **Migration plan** | Steps for consumers to migrate |
37
+ | **Timeline** | Target date or sprint |
38
+ | **Owner** | Person/team responsible |
39
+ | **Validation** | Metrics to confirm safe removal (error rates, usage counts) |
40
+ | **Rollback plan** | How to restore if issues found |
41
+
42
+ ---
43
+
44
+ ## Checklist Before Removal
45
+
46
+ - [ ] Searched codebase for all references (`rg`, `grep`)
47
+ - [ ] Checked for dynamic/reflection-based usage
48
+ - [ ] Verified no external consumers (APIs, SDKs, docs)
49
+ - [ ] Feature flag telemetry reviewed (if applicable)
50
+ - [ ] Tests updated/removed
51
+ - [ ] Documentation updated
52
+ - [ ] Team notified (if shared code)
@@ -0,0 +1,118 @@
1
+ # Security and Reliability Checklist
2
+
3
+ ## Input/Output Safety
4
+
5
+ - **XSS**: Unsafe HTML injection, `dangerouslySetInnerHTML`, unescaped templates, innerHTML assignments
6
+ - **Injection**: SQL/NoSQL/command/GraphQL injection via string concatenation or template literals
7
+ - **SSRF**: User-controlled URLs reaching internal services without allowlist validation
8
+ - **Path traversal**: User input in file paths without sanitization (`../` attacks)
9
+ - **Prototype pollution**: Unsafe object merging in JavaScript (`Object.assign`, spread with user input)
10
+
11
+ ## AuthN/AuthZ
12
+
13
+ - Missing tenant or ownership checks for read/write operations
14
+ - New endpoints without auth guards or RBAC enforcement
15
+ - Trusting client-provided roles/flags/IDs
16
+ - Broken access control (IDOR - Insecure Direct Object Reference)
17
+ - Session fixation or weak session management
18
+
19
+ ## JWT & Token Security
20
+
21
+ - Algorithm confusion attacks (accepting `none` or `HS256` when expecting `RS256`)
22
+ - Weak or hardcoded secrets
23
+ - Missing expiration (`exp`) or not validating it
24
+ - Sensitive data in JWT payload (tokens are base64, not encrypted)
25
+ - Not validating `iss` (issuer) or `aud` (audience)
26
+
27
+ ## Secrets and PII
28
+
29
+ - API keys, tokens, or credentials in code/config/logs
30
+ - Secrets in git history or environment variables exposed to client
31
+ - Excessive logging of PII or sensitive payloads
32
+ - Missing data masking in error messages
33
+
34
+ ## Supply Chain & Dependencies
35
+
36
+ - Unpinned dependencies allowing malicious updates
37
+ - Dependency confusion (private package name collision)
38
+ - Importing from untrusted sources or CDNs without integrity checks
39
+ - Outdated dependencies with known CVEs
40
+
41
+ ## CORS & Headers
42
+
43
+ - Overly permissive CORS (`Access-Control-Allow-Origin: *` with credentials)
44
+ - Missing security headers (CSP, X-Frame-Options, X-Content-Type-Options)
45
+ - Exposed internal headers or stack traces
46
+
47
+ ## Runtime Risks
48
+
49
+ - Unbounded loops, recursive calls, or large in-memory buffers
50
+ - Missing timeouts, retries, or rate limiting on external calls
51
+ - Blocking operations on request path (sync I/O in async context)
52
+ - Resource exhaustion (file handles, connections, memory)
53
+ - ReDoS (Regular Expression Denial of Service)
54
+
55
+ ## Cryptography
56
+
57
+ - Weak algorithms (MD5, SHA1 for security purposes)
58
+ - Hardcoded IVs or salts
59
+ - Using encryption without authentication (ECB mode, no HMAC)
60
+ - Insufficient key length
61
+
62
+ ## Race Conditions
63
+
64
+ Race conditions are subtle bugs that cause intermittent failures and security vulnerabilities. Pay special attention to:
65
+
66
+ ### Shared State Access
67
+ - Multiple threads/goroutines/async tasks accessing shared variables without synchronization
68
+ - Global state or singletons modified concurrently
69
+ - Lazy initialization without proper locking (double-checked locking issues)
70
+ - Non-thread-safe collections used in concurrent context
71
+
72
+ ### Check-Then-Act (TOCTOU)
73
+ - `if (exists) then use` patterns without atomic operations
74
+ - `if (authorized) then perform` where authorization can change
75
+ - File existence check followed by file operation
76
+ - Balance check followed by deduction (financial operations)
77
+ - Inventory check followed by order placement
78
+
79
+ ### Database Concurrency
80
+ - Missing optimistic locking (`version` column, `updated_at` checks)
81
+ - Missing pessimistic locking (`SELECT FOR UPDATE`)
82
+ - Read-modify-write without transaction isolation
83
+ - Counter increments without atomic operations (`UPDATE SET count = count + 1`)
84
+ - Unique constraint violations in concurrent inserts
85
+
86
+ ### Distributed Systems
87
+ - Missing distributed locks for shared resources
88
+ - Leader election race conditions
89
+ - Cache invalidation races (stale reads after writes)
90
+ - Event ordering dependencies without proper sequencing
91
+ - Split-brain scenarios in cluster operations
92
+
93
+ ### Common Patterns to Flag
94
+ ```
95
+ # Dangerous patterns:
96
+ if not exists(key): # TOCTOU
97
+ create(key)
98
+
99
+ value = get(key) # Read-modify-write
100
+ value += 1
101
+ set(key, value)
102
+
103
+ if user.balance >= amount: # Check-then-act
104
+ user.balance -= amount
105
+ ```
106
+
107
+ ### Questions to Ask
108
+ - "What happens if two requests hit this code simultaneously?"
109
+ - "Is this operation atomic or can it be interrupted?"
110
+ - "What shared state does this code access?"
111
+ - "How does this behave under high concurrency?"
112
+
113
+ ## Data Integrity
114
+
115
+ - Missing transactions, partial writes, or inconsistent state updates
116
+ - Weak validation before persistence (type coercion issues)
117
+ - Missing idempotency for retryable operations
118
+ - Lost updates due to concurrent modifications
@@ -0,0 +1,65 @@
1
+ # SOLID Smell Prompts
2
+
3
+ ## SRP (Single Responsibility)
4
+
5
+ - File owns unrelated concerns (e.g., HTTP + DB + domain rules in one file)
6
+ - Large class/module with low cohesion or multiple reasons to change
7
+ - Functions that orchestrate many unrelated steps
8
+ - God objects that know too much about the system
9
+ - **Ask**: "What is the single reason this module would change?"
10
+
11
+ ## OCP (Open/Closed)
12
+
13
+ - Adding a new behavior requires editing many switch/if blocks
14
+ - Feature growth requires modifying core logic rather than extending
15
+ - No plugin/strategy/hook points for variation
16
+ - **Ask**: "Can I add a new variant without touching existing code?"
17
+
18
+ ## LSP (Liskov Substitution)
19
+
20
+ - Subclass checks for concrete type or throws for base method
21
+ - Overridden methods weaken preconditions or strengthen postconditions
22
+ - Subclass ignores or no-ops parent behavior
23
+ - **Ask**: "Can I substitute any subclass without the caller knowing?"
24
+
25
+ ## ISP (Interface Segregation)
26
+
27
+ - Interfaces with many methods, most unused by implementers
28
+ - Callers depend on broad interfaces for narrow needs
29
+ - Empty/stub implementations of interface methods
30
+ - **Ask**: "Do all implementers use all methods?"
31
+
32
+ ## DIP (Dependency Inversion)
33
+
34
+ - High-level logic depends on concrete IO, storage, or network types
35
+ - Hard-coded implementations instead of abstractions or injection
36
+ - Import chains that couple business logic to infrastructure
37
+ - **Ask**: "Can I swap the implementation without changing business logic?"
38
+
39
+ ---
40
+
41
+ ## Common Code Smells (Beyond SOLID)
42
+
43
+ | Smell | Signs |
44
+ |-------|-------|
45
+ | **Long method** | Function > 30 lines, multiple levels of nesting |
46
+ | **Feature envy** | Method uses more data from another class than its own |
47
+ | **Data clumps** | Same group of parameters passed together repeatedly |
48
+ | **Primitive obsession** | Using strings/numbers instead of domain types |
49
+ | **Shotgun surgery** | One change requires edits across many files |
50
+ | **Divergent change** | One file changes for many unrelated reasons |
51
+ | **Dead code** | Unreachable or never-called code |
52
+ | **Speculative generality** | Abstractions for hypothetical future needs |
53
+ | **Magic numbers/strings** | Hardcoded values without named constants |
54
+
55
+ ---
56
+
57
+ ## Refactor Heuristics
58
+
59
+ 1. **Split by responsibility, not by size** - A small file can still violate SRP
60
+ 2. **Introduce abstraction only when needed** - Wait for the second use case
61
+ 3. **Keep refactors incremental** - Isolate behavior before moving
62
+ 4. **Preserve behavior first** - Add tests before restructuring
63
+ 5. **Name things by intent** - If naming is hard, the abstraction might be wrong
64
+ 6. **Prefer composition over inheritance** - Inheritance creates tight coupling
65
+ 7. **Make illegal states unrepresentable** - Use types to enforce invariants
package/index-pc.html CHANGED
@@ -390,8 +390,45 @@
390
390
  content: '';
391
391
  animation: loading-dots 1.2s steps(4, end) infinite;
392
392
  }
393
- #terminal.transitioning #switch-overlay { display: flex;
393
+ #terminal.transitioning #switch-overlay { display: flex; }
394
+ #init-overlay {
395
+ display: none;
396
+ position: absolute;
397
+ top: 0; left: 0; right: 0; bottom: 0;
398
+ background: #0a0a0a;
399
+ color: #ccc;
400
+ align-items: center;
401
+ justify-content: center;
402
+ font-size: 18px;
403
+ font-weight: 600;
404
+ font-family: -apple-system, BlinkMacSystemFont, sans-serif;
405
+ letter-spacing: 1px;
406
+ z-index: 10;
407
+ }
408
+ #init-overlay::after {
409
+ content: '';
410
+ animation: loading-dots 1.2s steps(4, end) infinite;
394
411
  }
412
+ #init-overlay.visible { display: flex; }
413
+ #reconnect-overlay {
414
+ display: none;
415
+ position: absolute;
416
+ top: 0; left: 0; right: 0; bottom: 0;
417
+ background: rgba(10, 10, 10, 0.85);
418
+ color: #f59e0b;
419
+ align-items: center;
420
+ justify-content: center;
421
+ font-size: 16px;
422
+ font-weight: 600;
423
+ font-family: -apple-system, BlinkMacSystemFont, sans-serif;
424
+ letter-spacing: 1px;
425
+ z-index: 10;
426
+ }
427
+ #reconnect-overlay::after {
428
+ content: '';
429
+ animation: loading-dots 1.2s steps(4, end) infinite;
430
+ }
431
+ #reconnect-overlay.visible { display: flex; }
395
432
 
396
433
 
397
434
  /* 选择模式:原位文本层 */
@@ -1019,6 +1056,8 @@
1019
1056
  <div id="terminal-container">
1020
1057
  <div id="terminal" style="position:relative;">
1021
1058
  <div id="switch-overlay">正在切换</div>
1059
+ <div id="init-overlay"></div>
1060
+ <div id="reconnect-overlay">连接断开,正在重连</div>
1022
1061
  <div id="select-text-layer">
1023
1062
  <div id="select-hint">长按选择文本 · 点右上角 ✕ 返回终端</div>
1024
1063
  <pre id="select-text-pre"></pre>
@@ -1040,6 +1079,9 @@
1040
1079
  var fontSize = isMobile ? 11 : 13;
1041
1080
  var currentMode = 'claude';
1042
1081
  var isTransitioning = false;
1082
+ var transitionEndTimer = null;
1083
+ var waitingInitData = false;
1084
+ var initDataTimer = null;
1043
1085
  var isBufferReplay = true; // 初始缓冲区回放中,不弹 toast
1044
1086
  var startupDialogShown = false;
1045
1087
 
@@ -1054,7 +1096,7 @@
1054
1096
  selectionBackground: '#264f78',
1055
1097
  },
1056
1098
  allowProposedApi: true,
1057
- scrollback: isIOS ? 200 : isMobile ? 1000 : 3000,
1099
+ scrollback: isIOS ? 2000 : isMobile ? 5000 : 50000,
1058
1100
  smoothScrollDuration: 0,
1059
1101
  scrollOnUserInput: true,
1060
1102
  });
@@ -1592,12 +1634,18 @@
1592
1634
  modeSelect.value = mode;
1593
1635
  document.getElementById('mode-label').textContent = '';
1594
1636
  var label = mode === 'claude' ? 'Claude' : 'OpenCode';
1595
- term.write('\r\n 正在启动 ' + label + (sessionId ? '(恢复会话)' : '') + '...\r\n');
1637
+ var initOv = document.getElementById('init-overlay');
1638
+ initOv.textContent = '正在启动 ' + label + (sessionId ? '(恢复会话)' : '');
1639
+ initOv.classList.add('visible');
1596
1640
  var msg = { type: 'init', mode: mode };
1597
1641
  if (sessionId) msg.sessionId = sessionId;
1598
1642
  ws.send(JSON.stringify(msg));
1599
1643
  }
1600
1644
 
1645
+ function hideInitOverlay() {
1646
+ document.getElementById('init-overlay').classList.remove('visible');
1647
+ }
1648
+
1601
1649
  function getTimeAgo(ts) {
1602
1650
  var diff = Date.now() - ts;
1603
1651
  var min = Math.floor(diff / 60000);
@@ -1615,16 +1663,16 @@
1615
1663
 
1616
1664
  ws.onopen = function() {
1617
1665
  isBufferReplay = true;
1666
+ document.getElementById('reconnect-overlay').classList.remove('visible');
1618
1667
  resize();
1619
1668
  rebindTouchScroll();
1620
- setTimeout(function() { isBufferReplay = false; }, 500);
1669
+ setTimeout(function() { isBufferReplay = false; }, 2000);
1621
1670
  // 不在这里初始化,等 state 消息判断是否需要弹对话框
1622
1671
  };
1623
1672
 
1624
1673
  ws.onclose = function() {
1625
1674
  ws = null;
1626
- term.reset();
1627
- term.write('\r\n \x1b[33m连接断开,正在重连...\x1b[0m\r\n');
1675
+ document.getElementById('reconnect-overlay').classList.add('visible');
1628
1676
  setTimeout(connect, 2000);
1629
1677
  };
1630
1678
 
@@ -1632,7 +1680,23 @@
1632
1680
  try {
1633
1681
  var msg = JSON.parse(e.data);
1634
1682
  if (msg.type === 'data') {
1635
- if (!isCreatingNewSession && !isTransitioning) {
1683
+ if (isTransitioning) {
1684
+ // 模式切换:TUI 渲染会发送一连串 data,debounce 等稳定后移除覆盖层
1685
+ term.write(msg.data);
1686
+ clearTimeout(transitionEndTimer);
1687
+ transitionEndTimer = setTimeout(function() {
1688
+ terminalEl.classList.remove('transitioning');
1689
+ isTransitioning = false;
1690
+ }, 2000);
1691
+ } else if (waitingInitData) {
1692
+ // 启动/恢复:同样 debounce 等 TUI 渲染完再隐藏启动覆盖层
1693
+ throttledWrite(msg.data);
1694
+ clearTimeout(initDataTimer);
1695
+ initDataTimer = setTimeout(function() {
1696
+ hideInitOverlay();
1697
+ waitingInitData = false;
1698
+ }, 2000);
1699
+ } else if (!isCreatingNewSession) {
1636
1700
  throttledWrite(msg.data);
1637
1701
  }
1638
1702
  }
@@ -1646,7 +1710,9 @@
1646
1710
  if (writeTimer) { cancelAnimationFrame(writeTimer); writeTimer = null; }
1647
1711
  writeBuffer = '';
1648
1712
  term.reset();
1649
- endTransition(msg.mode);
1713
+ // 更新模式但保留覆盖层,等首条 data 到达后再移除
1714
+ currentMode = msg.mode;
1715
+ modeSelect.value = msg.mode;
1650
1716
  if (msg.buffer) {
1651
1717
  term.write(msg.buffer);
1652
1718
  }
@@ -1673,6 +1739,8 @@
1673
1739
  }
1674
1740
  }
1675
1741
  else if (msg.type === 'restored') {
1742
+ // 不立即隐藏覆盖层,等 data debounce 后隐藏
1743
+ waitingInitData = true;
1676
1744
  if (writeTimer) { cancelAnimationFrame(writeTimer); writeTimer = null; }
1677
1745
  writeBuffer = '';
1678
1746
  term.reset();
@@ -1681,13 +1749,17 @@
1681
1749
  }
1682
1750
  }
1683
1751
  else if (msg.type === 'restore-error') {
1752
+ hideInitOverlay();
1684
1753
  term.write('恢复失败: ' + msg.error + '\r\n');
1685
1754
  }
1686
1755
  else if (msg.type === 'started') {
1756
+ // 不立即隐藏覆盖层,等 data debounce 后隐藏
1757
+ waitingInitData = true;
1687
1758
  rebindTouchScroll();
1688
1759
  preloadData();
1689
1760
  }
1690
1761
  else if (msg.type === 'new-session-ok') {
1762
+ waitingInitData = true;
1691
1763
  if (writeTimer) { cancelAnimationFrame(writeTimer); writeTimer = null; }
1692
1764
  writeBuffer = '';
1693
1765
  term.reset();
@@ -1710,7 +1782,7 @@
1710
1782
  } else {
1711
1783
  cacheRestored = true;
1712
1784
  }
1713
- }, 800);
1785
+ }, 2000);
1714
1786
  }
1715
1787
 
1716
1788
  window.addEventListener('resize', resize);
@@ -1879,7 +1951,7 @@
1879
1951
  setTimeout(function() {
1880
1952
  sessions = sessions.filter(function(s) { return s.id !== sessionId; });
1881
1953
  renderSessions();
1882
- }, 200);
1954
+ }, 2000);
1883
1955
  } else {
1884
1956
  itemEl.style.opacity = '';
1885
1957
  itemEl.style.pointerEvents = '';
@@ -2079,7 +2151,7 @@
2079
2151
  itemEl.style.padding = '0 12px';
2080
2152
  itemEl.style.margin = '0';
2081
2153
  itemEl.style.opacity = '0';
2082
- setTimeout(function() { itemEl.remove(); }, 200);
2154
+ setTimeout(function() { itemEl.remove(); }, 2000);
2083
2155
  } else {
2084
2156
  itemEl.style.opacity = '';
2085
2157
  itemEl.style.pointerEvents = '';
package/index.html CHANGED
@@ -38,7 +38,6 @@
38
38
  /* 隐藏 xterm textarea 的闪烁光标 (cc-viewer TerminalPanel.module.css) */
39
39
  .xterm-helper-textarea { caret-color: transparent !important; }
40
40
 
41
- /* 参考 cc-viewer 的 App.jsx 行 1319: 移动端容器使用 100vw/100vh */
42
41
  #layout {
43
42
  position: fixed;
44
43
  top: 0;
@@ -57,8 +56,7 @@
57
56
  background: #111;
58
57
  border-bottom: 1px solid #222;
59
58
  display: flex;
60
- align-items: flex-start;
61
- padding-top: 40px;
59
+ align-items: center;
62
60
  justify-content: space-between;
63
61
  flex-shrink: 0;
64
62
  height: 40px;
@@ -94,8 +92,7 @@
94
92
 
95
93
  #session-history-header {
96
94
  display: flex;
97
- align-items: flex-start;
98
- padding-top: 40px;
95
+ align-items: center;
99
96
  justify-content: space-between;
100
97
  padding: 12px 16px;
101
98
  background: #111;
@@ -129,8 +126,7 @@
129
126
 
130
127
  .session-item {
131
128
  display: flex;
132
- align-items: flex-start;
133
- padding-top: 40px;
129
+ align-items: center;
134
130
  gap: 8px;
135
131
  padding: 8px 12px;
136
132
  background: #1a1a1a;
@@ -214,8 +210,7 @@
214
210
  cursor: pointer;
215
211
  border-radius: 4px;
216
212
  display: flex;
217
- align-items: flex-start;
218
- padding-top: 40px;
213
+ align-items: center;
219
214
  gap: 3px;
220
215
  flex-shrink: 0;
221
216
  }
@@ -256,8 +251,7 @@
256
251
  cursor: pointer;
257
252
  border-radius: 50%;
258
253
  display: flex;
259
- align-items: flex-start;
260
- padding-top: 40px;
254
+ align-items: center;
261
255
  justify-content: center;
262
256
  transition: all 0.15s;
263
257
  -webkit-tap-highlight-color: transparent;
@@ -302,8 +296,7 @@
302
296
 
303
297
  #claude-detail-header {
304
298
  display: flex;
305
- align-items: flex-start;
306
- padding-top: 40px;
299
+ align-items: center;
307
300
  padding: 10px 12px;
308
301
  background: #111;
309
302
  border-bottom: 1px solid #222;
@@ -356,8 +349,7 @@
356
349
  #mode-switcher {
357
350
  display: flex;
358
351
  gap: 4px;
359
- align-items: flex-start;
360
- padding-top: 40px;
352
+ align-items: center;
361
353
  }
362
354
 
363
355
  #mode-label {
@@ -410,8 +402,7 @@
410
402
  top: 0; left: 0; right: 0; bottom: 0;
411
403
  background: #0a0a0a;
412
404
  color: #ccc;
413
- align-items: flex-start;
414
- padding-top: 40px;
405
+ align-items: center;
415
406
  justify-content: center;
416
407
  font-size: 16px;
417
408
  font-family: -apple-system, BlinkMacSystemFont, sans-serif;
@@ -481,8 +472,7 @@
481
472
  }
482
473
  #msg-viewer-header {
483
474
  display: flex;
484
- align-items: flex-start;
485
- padding-top: 40px;
475
+ align-items: center;
486
476
  justify-content: space-between;
487
477
  padding: 10px 14px;
488
478
  background: #111;
@@ -616,8 +606,7 @@
616
606
 
617
607
  #git-diff-header {
618
608
  display: flex;
619
- align-items: flex-start;
620
- padding-top: 40px;
609
+ align-items: center;
621
610
  justify-content: space-between;
622
611
  padding: 12px 16px;
623
612
  background: #111;
@@ -641,8 +630,7 @@
641
630
 
642
631
  .git-diff-file-item {
643
632
  display: flex;
644
- align-items: flex-start;
645
- padding-top: 40px;
633
+ align-items: center;
646
634
  padding: 6px 12px;
647
635
  cursor: pointer;
648
636
  color: #ccc;
@@ -686,8 +674,7 @@
686
674
 
687
675
  .git-diff-content-header {
688
676
  display: flex;
689
- align-items: flex-start;
690
- padding-top: 40px;
677
+ align-items: center;
691
678
  gap: 10px;
692
679
  padding: 8px 12px;
693
680
  border-bottom: 1px solid #2a2a2a;
@@ -727,8 +714,7 @@
727
714
  flex: 1;
728
715
  display: flex;
729
716
  flex-direction: column;
730
- align-items: flex-start;
731
- padding-top: 40px;
717
+ align-items: center;
732
718
  justify-content: center;
733
719
  gap: 12px;
734
720
  color: #333;
@@ -761,8 +747,7 @@
761
747
  #docs-bar.visible { display: flex; }
762
748
  #docs-header {
763
749
  display: flex;
764
- align-items: flex-start;
765
- padding-top: 40px;
750
+ align-items: center;
766
751
  justify-content: space-between;
767
752
  padding: 12px 16px;
768
753
  background: #111;
@@ -782,8 +767,7 @@
782
767
  }
783
768
  .docs-file-item {
784
769
  display: flex;
785
- align-items: flex-start;
786
- padding-top: 40px;
770
+ align-items: center;
787
771
  padding: 8px 12px;
788
772
  cursor: pointer;
789
773
  color: #ccc;
@@ -829,8 +813,7 @@
829
813
  flex: 1;
830
814
  display: flex;
831
815
  flex-direction: column;
832
- align-items: flex-start;
833
- padding-top: 40px;
816
+ align-items: center;
834
817
  justify-content: center;
835
818
  gap: 12px;
836
819
  color: #333;
@@ -1137,6 +1120,7 @@
1137
1120
  var fontSize = isMobile ? 11 : 13;
1138
1121
  var currentMode = 'claude';
1139
1122
  var isTransitioning = false;
1123
+ var transitionEndTimer = null;
1140
1124
  var mobileInitSent = false;
1141
1125
 
1142
1126
  var term = new Terminal({
@@ -1153,13 +1137,14 @@
1153
1137
  selectionBackground: '#264f78',
1154
1138
  },
1155
1139
  allowProposedApi: true,
1156
- scrollback: isIOS ? 200 : isMobile ? 1000 : 3000,
1140
+ scrollback: isIOS ? 2000 : isMobile ? 5000 : 50000,
1157
1141
  smoothScrollDuration: 0,
1158
1142
  scrollOnUserInput: true,
1159
1143
  });
1160
1144
 
1161
1145
  term.open(document.getElementById('terminal'));
1162
1146
 
1147
+
1163
1148
  // Unicode 11 宽字符支持:box-drawing、CJK、emoji 等字符宽度精确计算
1164
1149
  if (window.Unicode11Addon) {
1165
1150
  try {
@@ -1326,6 +1311,7 @@
1326
1311
  var lineHeight = (newCellDims && newCellDims.height) || cellDims.height;
1327
1312
  var rows = Math.max(5, Math.min(Math.floor(availH / lineHeight), 100));
1328
1313
  term.resize(MOBILE_COLS, rows);
1314
+ term.scrollToBottom();
1329
1315
  if (ws && ws.readyState === 1 && !isTransitioning) {
1330
1316
  ws.send(JSON.stringify({ type: 'resize', cols: MOBILE_COLS, rows: rows, mobile: true }));
1331
1317
  }
@@ -1696,7 +1682,14 @@
1696
1682
  var msg = JSON.parse(e.data);
1697
1683
  if (msg.type === 'data') {
1698
1684
  hideLoading();
1699
- if (!isCreatingNewSession && !isTransitioning) {
1685
+ if (isTransitioning) {
1686
+ term.write(msg.data);
1687
+ clearTimeout(transitionEndTimer);
1688
+ transitionEndTimer = setTimeout(function() {
1689
+ terminalEl.classList.remove('transitioning');
1690
+ isTransitioning = false;
1691
+ }, 2000);
1692
+ } else if (!isCreatingNewSession) {
1700
1693
  throttledWrite(msg.data);
1701
1694
  }
1702
1695
  }
@@ -1752,7 +1745,8 @@
1752
1745
  if (writeTimer) { cancelAnimationFrame(writeTimer); writeTimer = null; }
1753
1746
  writeBuffer = '';
1754
1747
  term.reset();
1755
- endTransition(msg.mode);
1748
+ currentMode = msg.mode;
1749
+ modeSelect.value = msg.mode;
1756
1750
  if (msg.buffer) {
1757
1751
  term.write(msg.buffer);
1758
1752
  }
@@ -1807,7 +1801,7 @@
1807
1801
  } else {
1808
1802
  cacheRestored = true;
1809
1803
  }
1810
- }, 800);
1804
+ }, 2000);
1811
1805
  }
1812
1806
 
1813
1807
  window.addEventListener('resize', resize);
@@ -2046,7 +2040,7 @@
2046
2040
  setTimeout(function() {
2047
2041
  sessions = sessions.filter(function(s) { return s.id !== sessionId; });
2048
2042
  renderSessions();
2049
- }, 200);
2043
+ }, 2000);
2050
2044
  } else {
2051
2045
  itemEl.style.opacity = '';
2052
2046
  itemEl.style.pointerEvents = '';
@@ -2249,7 +2243,7 @@
2249
2243
  itemEl.style.padding = '0 12px';
2250
2244
  itemEl.style.margin = '0';
2251
2245
  itemEl.style.opacity = '0';
2252
- setTimeout(function() { itemEl.remove(); }, 200);
2246
+ setTimeout(function() { itemEl.remove(); }, 2000);
2253
2247
  } else {
2254
2248
  itemEl.style.opacity = '';
2255
2249
  itemEl.style.pointerEvents = '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-opencode-viewer",
3
- "version": "2.6.46",
3
+ "version": "2.6.48",
4
4
  "description": "A unified terminal viewer for Claude Code and OpenCode with seamless switching",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -201,6 +201,7 @@ function killProcessTree(proc) {
201
201
 
202
202
  // 清理孤儿进程(PPID=1 的 opencode/claude)
203
203
  function cleanupOrphanProcesses() {
204
+ const tc = Date.now();
204
205
  try {
205
206
  const myPid = process.pid;
206
207
  const orphans = execSync(
@@ -212,15 +213,23 @@ function cleanupOrphanProcesses() {
212
213
  }
213
214
  if (orphans.length > 0) LOG(`[cleanup] 清理孤儿进程: ${orphans.join(', ')}`);
214
215
  } catch {}
216
+ console.log(`[perf] cleanupOrphanProcesses: ${Date.now() - tc}ms`);
215
217
  }
216
218
 
217
219
  async function spawnProcess(mode, sessionId = null) {
220
+ const t0 = Date.now();
218
221
  const pty = await getPty();
222
+ console.log(`[perf] getPty: ${Date.now() - t0}ms`);
223
+
224
+ const t1 = Date.now();
219
225
  fixSpawnHelperPermissions();
226
+ console.log(`[perf] fixSpawnHelperPermissions: ${Date.now() - t1}ms`);
220
227
 
221
228
  let command, args = [];
222
229
  if (mode === 'claude') {
230
+ const t2 = Date.now();
223
231
  const claudePath = findCommand('claude');
232
+ console.log(`[perf] findCommand(claude): ${Date.now() - t2}ms`);
224
233
  if (claudePath.endsWith('.js')) {
225
234
  command = process.execPath;
226
235
  args = [claudePath];
@@ -233,7 +242,9 @@ async function spawnProcess(mode, sessionId = null) {
233
242
  LOG(`[claude] 恢复会话: ${sessionId}`);
234
243
  }
235
244
  } else {
245
+ const t2 = Date.now();
236
246
  command = findCommand('opencode');
247
+ console.log(`[perf] findCommand(opencode): ${Date.now() - t2}ms`);
237
248
  // 如果提供了 sessionId,添加 --session 参数
238
249
  if (sessionId) {
239
250
  args = ['--session', sessionId];
@@ -244,6 +255,7 @@ async function spawnProcess(mode, sessionId = null) {
244
255
  const spawnEnv = { ...process.env };
245
256
 
246
257
  // 恢复会话时,使用会话记录的工作目录
258
+ const t3 = Date.now();
247
259
  let spawnCwd = process.cwd();
248
260
  if (sessionId && mode === 'opencode') {
249
261
  try {
@@ -286,6 +298,9 @@ async function spawnProcess(mode, sessionId = null) {
286
298
  }
287
299
  }
288
300
 
301
+ console.log(`[perf] cwdLookup: ${Date.now() - t3}ms`);
302
+
303
+ const t4 = Date.now();
289
304
  const proc = pty.spawn(command, args, {
290
305
  name: 'xterm-256color',
291
306
  cols: lastPtyCols,
@@ -294,6 +309,9 @@ async function spawnProcess(mode, sessionId = null) {
294
309
  env: spawnEnv,
295
310
  });
296
311
 
312
+ console.log(`[perf] pty.spawn: ${Date.now() - t4}ms`);
313
+ console.log(`[perf] spawnProcess total: ${Date.now() - t0}ms`);
314
+
297
315
  proc.onData((data) => {
298
316
  // 忽略已被替换的旧进程输出
299
317
  if (currentProcess !== proc) return;
@@ -786,6 +804,7 @@ const requestHandler = async (req, res) => {
786
804
 
787
805
  // API: 获取最近的 OpenCode 和 Claude 会话(用于启动对话框)
788
806
  if (req.url === '/api/last-sessions') {
807
+ const tls = Date.now();
789
808
  res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
790
809
  let opencode = null, claude = null;
791
810
  // OpenCode: 从 SQLite 查最近会话
@@ -861,6 +880,7 @@ const requestHandler = async (req, res) => {
861
880
  }
862
881
  }
863
882
  } catch {}
883
+ console.log(`[perf] last-sessions: ${Date.now() - tls}ms`);
864
884
  res.end(JSON.stringify({ opencode, claude }));
865
885
  return;
866
886
  }
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 1,
3
+ "skills": {
4
+ "code-review-expert": {
5
+ "source": "sanyuan0704/sanyuan-skills",
6
+ "sourceType": "github",
7
+ "computedHash": "6c2fe31851a34e63e033257527eab04eea835ca1cf4c4276d1392a323e36e377"
8
+ }
9
+ }
10
+ }