mirror of
https://github.com/cline/cline.git
synced 2026-09-01 15:11:04 +08:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8915aab20b | |||
| e44f9b1697 | |||
| 960ff55f83 | |||
| 07e2d0d3f8 | |||
| ba471b7db9 | |||
| 72e2c8de2b | |||
| 5e7257d218 | |||
| dcef5b29d1 | |||
| 6b530e5950 | |||
| 26daf9cb48 |
@@ -32,6 +32,7 @@ Requires Node.js 22+.
|
||||
| Example | Description | Concepts |
|
||||
|---------|-------------|----------|
|
||||
| [quickstart](./quickstart) | Send one prompt, stream the response. ~15 lines of code. | `Agent`, `subscribe`, `run()` |
|
||||
| [quickstart-clinecore](./quickstart-clinecore) | Send one prompt through a persisted Core session. | `ClineCore`, `start()`, `CoreSessionEvent`, `dispose()` |
|
||||
| [cli-agent](./cli-agent) | Interactive terminal chat with a shell tool. | `createTool`, multi-turn `run()`/`continue()`, streaming |
|
||||
| [cline-core-cli-agent](./cline-core-cli-agent) | Interactive terminal chat powered by ClineCore. | `ClineCore.create()`, `cline.start()`, `cline.send()`, built-in tools, streaming |
|
||||
|
||||
@@ -40,6 +41,7 @@ Requires Node.js 22+.
|
||||
| Example | Description | Concepts |
|
||||
|---------|-------------|----------|
|
||||
| [code-review-bot](./code-review-bot) | AI code reviewer that reads git diffs and produces structured comments. | Multiple tools, `completesRun` lifecycle, `systemPrompt`, zod schemas |
|
||||
| [security-review-bot](./security-review-bot) | AI application security reviewer that reads git diffs and produces structured security findings. | Specialized agent prompts, multiple tools, security schemas, `completesRun` lifecycle |
|
||||
| [multi-agent](./multi-agent) | Web app that fans out to three specialist agents in parallel, streams results via SSE, then synthesizes a unified answer. | Concurrent agents, `Promise.all`, per-agent `subscribe()`, SSE streaming, agent composition |
|
||||
|
||||
### Advanced
|
||||
|
||||
@@ -167,7 +167,20 @@ fn push_notification(
|
||||
}
|
||||
|
||||
fn resolve_sidecar_script(workspace_root: &str, launch_cwd: &str) -> Option<PathBuf> {
|
||||
let candidates = [
|
||||
sidecar_script_candidates(workspace_root, launch_cwd)
|
||||
.into_iter()
|
||||
.find(|p| p.exists())
|
||||
}
|
||||
|
||||
fn sidecar_script_candidates(workspace_root: &str, launch_cwd: &str) -> Vec<PathBuf> {
|
||||
vec![
|
||||
PathBuf::from(workspace_root)
|
||||
.join("sdk")
|
||||
.join("apps")
|
||||
.join("examples")
|
||||
.join("menubar")
|
||||
.join("sidecar")
|
||||
.join("index.ts"),
|
||||
PathBuf::from(workspace_root)
|
||||
.join("sdk")
|
||||
.join("apps")
|
||||
@@ -182,8 +195,7 @@ fn resolve_sidecar_script(workspace_root: &str, launch_cwd: &str) -> Option<Path
|
||||
.join("sidecar")
|
||||
.join("index.ts"),
|
||||
PathBuf::from(launch_cwd).join("sidecar").join("index.ts"),
|
||||
];
|
||||
candidates.into_iter().find(|p| p.exists())
|
||||
]
|
||||
}
|
||||
|
||||
fn sidecar_binary_name() -> String {
|
||||
@@ -261,8 +273,13 @@ fn start_sidecar(
|
||||
.current_dir(workspace_root);
|
||||
c
|
||||
} else {
|
||||
let searched_paths = sidecar_script_candidates(workspace_root, launch_cwd)
|
||||
.into_iter()
|
||||
.map(|path| path.display().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
return Err(format!(
|
||||
"menubar sidecar not found under workspace_root={workspace_root}"
|
||||
"menubar sidecar not found under workspace_root={workspace_root}; searched: {searched_paths}"
|
||||
));
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
# Quickstart with ClineCore
|
||||
|
||||
A minimal Cline SDK example that uses `ClineCore` instead of the lightweight `Agent` runtime. It starts one local Core session, sends a single prompt, streams the assistant text to stdout, prints token usage, and then disposes the Core runtime.
|
||||
|
||||
## Getting started
|
||||
|
||||
Use Node.js 22 or newer.
|
||||
|
||||
Install dependencies and build the SDK packages:
|
||||
|
||||
```bash
|
||||
bun install
|
||||
bun run build:sdk
|
||||
```
|
||||
|
||||
Set an API key:
|
||||
|
||||
```bash
|
||||
export CLINE_API_KEY="cline_..."
|
||||
```
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
bun dev
|
||||
```
|
||||
|
||||
Or pass a prompt from the command line:
|
||||
|
||||
```bash
|
||||
bun dev "Compare Agent and ClineCore in a markdown table."
|
||||
```
|
||||
|
||||
## What it does
|
||||
|
||||
1. Creates a local `ClineCore` runtime with `ClineCore.create()`
|
||||
2. Subscribes to `CoreSessionEvent` events with `cline.subscribe()`
|
||||
3. Reads a prompt from command-line arguments, falling back to the default prompt
|
||||
4. Starts a non-interactive session with `cline.start()`
|
||||
5. Renders assistant text from nested `agent_event` events
|
||||
6. Prints usage plus the persisted session ID
|
||||
7. Calls `cline.dispose()` to clean up runtime resources
|
||||
|
||||
## Agent vs ClineCore
|
||||
|
||||
The original [quickstart](../quickstart) uses `Agent`, while this example uses `ClineCore`.
|
||||
|
||||
| Area | `Agent` | `ClineCore` |
|
||||
| --- | --- | --- |
|
||||
| Best for | Simple in-process agent loops, custom tools, browser-compatible use cases | Full Cline runtime sessions, workspace-aware tools, persistence, automation, and multi-client/hub scenarios |
|
||||
| Construction | `new Agent({ providerId, modelId, apiKey })` | `await ClineCore.create({ clientName, backendMode })` |
|
||||
| Running | `await agent.run(prompt)` | `await cline.start({ prompt, config })` |
|
||||
| Events | `agent.subscribe()` emits `AgentRuntimeEvent` directly, e.g. `assistant-text-delta` | `cline.subscribe()` emits `CoreSessionEvent`; assistant events are usually nested under `event.type === "agent_event"` |
|
||||
| Result text | `result.outputText` | Session result is under `result.result`; stream text comes from events |
|
||||
| Tools | You provide tools explicitly with `tools` | Can use ClineCore built-in tools (`read_files`, `search_codebase`, shell/editor tools when enabled) and custom `extraTools` |
|
||||
| Persistence | Lightweight runtime state only | Creates persisted sessions with IDs, manifests, message artifacts, and history support |
|
||||
| Cleanup | Usually no explicit disposal needed for the simple runtime | Always call `await cline.dispose()` when done |
|
||||
|
||||
Use `Agent` when you want the smallest API surface and own all tools/context yourself. Use `ClineCore` when you want the SDK to manage Cline-like sessions, workspace context, built-in tools, persistence, hub/remote runtime options, or automation.
|
||||
|
||||
## Notes
|
||||
|
||||
This quickstart disables built-in tools (`enableTools: false`) to keep behavior close to the original one-prompt example. To explore ClineCore's workspace tools, enable tools and set tool policies appropriate for your app.
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@cline/example-quickstart-clinecore",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "bun run src/index.ts",
|
||||
"build:sdk": "bun run --cwd ../../.. build:sdk",
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cline/sdk": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { type AgentEvent, ClineCore } from "@cline/sdk";
|
||||
|
||||
const defaultPrompt = "Explain what an SDK is in two sentences.";
|
||||
const prompt = process.argv.slice(2).join(" ").trim() || defaultPrompt;
|
||||
|
||||
const cline = await ClineCore.create({
|
||||
clientName: "quickstart-clinecore",
|
||||
backendMode: "local",
|
||||
});
|
||||
|
||||
const unsubscribe = cline.subscribe((event) => {
|
||||
if (event.type === "agent_event") {
|
||||
renderAgentEvent(event.payload.event);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await cline.start({
|
||||
source: "cli",
|
||||
interactive: false,
|
||||
prompt,
|
||||
config: {
|
||||
providerId: "cline",
|
||||
modelId: "anthropic/claude-sonnet-4.6",
|
||||
apiKey: process.env.CLINE_API_KEY,
|
||||
cwd: process.cwd(),
|
||||
workspaceRoot: process.cwd(),
|
||||
mode: "act",
|
||||
systemPrompt: "You are a helpful assistant. Be concise.",
|
||||
maxIterations: 10,
|
||||
enableTools: true,
|
||||
enableSpawnAgent: false,
|
||||
enableAgentTeams: false,
|
||||
disableMcpSettingsTools: true,
|
||||
},
|
||||
});
|
||||
|
||||
const usage = result.result?.usage;
|
||||
console.log(
|
||||
`\n\nDone (${result.result?.iterations ?? 0} iteration, ${usage?.outputTokens ?? 0} output tokens)`,
|
||||
);
|
||||
console.log(`Session: ${result.sessionId}`);
|
||||
} finally {
|
||||
unsubscribe();
|
||||
await cline.dispose();
|
||||
}
|
||||
|
||||
function renderAgentEvent(event: AgentEvent) {
|
||||
if (event.type !== "content_start" || event.contentType !== "text") {
|
||||
return;
|
||||
}
|
||||
|
||||
process.stdout.write(event.text ?? "");
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../tsconfig.apps.json",
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"outDir": "dist",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -25,12 +25,19 @@ Run:
|
||||
bun dev
|
||||
```
|
||||
|
||||
Or pass a prompt from the command line:
|
||||
|
||||
```bash
|
||||
bun dev "Explain event streaming in AI apps to a frontend engineer."
|
||||
```
|
||||
|
||||
## What it does
|
||||
|
||||
1. Creates an `Agent` with a provider and model
|
||||
2. Subscribes to `assistant-text-delta` events to stream output
|
||||
3. Calls `agent.run()` with a prompt
|
||||
4. Prints token usage when done
|
||||
3. Reads a prompt from command-line arguments, falling back to the default prompt
|
||||
4. Calls `agent.run()` with that prompt
|
||||
5. Prints token usage when done
|
||||
|
||||
## Notes
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { Agent } from "@cline/sdk";
|
||||
|
||||
const defaultPrompt = "Explain what an SDK is in two sentences.";
|
||||
const prompt = process.argv.slice(2).join(" ").trim() || defaultPrompt;
|
||||
|
||||
const agent = new Agent({
|
||||
providerId: "cline",
|
||||
modelId: "anthropic/claude-sonnet-4.6",
|
||||
@@ -13,7 +16,7 @@ agent.subscribe((event) => {
|
||||
}
|
||||
});
|
||||
|
||||
const result = await agent.run("Explain what an SDK is in two sentences.");
|
||||
const result = await agent.run(prompt);
|
||||
console.log(
|
||||
`\n\nDone (${result.iterations} iteration, ${result.usage.outputTokens} output tokens)`,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
# Security Review Bot on git diffs
|
||||
|
||||
An AI-powered application security review agent that reads a git diff, analyzes it for security risk, and produces structured findings with severity, category, CWE/OWASP metadata, exploit scenarios, remediation guidance, and confidence levels.
|
||||
|
||||
This example is based on the `code-review-bot` example, but uses the `ClineCore` runtime instead of the lightweight `Agent` runtime. That means it can use ClineCore's built-in workspace tools, including `read_files` and `search_codebase`, while still adding custom security review tools through `extraTools`.
|
||||
|
||||
## Getting started
|
||||
|
||||
Install dependencies:
|
||||
|
||||
```bash
|
||||
bun install
|
||||
bun run build:sdk
|
||||
```
|
||||
|
||||
Set an API key:
|
||||
|
||||
```bash
|
||||
export CLINE_API_KEY="cline_..."
|
||||
```
|
||||
|
||||
Security review the last commit:
|
||||
|
||||
```bash
|
||||
bun dev
|
||||
```
|
||||
|
||||
Security review against a specific ref:
|
||||
|
||||
```bash
|
||||
bun dev main
|
||||
bun dev HEAD~5
|
||||
bun dev abc123
|
||||
```
|
||||
|
||||
Add run-specific review instructions with `--prompt` or `-p`:
|
||||
|
||||
```bash
|
||||
bun dev main --prompt "Focus on authorization bypasses and SSRF"
|
||||
bun dev HEAD~1 -p "Treat test-only secrets as low severity"
|
||||
```
|
||||
|
||||
You can also pass extra positional text after the ref; it will be appended as additional review instructions:
|
||||
|
||||
```bash
|
||||
bun dev main "Prioritize auth, secrets, and unsafe file access"
|
||||
```
|
||||
|
||||
## Review a specific repo
|
||||
|
||||
The bot reviews the git repository in the current working directory. To review a different repo, run the example command from that repo's directory and pass the ref you want to compare against.
|
||||
|
||||
Using Bun directly from another repo:
|
||||
|
||||
```bash
|
||||
cd /path/to/repo-you-want-to-review
|
||||
bun --cwd /path/to/cline/sdk/apps/examples/security-review-bot dev main
|
||||
```
|
||||
|
||||
With extra instructions:
|
||||
|
||||
```bash
|
||||
cd /path/to/repo-you-want-to-review
|
||||
bun --cwd /path/to/cline/sdk/apps/examples/security-review-bot dev main --prompt "Focus on payment and admin flows"
|
||||
```
|
||||
|
||||
Or build the example once and run the compiled JavaScript from any git repo:
|
||||
|
||||
```bash
|
||||
cd /path/to/cline/sdk/apps/examples/security-review-bot
|
||||
bun run build
|
||||
|
||||
cd /path/to/repo-you-want-to-review
|
||||
node /path/to/cline/sdk/apps/examples/security-review-bot/dist/index.js main
|
||||
```
|
||||
|
||||
The argument is passed to `git diff <ref>` in the target repo. Common examples:
|
||||
|
||||
```bash
|
||||
# Review all changes on your branch compared with main
|
||||
bun --cwd /path/to/cline/sdk/apps/examples/security-review-bot dev main
|
||||
|
||||
# Review the last commit
|
||||
bun --cwd /path/to/cline/sdk/apps/examples/security-review-bot dev HEAD~1
|
||||
|
||||
# Review changes since a release tag
|
||||
bun --cwd /path/to/cline/sdk/apps/examples/security-review-bot dev v1.2.3
|
||||
```
|
||||
|
||||
If you want to review uncommitted changes in a target repo, compare against the branch or commit you started from, for example:
|
||||
|
||||
```bash
|
||||
cd /path/to/repo-you-want-to-review
|
||||
bun --cwd /path/to/cline/sdk/apps/examples/security-review-bot dev main
|
||||
```
|
||||
|
||||
## What it does
|
||||
|
||||
1. Reads a `git diff` against the specified ref (defaults to `HEAD~1`)
|
||||
2. Starts a local `ClineCore` session with built-in tools enabled
|
||||
3. Lets the model use ClineCore's `read_files` and `search_codebase` tools for surrounding file context
|
||||
4. Adds two custom security review tools through `extraTools`:
|
||||
- `add_security_finding` - records a structured security finding with severity, category, exploit scenario, remediation, and confidence
|
||||
- `submit_security_review` - a completion tool that ends the run with a summary, overall risk rating, and merge-blocking decision
|
||||
5. Prints findings grouped by severity (`critical`, `high`, `medium`, `low`, `info`)
|
||||
|
||||
## Finding schema
|
||||
|
||||
Each finding includes:
|
||||
|
||||
- `file` and `line` - where the issue appears
|
||||
- `severity` - `critical`, `high`, `medium`, `low`, or `info`
|
||||
- `category` - security category such as `injection`, `authorization`, `secrets`, `ssrf`, `xss`, or `cryptography`
|
||||
- `cwe` and `owasp` - optional vulnerability taxonomy metadata
|
||||
- `title` and `description` - concise summary and risk explanation
|
||||
- `exploitScenario` - realistic abuse case
|
||||
- `remediation` - concrete fix or mitigation
|
||||
- `confidence` - `high`, `medium`, or `low`
|
||||
|
||||
## Concepts demonstrated
|
||||
|
||||
- Using `ClineCore.create()` and `cline.start()` for a local runtime session
|
||||
- Enabling selected ClineCore built-in tools, including `read_files` and `search_codebase`
|
||||
- Adding domain-specific custom tools with `extraTools`
|
||||
- `lifecycle: { completesRun: true }` to make a tool end the agent loop
|
||||
- Subscribing to `CoreSessionEvent` events with `cline.subscribe()`
|
||||
- Rendering parsed `agent_event` text/tool events instead of printing serialized `chunk` payloads
|
||||
- Cleaning up runtime resources with `cline.dispose()`
|
||||
- Processing structured results after the run completes
|
||||
|
||||
## Notes
|
||||
|
||||
This is an AI-assisted review tool, not a replacement for SAST, dependency scanning, secret scanning, manual threat modeling, or human security review. Treat findings as review input and verify them before acting.
|
||||
|
||||
For a general-purpose reviewer, see [code-review-bot](../code-review-bot). For a simpler starting point, see [quickstart](../quickstart).
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "@cline/example-security-review-bot",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "bun run src/index.ts",
|
||||
"build:sdk": "bun run --cwd ../../.. build:sdk",
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cline/sdk": "workspace:*",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,426 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import {
|
||||
type AgentEvent,
|
||||
type AgentTool,
|
||||
ClineCore,
|
||||
createTool,
|
||||
type ToolPolicy,
|
||||
} from "@cline/sdk";
|
||||
import { z } from "zod";
|
||||
|
||||
const SecurityFindingSchema = z.object({
|
||||
file: z.string().describe("File path"),
|
||||
line: z.number().describe("Line number (approximate is fine)"),
|
||||
severity: z.enum(["critical", "high", "medium", "low", "info"]),
|
||||
category: z
|
||||
.enum([
|
||||
"authentication",
|
||||
"authorization",
|
||||
"injection",
|
||||
"secrets",
|
||||
"cryptography",
|
||||
"data_exposure",
|
||||
"ssrf",
|
||||
"xss",
|
||||
"deserialization",
|
||||
"path_traversal",
|
||||
"dependency",
|
||||
"logging_monitoring",
|
||||
"configuration",
|
||||
"other",
|
||||
])
|
||||
.describe("Security weakness category"),
|
||||
cwe: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Relevant CWE identifier, such as CWE-89, if known"),
|
||||
owasp: z.string().optional().describe("Relevant OWASP category, if known"),
|
||||
title: z.string().describe("Short finding title"),
|
||||
description: z.string().describe("What is vulnerable and why it matters"),
|
||||
exploitScenario: z
|
||||
.string()
|
||||
.describe("A realistic abuse case or exploit path"),
|
||||
remediation: z.string().describe("Concrete recommended fix or mitigation"),
|
||||
confidence: z.enum(["high", "medium", "low"]),
|
||||
});
|
||||
|
||||
const SecurityReviewResultSchema = z.object({
|
||||
summary: z.string().describe("Brief overall security assessment"),
|
||||
overallRisk: z.enum(["critical", "high", "medium", "low", "none"]),
|
||||
blockMerge: z
|
||||
.boolean()
|
||||
.describe("Whether the changes should be blocked from merging"),
|
||||
});
|
||||
|
||||
const findings: z.infer<typeof SecurityFindingSchema>[] = [];
|
||||
let reviewResult: z.infer<typeof SecurityReviewResultSchema> | undefined;
|
||||
|
||||
const repoRoot = (() => {
|
||||
try {
|
||||
return execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
||||
encoding: "utf-8",
|
||||
}).trim();
|
||||
} catch {
|
||||
console.error("Error: must be run from within a git repository.");
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
|
||||
const systemPrompt = `You are a senior application security engineer performing a focused security review of a git diff.
|
||||
|
||||
Your goal is to identify exploitable or defense-in-depth security issues introduced or modified by the diff. The repository root is ${repoRoot}. Use the read_files tool with absolute paths, or search_codebase, when you need surrounding code to determine whether a finding is real.
|
||||
|
||||
Focus on:
|
||||
- Authentication and authorization bypasses
|
||||
- Injection flaws, including SQL/NoSQL/command/template/LDAP injection
|
||||
- Cross-site scripting and unsafe HTML/script rendering
|
||||
- Server-side request forgery and unsafe URL fetching
|
||||
- Path traversal and unsafe file operations
|
||||
- Hardcoded secrets, tokens, private keys, and credential leakage
|
||||
- Cryptographic misuse, weak randomness, insecure hashing, or missing integrity checks
|
||||
- Sensitive data exposure in logs, errors, telemetry, APIs, or client bundles
|
||||
- Unsafe deserialization or dynamic code execution
|
||||
- Insecure dependency, environment, CORS, cookie, header, or cloud configuration changes
|
||||
- Missing validation, rate limits, audit logging, or security checks around sensitive operations
|
||||
|
||||
Only report actionable findings that are supported by the diff or file context. Do not report generic best practices unless they materially reduce a realistic risk. Prefer fewer high-quality findings over many speculative ones.
|
||||
|
||||
Severity guidance:
|
||||
- critical: likely remote code execution, auth bypass, mass data exposure, secret compromise, or trivially exploitable injection on sensitive data paths
|
||||
- high: practical privilege escalation, significant data exposure, SSRF with meaningful impact, or exploitable stored XSS
|
||||
- medium: plausible vulnerability requiring constraints, limited data exposure, missing authorization on lower-risk operations, or reflected XSS with user interaction
|
||||
- low: hardening issue with limited exploitability
|
||||
- info: noteworthy observation with minimal direct risk
|
||||
|
||||
For each real issue, call add_security_finding with clear evidence, exploit scenario, remediation, confidence, and CWE/OWASP metadata when applicable.
|
||||
|
||||
When you are done reviewing, call submit_security_review with a brief summary, an overall risk rating, and whether the changes should be blocked from merging.`;
|
||||
|
||||
const addSecurityFindingTool = createTool({
|
||||
name: "add_security_finding",
|
||||
description:
|
||||
"Add an actionable security finding on a specific file and line.",
|
||||
inputSchema: SecurityFindingSchema,
|
||||
async execute(input) {
|
||||
findings.push(input);
|
||||
return {
|
||||
success: true,
|
||||
message: `Security finding added (${findings.length} total)`,
|
||||
findingCount: findings.length,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const submitSecurityReviewTool = createTool({
|
||||
name: "submit_security_review",
|
||||
description: "Submit the completed security review with a risk summary.",
|
||||
inputSchema: SecurityReviewResultSchema,
|
||||
lifecycle: { completesRun: true },
|
||||
async execute(input) {
|
||||
reviewResult = input;
|
||||
return {
|
||||
success: true,
|
||||
summary: input.summary,
|
||||
overallRisk: input.overallRisk,
|
||||
blockMerge: input.blockMerge,
|
||||
findingCount: findings.length,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const securityReviewTools: AgentTool[] = [
|
||||
addSecurityFindingTool as AgentTool,
|
||||
submitSecurityReviewTool as AgentTool,
|
||||
];
|
||||
|
||||
const securityReviewToolPolicies = {
|
||||
"*": { autoApprove: true },
|
||||
read_files: { autoApprove: true },
|
||||
search_codebase: { autoApprove: true },
|
||||
add_security_finding: { autoApprove: true },
|
||||
submit_security_review: { autoApprove: true },
|
||||
run_commands: { enabled: false },
|
||||
fetch_web_content: { enabled: false },
|
||||
editor: { enabled: false },
|
||||
apply_patch: { enabled: false },
|
||||
skills: { enabled: false },
|
||||
ask_question: { enabled: false },
|
||||
} satisfies Record<string, ToolPolicy>;
|
||||
|
||||
let reasoningOpen = false;
|
||||
|
||||
function usage() {
|
||||
console.error("Usage: bun dev [git-ref] [--prompt <instructions>]");
|
||||
console.error(" e.g. bun dev HEAD~3");
|
||||
console.error(" e.g. bun dev main");
|
||||
console.error(
|
||||
' e.g. bun dev main --prompt "Focus on authorization bypasses and SSRF"',
|
||||
);
|
||||
console.error(
|
||||
' e.g. bun dev main "Prioritize auth, secrets, and unsafe file access"',
|
||||
);
|
||||
}
|
||||
|
||||
function parseArgs(args: string[]): { ref: string; extraPrompt?: string } {
|
||||
let ref = "HEAD~1";
|
||||
let hasRef = false;
|
||||
const promptParts: string[] = [];
|
||||
const positionalPromptParts: string[] = [];
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
|
||||
if (arg === "--help" || arg === "-h") {
|
||||
usage();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (arg === "--prompt" || arg === "-p") {
|
||||
const value = args[i + 1];
|
||||
if (!value) {
|
||||
console.error(`Missing value for ${arg}`);
|
||||
usage();
|
||||
process.exit(1);
|
||||
}
|
||||
promptParts.push(value);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.startsWith("--prompt=")) {
|
||||
const value = arg.slice("--prompt=".length);
|
||||
if (!value) {
|
||||
console.error("Missing value for --prompt");
|
||||
usage();
|
||||
process.exit(1);
|
||||
}
|
||||
promptParts.push(value);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.startsWith("-")) {
|
||||
console.error(`Unknown option: ${arg}`);
|
||||
usage();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!hasRef) {
|
||||
ref = arg;
|
||||
hasRef = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
positionalPromptParts.push(arg);
|
||||
}
|
||||
|
||||
if (positionalPromptParts.length > 0) {
|
||||
promptParts.push(positionalPromptParts.join(" "));
|
||||
}
|
||||
|
||||
return {
|
||||
ref,
|
||||
extraPrompt: promptParts.length > 0 ? promptParts.join("\n\n") : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function closeReasoning() {
|
||||
if (reasoningOpen) {
|
||||
process.stdout.write("\n");
|
||||
reasoningOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
function renderToolStart(
|
||||
event: Extract<AgentEvent, { type: "content_start" }>,
|
||||
) {
|
||||
const toolName = event.toolName ?? "unknown_tool";
|
||||
if (toolName === "add_security_finding") {
|
||||
const parsed = SecurityFindingSchema.safeParse(event.input);
|
||||
if (!parsed.success) {
|
||||
console.log(`\n[tool] ${toolName}`);
|
||||
return;
|
||||
}
|
||||
const input = parsed.data;
|
||||
const icon =
|
||||
input.severity === "critical"
|
||||
? "X"
|
||||
: input.severity === "high"
|
||||
? "!"
|
||||
: input.severity === "medium"
|
||||
? "~"
|
||||
: "i";
|
||||
console.log(
|
||||
`\n [${icon}] ${input.severity.toUpperCase()} ${input.file}:${input.line} - ${input.title}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`\n[tool] ${toolName}`);
|
||||
}
|
||||
|
||||
function renderAgentEvent(event: AgentEvent) {
|
||||
switch (event.type) {
|
||||
case "content_start":
|
||||
if (event.contentType === "text" && event.text) {
|
||||
closeReasoning();
|
||||
process.stdout.write(event.text);
|
||||
return;
|
||||
}
|
||||
if (event.contentType === "reasoning") {
|
||||
if (event.redacted === true && !event.reasoning) {
|
||||
console.log("\n[thinking redacted]");
|
||||
return;
|
||||
}
|
||||
if (event.reasoning) {
|
||||
if (!reasoningOpen) {
|
||||
process.stdout.write("\n[thinking]\n");
|
||||
reasoningOpen = true;
|
||||
}
|
||||
process.stdout.write(event.reasoning);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (event.contentType === "tool") {
|
||||
closeReasoning();
|
||||
renderToolStart(event);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case "content_end":
|
||||
if (event.contentType === "reasoning") {
|
||||
closeReasoning();
|
||||
return;
|
||||
}
|
||||
if (event.contentType === "tool" && event.error) {
|
||||
closeReasoning();
|
||||
console.log(
|
||||
`\n[tool failed] ${event.toolName ?? "unknown_tool"}: ${event.error}`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Get the diff to review
|
||||
const { ref, extraPrompt } = parseArgs(process.argv.slice(2));
|
||||
if (ref.startsWith("-")) {
|
||||
console.error(`Invalid ref: ${ref}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let diff: string;
|
||||
try {
|
||||
diff = execFileSync("git", ["diff", ref], {
|
||||
encoding: "utf-8",
|
||||
maxBuffer: 5 * 1024 * 1024,
|
||||
});
|
||||
} catch {
|
||||
console.error(`Failed to get diff for ref: ${ref}`);
|
||||
usage();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!diff.trim()) {
|
||||
console.log("No diff found. Nothing to security review.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Security reviewing diff against ${ref} (${diff.split("\n").length} lines)...\n`,
|
||||
);
|
||||
if (extraPrompt) {
|
||||
console.log(`Additional review instructions: ${extraPrompt}\n`);
|
||||
}
|
||||
|
||||
const cline = await ClineCore.create({
|
||||
clientName: "security-review-bot",
|
||||
backendMode: "local",
|
||||
});
|
||||
|
||||
const unsubscribe = cline.subscribe((event) => {
|
||||
switch (event.type) {
|
||||
case "agent_event":
|
||||
renderAgentEvent(event.payload.event);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const prompt = `Security review this git diff.${
|
||||
extraPrompt ? `\n\nAdditional review instructions:\n${extraPrompt}` : ""
|
||||
}\n\n\`\`\`diff\n${diff}\n\`\`\``;
|
||||
|
||||
const result = await cline.start({
|
||||
source: "cli",
|
||||
interactive: false,
|
||||
prompt,
|
||||
config: {
|
||||
providerId: "cline",
|
||||
modelId: "anthropic/claude-sonnet-4.6",
|
||||
apiKey: process.env.CLINE_API_KEY,
|
||||
cwd: repoRoot,
|
||||
workspaceRoot: repoRoot,
|
||||
mode: "act",
|
||||
systemPrompt,
|
||||
maxIterations: 25,
|
||||
enableTools: true,
|
||||
enableSpawnAgent: false,
|
||||
enableAgentTeams: false,
|
||||
disableMcpSettingsTools: true,
|
||||
toolPolicies: securityReviewToolPolicies,
|
||||
},
|
||||
localRuntime: {
|
||||
extraTools: securityReviewTools,
|
||||
},
|
||||
});
|
||||
|
||||
console.log("\n\n--- Security Review Complete ---\n");
|
||||
|
||||
if (reviewResult) {
|
||||
console.log(`Summary: ${reviewResult.summary}`);
|
||||
console.log(`Overall risk: ${reviewResult.overallRisk}`);
|
||||
console.log(`Block merge: ${reviewResult.blockMerge ? "yes" : "no"}\n`);
|
||||
}
|
||||
|
||||
if (findings.length === 0) {
|
||||
console.log("No actionable security findings identified.");
|
||||
} else {
|
||||
const severityOrder = [
|
||||
"critical",
|
||||
"high",
|
||||
"medium",
|
||||
"low",
|
||||
"info",
|
||||
] as const;
|
||||
|
||||
for (const severity of severityOrder) {
|
||||
const group = findings.filter((finding) => finding.severity === severity);
|
||||
if (group.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`${severity.toUpperCase()}: ${group.length}`);
|
||||
for (const finding of group) {
|
||||
const metadata = [finding.cwe, finding.owasp]
|
||||
.filter(Boolean)
|
||||
.join(" | ");
|
||||
console.log(
|
||||
` ${finding.file}:${finding.line} - ${finding.title}${metadata ? ` (${metadata})` : ""}`,
|
||||
);
|
||||
console.log(` Category: ${finding.category}`);
|
||||
console.log(` Confidence: ${finding.confidence}`);
|
||||
console.log(` Risk: ${finding.description}`);
|
||||
console.log(` Exploit: ${finding.exploitScenario}`);
|
||||
console.log(` Fix: ${finding.remediation}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const usage = result.result?.usage;
|
||||
console.log(
|
||||
`\nSession: ${result.sessionId} | Status: ${result.result?.finishReason ?? "unknown"} | Iterations: ${result.result?.iterations ?? 0} | Tokens: ${usage?.outputTokens ?? 0} output`,
|
||||
);
|
||||
} finally {
|
||||
unsubscribe();
|
||||
await cline.dispose();
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../tsconfig.apps.json",
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"outDir": "dist",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -197,6 +197,16 @@
|
||||
"typescript": "^5.9.3",
|
||||
},
|
||||
},
|
||||
"apps/examples/quickstart-clinecore": {
|
||||
"name": "@cline/example-quickstart-clinecore",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@cline/sdk": "workspace:*",
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.9.3",
|
||||
},
|
||||
},
|
||||
"apps/examples/security-review-bot": {
|
||||
"name": "@cline/example-security-review-bot",
|
||||
"version": "0.0.0",
|
||||
@@ -635,6 +645,8 @@
|
||||
|
||||
"@cline/example-quickstart": ["@cline/example-quickstart@workspace:apps/examples/quickstart"],
|
||||
|
||||
"@cline/example-quickstart-clinecore": ["@cline/example-quickstart-clinecore@workspace:apps/examples/quickstart-clinecore"],
|
||||
|
||||
"@cline/example-security-review-bot": ["@cline/example-security-review-bot@workspace:apps/examples/security-review-bot"],
|
||||
|
||||
"@cline/llms": ["@cline/llms@workspace:packages/llms"],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type {
|
||||
GatewayModelRoute,
|
||||
GatewayReasoningFormat,
|
||||
GatewayProviderContext,
|
||||
GatewayReasoningFormat,
|
||||
GatewayStreamRequest,
|
||||
} from "@cline/shared";
|
||||
|
||||
@@ -21,7 +21,9 @@ function normalizedFamily(context: GatewayProviderContext): string {
|
||||
return normalizeRoutingValue(resolveModelFamily(context)) ?? "";
|
||||
}
|
||||
|
||||
function normalizedModelId(request: Pick<GatewayStreamRequest, "modelId">): string {
|
||||
function normalizedModelId(
|
||||
request: Pick<GatewayStreamRequest, "modelId">,
|
||||
): string {
|
||||
return normalizeRoutingValue(request.modelId) ?? "";
|
||||
}
|
||||
|
||||
@@ -38,7 +40,9 @@ function isClaudeLineageValue(value: string | undefined): boolean {
|
||||
|
||||
function isQwenLineageValue(value: string | undefined): boolean {
|
||||
const normalized = normalizeRoutingValue(value);
|
||||
return normalized ? /(^|[/:._-])qwen(?:$|[/:._-]|\d)/.test(normalized) : false;
|
||||
return normalized
|
||||
? /(^|[/:._-])qwen(?:$|[/:._-]|\d)/.test(normalized)
|
||||
: false;
|
||||
}
|
||||
|
||||
export function isAnthropicCompatibleModel(options: {
|
||||
|
||||
Reference in New Issue
Block a user