codegate-ai 0.4.0 → 0.5.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/README.md CHANGED
@@ -89,15 +89,17 @@ codegate scan .
89
89
 
90
90
  ## Core Commands
91
91
 
92
- | Command | Purpose |
93
- | ----------------------- | ---------------------------------------------------------------------- |
94
- | `codegate scan [dir]` | Scan a directory for AI tool config risks. Defaults to `.`. |
95
- | `codegate run <tool>` | Scan current directory, then launch selected AI tool if policy allows. |
96
- | `codegate undo [dir]` | Restore the most recent remediation backup session. Defaults to `.`. |
97
- | `codegate init` | Create `~/.codegate/config.json` with defaults. |
98
- | `codegate update-kb` | Show knowledge-base update guidance. |
99
- | `codegate update-rules` | Show rules update guidance. |
100
- | `codegate --help` | Show CLI usage. |
92
+ | Command | Purpose |
93
+ | ------------------------ | ---------------------------------------------------------------------- |
94
+ | `codegate scan [target]` | Scan a directory, file, or URL target for AI tool config risks. |
95
+ | `codegate run <tool>` | Scan current directory, then launch selected AI tool if policy allows. |
96
+ | `codegate skills [...]` | Wrap `npx skills` and preflight-scan `skills add` targets. |
97
+ | `codegate clawhub [...]` | Wrap `npx clawhub` and preflight-scan `clawhub install` targets. |
98
+ | `codegate undo [dir]` | Restore the most recent remediation backup session. Defaults to `.`. |
99
+ | `codegate init` | Create `~/.codegate/config.json` with defaults. |
100
+ | `codegate update-kb` | Show knowledge-base update guidance. |
101
+ | `codegate update-rules` | Show rules update guidance. |
102
+ | `codegate --help` | Show CLI usage. |
101
103
 
102
104
  ## `scan` Command Flags
103
105
 
@@ -167,6 +169,39 @@ codegate run codex
167
169
  codegate run cursor
