codebase-onboarder 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +275 -0
- package/bin/onboarder.js +14 -0
- package/bin/postinstall.js +6 -0
- package/cli/commands.js +436 -0
- package/cli/main.js +129 -0
- package/cli/prompt.js +101 -0
- package/cli/ui.js +55 -0
- package/cli/wizard.js +325 -0
- package/package.json +48 -0
- package/public/app.js +1339 -0
- package/public/index.html +455 -0
- package/public/js/about.js +284 -0
- package/public/js/aiDraft.js +163 -0
- package/public/js/analysisPanel.js +270 -0
- package/public/js/analysisReport.js +154 -0
- package/public/js/api.js +264 -0
- package/public/js/atlas.js +98 -0
- package/public/js/blameView.js +41 -0
- package/public/js/codeTab.js +256 -0
- package/public/js/codeViewer.js +184 -0
- package/public/js/components/FileChip.js +102 -0
- package/public/js/components/MetricSparkline.js +89 -0
- package/public/js/components/RiskBadge.js +105 -0
- package/public/js/deepAnalysisView.js +496 -0
- package/public/js/diagramPane.js +154 -0
- package/public/js/diffView.js +352 -0
- package/public/js/docsView.js +566 -0
- package/public/js/fileSourceBrowser.js +47 -0
- package/public/js/flameGraph.js +75 -0
- package/public/js/forceGraph.js +592 -0
- package/public/js/heatmap.js +185 -0
- package/public/js/highlight.js +122 -0
- package/public/js/html.js +50 -0
- package/public/js/insightsView.js +236 -0
- package/public/js/inspector.js +535 -0
- package/public/js/llm.js +345 -0
- package/public/js/mapView.js +159 -0
- package/public/js/markdown.js +119 -0
- package/public/js/mindmap.js +289 -0
- package/public/js/repoFiles.js +44 -0
- package/public/js/sbomView.js +142 -0
- package/public/js/scanCache.js +76 -0
- package/public/js/search.js +874 -0
- package/public/js/serverSettings.js +169 -0
- package/public/js/state.js +192 -0
- package/public/js/tour.js +17 -0
- package/public/js/transitions.js +15 -0
- package/public/js/tree.js +200 -0
- package/public/js/workflowsView.js +95 -0
- package/public/styles.css +2767 -0
- package/public/vendor/mermaid.min.js +3587 -0
- package/public/vendor/monaco/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
- package/public/vendor/monaco/vs/base/worker/workerMain.js +31 -0
- package/public/vendor/monaco/vs/basic-languages/abap/abap.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/apex/apex.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/azcli/azcli.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/bat/bat.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/bicep/bicep.js +11 -0
- package/public/vendor/monaco/vs/basic-languages/cameligo/cameligo.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/clojure/clojure.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/coffee/coffee.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/cpp/cpp.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/csharp/csharp.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/csp/csp.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/css/css.js +12 -0
- package/public/vendor/monaco/vs/basic-languages/cypher/cypher.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/dart/dart.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/dockerfile/dockerfile.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/ecl/ecl.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/elixir/elixir.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/flow9/flow9.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/freemarker2/freemarker2.js +12 -0
- package/public/vendor/monaco/vs/basic-languages/fsharp/fsharp.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/go/go.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/graphql/graphql.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/handlebars/handlebars.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/hcl/hcl.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/html/html.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/ini/ini.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/java/java.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/javascript/javascript.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/julia/julia.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/kotlin/kotlin.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/less/less.js +11 -0
- package/public/vendor/monaco/vs/basic-languages/lexon/lexon.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/liquid/liquid.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/lua/lua.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/m3/m3.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/markdown/markdown.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/mdx/mdx.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/mips/mips.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/msdax/msdax.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/mysql/mysql.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/objective-c/objective-c.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/pascal/pascal.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/pascaligo/pascaligo.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/perl/perl.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/pgsql/pgsql.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/php/php.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/pla/pla.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/postiats/postiats.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/powerquery/powerquery.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/powershell/powershell.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/protobuf/protobuf.js +11 -0
- package/public/vendor/monaco/vs/basic-languages/pug/pug.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/python/python.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/qsharp/qsharp.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/r/r.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/razor/razor.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/redis/redis.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/redshift/redshift.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/restructuredtext/restructuredtext.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/ruby/ruby.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/rust/rust.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/sb/sb.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/scala/scala.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/scheme/scheme.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/scss/scss.js +12 -0
- package/public/vendor/monaco/vs/basic-languages/shell/shell.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/solidity/solidity.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/sophia/sophia.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/sparql/sparql.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/sql/sql.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/st/st.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/swift/swift.js +13 -0
- package/public/vendor/monaco/vs/basic-languages/systemverilog/systemverilog.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/tcl/tcl.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/twig/twig.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/typescript/typescript.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/typespec/typespec.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/vb/vb.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/wgsl/wgsl.js +307 -0
- package/public/vendor/monaco/vs/basic-languages/xml/xml.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/yaml/yaml.js +10 -0
- package/public/vendor/monaco/vs/editor/editor.main.css +8 -0
- package/public/vendor/monaco/vs/editor/editor.main.js +798 -0
- package/public/vendor/monaco/vs/language/css/cssMode.js +13 -0
- package/public/vendor/monaco/vs/language/css/cssWorker.js +77 -0
- package/public/vendor/monaco/vs/language/html/htmlMode.js +13 -0
- package/public/vendor/monaco/vs/language/html/htmlWorker.js +454 -0
- package/public/vendor/monaco/vs/language/json/jsonMode.js +19 -0
- package/public/vendor/monaco/vs/language/json/jsonWorker.js +42 -0
- package/public/vendor/monaco/vs/language/typescript/tsMode.js +20 -0
- package/public/vendor/monaco/vs/language/typescript/tsWorker.js +51328 -0
- package/public/vendor/monaco/vs/loader.js +11 -0
- package/public/vendor/monaco/worker-boot.js +9 -0
- package/server/.fuse_hidden0000000800000001 +36 -0
- package/server/apiDiff.js +25 -0
- package/server/apiDocs.js +67 -0
- package/server/apiFile.js +36 -0
- package/server/apiGitBlame.js +63 -0
- package/server/apiMcp.js +110 -0
- package/server/apiScan.js +119 -0
- package/server/apiSearch.js +253 -0
- package/server/apiSettings.js +118 -0
- package/server/apiTools.js +90 -0
- package/server/config.js +227 -0
- package/server/fileSourceNode.js +44 -0
- package/server/gitClone.js +95 -0
- package/server/gitDiff.js +184 -0
- package/server/gitHistory.js +110 -0
- package/server/htmlText.js +44 -0
- package/server/http.js +55 -0
- package/server/httpGuards.js +87 -0
- package/server/index.js +156 -0
- package/server/llmProxy.js +162 -0
- package/server/logger.js +29 -0
- package/server/mcp/analysis.js +209 -0
- package/server/mcp/http.js +213 -0
- package/server/mcp/runner.js +278 -0
- package/server/mcp/server.js +241 -0
- package/server/mcp/standalone.js +42 -0
- package/server/mcp/tools.js +683 -0
- package/server/paths.js +39 -0
- package/server/router.js +218 -0
- package/server/searchIndex.js +118 -0
- package/server/sessions.js +163 -0
- package/server/static.js +59 -0
- package/server/tools/install.js +246 -0
- package/server/tools/parse.js +170 -0
- package/server/tools/platform.js +91 -0
- package/server/tools/registry.js +212 -0
- package/server/tools/scan.js +136 -0
- package/server/tools.js +212 -0
- package/server/tunnel.js +92 -0
- package/shared/analyzer/docs.js +135 -0
- package/shared/analyzer/explainLocal.js +163 -0
- package/shared/analyzer/graph.js +461 -0
- package/shared/analyzer/health.js +215 -0
- package/shared/analyzer/history.js +146 -0
- package/shared/analyzer/languages/csharp.js +39 -0
- package/shared/analyzer/languages/generic.js +131 -0
- package/shared/analyzer/languages/go.js +70 -0
- package/shared/analyzer/languages/index.js +43 -0
- package/shared/analyzer/languages/java.js +39 -0
- package/shared/analyzer/languages/javascript.js +240 -0
- package/shared/analyzer/languages/python.js +120 -0
- package/shared/analyzer/languages/rust.js +42 -0
- package/shared/analyzer/languages/typescript.js +138 -0
- package/shared/analyzer/licenses.js +151 -0
- package/shared/analyzer/metrics.js +120 -0
- package/shared/analyzer/pathUtil.js +69 -0
- package/shared/analyzer/patterns.js +272 -0
- package/shared/analyzer/scan.js +473 -0
- package/shared/analyzer/security.js +187 -0
- package/shared/analyzer/services.js +157 -0
- package/shared/analyzer/stack.js +173 -0
- package/shared/analyzer/tour.js +69 -0
- package/shared/analyzer/util.js +123 -0
- package/shared/analyzer/workflows.js +143 -0
- package/shared/diagram/aiFacts.js +145 -0
- package/shared/diagram/aiMermaid.js +59 -0
- package/shared/diagram/atlas.js +93 -0
- package/shared/diagram/mermaid.js +459 -0
- package/shared/search/query.js +518 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
// The deep-analysis store, and the panel the Security view draws from it.
|
|
2
|
+
//
|
|
3
|
+
// Onboarder's built-in scanner is a fast, zero-dependency first pass. This is
|
|
4
|
+
// the second pass — it runs the real analyzers the machine has (Semgrep,
|
|
5
|
+
// Gitleaks, Knip, Vulture, Depcheck) through the server, and merges what they
|
|
6
|
+
// find into the scan so the security grade includes them.
|
|
7
|
+
//
|
|
8
|
+
// Two consumers now share one store: this file's panel in the Security view's
|
|
9
|
+
// inspector, and the full Deep Analysis view
|
|
10
|
+
// (`deepAnalysisView.js`). Putting the run in one place is what keeps them
|
|
11
|
+
// from disagreeing — a run started from either surface lands in
|
|
12
|
+
// `state.tools`, merges identically, and notifies both to repaint. The
|
|
13
|
+
// alternative (each surface calling the API itself) is how you end up with two
|
|
14
|
+
// buttons that produce two different grades.
|
|
15
|
+
//
|
|
16
|
+
// The contract, in three lines:
|
|
17
|
+
//
|
|
18
|
+
// * Nothing is required. Both surfaces render with zero engines installed and
|
|
19
|
+
// say which to install — "no deep analysis" and "no findings" are never the
|
|
20
|
+
// same sentence.
|
|
21
|
+
// * Server-side scans only. A browser-picked folder never touches the disk
|
|
22
|
+
// from the server's point of view, so there is no root to point a tool at;
|
|
23
|
+
// both surfaces explain that instead of pretending.
|
|
24
|
+
// * Findings merge into the same shape the built-in scanner uses, with
|
|
25
|
+
// `source: 'builtin'|'external'` kept, so the attribution stays visible.
|
|
26
|
+
|
|
27
|
+
import { escapeHtml } from './html.js';
|
|
28
|
+
import { fetchToolsStatus, runAnalysisTools, streamToolInstall } from './api.js';
|
|
29
|
+
import { state } from './state.js';
|
|
30
|
+
import { engineRows, engineSummary } from './analysisReport.js';
|
|
31
|
+
import { mergeFindingsIntoScan, summarizeSecurity } from '/shared/analyzer/security.js';
|
|
32
|
+
|
|
33
|
+
// ---- the store -------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
// Who wants to know when a run starts, finishes or fails. The inspector panel
|
|
36
|
+
// and the Deep Analysis view both subscribe; neither polls.
|
|
37
|
+
const listeners = new Set();
|
|
38
|
+
|
|
39
|
+
export function subscribeAnalysis(listener) {
|
|
40
|
+
listeners.add(listener);
|
|
41
|
+
return () => listeners.delete(listener);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function notify() {
|
|
45
|
+
for (const listener of listeners) listener();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Which engines are installed. Asked once per repo and cached on the session —
|
|
49
|
+
// detection spawns `which`, and the answer does not change mid-scan.
|
|
50
|
+
export async function ensureToolsStatus(force = false) {
|
|
51
|
+
if (state.tools.running) return state.tools.status;
|
|
52
|
+
if (state.tools.status && !force) return state.tools.status;
|
|
53
|
+
if (state.tools.loading) return state.tools.status;
|
|
54
|
+
state.tools.loading = true;
|
|
55
|
+
notify();
|
|
56
|
+
try {
|
|
57
|
+
const status = await fetchToolsStatus();
|
|
58
|
+
if (status) state.tools.status = status;
|
|
59
|
+
} finally {
|
|
60
|
+
state.tools.loading = false;
|
|
61
|
+
notify();
|
|
62
|
+
}
|
|
63
|
+
return state.tools.status;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// The per-engine option values the Configure panels hold, shaped for the API:
|
|
67
|
+
// only engines that have a form contribute, and every run gets the full set —
|
|
68
|
+
// the server picks out what its `tools`/`kinds` filter selected.
|
|
69
|
+
function collectToolOptions() {
|
|
70
|
+
const out = {};
|
|
71
|
+
const status = state.tools.status || {};
|
|
72
|
+
for (const id of Object.keys(status)) {
|
|
73
|
+
if ((status[id].options || []).length) out[id] = state.tools.options[id] || {};
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Run the engines. `kinds` narrows by purpose (security / dead-code), `tools`
|
|
79
|
+
// to named engines — the view offers all three scopes.
|
|
80
|
+
//
|
|
81
|
+
// The merge happens here rather than in either caller, so the grade can never
|
|
82
|
+
// depend on which button was pressed. `mergeFindingsIntoScan` is idempotent
|
|
83
|
+
// (it dedups on path+line+rule), so re-running a subset after a full run adds
|
|
84
|
+
// nothing twice.
|
|
85
|
+
export async function runDeepAnalysis(options = {}) {
|
|
86
|
+
if (state.tools.running) return null;
|
|
87
|
+
if (!state.scanId) return null;
|
|
88
|
+
|
|
89
|
+
state.tools.running = true;
|
|
90
|
+
state.tools.error = null;
|
|
91
|
+
notify();
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const report = await runAnalysisTools(state.scanId, { ...options, options: collectToolOptions() });
|
|
95
|
+
state.tools.report = report;
|
|
96
|
+
state.tools.findings = report.findings || [];
|
|
97
|
+
state.tools.merged = mergeFindingsIntoScan(state.scan, state.tools.findings);
|
|
98
|
+
state.security = summarizeSecurity(state.scan);
|
|
99
|
+
return report;
|
|
100
|
+
} catch (err) {
|
|
101
|
+
state.tools.error = err.message;
|
|
102
|
+
return null;
|
|
103
|
+
} finally {
|
|
104
|
+
state.tools.running = false;
|
|
105
|
+
notify();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Install a missing engine without leaving the GUI. The server streams log
|
|
110
|
+
// lines, which collect on `state.tools.install[id]` so both surfaces can show
|
|
111
|
+
// a live console; on success the status is re-fetched (the install cleared
|
|
112
|
+
// the server's detection cache) and the card flips from "not installed" to
|
|
113
|
+
// "ready" without a restart.
|
|
114
|
+
export async function installEngine(id) {
|
|
115
|
+
const entry = state.tools.install[id];
|
|
116
|
+
if (entry && entry.running) return null;
|
|
117
|
+
state.tools.install[id] = { running: true, log: [], ok: null, error: null };
|
|
118
|
+
notify();
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const done = await streamToolInstall(id, (event) => {
|
|
122
|
+
const slot = state.tools.install[id];
|
|
123
|
+
if (event.type === 'log') slot.log.push(event.line);
|
|
124
|
+
if (slot.log.length > 400) slot.log.splice(0, slot.log.length - 400);
|
|
125
|
+
notify();
|
|
126
|
+
});
|
|
127
|
+
const slot = state.tools.install[id];
|
|
128
|
+
slot.running = false;
|
|
129
|
+
slot.ok = !!done.ok;
|
|
130
|
+
slot.error = done.ok ? null : (done.error || 'The install did not finish.');
|
|
131
|
+
if (done.ok) await ensureToolsStatus(true);
|
|
132
|
+
notify();
|
|
133
|
+
return done;
|
|
134
|
+
} catch (err) {
|
|
135
|
+
state.tools.install[id] = { running: false, log: state.tools.install[id].log, ok: false, error: err.message };
|
|
136
|
+
notify();
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// A sentence for whoever pressed the button. Kept out of the store proper —
|
|
142
|
+
// toasting is the caller's business, and the store has no hooks.
|
|
143
|
+
export function runOutcomeMessage(report) {
|
|
144
|
+
if (!report) return 'Deep analysis did not finish.';
|
|
145
|
+
if (!report.ranCount) {
|
|
146
|
+
return report.unavailable && report.unavailable.length
|
|
147
|
+
? 'No engine is installed — see the list for what to install.'
|
|
148
|
+
: 'No engine managed to run.';
|
|
149
|
+
}
|
|
150
|
+
const n = (report.findings || []).length;
|
|
151
|
+
return `${report.ranCount} engine${report.ranCount === 1 ? '' : 's'} ran — ${n} finding${n === 1 ? '' : 's'} merged.`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ---- the inspector panel ---------------------------------------------------
|
|
155
|
+
|
|
156
|
+
let host = null;
|
|
157
|
+
let hooks = { onToast: () => {}, onOpenFile: () => {} };
|
|
158
|
+
|
|
159
|
+
export function initAnalysisPanel(options) {
|
|
160
|
+
host = options.host;
|
|
161
|
+
hooks = { ...hooks, ...options };
|
|
162
|
+
|
|
163
|
+
host.addEventListener('click', (event) => {
|
|
164
|
+
const install = event.target.closest('[data-engine-install]');
|
|
165
|
+
if (install) {
|
|
166
|
+
installEngine(install.dataset.engineInstall).then((done) => {
|
|
167
|
+
if (done && done.ok) hooks.onToast(`${install.dataset.engineInstall} installed — it is ready to run.`);
|
|
168
|
+
else if (done && done.error) hooks.onToast(done.error);
|
|
169
|
+
});
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const run = event.target.closest('[data-run-tools]');
|
|
173
|
+
if (run) {
|
|
174
|
+
const scope = run.dataset.runTools;
|
|
175
|
+
runTools(scope ? { tools: [scope] } : {});
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const kind = event.target.closest('[data-run-kind]');
|
|
179
|
+
if (kind) runTools({ kinds: [kind.dataset.runKind] });
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
subscribeAnalysis(() => paint());
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function runTools(options) {
|
|
186
|
+
const report = await runDeepAnalysis(options);
|
|
187
|
+
hooks.onToast(runOutcomeMessage(report));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Renders into the security view's inspector. Also the place the "not a
|
|
191
|
+
// server-side scan" case is explained, since that is a property of the repo.
|
|
192
|
+
export function renderAnalysisPanel() {
|
|
193
|
+
if (!host) return;
|
|
194
|
+
if (!state.scanId) {
|
|
195
|
+
host.innerHTML = `<div class="tools-note">Deep analysis runs on a scanned folder or clone. A folder picked in the browser never leaves this tab, so there is nothing on the server for an external engine to read — the built-in scan is the whole story for this repo.</div>`;
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (!state.tools.status && !state.tools.loading) void ensureToolsStatus();
|
|
199
|
+
paint();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// The panel is the summary; the Deep Analysis view is where you go to read the
|
|
203
|
+
// whole report. So this stays tight: the engines, one button, and an outcome
|
|
204
|
+
// line — no findings table here.
|
|
205
|
+
function paint() {
|
|
206
|
+
if (!host) return;
|
|
207
|
+
const { status, report, running, loading, error } = state.tools;
|
|
208
|
+
const rows = engineRows(status, report);
|
|
209
|
+
|
|
210
|
+
const head = `<div class="tools-head">
|
|
211
|
+
<h4>Deep analysis <span class="tools-sub">external engines</span></h4>
|
|
212
|
+
<button class="btn btn-ghost tools-run" data-run-tools ${running ? 'disabled' : ''}>
|
|
213
|
+
${running ? 'Running…' : 'Run deep analysis'}
|
|
214
|
+
</button>
|
|
215
|
+
</div>`;
|
|
216
|
+
|
|
217
|
+
let body = '';
|
|
218
|
+
if (error) body += `<div class="tools-err">${escapeHtml(error)}</div>`;
|
|
219
|
+
|
|
220
|
+
if (!rows.length) {
|
|
221
|
+
body += `<div class="tools-note">${loading ? 'Checking which engines are available…' : 'No engine table yet.'}</div>`;
|
|
222
|
+
} else {
|
|
223
|
+
body += '<ul class="tools-list">' + rows.map(engineCard).join('') + '</ul>';
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const summary = engineSummary(report, rows);
|
|
227
|
+
if (report || rows.length) {
|
|
228
|
+
body += `<div class="tools-summary is-${escapeHtml(summary.tone)}">${escapeHtml(summary.text)}</div>`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (rows.some((r) => r.ran)) {
|
|
232
|
+
body += '<div class="tools-more">Every finding, with links to the file and line, is in the <b>Deep Analysis</b> tab.</div>';
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
host.innerHTML = head + body;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// One engine card. The badge is the whole story at a glance: did it run, what
|
|
239
|
+
// did it find, and if it could not run, why not. A missing engine gets an
|
|
240
|
+
// Install button — the person stays in the GUI, and the log lands in the Deep
|
|
241
|
+
// Analysis view's console.
|
|
242
|
+
function engineCard(engine) {
|
|
243
|
+
const installing = state.tools.install[engine.id];
|
|
244
|
+
let badge;
|
|
245
|
+
if (installing && installing.running) badge = '<span class="tools-badge is-running">installing…</span>';
|
|
246
|
+
else if (!engine.available) badge = '<span class="tools-badge is-missing">not installed</span>';
|
|
247
|
+
else if (state.tools.running) badge = '<span class="tools-badge is-running">running…</span>';
|
|
248
|
+
else if (engine.ran) badge = `<span class="tools-badge is-ran">${engine.findings} · ${engine.ms}ms</span>`;
|
|
249
|
+
else if (engine.failed) badge = '<span class="tools-badge is-failed">failed</span>';
|
|
250
|
+
else badge = `<span class="tools-badge is-ready">ready${engine.how ? ' · ' + escapeHtml(engine.how) : ''}</span>`;
|
|
251
|
+
|
|
252
|
+
const note = !engine.available && engine.reason
|
|
253
|
+
? `<span class="tools-reason">${escapeHtml(engine.reason)}</span>`
|
|
254
|
+
: (engine.failure ? `<span class="tools-reason">${escapeHtml(engine.failure)}</span>` : '');
|
|
255
|
+
|
|
256
|
+
const installBtn = !engine.available && engine.installable
|
|
257
|
+
? `<button class="btn btn-ghost btn-sm" data-engine-install="${escapeHtml(engine.id)}" ${installing && installing.running ? 'disabled' : ''}>${installing && installing.running ? 'Installing…' : 'Install'}</button>`
|
|
258
|
+
: '';
|
|
259
|
+
|
|
260
|
+
return `<li class="tools-item${engine.available ? '' : ' is-missing'}">
|
|
261
|
+
<div class="tools-item-main">
|
|
262
|
+
<span class="tools-name">${escapeHtml(engine.label)}</span>
|
|
263
|
+
${badge}
|
|
264
|
+
${installBtn}
|
|
265
|
+
<span class="tools-purpose">${escapeHtml(engine.purpose)}</span>
|
|
266
|
+
</div>
|
|
267
|
+
${note}
|
|
268
|
+
</li>`;
|
|
269
|
+
}
|
|
270
|
+
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// Shaping an analysis report for the Deep Analysis view — pure, DOM-free, and
|
|
2
|
+
// therefore testable in Node.
|
|
3
|
+
//
|
|
4
|
+
// Three questions get answered here, and nowhere else:
|
|
5
|
+
//
|
|
6
|
+
// * What findings do we actually have? The built-in scanner's findings live
|
|
7
|
+
// under `security.files[].findings`; the external engines' findings are
|
|
8
|
+
// merged into the same place, tagged `source: 'external'`. Flattening them
|
|
9
|
+
// into rows is the only place that difference is spelled out, so every view
|
|
10
|
+
// downstream can stop caring which engine answered.
|
|
11
|
+
// * What is the shape of the report? Counts by severity, by tool and by
|
|
12
|
+
// category are what the header chips and the AI prompt both run on.
|
|
13
|
+
// * What survives a filter? Severity, engine and free text, applied as one
|
|
14
|
+
// predicate so the counts above and the table below can never disagree
|
|
15
|
+
// about what is being shown.
|
|
16
|
+
|
|
17
|
+
export const SEV_ORDER = ['critical', 'high', 'medium', 'low', 'info'];
|
|
18
|
+
|
|
19
|
+
export function sevRank(severity) {
|
|
20
|
+
const at = SEV_ORDER.indexOf(severity);
|
|
21
|
+
return at === -1 ? SEV_ORDER.length : at;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const BUILTIN_TOOL = 'built-in';
|
|
25
|
+
|
|
26
|
+
// One flat list of findings, worst first. `security` is the roll-up from
|
|
27
|
+
// `summarizeSecurity`, which is also what the Security panel draws.
|
|
28
|
+
export function findingsFromSecurity(security) {
|
|
29
|
+
const rows = [];
|
|
30
|
+
// The parenthesised default is hoisted rather than inlined: `for (const x of
|
|
31
|
+
// (a || []))` reads as a call to a function named `of` to the front end's
|
|
32
|
+
// dangling-call lint, and the lint is right to be suspicious of that shape.
|
|
33
|
+
const files = (security && security.files) || [];
|
|
34
|
+
for (const file of files) {
|
|
35
|
+
for (const finding of file.findings || []) {
|
|
36
|
+
rows.push({
|
|
37
|
+
path: file.path,
|
|
38
|
+
name: file.name || String(file.path || '').split('/').pop(),
|
|
39
|
+
line: finding.line || 1,
|
|
40
|
+
severity: SEV_ORDER.includes(finding.severity) ? finding.severity : 'info',
|
|
41
|
+
rule: finding.rule || 'unknown',
|
|
42
|
+
category: finding.category || 'other',
|
|
43
|
+
message: finding.message || '',
|
|
44
|
+
excerpt: finding.excerpt || '',
|
|
45
|
+
tool: finding.source === 'external' ? (finding.tool || 'external') : BUILTIN_TOOL,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return rows.sort((a, b) => sevRank(a.severity) - sevRank(b.severity)
|
|
50
|
+
|| a.path.localeCompare(b.path)
|
|
51
|
+
|| a.line - b.line);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Counts for the header and the prompt. `external` vs `builtin` is kept because
|
|
55
|
+
// "the built-in regex scanner said so" and "Semgrep said so" carry different
|
|
56
|
+
// weight, and the UI should be able to say which.
|
|
57
|
+
export function tabulate(rows) {
|
|
58
|
+
const bySeverity = {};
|
|
59
|
+
const byTool = {};
|
|
60
|
+
const byCategory = {};
|
|
61
|
+
for (const row of rows) {
|
|
62
|
+
bySeverity[row.severity] = (bySeverity[row.severity] || 0) + 1;
|
|
63
|
+
byTool[row.tool] = (byTool[row.tool] || 0) + 1;
|
|
64
|
+
byCategory[row.category] = (byCategory[row.category] || 0) + 1;
|
|
65
|
+
}
|
|
66
|
+
const external = rows.filter((r) => r.tool !== BUILTIN_TOOL).length;
|
|
67
|
+
return {
|
|
68
|
+
total: rows.length,
|
|
69
|
+
bySeverity,
|
|
70
|
+
byTool,
|
|
71
|
+
byCategory,
|
|
72
|
+
external,
|
|
73
|
+
builtin: rows.length - external,
|
|
74
|
+
critical: bySeverity.critical || 0,
|
|
75
|
+
high: bySeverity.high || 0,
|
|
76
|
+
medium: bySeverity.medium || 0,
|
|
77
|
+
low: (bySeverity.low || 0) + (bySeverity.info || 0),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// The filter is one predicate over three independent axes. `all` on an axis
|
|
82
|
+
// means "do not filter on this", which is why the default object is usable as-is.
|
|
83
|
+
export function filterFindings(rows, { severity = 'all', tool = 'all', q = '' } = {}) {
|
|
84
|
+
const needle = String(q || '').trim().toLowerCase();
|
|
85
|
+
return rows.filter((row) => {
|
|
86
|
+
if (severity !== 'all' && row.severity !== severity) return false;
|
|
87
|
+
if (tool !== 'all' && row.tool !== tool) return false;
|
|
88
|
+
if (!needle) return true;
|
|
89
|
+
return row.path.toLowerCase().includes(needle)
|
|
90
|
+
|| row.message.toLowerCase().includes(needle)
|
|
91
|
+
|| row.rule.toLowerCase().includes(needle)
|
|
92
|
+
|| row.category.toLowerCase().includes(needle);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// The engine table: what each tool is, whether it is installed, and what it did
|
|
97
|
+
// last time it ran. `status` comes from `GET /api/tools`, `report` from the last
|
|
98
|
+
// `POST /api/tools/run` — they are joined here so the view has one list to draw
|
|
99
|
+
// instead of two to reconcile.
|
|
100
|
+
export function engineRows(status, report) {
|
|
101
|
+
const passes = (report && report.passes) || [];
|
|
102
|
+
return Object.keys(status || {}).map((id) => {
|
|
103
|
+
const meta = status[id] || {};
|
|
104
|
+
const pass = passes.find((p) => p.id === id) || null;
|
|
105
|
+
return {
|
|
106
|
+
id,
|
|
107
|
+
label: meta.label || id,
|
|
108
|
+
kind: meta.kind || 'security',
|
|
109
|
+
purpose: meta.purpose || '',
|
|
110
|
+
available: !!meta.available,
|
|
111
|
+
how: meta.how || null,
|
|
112
|
+
reason: meta.reason || null,
|
|
113
|
+
// The option schema and its defaults, for the Configure form; `installable`
|
|
114
|
+
// is whether the server knows how to install this engine from the GUI.
|
|
115
|
+
options: Array.isArray(meta.options) ? meta.options : [],
|
|
116
|
+
defaults: meta.defaults || {},
|
|
117
|
+
installable: meta.installable !== false,
|
|
118
|
+
ran: !!(pass && pass.ok),
|
|
119
|
+
failed: !!(pass && !pass.ok && pass.available),
|
|
120
|
+
findings: pass && pass.ok ? (pass.findings || []).length : 0,
|
|
121
|
+
ms: pass && pass.ok ? pass.ms : null,
|
|
122
|
+
failure: pass && !pass.ok && pass.available ? pass.reason : null,
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// The one-line verdict under the engine list. Written here rather than in the
|
|
128
|
+
// view because "nothing ran" and "nothing found" must never be confused, and
|
|
129
|
+
// that distinction is the whole point of the panel.
|
|
130
|
+
export function engineSummary(report, rows) {
|
|
131
|
+
if (!report) {
|
|
132
|
+
return { tone: 'idle', text: 'No run yet. The built-in scanner is the whole story until you run an engine.' };
|
|
133
|
+
}
|
|
134
|
+
const ran = rows.filter((r) => r.ran).length;
|
|
135
|
+
const missing = rows.filter((r) => !r.available).length;
|
|
136
|
+
const failed = rows.filter((r) => r.failed).length;
|
|
137
|
+
|
|
138
|
+
if (!ran) {
|
|
139
|
+
return {
|
|
140
|
+
tone: 'none',
|
|
141
|
+
text: missing === rows.length && rows.length
|
|
142
|
+
? 'No engine is installed on this machine. Install any of the ones below and run again — the built-in scan still stands.'
|
|
143
|
+
: 'No engine managed to run. The reasons are listed above.',
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const findings = (report.findings || []).length;
|
|
148
|
+
const parts = [`${ran} of ${rows.length} engines ran in ${report.ms}ms`];
|
|
149
|
+
parts.push(findings ? `${findings} finding${findings === 1 ? '' : 's'} came back` : 'none of them found anything');
|
|
150
|
+
if (failed) parts.push(`${failed} failed`);
|
|
151
|
+
if (missing) parts.push(`${missing} not installed`);
|
|
152
|
+
return { tone: findings ? 'found' : 'clean', text: parts.join(' · ') + '.' };
|
|
153
|
+
}
|
|
154
|
+
|
package/public/js/api.js
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// Server round-trips. Thin and honest: JSON in, JSON (or an SSE stream) out,
|
|
2
|
+
// server error messages surfaced untouched.
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------- access key --
|
|
5
|
+
//
|
|
6
|
+
// A self-hosted server gates every API route behind a Bearer key. The browser
|
|
7
|
+
// learns it one of two ways: the startup banner links here with `?key=…` in
|
|
8
|
+
// the URL (adopted once, then stripped so it does not linger in history), or
|
|
9
|
+
// the person pastes it into the Server drawer. Either way it lives in
|
|
10
|
+
// localStorage and rides along as an Authorization header from then on. Local
|
|
11
|
+
// mode ignores the header entirely, so sending it is harmless.
|
|
12
|
+
|
|
13
|
+
const ACCESS_KEY_STORAGE = 'onboarder.accessKey';
|
|
14
|
+
|
|
15
|
+
let urlKeyChecked = false;
|
|
16
|
+
|
|
17
|
+
// Lazy on purpose, and a function declaration for the same reason: running
|
|
18
|
+
// this at import would touch `window`, and the front-end test suite requires
|
|
19
|
+
// every module to be importable from Node. The first API call is still early
|
|
20
|
+
// enough — the key is adopted before any request leaves.
|
|
21
|
+
function adoptKeyFromUrl() {
|
|
22
|
+
if (urlKeyChecked) return;
|
|
23
|
+
urlKeyChecked = true;
|
|
24
|
+
try {
|
|
25
|
+
const url = new URL(window.location.href);
|
|
26
|
+
const key = url.searchParams.get('key');
|
|
27
|
+
if (!key) return;
|
|
28
|
+
localStorage.setItem(ACCESS_KEY_STORAGE, key);
|
|
29
|
+
url.searchParams.delete('key');
|
|
30
|
+
window.history.replaceState(null, '', url);
|
|
31
|
+
} catch {
|
|
32
|
+
/* no usable URL API — the drawer can still take the key by hand */
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getAccessKey() {
|
|
37
|
+
adoptKeyFromUrl();
|
|
38
|
+
return localStorage.getItem(ACCESS_KEY_STORAGE) || '';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function setAccessKey(key) {
|
|
42
|
+
const trimmed = String(key || '').trim();
|
|
43
|
+
if (trimmed) localStorage.setItem(ACCESS_KEY_STORAGE, trimmed);
|
|
44
|
+
else localStorage.removeItem(ACCESS_KEY_STORAGE);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function authHeaders() {
|
|
48
|
+
const key = getAccessKey();
|
|
49
|
+
return key ? { authorization: `Bearer ${key}` } : {};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function postJSON(url, body) {
|
|
53
|
+
const res = await fetch(url, {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers: { 'content-type': 'application/json', ...authHeaders() },
|
|
56
|
+
body: JSON.stringify(body),
|
|
57
|
+
});
|
|
58
|
+
let data = null;
|
|
59
|
+
try {
|
|
60
|
+
data = await res.json();
|
|
61
|
+
} catch {
|
|
62
|
+
/* error path handled below with a generic message */
|
|
63
|
+
}
|
|
64
|
+
if (!res.ok) {
|
|
65
|
+
throw new Error(data?.error || `The server said ${res.status}.`);
|
|
66
|
+
}
|
|
67
|
+
return data;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function scanOnServer(payload) {
|
|
71
|
+
return postJSON('/api/scan', payload);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function cleanupClone(cloneId) {
|
|
75
|
+
try {
|
|
76
|
+
await fetch('/api/scan/' + encodeURIComponent(cloneId), { method: 'DELETE', headers: authHeaders() });
|
|
77
|
+
} catch {
|
|
78
|
+
/* best-effort: the temp dir expires on its own eventually */
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function fetchFileText(scanId, path) {
|
|
83
|
+
const res = await fetch('/api/file?scan=' + encodeURIComponent(scanId) + '&path=' + encodeURIComponent(path), { headers: authHeaders() });
|
|
84
|
+
if (!res.ok) throw new Error('Could not read that file from the server.');
|
|
85
|
+
return res.text();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Which deep analyzers this machine can run, and how. Server-side scans only —
|
|
89
|
+
// a browser-picked folder has no scan id and no server-visible root to point a
|
|
90
|
+
// tool at, and this returns that honestly instead of pretending.
|
|
91
|
+
export async function fetchToolsStatus() {
|
|
92
|
+
try {
|
|
93
|
+
const res = await fetch('/api/tools', { headers: authHeaders() });
|
|
94
|
+
if (!res.ok) return null;
|
|
95
|
+
const data = await res.json();
|
|
96
|
+
return data && data.tools ? data.tools : null;
|
|
97
|
+
} catch {
|
|
98
|
+
return null; // an unreachable server is just "no deep analysis here"
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Run the available analyzers against a live scan. `kinds`/`tools` narrow which
|
|
103
|
+
// engines run; `options` carries the per-engine GUI settings, validated against
|
|
104
|
+
// the registry schema on the server. The report normalizes every finding into
|
|
105
|
+
// the shape the security view already draws, plus a per-engine pass list so
|
|
106
|
+
// the UI can say which tool answered and which are missing.
|
|
107
|
+
export function runAnalysisTools(scanId, options = {}) {
|
|
108
|
+
return postJSON('/api/tools/run', {
|
|
109
|
+
scanId,
|
|
110
|
+
kinds: options.kinds || undefined,
|
|
111
|
+
tools: options.tools || undefined,
|
|
112
|
+
options: options.options || undefined,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Install a missing engine, streaming progress events. The server emits SSE
|
|
117
|
+
// frames shaped like the explain stream, minus the OpenAI envelope:
|
|
118
|
+
// { type: 'log', line } / { type: 'plan', label } / { type: 'done', ok, … }
|
|
119
|
+
// `onEvent` gets each one as it arrives; the promise resolves with the final
|
|
120
|
+
// `done` event (or throws if the request itself failed).
|
|
121
|
+
export async function streamToolInstall(tool, onEvent) {
|
|
122
|
+
const emit = typeof onEvent === 'function' ? onEvent : () => {};
|
|
123
|
+
const res = await fetch('/api/tools/install', {
|
|
124
|
+
method: 'POST',
|
|
125
|
+
headers: { 'content-type': 'application/json', ...authHeaders() },
|
|
126
|
+
body: JSON.stringify({ tool }),
|
|
127
|
+
});
|
|
128
|
+
if (!res.ok || !res.body) {
|
|
129
|
+
let detail = '';
|
|
130
|
+
try { detail = (await res.json()).error || ''; } catch { /* generic below */ }
|
|
131
|
+
throw new Error(detail || `The server said ${res.status}.`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const reader = res.body.getReader();
|
|
135
|
+
const decoder = new TextDecoder();
|
|
136
|
+
let buffer = '';
|
|
137
|
+
let done = null;
|
|
138
|
+
while (true) {
|
|
139
|
+
const { done: ended, value } = await reader.read();
|
|
140
|
+
if (ended) break;
|
|
141
|
+
buffer += decoder.decode(value, { stream: true });
|
|
142
|
+
const frames = buffer.split('\n\n');
|
|
143
|
+
buffer = frames.pop();
|
|
144
|
+
for (const frame of frames) {
|
|
145
|
+
for (const line of frame.split('\n')) {
|
|
146
|
+
if (!line.startsWith('data:')) continue;
|
|
147
|
+
try {
|
|
148
|
+
const event = JSON.parse(line.slice(5).trim());
|
|
149
|
+
emit(event);
|
|
150
|
+
if (event.type === 'done') done = event;
|
|
151
|
+
} catch { /* a partial frame — the next chunk completes it */ }
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return done || { type: 'done', ok: false, error: 'The install stream ended without a verdict.' };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// The MCP button's three round-trips. The status one is a plain GET like
|
|
159
|
+
// `/api/tools`; start and stop are POSTs because each one changes a process on
|
|
160
|
+
// this machine, which is exactly the side effect `postJSON` is here for.
|
|
161
|
+
export async function fetchMcpStatus() {
|
|
162
|
+
try {
|
|
163
|
+
const res = await fetch('/api/mcp', { headers: authHeaders() });
|
|
164
|
+
if (!res.ok) return null;
|
|
165
|
+
return await res.json();
|
|
166
|
+
} catch {
|
|
167
|
+
return null; // no server means no button state, not an error to shout about
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function startMcpServer() {
|
|
172
|
+
return postJSON('/api/mcp/start', {});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function stopMcpServer() {
|
|
176
|
+
return postJSON('/api/mcp/stop', {});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// The command an agent harness is configured with. Kept separate from the status
|
|
180
|
+
// fetch because it never changes, so it is read once when the panel opens rather
|
|
181
|
+
// than on every poll.
|
|
182
|
+
export async function fetchMcpCommand() {
|
|
183
|
+
try {
|
|
184
|
+
const res = await fetch('/api/mcp/command', { headers: authHeaders() });
|
|
185
|
+
if (!res.ok) return null;
|
|
186
|
+
return await res.json();
|
|
187
|
+
} catch {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Streams an OpenAI-compatible chat completion through the local proxy.
|
|
193
|
+
// Yields text deltas as they arrive; throws with the provider's message on
|
|
194
|
+
// a non-200 from upstream.
|
|
195
|
+
export async function* streamExplain({ baseUrl, apiKey, model, messages, maxTokens = 1200, providerOptions = {} }) {
|
|
196
|
+
const res = await fetch('/api/explain', {
|
|
197
|
+
method: 'POST',
|
|
198
|
+
headers: { 'content-type': 'application/json', ...authHeaders() },
|
|
199
|
+
body: JSON.stringify({ baseUrl, apiKey, model, messages, stream: true, max_tokens: maxTokens, ...providerOptions }),
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
if (!res.ok) {
|
|
203
|
+
let detail = '';
|
|
204
|
+
try {
|
|
205
|
+
const data = await res.json();
|
|
206
|
+
detail = data.detail || data.error || '';
|
|
207
|
+
} catch {
|
|
208
|
+
/* keep the generic line */
|
|
209
|
+
}
|
|
210
|
+
throw new Error(detail || `The provider said ${res.status}.`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const reader = res.body.getReader();
|
|
214
|
+
const decoder = new TextDecoder();
|
|
215
|
+
let buffer = '';
|
|
216
|
+
while (true) {
|
|
217
|
+
const { done, value } = await reader.read();
|
|
218
|
+
if (done) break;
|
|
219
|
+
buffer += decoder.decode(value, { stream: true });
|
|
220
|
+
const frames = buffer.split('\n\n');
|
|
221
|
+
buffer = frames.pop();
|
|
222
|
+
for (const frame of frames) {
|
|
223
|
+
for (const line of frame.split('\n')) {
|
|
224
|
+
if (!line.startsWith('data:')) continue;
|
|
225
|
+
const payload = line.slice(5).trim();
|
|
226
|
+
if (payload === '[DONE]') return;
|
|
227
|
+
try {
|
|
228
|
+
const json = JSON.parse(payload);
|
|
229
|
+
const delta = json.choices?.[0]?.delta?.content;
|
|
230
|
+
if (delta) yield delta;
|
|
231
|
+
} catch {
|
|
232
|
+
/* a partial JSON frame — the next chunk completes it */
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// The Server drawer's three round-trips. The read is a GET that comes back
|
|
240
|
+
// with the secret masked; writes go through PUT with only the changed keys;
|
|
241
|
+
// rotation is a POST because it mints a new key on the server — the one and
|
|
242
|
+
// only time a key ever crosses the wire in the clear.
|
|
243
|
+
export async function fetchServerSettings() {
|
|
244
|
+
const res = await fetch('/api/settings', { headers: authHeaders() });
|
|
245
|
+
const data = await res.json().catch(() => null);
|
|
246
|
+
if (!res.ok) throw new Error(data?.error || `The server said ${res.status}.`);
|
|
247
|
+
return data;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export async function updateServerSettings(patch) {
|
|
251
|
+
const res = await fetch('/api/settings', {
|
|
252
|
+
method: 'PUT',
|
|
253
|
+
headers: { 'content-type': 'application/json', ...authHeaders() },
|
|
254
|
+
body: JSON.stringify(patch),
|
|
255
|
+
});
|
|
256
|
+
const data = await res.json().catch(() => null);
|
|
257
|
+
if (!res.ok) throw new Error(data?.error || `The server said ${res.status}.`);
|
|
258
|
+
return data;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function rotateServerAccessKey() {
|
|
262
|
+
return postJSON('/api/settings/access-key', {});
|
|
263
|
+
}
|
|
264
|
+
|