Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@qoder-ai/qodercli

Package Overview
Dependencies
Maintainers
1
Versions
114
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@qoder-ai/qodercli - npm Package Compare versions

Comparing version
0.2.6
to
0.2.7-beta
+327
bundle/builtin/agent-creator/SKILL.md
---
name: agent-creator
description:
Guide for creating custom agents. Use when users want to create a new agent
that runs in an isolated context with custom system prompts and
specific tool access.
allowed-tools: Edit, Write
---
# Creating Custom Agents for Qoder CLI
This skill guides you through creating custom agents. Agents are
specialized AI assistants that run in isolated contexts with custom system
prompts, specific tool access, and independent permissions.
## When to Use Agents
Use agents when you need:
- **Context isolation** for long research or exploration tasks
- **Parallel execution** of multiple independent workstreams
- **Specialized expertise** with custom prompts for specific domains
- **Reusable configurations** across projects
**When NOT to use agents:**
- Simple, single-purpose tasks (use skills instead)
- Tasks requiring frequent back-and-forth with the user
- Quick, targeted changes
## Agent Locations
| Location | Scope | Priority |
| -------------------------------- | ----------------- | -------- |
| `<project>/${QODER_CONFIG_DIR}/agents/` | Current project | Higher |
| `~/${QODER_CONFIG_DIR}/agents/` | All your projects | Lower |
**Project agents** (`${QODER_CONFIG_DIR}/agents/`): Ideal for codebase-specific
agents. Check into version control to share with your team.
**User agents** (`~/${QODER_CONFIG_DIR}/agents/`): Personal agents available across
all your projects.
## Agent File Format
Each agent is a Markdown file with YAML frontmatter:
```markdown
---
name: agent-name
description: When to use this agent. Be specific!
---
You are a [role]. When invoked:
1. [First step]
2. [Second step]
3. [Output format]
```
### Required Fields
| Field | Description |
| ------------- | -------------------------------------------------------------------------- |
| `name` | Unique identifier (lowercase letters and hyphens only) |
| `description` | When to delegate to this agent (be specific). Including trigger scenarios. |
## Writing Effective Descriptions
The description is **critical**. Include "use proactively" to encourage
automatic delegation - Qoder CLI uses it to decide when to delegate.
```yaml
# Bad - Too vague
description: Helps with code
# Good - Specific and actionable
description: Expert code review specialist. Proactively reviews code for quality, security, and maintainability. Use immediately after writing or modifying code.
```
### Optional Fields
| Field | Description |
| ---------------- | ----------------------------------------------------------------------------------- |
| `tools` | Tools the agent can use (string or array) |
| `disallowedTools`| Tools to explicitly deny (string or array) |
| `model` | Model to use: `inherit` (default), `sonnet`, `opus`, `haiku` |
| `color` | Display color: `red`, `blue`, `green`, `yellow`, `purple`, `orange`, `pink`, `cyan` |
| `displayName` | Human-friendly display name |
| `maxTurns` | Maximum conversation turns (positive integer) |
| `timeoutMins` | Timeout in minutes (positive integer) |
| `effort` | Thinking effort: `low`, `medium`, `high`, `max` |
| `skills` | Skills the agent can use (string or array) |
#### Tools
Specify which tools the agent has access to. This limits the agent's
capabilities for security and focus.
```yaml
# Read-only access
tools: Read, Grep, Glob
# Full development access
tools: Bash, Read, Write, Edit, Glob, Grep
# Web research capabilities
tools: Read, WebSearch, WebFetch
```
**Available Tools:**
- `Bash` - Execute shell commands
- `Read` - Read file contents
- `Write` - Create new files
- `Edit` - Modify existing files
- `Glob` - Find files by pattern
- `Grep` - Search file contents
- `WebSearch` - Search the web
- `WebFetch` - Fetch web page content
If not specified, the agent inherits default tool access.
## Agent Creation Workflow
### Step 1: Decide the Scope
If not sure where to create the agent, ask the user with two options:
- **Project-level** (`.agents/`): For team-shared, codebase-specific agents
- **User-level** (`~/.agents/`): For personal agents across all projects
### Step 2: Gather Requirements
Understand what the agent should do:
- What specific task or domain?
- What tools does it need?
- Should it be read-only or have write access?
- Any special constraints or workflows?
### Step 3: Create the File
```bash
# For project-level
mkdir -p ${QODER_CONFIG_DIR}/agents
touch ${QODER_CONFIG_DIR}/agents/agent-name.md
# For user-level
mkdir -p ~/${QODER_CONFIG_DIR}/agents
touch ~/${QODER_CONFIG_DIR}/agents/agent-name.md
```
### Step 4: Write Configuration
Create the markdown file with:
1. YAML frontmatter with required fields
2. System prompt in the body
### Step 5: Verify
- Check file location is correct
- Verify YAML syntax is valid
- Confirm the description clearly describes when to use it
- Tell the user: run `/agents reload` to make the new agent available in the
current session. They can then invoke it with:
```
@agent-name [task description]
```
## Best Practices
1. **Design focused agents** - Each should excel at one specific task
2. **Write detailed descriptions** - Be detailed and specific so Qoder CLI knows
when to delegate
3. **Limit tool access** - Grant only necessary permissions for security and
focus
4. **Keep prompts concise** - Long, rambling prompts dilute focus
## Anti-Patterns to Avoid
- **Vague descriptions** - "Use for general tasks" gives no signal
- **Overly long prompts** - A 2000-word prompt doesn't make it smarter
## Examples
### Verifier
```markdown
---
name: verifier
description:
Validates completed work. Use after tasks are marked done to confirm
implementations are functional.
color: yellow
---
You are a skeptical validator. Your job is to verify that work claimed as
complete actually works.
When invoked:
1. Identify what was claimed to be completed
2. Check that the implementation exists and is functional
3. Run relevant tests or verification steps
4. Look for edge cases that may have been missed
Be thorough and skeptical. Report:
- What was verified and passed
- What was claimed but incomplete or broken
- Specific issues that need to be addressed
Do not accept claims at face value. Test everything.
```
### Debugger
```markdown
---
name: debugger
description:
Debugging specialist for errors and test failures. Use when encountering
issues.
color: red
---
You are an expert debugger specializing in root cause analysis.
When invoked:
1. Capture error message and stack trace
2. Identify reproduction steps
3. Isolate the failure location
4. Implement minimal fix
5. Verify solution works
For each issue, provide:
- Root cause explanation
- Evidence supporting the diagnosis
- Specific code fix
- Testing approach
Focus on fixing the underlying issue, not symptoms.
```
### Data Scientist
```markdown
---
name: data-scientist
description:
Data analysis expert for SQL queries, BigQuery operations, and data insights.
Use proactively for data analysis tasks and queries.
tools: Bash, Read, Write
---
You are a data scientist specializing in SQL and BigQuery analysis.
When invoked:
1. Understand the data analysis requirement
2. Write efficient SQL queries
3. Use BigQuery command line tools (bq) when appropriate
4. Analyze and summarize results
5. Present findings clearly
Key practices:
- Write optimized SQL queries with proper filters
- Use appropriate aggregations and joins
- Include comments explaining complex logic
- Format results for readability
- Provide data-driven recommendations
For each analysis:
- Explain the query approach
- Document any assumptions
- Highlight key findings
- Suggest next steps based on data
Always ensure queries are efficient and cost-effective.
```
### Security Auditor
```markdown
---
name: security-auditor
description:
Security specialist. Use when implementing auth, payments, or handling
sensitive data. Proactively audit security-sensitive code.
tools: Read, Grep, Glob
color: red
model: sonnet
---
You are a security expert auditing code for vulnerabilities.
When invoked:
1. Identify security-sensitive code paths
2. Check for common vulnerabilities (injection, XSS, auth bypass)
3. Verify secrets are not hardcoded
4. Review input validation and sanitization
Report findings by severity:
- Critical (must fix before deploy)
- High (fix soon)
- Medium (address when possible)
Security checklist:
- SQL injection prevention
- XSS protection
- CSRF tokens
- Authentication bypass risks
- Authorization checks
- Secret management
- Input validation
- Output encoding
```
---
name: hook-config
description:
Guide for creating and configuring hooks. Use when users want to add
automated behaviors triggered by tool execution, session lifecycle,
or other events in the Qoder CLI hook system.
allowed-tools: Edit, Write
---
# Creating Hooks for Qoder CLI
This skill guides you through creating hooks — automated behaviors that
trigger on specific events like tool execution, session lifecycle, file
changes, and notifications. Hooks are configured in settings.json files.
## When to Use Hooks
Use hooks when you need:
- **Automated side effects** after tool execution (format, lint, test)
- **Guardrails** before tool execution (block protected files, detect secrets)
- **Notifications** when events occur (desktop alerts, webhooks)
- **Validation gates** that block or allow operations based on conditions
- **Session automation** at start, end, or compaction
**When NOT to use hooks:**
- One-off tasks (just do them directly)
- Complex multi-step workflows (use agents or skills instead)
- Anything that needs user interaction mid-execution
## Configuration Scopes
| Scope | File | Use Case |
| ----------- | ------------------------------------ | -------------------------------- |
| **Project** | `${QODER_CONFIG_DIR}/settings.json` | Team-shared, version controlled |
| **Local** | `${QODER_CONFIG_DIR}/settings.local.json` | Personal, not committed |
| **User** | `~/${QODER_CONFIG_DIR}/settings.json` | Global across all projects |
**Choose project** for team guardrails and standards.
**Choose local** for personal preferences and notifications.
**Choose user** for global behaviors across all projects.
## Configuration Format
```jsonc
{
"hooks": {
"<EventName>": [
{
"matcher": "<regex>",
"hooks": [
{
"type": "command",
"command": "${QODER_CONFIG_DIR}/hooks/my-hook.sh",
"name": "my-hook",
"timeout": 60
}
]
}
]
}
}
```
Each event maps to an array of **hook definitions**. Each definition has:
| Field | Required | Description |
| ------------ | -------- | --------------------------------------------------- |
| `matcher` | No | Regex filter (matches tool name for tool events) |
| `hooks` | Yes | Array of hook handlers |
| `sequential` | No | Run hooks in order instead of parallel |
| `async` | No | Fire-and-forget, don't block the operation |
### Hook Handler Fields
| Field | Required | Description |
| --------------- | ------------ | ----------------------------------------------------- |
| `type` | Yes | `command`, `http`, `prompt`, or `agent` |
| `command` | command type | Shell command to execute |
| `url` | http type | Webhook URL to POST to |
| `prompt` | prompt/agent | LLM prompt text |
| `if` | No | Per-hook condition: `"ToolName(glob)"` or `"ToolName"` |
| `name` | No | Display name |
| `description` | No | Human-readable description |
| `timeout` | No | Seconds before timeout |
| `statusMessage` | No | Text shown in UI during execution |
| `async` | No | Run in background without blocking |
| `asyncRewake` | No | Background hook; exit code 2 wakes model |
## Hook Events
### Tool Events (matcher = tool name regex)
| Event | When |
| -------------------- | ------------------------------- |
| `PreToolUse` | Before tool execution |
| `PostToolUse` | After successful tool execution |
| `PostToolUseFailure` | After tool execution fails |
| `PermissionRequest` | Tool requests user permission |
### Session & Agent Lifecycle
| Event | When |
| --------------- | ------------------------------------ |
| `SessionStart` | Session initializes |
| `SessionEnd` | Session tears down |
| `SubagentStart` | Sub-agent session begins |
| `SubagentStop` | Sub-agent session ends |
| `Stop` | Agent decides to stop |
| `StopFailure` | Stop process fails (error, timeout) |
| `PreCompact` | Before context compaction |
| `PostCompact` | After context compaction |
### User, Config & Notification Events
| Event | When |
| -------------------- | ------------------------------- |
| `UserPromptSubmit` | User submits a prompt |
| `ConfigChange` | Settings change at runtime |
| `Notification` | External notification arrives |
| `InstructionsLoaded` | System instructions loaded |
### File & Workspace Events
| Event | When |
| ---------------- | -------------------------- |
| `CwdChanged` | Working directory changes |
| `FileChanged` | Watched file changes |
| `WorktreeCreate` | Git worktree created |
| `WorktreeRemove` | Git worktree removed |
### Task Events
| Event | When |
| --------------- | ----------------------------- |
| `TaskCreated` | Background task created |
| `TaskCompleted` | Background task completes |
## Handler Types
### Command (`type: "command"`)
Executes a shell command. Hook input arrives as JSON on **stdin**.
Output is parsed from **stdout** as JSON.
**Exit code semantics:**
| Exit Code | Meaning |
| --------- | -------------------------------- |
| 0 | Success — parse stdout as JSON |
| 2 | Blocking deny — stderr is reason |
| Other | Non-blocking warning |
**Stdin** receives the full hook input as JSON (fields vary by event):
```json
{
"session_id": "abc-123",
"cwd": "/path/to/project",
"hook_event_name": "PreToolUse",
"tool_name": "Edit",
"tool_input": { "file_path": "src/app.ts", "old_string": "...", "new_string": "..." }
}
```
**Stdout** JSON output (all fields optional):
```json
{
"decision": "allow",
"reason": "Checks passed",
"hookSpecificOutput": {
"additionalContext": "Message injected into conversation"
}
}
```
Best for: scripts, linters, formatters, external CLI tools.
### HTTP (`type: "http"`)
POSTs hook input as JSON to the URL. Response parsed as JSON hook
output. Headers support `${ENV_VAR}` interpolation.
```jsonc
{
"type": "http",
"url": "https://api.example.com/hooks",
"headers": { "Authorization": "Bearer ${API_TOKEN}" }
}
```
Best for: webhooks, external integrations, CI/CD triggers.
### Prompt (`type: "prompt"`)
Single-turn LLM evaluation. The prompt plus hook input go to the model,
which returns a structured JSON decision.
```jsonc
{
"type": "prompt",
"prompt": "Review this edit for security issues. Return {\"decision\": \"allow\"} or {\"decision\": \"deny\", \"reason\": \"...\"}"
}
```
Best for: AI-powered review gates, semantic validation.
### Agent (`type: "agent"`)
Spawns a sub-agent with tool access.
```jsonc
{
"type": "agent",
"prompt": "Verify the edited file passes type checking. $ARGUMENTS",
"tools": ["Bash", "Read"],
"maxTurns": 10,
"timeout": 120
}
```
Best for: complex verification needing tool use (run tests, read files,
check types).
## The `if` Condition
Narrow when a hook fires within a matched definition:
```jsonc
{ "if": "Edit(*.ts)" } // Only Edit calls on .ts files
{ "if": "Write(src/**)" } // Only Write calls under src/
{ "if": "Bash" } // Any Bash call
```
Format: `"ToolName(glob_pattern)"` or `"ToolName"`.
The glob matches the tool's primary argument (typically a file path).
## Hook Creation Workflow
### Step 1: Determine the Behavior
Understand what the user wants:
- What should happen automatically?
- When should it trigger? (before/after tool use, session event, etc.)
- Should it block operations or just observe?
- Who needs it? (team or personal)
**Avoid interrogation loops.** Propose a concrete hook config based on
initial understanding and ask the user to refine.
### Step 2: Choose Event, Matcher, and Handler
Map the behavior:
| Behavior | Event | Matcher | Handler |
| ---------------------- | ------------- | -------------- | ------- |
| Auto-format after edits| `PostToolUse` | `Edit\|Write` | command |
| Block protected files | `PreToolUse` | `Edit\|Write` | command |
| Secret detection | `PreToolUse` | `Edit\|Write` | command |
| Desktop notifications | `Notification`| — | command |
| Run tests after changes| `PostToolUse` | `Edit\|Write` | command |
| AI code review gate | `PreToolUse` | `Edit` | prompt |
| Type-check verification| `PostToolUse` | `Edit\|Write` | agent |
| Webhook to CI/CD | `PostToolUse` | `Edit\|Write` | http |
| Dependency guard | `PreToolUse` | `Edit` | command |
### Step 3: Choose Scope
- **Project** (`${QODER_CONFIG_DIR}/settings.json`): Team standards, check in
- **Local** (`${QODER_CONFIG_DIR}/settings.local.json`): Personal, not committed
- **User** (`~/${QODER_CONFIG_DIR}/settings.json`): Global, all projects
### Step 4: Create the Configuration
1. Read the target settings.json (if it exists)
2. Merge the new hook into the existing `hooks` object
3. Write back the file
If no hooks exist yet, create the full structure.
### Step 5: Create Script Files (command type)
For command hooks with non-trivial logic, create scripts in
`${QODER_CONFIG_DIR}/hooks/`:
```bash
mkdir -p ${QODER_CONFIG_DIR}/hooks
```
Script requirements:
- Read JSON from stdin
- Write JSON to stdout (or nothing for simple success)
- Exit 0 for success, 2 for blocking deny, other for warning
- Use stderr for error messages and debug output
- Make executable: `chmod +x ${QODER_CONFIG_DIR}/hooks/my-hook.sh`
Test independently:
```bash
echo '{"tool_name":"Edit","tool_input":{"file_path":"test.ts"}}' | \
${QODER_CONFIG_DIR}/hooks/my-hook.sh
```
### Step 6: Verify
Tell the user to verify:
1. Restart the session or trigger the relevant event
2. Check the hook fires and produces expected output
3. Test the blocking path (for PreToolUse guards)
4. Confirm `async` hooks don't block the session
## Best Practices
1. **Keep hooks fast** — Target <5s for synchronous hooks
2. **Test scripts independently** — Pipe sample JSON stdin, verify output
3. **Use `async: true`** for non-blocking side effects (notifications, logging)
4. **Scope matchers narrowly** — Don't fire on every tool invocation
5. **Prefer `if` conditions** for file-type filtering
6. **Log to stderr** — stdout is parsed as JSON; debug output goes to stderr
7. **Handle edge cases** — Exit 0 if the condition doesn't apply
8. **Use `statusMessage`** for meaningful UI feedback during execution
## Anti-Patterns to Avoid
- **Overly broad matchers** — `".*"` on PreToolUse fires on every single tool call
- **Long synchronous hooks** — Block the entire interactive session
- **Stdout pollution** — Non-JSON stdout causes parse errors
- **Missing shebang** — Scripts without `#!/bin/bash` may fail silently
## Examples
### Auto-Format TypeScript
Runs Prettier on TypeScript files after every Edit or Write.
**`${QODER_CONFIG_DIR}/settings.json`:**
```jsonc
{
"hooks": {
"PostToolUse": [{
"matcher": "Edit|Write",
"hooks": [{
"type": "command",
"command": "${QODER_CONFIG_DIR}/hooks/auto-format.sh",
"if": "Edit(*.{ts,tsx,js,jsx})",
"name": "auto-format",
"statusMessage": "Formatting..."
}]
}]
}
}
```
**`${QODER_CONFIG_DIR}/hooks/auto-format.sh`:**
```bash
#!/bin/bash
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
[ -z "$FILE" ] && exit 0
[ -f "$FILE" ] || exit 0
npx prettier --write "$FILE" >/dev/null 2>&1
exit 0
```
### Protected File Guard
Blocks edits to lock files and CI configuration.
**`${QODER_CONFIG_DIR}/settings.json`:**
```jsonc
{
"hooks": {
"PreToolUse": [{
"matcher": "Edit|Write",
"hooks": [{
"type": "command",
"command": "${QODER_CONFIG_DIR}/hooks/protected-files.sh",
"name": "protected-file-guard",
"statusMessage": "Checking file permissions..."
}]
}]
}
}
```
**`${QODER_CONFIG_DIR}/hooks/protected-files.sh`:**
```bash
#!/bin/bash
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
[ -z "$FILE" ] && exit 0
PROTECTED_PATTERNS=(
"package-lock.json"
"yarn.lock"
"pnpm-lock.yaml"
".github/workflows/*"
".gitlab-ci.yml"
)
for PATTERN in "${PROTECTED_PATTERNS[@]}"; do
if [[ "$FILE" == $PATTERN ]]; then
echo "Blocked: $FILE is a protected file" >&2
exit 2
fi
done
exit 0
```
### Desktop Notification
Sends a macOS notification when Qoder CLI needs attention.
**`${QODER_CONFIG_DIR}/settings.local.json`:**
```jsonc
{
"hooks": {
"Notification": [{
"hooks": [{
"type": "command",
"command": "${QODER_CONFIG_DIR}/hooks/notify.sh",
"name": "desktop-notify",
"async": true
}]
}]
}
}
```
**`${QODER_CONFIG_DIR}/hooks/notify.sh`:**
```bash
#!/bin/bash
INPUT=$(cat)
TITLE=$(echo "$INPUT" | jq -r '.title // "Qoder CLI"')
MSG=$(echo "$INPUT" | jq -r '.message // "Notification"')
if command -v osascript &>/dev/null; then
osascript -e "display notification \"$MSG\" with title \"$TITLE\""
elif command -v notify-send &>/dev/null; then
notify-send "$TITLE" "$MSG"
fi
exit 0
```
### Type-Check Gate
Uses an agent to verify TypeScript compiles after edits.
**`${QODER_CONFIG_DIR}/settings.json`:**
```jsonc
{
"hooks": {
"PostToolUse": [{
"matcher": "Edit|Write",
"hooks": [{
"type": "agent",
"prompt": "Run `npx tsc --noEmit` and check for type errors in the edited file. If there are errors, report them. $ARGUMENTS",
"tools": ["Bash", "Read"],
"if": "Edit(*.{ts,tsx})",
"name": "type-check",
"statusMessage": "Type checking...",
"maxTurns": 5,
"timeout": 60
}]
}]
}
}
```
---
name: mcp-config
description: Interactively add, update, or remove MCP (Model Context Protocol) servers in QoderCLI config files. Use this skill whenever the user pastes an MCP server config snippet, asks to "add an MCP", "配置 MCP", "install this MCP server", "register an MCP", wants to move an MCP between project/user/local scope, or asks why a newly pasted MCP isn't showing up. Handles stdio, http, sse, and ws transports, merges safely into the right target file (`<repo>/.qoder/settings.json`, `~/.qoder/settings.json`, or `<repo>/.qoder/settings.local.json`), and tells the user exactly how to reload so the server actually connects.
allowed-tools: Bash, Edit, Read, Write
---
# MCP Config Helper (QoderCLI)
Help the user land an MCP server config into the right QoderCLI config file, merge it cleanly with what's already there, and reload it so it actually takes effect.
## When this skill fires
Typical user inputs:
- Pastes a JSON blob that looks like an MCP server definition (has `command`, `args`, or `url` + `type`)
- "帮我把这个 MCP 加到项目里"
- "Add this MCP server to user scope"
- "Why isn't my new MCP showing up in `/mcp`?"
- "Move the filesystem MCP from project to user scope"
- "Remove the old foo MCP"
If the user's request doesn't involve MCP server registration, don't invoke this skill.
## Prefer the built-in CLI when possible
QoderCLI ships first-class commands for MCP CRUD:
- `qodercli mcp add <name> <commandOrUrl> [args...] --scope <user|local|project> --transport <stdio|sse|http|ws>`
- `qodercli mcp add-json <name> <json> --scope <user|local|project>` — ideal when the user pasted a full server body, just wrap it and pass through.
- `qodercli mcp list`, `qodercli mcp get <name>`, `qodercli mcp remove <name> --scope ...`
If the user's ask maps cleanly onto one of these, run the command via Bash instead of hand-editing JSON — the CLI handles collision detection, OAuth setup, scope validation, and writes to the right file. Fall back to direct file editing only when: (a) the user is doing a cross-scope move (read from one, write to another), (b) the user wants a surgical edit to an env var or header on an existing entry, or (c) the CLI rejects the input and you need to diagnose why.
## What the user pastes
MCP configs come in several shapes. Normalize them before writing. Common inputs:
**Full server block (most common — from docs/README):**
```json
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path"]
}
}
}
```
**Single server entry (name + body):**
```json
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path"]
}
```
**Just the body (user will supply the name):**
```json
{ "command": "npx", "args": ["-y", "..."] }
```
**HTTP / SSE transport:**
```json
{
"type": "http",
"url": "https://example.com/mcp",
"headers": { "Authorization": "Bearer ..." }
}
```
If the name is missing from the snippet, ask the user what to call the server. Don't invent one.
## Target files (scopes)
QoderCLI supports three MCP scopes. Pick the right file — the difference matters:
| Scope | File | `mcpServers` lives at | When to use |
| --------- | ------------------------------------------ | ------------------------------ | --------------------------------------------------------------------- |
| `project` | `<repo>/.qoder/settings.json` | top-level `mcpServers` | Shared with the team via git. Use for project-wide, committed config. |
| `user` | `~/.qoder/settings.json` | top-level `mcpServers` | Available across all projects for this user. Use for personal tools. |
| `local` | `<repo>/.qoder/settings.local.json` | top-level `mcpServers` | Project-specific, user-only, **gitignored**. This is the CLI default. |
**Do NOT write to any of these — they are wrong targets that look plausible but the CLI does not read:**
- `<repo>/.mcp.json` or `~/.mcp.json` (Claude Code convention, not QoderCLI)
- `~/.qoder/mcp.json` (no such file — user MCPs go inside `~/.qoder/settings.json` under `mcpServers`)
- `~/Library/Application Support/Qoder/User/mcp.json` or any VS Code extension path
- `~/.claude.json` or `~/.claude/settings.json` (those are Claude Code, not QoderCLI)
The only three valid targets are the three rows in the table above. If you find yourself about to write somewhere else, stop.
**Scope selection — don't default silently.** If the user hasn't specified a scope, ask before writing. A one-liner is enough: "Add this to **project** (`<repo>/.qoder/settings.json`, committed), **user** (`~/.qoder/settings.json`, global), or **local** (`<repo>/.qoder/settings.local.json`, gitignored)?"
Don't guess "probably local scope just because it's the CLI default" — the difference between project (checked into git, teammates see it) and user/local (personal) is significant, and picking wrong can leak secrets or clutter a teammate's config. Users routinely paste MCP configs from docs that don't mention scope; they need the prompt.
Exception: proceed without asking only when phrasing makes it genuinely unambiguous — "add to this project", "push this to the team", "only for me globally", "just for this repo". Vague signals like "装一下" / "加进去" do not qualify.
## Workflow
1. **Parse the pasted config.** Extract: server name, transport (`stdio` if `command` present, else `http`/`sse`/`ws` based on `type`/`url`), and the server body. Normalize so the write step only deals with a clean `(name, body)` pair.
2. **Confirm scope** if not obvious from the user's message (see table above).
3. **Decide: CLI or direct edit?** For a plain add with a clean body, `qodercli mcp add-json <name> '<json>' --scope <scope>` is the shortest path and handles collision errors for you. For cross-scope moves, surgical field edits, or removals from multiple scopes at once, go direct.
4. **If direct-editing, read the target file.** Use the Read tool. If the file doesn't exist, plan to create it with `{ "mcpServers": { ... } }`. If it exists, add/update a top-level `mcpServers` key and preserve every other setting already in the file.
5. **Detect collisions — but don't be precious.** Two sub-cases:
- **User explicitly asked to update/replace/move an existing entry** (e.g. "把 filesystem 路径改成...", "update the github token", "replace example-api"): just do it. Don't ask for reconfirmation. A one-line "found existing filesystem at /old/path → updating to /new/path" is plenty; the user already decided.
- **User pasted a new config that happens to collide** (no mention of the existing entry, likely unaware it's there): stop and show them the existing entry vs. the new one, ask replace/rename/cancel. Silent overwrite here loses hand-edited fields (env vars, auth headers) and surprises the user.
The distinguishing signal: did the user's wording acknowledge that the server already exists? If yes, proceed. If no and there's a collision, surface it. Note that `qodercli mcp add`/`add-json` *refuses* to overwrite by default — if the user wants a replace, either remove-then-add via CLI, or fall back to a direct Edit.
6. **Merge and write.** For direct edits, use the Edit tool for surgical changes when the file exists (especially `~/.qoder/settings.json`, which holds unrelated settings and shouldn't be rewritten wholesale). Only rewrite the whole file with Write when creating it fresh. Preserve formatting — match the existing indentation.
7. **Validate.** After writing, read the relevant slice back and confirm the JSON parses and the server is present. All three targets are real settings files — a corrupted write breaks settings on the next start, so validation isn't optional.
8. **Tell the user how to reload.** See below.
## Reload instructions
MCP servers are loaded at startup and on explicit reconnect. After editing config, the user must reload. Tell them the shortest path:
- **Added or changed a server (running session)**: run `/mcp reload` inside QoderCLI — it restarts all MCP clients and refreshes the tool surface. This is the one you want 95% of the time.
- **Inspect what's loaded**: `/mcp` lists current servers and their status.
- **Project-scope first-time add**: QoderCLI prompts for approval before running project-level MCP servers (this is enforced via the project-MCP approval flow). The user will see a trust prompt the first time `/mcp reload` picks up the new entry — mention this so they're not surprised.
- **Removed a server**: `/mcp reload` drops the connection. If you only removed it from one of multiple scopes, it may still show up from another — `qodercli mcp list` confirms where it still lives.
Keep the reload instruction to one or two sentences — don't lecture.
## Gotchas worth flagging
- **Env vars with secrets**: if the pasted config has `"env": { "API_KEY": "sk-..." }` with a real-looking secret, flag it: "This config has what looks like a real API key — do you want to move it to an env var reference or keep it inline?" Don't refuse; just surface the choice.
- **Settings files hold unrelated keys**: all three targets also store user preferences, keybinding toggles, auth hints, etc. Never rewrite a settings file whole — Edit the specific `mcpServers` slice.
- **Shared `.qoder` directory name**: user settings live in `~/.qoder/settings.json`; project shared settings in `<repo>/.qoder/settings.json`; project-local (gitignored) in `<repo>/.qoder/settings.local.json`. Keep the scope clear before writing.
- **Relative paths**: stdio `command`/`args` often reference local scripts. If the user pastes `./server.js`, ask whether they want it resolved to an absolute path — relative paths break when the CLI starts from a different cwd.
- **Project-scope is committed**: `<repo>/.qoder/settings.json` ends up in git. Remind the user not to include secrets there — for secret-bearing configs prefer `user` or `local`.
- **Plugin-provided MCPs**: servers whose names start with `mcp__plugin_` come from installed extensions, not user config. Don't try to edit them via this skill — point the user to the extension's own config.
## Output style
Length should match what's actually at stake — not a fixed rule.
**Default (simple add, no concerns):** one or two sentences. What was written where, how to reload. The user pasted config and wants it landed.
> Added `filesystem` (stdio) to `~/.qoder/settings.json` (user scope). Run `/mcp reload` to pick it up.
**When there's something worth flagging, don't suppress it to stay terse.** Brief matters less than "the user understood what just happened and what to do next." Specifically, add a short follow-up paragraph (2-4 sentences) when:
- **The pasted config contained a real-looking secret** (Bearer token, API key, password). Tell them it's now sitting in plaintext in the config file, that project-scope `<repo>/.qoder/settings.json` is committed to git and `~/.qoder/settings.json` is often synced via dotfiles, and offer to switch to env-var reference or rotation if exposed. Don't assume they've thought this through — many people paste from copy-paste without realizing.
- **The operation has a non-obvious side effect** the user probably didn't anticipate. E.g., for `@modelcontextprotocol/server-filesystem`, changing the path replaces — it doesn't merge, so the old dir loses access. Mention it in one line.
- **The collision was silent** (user didn't mention the existing entry). Surface what was there before the change.
- **Scope crossed trust boundary** (e.g. moved from `local` → `project`). Remind them it's now committed and teammates will pick it up on next pull.
Don't pad. Don't add headers (`## 生效方式`, `## 安全提醒`) for a 2-sentence response — just write the sentences. Headers only earn their keep when the response is long enough that scanning matters.
---
name: skill-creator
description:
Guide for creating effective skills. This skill should be used when users want
to create a new skill (or update an existing skill) that extends Qoder CLI's
capabilities with specialized knowledge, workflows, or tool integrations.
allowed-tools: Edit, Write
---
# Skill Creator
This skill provides guidance for creating effective skills.
## About Skills
Skills are modular, self-contained packages that extend Qoder CLI's
capabilities by providing specialized knowledge, workflows, and tools. Think of
them as "onboarding guides" for specific domains or tasks—they transform Qoder
CLI from a general-purpose agent into a specialized agent equipped with
procedural knowledge that no model can fully possess.
### What Skills Provide
1. Specialized workflows - Multi-step procedures for specific domains
2. Tool integrations - Instructions for working with specific file formats or
APIs
3. Domain expertise - Company-specific knowledge, schemas, business logic
4. Bundled resources - Scripts, references, and assets for complex and
repetitive tasks
## Core Principles
### Concise is Key
The context window is a public good. Skills share the context window with
everything else Qoder CLI needs: system prompt, conversation history, other
Skills' metadata, and the actual user request.
**Default assumption: Qoder CLI is already very smart.** Only add context
Qoder CLI doesn't already have. Challenge each piece of information: "Does
Qoder CLI really need this explanation?" and "Does this paragraph justify its
token cost?"
Prefer concise examples over verbose explanations.
### Set Appropriate Degrees of Freedom
Match the level of specificity to the task's fragility and variability:
**High freedom (text-based instructions)**: Use when multiple approaches are
valid, decisions depend on context, or heuristics guide the approach.
**Medium freedom (pseudocode or scripts with parameters)**: Use when a preferred
pattern exists, some variation is acceptable, or configuration affects behavior.
**Low freedom (specific scripts, few parameters)**: Use when operations are
fragile and error-prone, consistency is critical, or a specific sequence must be
followed.
### Anatomy of a Skill
Every skill consists of a required SKILL.md file and optional bundled resources:
```
skill-name/
├── SKILL.md (required)
│ ├── YAML frontmatter metadata (required)
│ │ ├── name: (required)
│ │ ├── description: (required)
│ │ └── model/tools/when_to_use/... (optional)
│ └── Markdown instructions (required)
└── Bundled Resources (optional)
├── scripts/ - Executable code (Node.js/Python/Bash/etc.)
├── references/ - Documentation intended to be loaded into context as needed
└── assets/ - Files used in output (templates, icons, fonts, etc.)
```
#### SKILL.md (required)
Every SKILL.md consists of:
- **Frontmatter** (YAML): Contains `name` and `description` fields. These are
the only fields that Qoder CLI reads to determine when the skill gets used,
thus it is very important to be clear and comprehensive in describing what the
skill is, and when it should be used.
- **Body** (Markdown): Instructions and guidance for using the skill. Only
loaded AFTER the skill triggers (if at all).
#### Bundled Resources (optional)
##### Scripts (`scripts/`)
Executable code (Node.js/Python/Bash/etc.) for tasks that require deterministic
reliability or are repeatedly rewritten.
- **When to include**: When the same code is being rewritten repeatedly or
deterministic reliability is needed
- **Example**: `scripts/rotate_pdf.cjs` for PDF rotation tasks
- **Benefits**: Token efficient, deterministic, may be executed without loading
into context
- **Agentic Ergonomics**: Scripts must output LLM-friendly stdout. Suppress
standard tracebacks. Output clear, concise success/failure messages, and
paginate or truncate outputs to prevent context window overflow.
##### References (`references/`)
Documentation and reference material intended to be loaded as needed into
context to inform Qoder CLI's process and thinking.
- **When to include**: For documentation that Qoder CLI should reference while
working
- **Examples**: `references/finance.md` for financial schemas,
`references/api_docs.md` for API specifications
- **Best practice**: If files are large (>10k words), include grep search
patterns in SKILL.md
##### Assets (`assets/`)
Files not intended to be loaded into context, but rather used within the output
Qoder CLI produces.
- **When to include**: When the skill needs files that will be used in the final
output
- **Examples**: `assets/logo.png` for brand assets, `assets/slides.pptx` for
PowerPoint templates
#### What to Not Include in a Skill
Do NOT create extraneous documentation or auxiliary files like README.md,
CHANGELOG.md, INSTALLATION_GUIDE.md, etc. The skill should only contain the
information needed for an AI agent to do the job.
### Progressive Disclosure Design Principle
Skills use a three-level loading system to manage context efficiently:
1. **Metadata (name + description)** - Always in context (~100 words)
2. **SKILL.md body** - When skill triggers (<5k words)
3. **Bundled resources** - As needed (unlimited because scripts can be executed
without reading into context window)
Keep SKILL.md body under 500 lines. Split content into separate reference files
when approaching this limit.
## Skill Creation Process
1. Understand the skill with concrete examples
2. Plan reusable skill contents (scripts, references, assets)
3. Create the skill directory and template files
4. Edit the skill (implement resources and write SKILL.md)
5. Validate the skill
6. Install and test
Follow these steps in order.
### Skill Naming
- Use lowercase letters, digits, and hyphens only; normalize user-provided
titles to hyphen-case (e.g., "Plan Mode" -> `plan-mode`).
- Prefer short, verb-led phrases that describe the action.
- Name the skill folder exactly after the skill name.
### Step 1: Understanding the Skill with Concrete Examples
To create an effective skill, clearly understand concrete examples of how the
skill will be used. Ask users:
- "What functionality should this skill support?"
- "Can you give some examples of how this skill would be used?"
- "What would a user say that should trigger this skill?"
**Avoid interrogation loops:** Do not ask more than one or two clarifying
questions at a time. Bias toward action: propose a concrete list of features
based on your initial understanding, and ask the user to refine them.
### Step 2: Planning the Reusable Skill Contents
Analyze each concrete example to identify reusable resources:
- **Scripts**: Code that would be rewritten each time (e.g., `rotate_pdf.cjs`)
- **References**: Documentation needed for context (e.g., `schema.md`)
- **Assets**: Files used in output (e.g., template directories, images)
### Step 3: Creating the Skill
Create the skill directory structure directly. Ask the user for the target
location:
- **Project-level**: `${QODER_CONFIG_DIR}/skills/<skill-name>/`
- **User-level**: `~/${QODER_CONFIG_DIR}/skills/<skill-name>/`
Create the following directory structure:
```
<skill-name>/
├── SKILL.md
├── scripts/ (if needed)
├── references/ (if needed)
└── assets/ (if needed)
```
Write the initial `SKILL.md` with this template:
```markdown
---
name: <skill-name>
description: <Complete explanation of what the skill does and when to use it. Include specific scenarios, file types, or tasks that trigger it.>
---
# <Skill Title>
## Overview
[1-2 sentences explaining what this skill enables]
## [Main section based on chosen structure]
[Skill instructions and guidance]
## Resources
[Reference any bundled scripts, references, or assets here]
```
Only create the resource directories (`scripts/`, `references/`, `assets/`)
that are actually needed for this skill. Do not create empty placeholder
directories.
### Step 4: Edit the Skill
When editing the skill, remember it is being created for another instance of
Qoder CLI to use. Include information that would be beneficial and non-obvious.
#### Start with Reusable Skill Contents
Implement the resources identified in Step 2. This may require user input
(e.g., brand assets, documentation to store).
Scripts must be tested by actually running them to ensure they work correctly.
#### Update SKILL.md
**Writing Guidelines:** Always use imperative/infinitive form.
##### Frontmatter
**Required fields:**
- `name`: The skill name (hyphen-case, lowercase)
- `description`: Primary triggering mechanism. Include both what the Skill does
and specific triggers/contexts for when to use it. **Must be a single-line
string** (max 1024 characters).
- Example:
`description: Data ingestion, cleaning, and transformation for tabular data. Use when working with CSV/TSV files to analyze large datasets, normalize schemas, or merge sources.`
**Optional fields:**
- `model`: Override the model used when this skill is active
- `tools`: List of tools this skill is allowed to use (supports glob patterns)
- `when_to_use`: Concise hint for automatic skill selection
- `argument-hint`: Short hint for expected arguments (e.g., `<file-path>`)
- `arguments`: Structured argument definitions for complex skills
##### Body
Write instructions for using the skill and its bundled resources.
### Step 5: Validate the Skill
After writing the skill, verify it meets these requirements:
**Checklist:**
- [ ] `SKILL.md` exists in the skill directory
- [ ] YAML frontmatter starts with `---` and ends with `---`
- [ ] `name` field is present and uses hyphen-case (`/^[a-z0-9-]+$/`)
- [ ] `description` field is present, single-line, and ≤ 1024 characters
- [ ] No unresolved `TODO:` strings remain in any file
- [ ] Skill folder name matches the `name` field
- [ ] No extraneous files (README.md, CHANGELOG.md, etc.)
- [ ] SKILL.md body is under 500 lines
If any check fails, fix the issue before proceeding.
### Step 6: Install and Test
Tell the user their skill is ready. They can use it by:
1. Restarting the session or running `/skills reload`
2. Invoking with `/<skill-name>`
3. Verifying with `/skills list`
**Iteration:** After testing, users may request improvements. Use the skill on
real tasks, notice struggles or inefficiencies, and update accordingly.

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { EventEmitter } from 'node:events';
import { type WebSocket } from 'ws';
import type { NetworkLog, ConsoleLogPayload } from './types.js';
export type { NetworkLog, ConsoleLogPayload, InspectorConsoleLog, } from './types.js';
interface IncomingNetworkPayload extends Partial<NetworkLog> {
chunk?: {
index: number;
data: string;
timestamp: number;
};
}
export interface SessionInfo {
sessionId: string;
ws: WebSocket;
lastPing: number;
}
/**
* DevTools Viewer
*
* Receives logs via WebSocket from CLI sessions.
*/
export declare class DevTools extends EventEmitter {
private static instance;
private logs;
private consoleLogs;
private server;
private wss;
private sessions;
private heartbeatTimer;
private port;
private static readonly DEFAULT_PORT;
private static readonly MAX_PORT_RETRIES;
private constructor();
static getInstance(): DevTools;
addInternalConsoleLog(payload: ConsoleLogPayload, sessionId?: string, timestamp?: number): void;
addInternalNetworkLog(payload: IncomingNetworkPayload, sessionId?: string, timestamp?: number): void;
getUrl(): string;
getPort(): number;
stop(): Promise<void>;
start(): Promise<string>;
private setupWebSocketServer;
private handleWebSocketMessage;
}
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import http from 'node:http';
import { randomUUID } from 'node:crypto';
import { EventEmitter } from 'node:events';
import { WebSocketServer } from 'ws';
import { INDEX_HTML, CLIENT_JS } from './_client-assets.js';
/**
* DevTools Viewer
*
* Receives logs via WebSocket from CLI sessions.
*/
export class DevTools extends EventEmitter {
static instance;
logs = [];
consoleLogs = [];
server = null;
wss = null;
sessions = new Map();
heartbeatTimer = null;
port = 25417;
static DEFAULT_PORT = 25417;
static MAX_PORT_RETRIES = 10;
constructor() {
super();
// Each SSE client adds 3 listeners; raise the limit to avoid warnings
this.setMaxListeners(50);
}
static getInstance() {
if (!DevTools.instance) {
DevTools.instance = new DevTools();
}
return DevTools.instance;
}
addInternalConsoleLog(payload, sessionId, timestamp) {
const entry = {
...payload,
id: randomUUID(),
sessionId,
timestamp: timestamp || Date.now(),
};
this.consoleLogs.push(entry);
if (this.consoleLogs.length > 5000)
this.consoleLogs.shift();
this.emit('console-update', entry);
}
addInternalNetworkLog(payload, sessionId, timestamp) {
if (!payload.id)
return;
const existingIndex = this.logs.findIndex((l) => l.id === payload.id);
if (existingIndex > -1) {
const existing = this.logs[existingIndex];
// Handle chunk accumulation
if (payload.chunk) {
const chunks = existing.chunks || [];
chunks.push(payload.chunk);
this.logs[existingIndex] = {
...existing,
chunks,
sessionId: sessionId || existing.sessionId,
};
}
else {
this.logs[existingIndex] = {
...existing,
...payload,
sessionId: sessionId || existing.sessionId,
// Drop chunks once we have the full response body — the data
// is redundant and keeping both can blow past V8's string limit
// when serializing the snapshot.
chunks: payload.response?.body ? undefined : existing.chunks,
response: payload.response
? { ...existing.response, ...payload.response }
: existing.response,
};
}
this.emit('update', this.logs[existingIndex]);
}
else if (payload.url) {
const entry = {
...payload,
sessionId,
timestamp: timestamp || Date.now(),
chunks: payload.chunk ? [payload.chunk] : undefined,
};
this.logs.push(entry);
if (this.logs.length > 2000)
this.logs.shift();
this.emit('update', entry);
}
}
getUrl() {
return `http://127.0.0.1:${this.port}`;
}
getPort() {
return this.port;
}
stop() {
return new Promise((resolve) => {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
if (this.wss) {
this.wss.close();
this.wss = null;
}
if (this.server) {
this.server.close(() => resolve());
this.server = null;
}
else {
resolve();
}
// Reset singleton so a fresh start() is possible
DevTools.instance = undefined;
});
}
start() {
return new Promise((resolve, reject) => {
if (this.server) {
resolve(this.getUrl());
return;
}
this.server = http.createServer((req, res) => {
// Only allow same-origin requests — the client is served from this
// server so cross-origin access is unnecessary and would let arbitrary
// websites exfiltrate logs (which may contain API keys/headers).
const origin = req.headers.origin;
if (origin) {
const allowed = `http://127.0.0.1:${this.port}`;
if (origin === allowed) {
res.setHeader('Access-Control-Allow-Origin', allowed);
}
}
// API routes
if (req.url === '/api/trigger-debugger' && req.method === 'POST') {
let body = '';
req.on('data', (chunk) => {
body += chunk;
});
req.on('end', () => {
try {
const parsed = JSON.parse(body);
if (typeof parsed !== 'object' ||
parsed === null ||
!('sessionId' in parsed) ||
typeof parsed.sessionId !== 'string') {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Invalid request' }));
return;
}
const sessionId = parsed.sessionId;
const session = this.sessions.get(sessionId);
if (session) {
session.ws.send(JSON.stringify({ type: 'trigger-debugger' }));
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true }));
}
else {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Session not found' }));
}
}
catch {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Invalid request' }));
}
});
}
else if (req.url === '/events') {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
});
// Send full snapshot on connect
const snapshot = JSON.stringify({
networkLogs: this.logs,
consoleLogs: this.consoleLogs,
sessions: Array.from(this.sessions.keys()),
});
res.write(`event: snapshot\ndata: ${snapshot}\n\n`);
// Incremental updates
const onNetwork = (log) => {
res.write(`event: network\ndata: ${JSON.stringify(log)}\n\n`);
};
const onConsole = (log) => {
res.write(`event: console\ndata: ${JSON.stringify(log)}\n\n`);
};
const onSession = () => {
const sessions = Array.from(this.sessions.keys());
res.write(`event: session\ndata: ${JSON.stringify(sessions)}\n\n`);
};
this.on('update', onNetwork);
this.on('console-update', onConsole);
this.on('session-update', onSession);
req.on('close', () => {
this.off('update', onNetwork);
this.off('console-update', onConsole);
this.off('session-update', onSession);
});
}
else if (req.url === '/' || req.url === '/index.html') {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(INDEX_HTML);
}
else if (req.url === '/assets/main.js') {
res.writeHead(200, { 'Content-Type': 'application/javascript' });
res.end(CLIENT_JS);
}
else {
res.writeHead(404);
res.end('Not Found');
}
});
this.server.on('error', (e) => {
if (typeof e === 'object' &&
e !== null &&
'code' in e &&
e.code === 'EADDRINUSE') {
if (this.port - DevTools.DEFAULT_PORT >= DevTools.MAX_PORT_RETRIES) {
reject(new Error(`DevTools: all ports ${DevTools.DEFAULT_PORT}–${this.port} in use`));
return;
}
this.port++;
this.server?.listen(this.port, '127.0.0.1');
}
else {
reject(e instanceof Error ? e : new Error(String(e)));
}
});
this.server.listen(this.port, '127.0.0.1', () => {
this.setupWebSocketServer();
resolve(this.getUrl());
});
});
}
setupWebSocketServer() {
if (!this.server)
return;
this.wss = new WebSocketServer({ server: this.server, path: '/ws' });
this.wss.on('connection', (ws) => {
let sessionId = null;
ws.on('message', (data) => {
try {
const message = JSON.parse(data.toString());
// Handle registration first
if (message.type === 'register') {
sessionId = String(message.sessionId);
if (!sessionId)
return;
this.sessions.set(sessionId, {
sessionId,
ws,
lastPing: Date.now(),
});
// Notify session update
this.emit('session-update');
// Send registration acknowledgement
ws.send(JSON.stringify({
type: 'registered',
sessionId,
timestamp: Date.now(),
}));
}
else if (sessionId) {
this.handleWebSocketMessage(sessionId, message);
}
}
catch {
// Invalid WebSocket message
}
});
ws.on('close', () => {
if (sessionId) {
this.sessions.delete(sessionId);
this.emit('session-update');
}
});
ws.on('error', () => {
// WebSocket error — no action needed
});
});
// Heartbeat mechanism
this.heartbeatTimer = setInterval(() => {
const now = Date.now();
this.sessions.forEach((session, sessionId) => {
if (now - session.lastPing > 30000) {
session.ws.close();
this.sessions.delete(sessionId);
}
else {
// Send ping
session.ws.send(JSON.stringify({ type: 'ping', timestamp: now }));
}
});
}, 10000);
this.heartbeatTimer.unref();
}
handleWebSocketMessage(sessionId, message) {
const session = this.sessions.get(sessionId);
if (!session)
return;
switch (message['type']) {
case 'pong':
session.lastPing = Date.now();
break;
case 'console':
this.addInternalConsoleLog(message['payload'], sessionId, message['timestamp']);
break;
case 'network':
this.addInternalNetworkLog(message['payload'], sessionId, message['timestamp']);
break;
default:
break;
}
}
}
//# sourceMappingURL=index.js.map
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
export interface NetworkLog {
id: string;
sessionId?: string;
timestamp: number;
method: string;
url: string;
headers: Record<string, string | string[] | undefined>;
body?: string;
pending?: boolean;
chunks?: Array<{
index: number;
data: string;
timestamp: number;
}>;
response?: {
status: number;
headers: Record<string, string | string[] | undefined>;
body?: string;
durationMs: number;
};
error?: string;
}
export interface ConsoleLogPayload {
type: 'log' | 'warn' | 'error' | 'debug' | 'info';
content: string;
}
export interface InspectorConsoleLog extends ConsoleLogPayload {
id: string;
sessionId?: string;
timestamp: number;
}
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
export {};
//# sourceMappingURL=types.js.map
{
"name": "@google/gemini-cli-devtools",
"version": "0.2.0",
"license": "Apache-2.0",
"type": "module",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
"exports": {
".": {
"types": "./dist/src/index.d.ts",
"default": "./dist/src/index.js"
}
},
"scripts": {
"build": "npm run build:client && tsc -p tsconfig.build.json",
"build:client": "node esbuild.client.js"
},
"files": [
"dist",
"client/index.html"
],
"engines": {
"node": ">=20"
},
"devDependencies": {
"@types/node": "^20.11.24",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"typescript": "^5.3.3"
},
"dependencies": {
"ws": "^8.16.0"
}
}
[modes.plan]
network = false
readonly = true
approvedTools = []
allowOverrides = true
[modes.default]
network = false
readonly = false
approvedTools = ['cat', 'ls', 'grep', 'head', 'tail', 'less', 'Get-Content', 'dir', 'type', 'findstr', 'Get-ChildItem', 'echo']
allowOverrides = true
[modes.accepting_edits]
network = false
readonly = false
approvedTools = ['sed', 'grep', 'awk', 'perl', 'cat', 'echo', 'Add-Content', 'Set-Content']
allowOverrides = true
[commands]

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

