mirror of
https://github.com/KeygraphHQ/shannon.git
synced 2026-10-03 06:46:49 +02:00
Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cbf2019e7f | ||
|
|
692440135a | ||
|
|
fabf56e574 | ||
|
|
85a29e2ea3 | ||
|
|
82436dafe6 | ||
|
|
8a3fbc5286 | ||
|
|
d9748355e3 | ||
|
|
9a5f201f15 | ||
|
|
945fd2ce57 | ||
|
|
4b26e677dd | ||
|
|
c8ee22f2d1 | ||
|
|
529f6a7a95 | ||
|
|
5a43c6127f | ||
|
|
9f7a349715 | ||
|
|
d017d74519 | ||
|
|
6108de3cfc | ||
|
|
7e0464bf79 | ||
|
|
dc2a4fe4e8 |
No files matched your search
@@ -53,3 +53,8 @@ SHANNON_AI_MODEL=anthropic:claude-sonnet-4-6
|
||||
# https://github.com/KeygraphHQ/shannon/blob/main/docs/ai-providers.md#openai-codex-chatgpt-pluspro-subscription
|
||||
# SHANNON_USE_PI_AUTH=1
|
||||
# SHANNON_AI_MODEL=openai-codex:gpt-5.5
|
||||
|
||||
# Or the guide below to use an xAI subscription
|
||||
# https://github.com/KeygraphHQ/shannon/blob/main/docs/ai-providers.md#xai-grok-subscription
|
||||
# SHANNON_USE_PI_AUTH=1
|
||||
# SHANNON_AI_MODEL=xai:grok-4.6
|
||||
@@ -5,3 +5,5 @@ credentials/
|
||||
dist/
|
||||
repos/
|
||||
.turbo/
|
||||
|
||||
.DS_Store
|
||||
@@ -155,7 +155,7 @@ Durable workflow orchestration with crash recovery, queryable progress, intellig
|
||||
- **Configuration** — YAML configs in `apps/worker/configs/` with JSON Schema validation (`config-schema.json`). Supports auth settings (MFA/TOTP), URL/code rule scoping (`rules.avoid`/`rules.focus`), run-scope steering (`vuln_classes`, `exploit`), free-form `rules_of_engagement`, and post-hoc `report` options (`min_severity`, `min_confidence`, `guidance`, and `sarif` for a SARIF 2.1.0 log via `apps/worker/src/services/sarif-renderer.ts`, on by default for exploit runs and opt out with `report.sarif: false`). `code_path` avoid rules are enforced via the `@gotgenes/pi-permission-system` extension: `apps/worker/src/temporal/activities.ts:syncCodePathDenyRules` writes a global `path` deny config once per workflow (`apps/worker/src/ai/pi/permission-system.ts:syncPermissionSystemConfig`), and the executor loads the extension when that config is present (`apps/worker/src/ai/pi/pi-executor.ts`), so denies fire across every tool and child `task` session. `vuln_classes`/`exploit` scope is locked into `session.json` on first run; resumes with a different scope fail fast (`persistOrValidateRunScope`). Credential resolution — local mode: env vars → `./.env`; npx mode: env vars → `~/.shannon/config.toml` (via `npx @keygraph/shannon setup`)
|
||||
- **Prompts** — Per-phase templates in `apps/worker/prompts/` with variable substitution (`{{TARGET_URL}}`, `{{CONFIG_CONTEXT}}`). Shared partials in `apps/worker/prompts/shared/` via `apps/worker/src/services/prompt-manager.ts`, including `_code-path-rules.txt` (focus/avoid `[FILE]`/`[GLOB]` routing) and `_rules-of-engagement.txt` (free-text engagement rules). When `exploit: false`, `apps/worker/src/services/findings-renderer.ts` deterministically converts each `*_exploitation_queue.json` into a `*_findings.md` for report assembly — no LLM in the loop
|
||||
- **Agent Harness (pi)** — Uses the **pi harness** (`@earendil-works/pi-coding-agent`, requires Node ≥ 22.19) via `apps/worker/src/ai/pi/pi-executor.ts` (`runPiPrompt` → `createAgentSession`). Retry is split in `apps/worker/src/ai/pi/retry-settings.ts`: pi's agent-level loop is off so Temporal owns agent restarts, while `provider.maxRetries` stays on — pi reads the `provider` block independently of the `enabled` flag — so transport faults are absorbed in-session rather than costing a full agent re-run. `maxRetryDelayMs` is left at pi's 60s default. One model runs every phase, named by `SHANNON_AI_MODEL=<provider>:<model-id>` (default `anthropic:claude-sonnet-4-6`). `apps/worker/src/ai/models.ts` parses the spec — splitting on the **first** colon only, so Bedrock IDs keep theirs — and resolves it through pi's `ModelRuntime`. pi ships the `CredentialStore` interface but no in-memory implementation (its own reads `auth.json` from disk), so `RuntimeCredentialStore` in that file supplies one: credentials arrive as env vars in an ephemeral container and must never touch disk. `createModelRuntime(providerId, apiKey)` builds the runtime; `allowModelNetwork` stays at its default `false` so a scan never blocks on a catalog refresh. `resolveModelSelection()` is **async** because `ModelRuntime.create()` is. Any pi-ai provider id is accepted — `parseModelSpec` no longer rejects against a hardcoded list, so pi's registry is the authority (an unknown provider/model surfaces as a clear "not found in pi registry" error at preflight, which points to the browsable catalogue at `pi.dev/models` — `PI_CATALOG_URL` in `apps/worker/src/ai/models.ts`, appended to the not-found errors and shown in the setup wizard's "Other provider" hint). Four providers are **curated** (`CURATED_PROVIDERS`: `anthropic`, `openai`, `xai`, `amazon-bedrock`) with their own credential variables, config sections, and setup flows; each provider's API key env var is declared once in `PROVIDER_API_KEY_ENV` — Shannon uses each vendor's own variable name (`OPENAI_API_KEY`, `XAI_API_KEY`, …), never an invented one; Bedrock's entry is `AWS_BEARER_TOKEN_BEDROCK`, paired with `AWS_REGION`, which preflight requires separately as provider config rather than a credential. Any other provider uses the **generic** credential path: `SHANNON_AI_API_KEY` (`GENERIC_API_KEY_ENV`) supplies the key for any provider whose credential is a plain API key. Curated providers' own variables take precedence over it, and it also works as a fallback for them — Bedrock is the sole exception (it authenticates through its AWS_ variables, so the generic key never stands in for it). The CLI forwards `SHANNON_AI_API_KEY` in `COMMON_FORWARD_VARS` (it is provider-neutral, binding to whatever `SHANNON_AI_MODEL` names, so the "only one provider configured" guard counts only named credentials), and stores it under a generic `[provider]` config.toml section (`provider.api_key`). `npx @keygraph/shannon setup` exposes this as the "Other provider" option: free-text provider id + model id + key (a curated provider id is rejected there, since it has its own option). `SHANNON_AI_BASE_URL` overrides the endpoint for any provider (proxies/gateways); the credential is unchanged. `pointAtGateway` (`apps/worker/src/ai/models.ts`) applies the one dialect change: behind a base URL, `openai` follows `SHANNON_AI_OPENAI_FORMAT` (`chat-completions` default, or `responses`). On `chat-completions` it switches the API to `openai-completions` and drops the catalogue's Responses-shaped `compat` block so pi's `detectCompat` derives completions settings; on `responses` the descriptor is unchanged but for the endpoint. `resolveGatewayFormat` rejects the variable when the provider is not `openai` or no base URL is set, since it cannot take effect there. All other providers keep their API. The CLI mirrors the accepted values in `apps/cli/src/model-spec.ts`, forwards the variable in `COMMON_FORWARD_VARS`, and maps it to `openai.format` in config.toml. `buildEnvFlags` forwards only the selected provider's credential into the worker container. The CLI mirrors the parse rule and the provider/credential tables in `apps/cli/src/model-spec.ts` (it cannot import from the worker package); the two must stay in sync. pi ships no JSON-schema output or `Task`/`TodoWrite` built-ins, so structured queues are captured via a `submit_exploitation_queue` custom tool (`apps/worker/src/ai/queue-schemas.ts`), and `task` (child sessions scoped to `read`, `grep`, `find`, `ls`, `write`, and `bash` — no nested `task` or collector tools; `CHILD_TOOLS` in `apps/worker/src/ai/pi/task-tool.ts`) + `todo_write` (`apps/worker/src/ai/pi/session-tools.ts`) are provided as custom tools; the per-phase collectors are pi custom tools (TypeBox `defineTool` in `apps/worker/src/collectors/`). Shannon sets no thinking configuration at all — no `thinkingLevel` is passed to any `createAgentSession` call, so pi's own default applies. There Line truncated
|
||||
- **Pi Credential Reuse** — `SHANNON_USE_PI_AUTH=1` opts into reusing the host's Pi login, including an `openai-codex` ChatGPT Plus/Pro subscription selected with `SHANNON_AI_MODEL=openai-codex:<model-id>`. `apps/cli/src/env.ts` requires `~/.pi/agent/auth.json`; `start.ts` passes its path to `spawnWorker`, which mounts only that file read-write at `/tmp/.pi/agent/auth.json`. The flag itself is not forwarded: the worker detects the file with `piAuthPresent()` and passes its path to `ModelRuntime.create`. CLI and worker API-key presence checks are skipped on this path, but the normal preflight model probe still validates the credential. The image and UID-remapping entrypoint keep `/tmp/.pi/agent` owned by `pentest` so adjacent Pi/Shannon configuration remains writable. Refreshed OAuth state is persisted to the host for subsequent scans.
|
||||
- **Pi Credential Reuse** — `SHANNON_USE_PI_AUTH=1` opts into reusing the host's Pi login, including an `openai-codex` ChatGPT Plus/Pro subscription (`SHANNON_AI_MODEL=openai-codex:<model-id>`) or an `xai` Grok subscription (`SHANNON_AI_MODEL=xai:<model-id>`); the mechanism is provider-agnostic and works for any Pi login. `apps/cli/src/env.ts` requires `~/.pi/agent/auth.json`; `start.ts` passes its path to `spawnWorker`, which mounts only that file read-write at `/tmp/.pi/agent/auth.json`. The flag itself is not forwarded: the worker detects the file with `piAuthPresent()` and passes its path to `ModelRuntime.create`. CLI and worker API-key presence checks are skipped on this path, but the normal preflight model probe still validates the credential. The image and UID-remapping entrypoint keep `/tmp/.pi/agent` owned by `pentest` so adjacent Pi/Shannon configuration remains writable. Refreshed OAuth state is persisted to the host for subsequent scans.
|
||||
- **Audit System** — Crash-safe append-only logging in `workspaces/{hostname}_{sessionId}/`. The run directory's top level holds the human-facing report in both formats (`Security-Assessment-Report.pdf` and `Security-Assessment-Report.md`, `FINAL_REPORT_PDF_FILENAME`/`FINAL_REPORT_MD_FILENAME` in `apps/worker/src/paths.ts`); everything else — deliverables, per-agent logs, prompts, `session.json`, `workflow.log`, and browser artifacts — is nested under a hidden `.shannon/` internals dir (`INTERNAL_DIR`) so a customer sees only the report. Audit path helpers route through `generateInternalPath` (`apps/worker/src/audit/utils.ts`); the CLI nests the overlay backing dirs under the same `.shannon/` (`apps/cli/src/docker.ts`, `start.ts`). `session.json`/`workflow.log` reads use dual-read resolvers (`resolveSessionJsonPath`, `resolveRunFile`) that prefer `.shannon/` and fall back to the legacy run-root layout, so pre-restructure workspaces stay listable (`workspaces`/`logs`) without migration. Resuming a pre-restructure workspace upgrades it in place first: `migrateLegacyWorkspaceLayout` (`apps/cli/src/commands/start.ts`) renames the flat deliverables/logs/session entries into `.shannon/` (carrying the deliverables `.git` along) before the overlay dirs are mounted, so resume finds the old checkpoints instead of re-running every agent. The report agent writes structured findings to `report.json`, from which `report-renderer.ts` renders the assembled markdown and `report-json-adapter.ts` produces the Typst-shaped JSON that `pdf-renderer.ts` compiles into `comprehensive_security_assessment_report.pdf` using the bundled `apps/worker/templates/typst/report.typ` template (the `typst` binary is installed in the worker image). `copyReportToRunRoot` (`apps/worker/src/services/reporting.ts`) surfaces both the PDF and the markdown to the run root as `Security-Assessment-Report.pdf` and `Security-Assessment-Report.md`; the deliverables-dir copies remain as the git-checkpointed sources. PDF compilation is best-effort — a failure is logged and the run still completes. WorkflowLogger (`apps/worker/src/audit/workflow-logger.ts`) provides unified human-readable per-workflow logs, backed by LogStream (`apps/worker/src/audit/log-stream.ts`) shared stream primitive
|
||||
- **Deliverables** — Saved to `.shannon/deliverables/` in the target repo via the `save-deliverable` CLI script (`apps/worker/src/scripts/save-deliverable.ts`)
|
||||
- **Workspaces & Resume** — Named workspaces via `-w <name>` or auto-named from URL+timestamp. Resume detects completed agents via `session.json`. `loadResumeState()` in `apps/worker/src/temporal/activities.ts` validates deliverable existence, restores git checkpoints, and cleans up incomplete deliverables
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./assets/github-banner-dark.png">
|
||||
<source media="(prefers-color-scheme: light)" srcset="./assets/github-banner-light.png">
|
||||
<img src="./assets/github-banner.png" alt="Shannon - AI Pentester by Keygraph" width="100%">
|
||||
<img src="./assets/github-banner-light.png" alt="Shannon, AI Pentester for Web Apps and APIs, by Keygraph" width="100%">
|
||||
</picture>
|
||||
|
||||
<a href="https://trendshift.io/repositories/15604" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15604" alt="KeygraphHQ%2Fshannon | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
@@ -57,10 +57,16 @@ Thanks to tools like Claude Code and Cursor, your team ships code non-stop. But
|
||||
|
||||
Shannon closes that gap by providing on-demand, automated penetration testing that can run against every build or release.
|
||||
|
||||
### Why "Shannon"?
|
||||
|
||||
It's named after Claude Shannon, the father of information theory. At its core, pentesting is an information problem: every probe reduces uncertainty about a system's state. The best tools maximize the signal gained from every request, turning those bits of knowledge into an exploit path.
|
||||
|
||||
Also, we wanted you to be able to say, "Hey Claude, run Shannon" to find all the security flaws in your vibe-coded app.
|
||||
|
||||
## Shannon in Action
|
||||
|
||||
<p align="center">
|
||||
<img src="assets/shannon-action.gif" alt="Shannon running an autonomous pentest" width="100%">
|
||||
<img src="assets/Shannon3GIF.gif" alt="Shannon running an autonomous pentest" width="100%">
|
||||
</p>
|
||||
|
||||
Sample penetration test reports from intentionally vulnerable applications, produced by Shannon Open Source:
|
||||
@@ -101,6 +107,7 @@ For source builds, authenticated scans, provider-specific setup, and platform no
|
||||
> **Prefer to use a subscription instead of API credits?**
|
||||
>
|
||||
> - **OpenAI Codex:** The latest version of Shannon supports ChatGPT Plus and Pro subscriptions. Follow the [OpenAI Codex subscription setup guide](docs/ai-providers.md#openai-codex-chatgpt-pluspro-subscription) to get started.
|
||||
> - **xAI (Grok):** The latest version of Shannon supports xAI subscriptions. Follow the [xAI subscription setup guide](docs/ai-providers.md#xai-grok-subscription) to get started.
|
||||
> - **Claude Code:** The latest version of Shannon does not support Claude Code subscriptions. Follow the [Claude Code subscription setup guide](docs/ai-providers.md#claude-code-subscription) to use version `1.9.0`, which is the final release built on the Claude Agent SDK.
|
||||
|
||||
## Key Capabilities
|
||||
|
||||
+49
-11
@@ -1,22 +1,60 @@
|
||||
<div align="center">
|
||||
|
||||
<img src="https://raw.githubusercontent.com/KeygraphHQ/shannon/main/assets/github-banner.png" alt="Shannon — AI Pentester for Web Applications and APIs" width="100%">
|
||||
<img src="https://raw.githubusercontent.com/KeygraphHQ/shannon/main/assets/github-banner-light.png" alt="Shannon, AI Pentester for Web Apps and APIs, by Keygraph" width="100%">
|
||||
|
||||
# Shannon — AI Pentester by Keygraph
|
||||
### Shannon is an autonomous, AI pentester for web applications and APIs.
|
||||
|
||||
Shannon is an autonomous, white-box AI pentester for web applications and APIs. <br />
|
||||
It analyzes your source code, identifies attack vectors, and executes real exploits to prove vulnerabilities before they reach production.
|
||||
It analyzes your source code, identifies attack paths, and executes real exploits to prove vulnerabilities before they reach production.
|
||||
|
||||
**This package is Shannon Open Source: the full agent, run locally from your command line.**
|
||||
|
||||
---
|
||||
|
||||
<a href="https://github.com/KeygraphHQ/shannon/discussions/categories/announcements"><img src="https://raw.githubusercontent.com/KeygraphHQ/shannon/main/assets/announcements.png" height="40" alt="Announcements"></a>
|
||||
<a href="https://discord.gg/9ZqQPuhJB7"><img src="https://raw.githubusercontent.com/KeygraphHQ/shannon/main/assets/discord.png" height="40" alt="Join Discord"></a>
|
||||
<a href="https://keygraph.io/"><img src="https://raw.githubusercontent.com/KeygraphHQ/shannon/main/assets/Keygraph_Button.png" height="40" alt="Visit Keygraph.io"></a>
|
||||
<a href="https://www.linkedin.com/company/keygraph/"><img src="https://raw.githubusercontent.com/KeygraphHQ/shannon/main/assets/linkedin.png" height="40" alt="Follow Us on Linkedin"></a>
|
||||
<a href="https://discord.gg/9ZqQPuhJB7"><img src="https://raw.githubusercontent.com/KeygraphHQ/shannon/main/assets/discord_button_light.png" height="40" alt="Join Discord"></a> <a href="https://keygraph.io/"><img src="https://raw.githubusercontent.com/KeygraphHQ/shannon/main/assets/keygraph_button_light.png" height="40" alt="Visit Keygraph.io"></a>
|
||||
|
||||
---
|
||||
|
||||
**Full README and usage guide**
|
||||
[https://github.com/KeygraphHQ/shannon#readme](https://github.com/KeygraphHQ/shannon#readme)
|
||||
|
||||
</div>
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **Docker**: required for the worker container.
|
||||
- **Node.js 18+**: required for the recommended `npx` workflow.
|
||||
- **AI provider credentials**: Shannon runs on Anthropic, OpenAI, xAI, AWS Bedrock, any other provider in the harness catalogue, and any endpoint that speaks the Anthropic Messages API or the OpenAI Chat Completions or Responses API through a custom base URL. You bring your own key, and Keygraph never proxies your model traffic. Shannon is provider-agnostic.
|
||||
- **Cyber safeguards cleared with your provider**: Anthropic and OpenAI apply real-time safeguards to cyber-security workloads, which can interrupt a scan mid-run. Complete their guidance for legitimate security testers before your first run.
|
||||
|
||||
### Run Shannon
|
||||
|
||||
> **Warning:** Shannon actively executes exploits. Run it only against applications and environments you own or have explicit written authorization to test. Do not run Shannon against production systems.
|
||||
|
||||
```bash
|
||||
# Configure credentials with the interactive wizard.
|
||||
npx @keygraph/shannon setup
|
||||
|
||||
# Run a pentest against a source-available target.
|
||||
npx @keygraph/shannon start -u https://your-app.com -r /path/to/your-repo
|
||||
```
|
||||
|
||||
Shannon pulls the worker image from Docker Hub, starts the required local infrastructure, mounts the target repository read-only inside an ephemeral worker container, and writes results to a local workspace.
|
||||
|
||||
## Editions
|
||||
|
||||
Shannon ships in two ways. **Shannon Open Source** is this package: the standalone pentester you run yourself, on demand, and complete in that lane. The **Keygraph platform** is the commercial product that runs an enhanced build of Shannon continuously and closes the full AppSec lifecycle around it - code analysis, finding management, automated remediation, verification, and enterprise deployment.
|
||||
|
||||
## Documentation
|
||||
|
||||
**Full README, guides, and usage documentation:** [github.com/KeygraphHQ/shannon](https://github.com/KeygraphHQ/shannon#readme)
|
||||
|
||||
## License
|
||||
|
||||
Shannon Open Source is licensed under the [GNU Affero General Public License v3.0](https://github.com/KeygraphHQ/shannon/blob/main/LICENSE).
|
||||
|
||||
Commercial and enterprise licensing is available for organizations that need different license terms, commercial support, private redistribution, managed-service use, or broader deployment options, including the Keygraph platform.
|
||||
|
||||
For commercial licensing, contact [shannon@keygraph.io](mailto:shannon@keygraph.io).
|
||||
|
||||
<p align="center">
|
||||
<b>Built by <a href="https://keygraph.io">Keygraph</a></b>
|
||||
</p>
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@keygraph/shannon",
|
||||
"version": "0.0.0",
|
||||
"description": "Shannon - Autonomous white-box AI pentester for web applications and APIs by Keygraph",
|
||||
"description": "Shannon is an autonomous white-box AI pentester for web applications and APIs, by Keygraph.",
|
||||
"type": "module",
|
||||
"main": "dist/index.mjs",
|
||||
"bin": {
|
||||
@@ -35,8 +35,12 @@
|
||||
"appsec",
|
||||
"keygraph"
|
||||
],
|
||||
"author": "",
|
||||
"author": "Keygraph, Inc.",
|
||||
"license": "AGPL-3.0-only",
|
||||
"bugs": {
|
||||
"url": "https://github.com/KeygraphHQ/shannon/issues"
|
||||
},
|
||||
"homepage": "https://github.com/KeygraphHQ/shannon#readme",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/KeygraphHQ/shannon.git",
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* Section chrome — the sunset-ramp hierarchy beneath the splash wordmark.
|
||||
* (Distinct from ui.ts, which holds the spinner/step helpers.)
|
||||
*
|
||||
* Three treatments, one job each:
|
||||
* rule() static blocks that print once — `start`, `status`, a log's header
|
||||
* gutter() streaming output, where a section scrolls off the top of the screen
|
||||
* panel() the single summary block at the end of a run
|
||||
*
|
||||
* Presentation only. None of this is ever written to workflow.log — the file on disk
|
||||
* stays plain text so `tail`, `grep`, and the completion regex in commands/logs.ts keep
|
||||
* working against it.
|
||||
*/
|
||||
|
||||
import { supportsColor } from './tty.js';
|
||||
|
||||
/**
|
||||
* Sunset ramp, yellow at the top row down to burnt orange at the base.
|
||||
* The wordmark paints row i with stop i and edges it with stop i + 1; section chrome
|
||||
* draws from the same seven stops so the hierarchy reads as one family.
|
||||
* `xterm` is the 256-color approximation for terminals without 24-bit color.
|
||||
*/
|
||||
export const SUNSET: ReadonlyArray<{ rgb: readonly [number, number, number]; xterm: number }> = [
|
||||
{ rgb: [247, 203, 45], xterm: 220 },
|
||||
{ rgb: [246, 182, 38], xterm: 220 },
|
||||
{ rgb: [245, 160, 32], xterm: 214 },
|
||||
{ rgb: [242, 141, 28], xterm: 214 },
|
||||
{ rgb: [238, 121, 24], xterm: 208 },
|
||||
{ rgb: [231, 100, 21], xterm: 208 },
|
||||
{ rgb: [222, 82, 19], xterm: 202 },
|
||||
];
|
||||
|
||||
/** Half-block bar for streaming sections — the wordmark's █ at one eighth the weight. */
|
||||
const BAR = '▌';
|
||||
|
||||
/** Columns reserved to the left of every section, matching the existing output grid. */
|
||||
const INDENT = 2;
|
||||
|
||||
/** Rules stop here even in a wide terminal; a rule spanning 200 columns reads as a divider, not a header. */
|
||||
const MAX_RULE = 64;
|
||||
|
||||
export interface Palette {
|
||||
color: boolean;
|
||||
RESET: string;
|
||||
WHITE: string;
|
||||
GRAY: string;
|
||||
DIM: string;
|
||||
RED: string;
|
||||
/** The seven sunset stops, ready to emit. Empty strings when color is off. */
|
||||
ramp: string[];
|
||||
/** Ramp stop 0 — the solid yellow used for every static rule. */
|
||||
YELLOW: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the escape set for the current terminal, degrading 24-bit → 256-color → bare text.
|
||||
* Resolved per call rather than at import so NO_COLOR/FORCE_COLOR are honored whenever they land.
|
||||
*/
|
||||
export function palette(): Palette {
|
||||
const color = supportsColor();
|
||||
const truecolor = color && /truecolor|24bit/i.test(process.env.COLORTERM ?? '');
|
||||
|
||||
const ramp = SUNSET.map(({ rgb: [r, g, b], xterm }) => {
|
||||
if (!color) return '';
|
||||
return truecolor ? `\x1b[38;2;${r};${g};${b}m` : `\x1b[38;5;${xterm}m`;
|
||||
});
|
||||
|
||||
return {
|
||||
color,
|
||||
RESET: color ? '\x1b[0m' : '',
|
||||
WHITE: color ? '\x1b[1;97m' : '',
|
||||
GRAY: color ? '\x1b[0;37m' : '',
|
||||
DIM: color ? '\x1b[90m' : '',
|
||||
RED: color ? '\x1b[0;31m' : '',
|
||||
ramp,
|
||||
YELLOW: ramp[0] ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
/** Usable width, leaving the indent and a column of breathing room at the right edge. */
|
||||
function columns(): number {
|
||||
return process.stdout.columns && process.stdout.columns > 0 ? process.stdout.columns : 80;
|
||||
}
|
||||
|
||||
/** Printed width of a string, ignoring any escapes already embedded in it. */
|
||||
export function visibleWidth(text: string): number {
|
||||
// biome-ignore lint/suspicious/noControlCharactersInRegex: matching SGR escapes is the point
|
||||
return text.replace(/\x1b\[[0-9;]*m/g, '').length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Option A — a static section header: the existing label, then a solid yellow rule.
|
||||
* The label keeps whatever case and punctuation it already had; only the rule is added.
|
||||
* Degrades to an undecorated label when the terminal is too narrow to carry one.
|
||||
*/
|
||||
export function rule(label: string, indent = INDENT): string {
|
||||
const { WHITE, YELLOW, RESET } = palette();
|
||||
const pad = ' '.repeat(indent);
|
||||
const width = Math.min(MAX_RULE, columns() - indent - 1);
|
||||
const dashes = width - visibleWidth(label) - 1;
|
||||
|
||||
if (dashes < 2) return `${pad}${WHITE}${label}${RESET}`;
|
||||
return `${pad}${WHITE}${label}${RESET} ${YELLOW}${'─'.repeat(dashes)}${RESET}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Grey the label half of an aligned `Label: value` line, leaving the value at default
|
||||
* weight. Purely additive: the string's own characters are never rewritten, so alignment
|
||||
* that was already correct stays correct.
|
||||
*/
|
||||
export function field(line: string): string {
|
||||
const { GRAY, RESET } = palette();
|
||||
const match = /^(\s*)([A-Za-z][A-Za-z ]*:)(\s*)(.*)$/.exec(line);
|
||||
if (!match) return line;
|
||||
return `${match[1]}${GRAY}${match[2]}${RESET}${match[3]}${match[4]}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Option C — one line of a streaming section, carrying the section's bar in the gutter.
|
||||
* `stop` indexes the sunset ramp and wraps, so consecutive sections stay distinguishable
|
||||
* however many a run produces.
|
||||
*/
|
||||
export function gutter(text: string, stop: number, indent = INDENT): string {
|
||||
const { ramp, RESET } = palette();
|
||||
const color = ramp[((stop % ramp.length) + ramp.length) % ramp.length] ?? '';
|
||||
const pad = ' '.repeat(indent);
|
||||
// Trailing space is dropped on empty lines so sections don't emit trailing whitespace.
|
||||
return text ? `${pad}${color}${BAR}${RESET} ${text}` : `${pad}${color}${BAR}${RESET}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Option E — the framed summary block, used once at the end of a run.
|
||||
* Falls back to a rule plus indented lines when the terminal is too narrow to hold the
|
||||
* frame, since a box that wraps is worse than no box at all.
|
||||
*/
|
||||
export function panel(title: string, body: string[], indent = INDENT): string[] {
|
||||
const { WHITE, YELLOW, RESET } = palette();
|
||||
const pad = ' '.repeat(indent);
|
||||
const titleWidth = visibleWidth(title);
|
||||
const widest = body.reduce((max, line) => Math.max(max, visibleWidth(line)), 0);
|
||||
|
||||
const available = columns() - indent - 6;
|
||||
const inner = Math.max(titleWidth + 1, widest);
|
||||
|
||||
if (available < inner || available < titleWidth + 3) {
|
||||
return [rule(title, indent), '', ...body.map((line) => `${pad} ${line}`)];
|
||||
}
|
||||
|
||||
const frame = (s: string): string => `${YELLOW}${s}${RESET}`;
|
||||
const top = `${pad}${frame('╭─')} ${WHITE}${title}${RESET} ${frame(`${'─'.repeat(inner + 1 - titleWidth)}╮`)}`;
|
||||
const bottom = `${pad}${frame(`╰${'─'.repeat(inner + 4)}╯`)}`;
|
||||
|
||||
const rows = body.map((line) => {
|
||||
const fill = ' '.repeat(inner - visibleWidth(line));
|
||||
return `${pad}${frame('│')} ${line}${fill} ${frame('│')}`;
|
||||
});
|
||||
|
||||
return [top, ...rows, bottom];
|
||||
}
|
||||
@@ -11,8 +11,10 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { setTimeout as sleep } from 'node:timers/promises';
|
||||
import { watch } from 'chokidar';
|
||||
import { field } from '../chrome.js';
|
||||
import { fail } from '../errors.js';
|
||||
import { getWorkspacesDir } from '../home.js';
|
||||
import { LogRenderer } from '../log-render.js';
|
||||
import { resolveRunFile } from '../paths.js';
|
||||
import { resolveWorkflowId } from '../session.js';
|
||||
import { waitForWorkflowClose } from '../temporal-client.js';
|
||||
@@ -90,6 +92,9 @@ export function tailUntilComplete(logFile: string, opts: TailOptions = {}): Prom
|
||||
let position = 0;
|
||||
let done = false;
|
||||
let sawFailure = false;
|
||||
// Decorates the streamed log for the terminal; a pass-through when colour is off, so
|
||||
// piped/redirected output stays byte-identical and the failure check still sees raw text.
|
||||
const renderer = new LogRenderer();
|
||||
const controller = new AbortController();
|
||||
let watcher: ReturnType<typeof watch> | undefined;
|
||||
|
||||
@@ -99,7 +104,7 @@ export function tailUntilComplete(logFile: string, opts: TailOptions = {}): Prom
|
||||
const { size } = fs.statSync(logFile);
|
||||
if (size <= position) return;
|
||||
const data = readRange(logFile, position, size);
|
||||
process.stdout.write(data);
|
||||
process.stdout.write(renderer.write(data));
|
||||
position = size;
|
||||
if (!sawFailure && FAILURE_MARKER.test(data)) {
|
||||
sawFailure = true;
|
||||
@@ -112,6 +117,7 @@ export function tailUntilComplete(logFile: string, opts: TailOptions = {}): Prom
|
||||
function finish(): void {
|
||||
if (done) return;
|
||||
done = true;
|
||||
process.stdout.write(renderer.end());
|
||||
controller.abort();
|
||||
if (watcher) {
|
||||
watcher.close().finally(() => resolve({ sawFailure }));
|
||||
@@ -165,7 +171,7 @@ export function tailUntilComplete(logFile: string, opts: TailOptions = {}): Prom
|
||||
export function logs(workspaceId: string): void {
|
||||
const logFile = resolveLogFile(workspaceId);
|
||||
const workflowId = resolveWorkflowId(workspaceId);
|
||||
console.error(stdoutIsTerminal() ? `Tailing scan log: ${logFile}` : 'Tailing scan log');
|
||||
console.error(stdoutIsTerminal() ? field(`Tailing scan log: ${logFile}`) : 'Tailing scan log');
|
||||
|
||||
let unreachable = false;
|
||||
tailUntilComplete(logFile, {
|
||||
|
||||
@@ -10,6 +10,7 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { setTimeout as sleep } from 'node:timers/promises';
|
||||
import * as p from '@clack/prompts';
|
||||
import { field, rule } from '../chrome.js';
|
||||
import { ensureDocker, ensureImage, ensureInfra, randomSuffix, spawnWorker } from '../docker.js';
|
||||
import { buildEnvFlags, loadEnv, resolveHostPiAuthPath, shouldUsePiAuth, validateCredentials } from '../env.js';
|
||||
import { fail } from '../errors.js';
|
||||
@@ -26,7 +27,7 @@ import {
|
||||
} from '../paths.js';
|
||||
import { indentFailureSegments } from '../scan/failure.js';
|
||||
import { resolveWorkflowId } from '../session.js';
|
||||
import { displaySplash } from '../splash.js';
|
||||
import { displayPlainBanner, displaySplash } from '../splash.js';
|
||||
import { getTerminalOutcome } from '../temporal-client.js';
|
||||
import { stdoutIsTerminal } from '../tty.js';
|
||||
import { tailUntilComplete } from './logs.js';
|
||||
@@ -81,10 +82,12 @@ export async function start(args: StartArgs): Promise<void> {
|
||||
const repo = resolveRepo(args.repo);
|
||||
const config = args.config ? resolveConfig(args.config) : undefined;
|
||||
|
||||
// Inputs are valid — show the splash before the Docker/Temporal setup work.
|
||||
// Skip it off a real terminal (e.g. CI) so piped/logged output stays clean.
|
||||
// Inputs are valid — identify the run before the Docker/Temporal setup work.
|
||||
const bannerVersion = isLocal() ? undefined : args.version;
|
||||
if (stdoutIsTerminal()) {
|
||||
displaySplash(isLocal() ? undefined : args.version);
|
||||
displaySplash(bannerVersion);
|
||||
} else {
|
||||
displayPlainBanner(bannerVersion);
|
||||
}
|
||||
|
||||
// 4. Ensure workspaces dir is writable by container user (UID 1001)
|
||||
@@ -296,9 +299,9 @@ async function followScan(workspace: string, workspacesDir: string): Promise<nev
|
||||
|
||||
function printPreservedContainerHint(containerName: string): void {
|
||||
console.log('');
|
||||
console.log(` Worker container preserved: ${containerName}`);
|
||||
console.log(` Inspect logs: docker logs ${containerName}`);
|
||||
console.log(` Remove: docker rm ${containerName}`);
|
||||
console.log(field(` Worker container preserved: ${containerName}`));
|
||||
console.log(field(` Inspect logs: docker logs ${containerName}`));
|
||||
console.log(field(` Remove: docker rm ${containerName}`));
|
||||
console.log('');
|
||||
}
|
||||
|
||||
@@ -310,19 +313,19 @@ function printInfo(args: StartArgs, workspace: string, repoPath: string, workspa
|
||||
console.log('');
|
||||
}
|
||||
|
||||
console.log(` Target: ${args.url}`);
|
||||
console.log(` Repository: ${interactive ? repoPath : path.basename(repoPath)}`);
|
||||
console.log(` Workspace: ${workspace}`);
|
||||
console.log(field(` Target: ${args.url}`));
|
||||
console.log(field(` Repository: ${interactive ? repoPath : path.basename(repoPath)}`));
|
||||
console.log(field(` Workspace: ${workspace}`));
|
||||
if (args.config) {
|
||||
console.log(` Config: ${interactive ? path.resolve(args.config) : path.basename(args.config)}`);
|
||||
console.log(field(` Config: ${interactive ? path.resolve(args.config) : path.basename(args.config)}`));
|
||||
}
|
||||
if (args.pipelineTesting) {
|
||||
console.log(' Mode: Pipeline Testing');
|
||||
console.log(field(' Mode: Pipeline Testing'));
|
||||
}
|
||||
|
||||
const spec = resolveModelSpec();
|
||||
if (typeof spec !== 'string') {
|
||||
console.log(` Model: ${spec.providerId}:${spec.modelId}`);
|
||||
console.log(field(` Model: ${spec.providerId}:${spec.modelId}`));
|
||||
}
|
||||
|
||||
if (!interactive) {
|
||||
@@ -336,13 +339,13 @@ function printInfo(args: StartArgs, workspace: string, repoPath: string, workspa
|
||||
if (!args.follow) {
|
||||
const prefix = commandPrefix();
|
||||
console.log('');
|
||||
console.log(' Watch scan progress:');
|
||||
console.log(` Live logs: ${prefix} logs ${workspace}`);
|
||||
console.log(` Progress: ${prefix} status ${workspace}`);
|
||||
console.log(rule('Watch scan progress:'));
|
||||
console.log(field(` Live logs: ${prefix} logs ${workspace}`));
|
||||
console.log(field(` Progress: ${prefix} status ${workspace}`));
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log(' Report (when the scan finishes):');
|
||||
console.log(rule('Report (when the scan finishes):'));
|
||||
console.log(` ${reportPath}`);
|
||||
console.log('');
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Shannon CLI — AI Penetration Testing Framework
|
||||
* Shannon CLI — AI Pentester for Web Apps and APIs
|
||||
*
|
||||
* Unified CLI supporting two modes:
|
||||
* Local mode: Run from cloned repo — builds locally, mounts prompts, uses ./workspaces/
|
||||
@@ -79,7 +79,7 @@ function showHelp(withSplash: boolean): void {
|
||||
const mode = getMode();
|
||||
const prefix = commandPrefix();
|
||||
|
||||
const header = withSplash ? '' : '\nShannon - AI Penetration Testing Framework\n';
|
||||
const header = withSplash ? '' : '\nShannon — AI Pentester by Keygraph\n';
|
||||
|
||||
console.log(`${header}
|
||||
Usage:
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* Decorates a tailed workflow.log for the terminal.
|
||||
*
|
||||
* The worker writes workflow.log as plain text and the CLI reads it back, so all of the
|
||||
* chrome lives here on the read side. Nothing in this file changes what the log *says* —
|
||||
* it adds the section treatments the plain file has no way to carry:
|
||||
*
|
||||
* the log header and RESUMED banner -> rule() (their ==== bars become the rule)
|
||||
* everything between phases -> gutter() (one bar per phase, walking the ramp)
|
||||
* the closing Scan COMPLETED block -> panel() (its ==== bars become the frame)
|
||||
*
|
||||
* When stdout is not a terminal the renderer is a pass-through and emits the file's bytes
|
||||
* unchanged, so redirected logs, pipes, and CI keep grepping the same text they always did.
|
||||
*/
|
||||
|
||||
import { field, gutter, palette, panel, rule } from './chrome.js';
|
||||
|
||||
/** The ==== bars that open and close a block; replaced by our own chrome. */
|
||||
const BLOCK_BAR = /^={10,}\s*$/;
|
||||
|
||||
/** The ──── bar dividing a block's title from its body; replaced by the panel frame. */
|
||||
const INNER_BAR = /^─{10,}\s*$/;
|
||||
|
||||
/** `[2026-08-26 17:04:11] ` — every streamed event line carries one. */
|
||||
const TIMESTAMP = /^(\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\])( .*)$/;
|
||||
|
||||
/** A phase transition opens a new gutter section. */
|
||||
const PHASE_START = /^\[[^\]]*\] \[PHASE\] Starting: /;
|
||||
|
||||
/** Titles of the two banner blocks, which take the static rule treatment. */
|
||||
const BANNER_TITLE = /^(Shannon Pentest - Scan Log|RESUMED)$/;
|
||||
|
||||
/** Title of the final block, which takes the panel treatment. Mirrors logs.ts's completion regex. */
|
||||
const COMPLETION_TITLE = /^Scan (COMPLETED|FAILED)$/;
|
||||
|
||||
/** Dim the timestamp so the message reads first; errors take the semantic red, not a ramp colour. */
|
||||
function colorizeEvent(line: string): string {
|
||||
const { DIM, RED, RESET } = palette();
|
||||
const match = TIMESTAMP.exec(line);
|
||||
if (!match) return line;
|
||||
const rest = match[2] ?? '';
|
||||
const body = rest.includes('[ERROR]') ? `${RED}${rest}${RESET}` : rest;
|
||||
return `${DIM}${match[1]}${RESET}${body}`;
|
||||
}
|
||||
|
||||
type Mode = 'stream' | 'banner' | 'summary';
|
||||
|
||||
export class LogRenderer {
|
||||
/** Bytes past the last newline, held until the rest of the line arrives. */
|
||||
private carry = '';
|
||||
private mode: Mode = 'stream';
|
||||
/** Suppresses a second consecutive blank line; starts true so the stream can't open on one. */
|
||||
private lastBlank = true;
|
||||
/** Current sunset stop for the gutter bar; advanced by each phase transition. */
|
||||
private stop = 0;
|
||||
private summaryTitle = '';
|
||||
private summaryBody: string[] = [];
|
||||
private readonly passthrough: boolean;
|
||||
|
||||
constructor() {
|
||||
this.passthrough = !palette().color;
|
||||
}
|
||||
|
||||
/** Decorate a chunk of newly appended log text. Incomplete trailing lines are held back. */
|
||||
write(chunk: string): string {
|
||||
if (this.passthrough) return chunk;
|
||||
|
||||
const text = this.carry + chunk;
|
||||
const lines = text.split('\n');
|
||||
// The final element is whatever followed the last newline — possibly a partial line.
|
||||
this.carry = lines.pop() ?? '';
|
||||
|
||||
const out: string[] = [];
|
||||
for (const line of lines) {
|
||||
out.push(...this.renderLine(line));
|
||||
}
|
||||
return this.emit(out);
|
||||
}
|
||||
|
||||
/** Join rendered lines, dropping blank runs left behind by the bars we removed. */
|
||||
private emit(lines: string[]): string {
|
||||
const kept: string[] = [];
|
||||
for (const line of lines) {
|
||||
const blank = line === '';
|
||||
if (blank && this.lastBlank) continue;
|
||||
this.lastBlank = blank;
|
||||
kept.push(line);
|
||||
}
|
||||
return kept.length ? `${kept.join('\n')}\n` : '';
|
||||
}
|
||||
|
||||
/** Flush a held partial line and close an unterminated summary block. */
|
||||
end(): string {
|
||||
if (this.passthrough) return '';
|
||||
|
||||
const out: string[] = [];
|
||||
if (this.carry) {
|
||||
out.push(...this.renderLine(this.carry));
|
||||
this.carry = '';
|
||||
}
|
||||
if (this.mode === 'summary') {
|
||||
out.push(...this.closeSummary());
|
||||
}
|
||||
return this.emit(out);
|
||||
}
|
||||
|
||||
private renderLine(raw: string): string[] {
|
||||
// Strip the \r from CRLF logs so it never lands in the middle of a decorated line.
|
||||
const line = raw.endsWith('\r') ? raw.slice(0, -1) : raw;
|
||||
|
||||
// Only a ==== bar closes the summary; its ──── divider is chrome we replace, not a terminator.
|
||||
if (BLOCK_BAR.test(line)) {
|
||||
return this.mode === 'summary' ? this.closeSummary() : [];
|
||||
}
|
||||
if (INNER_BAR.test(line)) return [];
|
||||
|
||||
if (COMPLETION_TITLE.test(line)) {
|
||||
this.mode = 'summary';
|
||||
this.summaryTitle = line;
|
||||
this.summaryBody = [];
|
||||
return [''];
|
||||
}
|
||||
|
||||
if (BANNER_TITLE.test(line)) {
|
||||
this.mode = 'banner';
|
||||
return ['', rule(line)];
|
||||
}
|
||||
|
||||
if (this.mode === 'summary') {
|
||||
this.summaryBody.push(line);
|
||||
return [];
|
||||
}
|
||||
|
||||
if (this.mode === 'banner') {
|
||||
// The banner runs until the first streamed event.
|
||||
if (!TIMESTAMP.test(line)) {
|
||||
return [line.trim() ? ` ${field(line)}` : ''];
|
||||
}
|
||||
this.mode = 'stream';
|
||||
}
|
||||
|
||||
// A blank line separates sections; the bar resumes on the next line of content.
|
||||
if (!line.trim()) return [''];
|
||||
|
||||
if (PHASE_START.test(line)) {
|
||||
// Two stops per phase, not one: the 256-colour tier collapses the seven stops into
|
||||
// four xterm colours, and a single step would give consecutive phases the same bar.
|
||||
// Seven is odd, so a stride of two still visits every stop before repeating.
|
||||
this.stop += 2;
|
||||
}
|
||||
return [gutter(colorizeEvent(line), this.stop)];
|
||||
}
|
||||
|
||||
private closeSummary(): string[] {
|
||||
const title = this.summaryTitle;
|
||||
const body = [...this.summaryBody];
|
||||
while (body.length && !body[body.length - 1]?.trim()) body.pop();
|
||||
while (body.length && !body[0]?.trim()) body.shift();
|
||||
|
||||
this.mode = 'stream';
|
||||
this.summaryTitle = '';
|
||||
this.summaryBody = [];
|
||||
|
||||
const rows = body.map((line) => (line.trim() ? field(line) : ''));
|
||||
return [...panel(title, rows), ''];
|
||||
}
|
||||
}
|
||||
+19
-28
@@ -3,7 +3,7 @@
|
||||
* Color escapes are gated on terminal support; the Unicode art is always kept.
|
||||
*/
|
||||
|
||||
import { supportsColor } from './tty.js';
|
||||
import { palette } from './chrome.js';
|
||||
|
||||
/** SHANNON wordmark. Block glyphs take the row fill; box-drawing strokes take the deeper edge shade. */
|
||||
const SHANNON = [
|
||||
@@ -15,34 +15,8 @@ const SHANNON = [
|
||||
'╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═══╝',
|
||||
];
|
||||
|
||||
/**
|
||||
* Sunset ramp, yellow at the top row down to burnt orange at the base.
|
||||
* Wordmark row i is filled with stop i and edged with stop i + 1, so the
|
||||
* box-drawing strokes read as a shadow one shade deeper than their row.
|
||||
* `xterm` is the 256-color approximation for terminals without 24-bit color.
|
||||
*/
|
||||
const SUNSET: ReadonlyArray<{ rgb: readonly [number, number, number]; xterm: number }> = [
|
||||
{ rgb: [247, 203, 45], xterm: 220 },
|
||||
{ rgb: [246, 182, 38], xterm: 220 },
|
||||
{ rgb: [245, 160, 32], xterm: 214 },
|
||||
{ rgb: [242, 141, 28], xterm: 214 },
|
||||
{ rgb: [238, 121, 24], xterm: 208 },
|
||||
{ rgb: [231, 100, 21], xterm: 208 },
|
||||
{ rgb: [222, 82, 19], xterm: 202 },
|
||||
];
|
||||
|
||||
export function displaySplash(version?: string): void {
|
||||
const color = supportsColor();
|
||||
const truecolor = color && /truecolor|24bit/i.test(process.env.COLORTERM ?? '');
|
||||
const RESET = color ? '\x1b[0m' : '';
|
||||
const WHITE = color ? '\x1b[1;97m' : '';
|
||||
const GRAY = color ? '\x1b[0;37m' : '';
|
||||
const DIM = color ? '\x1b[90m' : '';
|
||||
|
||||
const ramp = SUNSET.map(({ rgb: [r, g, b], xterm }) => {
|
||||
if (!color) return '';
|
||||
return truecolor ? `\x1b[38;2;${r};${g};${b}m` : `\x1b[38;5;${xterm}m`;
|
||||
});
|
||||
const { color, RESET, WHITE, GRAY, DIM, ramp } = palette();
|
||||
|
||||
/** Color one wordmark row, emitting an escape only where the run changes. Spaces stay unpainted. */
|
||||
const paint = (row: string, fill: string, edge: string): string => {
|
||||
@@ -75,3 +49,20 @@ export function displaySplash(version?: string): void {
|
||||
|
||||
console.log(lines.join('\n'));
|
||||
}
|
||||
|
||||
/** Matches the divider width the CI wrappers and the scan renderer already use. */
|
||||
const RULE_WIDTH = 60;
|
||||
|
||||
/**
|
||||
* Plain-text banner for non-terminal output (CI logs, pipes, redirects).
|
||||
* Drops the wordmark but keeps the authorized-use notice, which a reader of
|
||||
* someone else's pipeline log still needs to see.
|
||||
*/
|
||||
export function displayPlainBanner(version?: string): void {
|
||||
const rule = '─'.repeat(RULE_WIDTH);
|
||||
console.log(rule);
|
||||
console.log(version ? ` Shannon v${version}` : ' Shannon');
|
||||
console.log(' AI Pentester for Web Apps and APIs, by Keygraph');
|
||||
console.log(' Authorized security testing only.');
|
||||
console.log(rule);
|
||||
}
|
||||
@@ -19,9 +19,9 @@
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@earendil-works/pi-agent-core": "^0.82.1",
|
||||
"@earendil-works/pi-ai": "^0.82.1",
|
||||
"@earendil-works/pi-coding-agent": "^0.82.1",
|
||||
"@earendil-works/pi-agent-core": "^0.84.2",
|
||||
"@earendil-works/pi-ai": "^0.84.2",
|
||||
"@earendil-works/pi-coding-agent": "^0.84.2",
|
||||
"@gotgenes/pi-permission-system": "^10.9.0",
|
||||
"@temporalio/activity": "^1.11.0",
|
||||
"@temporalio/client": "^1.11.0",
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright (C) 2025 Keygraph, Inc.
|
||||
|
||||
/**
|
||||
* Centralized brand strings for report deliverables.
|
||||
*
|
||||
* Kept in two parts because the two renderers join them differently: the Typst
|
||||
* template splits its `brand` input on a pipe to set the cover's two lines
|
||||
* (`report.typ:212`), while a human-read line takes an em dash.
|
||||
*/
|
||||
|
||||
export const PRODUCT_NAME = 'Shannon';
|
||||
export const PRODUCT_DESCRIPTOR = 'AI Pentester by Keygraph';
|
||||
|
||||
/** Cover wordmark for the Typst template, which parses the pipe. */
|
||||
export const TYPST_BRAND = `${PRODUCT_NAME} | ${PRODUCT_DESCRIPTOR}`;
|
||||
|
||||
/** Attribution line for prose surfaces. */
|
||||
export const BRAND_LOCKUP = `${PRODUCT_NAME} — ${PRODUCT_DESCRIPTOR}`;
|
||||
@@ -22,13 +22,14 @@ import { copyFile, cp, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { promisify } from 'node:util';
|
||||
import { TYPST_BRAND } from '../branding.js';
|
||||
import { adaptReportToTypst } from './report-json-adapter.js';
|
||||
import type { ReportData } from './report-renderer.js';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
const DEFAULT_TESTER = 'Shannon';
|
||||
const DEFAULT_BRAND = 'Shannon | AI Pentester by Keygraph';
|
||||
const DEFAULT_BRAND = TYPST_BRAND;
|
||||
|
||||
const DATA_FILENAME = 'data.json';
|
||||
const TEMPLATE_FILENAME = 'report.typ';
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
* report agent previously wrote by hand. No LLM in the loop.
|
||||
*/
|
||||
|
||||
import { BRAND_LOCKUP } from '../branding.js';
|
||||
import type { AddFindingInput, AdditionalSection, StepItem, StructuredStep } from '../collectors/finding-collector.js';
|
||||
import type { VulnClass } from '../types/config.js';
|
||||
|
||||
@@ -212,6 +213,8 @@ export function renderReport(data: ReportData): string {
|
||||
// 1. Executive Summary
|
||||
sections.push('# Security Assessment Report');
|
||||
sections.push('');
|
||||
sections.push(`*${BRAND_LOCKUP}*`);
|
||||
sections.push('');
|
||||
sections.push('## Executive Summary');
|
||||
sections.push(`- Target: ${report_meta.target}`);
|
||||
sections.push(`- Assessment Date: ${report_meta.assessment_date}`);
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 25 MiB |
+19
-1
@@ -58,7 +58,7 @@ These are the models `npx @keygraph/shannon setup` offers, best-first. They are
|
||||
| --- | --- |
|
||||
| `anthropic` | `claude-sonnet-4-6`, `claude-opus-4-8`, `claude-opus-4-7`, `claude-haiku-4-5-20251001` |
|
||||
| `openai` | `gpt-5.6-sol`, `gpt-5.5`, `gpt-5.4` |
|
||||
| `xai` | `grok-4.5` |
|
||||
| `xai` | `grok-4.6`, `grok-4.5` |
|
||||
| `amazon-bedrock` | `us.anthropic.claude-sonnet-4-6`, `us.anthropic.claude-opus-4-8`, `us.anthropic.claude-opus-4-7` |
|
||||
|
||||
Bedrock IDs are region-prefixed and must be enabled in your account, so the ID that works for you may differ from the one listed here.
|
||||
@@ -164,6 +164,24 @@ Before running a pentest, review the [cyber safeguards requirements](#cyber-safe
|
||||
|
||||
Supported Codex models are `gpt-5.6-sol`, `gpt-5.5`, and `gpt-5.4`.
|
||||
|
||||
## xAI (Grok subscription)
|
||||
|
||||
An xAI subscription can run Shannon. Shannon reuses a login created by Pi.
|
||||
|
||||
1. Install Pi by following the instructions at [pi.dev](https://pi.dev).
|
||||
2. Log in with your subscription using Pi's [subscription authentication guide](https://pi.dev/docs/latest/providers#subscriptions). This creates `~/.pi/agent/auth.json` with an `xai` entry.
|
||||
|
||||
3. Select an xAI model and enable Pi authentication:
|
||||
|
||||
```bash
|
||||
export SHANNON_USE_PI_AUTH=1
|
||||
export SHANNON_AI_MODEL=xai:grok-4.6
|
||||
```
|
||||
|
||||
4. In npx mode, run `npx @keygraph/shannon start ...` from the same shell. In source-build mode, add the two variables to `.env` and run `./shannon start ...`.
|
||||
|
||||
Suggested Grok models are `grok-4.6` and `grok-4.5`.
|
||||
|
||||
## Claude Code subscription
|
||||
|
||||
The latest version of Shannon does not support Claude Code subscriptions. The [`shannon-v1`](https://github.com/KeygraphHQ/shannon/tree/shannon-v1) branch is the final release built on the Claude Agent SDK and supports Claude Code OAuth.
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ This guide covers platform-specific notes and Docker networking behavior.
|
||||
|
||||
## Windows
|
||||
|
||||
Shannon on Windows is supported through WSL2. Native Windows, including Git Bash, is not supported.
|
||||
Shannon on Windows is supported through WSL2, which behaves like Linux for everything below. Native Windows, including Git Bash, is community-supported: contributions are welcome, but Keygraph does not actively develop or test against it.
|
||||
|
||||
### Ensure WSL2
|
||||
|
||||
|
||||
+28
-3
@@ -15,7 +15,7 @@
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./assets/github-banner-dark.png">
|
||||
<source media="(prefers-color-scheme: light)" srcset="./assets/github-banner-light.png">
|
||||
<img src="./assets/github-banner.png" alt="Shannon - AI Pentester by Keygraph" width="100%">
|
||||
<img src="./assets/github-banner-light.png" alt="Shannon, AI Pentester for Web Apps and APIs, by Keygraph" width="100%">
|
||||
</picture>
|
||||
|
||||
<a href="https://trendshift.io/repositories/15604" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15604" alt="KeygraphHQ%2Fshannon | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
@@ -66,6 +66,12 @@ Thanks to tools like Claude Code and Cursor, your team ships code non-stop. But
|
||||
|
||||
Shannon closes that gap by providing on-demand, automated penetration testing that can run against every build or release.
|
||||
|
||||
### Why "Shannon"?
|
||||
|
||||
It's named after Claude Shannon, the father of information theory. At its core, pentesting is an information problem: every probe reduces uncertainty about a system's state. The best tools maximize the signal gained from every request, turning those bits of knowledge into an exploit path.
|
||||
|
||||
Also, we wanted you to be able to say, "Hey Claude, run Shannon" to find all the security flaws in your vibe-coded app.
|
||||
|
||||
## Shannon in Action
|
||||
|
||||
<p align="center">
|
||||
@@ -110,6 +116,7 @@ For source builds, authenticated scans, provider-specific setup, and platform no
|
||||
> **Prefer to use a subscription instead of API credits?**
|
||||
>
|
||||
> - **OpenAI Codex:** The latest version of Shannon supports ChatGPT Plus and Pro subscriptions. Follow the [OpenAI Codex subscription setup guide](docs/ai-providers.md#openai-codex-chatgpt-pluspro-subscription) to get started.
|
||||
> - **xAI (Grok):** The latest version of Shannon supports xAI subscriptions. Follow the [xAI subscription setup guide](docs/ai-providers.md#xai-grok-subscription) to get started.
|
||||
> - **Claude Code:** The latest version of Shannon does not support Claude Code subscriptions. Follow the [Claude Code subscription setup guide](docs/ai-providers.md#claude-code-subscription) to use version `1.9.0`, which is the final release built on the Claude Agent SDK.
|
||||
|
||||
## Key Capabilities
|
||||
@@ -688,7 +695,7 @@ These are the models `npx @keygraph/shannon setup` offers, best-first. They are
|
||||
| --- | --- |
|
||||
| `anthropic` | `claude-sonnet-4-6`, `claude-opus-4-8`, `claude-opus-4-7`, `claude-haiku-4-5-20251001` |
|
||||
| `openai` | `gpt-5.6-sol`, `gpt-5.5`, `gpt-5.4` |
|
||||
| `xai` | `grok-4.5` |
|
||||
| `xai` | `grok-4.6`, `grok-4.5` |
|
||||
| `amazon-bedrock` | `us.anthropic.claude-sonnet-4-6`, `us.anthropic.claude-opus-4-8`, `us.anthropic.claude-opus-4-7` |
|
||||
|
||||
Bedrock IDs are region-prefixed and must be enabled in your account, so the ID that works for you may differ from the one listed here.
|
||||
@@ -794,6 +801,24 @@ Before running a pentest, review the [cyber safeguards requirements](#cyber-safe
|
||||
|
||||
Supported Codex models are `gpt-5.6-sol`, `gpt-5.5`, and `gpt-5.4`.
|
||||
|
||||
## xAI (Grok subscription)
|
||||
|
||||
An xAI subscription can run Shannon. Shannon reuses a login created by Pi.
|
||||
|
||||
1. Install Pi by following the instructions at [pi.dev](https://pi.dev).
|
||||
2. Log in with your subscription using Pi's [subscription authentication guide](https://pi.dev/docs/latest/providers#subscriptions). This creates `~/.pi/agent/auth.json` with an `xai` entry.
|
||||
|
||||
3. Select an xAI model and enable Pi authentication:
|
||||
|
||||
```bash
|
||||
export SHANNON_USE_PI_AUTH=1
|
||||
export SHANNON_AI_MODEL=xai:grok-4.6
|
||||
```
|
||||
|
||||
4. In npx mode, run `npx @keygraph/shannon start ...` from the same shell. In source-build mode, add the two variables to `.env` and run `./shannon start ...`.
|
||||
|
||||
Suggested Grok models are `grok-4.6` and `grok-4.5`.
|
||||
|
||||
## Claude Code subscription
|
||||
|
||||
The latest version of Shannon does not support Claude Code subscriptions. The [`shannon-v1`](https://github.com/KeygraphHQ/shannon/tree/shannon-v1) branch is the final release built on the Claude Agent SDK and supports Claude Code OAuth.
|
||||
@@ -858,7 +883,7 @@ This guide covers platform-specific notes and Docker networking behavior.
|
||||
|
||||
## Windows
|
||||
|
||||
Shannon on Windows is supported through WSL2. Native Windows, including Git Bash, is not supported.
|
||||
Shannon on Windows is supported through WSL2, which behaves like Linux for everything below. Native Windows, including Git Bash, is community-supported: contributions are welcome, but Keygraph does not actively develop or test against it.
|
||||
|
||||
### Ensure WSL2
|
||||
|
||||
|
||||
Generated
+78
-66
@@ -46,17 +46,17 @@ importers:
|
||||
apps/worker:
|
||||
dependencies:
|
||||
'@earendil-works/pi-agent-core':
|
||||
specifier: ^0.82.1
|
||||
version: 0.82.1(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)
|
||||
specifier: ^0.84.2
|
||||
version: 0.84.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)
|
||||
'@earendil-works/pi-ai':
|
||||
specifier: ^0.82.1
|
||||
version: 0.82.1(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)
|
||||
specifier: ^0.84.2
|
||||
version: 0.84.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)
|
||||
'@earendil-works/pi-coding-agent':
|
||||
specifier: ^0.82.1
|
||||
version: 0.82.1(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)
|
||||
specifier: ^0.84.2
|
||||
version: 0.84.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)
|
||||
'@gotgenes/pi-permission-system':
|
||||
specifier: ^10.9.0
|
||||
version: 10.9.0(@earendil-works/pi-coding-agent@0.82.1(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6))(@earendil-works/pi-tui@0.82.1)
|
||||
version: 10.9.0(@earendil-works/pi-coding-agent@0.84.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6))(@earendil-works/pi-tui@0.84.2)
|
||||
'@temporalio/activity':
|
||||
specifier: ^1.11.0
|
||||
version: 1.15.0
|
||||
@@ -292,22 +292,34 @@ packages:
|
||||
'@clack/prompts@1.1.0':
|
||||
resolution: {integrity: sha512-pkqbPGtohJAvm4Dphs2M8xE29ggupihHdy1x84HNojZuMtFsHiUlRvqD24tM2+XmI+61LlfNceM3Wr7U5QES5g==}
|
||||
|
||||
'@earendil-works/pi-agent-core@0.82.1':
|
||||
resolution: {integrity: sha512-Z3kloziJIE2dmrisRckZX8zDca/gIv9/YdFAzeoqpHiLV2wsni6bL4hInNSjVKLbqT+4kqLIkph2JQLKvSepjg==}
|
||||
'@earendil-works/pi-agent-core@0.84.2':
|
||||
resolution: {integrity: sha512-8Pn3wSCxj0cfo5I6jxQYVB/3uuQRmHhAlEclyjqpOuMEdQMIODHizRogv56FLdbU+dTiGnybeHQ2N+sV1/L2YA==}
|
||||
engines: {node: '>=22.19.0'}
|
||||
|
||||
'@earendil-works/pi-ai@0.82.1':
|
||||
resolution: {integrity: sha512-3WFYRhEp3lQB3444EhPMBcM7zSaEUE3eJgHOR7s4081NLqbw/FsWilIKWXSua0Gv3sRr7m9xMidR3pPDE7jI/A==}
|
||||
'@earendil-works/pi-ai@0.84.2':
|
||||
resolution: {integrity: sha512-6MzsrYIYNVlE7SfpbL2yYb67Qo58p/7Q+xWG1RZvoX1P80aRCHSod2/13aFpxkow1lPO2LEh3c495J0Gwmyjig==}
|
||||
engines: {node: '>=22.19.0'}
|
||||
hasBin: true
|
||||
|
||||
'@earendil-works/pi-coding-agent@0.82.1':
|
||||
resolution: {integrity: sha512-zbkAhoIuDPMF3pKuja0ajZabrMWU29FUMV9A/XMXT/XC1yXs5xt6t6t13GogQFsDrDqbFP4DkZQO1w8rWRAzYA==}
|
||||
'@earendil-works/pi-client@0.84.2':
|
||||
resolution: {integrity: sha512-/RFSPhD/bZbpOp1oJj+UneSUFSgZhWxzcSENUY+8+8xhoBrWXMYI2t77XNx4Yf+c8YK2qTHquForhNcelYpXvg==}
|
||||
engines: {node: '>=22.19.0'}
|
||||
|
||||
'@earendil-works/pi-coding-agent@0.84.2':
|
||||
resolution: {integrity: sha512-l4E+B7hgXKWddRo8bC/eSue2aWZjEgJ9xIpf5p0Og+lq8a2TArCwJ0HCoCPCgaBP/tN4zbYH/wOwvx9pJpeLCA==}
|
||||
engines: {node: '>=22.19.0'}
|
||||
hasBin: true
|
||||
|
||||
'@earendil-works/pi-tui@0.82.1':
|
||||
resolution: {integrity: sha512-9yN8hALfKaxZq7n54EMxqhFCWnMi6LHkraMJ/1YjHiATq75XrI6XDMVppn9EDtiK7Fks8hUe1SDXUTrIvwRWfQ==}
|
||||
'@earendil-works/pi-protocol@0.84.2':
|
||||
resolution: {integrity: sha512-jbBh03fkeckWEroHpcZBr4w5/Ibat8WwdXFlXHivYQImrQNFtLpDeL0t1cku4hmK0q3pceIRQHkw4fwbM4YILQ==}
|
||||
engines: {node: '>=22.19.0'}
|
||||
|
||||
'@earendil-works/pi-telemetry@0.84.2':
|
||||
resolution: {integrity: sha512-wg5caea7uIv1BHRBm2Y116RvFG4oSAiP5qk9tA2463PDGIr4K8M1Ceyyg5DOpF/shUUl0gk826yQJAeAcHYB9g==}
|
||||
engines: {node: '>=22.19.0'}
|
||||
|
||||
'@earendil-works/pi-tui@0.84.2':
|
||||
resolution: {integrity: sha512-ds2TLihOnM5sLJB3VpXV6y0uR5efVuHf4MN7yDpsty6hA2DUO/EDVzjp/0od0G2JslzVLMjT8T8zavtxVb+qbg==}
|
||||
engines: {node: '>=22.19.0'}
|
||||
|
||||
'@emnapi/core@1.9.1':
|
||||
@@ -557,14 +569,6 @@ packages:
|
||||
resolution: {integrity: sha512-ABnA53mdfkGZwOFUdZNv2S0CWGO/EIuPj8Vv9xmBFmSYg/qFc7ihO6q5FcQjvoE67kZpWkEc4AhD6B/os04yuA==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
'@mistralai/mistralai@2.2.6':
|
||||
resolution: {integrity: sha512-W8pX7zHxjJvMIpw8JMxeJEleapXX0Q9NPszdNzqkM3MIEoIGPObdodujj+WHteXEvGfaP/AMwlNyRfEzSY6dQQ==}
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': ^1.9.0
|
||||
peerDependenciesMeta:
|
||||
'@opentelemetry/api':
|
||||
optional: true
|
||||
|
||||
'@modelcontextprotocol/sdk@1.29.0':
|
||||
resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -585,10 +589,6 @@ packages:
|
||||
resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
||||
'@opentelemetry/semantic-conventions@1.43.0':
|
||||
resolution: {integrity: sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
'@oxc-project/types@0.122.0':
|
||||
resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==}
|
||||
|
||||
@@ -1376,6 +1376,10 @@ packages:
|
||||
graceful-fs@4.2.11:
|
||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||
|
||||
grok-mermaid@0.2.2:
|
||||
resolution: {integrity: sha512-XcJEP5dDC8liHBh52mlLjU18fNvu1ckFsu0QpIG3+APZ270fsj9wxpiA6cOURmbUEuoMVgjbC2+UYgTdCqqgzA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
has-flag@4.0.0:
|
||||
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1620,9 +1624,8 @@ packages:
|
||||
once@1.4.0:
|
||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||
|
||||
openai@6.26.0:
|
||||
resolution: {integrity: sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==}
|
||||
hasBin: true
|
||||
openai@6.40.0:
|
||||
resolution: {integrity: sha512-MWtTjd/gQt4jpbji61NTgFWJLoY/PdRJ6wG9/ZDRMYNMlBKrCrSlkLI+KgHP1vR1qT6LKSAyAqIxno6lcK9JiA==}
|
||||
peerDependencies:
|
||||
ws: ^8.18.0
|
||||
zod: ^3.25 || ^4.0
|
||||
@@ -2003,6 +2006,9 @@ packages:
|
||||
typebox@1.1.38:
|
||||
resolution: {integrity: sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==}
|
||||
|
||||
typebox@1.3.7:
|
||||
resolution: {integrity: sha512-meKuifc33Pccx0O6PdIzYMq3Og8zvP4TIi/a+Bw3AEMZMxOD0+RHGQvpglEe6Zdy3wZ8nqn/j95h8LUZLk/6Hg==}
|
||||
|
||||
typescript@5.9.3:
|
||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||
engines: {node: '>=14.17'}
|
||||
@@ -2014,8 +2020,8 @@ packages:
|
||||
undici-types@7.18.2:
|
||||
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
|
||||
|
||||
undici@8.5.0:
|
||||
resolution: {integrity: sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==}
|
||||
undici@8.9.0:
|
||||
resolution: {integrity: sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA==}
|
||||
engines: {node: '>=22.19.0'}
|
||||
|
||||
unionfs@4.6.0:
|
||||
@@ -2431,12 +2437,13 @@ snapshots:
|
||||
'@clack/core': 1.1.0
|
||||
sisteransi: 1.0.5
|
||||
|
||||
'@earendil-works/pi-agent-core@0.82.1(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)':
|
||||
'@earendil-works/pi-agent-core@0.84.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)':
|
||||
dependencies:
|
||||
'@earendil-works/pi-ai': 0.82.1(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)
|
||||
'@earendil-works/pi-ai': 0.84.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)
|
||||
'@earendil-works/pi-telemetry': 0.84.2
|
||||
diff: 8.0.4
|
||||
ignore: 7.0.5
|
||||
typebox: 1.1.38
|
||||
typebox: 1.3.7
|
||||
yaml: 2.9.0
|
||||
transitivePeerDependencies:
|
||||
- '@modelcontextprotocol/sdk'
|
||||
@@ -2446,19 +2453,19 @@ snapshots:
|
||||
- ws
|
||||
- zod
|
||||
|
||||
'@earendil-works/pi-ai@0.82.1(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)':
|
||||
'@earendil-works/pi-ai@0.84.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)':
|
||||
dependencies:
|
||||
'@anthropic-ai/sdk': 0.91.1(zod@4.3.6)
|
||||
'@aws-sdk/client-bedrock-runtime': 3.1048.0
|
||||
'@earendil-works/pi-telemetry': 0.84.2
|
||||
'@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))
|
||||
'@mistralai/mistralai': 2.2.6(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@smithy/node-http-handler': 4.7.3
|
||||
http-proxy-agent: 7.0.2
|
||||
https-proxy-agent: 7.0.6
|
||||
openai: 6.26.0(ws@8.21.0)(zod@4.3.6)
|
||||
openai: 6.40.0(ws@8.21.0)(zod@4.3.6)
|
||||
partial-json: 0.1.7
|
||||
typebox: 1.1.38
|
||||
typebox: 1.3.7
|
||||
transitivePeerDependencies:
|
||||
- '@modelcontextprotocol/sdk'
|
||||
- bufferutil
|
||||
@@ -2467,16 +2474,23 @@ snapshots:
|
||||
- ws
|
||||
- zod
|
||||
|
||||
'@earendil-works/pi-coding-agent@0.82.1(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)':
|
||||
'@earendil-works/pi-client@0.84.2':
|
||||
dependencies:
|
||||
'@earendil-works/pi-agent-core': 0.82.1(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)
|
||||
'@earendil-works/pi-ai': 0.82.1(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)
|
||||
'@earendil-works/pi-tui': 0.82.1
|
||||
'@earendil-works/pi-protocol': 0.84.2
|
||||
|
||||
'@earendil-works/pi-coding-agent@0.84.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)':
|
||||
dependencies:
|
||||
'@earendil-works/pi-agent-core': 0.84.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)
|
||||
'@earendil-works/pi-ai': 0.84.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)
|
||||
'@earendil-works/pi-client': 0.84.2
|
||||
'@earendil-works/pi-protocol': 0.84.2
|
||||
'@earendil-works/pi-tui': 0.84.2
|
||||
'@silvia-odwyer/photon-node': 0.3.4
|
||||
chalk: 5.6.2
|
||||
cross-spawn: 7.0.6
|
||||
diff: 8.0.4
|
||||
glob: 13.0.6
|
||||
grok-mermaid: 0.2.2
|
||||
highlight.js: 10.7.3
|
||||
hosted-git-info: 9.0.3
|
||||
ignore: 7.0.5
|
||||
@@ -2484,8 +2498,8 @@ snapshots:
|
||||
minimatch: 10.2.5
|
||||
proper-lockfile: 4.1.2
|
||||
semver: 7.8.0
|
||||
typebox: 1.1.38
|
||||
undici: 8.5.0
|
||||
typebox: 1.3.7
|
||||
undici: 8.9.0
|
||||
yaml: 2.9.0
|
||||
optionalDependencies:
|
||||
'@mariozechner/clipboard': 0.3.9
|
||||
@@ -2497,7 +2511,13 @@ snapshots:
|
||||
- ws
|
||||
- zod
|
||||
|
||||
'@earendil-works/pi-tui@0.82.1':
|
||||
'@earendil-works/pi-protocol@0.84.2':
|
||||
dependencies:
|
||||
typebox: 1.3.7
|
||||
|
||||
'@earendil-works/pi-telemetry@0.84.2': {}
|
||||
|
||||
'@earendil-works/pi-tui@0.84.2':
|
||||
dependencies:
|
||||
get-east-asian-width: 1.6.0
|
||||
marked: 18.0.5
|
||||
@@ -2531,10 +2551,10 @@ snapshots:
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
'@gotgenes/pi-permission-system@10.9.0(@earendil-works/pi-coding-agent@0.82.1(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6))(@earendil-works/pi-tui@0.82.1)':
|
||||
'@gotgenes/pi-permission-system@10.9.0(@earendil-works/pi-coding-agent@0.84.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6))(@earendil-works/pi-tui@0.84.2)':
|
||||
dependencies:
|
||||
'@earendil-works/pi-coding-agent': 0.82.1(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)
|
||||
'@earendil-works/pi-tui': 0.82.1
|
||||
'@earendil-works/pi-coding-agent': 0.84.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)
|
||||
'@earendil-works/pi-tui': 0.84.2
|
||||
tree-sitter-bash: 0.25.1
|
||||
web-tree-sitter: 0.26.9
|
||||
transitivePeerDependencies:
|
||||
@@ -2749,18 +2769,6 @@ snapshots:
|
||||
'@mariozechner/clipboard-win32-x64-msvc': 0.3.9
|
||||
optional: true
|
||||
|
||||
'@mistralai/mistralai@2.2.6(@opentelemetry/api@1.9.0)':
|
||||
dependencies:
|
||||
'@opentelemetry/semantic-conventions': 1.43.0
|
||||
ws: 8.21.0
|
||||
zod: 4.3.6
|
||||
zod-to-json-schema: 3.25.2(zod@4.3.6)
|
||||
optionalDependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
'@modelcontextprotocol/sdk@1.29.0(zod@4.3.6)':
|
||||
dependencies:
|
||||
'@hono/node-server': 1.19.14(hono@4.12.14)
|
||||
@@ -2795,8 +2803,6 @@ snapshots:
|
||||
|
||||
'@opentelemetry/api@1.9.0': {}
|
||||
|
||||
'@opentelemetry/semantic-conventions@1.43.0': {}
|
||||
|
||||
'@oxc-project/types@0.122.0': {}
|
||||
|
||||
'@protobufjs/aspromise@1.1.2': {}
|
||||
@@ -3598,6 +3604,8 @@ snapshots:
|
||||
|
||||
graceful-fs@4.2.11: {}
|
||||
|
||||
grok-mermaid@0.2.2: {}
|
||||
|
||||
has-flag@4.0.0: {}
|
||||
|
||||
has-symbols@1.1.0:
|
||||
@@ -3820,7 +3828,7 @@ snapshots:
|
||||
wrappy: 1.0.2
|
||||
optional: true
|
||||
|
||||
openai@6.26.0(ws@8.21.0)(zod@4.3.6):
|
||||
openai@6.40.0(ws@8.21.0)(zod@4.3.6):
|
||||
optionalDependencies:
|
||||
ws: 8.21.0
|
||||
zod: 4.3.6
|
||||
@@ -4212,6 +4220,8 @@ snapshots:
|
||||
|
||||
typebox@1.1.38: {}
|
||||
|
||||
typebox@1.3.7: {}
|
||||
|
||||
typescript@5.9.3: {}
|
||||
|
||||
unconfig-core@7.5.0:
|
||||
@@ -4221,7 +4231,7 @@ snapshots:
|
||||
|
||||
undici-types@7.18.2: {}
|
||||
|
||||
undici@8.5.0: {}
|
||||
undici@8.9.0: {}
|
||||
|
||||
unionfs@4.6.0:
|
||||
dependencies:
|
||||
@@ -4324,7 +4334,9 @@ snapshots:
|
||||
zod-to-json-schema@3.25.2(zod@4.3.6):
|
||||
dependencies:
|
||||
zod: 4.3.6
|
||||
optional: true
|
||||
|
||||
zod@4.3.6: {}
|
||||
zod@4.3.6:
|
||||
optional: true
|
||||
|
||||
zx@8.8.5: {}
|
||||
Reference in new issue
Block a user