openhacker 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +2 -3
  2. package/bin/openhacker +1 -1
  3. package/package.json +3 -3
  4. package/src/index.d.ts +1 -0
  5. package/src/index.js +305 -1
  6. package/templates/agent/.env.example +0 -7
  7. package/templates/agent/README.md +1 -2
  8. package/templates/agent/agent/agent.ts +1 -5
  9. package/templates/agent/agent/channels/eve.ts +7 -0
  10. package/templates/agent/agent/instructions.md +7 -45
  11. package/templates/agent/app/globals.css +65 -197
  12. package/templates/agent/app/layout.tsx +2 -22
  13. package/templates/agent/app/page.tsx +80 -102
  14. package/templates/agent/package.json +2 -3
  15. package/src/cli.js +0 -153
  16. package/src/index.ts +0 -1
  17. package/templates/agent/agent/lib/auth.ts +0 -23
  18. package/templates/agent/agent/lib/github.ts +0 -74
  19. package/templates/agent/agent/lib/osv.ts +0 -152
  20. package/templates/agent/agent/lib/scan.ts +0 -153
  21. package/templates/agent/agent/lib/store.ts +0 -151
  22. package/templates/agent/agent/lib/types.ts +0 -63
  23. package/templates/agent/agent/schedules/daily_audit.ts +0 -20
  24. package/templates/agent/agent/tools/check_advisories.ts +0 -27
  25. package/templates/agent/agent/tools/list_targets.ts +0 -21
  26. package/templates/agent/agent/tools/read_repo_file.ts +0 -31
  27. package/templates/agent/agent/tools/report_finding.ts +0 -59
  28. package/templates/agent/agent/tools/run_dependency_scan.ts +0 -16
  29. package/templates/agent/app/_components/ui.tsx +0 -29
  30. package/templates/agent/app/actions.ts +0 -120
  31. package/templates/agent/app/api/scan/route.ts +0 -34
  32. package/templates/agent/app/login/page.tsx +0 -40
  33. package/templates/agent/app/settings/page.tsx +0 -92
  34. package/templates/agent/app/targets/[id]/page.tsx +0 -127
  35. package/templates/agent/proxy.ts +0 -21
@@ -8,11 +8,6 @@
8
8
  --accent: #ffffff;
9
9
  --accent-dim: #333333;
10
10
  --crit: #ffffff;
11
- --high: #cfcfcf;
12
- --med: #9a9a9a;
13
- --low: #6f6f6f;
14
- --info: #555555;
15
- --ok: #f5f5f5;
16
11
  }
17
12
 