+28
-81
{
"name": "@qoder-ai/qodercli",
"version": "0.2.6",
"description": "qodercli - npm installer",
"private": false,
"publishConfig": {
"access": "public"
},
"version": "0.2.7-beta",
"description": "Qoder AI CLI - AI-powered coding assistant",
"author": "Qoder AI",
"license": "Apache-2.0",
"type": "module",
"bin": {
"qodercli": "./bin/qodercli"
"qodercli": "bundle/qodercli.js"
},
"scripts": {
"postinstall": "node scripts/install.js"
},
"keywords": [
"qoder",
"ai",
"cli"
],
"author": "Qoder AI",
"license": "SEE LICENSE IN README.md",
"homepage": "https://qoder.com",
"files": [
"scripts/",
"bin/",
"bundle/",
"README.md",

@@ -30,8 +17,4 @@ "LICENSE"

"engines": {
"node": ">=14"
"node": ">=20.0.0"
},
"dependencies": {
"adm-zip": "^0.5.10",
"tar": "^4.4.19"
},
"os": [

@@ -42,60 +25,24 @@ "darwin",

],
"cpu": [
"x64",
"arm64"
"keywords": [
"qoder",
"ai",
"cli",
"coding",
"assistant",
"gemini"
],
"preferGlobal": true,
"binaries": {
"version": "0.2.6",
"files": [
{
"os": "darwin",
"arch": "arm64",
"url": "https://qoder-ide.oss-accelerate.aliyuncs.com/qodercli/releases/0.2.6/qodercli-darwin-arm64.tar.gz",
"sha256": "a04a55f730470cefdda69aeb383c43a92e945539d192b344c92b0f6625e3f9b2"
},
{
"os": "darwin",
"arch": "amd64",
"url": "https://qoder-ide.oss-accelerate.aliyuncs.com/qodercli/releases/0.2.6/qodercli-darwin-x64.tar.gz",
"sha256": "b98f2567f34f68269001326813f3c7b42fc5e31a342d2c0b19867684c43de185"
},
{
"os": "linux",
"arch": "arm64-musl",
"url": "https://qoder-ide.oss-accelerate.aliyuncs.com/qodercli/releases/0.2.6/qodercli-linux-arm64-musl.tar.gz",
"sha256": "f610dc8ddb74e6cdf3e3d1c382762929d7f5a1b81537dcbe5e47ff40c617f938"
},
{
"os": "linux",
"arch": "arm64",
"url": "https://qoder-ide.oss-accelerate.aliyuncs.com/qodercli/releases/0.2.6/qodercli-linux-arm64.tar.gz",
"sha256": "fbcd05cf64938ef2fa6228ae9026cebb1640d065f57c5bc3736748443e68746d"
},
{
"os": "linux",
"arch": "amd64-baseline",
"url": "https://qoder-ide.oss-accelerate.aliyuncs.com/qodercli/releases/0.2.6/qodercli-linux-x64-baseline.tar.gz",
"sha256": "1985dd1d92c812c14f1869d556f7e0bc78cb3d154c43795bca5ec095d72fbd09"
},
{
"os": "linux",
"arch": "amd64-musl",
"url": "https://qoder-ide.oss-accelerate.aliyuncs.com/qodercli/releases/0.2.6/qodercli-linux-x64-musl.tar.gz",
"sha256": "15842d2aa43c60c8985801ac9d1491054a140448f700fe4d14bc228dda6c7a7e"
},
{
"os": "linux",
"arch": "amd64",
"url": "https://qoder-ide.oss-accelerate.aliyuncs.com/qodercli/releases/0.2.6/qodercli-linux-x64.tar.gz",
"sha256": "be3b283db58e36394ac9c15b3b966f23188d6795be7e1edbec5535c9d8153293"
},
{
"os": "windows",
"arch": "amd64",
"url": "https://qoder-ide.oss-accelerate.aliyuncs.com/qodercli/releases/0.2.6/qodercli-windows-x64.zip",
"sha256": "2ad9cfb48b9d686f561507bbbe49b064441762363db1f63717f1840efcc6aca1"
}
]
"homepage": "https://github.com/nicepkg/qodercli",
"repository": {
"type": "git",
"url": "https://github.com/nicepkg/qodercli.git"
},
"publishConfig": {
"access": "public"
},
"optionalDependencies": {
"keytar": "^7.9.0"
},
"scripts": {
"postinstall": "node -e \"try{require('child_process').execSync('rg --version',{stdio:'ignore'})}catch{console.log('\\n ripgrep (rg) not found. Install for best search: https://github.com/BurntSushi/ripgrep#installation\\n')}\""
}
}

Sorry, the diff of this file is not supported yet

#!/usr/bin/env node
/* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/no-unused-vars, @typescript-eslint/no-this-alias, no-useless-catch, no-empty */
const fs = require('fs');
const path = require('path');
const https = require('https');
const http = require('http');
const { execSync } = require('child_process');
const crypto = require('crypto');
const os = require('os');
// Configuration
const BINARY_NAME = 'qodercli';
const PACKAGE_ROOT = path.resolve(__dirname, '..');
const BIN_DIR = path.join(PACKAGE_ROOT, 'bin');
const PACKAGE_JSON_PATH = path.join(PACKAGE_ROOT, 'package.json');
const INSTALL_METHOD = 'npm';
class QoderInstaller {
constructor() {
// Mark this as an installation process
process.env.QODER_CLI_INSTALL = '1';
// Initialize logging variables
this.logFile = null;
this.logStream = null;
this.originalConsoleLog = console.log;
this.originalConsoleError = console.error;
// Setup logging first to capture all output including platform detection
this.setupLogging();
// Detect platform and architecture (will be logged if they fail)
this.platform = this.detectPlatform();
this.arch = this.detectArch();
this.binPath = path.join(
BIN_DIR,
BINARY_NAME + (process.platform === 'win32' ? '.exe' : ''),
);
this.packageInfo = this.loadPackageInfo();
}
detectPlatform() {
switch (process.platform) {
case 'darwin':
return 'darwin';
case 'linux':
return 'linux';
case 'win32':
return 'windows';
default:
throw new Error(`Unsupported platform: ${process.platform}`);
}
}
detectArch() {
const arch = process.arch;
switch (arch) {
case 'x64':
// On Linux x64, check if CPU supports AVX2 instructions.
// Bun optimized binaries require AVX2/BMI2/FMA3 (-march=haswell).
// CPUs with only AVX1 (e.g. Ivy Bridge) will SIGILL on BMI2 instructions.
if (process.platform === 'linux' && !this.hasAVX2()) {
return 'amd64-baseline';
}
return 'amd64';
case 'arm64':
return 'arm64';
default:
throw new Error(`Unsupported architecture: ${arch}`);
}
}
/**
* Check if the CPU supports AVX2 instructions by reading /proc/cpuinfo.
* Bun optimized binaries are compiled with -march=haswell, requiring
* AVX2/BMI1/BMI2/FMA3. AVX1-only CPUs (e.g. Ivy Bridge) will SIGILL.
* Returns true if AVX2 is present or detection is unavailable.
* Only returns false when we can confirm AVX2 is absent.
*/
hasAVX2() {
try {
const cpuinfo = fs.readFileSync('/proc/cpuinfo', 'utf-8');
return /^flags\s*:.*\bavx2\b/m.test(cpuinfo);
} catch (e) {
// If we can't read cpuinfo, assume AVX2 is present (safer default)
return true;
}
}
setupLogging() {
try {
// Create log directory: ~/.qoder/logs on all platforms
const logDir = path.join(os.homedir(), '.qoder', 'logs');
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
// Create log file with timestamp
const timestamp = new Date()
.toISOString()
.replace(/[:.]/g, '-')
.replace('T', '_')
.split('Z')[0];
this.logFile = path.join(
logDir,
`qodercli_install_${INSTALL_METHOD}_${timestamp}.log`,
);
// Create write stream
this.logStream = fs.createWriteStream(this.logFile, { flags: 'a' });
// Write log header
this.logStream.write(
`Installation started at ${new Date().toISOString()}\n`,
);
this.logStream.write(`Installation method: ${INSTALL_METHOD}\n`);
this.logStream.write(`Platform: ${process.platform}/${process.arch}\n`);
this.logStream.write(`Node.js version: ${process.version}\n`);
this.logStream.write('================================\n\n');
// Create latest log marker
// Unix: symlink immediately, Windows: will copy after completion
const latestLogLink = path.join(logDir, 'qodercli_install.log');
try {
if (process.platform !== 'win32') {
// Unix: use symlink (immediate)
if (fs.existsSync(latestLogLink)) {
fs.unlinkSync(latestLogLink);
}
fs.symlinkSync(this.logFile, latestLogLink);
}
// Windows: will copy complete log in closeLogging()
} catch (e) {
// Ignore errors - not critical
}
// Redirect console.log and console.error to both terminal and log file
const self = this;
console.log = function (...args) {
const message = args
.map((arg) =>
typeof arg === 'object'
? JSON.stringify(arg, null, 2)
: String(arg),
)
.join(' ');
self.originalConsoleLog.apply(console, args);
if (self.logStream) {
self.logStream.write(message + '\n');
}
};
console.error = function (...args) {
const message = args
.map((arg) =>
typeof arg === 'object'
? JSON.stringify(arg, null, 2)
: String(arg),
)
.join(' ');
self.originalConsoleError.apply(console, args);
if (self.logStream) {
self.logStream.write('[ERROR] ' + message + '\n');
}
};
// Log file location saved but not printed (will show on error)
} catch (error) {
// If logging setup fails, continue without logging
this.originalConsoleError.call(
console,
'Warning: Failed to setup logging:',
error.message,
);
this.originalConsoleError.call(
console,
'Installation will continue without logging',
);
}
}
closeLogging() {
if (this.logStream) {
this.logStream.end();
this.logStream = null;
}
// Update latest log marker after installation completes
if (this.logFile && process.platform === 'win32') {
try {
const logDir = path.dirname(this.logFile);
const latestLogLink = path.join(logDir, 'qodercli_install.log');
fs.copyFileSync(this.logFile, latestLogLink);
} catch (e) {
// Ignore errors - not critical
}
}
// Restore original console methods
console.log = this.originalConsoleLog;
console.error = this.originalConsoleError;
}
loadPackageInfo() {
try {
const packageJson = fs.readFileSync(PACKAGE_JSON_PATH, 'utf8');
const packageInfo = JSON.parse(packageJson);
if (!packageInfo.binaries || !packageInfo.binaries.files) {
throw new Error('Binary information missing in package configuration');
}
return packageInfo;
} catch (error) {
throw new Error(`Unable to read package configuration: ${error.message}`);
}
}
findBinaryInfo() {
const files = this.packageInfo.binaries.files;
const targetFile = files.find(
(file) => file.os === this.platform && file.arch === this.arch,
);
if (!targetFile) {
throw new Error(`Unsupported platform: ${this.platform}/${this.arch}`);
}
return targetFile;
}
async downloadBinary(url, expectedSha256) {
console.log(`Downloading binary: ${url}`);
// Create temporary directory for download operations
const os = require('os');
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'qodercli-install-'));
// Ensure target directory exists
if (!fs.existsSync(BIN_DIR)) {
fs.mkdirSync(BIN_DIR, { recursive: true });
}
// Download file to temporary directory
const filename = path.basename(url);
const archivePath = path.join(tempDir, filename);
try {
await this.downloadFile(url, archivePath);
// Verify checksum
console.log('Verifying file integrity...');
const actualSha256 = this.calculateSha256(archivePath);
if (actualSha256 !== expectedSha256) {
throw new Error(
`Checksum mismatch. Expected: ${expectedSha256}, Got: ${actualSha256}`,
);
}
// Extract file to temporary directory first
console.log('Extracting binary...');
const extractDir = path.join(tempDir, 'extract');
fs.mkdirSync(extractDir, { recursive: true });
await this.extractArchive(archivePath, filename, extractDir);
// Move extracted binary to final destination
const extractedBinary = this.findExtractedBinary(extractDir);
if (extractedBinary.length === 0) {
throw new Error(
`Binary file not found after extraction in ${extractDir}`,
);
}
// Try rename first (efficient), fallback to copy+delete if cross-device
try {
fs.renameSync(extractedBinary[0], this.binPath);
} catch (error) {
if (error.code === 'EXDEV') {
// Cross-device link not permitted, use copy+delete fallback
console.log(
'Cross-device link detected, using copy+delete method...',
);
fs.copyFileSync(extractedBinary[0], this.binPath);
fs.unlinkSync(extractedBinary[0]);
} else {
throw error;
}
}
// Set executable permission
if (process.platform !== 'win32') {
fs.chmodSync(this.binPath, 0o755);
}
// Create installation source marker
const sourceFile = path.join(BIN_DIR, '.qodercli-install-resource');
fs.writeFileSync(sourceFile, 'npm', 'utf8');
} catch (error) {
throw error;
} finally {
// Always cleanup temporary directory
try {
fs.rmSync(tempDir, { recursive: true, force: true });
} catch (cleanupError) {
console.warn(
'Warning: Failed to cleanup temporary directory:',
cleanupError.message,
);
}
}
}
async extractArchive(archivePath, filename, extractDir) {
if (filename.endsWith('.zip')) {
// Extract ZIP file using Node.js packages first
let extracted = false;
// Method 1: Use adm-zip package (preferred)
try {
const AdmZip = require('adm-zip');
const zip = new AdmZip(archivePath);
zip.extractAllTo(extractDir, true);
extracted = true;
console.log('ZIP extracted using Node.js adm-zip package');
} catch (error) {
console.log(
'adm-zip extraction failed, trying system commands...',
error.message,
);
}
// Method 2: System command fallbacks
if (!extracted) {
if (process.platform === 'win32') {
// Windows: Try PowerShell then 7-Zip
try {
execSync(
`powershell -command "Expand-Archive -Path '${archivePath}' -DestinationPath '${extractDir}' -Force"`,
{
stdio: 'pipe',
},
);
extracted = true;
} catch (error) {
try {
execSync(`7z x "${archivePath}" -o"${extractDir}" -y`, {
stdio: 'pipe',
});
extracted = true;
} catch (error2) {
// Will fail below
}
}
} else {
// Unix: Use unzip command
try {
execSync(`unzip -o "${archivePath}" -d "${extractDir}"`, {
stdio: 'pipe',
});
extracted = true;
} catch (error) {
// Will fail below
}
}
}
if (!extracted) {
const platform = process.platform === 'win32' ? 'Windows' : 'Unix';
throw new Error(
`ZIP extraction failed on ${platform}. Please ensure extraction tools are available.`,
);
}
} else {
// Extract tar.gz file using Node.js tar package first
let extracted = false;
// Method 1: Use tar package (preferred)
try {
const tar = require('tar');
// tar v4.x uses different API than v6.x
await tar.extract({
file: archivePath,
cwd: extractDir,
});
extracted = true;
console.log('tar.gz extracted using Node.js tar package');
} catch (error) {
console.log(
'Node.js tar extraction failed, trying system tar command...',
error.message,
);
}
// Method 2: System tar command fallback
if (!extracted) {
try {
execSync(`tar -xzf "${archivePath}" -C "${extractDir}"`, {
stdio: 'pipe',
});
extracted = true;
} catch (error) {
throw new Error(
'tar.gz extraction failed. Please ensure tar command is installed.',
);
}
}
}
}
calculateSha256(filePath) {
const fileBuffer = fs.readFileSync(filePath);
const hashSum = crypto.createHash('sha256');
hashSum.update(fileBuffer);
return hashSum.digest('hex');
}
findExtractedBinary(searchDir) {
const results = [];
const expectedFilename =
BINARY_NAME + (process.platform === 'win32' ? '.exe' : '');
try {
const items = fs.readdirSync(searchDir, { withFileTypes: true });
for (const item of items) {
const fullPath = path.join(searchDir, item.name);
if (item.isDirectory()) {
// Recursively search in subdirectories
results.push(...this.findExtractedBinary(fullPath));
} else if (item.name === expectedFilename) {
results.push(fullPath);
}
}
} catch (error) {
console.warn(`Unable to search directory ${searchDir}:`, error.message);
}
return results;
}
verifyInstallation() {
if (!fs.existsSync(this.binPath)) {
throw new Error('Binary installation failed');
}
if (this.logStream?.fd !== undefined) {
try {
fs.fsyncSync(this.logStream.fd);
} catch (e) {}
}
try {
const output = execSync(`"${this.binPath}" --version`, {
encoding: 'utf8',
stdio: 'pipe',
env: { ...process.env, QODER_CLI_INSTALL: '1' },
});
const versionInfo = output.trim();
console.log('Installation verified successfully');
return versionInfo;
} catch (error) {
console.warn(
'Warning: Unable to verify installation, but binary file exists',
);
return null;
}
}
async downloadFile(url, filePath, timeout = 60000) {
return new Promise((resolve, reject) => {
const file = fs.createWriteStream(filePath);
const client = url.startsWith('https:') ? https : http;
let cleanupDone = false;
const cleanup = () => {
if (cleanupDone) return;
cleanupDone = true;
try {
file.close();
} catch (e) {
// Ignore errors during cleanup
}
try {
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
} catch (e) {
// Ignore errors during cleanup
}
};
const parsedUrl = new URL(url);
const options = {
hostname: parsedUrl.hostname,
port: parsedUrl.port,
path: parsedUrl.pathname + parsedUrl.search,
headers: {
'User-Agent': 'qodercli-installer/npm (https://qoder.com)',
},
};
const request = client
.get(options, (response) => {
if (response.statusCode === 302 || response.statusCode === 301) {
// Handle redirect
cleanup();
return this.downloadFile(
response.headers.location,
filePath,
timeout,
)
.then(resolve)
.catch(reject);
}
if (response.statusCode !== 200) {
cleanup();
reject(
new Error(
`HTTP ${response.statusCode}: ${response.statusMessage}`,
),
);
return;
}
response.pipe(file);
file.on('finish', () => {
if (!cleanupDone) {
file.close();
resolve();
}
});
file.on('error', (error) => {
cleanup();
reject(error);
});
})
.on('error', (error) => {
cleanup();
reject(error);
});
// Set timeout
request.setTimeout(timeout, () => {
request.destroy();
cleanup();
reject(new Error(`Download timeout (${timeout}ms): ${url}`));
});
// Handle process interruption signals
const handleSignal = () => {
request.destroy();
cleanup();
reject(new Error('Download interrupted by signal'));
};
process.once('SIGINT', handleSignal);
process.once('SIGTERM', handleSignal);
// Clean up signal handlers when promise resolves/rejects
const originalResolve = resolve;
const originalReject = reject;
resolve = (...args) => {
process.removeListener('SIGINT', handleSignal);
process.removeListener('SIGTERM', handleSignal);
originalResolve(...args);
};
reject = (...args) => {
process.removeListener('SIGINT', handleSignal);
process.removeListener('SIGTERM', handleSignal);
originalReject(...args);
};
});
}
async install() {
let installSuccess = false;
let versionInfo = null;
try {
console.log('Installing Qoder CLI...');
console.log(`Target platform: ${this.platform}/${this.arch}`);
console.log(`Version: ${this.packageInfo.binaries.version}`);
// If already installed, reinstall
if (fs.existsSync(this.binPath)) {
console.log('Existing version detected, will reinstall');
}
const binaryInfo = this.findBinaryInfo();
await this.downloadBinary(binaryInfo.url, binaryInfo.sha256);
// Verify and get version info
versionInfo = this.verifyInstallation();
installSuccess = true;
} catch (error) {
console.error('');
console.error('Installation failed:', error.message);
console.error('');
if (this.logFile) {
console.error(`Installation log: ${this.logFile}`);
console.error('');
}
console.error('For help, visit: https://forum.qoder.com/c/bug-reports');
console.error('');
this.closeLogging();
process.exit(1);
} finally {
// Show final summary
if (installSuccess) {
this.showSuccessSummary(versionInfo);
}
// Close logging
this.closeLogging();
}
}
showSuccessSummary(versionInfo) {
console.log('');
if (versionInfo) {
console.log(`Qoder CLI ${versionInfo} installed successfully!`);
} else {
console.log('Qoder CLI installed successfully!');
}
console.log('');
console.log('Get started: qodercli --help');
console.log('Need help? Visit: https://forum.qoder.com/c/bug-reports');
console.log('');
}
}
// Main program
if (require.main === module) {
let installer = null;
try {
installer = new QoderInstaller();
installer.install();
} catch (error) {
console.error('');
console.error('Failed to initialize installer:', error.message);
console.error('');
console.error('This might be due to Node.js version compatibility issues.');
console.error(`Current Node.js version: ${process.version}`);
console.error('Required Node.js version: >=14');
console.error('');
// Show log file location if logging was initialized
// (now possible since setupLogging() runs first)
if (installer && installer.logFile) {
console.error(`Installation log: ${installer.logFile}`);
console.error('');
}
console.error('For help, visit: https://forum.qoder.com/c/bug-reports');
console.error('');
process.exit(1);
}
}
module.exports = QoderInstaller;