bluekiwi 0.1.2 → 0.1.4
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/dist/assets/mcp/api-client.d.ts +6 -0
- package/dist/assets/mcp/api-client.js +61 -0
- package/dist/assets/mcp/errors.d.ts +12 -0
- package/dist/assets/mcp/errors.js +24 -0
- package/dist/assets/mcp/server.d.ts +1 -0
- package/dist/assets/mcp/server.js +805 -0
- package/dist/index.js +0 -0
- package/package.json +1 -1
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { BlueKiwiAuthError, BlueKiwiApiError, BlueKiwiNetworkError, } from "./errors.js";
|
|
2
|
+
const RETRY_DELAYS_MS = [100, 500, 2000];
|
|
3
|
+
export class BlueKiwiClient {
|
|
4
|
+
baseUrl;
|
|
5
|
+
apiKey;
|
|
6
|
+
constructor(baseUrl, apiKey) {
|
|
7
|
+
this.baseUrl = baseUrl;
|
|
8
|
+
this.apiKey = apiKey;
|
|
9
|
+
if (!baseUrl)
|
|
10
|
+
throw new Error("BlueKiwiClient: baseUrl is required");
|
|
11
|
+
if (!apiKey)
|
|
12
|
+
throw new Error("BlueKiwiClient: apiKey is required");
|
|
13
|
+
}
|
|
14
|
+
async request(method, path, body) {
|
|
15
|
+
const url = `${this.baseUrl.replace(/\/$/, "")}${path}`;
|
|
16
|
+
const init = {
|
|
17
|
+
method,
|
|
18
|
+
headers: {
|
|
19
|
+
"Content-Type": "application/json",
|
|
20
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
21
|
+
},
|
|
22
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
23
|
+
};
|
|
24
|
+
let lastError;
|
|
25
|
+
for (let attempt = 0; attempt <= RETRY_DELAYS_MS.length; attempt++) {
|
|
26
|
+
try {
|
|
27
|
+
const res = await fetch(url, init);
|
|
28
|
+
if (res.status === 401) {
|
|
29
|
+
throw new BlueKiwiAuthError();
|
|
30
|
+
}
|
|
31
|
+
if (!res.ok) {
|
|
32
|
+
const text = await res.text().catch(() => "");
|
|
33
|
+
if (res.status >= 500 && attempt < RETRY_DELAYS_MS.length) {
|
|
34
|
+
lastError = new BlueKiwiApiError(res.status, text);
|
|
35
|
+
await sleep(RETRY_DELAYS_MS[attempt]);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
throw new BlueKiwiApiError(res.status, text);
|
|
39
|
+
}
|
|
40
|
+
const text = await res.text();
|
|
41
|
+
return (text ? JSON.parse(text) : null);
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
if (err instanceof BlueKiwiAuthError ||
|
|
45
|
+
err instanceof BlueKiwiApiError) {
|
|
46
|
+
throw err;
|
|
47
|
+
}
|
|
48
|
+
lastError = err;
|
|
49
|
+
if (attempt < RETRY_DELAYS_MS.length) {
|
|
50
|
+
await sleep(RETRY_DELAYS_MS[attempt]);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
throw new BlueKiwiNetworkError(`Failed to reach ${url} after ${RETRY_DELAYS_MS.length + 1} attempts`, err);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
throw new BlueKiwiNetworkError("Unreachable", lastError);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function sleep(ms) {
|
|
60
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
61
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare class BlueKiwiAuthError extends Error {
|
|
2
|
+
constructor(message?: string);
|
|
3
|
+
}
|
|
4
|
+
export declare class BlueKiwiApiError extends Error {
|
|
5
|
+
readonly status: number;
|
|
6
|
+
readonly body: string;
|
|
7
|
+
constructor(status: number, body: string);
|
|
8
|
+
}
|
|
9
|
+
export declare class BlueKiwiNetworkError extends Error {
|
|
10
|
+
readonly cause?: unknown | undefined;
|
|
11
|
+
constructor(message: string, cause?: unknown | undefined);
|
|
12
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export class BlueKiwiAuthError extends Error {
|
|
2
|
+
constructor(message = "Invalid or expired API key") {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = "BlueKiwiAuthError";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export class BlueKiwiApiError extends Error {
|
|
8
|
+
status;
|
|
9
|
+
body;
|
|
10
|
+
constructor(status, body) {
|
|
11
|
+
super(`BlueKiwi API error ${status}: ${body}`);
|
|
12
|
+
this.status = status;
|
|
13
|
+
this.body = body;
|
|
14
|
+
this.name = "BlueKiwiApiError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export class BlueKiwiNetworkError extends Error {
|
|
18
|
+
cause;
|
|
19
|
+
constructor(message, cause) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.cause = cause;
|
|
22
|
+
this.name = "BlueKiwiNetworkError";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,805 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
import { BlueKiwiClient } from "./api-client.js";
|
|
5
|
+
import { BlueKiwiApiError, BlueKiwiAuthError, BlueKiwiNetworkError, } from "./errors.js";
|
|
6
|
+
import * as fs from "fs";
|
|
7
|
+
import * as path from "path";
|
|
8
|
+
const KOREA_OTA_PATTERNS = (() => {
|
|
9
|
+
const rrnSource = String.raw `\b\d{6}-?\d{7}\b`;
|
|
10
|
+
const secretSource = String.raw `(password|secret|api[_-]?key|token|private[_-]?key)\s*[=:]\s*["'` +
|
|
11
|
+
"`" +
|
|
12
|
+
String.raw `].{8,}`;
|
|
13
|
+
const fieldSource = String.raw `(residentRegistration|rrn|passport|foreignerRegistration|visaNumber|cardNumber|cvv|cvc|accountNumber)`;
|
|
14
|
+
const httpSource = String.raw `http://(?!localhost|127\.0\.0\.1|0\.0\.0\.0)`;
|
|
15
|
+
const geoSource = String.raw `navigator\.geolocation\.(getCurrentPosition|watchPosition)`;
|
|
16
|
+
const precheckSource = String.raw `(defaultChecked|checked)\s*=\s*\{?\s*true`;
|
|
17
|
+
return [
|
|
18
|
+
{
|
|
19
|
+
id: "PIPA-001-RRN",
|
|
20
|
+
severity: "REVIEW",
|
|
21
|
+
source: rrnSource,
|
|
22
|
+
flags: "",
|
|
23
|
+
description: "RRN/외국인등록번호 형식",
|
|
24
|
+
regex: new RegExp(rrnSource, ""),
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: "ISMS-001-SECRET",
|
|
28
|
+
severity: "BLOCK",
|
|
29
|
+
source: secretSource,
|
|
30
|
+
flags: "i",
|
|
31
|
+
description: "하드코딩 시크릿 의심",
|
|
32
|
+
regex: new RegExp(secretSource, "i"),
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: "PIPA-002-FIELD",
|
|
36
|
+
severity: "REVIEW",
|
|
37
|
+
source: fieldSource,
|
|
38
|
+
flags: "i",
|
|
39
|
+
description: "고위험 식별자 필드명",
|
|
40
|
+
regex: new RegExp(fieldSource, "i"),
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: "ISMS-004-HTTP",
|
|
44
|
+
severity: "WARN",
|
|
45
|
+
source: httpSource,
|
|
46
|
+
flags: "",
|
|
47
|
+
description: "평문 HTTP URL (외부)",
|
|
48
|
+
regex: new RegExp(httpSource, ""),
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "LIA-001-GEO",
|
|
52
|
+
severity: "REVIEW",
|
|
53
|
+
source: geoSource,
|
|
54
|
+
flags: "",
|
|
55
|
+
description: "Geolocation API 사용",
|
|
56
|
+
regex: new RegExp(geoSource, ""),
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "PIPA-004-PRECHECK",
|
|
60
|
+
severity: "REVIEW",
|
|
61
|
+
source: precheckSource,
|
|
62
|
+
flags: "",
|
|
63
|
+
description: "사전 체크된 동의 체크박스",
|
|
64
|
+
regex: new RegExp(precheckSource, ""),
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
})();
|
|
68
|
+
const SCAN_REPO_DEFAULT_GLOBS = [
|
|
69
|
+
"*.ts",
|
|
70
|
+
"*.tsx",
|
|
71
|
+
"*.js",
|
|
72
|
+
"*.jsx",
|
|
73
|
+
"*.java",
|
|
74
|
+
"*.py",
|
|
75
|
+
"*.go",
|
|
76
|
+
"*.rs",
|
|
77
|
+
"*.sql",
|
|
78
|
+
"*.yml",
|
|
79
|
+
"*.yaml",
|
|
80
|
+
"*.properties",
|
|
81
|
+
"*.env",
|
|
82
|
+
"*.tf",
|
|
83
|
+
"*.hcl",
|
|
84
|
+
];
|
|
85
|
+
const SCAN_REPO_SKIP_DIRS = new Set([
|
|
86
|
+
"node_modules",
|
|
87
|
+
".git",
|
|
88
|
+
"dist",
|
|
89
|
+
"build",
|
|
90
|
+
".next",
|
|
91
|
+
"coverage",
|
|
92
|
+
".turbo",
|
|
93
|
+
]);
|
|
94
|
+
const apiUrl = process.env.BLUEKIWI_API_URL;
|
|
95
|
+
const apiKey = process.env.BLUEKIWI_API_KEY ?? parseApiKeyFlag();
|
|
96
|
+
if (!apiUrl) {
|
|
97
|
+
throw new Error("BLUEKIWI_API_URL is required");
|
|
98
|
+
}
|
|
99
|
+
if (!apiKey) {
|
|
100
|
+
throw new Error("BLUEKIWI_API_KEY is required");
|
|
101
|
+
}
|
|
102
|
+
const client = new BlueKiwiClient(apiUrl, apiKey);
|
|
103
|
+
const server = new Server({ name: "bluekiwi", version: "1.0.0" }, { capabilities: { tools: {} } });
|
|
104
|
+
const tools = [
|
|
105
|
+
tool("list_workflows", "List workflows visible to the current user. By default only active versions are returned. Pass include_inactive=true to see archived versions. Pass folder_id to filter by a specific folder.", {
|
|
106
|
+
include_inactive: { type: "boolean" },
|
|
107
|
+
folder_id: { type: "number" },
|
|
108
|
+
}),
|
|
109
|
+
tool("list_workflow_versions", "List every version in the same family as the given workflow id, including active and archived ones. Returns the active_version_id and an ordered versions array.", {
|
|
110
|
+
workflow_id: { type: "number" },
|
|
111
|
+
}, ["workflow_id"]),
|
|
112
|
+
tool("activate_workflow", "Activate a specific workflow version. Automatically deactivates the other active version in the same family (one active version per family).", {
|
|
113
|
+
workflow_id: { type: "number" },
|
|
114
|
+
}, ["workflow_id"]),
|
|
115
|
+
tool("deactivate_workflow", "Deactivate a specific workflow version. Archived versions remain readable but cannot be pinned by new task starts.", {
|
|
116
|
+
workflow_id: { type: "number" },
|
|
117
|
+
}, ["workflow_id"]),
|
|
118
|
+
tool("start_workflow", "Start a task from a workflow id. Optionally pin a specific version within the same family. Starting against an archived (inactive) version fails with HTTP 409.", {
|
|
119
|
+
workflow_id: { type: "number" },
|
|
120
|
+
version: { type: "string" },
|
|
121
|
+
context: { type: "string" },
|
|
122
|
+
session_meta: { type: "string" },
|
|
123
|
+
target: { type: "object" },
|
|
124
|
+
}, ["workflow_id"]),
|
|
125
|
+
tool("execute_step", "Submit the result for the current workflow step", {
|
|
126
|
+
task_id: { type: "number" },
|
|
127
|
+
node_id: { type: "number" },
|
|
128
|
+
output: { type: "string" },
|
|
129
|
+
status: { type: "string" },
|
|
130
|
+
visual_html: { type: "string" },
|
|
131
|
+
loop_continue: { type: "boolean" },
|
|
132
|
+
context_snapshot: { type: "object" },
|
|
133
|
+
structured_output: { type: "object" },
|
|
134
|
+
artifacts: { type: "array" },
|
|
135
|
+
session_id: { type: "string" },
|
|
136
|
+
agent_id: { type: "string" },
|
|
137
|
+
user_name: { type: "string" },
|
|
138
|
+
model_id: { type: "string" },
|
|
139
|
+
}, ["task_id", "node_id", "output", "status"]),
|
|
140
|
+
tool("advance", "Advance a task to the next step or inspect the current step", {
|
|
141
|
+
task_id: { type: "number" },
|
|
142
|
+
peek: { type: "boolean" },
|
|
143
|
+
}, ["task_id"]),
|
|
144
|
+
tool("heartbeat", "Append progress information for a running task step", {
|
|
145
|
+
task_id: { type: "number" },
|
|
146
|
+
node_id: { type: "number" },
|
|
147
|
+
progress: { type: "string" },
|
|
148
|
+
}, ["task_id", "node_id", "progress"]),
|
|
149
|
+
tool("complete_task", "Mark a task as completed or failed", {
|
|
150
|
+
task_id: { type: "number" },
|
|
151
|
+
status: { type: "string" },
|
|
152
|
+
summary: { type: "string" },
|
|
153
|
+
}, ["task_id", "status"]),
|
|
154
|
+
tool("rewind", "Rewind a task to a previous step", {
|
|
155
|
+
task_id: { type: "number" },
|
|
156
|
+
to_step: { type: "number" },
|
|
157
|
+
}, ["task_id", "to_step"]),
|
|
158
|
+
tool("get_web_response", "Fetch the pending web response payload for a task", {
|
|
159
|
+
task_id: { type: "number" },
|
|
160
|
+
}, ["task_id"]),
|
|
161
|
+
tool("submit_visual", "Submit visual HTML for a task step", {
|
|
162
|
+
task_id: { type: "number" },
|
|
163
|
+
node_id: { type: "number" },
|
|
164
|
+
visual_html: { type: "string" },
|
|
165
|
+
}, ["task_id", "node_id", "visual_html"]),
|
|
166
|
+
tool("save_artifacts", "Save artifacts for a task step", {
|
|
167
|
+
task_id: { type: "number" },
|
|
168
|
+
artifacts: { type: "array" },
|
|
169
|
+
}, ["task_id", "artifacts"]),
|
|
170
|
+
tool("load_artifacts", "Load artifacts for a task", {
|
|
171
|
+
task_id: { type: "number" },
|
|
172
|
+
}, ["task_id"]),
|
|
173
|
+
tool("get_comments", "List comments for a task", {
|
|
174
|
+
task_id: { type: "number" },
|
|
175
|
+
}, ["task_id"]),
|
|
176
|
+
tool("list_credentials", "List credentials available to the current user"),
|
|
177
|
+
tool("create_workflow", "Create a new workflow. Optionally place it in a specific folder_id (defaults to the caller's My Workspace).", {
|
|
178
|
+
title: { type: "string" },
|
|
179
|
+
description: { type: "string" },
|
|
180
|
+
version: { type: "string" },
|
|
181
|
+
parent_workflow_id: { type: "number" },
|
|
182
|
+
evaluation_contract: { type: "object" },
|
|
183
|
+
nodes: { type: "array" },
|
|
184
|
+
folder_id: { type: "number" },
|
|
185
|
+
}, ["title"]),
|
|
186
|
+
tool("update_workflow", "Update an existing workflow", {
|
|
187
|
+
workflow_id: { type: "number" },
|
|
188
|
+
title: { type: "string" },
|
|
189
|
+
description: { type: "string" },
|
|
190
|
+
version: { type: "string" },
|
|
191
|
+
evaluation_contract: { type: "object" },
|
|
192
|
+
create_new_version: { type: "boolean" },
|
|
193
|
+
nodes: { type: "array" },
|
|
194
|
+
}, ["workflow_id"]),
|
|
195
|
+
tool("delete_workflow", "Delete a workflow", {
|
|
196
|
+
workflow_id: { type: "number" },
|
|
197
|
+
}, ["workflow_id"]),
|
|
198
|
+
tool("save_findings", "Save one or more compliance findings for a task", {
|
|
199
|
+
task_id: { type: "number" },
|
|
200
|
+
findings: { type: "array" },
|
|
201
|
+
}, ["task_id", "findings"]),
|
|
202
|
+
tool("list_findings", "List compliance findings for a task", {
|
|
203
|
+
task_id: { type: "number" },
|
|
204
|
+
}, ["task_id"]),
|
|
205
|
+
tool("list_folders", "List folders visible to the current user. Optionally filter by parent_id.", {
|
|
206
|
+
parent_id: { type: "number" },
|
|
207
|
+
}),
|
|
208
|
+
tool("create_folder", "Create a new folder under an optional parent. Visibility defaults to 'personal'.", {
|
|
209
|
+
name: { type: "string" },
|
|
210
|
+
description: { type: "string" },
|
|
211
|
+
parent_id: { type: "number" },
|
|
212
|
+
visibility: { type: "string" },
|
|
213
|
+
}, ["name"]),
|
|
214
|
+
tool("share_folder", "Share a folder with a user group at the given access level (viewer or editor). Owner or admin only.", {
|
|
215
|
+
folder_id: { type: "number" },
|
|
216
|
+
group_id: { type: "number" },
|
|
217
|
+
access_level: { type: "string" },
|
|
218
|
+
}, ["folder_id", "group_id", "access_level"]),
|
|
219
|
+
tool("unshare_folder", "Remove a group share from a folder.", {
|
|
220
|
+
folder_id: { type: "number" },
|
|
221
|
+
group_id: { type: "number" },
|
|
222
|
+
}, ["folder_id", "group_id"]),
|
|
223
|
+
tool("move_workflow", "Move a workflow into a different folder. Caller must have edit permission on the destination folder.", {
|
|
224
|
+
workflow_id: { type: "number" },
|
|
225
|
+
folder_id: { type: "number" },
|
|
226
|
+
}, ["workflow_id", "folder_id"]),
|
|
227
|
+
tool("move_instruction", "Move an instruction into a different folder.", {
|
|
228
|
+
instruction_id: { type: "number" },
|
|
229
|
+
folder_id: { type: "number" },
|
|
230
|
+
}, ["instruction_id", "folder_id"]),
|
|
231
|
+
tool("transfer_workflow", "Transfer ownership of a workflow to another user. Owner, admin, or superuser only.", {
|
|
232
|
+
workflow_id: { type: "number" },
|
|
233
|
+
new_owner_id: { type: "number" },
|
|
234
|
+
}, ["workflow_id", "new_owner_id"]),
|
|
235
|
+
tool("list_my_groups", "List user groups the current user belongs to."),
|
|
236
|
+
/*
|
|
237
|
+
scan_repo is the single local-execution exception in an otherwise REST-thin MCP wrapper.
|
|
238
|
+
The REST backend has no visibility into the agent's filesystem, so delegating static
|
|
239
|
+
pattern scans to it is impossible without an upload/clone model. Keeping this step
|
|
240
|
+
in-process preserves determinism and reproducibility of compliance scans. All other
|
|
241
|
+
tools must stay thin proxies — do not replicate this pattern elsewhere.
|
|
242
|
+
*/
|
|
243
|
+
tool("scan_repo", "리포지토리의 특정 경로를 정적 패턴(정규식)으로 스캔하여 컴플라이언스 리스크 신호를 찾아 반환합니다. korea-ota-code 룰셋이 내장되어 있으며, custom 패턴도 추가로 전달할 수 있습니다. [로컬 실행 예외: 이 도구는 REST 백엔드를 거치지 않고 에이전트 파일시스템에서 직접 스캔을 수행합니다.]", {
|
|
244
|
+
path: { type: "string" },
|
|
245
|
+
rule_set: { type: "string" },
|
|
246
|
+
custom_patterns: { type: "array" },
|
|
247
|
+
include_globs: { type: "array" },
|
|
248
|
+
max_matches: { type: "number" },
|
|
249
|
+
task_id: { type: "number" },
|
|
250
|
+
}, ["path"]),
|
|
251
|
+
];
|
|
252
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
253
|
+
tools,
|
|
254
|
+
}));
|
|
255
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
256
|
+
const name = request.params.name;
|
|
257
|
+
const args = toArgs(request.params.arguments);
|
|
258
|
+
try {
|
|
259
|
+
switch (name) {
|
|
260
|
+
case "list_workflows": {
|
|
261
|
+
const qs = new URLSearchParams();
|
|
262
|
+
if (args.include_inactive === true)
|
|
263
|
+
qs.set("include_inactive", "true");
|
|
264
|
+
if (typeof args.folder_id === "number")
|
|
265
|
+
qs.set("folder_id", String(args.folder_id));
|
|
266
|
+
const suffix = qs.toString() ? `?${qs.toString()}` : "";
|
|
267
|
+
return wrap(await client.request("GET", `/api/workflows${suffix}`));
|
|
268
|
+
}
|
|
269
|
+
case "list_workflow_versions": {
|
|
270
|
+
const workflowId = requireNumberArg(args, "workflow_id");
|
|
271
|
+
return wrap(await client.request("GET", `/api/workflows/${workflowId}/versions`));
|
|
272
|
+
}
|
|
273
|
+
case "activate_workflow": {
|
|
274
|
+
const workflowId = requireNumberArg(args, "workflow_id");
|
|
275
|
+
return wrap(await client.request("POST", `/api/workflows/${workflowId}/activate`));
|
|
276
|
+
}
|
|
277
|
+
case "deactivate_workflow": {
|
|
278
|
+
const workflowId = requireNumberArg(args, "workflow_id");
|
|
279
|
+
return wrap(await client.request("POST", `/api/workflows/${workflowId}/deactivate`));
|
|
280
|
+
}
|
|
281
|
+
case "start_workflow":
|
|
282
|
+
return wrap(await client.request("POST", "/api/tasks/start", args));
|
|
283
|
+
case "execute_step": {
|
|
284
|
+
const taskId = requireNumberArg(args, "task_id");
|
|
285
|
+
const body = { ...args };
|
|
286
|
+
delete body.task_id;
|
|
287
|
+
return wrap(await client.request("POST", `/api/tasks/${taskId}/execute`, body));
|
|
288
|
+
}
|
|
289
|
+
case "advance": {
|
|
290
|
+
const taskId = requireNumberArg(args, "task_id");
|
|
291
|
+
const body = { ...args };
|
|
292
|
+
delete body.task_id;
|
|
293
|
+
return wrap(await client.request("POST", `/api/tasks/${taskId}/advance`, body));
|
|
294
|
+
}
|
|
295
|
+
case "heartbeat": {
|
|
296
|
+
const taskId = requireNumberArg(args, "task_id");
|
|
297
|
+
const body = { ...args };
|
|
298
|
+
delete body.task_id;
|
|
299
|
+
return wrap(await client.request("POST", `/api/tasks/${taskId}/heartbeat`, body));
|
|
300
|
+
}
|
|
301
|
+
case "complete_task": {
|
|
302
|
+
const taskId = requireNumberArg(args, "task_id");
|
|
303
|
+
const body = { ...args };
|
|
304
|
+
delete body.task_id;
|
|
305
|
+
return wrap(await client.request("POST", `/api/tasks/${taskId}/complete`, body));
|
|
306
|
+
}
|
|
307
|
+
case "rewind": {
|
|
308
|
+
const taskId = requireNumberArg(args, "task_id");
|
|
309
|
+
const body = { ...args };
|
|
310
|
+
delete body.task_id;
|
|
311
|
+
return wrap(await client.request("POST", `/api/tasks/${taskId}/rewind`, body));
|
|
312
|
+
}
|
|
313
|
+
case "get_web_response": {
|
|
314
|
+
const taskId = requireNumberArg(args, "task_id");
|
|
315
|
+
return wrap(await client.request("GET", `/api/tasks/${taskId}/respond`));
|
|
316
|
+
}
|
|
317
|
+
case "submit_visual": {
|
|
318
|
+
const taskId = requireNumberArg(args, "task_id");
|
|
319
|
+
const body = { ...args };
|
|
320
|
+
delete body.task_id;
|
|
321
|
+
return wrap(await client.request("POST", `/api/tasks/${taskId}/visual`, body));
|
|
322
|
+
}
|
|
323
|
+
case "save_artifacts": {
|
|
324
|
+
const taskId = requireNumberArg(args, "task_id");
|
|
325
|
+
const body = { ...args };
|
|
326
|
+
delete body.task_id;
|
|
327
|
+
return wrap(await client.request("POST", `/api/tasks/${taskId}/artifacts`, body));
|
|
328
|
+
}
|
|
329
|
+
case "load_artifacts": {
|
|
330
|
+
const taskId = requireNumberArg(args, "task_id");
|
|
331
|
+
return wrap(await client.request("GET", `/api/tasks/${taskId}/artifacts`));
|
|
332
|
+
}
|
|
333
|
+
case "get_comments": {
|
|
334
|
+
const taskId = requireNumberArg(args, "task_id");
|
|
335
|
+
return wrap(await client.request("GET", `/api/tasks/${taskId}/comments`));
|
|
336
|
+
}
|
|
337
|
+
case "list_credentials":
|
|
338
|
+
return wrap(await client.request("GET", "/api/credentials"));
|
|
339
|
+
case "create_workflow":
|
|
340
|
+
return wrap(await client.request("POST", "/api/workflows", args));
|
|
341
|
+
case "update_workflow": {
|
|
342
|
+
const workflowId = requireNumberArg(args, "workflow_id");
|
|
343
|
+
const body = { ...args };
|
|
344
|
+
delete body.workflow_id;
|
|
345
|
+
return wrap(await client.request("PUT", `/api/workflows/${workflowId}`, body));
|
|
346
|
+
}
|
|
347
|
+
case "delete_workflow": {
|
|
348
|
+
const workflowId = requireNumberArg(args, "workflow_id");
|
|
349
|
+
return wrap(await client.request("DELETE", `/api/workflows/${workflowId}`));
|
|
350
|
+
}
|
|
351
|
+
case "save_findings": {
|
|
352
|
+
const taskId = requireNumberArg(args, "task_id");
|
|
353
|
+
const body = { findings: args.findings };
|
|
354
|
+
return wrap(await client.request("POST", `/api/tasks/${taskId}/findings`, body));
|
|
355
|
+
}
|
|
356
|
+
case "list_findings": {
|
|
357
|
+
const taskId = requireNumberArg(args, "task_id");
|
|
358
|
+
return wrap(await client.request("GET", `/api/tasks/${taskId}/findings`));
|
|
359
|
+
}
|
|
360
|
+
/*
|
|
361
|
+
scan_repo is the single local-execution exception in an otherwise REST-thin MCP wrapper.
|
|
362
|
+
The REST backend has no visibility into the agent's filesystem, so delegating static
|
|
363
|
+
pattern scans to it is impossible without an upload/clone model. Keeping this step
|
|
364
|
+
in-process preserves determinism and reproducibility of compliance scans. All other
|
|
365
|
+
tools must stay thin proxies — do not replicate this pattern elsewhere.
|
|
366
|
+
*/
|
|
367
|
+
case "list_folders": {
|
|
368
|
+
const qs = new URLSearchParams();
|
|
369
|
+
if (typeof args.parent_id === "number")
|
|
370
|
+
qs.set("parent_id", String(args.parent_id));
|
|
371
|
+
const suffix = qs.toString() ? `?${qs.toString()}` : "";
|
|
372
|
+
return wrap(await client.request("GET", `/api/folders${suffix}`));
|
|
373
|
+
}
|
|
374
|
+
case "create_folder":
|
|
375
|
+
return wrap(await client.request("POST", "/api/folders", args));
|
|
376
|
+
case "share_folder": {
|
|
377
|
+
const folderId = requireNumberArg(args, "folder_id");
|
|
378
|
+
return wrap(await client.request("POST", `/api/folders/${folderId}/shares`, {
|
|
379
|
+
group_id: args.group_id,
|
|
380
|
+
access_level: args.access_level,
|
|
381
|
+
}));
|
|
382
|
+
}
|
|
383
|
+
case "unshare_folder": {
|
|
384
|
+
const folderId = requireNumberArg(args, "folder_id");
|
|
385
|
+
const groupId = requireNumberArg(args, "group_id");
|
|
386
|
+
return wrap(await client.request("DELETE", `/api/folders/${folderId}/shares/${groupId}`));
|
|
387
|
+
}
|
|
388
|
+
case "move_workflow": {
|
|
389
|
+
const workflowId = requireNumberArg(args, "workflow_id");
|
|
390
|
+
const folderId = requireNumberArg(args, "folder_id");
|
|
391
|
+
return wrap(await client.request("PUT", `/api/workflows/${workflowId}`, {
|
|
392
|
+
folder_id: folderId,
|
|
393
|
+
}));
|
|
394
|
+
}
|
|
395
|
+
case "move_instruction": {
|
|
396
|
+
const instructionId = requireNumberArg(args, "instruction_id");
|
|
397
|
+
const folderId = requireNumberArg(args, "folder_id");
|
|
398
|
+
return wrap(await client.request("PUT", `/api/instructions/${instructionId}`, {
|
|
399
|
+
folder_id: folderId,
|
|
400
|
+
}));
|
|
401
|
+
}
|
|
402
|
+
case "transfer_workflow": {
|
|
403
|
+
const workflowId = requireNumberArg(args, "workflow_id");
|
|
404
|
+
const newOwnerId = requireNumberArg(args, "new_owner_id");
|
|
405
|
+
return wrap(await client.request("POST", `/api/workflows/${workflowId}/transfer`, { new_owner_id: newOwnerId }));
|
|
406
|
+
}
|
|
407
|
+
case "list_my_groups":
|
|
408
|
+
return wrap(await client.request("GET", "/api/auth/me/groups"));
|
|
409
|
+
case "scan_repo":
|
|
410
|
+
return await scanRepoLocal(args);
|
|
411
|
+
default:
|
|
412
|
+
return wrapError(`Unknown tool: ${name}`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
catch (error) {
|
|
416
|
+
if (error instanceof BlueKiwiAuthError) {
|
|
417
|
+
return wrap({
|
|
418
|
+
error: "auth_failed",
|
|
419
|
+
hint: "Run `npx bluekiwi status` to verify your config, or re-authenticate with `npx bluekiwi accept <new-token>`.",
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
if (error instanceof BlueKiwiApiError && error.status >= 500) {
|
|
423
|
+
return wrap({
|
|
424
|
+
error: "server_error",
|
|
425
|
+
status: error.status,
|
|
426
|
+
hint: `${apiUrl.replace(/\/$/, "")}/api/health`,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
if (error instanceof BlueKiwiNetworkError) {
|
|
430
|
+
return wrap({
|
|
431
|
+
error: "network_error",
|
|
432
|
+
message: error.message,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
return wrapError(error instanceof Error ? error.message : String(error));
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
const transport = new StdioServerTransport();
|
|
439
|
+
await server.connect(transport);
|
|
440
|
+
function tool(name, description, properties, required) {
|
|
441
|
+
return {
|
|
442
|
+
name,
|
|
443
|
+
description,
|
|
444
|
+
inputSchema: {
|
|
445
|
+
type: "object",
|
|
446
|
+
properties,
|
|
447
|
+
required,
|
|
448
|
+
},
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
function toArgs(args) {
|
|
452
|
+
return args ?? {};
|
|
453
|
+
}
|
|
454
|
+
function requireNumberArg(args, key) {
|
|
455
|
+
const value = args[key];
|
|
456
|
+
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
457
|
+
throw new Error(`${key} must be a number`);
|
|
458
|
+
}
|
|
459
|
+
return value;
|
|
460
|
+
}
|
|
461
|
+
function wrap(data) {
|
|
462
|
+
return {
|
|
463
|
+
content: [{ type: "text", text: JSON.stringify(data) }],
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
function wrapError(message) {
|
|
467
|
+
return {
|
|
468
|
+
content: [{ type: "text", text: JSON.stringify({ error: message }) }],
|
|
469
|
+
isError: true,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
async function scanRepoLocal(args) {
|
|
473
|
+
const scanPath = args["path"];
|
|
474
|
+
if (typeof scanPath !== "string" || scanPath.trim().length === 0) {
|
|
475
|
+
return wrapError("path must be a non-empty string");
|
|
476
|
+
}
|
|
477
|
+
if (scanPath.includes("\u0000")) {
|
|
478
|
+
return wrapError("path contains a null byte");
|
|
479
|
+
}
|
|
480
|
+
const rawRuleSet = args["rule_set"];
|
|
481
|
+
let ruleSet = "korea-ota-code";
|
|
482
|
+
if (rawRuleSet !== undefined) {
|
|
483
|
+
if (typeof rawRuleSet !== "string") {
|
|
484
|
+
return wrapError("rule_set must be a string");
|
|
485
|
+
}
|
|
486
|
+
if (rawRuleSet !== "korea-ota-code" && rawRuleSet !== "none") {
|
|
487
|
+
return wrapError("rule_set must be 'korea-ota-code' or 'none'");
|
|
488
|
+
}
|
|
489
|
+
ruleSet = rawRuleSet;
|
|
490
|
+
}
|
|
491
|
+
const rawIncludeGlobs = args["include_globs"];
|
|
492
|
+
let includeGlobs = SCAN_REPO_DEFAULT_GLOBS;
|
|
493
|
+
if (rawIncludeGlobs !== undefined) {
|
|
494
|
+
if (!Array.isArray(rawIncludeGlobs) ||
|
|
495
|
+
rawIncludeGlobs.some((glob) => typeof glob !== "string")) {
|
|
496
|
+
return wrapError("include_globs must be an array of strings");
|
|
497
|
+
}
|
|
498
|
+
includeGlobs = rawIncludeGlobs;
|
|
499
|
+
}
|
|
500
|
+
const rawMaxMatches = args["max_matches"];
|
|
501
|
+
let maxMatches = 200;
|
|
502
|
+
if (rawMaxMatches !== undefined) {
|
|
503
|
+
if (typeof rawMaxMatches !== "number" ||
|
|
504
|
+
Number.isNaN(rawMaxMatches) ||
|
|
505
|
+
!Number.isFinite(rawMaxMatches)) {
|
|
506
|
+
return wrapError("max_matches must be a number");
|
|
507
|
+
}
|
|
508
|
+
maxMatches = Math.min(1000, Math.max(1, Math.floor(rawMaxMatches)));
|
|
509
|
+
}
|
|
510
|
+
const rawTaskId = args["task_id"];
|
|
511
|
+
let taskId = null;
|
|
512
|
+
if (rawTaskId !== undefined) {
|
|
513
|
+
if (typeof rawTaskId !== "number" ||
|
|
514
|
+
Number.isNaN(rawTaskId) ||
|
|
515
|
+
!Number.isFinite(rawTaskId)) {
|
|
516
|
+
return wrapError("task_id must be a number");
|
|
517
|
+
}
|
|
518
|
+
taskId = rawTaskId;
|
|
519
|
+
}
|
|
520
|
+
const rawCustomPatterns = args["custom_patterns"];
|
|
521
|
+
const customPatterns = [];
|
|
522
|
+
if (rawCustomPatterns !== undefined) {
|
|
523
|
+
if (!Array.isArray(rawCustomPatterns)) {
|
|
524
|
+
return wrapError("custom_patterns must be an array");
|
|
525
|
+
}
|
|
526
|
+
for (const pattern of rawCustomPatterns) {
|
|
527
|
+
if (!pattern || typeof pattern !== "object") {
|
|
528
|
+
return wrapError("custom_patterns entries must be objects");
|
|
529
|
+
}
|
|
530
|
+
const entry = pattern;
|
|
531
|
+
const id = entry["id"];
|
|
532
|
+
const regex = entry["regex"];
|
|
533
|
+
const description = entry["description"];
|
|
534
|
+
const severity = entry["severity"];
|
|
535
|
+
if (typeof id !== "string" || id.trim().length === 0) {
|
|
536
|
+
return wrapError("custom_patterns entry id must be a non-empty string");
|
|
537
|
+
}
|
|
538
|
+
if (typeof regex !== "string" || regex.length === 0) {
|
|
539
|
+
return wrapError(`custom_patterns entry regex missing for id ${id}`);
|
|
540
|
+
}
|
|
541
|
+
if (description !== undefined && typeof description !== "string") {
|
|
542
|
+
return wrapError(`custom_patterns entry description must be a string for id ${id}`);
|
|
543
|
+
}
|
|
544
|
+
if (severity !== undefined &&
|
|
545
|
+
severity !== "BLOCK" &&
|
|
546
|
+
severity !== "REVIEW" &&
|
|
547
|
+
severity !== "WARN" &&
|
|
548
|
+
severity !== "INFO") {
|
|
549
|
+
return wrapError(`custom_patterns entry severity invalid for id ${id}`);
|
|
550
|
+
}
|
|
551
|
+
customPatterns.push({
|
|
552
|
+
id,
|
|
553
|
+
regex,
|
|
554
|
+
description: typeof description === "string" ? description : undefined,
|
|
555
|
+
severity: severity === undefined ? undefined : severity,
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
const workspaceCwd = path.resolve(process.cwd());
|
|
560
|
+
const resolvedScanPath = path.resolve(workspaceCwd, scanPath);
|
|
561
|
+
if (resolvedScanPath.includes("\u0000")) {
|
|
562
|
+
return wrapError("scan path contains a null byte");
|
|
563
|
+
}
|
|
564
|
+
const workspacePrefix = workspaceCwd.endsWith(path.sep)
|
|
565
|
+
? workspaceCwd
|
|
566
|
+
: workspaceCwd + path.sep;
|
|
567
|
+
if (resolvedScanPath !== workspaceCwd &&
|
|
568
|
+
!resolvedScanPath.startsWith(workspacePrefix)) {
|
|
569
|
+
return wrapError("scan path escapes workspace");
|
|
570
|
+
}
|
|
571
|
+
let scanStat;
|
|
572
|
+
try {
|
|
573
|
+
scanStat = await fs.promises.stat(resolvedScanPath);
|
|
574
|
+
}
|
|
575
|
+
catch {
|
|
576
|
+
return wrapError("scan path does not exist");
|
|
577
|
+
}
|
|
578
|
+
const compiledPatterns = [];
|
|
579
|
+
function normalizeFlags(flags) {
|
|
580
|
+
const flagSet = new Set(flags.split("").filter(Boolean));
|
|
581
|
+
flagSet.add("g");
|
|
582
|
+
const ordered = ["g", "i", "m", "s", "u", "y", "d"];
|
|
583
|
+
return ordered.filter((flag) => flagSet.has(flag)).join("");
|
|
584
|
+
}
|
|
585
|
+
if (ruleSet !== "none") {
|
|
586
|
+
for (const pattern of KOREA_OTA_PATTERNS) {
|
|
587
|
+
compiledPatterns.push({
|
|
588
|
+
id: pattern.id,
|
|
589
|
+
severity: pattern.severity,
|
|
590
|
+
description: pattern.description,
|
|
591
|
+
regex: new RegExp(pattern.source, normalizeFlags(pattern.flags)),
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
for (const pattern of customPatterns) {
|
|
596
|
+
try {
|
|
597
|
+
compiledPatterns.push({
|
|
598
|
+
id: pattern.id,
|
|
599
|
+
severity: pattern.severity ?? "INFO",
|
|
600
|
+
description: pattern.description ?? "",
|
|
601
|
+
regex: new RegExp(pattern.regex, "g"),
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
catch (error) {
|
|
605
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
606
|
+
return wrapError(`custom pattern failed to compile: ${pattern.id}: ${message}`);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
function toPosixPath(p) {
|
|
610
|
+
return p.split(path.sep).join("/");
|
|
611
|
+
}
|
|
612
|
+
function truncate(text, maxLen) {
|
|
613
|
+
return text.length <= maxLen ? text : text.slice(0, maxLen);
|
|
614
|
+
}
|
|
615
|
+
function globToRegExp(glob) {
|
|
616
|
+
function globPartToRegexSource(part) {
|
|
617
|
+
let out = "";
|
|
618
|
+
for (let i = 0; i < part.length; i += 1) {
|
|
619
|
+
const char = part[i];
|
|
620
|
+
if (char === "*") {
|
|
621
|
+
out += "[^/]*";
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
if (char === "{") {
|
|
625
|
+
const endIndex = part.indexOf("}", i + 1);
|
|
626
|
+
if (endIndex === -1) {
|
|
627
|
+
out += "\\{";
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
const inner = part.slice(i + 1, endIndex);
|
|
631
|
+
const options = inner.split(",").map((value) => value.trim());
|
|
632
|
+
const optionSources = options.map((option) => globPartToRegexSource(option));
|
|
633
|
+
out += `(?:${optionSources.join("|")})`;
|
|
634
|
+
i = endIndex;
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
if (/[\\^$+?.()|[\]{}]/.test(char)) {
|
|
638
|
+
out += `\\${char}`;
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
out += char;
|
|
642
|
+
}
|
|
643
|
+
return out;
|
|
644
|
+
}
|
|
645
|
+
return new RegExp(`^${globPartToRegexSource(glob)}$`);
|
|
646
|
+
}
|
|
647
|
+
const includeMatchers = includeGlobs.map((glob) => ({
|
|
648
|
+
matchBasename: !glob.includes("/"),
|
|
649
|
+
regex: globToRegExp(glob),
|
|
650
|
+
}));
|
|
651
|
+
const matches = [];
|
|
652
|
+
const byRule = {};
|
|
653
|
+
const bySeverity = {
|
|
654
|
+
BLOCK: 0,
|
|
655
|
+
REVIEW: 0,
|
|
656
|
+
WARN: 0,
|
|
657
|
+
INFO: 0,
|
|
658
|
+
};
|
|
659
|
+
let filesScanned = 0;
|
|
660
|
+
let filesSkipped = 0;
|
|
661
|
+
let truncatedOutput = false;
|
|
662
|
+
function shouldIncludeFile(relativePosixPath) {
|
|
663
|
+
if (includeMatchers.length === 0) {
|
|
664
|
+
return true;
|
|
665
|
+
}
|
|
666
|
+
const base = path.posix.basename(relativePosixPath);
|
|
667
|
+
return includeMatchers.some((matcher) => matcher.regex.test(matcher.matchBasename ? base : relativePosixPath));
|
|
668
|
+
}
|
|
669
|
+
async function scanFile(absolutePath) {
|
|
670
|
+
if (truncatedOutput) {
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
const relativePath = toPosixPath(path.relative(workspaceCwd, absolutePath));
|
|
674
|
+
if (!shouldIncludeFile(relativePath)) {
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
let fileStat;
|
|
678
|
+
try {
|
|
679
|
+
fileStat = await fs.promises.stat(absolutePath);
|
|
680
|
+
}
|
|
681
|
+
catch {
|
|
682
|
+
filesSkipped += 1;
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
if (fileStat.size > 1024 * 1024) {
|
|
686
|
+
filesSkipped += 1;
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
let content;
|
|
690
|
+
try {
|
|
691
|
+
content = await fs.promises.readFile(absolutePath, "utf8");
|
|
692
|
+
}
|
|
693
|
+
catch {
|
|
694
|
+
filesSkipped += 1;
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
filesScanned += 1;
|
|
698
|
+
const lines = content.split(/\r?\n/);
|
|
699
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
700
|
+
if (truncatedOutput) {
|
|
701
|
+
break;
|
|
702
|
+
}
|
|
703
|
+
const line = lines[lineIndex];
|
|
704
|
+
for (const pattern of compiledPatterns) {
|
|
705
|
+
if (truncatedOutput) {
|
|
706
|
+
break;
|
|
707
|
+
}
|
|
708
|
+
for (const match of line.matchAll(pattern.regex)) {
|
|
709
|
+
const matchedText = match[0] ?? "";
|
|
710
|
+
const index = match.index ?? 0;
|
|
711
|
+
matches.push({
|
|
712
|
+
rule_id: pattern.id,
|
|
713
|
+
severity: pattern.severity,
|
|
714
|
+
file: relativePath,
|
|
715
|
+
line: lineIndex + 1,
|
|
716
|
+
column: index + 1,
|
|
717
|
+
match: truncate(matchedText, 200),
|
|
718
|
+
snippet: truncate(line, 300),
|
|
719
|
+
description: pattern.description,
|
|
720
|
+
});
|
|
721
|
+
byRule[pattern.id] = (byRule[pattern.id] ?? 0) + 1;
|
|
722
|
+
bySeverity[pattern.severity] += 1;
|
|
723
|
+
if (matches.length >= maxMatches) {
|
|
724
|
+
truncatedOutput = true;
|
|
725
|
+
break;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
async function walkDirectory(directoryPath) {
|
|
732
|
+
if (truncatedOutput) {
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
let entries;
|
|
736
|
+
try {
|
|
737
|
+
entries = await fs.promises.readdir(directoryPath, {
|
|
738
|
+
withFileTypes: true,
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
catch {
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
745
|
+
for (const entry of entries) {
|
|
746
|
+
if (truncatedOutput) {
|
|
747
|
+
break;
|
|
748
|
+
}
|
|
749
|
+
if (entry.isDirectory()) {
|
|
750
|
+
if (SCAN_REPO_SKIP_DIRS.has(entry.name)) {
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
await walkDirectory(path.join(directoryPath, entry.name));
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
if (entry.isFile()) {
|
|
757
|
+
await scanFile(path.join(directoryPath, entry.name));
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
if (compiledPatterns.length === 0) {
|
|
762
|
+
return wrap({
|
|
763
|
+
scanned_path: scanStat.isDirectory()
|
|
764
|
+
? toPosixPath(path.relative(workspaceCwd, resolvedScanPath)) || "."
|
|
765
|
+
: toPosixPath(path.relative(workspaceCwd, resolvedScanPath)),
|
|
766
|
+
rule_set: ruleSet,
|
|
767
|
+
patterns_applied: 0,
|
|
768
|
+
files_scanned: 0,
|
|
769
|
+
files_skipped: 0,
|
|
770
|
+
total_matches: 0,
|
|
771
|
+
truncated: false,
|
|
772
|
+
by_rule: {},
|
|
773
|
+
by_severity: bySeverity,
|
|
774
|
+
matches: [],
|
|
775
|
+
task_id: taskId,
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
if (scanStat.isDirectory()) {
|
|
779
|
+
await walkDirectory(resolvedScanPath);
|
|
780
|
+
}
|
|
781
|
+
else if (scanStat.isFile()) {
|
|
782
|
+
await scanFile(resolvedScanPath);
|
|
783
|
+
}
|
|
784
|
+
else {
|
|
785
|
+
return wrapError("scan path must be a file or directory");
|
|
786
|
+
}
|
|
787
|
+
const relativeScannedPath = toPosixPath(path.relative(workspaceCwd, resolvedScanPath));
|
|
788
|
+
return wrap({
|
|
789
|
+
scanned_path: relativeScannedPath.length === 0 ? "." : relativeScannedPath,
|
|
790
|
+
rule_set: ruleSet,
|
|
791
|
+
patterns_applied: compiledPatterns.length,
|
|
792
|
+
files_scanned: filesScanned,
|
|
793
|
+
files_skipped: filesSkipped,
|
|
794
|
+
total_matches: matches.length,
|
|
795
|
+
truncated: truncatedOutput,
|
|
796
|
+
by_rule: byRule,
|
|
797
|
+
by_severity: bySeverity,
|
|
798
|
+
matches,
|
|
799
|
+
task_id: taskId,
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
function parseApiKeyFlag() {
|
|
803
|
+
const index = process.argv.indexOf("--api-key");
|
|
804
|
+
return index >= 0 ? process.argv[index + 1] : undefined;
|
|
805
|
+
}
|
package/dist/index.js
CHANGED
|
File without changes
|