18
13
  * {
@@ -27,254 +22,127 @@ body {
27
22
  color: var(--text);
28
23
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
29
24
  font-size: 14px;
30
- line-height: 1.5;
31
- }
32
-
33
- a {
34
- color: var(--accent);
35
- text-decoration: none;
36
- }
37
- a:hover {
38
- text-decoration: underline;
39
- }
40
-
41
- .nav {
42
- display: flex;
43
- align-items: center;
44
- gap: 20px;
45
- padding: 14px 24px;
46
- border-bottom: 1px solid var(--border);
47
- background: var(--panel);
48
- }
49
- .nav .brand {
50
- font-weight: 700;
51
- letter-spacing: 0.5px;
52
- color: var(--text);
53
- }
54
- .nav .brand span {
55
- color: var(--accent);
56
- }
57
- .nav .spacer {
58
- flex: 1;
59
- }
60
- .nav a {
61
- color: var(--muted);
62
- }
63
- .nav a:hover {
64
- color: var(--text);
65
- text-decoration: none;
25
+ line-height: 1.6;
66
26
  }
67
27
 
68
28
  .container {
69
- max-width: 960px;
29
+ max-width: 720px;
70
30
  margin: 0 auto;
71
- padding: 28px 24px 80px;
31
+ padding: 64px 24px 96px;
72
32
  }
73
33
 
74
34
  h1 {
75
- font-size: 20px;
35
+ font-size: 22px;
36
+ font-weight: 700;
37
+ letter-spacing: 0.5px;
76
38
  margin: 0 0 4px;
77
39
  }
78
- h2 {
79
- font-size: 15px;
80
- margin: 28px 0 12px;
81
- color: var(--muted);
82
- text-transform: uppercase;
83
- letter-spacing: 1px;
40
+ h1 span {
41
+ color: var(--accent);
84
42
  }
85
43
  .sub {
86
44
  color: var(--muted);
87
- margin: 0 0 24px;
45
+ margin: 0 0 28px;
88
46
  }
89
47
 
90
- .panel {
91
- background: var(--panel);
92
- border: 1px solid var(--border);
93
- border-radius: 10px;
94
- padding: 18px;
95
- margin-bottom: 16px;
96
- }
97
-
98
- .card {
99
- background: var(--panel);
100
- border: 1px solid var(--border);
101
- border-radius: 10px;
102
- padding: 16px 18px;
103
- margin-bottom: 12px;
48
+ .ask {
104
49
  display: flex;
105
- align-items: center;
106
- gap: 16px;
50
+ gap: 8px;
107
51
  }
108
- .card .grow {
52
+ .ask input {
109
53
  flex: 1;
110
- min-width: 0;
111
- }
112
- .card .repo {
113
- font-weight: 600;
114
- }
115
- .card .meta {
116
- color: var(--muted);
117
- font-size: 12px;
118
- margin-top: 2px;
119
- }
120
-
121
- label {
122
- display: block;
123
- font-size: 12px;
124
- color: var(--muted);
125
- margin-bottom: 6px;
126
- }
127
- input[type="text"],
128
- input[type="password"],
129
- select {
130
- width: 100%;
131
54
  background: var(--panel-2);
132
55
  border: 1px solid var(--border);
133
56
  color: var(--text);
134
57
  border-radius: 8px;
135
- padding: 9px 11px;
58
+ padding: 11px 13px;
136
59
  font: inherit;
137
60
  }
138
- input:focus,
139
- select:focus {
61
+ .ask input:focus {
140
62
  outline: none;
141
63
  border-color: var(--accent);
142
64
  }
143
- .row {
144
- display: flex;
145
- gap: 12px;
146
- flex-wrap: wrap;
147
- margin-bottom: 12px;
148
- }
149
- .row > div {
150
- flex: 1;
151
- min-width: 160px;
152
- }
153
- .check {
154
- display: flex;
155
- align-items: center;
156
- gap: 8px;
157
- }
158
- .check label {
159
- margin: 0;
160
- }
161
-
162
- button,
163
- .btn {
65
+ .ask button {
164
66
  background: var(--accent);
165
67
  color: #000000;
166
68
  border: none;
167
69
  border-radius: 8px;
168
- padding: 9px 14px;
70
+ padding: 11px 18px;
169
71
  font: inherit;
170
72
  font-weight: 600;
171
73
  cursor: pointer;
172
74
  }
173
- button:hover {
75
+ .ask button:hover:not(:disabled) {
174
76
  filter: brightness(1.08);
175
77
  }
176
- .btn-ghost {
177
- background: transparent;
178
- color: var(--muted);
179
- border: 1px solid var(--border);
180
- }
181
- .btn-ghost:hover {
182
- color: var(--text);
183
- }
184
- .btn-danger {
185
- background: transparent;
186
- color: var(--crit);
187
- border: 1px solid var(--accent-dim);
78
+ .ask button:disabled {
79
+ opacity: 0.5;
80
+ cursor: not-allowed;
188
81
  }
189
82
 
190
- .badge {
191
- display: inline-block;
192
- padding: 1px 8px;
193
- border-radius: 20px;
194
- font-size: 11px;
195
- font-weight: 700;
196
- text-transform: uppercase;
197
- letter-spacing: 0.5px;
83
+ .reply {
84
+ margin-top: 28px;
85
+ background: var(--panel);
198
86
  border: 1px solid var(--border);
199
- background: transparent;
200
- color: var(--text);
201
- }
202
- .sev-critical {
203
- background: #ffffff;
204
- color: #000000;
205
- border-color: #ffffff;
206
- }
207
- .sev-high {
208
- color: #ffffff;
209
- border-color: #cfcfcf;
210
- background: rgba(255, 255, 255, 0.1);
87
+ border-radius: 10px;
88
+ padding: 18px 20px;
211
89
  }
212
- .sev-medium {
213
- color: var(--high);
214
- border-color: #5a5a5a;
90
+ .reply .text {
91
+ white-space: pre-wrap;
92
+ margin: 0 0 12px;
215
93
  }
216
- .sev-low {
217
- color: var(--med);
218
- border-color: #3a3a3a;
94
+ .reply .text:last-child {
95
+ margin-bottom: 0;
219
96
  }
220
- .sev-info {
221
- color: var(--info);
222
- border-color: #2a2a2a;
97
+ .reply .reasoning {
98
+ white-space: pre-wrap;
99
+ color: var(--muted);
100
+ font-size: 13px;
101
+ margin: 0 0 12px;
102
+ padding-left: 12px;
103
+ border-left: 2px solid var(--border);
223
104
  }
224
105
 
225
- .counts {
106
+ .tool {
226
107
  display: flex;
227
- gap: 6px;
228
- }
229
-
230
- table {
231
- width: 100%;
232
- border-collapse: collapse;
108
+ align-items: center;
109
+ gap: 10px;
110
+ font-size: 12.5px;
111
+ color: var(--muted);
112
+ padding: 4px 0;
233
113
  }
234
- th,
235
- td {
236
- text-align: left;
237
- padding: 10px 12px;
238
- border-bottom: 1px solid var(--border);
239
- vertical-align: top;
240
- font-size: 13px;
114
+ .tool-name {
115
+ color: var(--text);
241
116
  }
242
- th {
243
- color: var(--muted);
117
+ .tool-state {
244
118
  font-size: 11px;
245
119
  text-transform: uppercase;
246
120
  letter-spacing: 0.5px;
121
+ border: 1px solid var(--border);
122
+ border-radius: 20px;
123
+ padding: 0 8px;
124
+ }
125
+
126
+ .cursor {
127
+ display: inline-block;
128
+ width: 8px;
129
+ height: 1em;
130
+ background: var(--accent);
131
+ vertical-align: text-bottom;
132
+ animation: blink 1s steps(2) infinite;
133
+ }
134
+ @keyframes blink {
135
+ 50% {
136
+ opacity: 0;
137
+ }
247
138
  }
248
139
 
249
140
  .banner {
141
+ margin-top: 20px;
250
142
  border: 1px solid var(--border);
251
143
  background: rgba(255, 255, 255, 0.04);
252
- color: var(--text);
144
+ color: var(--crit);
253
145
  border-radius: 8px;
254
146
  padding: 10px 14px;
255
- margin-bottom: 18px;
256
147
  font-size: 13px;
257
148
  }
258
- .empty {
259
- color: var(--muted);
260
- padding: 24px;
261
- text-align: center;
262
- border: 1px dashed var(--border);
263
- border-radius: 10px;
264
- }
265
- .inline {
266
- display: inline;
267
- }
268
- .actions {
269
- display: flex;
270
- gap: 8px;
271
- align-items: center;
272
- }
273
- .mono-sm {
274
- font-size: 12px;
275
- color: var(--muted);
276
- }
277
- .login-wrap {
278
- max-width: 360px;
279
- margin: 12vh auto 0;
280
- }
@@ -1,35 +1,15 @@
1
1
  import type { Metadata } from "next";
2
- import Link from "next/link";
3
- import { authEnabled } from "@/agent/lib/auth";
4
- import { logout } from "./actions";
5
2
  import "./globals.css";
6
3
 
7
4
  export const metadata: Metadata = {
8
5
  title: "OpenHacker",
9
- description: "Autonomous application security agent",
6
+ description: "Analyze a GitHub repo for vulnerabilities",
10
7
  };
11
8
 
12
9
  export default function RootLayout({ children }: { children: React.ReactNode }) {
13
10
  return (
14
11
  <html lang="en">
15
- <body>
16
- <nav className="nav">
17
- <Link href="/" className="brand">
18
- open<span>hacker</span>
19
- </Link>
20
- <div className="spacer" />
21
- <Link href="/">Dashboard</Link>
22
- <Link href="/settings">Settings</Link>
23
- {authEnabled() ? (
24
- <form action={logout} className="inline">
25
- <button className="btn-ghost" type="submit">
26
- Sign out
27
- </button>
28
- </form>
29
- ) : null}
30
- </nav>
31
- {children}
32
- </body>
12
+ <body>{children}</body>
33
13
  </html>
34
14
  );
35
15
  }
@@ -1,114 +1,92 @@
1
- import Link from "next/link";
2
- import { getStore, isPersistent } from "@/agent/lib/store";
3
- import { SeverityCounts } from "./_components/ui";
4
- import { addTarget, deleteTarget, scanTarget } from "./actions";
1
+ "use client";
5
2
 
6
- export const dynamic = "force-dynamic";
3
+ import { useState } from "react";
4
+ import { useEveAgent } from "eve/react";
7
5
 
8
- export default async function Dashboard({
9
- searchParams,
10
- }: {
11
- searchParams: Promise<{ error?: string }>;
12
- }) {
13
- const { error } = await searchParams;
14
- const store = getStore();
15
- const targets = await store.listTargets();
16
- const findingsByTarget = new Map(
17
- await Promise.all(
18
- targets.map(async (t) => [t.id, await store.listFindings(t.id)] as const),
19
- ),
20
- );
6
+ export default function Home() {
7
+ const [repo, setRepo] = useState("");
8
+ const agent = useEveAgent();
9
+
10
+ const busy = agent.status === "submitted" || agent.status === "streaming";
11
+
12
+ function onSubmit(e: React.FormEvent) {
13
+ e.preventDefault();
14
+ const value = repo.trim();
15
+ if (!value || busy) return;
16
+ agent.reset();
17
+ agent.send({
18
+ message: `Analyze the GitHub repository "${value}" for security vulnerabilities. Walk through what you check and report what you find.`,
19
+ });
20
+ }
21
+
22
+ const reply = [...agent.data.messages]
23
+ .reverse()
24
+ .find((m) => m.role === "assistant");
21
25
 
22
26
  return (
23
27
  <main className="container">
24
- <h1>Targets</h1>
25
- <p className="sub">Repositories OpenHacker continuously scans for vulnerabilities.</p>
28
+ <h1>
29
+ open<span>hacker</span>
30
+ </h1>
31
+ <p className="sub">
32
+ Paste a GitHub repo and the agent will analyze it for vulnerabilities.
33
+ </p>
34
+
35
+ <form className="ask" onSubmit={onSubmit}>
36
+ <input
37
+ type="text"
38
+ value={repo}
39
+ onChange={(e) => setRepo(e.target.value)}
40
+ placeholder="owner/name or https://github.com/owner/name"
41
+ autoFocus
42
+ />
43
+ <button type="submit" disabled={busy || !repo.trim()}>
44
+ {busy ? "Analyzing…" : "Analyze"}
45
+ </button>
46
+ </form>
47
+
48
+ {reply ? (
49
+ <section className="reply">
50
+ {reply.parts.map((part, i) => {
51
+ if (part.type === "reasoning") {
52
+ return (
53
+ <p key={i} className="reasoning">
54
+ {part.text}
55
+ </p>
56
+ );
57
+ }
58
+ if (part.type === "text") {
59
+ return (
60
+ <p key={i} className="text">
61
+ {part.text}
62
+ </p>
63
+ );
64
+ }
65
+ if (part.type === "dynamic-tool") {
66
+ return (
67
+ <div key={i} className="tool">
68
+ <span className="tool-name">{part.toolName}</span>
69
+ <span className="tool-state">{part.state}</span>
70
+ </div>
71
+ );
72
+ }
73
+ return null;
74
+ })}
75
+ {agent.status === "streaming" ? (
76
+ <span className="cursor" aria-hidden />
77
+ ) : null}
78
+ </section>
79
+ ) : busy ? (
80
+ <section className="reply">
81
+ <span className="cursor" aria-hidden />
82
+ </section>
83
+ ) : null}
26
84
 
27
- {!isPersistent() ? (
85
+ {agent.status === "error" ? (
28
86
  <div className="banner">
29
- Using an in-memory store — data will not persist across restarts/deploys. Add a Vercel KV
30
- or Upstash Redis integration and set <code>KV_REST_API_URL</code> /{" "}
31
- <code>KV_REST_API_TOKEN</code> to persist.
87
+ {String(agent.error ?? "Something went wrong.")}
32
88
  </div>
33
89
  ) : null}
34
- {error === "invalid-repo" ? (
35
- <div className="banner">Enter a valid GitHub repository (owner/name or a github.com URL).</div>
36
- ) : null}
37
-
38
- <div className="panel">
39
- <h2 style={{ marginTop: 0 }}>Add a target</h2>
40
- <form action={addTarget}>
41
- <div className="row">
42
- <div>
43
- <label htmlFor="repo">GitHub repository</label>
44
- <input id="repo" name="repo" type="text" placeholder="owner/name or URL" required />
45
- </div>
46
- <div>
47
- <label htmlFor="branch">Branch (optional)</label>
48
- <input id="branch" name="branch" type="text" placeholder="default branch" />
49
- </div>
50
- </div>
51
- <div className="row">
52
- <div>
53
- <label htmlFor="name">Display name (optional)</label>
54
- <input id="name" name="name" type="text" placeholder="My app" />
55
- </div>
56
- <div>
57
- <label htmlFor="token">Access token (optional, for private repos)</label>
58
- <input id="token" name="token" type="password" placeholder="ghp_..." />
59
- </div>
60
- </div>
61
- <div className="check" style={{ marginBottom: 14 }}>
62
- <input id="autoRemediate" name="autoRemediate" type="checkbox" />
63
- <label htmlFor="autoRemediate">Open remediation PRs automatically</label>
64
- </div>
65
- <button type="submit">Add target</button>
66
- </form>
67
- </div>
68
-
69
- <h2>Configured targets</h2>
70
- {targets.length === 0 ? (
71
- <div className="empty">No targets yet. Add a repository above to start scanning.</div>
72
- ) : (
73
- targets.map((t) => {
74
- const findings = findingsByTarget.get(t.id) ?? [];
75
- return (
76
- <div className="card" key={t.id}>
77
- <div className="grow">
78
- <div className="repo">
79
- <Link href={`/targets/${t.id}`}>{t.name}</Link>{" "}
80
- <span className="mono-sm">{t.repo}@{t.branch}</span>
81
- </div>
82
- <div className="meta">
83
- {t.lastScanAt
84
- ? `last scan ${new Date(t.lastScanAt).toLocaleString()}${
85
- t.lastScanStatus === "error" ? ` — error: ${t.lastScanError}` : ""
86
- }`
87
- : "never scanned"}
88
- </div>
89
- <div style={{ marginTop: 8 }}>
90
- <SeverityCounts findings={findings} />
91
- </div>
92
- </div>
93
- <div className="actions">
94
- <form action={scanTarget} className="inline">
95
- <input type="hidden" name="id" value={t.id} />
96
- <button type="submit">Scan now</button>
97
- </form>
98
- <Link className="btn btn-ghost" href={`/targets/${t.id}`}>
99
- View
100
- </Link>
101
- <form action={deleteTarget} className="inline">
102
- <input type="hidden" name="id" value={t.id} />
103
- <button type="submit" className="btn-danger">
104
- Delete
105
- </button>
106
- </form>
107
- </div>
108
- </div>
109
- );
110
- })
111
- )}
112
90
  </main>
113
91
  );
114
92
  }
@@ -13,18 +13,17 @@
13
13
  "eve:info": "eve info"
14
14
  },
15
15
  "dependencies": {
16
- "@upstash/redis": "^1.38.0",
17
16
  "ai": "^7.0.3",
18
17
  "eve": "^0.16.2",
19
18
  "next": "^16.2.9",
20
19
  "react": "^19.2.7",
21
- "react-dom": "^19.2.7",
22
- "zod": "^4.4.3"
20
+ "react-dom": "^19.2.7"
23
21
  },
24
22
  "devDependencies": {
25
23
  "@types/node": "25.5.2",
26
24
  "@types/react": "19.2.14",
27
25
  "@types/react-dom": "^19.2.3",
26
+ "microsandbox": "^0.6.0",
28
27
  "typescript": "6.0.2"
29
28
  }
30
29
  }