168
170
  ```
169
171
 
172
+ ## Installer Wrappers (`skills` and `clawhub`)
173
+
174
+ CodeGate also provides scan-first wrappers for skill installers:
175
+
176
+ - `codegate skills [skillsArgs...]` wraps `npx skills`.
177
+ - `codegate clawhub [clawhubArgs...]` wraps `npx clawhub`.
178
+
179
+ Behavior:
180
+
181
+ - For `skills add ...`, CodeGate resolves the requested source target, scans it, and only proceeds if policy allows.
182
+ - For `clawhub install ...`, CodeGate stages the remote skill content via `clawhub inspect`, scans the staged content, and only proceeds if policy allows.
183
+ - Dangerous findings block execution (fail-closed).
184
+ - Warning-level findings can still require confirmation unless `--cg-force` is provided.
185
+ - Non-install subcommands (for example `skills find` or `clawhub search`) are passed through without preflight scanning.
186
+
187
+ Wrapper flags (consumed by CodeGate, not forwarded):
188
+
189
+ - `--cg-force`
190
+ - `--cg-no-tui`
191
+ - `--cg-include-user-scope`
192
+ - `--cg-format <type>`
193
+ - `--cg-config <path>`
194
+
195
+ Examples:
196
+
197
+ ```bash
198
+ codegate skills add https://github.com/vercel-labs/skills --skill find-skills
199
+ codegate skills add https://github.com/owner/repo --skill security-review --cg-force
200
+ codegate clawhub install security-auditor
201
+ codegate clawhub install security-auditor --version 1.0.0
202
+ codegate clawhub search security
203
+ ```
204
+
170
205
  ## Deep Scan (Layer 3)
171
206
 
172
207
  Deep scan is opt-in and only runs with `--deep`.
package/dist/cli.d.ts CHANGED
@@ -9,6 +9,7 @@ import { type ResolvedScanTarget } from "./scan-target.js";
9
9
  import type { CodeGateReport } from "./types/report.js";
10
10
  import { type RemediationRunnerInput, type RemediationRunnerResult } from "./layer4-remediation/remediation-runner.js";
11
11
  import { type SkillsWrapperLaunchResult } from "./commands/skills-wrapper.js";
12
+ import { type ClawhubWrapperLaunchResult } from "./commands/clawhub-wrapper.js";
12
13
  import { type DeepAgentOption, type MetaAgentCommandConsentContext, type MetaAgentCommandRunResult, type RemediationConsentContext, type ScanRunnerInput } from "./commands/scan-command.js";
13
14
  export interface RunWarningConsentContext {
14
15
  target: string;
@@ -57,10 +58,15 @@ export interface CliDeps {
57
58
  requestSkillSelection?: (options: string[]) => Promise<string | null> | string | null;
58
59
  executeDeepResource?: (resource: DeepScanResource) => Promise<ResourceFetchResult>;
59
60
  launchSkills?: (args: string[], cwd: string) => SkillsWrapperLaunchResult;
61
+ launchClawhub?: (args: string[], cwd: string) => ClawhubWrapperLaunchResult;
60
62
  runSkillsWrapper?: (input: {
61
63
  version: string;
62
64
  skillsArgs: string[];
63
65
  }) => Promise<void>;
66
+ runClawhubWrapper?: (input: {
67
+ version: string;
68
+ clawhubArgs: string[];
69
+ }) => Promise<void>;
64
70
  runWrapper?: (input: {
65
71
  target: string;
66
72
  cwd: string;
package/dist/cli.js CHANGED
@@ -21,6 +21,7 @@ import { runRemediation as runRemediationWorkflow, } from "./layer4-remediation/
21
21
  import { undoLatestSession } from "./commands/undo.js";
22
22
  import { executeScanCommand } from "./commands/scan-command.js";
23
23
  import { executeSkillsWrapper, launchSkillsPassthrough, } from "./commands/skills-wrapper.js";
24
+ import { executeClawhubWrapper, launchClawhubPassthrough, } from "./commands/clawhub-wrapper.js";
24
25
  import { promptDeepAgentSelection, promptDeepScanConsent, promptMetaAgentCommandConsent, promptRemediationConsent, promptSkillSelection, } from "./cli-prompts.js";
25
26
  import { resetScanState } from "./layer2-static/state/scan-state.js";
26
27
  const require = createRequire(import.meta.url);
@@ -157,6 +158,7 @@ const defaultCliDeps = {
157
158
  return fetchResourceMetadata(resource.request);
158
159
  },
159
160
  launchSkills: (args, cwd) => launchSkillsPassthrough(args, cwd),
161
+ launchClawhub: (args, cwd) => launchClawhubPassthrough(args, cwd),
160
162
  };
161
163
  function addScanCommand(program, version, deps) {
162
164
  program
@@ -409,6 +411,47 @@ function addSkillsCommand(program, version, deps) {
409
411
  }
410
412
  });
411
413
  }
414
+ function addClawhubCommand(program, version, deps) {
415
+ program
416
+ .command("clawhub [clawhubArgs...]")
417
+ .description("Wrap npx clawhub with CodeGate preflight scanning for installs")
418
+ .allowUnknownOption(true)
419
+ .allowExcessArguments(true)
420
+ .addHelpText("after", renderExampleHelp([
421
+ "codegate clawhub install security-auditor",
422
+ "codegate clawhub install security-auditor --version 1.0.0",
423
+ "codegate clawhub search security",
424
+ "codegate clawhub install security-auditor --cg-force",
425
+ ]))
426
+ .action(async (clawhubArgs) => {
427
+ try {
428
+ const runClawhubWrapper = deps.runClawhubWrapper ??
429
+ ((input) => executeClawhubWrapper(input, {
430
+ cwd: deps.cwd,
431
+ isTTY: deps.isTTY,
432
+ pathExists: deps.pathExists,
433
+ resolveConfig: deps.resolveConfig,
434
+ runScan: deps.runScan,
435
+ resolveScanTarget: deps.resolveScanTarget,
436
+ requestWarningProceed: deps.requestRunWarningConsent,
437
+ launchClawhub: deps.launchClawhub ?? ((args, cwd) => launchClawhubPassthrough(args, cwd)),
438
+ stdout: deps.stdout,
439
+ stderr: deps.stderr,
440
+ setExitCode: deps.setExitCode,
441
+ renderTui: deps.renderTui,
442
+ }));
443
+ await runClawhubWrapper({
444
+ version,
445
+ clawhubArgs: clawhubArgs ?? [],
446
+ });
447
+ }
448
+ catch (error) {
449
+ const message = error instanceof Error ? error.message : String(error);
450
+ deps.stderr(`ClawHub wrapper failed: ${message}`);
451
+ deps.setExitCode(3);
452
+ }
453
+ });
454
+ }
412
455
  function addUndoCommand(program, deps) {
413
456
  program
414
457
  .command("undo [dir]")
@@ -513,10 +556,12 @@ export function createCli(version = packageJson.version ?? "0.0.0-dev", deps = d
513
556
  "codegate scan https://github.com/owner/repo",
514
557
  "codegate scan https://github.com/owner/repo/blob/main/skills/security-review/SKILL.md",
515
558
  "codegate skills add owner/repo --skill security-review",
559
+ "codegate clawhub install security-auditor",
516
560
  "codegate run claude",
517
561
  ]));
518
562
  addScanCommand(program, version, deps);
519
563
  addSkillsCommand(program, version, deps);
564
+ addClawhubCommand(program, version, deps);
520
565
  addRunCommand(program, version, deps);
521
566
  addUndoCommand(program, deps);
522
567
  addInitCommand(program, deps);
@@ -0,0 +1,67 @@
1
+ import { type CodeGateConfig, type OutputFormat, type ResolveConfigOptions } from "../config.js";
2
+ import { type ResolvedScanTarget } from "../scan-target.js";
3
+ import type { CodeGateReport } from "../types/report.js";
4
+ import type { ScanRunnerInput } from "./scan-command.js";
5
+ interface SourceDetectionContext {
6
+ cwd: string;
7
+ pathExists: (path: string) => boolean;
8
+ }
9
+ export interface ClawhubWrapperRuntimeOptions {
10
+ force: boolean;
11
+ noTui: boolean;
12
+ includeUserScope: boolean;
13
+ format?: OutputFormat;
14
+ configPath?: string;
15
+ }
16
+ export interface ParsedClawhubInvocation {
17
+ passthroughArgs: string[];
18
+ wrapper: ClawhubWrapperRuntimeOptions;
19
+ subcommand: string | null;
20
+ sourceTarget: string | null;
21
+ requestedVersion: string | null;
22
+ }
23
+ export interface ClawhubWrapperLaunchResult {
24
+ status: number | null;
25
+ error?: Error;
26
+ }
27
+ export interface ExecuteClawhubWrapperInput {
28
+ version: string;
29
+ clawhubArgs: string[];
30
+ }
31
+ export interface ClawhubWarningConsentContext {
32
+ target: string;
33
+ report: CodeGateReport;
34
+ }
35
+ export interface ClawhubWrapperDeps {
36
+ cwd: () => string;
37
+ isTTY: () => boolean;
38
+ pathExists?: (path: string) => boolean;
39
+ resolveConfig: (options: ResolveConfigOptions) => CodeGateConfig;
40
+ runScan: (input: ScanRunnerInput) => Promise<CodeGateReport>;
41
+ resolveScanTarget?: (input: {
42
+ rawTarget: string;
43
+ cwd: string;
44
+ preferredSkill?: string;
45
+ interactive?: boolean;
46
+ requestSkillSelection?: (options: string[]) => Promise<string | null> | string | null;
47
+ }) => Promise<ResolvedScanTarget> | ResolvedScanTarget;
48
+ stageClawhubTarget?: (input: {
49
+ sourceTarget: string;
50
+ requestedVersion?: string;
51
+ cwd: string;
52
+ }) => Promise<ResolvedScanTarget> | ResolvedScanTarget;
53
+ requestWarningProceed?: (context: ClawhubWarningConsentContext) => Promise<boolean> | boolean;
54
+ launchClawhub: (args: string[], cwd: string) => ClawhubWrapperLaunchResult;
55
+ stdout: (message: string) => void;
56
+ stderr: (message: string) => void;
57
+ setExitCode: (code: number) => void;
58
+ renderTui?: (props: {
59
+ view: "dashboard" | "summary";
60
+ report: CodeGateReport;
61
+ notices?: string[];
62
+ }) => void;
63
+ }
64
+ export declare function parseClawhubInvocation(rawArgs: string[], context?: SourceDetectionContext): ParsedClawhubInvocation;
65
+ export declare function launchClawhubPassthrough(args: string[], cwd: string): ClawhubWrapperLaunchResult;
66
+ export declare function executeClawhubWrapper(input: ExecuteClawhubWrapperInput, deps: ClawhubWrapperDeps): Promise<void>;
67
+ export {};
@@ -0,0 +1,612 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { createInterface } from "node:readline/promises";
6
+ import { applyConfigPolicy, OUTPUT_FORMATS, } from "../config.js";
7
+ import { reorderRequestedTargetFindings } from "../report/requested-target-findings.js";
8
+ import { resolveScanTarget } from "../scan-target.js";
9
+ import { renderByFormat, summarizeRequestedTargetFindings } from "./scan-command/helpers.js";
10
+ const CLAWHUB_GLOBAL_OPTIONS_WITH_VALUE = new Set(["--workdir", "--dir", "--site", "--registry"]);
11
+ const CLAWHUB_INSTALL_OPTIONS_WITH_VALUE = new Set(["--version"]);
12
+ const NPX_CLAWHUB_BASE_ARGS = ["--yes", "clawhub"];
13
+ function isRecord(value) {
14
+ return typeof value === "object" && value !== null && !Array.isArray(value);
15
+ }
16
+ function parseWrapperOptionValue(args, index, flag) {
17
+ const current = args[index] ?? "";
18
+ const withEquals = `${flag}=`;
19
+ if (current.startsWith(withEquals)) {
20
+ const value = current.slice(withEquals.length).trim();
21
+ if (value.length === 0) {
22
+ throw new Error(`${flag} requires a value`);
23
+ }
24
+ return [value, index];
25
+ }
26
+ const nextValue = args[index + 1];
27
+ if (!nextValue ||
28
+ nextValue.trim().length === 0 ||
29
+ nextValue === "--" ||
30
+ nextValue.startsWith("-")) {
31
+ throw new Error(`${flag} requires a value`);
32
+ }
33
+ return [nextValue, index + 1];
34
+ }
35
+ function parseOutputFormat(value) {
36
+ const normalized = value.trim().toLowerCase();
37
+ const matched = OUTPUT_FORMATS.find((format) => format === normalized);
38
+ if (!matched) {
39
+ throw new Error(`Unsupported --cg-format value "${value}". Valid values: ${OUTPUT_FORMATS.join(", ")}.`);
40
+ }
41
+ return matched;
42
+ }
43
+ function isLikelyHttpUrl(value) {
44
+ return /^https?:\/\//iu.test(value);
45
+ }
46
+ function splitLongOption(token) {
47
+ const equalsIndex = token.indexOf("=");
48
+ if (equalsIndex < 0) {
49
+ return [token, null];
50
+ }
51
+ return [token.slice(0, equalsIndex), token.slice(equalsIndex + 1)];
52
+ }
53
+ function isValueOption(flag) {
54
+ return (CLAWHUB_GLOBAL_OPTIONS_WITH_VALUE.has(flag) || CLAWHUB_INSTALL_OPTIONS_WITH_VALUE.has(flag));
55
+ }
56
+ function firstPositionalToken(args) {
57
+ for (let index = 0; index < args.length; index += 1) {
58
+ const token = args[index] ?? "";
59
+ if (token === "--") {
60
+ return [null, -1];
61
+ }
62
+ if (token.startsWith("--")) {
63
+ const [flag, inlineValue] = splitLongOption(token);
64
+ if (inlineValue === null && isValueOption(flag)) {
65
+ index += 1;
66
+ }
67
+ continue;
68
+ }
69
+ if (token.startsWith("-")) {
70
+ continue;
71
+ }
72
+ return [token.toLowerCase(), index];
73
+ }
74
+ return [null, -1];
75
+ }
76
+ function isLikelyLeadingGlobalOptionSequence(args, endExclusive) {
77
+ for (let index = 0; index < endExclusive; index += 1) {
78
+ const token = args[index] ?? "";
79
+ if (token === "--") {
80
+ return false;
81
+ }
82
+ if (token.startsWith("--")) {
83
+ const [flag, inlineValue] = splitLongOption(token);
84
+ if (inlineValue === null && isValueOption(flag)) {
85
+ index += 1;
86
+ }
87
+ continue;
88
+ }
89
+ if (token.startsWith("-")) {
90
+ continue;
91
+ }
92
+ return false;
93
+ }
94
+ return true;
95
+ }
96
+ function looksLikeSourceToken(value, context) {
97
+ if (value.trim().length === 0 || value.startsWith("-")) {
98
+ return false;
99
+ }
100
+ if (isLikelyHttpUrl(value) ||
101
+ value.startsWith("./") ||
102
+ value.startsWith("../") ||
103
+ value.startsWith("/") ||
104
+ value.startsWith("~/") ||
105
+ value.startsWith("~\\")) {
106
+ return true;
107
+ }
108
+ if (context) {
109
+ const localCandidate = resolve(context.cwd, value);
110
+ if (context.pathExists(localCandidate)) {
111
+ return true;
112
+ }
113
+ }
114
+ return true;
115
+ }
116
+ function firstLikelySourceAfterInstall(args, installIndex, context) {
117
+ for (let index = installIndex + 1; index < args.length; index += 1) {
118
+ const token = args[index] ?? "";
119
+ if (token === "--") {
120
+ for (let tailIndex = index + 1; tailIndex < args.length; tailIndex += 1) {
121
+ const candidate = args[tailIndex] ?? "";
122
+ if (!candidate.startsWith("-")) {
123
+ return candidate;
124
+ }
125
+ }
126
+ return null;
127
+ }
128
+ if (token.startsWith("--")) {
129
+ const [flag, inlineValue] = splitLongOption(token);
130
+ if (inlineValue === null && isValueOption(flag)) {
131
+ if (flag === "--version") {
132
+ index += 1;
133
+ continue;
134
+ }
135
+ index += 1;
136
+ }
137
+ continue;
138
+ }
139
+ if (token.startsWith("-")) {
140
+ continue;
141
+ }
142
+ if (looksLikeSourceToken(token, context)) {
143
+ // Heuristic to preserve forward compatibility with new ClawHub options:
144
+ // if this token follows a flag and another source-like token follows,
145
+ // treat this one as the option value and keep scanning.
146
+ const previous = index > installIndex + 1 ? (args[index - 1] ?? "") : "";
147
+ const next = args[index + 1] ?? "";
148
+ if (previous.startsWith("-") && looksLikeSourceToken(next, context)) {
149
+ continue;
150
+ }
151
+ return token;
152
+ }
153
+ }
154
+ return null;
155
+ }
156
+ function requestedVersionAfterInstall(args, installIndex) {
157
+ for (let index = installIndex + 1; index < args.length; index += 1) {
158
+ const token = args[index] ?? "";
159
+ if (token === "--") {
160
+ return null;
161
+ }
162
+ if (!token.startsWith("--")) {
163
+ continue;
164
+ }
165
+ const [flag, inlineValue] = splitLongOption(token);
166
+ if (flag !== "--version") {
167
+ continue;
168
+ }
169
+ if (inlineValue !== null) {
170
+ const value = inlineValue.trim();
171
+ return value.length > 0 ? value : null;
172
+ }
173
+ const next = args[index + 1] ?? "";
174
+ if (next.trim().length === 0 || next.startsWith("-")) {
175
+ return null;
176
+ }
177
+ return next.trim();
178
+ }
179
+ return null;
180
+ }
181
+ function findInstallSubcommandIndex(args, context) {
182
+ const [subcommand, subcommandIndex] = firstPositionalToken(args);
183
+ if (subcommand === "install") {
184
+ return subcommandIndex;
185
+ }
186
+ for (let index = 0; index < args.length; index += 1) {
187
+ const token = args[index]?.toLowerCase();
188
+ if (token !== "install") {
189
+ continue;
190
+ }
191
+ if (isLikelyLeadingGlobalOptionSequence(args, index) &&
192
+ firstLikelySourceAfterInstall(args, index, context)) {
193
+ return index;
194
+ }
195
+ }
196
+ return -1;
197
+ }
198
+ function normalizeSlashes(value) {
199
+ return value.replaceAll("\\", "/");
200
+ }
201
+ function sanitizeSlugForPath(slug) {
202
+ const segments = normalizeSlashes(slug)
203
+ .split("/")
204
+ .filter((segment) => segment.length > 0)
205
+ .map((segment) => {
206
+ const sanitized = segment.replace(/[^a-z0-9._-]/giu, "-");
207
+ // Prevent "." and ".." from altering join() resolution semantics.
208
+ return sanitized === "." || sanitized === ".." ? "_" : sanitized;
209
+ })
210
+ .filter((segment) => segment.length > 0);
211
+ return segments.length > 0 ? segments.join("/") : "skill";
212
+ }
213
+ function sanitizeRelativeRemotePath(value) {
214
+ const normalized = normalizeSlashes(value).replace(/^\/+/, "");
215
+ if (normalized.length === 0 || normalized.includes("\u0000")) {
216
+ throw new Error(`Invalid file path returned by clawhub inspect: ${value}`);
217
+ }
218
+ const segments = normalized.split("/");
219
+ if (segments.some((segment) => segment.length === 0 || segment === "." || segment === "..")) {
220
+ throw new Error(`Unsafe file path returned by clawhub inspect: ${value}`);
221
+ }
222
+ return segments.join("/");
223
+ }
224
+ function parseJsonObjectFromCliOutput(raw, context) {
225
+ const trimmed = raw.trim();
226
+ const start = trimmed.indexOf("{");
227
+ if (start < 0) {
228
+ throw new Error(`clawhub ${context} did not produce JSON output`);
229
+ }
230
+ const jsonCandidate = trimmed.slice(start);
231
+ let parsed;
232
+ try {
233
+ parsed = JSON.parse(jsonCandidate);
234
+ }
235
+ catch (error) {
236
+ const message = error instanceof Error ? error.message : String(error);
237
+ throw new Error(`Failed to parse clawhub ${context} JSON output: ${message}`, {
238
+ cause: error,
239
+ });
240
+ }
241
+ if (!isRecord(parsed)) {
242
+ throw new Error(`Unexpected clawhub ${context} JSON payload`);
243
+ }
244
+ return parsed;
245
+ }
246
+ function runClawhubCli(args, cwd) {
247
+ // --yes avoids interactive install prompts when npx needs to fetch clawhub.
248
+ const result = spawnSync("npx", [...NPX_CLAWHUB_BASE_ARGS, ...args], {
249
+ cwd,
250
+ encoding: "utf8",
251
+ });
252
+ if (result.error) {
253
+ throw result.error;
254
+ }
255
+ if (result.status !== 0) {
256
+ const stderr = (result.stderr ?? "").trim();
257
+ const stdout = (result.stdout ?? "").trim();
258
+ throw new Error(stderr.length > 0
259
+ ? stderr
260
+ : stdout.length > 0
261
+ ? stdout
262
+ : `npx clawhub ${args.join(" ")} failed`);
263
+ }
264
+ return result.stdout ?? "";
265
+ }
266
+ function extractInspectFiles(payload) {
267
+ const version = payload.version;
268
+ if (!isRecord(version)) {
269
+ return [];
270
+ }
271
+ const files = version.files;
272
+ if (!Array.isArray(files)) {
273
+ return [];
274
+ }
275
+ return files
276
+ .filter((item) => isRecord(item))
277
+ .map((item) => item.path)
278
+ .filter((path) => typeof path === "string" && path.trim().length > 0);
279
+ }
280
+ function extractInspectFileContent(payload, fallbackPath) {
281
+ const file = payload.file;
282
+ if (!isRecord(file)) {
283
+ throw new Error(`clawhub inspect --file ${fallbackPath} did not return file metadata`);
284
+ }
285
+ const content = file.content;
286
+ if (typeof content !== "string") {
287
+ throw new Error(`clawhub inspect --file ${fallbackPath} did not return textual file content`);
288
+ }
289
+ return content;
290
+ }
291
+ function canonicalClawhubUrlFromSlug(slug) {
292
+ const normalized = normalizeSlashes(slug).replace(/^\/+/, "").replace(/\/+$/, "");
293
+ return `https://clawhub.ai/${normalized}`;
294
+ }
295
+ function isLikelyLocalPathLike(value, context) {
296
+ if (value.startsWith("./") ||
297
+ value.startsWith("../") ||
298
+ value.startsWith("/") ||
299
+ value.startsWith("~/") ||
300
+ value.startsWith("~\\") ||
301
+ value.includes("\\")) {
302
+ return true;
303
+ }
304
+ return context.pathExists(resolve(context.cwd, value));
305
+ }
306
+ function extractClawhubSlugFromSource(source, context) {
307
+ const trimmed = source.trim();
308
+ if (trimmed.length === 0 || trimmed.startsWith("-")) {
309
+ return null;
310
+ }
311
+ if (isLikelyHttpUrl(trimmed)) {
312
+ try {
313
+ const url = new URL(trimmed);
314
+ if (!url.hostname.toLowerCase().endsWith("clawhub.ai")) {
315
+ return null;
316
+ }
317
+ const normalizedPath = url.pathname.replace(/^\/+/, "").replace(/\/+$/, "");
318
+ return normalizedPath.length > 0 ? decodeURIComponent(normalizedPath) : null;
319
+ }
320
+ catch {
321
+ return null;
322
+ }
323
+ }
324
+ if (isLikelyLocalPathLike(trimmed, context)) {
325
+ return null;
326
+ }
327
+ return trimmed;
328
+ }
329
+ async function stageClawhubSkillFromInspect(input) {
330
+ const versionArgs = input.version ? ["--version", input.version] : [];
331
+ const filesPayload = parseJsonObjectFromCliOutput(runClawhubCli(["inspect", "--json", "--files", ...versionArgs, input.slug], input.cwd), "inspect --files");
332
+ const filePaths = extractInspectFiles(filesPayload);
333
+ if (filePaths.length === 0) {
334
+ throw new Error(`clawhub inspect returned no files for ${input.slug}`);
335
+ }
336
+ const uniqueSortedFilePaths = [...new Set(filePaths)].sort((left, right) => left.localeCompare(right));
337
+ const tempRoot = mkdtempSync(join(tmpdir(), "codegate-scan-clawhub-"));
338
+ const stageRoot = join(tempRoot, "staged");
339
+ const stageSkillRoot = join(stageRoot, "skills", sanitizeSlugForPath(input.slug));
340
+ try {
341
+ for (const path of uniqueSortedFilePaths) {
342
+ const filePayload = parseJsonObjectFromCliOutput(runClawhubCli(["inspect", "--json", "--file", path, ...versionArgs, input.slug], input.cwd), `inspect --file ${path}`);
343
+ const content = extractInspectFileContent(filePayload, path);
344
+ const safeRelativePath = sanitizeRelativeRemotePath(path);
345
+ const destination = join(stageSkillRoot, safeRelativePath);
346
+ mkdirSync(dirname(destination), { recursive: true });
347
+ writeFileSync(destination, content, "utf8");
348
+ }
349
+ return {
350
+ scanTarget: stageRoot,
351
+ displayTarget: input.displayTarget,
352
+ cleanup: () => rmSync(tempRoot, { recursive: true, force: true }),
353
+ };
354
+ }
355
+ catch (error) {
356
+ rmSync(tempRoot, { recursive: true, force: true });
357
+ throw error;
358
+ }
359
+ }
360
+ async function stageClawhubTargetDefault(input, deps) {
361
+ const sourceContext = {
362
+ cwd: input.cwd,
363
+ pathExists: deps.pathExists,
364
+ };
365
+ const slug = extractClawhubSlugFromSource(input.sourceTarget, sourceContext);
366
+ if (slug) {
367
+ return stageClawhubSkillFromInspect({
368
+ slug,
369
+ version: input.requestedVersion,
370
+ cwd: input.cwd,
371
+ displayTarget: isLikelyHttpUrl(input.sourceTarget)
372
+ ? input.sourceTarget
373
+ : canonicalClawhubUrlFromSlug(slug),
374
+ });
375
+ }
376
+ return await deps.resolveScanTarget({
377
+ rawTarget: input.sourceTarget,
378
+ cwd: input.cwd,
379
+ });
380
+ }
381
+ export function parseClawhubInvocation(rawArgs, context) {
382
+ const wrapper = {
383
+ force: false,
384
+ noTui: false,
385
+ includeUserScope: false,
386
+ format: undefined,
387
+ configPath: undefined,
388
+ };
389
+ const passthroughArgs = [];
390
+ for (let index = 0; index < rawArgs.length; index += 1) {
391
+ const token = rawArgs[index] ?? "";
392
+ if (token === "--") {
393
+ passthroughArgs.push("--");
394
+ for (let tailIndex = index + 1; tailIndex < rawArgs.length; tailIndex += 1) {
395
+ passthroughArgs.push(rawArgs[tailIndex] ?? "");
396
+ }
397
+ break;
398
+ }
399
+ if (token === "--cg-force") {
400
+ wrapper.force = true;
401
+ continue;
402
+ }
403
+ if (token === "--cg-no-tui") {
404
+ wrapper.noTui = true;
405
+ continue;
406
+ }
407
+ if (token === "--cg-include-user-scope") {
408
+ wrapper.includeUserScope = true;
409
+ continue;
410
+ }
411
+ if (token === "--cg-format" || token.startsWith("--cg-format=")) {
412
+ const [value, consumedIndex] = parseWrapperOptionValue(rawArgs, index, "--cg-format");
413
+ wrapper.format = parseOutputFormat(value);
414
+ index = consumedIndex;
415
+ continue;
416
+ }
417
+ if (token === "--cg-config" || token.startsWith("--cg-config=")) {
418
+ const [value, consumedIndex] = parseWrapperOptionValue(rawArgs, index, "--cg-config");
419
+ wrapper.configPath = value;
420
+ index = consumedIndex;
421
+ continue;
422
+ }
423
+ if (token.startsWith("--cg-")) {
424
+ throw new Error(`Unknown CodeGate wrapper option: ${token}`);
425
+ }
426
+ passthroughArgs.push(token);
427
+ }
428
+ const installSubcommandIndex = findInstallSubcommandIndex(passthroughArgs, context);
429
+ const subcommand = installSubcommandIndex >= 0 ? "install" : firstPositionalToken(passthroughArgs)[0];
430
+ const sourceTarget = installSubcommandIndex >= 0
431
+ ? firstLikelySourceAfterInstall(passthroughArgs, installSubcommandIndex, context)
432
+ : null;
433
+ const requestedVersion = installSubcommandIndex >= 0
434
+ ? requestedVersionAfterInstall(passthroughArgs, installSubcommandIndex)
435
+ : null;
436
+ return {
437
+ passthroughArgs,
438
+ wrapper,
439
+ subcommand,
440
+ sourceTarget,
441
+ requestedVersion,
442
+ };
443
+ }
444
+ async function promptWarningProceed(context) {
445
+ const rl = createInterface({
446
+ input: process.stdin,
447
+ output: process.stdout,
448
+ });
449
+ const prompt = [
450
+ `Warning findings detected for ${context.target}.`,
451
+ `Findings: ${context.report.summary.total}`,
452
+ "Proceed with clawhub install? [y/N]: ",
453
+ ].join("\n");
454
+ try {
455
+ const answer = await rl.question(prompt);
456
+ return /^y(es)?$/iu.test(answer.trim());
457
+ }
458
+ finally {
459
+ rl.close();
460
+ }
461
+ }
462
+ function finalizeLaunch(result, deps) {
463
+ if (result.error) {
464
+ deps.stderr(`Failed to run npx clawhub: ${result.error.message}`);
465
+ deps.setExitCode(3);
466
+ return;
467
+ }
468
+ deps.setExitCode(result.status ?? 1);
469
+ }
470
+ function shouldPromptForWarning(report, config, force) {
471
+ return (report.summary.exit_code === 1 &&
472
+ report.findings.length > 0 &&
473
+ force !== true &&
474
+ config.auto_proceed_below_threshold !== true);
475
+ }
476
+ export function launchClawhubPassthrough(args, cwd) {
477
+ const result = spawnSync("npx", [...NPX_CLAWHUB_BASE_ARGS, ...args], {
478
+ cwd,
479
+ stdio: "inherit",
480
+ });
481
+ return {
482
+ status: result.status,
483
+ error: result.error ?? undefined,
484
+ };
485
+ }
486
+ export async function executeClawhubWrapper(input, deps) {
487
+ const cwd = deps.cwd();
488
+ const isTTY = deps.isTTY();
489
+ const pathExists = deps.pathExists ?? ((path) => existsSync(path));
490
+ const sourceDetectionContext = { cwd, pathExists };
491
+ const parsed = parseClawhubInvocation(input.clawhubArgs, sourceDetectionContext);
492
+ if (parsed.subcommand !== "install") {
493
+ finalizeLaunch(deps.launchClawhub(parsed.passthroughArgs, cwd), deps);
494
+ return;
495
+ }
496
+ const resolvedSourceTarget = parsed.sourceTarget;
497
+ if (!resolvedSourceTarget) {
498
+ deps.stderr("Could not determine the source target for `clawhub install`. Provide a skill slug or source target after `install`.");
499
+ deps.setExitCode(3);
500
+ return;
501
+ }
502
+ let resolvedTarget;
503
+ const interactivePromptsEnabled = isTTY && parsed.wrapper.noTui !== true;
504
+ try {
505
+ const resolveTarget = deps.resolveScanTarget ??
506
+ ((resolverInput) => resolveScanTarget(resolverInput));
507
+ const stageClawhubTarget = deps.stageClawhubTarget ??
508
+ ((stageInput) => stageClawhubTargetDefault(stageInput, {
509
+ pathExists,
510
+ resolveScanTarget: resolveTarget,
511
+ }));
512
+ resolvedTarget = await stageClawhubTarget({
513
+ sourceTarget: resolvedSourceTarget,
514
+ requestedVersion: parsed.requestedVersion ?? undefined,
515
+ cwd,
516
+ });
517
+ const noTui = parsed.wrapper.noTui === true || !isTTY;
518
+ const cliConfig = {
519
+ format: parsed.wrapper.format,
520
+ configPath: parsed.wrapper.configPath,
521
+ noTui,
522
+ };
523
+ const baseConfig = deps.resolveConfig({
524
+ scanTarget: resolvedTarget.scanTarget,
525
+ cli: cliConfig,
526
+ });
527
+ const config = parsed.wrapper.includeUserScope
528
+ ? { ...baseConfig, scan_user_scope: true }
529
+ : baseConfig;
530
+ let report = await deps.runScan({
531
+ version: input.version,
532
+ scanTarget: resolvedTarget.scanTarget,
533
+ config,
534
+ flags: {
535
+ noTui,
536
+ format: parsed.wrapper.format,
537
+ force: parsed.wrapper.force,
538
+ includeUserScope: parsed.wrapper.includeUserScope,
539
+ },
540
+ discoveryContext: undefined,
541
+ });
542
+ if (resolvedTarget.displayTarget && resolvedTarget.displayTarget !== report.scan_target) {
543
+ report = {
544
+ ...report,
545
+ scan_target: resolvedTarget.displayTarget,
546
+ };
547
+ }
548
+ report = applyConfigPolicy(report, config);
549
+ report = reorderRequestedTargetFindings(report, resolvedTarget.displayTarget);
550
+ const shouldUseTui = config.tui.enabled && isTTY && deps.renderTui !== undefined && noTui !== true;
551
+ const targetSummaryNote = config.output_format === "terminal"
552
+ ? summarizeRequestedTargetFindings(report, resolvedTarget.displayTarget)
553
+ : null;
554
+ if (shouldUseTui) {
555
+ deps.renderTui?.({
556
+ view: "dashboard",
557
+ report,
558
+ notices: targetSummaryNote ? [targetSummaryNote] : undefined,
559
+ });
560
+ deps.renderTui?.({ view: "summary", report });
561
+ }
562
+ else {
563
+ if (targetSummaryNote) {
564
+ deps.stdout(targetSummaryNote);
565
+ }
566
+ deps.stdout(renderByFormat(config.output_format, report));
567
+ }
568
+ if (report.summary.exit_code === 2 && parsed.wrapper.force !== true) {
569
+ deps.stderr("Dangerous findings detected. Aborting `clawhub install` (fail-closed).");
570
+ deps.setExitCode(2);
571
+ return;
572
+ }
573
+ if (shouldPromptForWarning(report, config, parsed.wrapper.force)) {
574
+ if (!interactivePromptsEnabled) {
575
+ deps.stderr("Warning findings detected. Aborting `clawhub install` in non-interactive mode (fail-closed). Use --cg-force to override.");
576
+ deps.setExitCode(1);
577
+ return;
578
+ }
579
+ const requestProceed = deps.requestWarningProceed ?? promptWarningProceed;
580
+ const approved = await requestProceed({
581
+ target: resolvedSourceTarget,
582
+ report,
583
+ });
584
+ if (!approved) {
585
+ deps.stderr("Install cancelled by user.");
586
+ deps.setExitCode(1);
587
+ return;
588
+ }
589
+ }
590
+ }
591
+ catch (error) {
592
+ const message = error instanceof Error ? error.message : String(error);
593
+ if (parsed.wrapper.force !== true) {
594
+ deps.stderr(`Preflight scan failed (fail-closed): ${message}`);
595
+ deps.setExitCode(3);
596
+ return;
597
+ }
598
+ deps.stderr(`Preflight scan failed, continuing due to --cg-force: ${message}`);
599
+ }
600
+ finally {
601
+ if (resolvedTarget?.cleanup) {
602
+ try {
603
+ await resolvedTarget.cleanup();
604
+ }
605
+ catch (error) {
606
+ const message = error instanceof Error ? error.message : String(error);
607
+ deps.stderr(`Scan target cleanup failed: ${message}`);
608
+ }
609
+ }
610
+ }
611
+ finalizeLaunch(deps.launchClawhub(parsed.passthroughArgs, cwd), deps);
612
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codegate-ai",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Pre-flight security scanner for AI coding tool configurations.",
5
5
  "license": "MIT",
6
6
  "type": "module",