kanzaki 0.2.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/dist/bin.js ADDED
@@ -0,0 +1,1106 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command } from "commander";
5
+ import chalk2 from "chalk";
6
+ import { existsSync as existsSync5, writeFileSync as writeFileSync3, readFileSync as readFileSync4, mkdirSync as mkdirSync3 } from "fs";
7
+ import { resolve as resolve4, dirname } from "path";
8
+ import { fileURLToPath } from "url";
9
+ import { createInterface } from "readline";
10
+ import { execSync as execSync2 } from "child_process";
11
+
12
+ // src/config.ts
13
+ import { existsSync as existsSync2 } from "fs";
14
+ import { resolve as resolve2 } from "path";
15
+ import dotenv from "dotenv";
16
+
17
+ // src/auth.ts
18
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
19
+ import { resolve } from "path";
20
+ import { homedir } from "os";
21
+ import { createServer } from "http";
22
+ import { randomBytes, createHash } from "crypto";
23
+ import open from "open";
24
+ var CONFIG_DIR = resolve(homedir(), ".config", "kanzaki");
25
+ var CREDENTIALS_PATH = resolve(CONFIG_DIR, "credentials.json");
26
+ function loadCredentials() {
27
+ if (!existsSync(CREDENTIALS_PATH)) return null;
28
+ try {
29
+ const raw = readFileSync(CREDENTIALS_PATH, "utf-8");
30
+ return JSON.parse(raw);
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+ function saveCredentials(credentials) {
36
+ if (!existsSync(CONFIG_DIR)) {
37
+ mkdirSync(CONFIG_DIR, { recursive: true });
38
+ }
39
+ writeFileSync(CREDENTIALS_PATH, JSON.stringify(credentials, null, 2), "utf-8");
40
+ }
41
+ function clearCredentials() {
42
+ if (existsSync(CREDENTIALS_PATH)) {
43
+ writeFileSync(CREDENTIALS_PATH, "{}", "utf-8");
44
+ }
45
+ }
46
+ function getActiveApiKey(creds) {
47
+ if (creds.useClaudeCli) {
48
+ return "claude-cli";
49
+ }
50
+ if (creds.oauthToken) {
51
+ if (!creds.expiresAt) {
52
+ return creds.oauthToken;
53
+ }
54
+ const expiry = new Date(creds.expiresAt);
55
+ if (expiry > /* @__PURE__ */ new Date()) {
56
+ return creds.oauthToken;
57
+ }
58
+ }
59
+ return creds.apiKey;
60
+ }
61
+ var OPENAI_AUTH_URL = "https://auth.openai.com/oauth/authorize";
62
+ var OPENAI_TOKEN_URL = "https://auth.openai.com/oauth/token";
63
+ var OPENAI_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
64
+ var REDIRECT_URI = "http://localhost:1455/auth/callback";
65
+ function generatePKCE() {
66
+ const verifier = randomBytes(32).toString("base64url");
67
+ const challenge = createHash("sha256").update(verifier).digest("base64url");
68
+ return { verifier, challenge };
69
+ }
70
+ async function loginWithOAuthPKCE() {
71
+ const { verifier, challenge } = generatePKCE();
72
+ const state = randomBytes(16).toString("base64url");
73
+ const scope = encodeURIComponent("openid profile email offline_access api.connectors.read api.connectors.invoke");
74
+ const authUrl = `${OPENAI_AUTH_URL}?response_type=code&client_id=${OPENAI_CLIENT_ID}&code_challenge=${challenge}&code_challenge_method=S256&redirect_uri=${encodeURIComponent(REDIRECT_URI)}&scope=${scope}&state=${state}&id_token_add_organizations=true&codex_cli_simplified_flow=true`;
75
+ console.log("Opening browser for authentication...");
76
+ console.log("If your browser does not open automatically, please open this link:");
77
+ console.log(authUrl);
78
+ try {
79
+ await open(authUrl);
80
+ } catch {
81
+ }
82
+ return new Promise((resolve5, reject) => {
83
+ const server = createServer(async (req, res) => {
84
+ try {
85
+ if (!req.url?.startsWith("/auth/callback")) {
86
+ res.writeHead(404);
87
+ res.end("Not found");
88
+ return;
89
+ }
90
+ const url = new URL(req.url, `http://${req.headers.host}`);
91
+ const error = url.searchParams.get("error");
92
+ const errorDesc = url.searchParams.get("error_description");
93
+ const code = url.searchParams.get("code");
94
+ const returnedState = url.searchParams.get("state");
95
+ if (error) {
96
+ res.writeHead(400, { "Content-Type": "text/html" });
97
+ res.end(`<h1>Authentication Failed</h1><p>${error}: ${errorDesc || "Unknown error"}</p>`);
98
+ server.close();
99
+ return reject(new Error(`OAuth error: ${error} - ${errorDesc}`));
100
+ }
101
+ if (!code) {
102
+ res.writeHead(400);
103
+ res.end("Missing authorization code");
104
+ server.close();
105
+ return reject(new Error("No authorization code received"));
106
+ }
107
+ if (returnedState !== state) {
108
+ res.writeHead(400);
109
+ res.end("Invalid state");
110
+ server.close();
111
+ return reject(new Error("OAuth state mismatch"));
112
+ }
113
+ res.writeHead(200, { "Content-Type": "text/html" });
114
+ res.end("<h1>Authentication Successful!</h1><p>You can close this tab and return to your terminal.</p>");
115
+ server.close();
116
+ const tokenRes = await fetch(OPENAI_TOKEN_URL, {
117
+ method: "POST",
118
+ headers: { "Content-Type": "application/json" },
119
+ body: JSON.stringify({
120
+ client_id: OPENAI_CLIENT_ID,
121
+ grant_type: "authorization_code",
122
+ code,
123
+ code_verifier: verifier,
124
+ redirect_uri: REDIRECT_URI
125
+ })
126
+ });
127
+ if (!tokenRes.ok) {
128
+ const body = await tokenRes.text();
129
+ return reject(new Error(`Token exchange failed: ${tokenRes.status} ${body}`));
130
+ }
131
+ const tokenData = await tokenRes.json();
132
+ resolve5(tokenData);
133
+ } catch (err) {
134
+ res.writeHead(500);
135
+ res.end("Internal Server Error");
136
+ server.close();
137
+ reject(err);
138
+ }
139
+ });
140
+ server.listen(1455, "localhost");
141
+ });
142
+ }
143
+
144
+ // src/config.ts
145
+ var DEFAULT_MODELS = {
146
+ openai: "gpt-5.4",
147
+ anthropic: "claude-sonnet-4-20250514"
148
+ };
149
+ function loadConfig(overrides = {}) {
150
+ const envPath = resolve2(process.cwd(), ".env");
151
+ if (existsSync2(envPath)) {
152
+ dotenv.config({ path: envPath });
153
+ }
154
+ const stored = loadCredentials();
155
+ const provider = overrides.provider ?? process.env.KANZAKI_PROVIDER ?? stored?.provider ?? "openai";
156
+ let apiKey = overrides.apiKey ?? process.env.KANZAKI_API_KEY ?? "";
157
+ if (!apiKey && stored) {
158
+ apiKey = getActiveApiKey(stored);
159
+ }
160
+ if (!apiKey) {
161
+ throw new Error(
162
+ "API key is required. Run 'kanzaki login' or set KANZAKI_API_KEY environment variable."
163
+ );
164
+ }
165
+ const model = overrides.model ?? process.env.KANZAKI_MODEL ?? DEFAULT_MODELS[provider] ?? "gpt-5.4";
166
+ const rulesPath = overrides.rulesPath ?? process.env.KANZAKI_RULES_PATH ?? ".kanzaki/rules.md";
167
+ return {
168
+ provider,
169
+ apiKey,
170
+ model,
171
+ rulesPath: resolve2(process.cwd(), rulesPath),
172
+ verbose: overrides.verbose ?? false,
173
+ noBlock: overrides.noBlock ?? false,
174
+ useOAuth: !!stored?.oauthToken,
175
+ useClaudeCli: !!stored?.useClaudeCli
176
+ };
177
+ }
178
+
179
+ // src/core/parser.ts
180
+ import { readFileSync as readFileSync2 } from "fs";
181
+ function parseRulesFile(filePath) {
182
+ const content = readFileSync2(filePath, "utf-8");
183
+ return parseRulesFromContent(content);
184
+ }
185
+ function parseRulesFromContent(content) {
186
+ const lines = content.split("\n");
187
+ const rules = [];
188
+ const contextLines = [];
189
+ const errors = [];
190
+ let currentGroup = "General";
191
+ let currentPatterns = [];
192
+ for (let i = 0; i < lines.length; i++) {
193
+ const line = lines[i];
194
+ const trimmed = line.trim();
195
+ const lineNumber = i + 1;
196
+ const headerMatch = trimmed.match(/^#{1,6}\s+(.+)$/);
197
+ if (headerMatch) {
198
+ const headerText = headerMatch[1].trim();
199
+ const { name, patterns } = parseHeaderWithPatterns(headerText);
200
+ currentGroup = name;
201
+ currentPatterns = patterns;
202
+ if (headerText.includes("(") && !headerText.includes(")")) {
203
+ errors.push({ line: lineNumber, message: `Missing closing parenthesis in file scope: "${headerText}"` });
204
+ }
205
+ const emptyParenMatch = headerText.match(/\(\s*\)/);
206
+ if (emptyParenMatch) {
207
+ errors.push({ line: lineNumber, message: `Empty file scope parentheses: "${headerText}". Remove "()" or specify glob patterns inside.` });
208
+ }
209
+ continue;
210
+ }
211
+ const emptyRuleMatch = trimmed.match(/^-\s*\[[\s]?\]\s*$/);
212
+ if (emptyRuleMatch) {
213
+ errors.push({ line: lineNumber, message: `Empty rule. Checklist item has no text.` });
214
+ continue;
215
+ }
216
+ const ruleMatch = trimmed.match(/^-\s*\[[\s]?\]\s+(.+)$/);
217
+ if (ruleMatch) {
218
+ const rawText = ruleMatch[1].trim();
219
+ const invalidTagMatch = rawText.match(/^!(err|warning|info|critical|block)\b/i);
220
+ if (invalidTagMatch) {
221
+ errors.push({ line: lineNumber, message: `Unknown severity tag "${invalidTagMatch[0]}". Use !error or !warn.` });
222
+ }
223
+ const { severity, text } = parseSeverity(rawText);
224
+ if (text.length === 0) {
225
+ errors.push({ line: lineNumber, message: `Empty rule. Checklist item has only a severity tag with no description.` });
226
+ continue;
227
+ }
228
+ rules.push({
229
+ group: currentGroup,
230
+ text,
231
+ severity,
232
+ filePatterns: currentPatterns,
233
+ lineNumber
234
+ });
235
+ continue;
236
+ }
237
+ if (trimmed.length > 0) {
238
+ if (trimmed.match(/^[-*+]\s*\[(.*?)\]/)) {
239
+ errors.push({ line: lineNumber, message: `Invalid rule format. Checkbox must be '- [ ]'. Found: "${trimmed}"` });
240
+ }
241
+ contextLines.push(trimmed);
242
+ }
243
+ }
244
+ const seen = /* @__PURE__ */ new Map();
245
+ for (const rule of rules) {
246
+ const key = `${rule.group}\0${rule.text.toLowerCase()}`;
247
+ const firstLine = seen.get(key);
248
+ if (firstLine !== void 0) {
249
+ errors.push({
250
+ line: rule.lineNumber ?? 0,
251
+ message: `Duplicate rule in group "${rule.group}" (first defined at line ${firstLine}): "${rule.text}"`
252
+ });
253
+ } else {
254
+ seen.set(key, rule.lineNumber ?? 0);
255
+ }
256
+ }
257
+ return {
258
+ rules,
259
+ context: contextLines.join("\n"),
260
+ errors
261
+ };
262
+ }
263
+ function parseHeaderWithPatterns(header) {
264
+ const match = header.match(/^(.+?)\s*\(([^)]+)\)\s*$/);
265
+ if (match) {
266
+ const name = match[1].trim();
267
+ const patterns = match[2].split(",").map((p) => p.trim()).filter(Boolean);
268
+ return { name, patterns };
269
+ }
270
+ return { name: header, patterns: [] };
271
+ }
272
+ function parseSeverity(rawText) {
273
+ const warnMatch = rawText.match(/^!warn\s+(.+)$/i);
274
+ if (warnMatch) {
275
+ return { severity: "warn", text: warnMatch[1].trim() };
276
+ }
277
+ const errorMatch = rawText.match(/^!error\s+(.+)$/i);
278
+ if (errorMatch) {
279
+ return { severity: "error", text: errorMatch[1].trim() };
280
+ }
281
+ return { severity: "error", text: rawText };
282
+ }
283
+ function formatRulesForPrompt(rules) {
284
+ const grouped = /* @__PURE__ */ new Map();
285
+ for (const rule of rules) {
286
+ const group = grouped.get(rule.group) ?? [];
287
+ group.push(rule);
288
+ grouped.set(rule.group, group);
289
+ }
290
+ const sections = [];
291
+ for (const [group, groupRules] of grouped) {
292
+ const scope = groupRules[0]?.filePatterns.length > 0 ? ` (applies to: ${groupRules[0].filePatterns.join(", ")})` : "";
293
+ sections.push(`### ${group}${scope}`);
294
+ for (let i = 0; i < groupRules.length; i++) {
295
+ const r = groupRules[i];
296
+ const tag = r.severity === "warn" ? "[WARNING]" : "[ERROR]";
297
+ sections.push(`${i + 1}. ${tag} ${r.text}`);
298
+ }
299
+ sections.push("");
300
+ }
301
+ return sections.join("\n");
302
+ }
303
+ function filterRulesByFiles(rules, changedFiles) {
304
+ return rules.filter((rule) => {
305
+ if (rule.filePatterns.length === 0) return true;
306
+ return changedFiles.some(
307
+ (file) => rule.filePatterns.some((pattern) => matchGlob(file, pattern))
308
+ );
309
+ });
310
+ }
311
+ function matchGlob(filePath, pattern) {
312
+ const regexStr = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "{{DOUBLE_STAR}}").replace(/\*/g, "[^/]*").replace(/{{DOUBLE_STAR}}/g, ".*").replace(/\?/g, "[^/]");
313
+ const regex = new RegExp(`(^|/)${regexStr}$`, "i");
314
+ return regex.test(filePath);
315
+ }
316
+
317
+ // src/core/git.ts
318
+ import { execSync } from "child_process";
319
+ import { readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
320
+ import { resolve as resolve3 } from "path";
321
+ function getStagedChanges() {
322
+ const diff = exec("git diff --staged");
323
+ const filesRaw = exec("git diff --staged --name-only");
324
+ const files = filesRaw.split("\n").map((f) => f.trim()).filter(Boolean);
325
+ return { diff, files };
326
+ }
327
+ function getFileContexts(files) {
328
+ const repoRoot = getRepoRoot();
329
+ const contexts = [];
330
+ for (const file of files) {
331
+ const absPath = resolve3(repoRoot, file);
332
+ if (!existsSync3(absPath)) continue;
333
+ if (isBinaryPath(file)) continue;
334
+ try {
335
+ const content = readFileSync3(absPath, "utf-8");
336
+ if (content.length > 1e5) continue;
337
+ contexts.push({ path: file, content });
338
+ } catch {
339
+ }
340
+ }
341
+ return contexts;
342
+ }
343
+ function getRepoRoot() {
344
+ return exec("git rev-parse --show-toplevel").trim();
345
+ }
346
+ function hasStagedChanges() {
347
+ const output = exec("git diff --staged --name-only");
348
+ return output.trim().length > 0;
349
+ }
350
+ function exec(command) {
351
+ try {
352
+ return execSync(command, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
353
+ } catch (error) {
354
+ const err = error;
355
+ throw new Error(`Git command failed: ${command}
356
+ ${err.stderr ?? err.message}`);
357
+ }
358
+ }
359
+ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
360
+ ".png",
361
+ ".jpg",
362
+ ".jpeg",
363
+ ".gif",
364
+ ".bmp",
365
+ ".ico",
366
+ ".webp",
367
+ ".svg",
368
+ ".mp4",
369
+ ".webm",
370
+ ".mov",
371
+ ".avi",
372
+ ".mp3",
373
+ ".wav",
374
+ ".ogg",
375
+ ".zip",
376
+ ".tar",
377
+ ".gz",
378
+ ".rar",
379
+ ".7z",
380
+ ".pdf",
381
+ ".doc",
382
+ ".docx",
383
+ ".xls",
384
+ ".xlsx",
385
+ ".woff",
386
+ ".woff2",
387
+ ".ttf",
388
+ ".eot",
389
+ ".otf",
390
+ ".exe",
391
+ ".dll",
392
+ ".so",
393
+ ".dylib",
394
+ ".lock"
395
+ ]);
396
+ function isBinaryPath(filePath) {
397
+ const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
398
+ return BINARY_EXTENSIONS.has(ext);
399
+ }
400
+
401
+ // src/llm/openai.ts
402
+ import OpenAI from "openai";
403
+ var CHATGPT_BASE_URL = "https://chatgpt.com/backend-api/codex";
404
+ var OpenAIProvider = class {
405
+ apiKey;
406
+ model;
407
+ useOAuth;
408
+ constructor(apiKey, model, useOAuth = false) {
409
+ this.apiKey = apiKey;
410
+ this.model = model;
411
+ this.useOAuth = useOAuth;
412
+ }
413
+ async review(systemPrompt, userPrompt) {
414
+ if (this.useOAuth) {
415
+ return this.reviewWithCodexBackend(systemPrompt, userPrompt);
416
+ }
417
+ return this.reviewWithChatCompletions(systemPrompt, userPrompt);
418
+ }
419
+ async reviewWithChatCompletions(systemPrompt, userPrompt) {
420
+ const client = new OpenAI({ apiKey: this.apiKey });
421
+ const response = await client.chat.completions.create({
422
+ model: this.model,
423
+ messages: [
424
+ { role: "system", content: systemPrompt },
425
+ { role: "user", content: userPrompt }
426
+ ],
427
+ response_format: { type: "json_object" },
428
+ temperature: 0.1
429
+ });
430
+ const content = response.choices[0]?.message?.content;
431
+ if (!content) {
432
+ throw new Error("OpenAI returned an empty response.");
433
+ }
434
+ return parseReviewResponse(content);
435
+ }
436
+ async reviewWithCodexBackend(systemPrompt, userPrompt) {
437
+ const url = `${CHATGPT_BASE_URL}/responses`;
438
+ const body = {
439
+ model: this.model,
440
+ instructions: systemPrompt,
441
+ input: [
442
+ { role: "user", content: userPrompt }
443
+ ],
444
+ store: false,
445
+ stream: true
446
+ };
447
+ const res = await fetch(url, {
448
+ method: "POST",
449
+ headers: {
450
+ "Content-Type": "application/json",
451
+ "Authorization": `Bearer ${this.apiKey}`
452
+ },
453
+ body: JSON.stringify(body)
454
+ });
455
+ if (!res.ok) {
456
+ const errorBody = await res.text();
457
+ throw new Error(`${res.status} ${errorBody}`);
458
+ }
459
+ const content = await this.readSSEStream(res);
460
+ return parseReviewResponse(content);
461
+ }
462
+ async readSSEStream(res) {
463
+ const reader = res.body?.getReader();
464
+ if (!reader) {
465
+ throw new Error("No response body");
466
+ }
467
+ const decoder = new TextDecoder();
468
+ let content = "";
469
+ let buffer = "";
470
+ while (true) {
471
+ const { done, value } = await reader.read();
472
+ if (done) break;
473
+ buffer += decoder.decode(value, { stream: true });
474
+ const lines = buffer.split("\n");
475
+ buffer = lines.pop() ?? "";
476
+ for (const line of lines) {
477
+ if (!line.startsWith("data: ")) continue;
478
+ const data = line.slice(6).trim();
479
+ if (data === "[DONE]") continue;
480
+ try {
481
+ const event = JSON.parse(data);
482
+ if (event.type === "response.output_text.delta" && event.delta) {
483
+ content += event.delta;
484
+ }
485
+ } catch {
486
+ }
487
+ }
488
+ }
489
+ if (!content) {
490
+ throw new Error("OpenAI returned an empty response from stream.");
491
+ }
492
+ return content;
493
+ }
494
+ };
495
+ function parseReviewResponse(raw) {
496
+ try {
497
+ const parsed = JSON.parse(raw);
498
+ if (!Array.isArray(parsed.results)) {
499
+ throw new Error("Response missing 'results' array.");
500
+ }
501
+ return {
502
+ results: parsed.results.map((r) => ({
503
+ rule: String(r.rule ?? ""),
504
+ passed: Boolean(r.passed),
505
+ reason: String(r.reason ?? "")
506
+ })),
507
+ summary: String(parsed.summary ?? "")
508
+ };
509
+ } catch (error) {
510
+ throw new Error(`Failed to parse LLM response as JSON:
511
+ ${raw}
512
+
513
+ ${error}`);
514
+ }
515
+ }
516
+
517
+ // src/llm/anthropic.ts
518
+ import Anthropic from "@anthropic-ai/sdk";
519
+ var AnthropicProvider = class {
520
+ client;
521
+ model;
522
+ constructor(apiKey, model) {
523
+ this.client = new Anthropic({ apiKey });
524
+ this.model = model;
525
+ }
526
+ async review(systemPrompt, userPrompt) {
527
+ const response = await this.client.messages.create({
528
+ model: this.model,
529
+ max_tokens: 4096,
530
+ system: systemPrompt,
531
+ messages: [{ role: "user", content: userPrompt }]
532
+ });
533
+ const textBlock = response.content.find((b) => b.type === "text");
534
+ if (!textBlock || textBlock.type !== "text") {
535
+ throw new Error("Anthropic returned no text content.");
536
+ }
537
+ return parseReviewResponse2(textBlock.text);
538
+ }
539
+ };
540
+ function parseReviewResponse2(raw) {
541
+ const jsonMatch = raw.match(/```(?:json)?\s*([\s\S]*?)```/);
542
+ const jsonStr = jsonMatch ? jsonMatch[1].trim() : raw.trim();
543
+ try {
544
+ const parsed = JSON.parse(jsonStr);
545
+ if (!Array.isArray(parsed.results)) {
546
+ throw new Error("Response missing 'results' array.");
547
+ }
548
+ return {
549
+ results: parsed.results.map((r) => ({
550
+ rule: String(r.rule ?? ""),
551
+ passed: Boolean(r.passed),
552
+ reason: String(r.reason ?? "")
553
+ })),
554
+ summary: String(parsed.summary ?? "")
555
+ };
556
+ } catch (error) {
557
+ throw new Error(`Failed to parse LLM response as JSON:
558
+ ${raw}
559
+
560
+ ${error}`);
561
+ }
562
+ }
563
+
564
+ // src/llm/claude-cli.ts
565
+ import { spawn } from "child_process";
566
+ var ClaudeCliProvider = class {
567
+ async review(systemPrompt, userPrompt) {
568
+ const combinedPrompt = `${systemPrompt}
569
+
570
+ ---
571
+
572
+ ${userPrompt}`;
573
+ const output = await this.runClaudeCli(combinedPrompt);
574
+ return parseReviewResponse3(output);
575
+ }
576
+ runClaudeCli(prompt) {
577
+ return new Promise((resolve5, reject) => {
578
+ const isWin = process.platform === "win32";
579
+ const child = isWin ? spawn("cmd.exe", ["/d", "/s", "/c", "claude -p"], {
580
+ stdio: ["pipe", "pipe", "pipe"],
581
+ windowsVerbatimArguments: true
582
+ }) : spawn("claude", ["-p"], {
583
+ stdio: ["pipe", "pipe", "pipe"]
584
+ });
585
+ let stdout = "";
586
+ let stderr = "";
587
+ child.stdout.on("data", (chunk) => {
588
+ stdout += chunk.toString("utf-8");
589
+ });
590
+ child.stderr.on("data", (chunk) => {
591
+ stderr += chunk.toString("utf-8");
592
+ });
593
+ child.on("error", (err) => {
594
+ reject(new Error(`Failed to spawn claude CLI: ${err.message}`));
595
+ });
596
+ child.on("close", (code) => {
597
+ if (code !== 0) {
598
+ reject(new Error(`claude CLI exited with code ${code}: ${stderr || stdout}`));
599
+ return;
600
+ }
601
+ resolve5(stdout);
602
+ });
603
+ child.stdin.write(prompt);
604
+ child.stdin.end();
605
+ });
606
+ }
607
+ };
608
+ function parseReviewResponse3(raw) {
609
+ const jsonMatch = raw.match(/```(?:json)?\s*([\s\S]*?)```/);
610
+ const jsonStr = jsonMatch ? jsonMatch[1].trim() : raw.trim();
611
+ try {
612
+ const parsed = JSON.parse(jsonStr);
613
+ if (!Array.isArray(parsed.results)) {
614
+ throw new Error("Response missing 'results' array.");
615
+ }
616
+ return {
617
+ results: parsed.results.map((r) => ({
618
+ rule: String(r.rule ?? ""),
619
+ passed: Boolean(r.passed),
620
+ reason: String(r.reason ?? "")
621
+ })),
622
+ summary: String(parsed.summary ?? "")
623
+ };
624
+ } catch (error) {
625
+ throw new Error(`Failed to parse Claude CLI response as JSON:
626
+ ${raw}
627
+
628
+ ${error}`);
629
+ }
630
+ }
631
+
632
+ // src/core/reviewer.ts
633
+ var SYSTEM_PROMPT = `You are a strict quality reviewer. Your job is to review changes (git diff) against a checklist of rules defined by the user.
634
+
635
+ The rules may cover ANY domain \u2014 code, documentation, research, writing, presentations, design, or any other type of output. Evaluate each rule based on its intent, not just literal text matching.
636
+
637
+ For each rule in the checklist, determine whether the staged changes comply with it.
638
+
639
+ IMPORTANT:
640
+ - Rules marked [ERROR] are critical. Be strict when evaluating them.
641
+ - Rules marked [WARNING] are advisory. Be fair but flag clear violations.
642
+ - Only evaluate rules that are RELEVANT to the changes. If a rule clearly does not apply to the content being changed, mark it as passed with reason "Not applicable to these changes."
643
+ - Use the context section (if provided) to understand the project's goals and constraints.
644
+ - Look at both the diff AND the full file context to make accurate judgments.
645
+
646
+ You MUST respond with valid JSON in this exact format:
647
+ {
648
+ "results": [
649
+ { "rule": "<exact rule text>", "passed": true, "reason": "<brief explanation>" },
650
+ { "rule": "<exact rule text>", "passed": false, "reason": "<specific explanation of the violation>" }
651
+ ],
652
+ "summary": "<one-line overall summary>"
653
+ }`;
654
+ async function review(config, rules, staged, fileContexts, rulesContext) {
655
+ const provider = createProvider(config);
656
+ const userPrompt = buildUserPrompt(rules, staged, fileContexts, rulesContext);
657
+ const rawResult = await provider.review(SYSTEM_PROMPT, userPrompt);
658
+ return mapSeverities(rawResult, rules);
659
+ }
660
+ function createProvider(config) {
661
+ switch (config.provider) {
662
+ case "openai":
663
+ return new OpenAIProvider(config.apiKey, config.model, config.useOAuth);
664
+ case "anthropic":
665
+ if (config.useClaudeCli) {
666
+ return new ClaudeCliProvider();
667
+ }
668
+ return new AnthropicProvider(config.apiKey, config.model);
669
+ default:
670
+ throw new Error(`Unknown provider: ${config.provider}`);
671
+ }
672
+ }
673
+ function buildUserPrompt(rules, staged, fileContexts, rulesContext) {
674
+ const parts = [];
675
+ if (rulesContext && rulesContext.trim().length > 0) {
676
+ parts.push("## Project Context\n");
677
+ parts.push(rulesContext);
678
+ parts.push("");
679
+ }
680
+ parts.push("## Checklist Rules\n");
681
+ parts.push(formatRulesForPrompt(rules));
682
+ parts.push("## Staged Changes (git diff)\n");
683
+ parts.push("```diff");
684
+ parts.push(truncate(staged.diff, 5e4));
685
+ parts.push("```\n");
686
+ if (fileContexts.length > 0) {
687
+ parts.push("## Full File Context\n");
688
+ parts.push("The following are the full contents of modified files for additional context:\n");
689
+ for (const ctx of fileContexts) {
690
+ const ext = ctx.path.split(".").pop() ?? "";
691
+ parts.push(`### ${ctx.path}
692
+ `);
693
+ parts.push(`\`\`\`${ext}`);
694
+ parts.push(truncate(ctx.content, 2e4));
695
+ parts.push("```\n");
696
+ }
697
+ }
698
+ return parts.join("\n");
699
+ }
700
+ function mapSeverities(result, rules) {
701
+ const severityMap = /* @__PURE__ */ new Map();
702
+ for (const rule of rules) {
703
+ severityMap.set(rule.text.toLowerCase(), rule.severity);
704
+ }
705
+ return {
706
+ ...result,
707
+ results: result.results.map((r) => ({
708
+ ...r,
709
+ severity: severityMap.get(r.rule.toLowerCase()) ?? "error"
710
+ }))
711
+ };
712
+ }
713
+ function truncate(text, maxLength) {
714
+ if (text.length <= maxLength) return text;
715
+ return text.slice(0, maxLength) + "\n\n... (truncated)";
716
+ }
717
+
718
+ // src/core/reporter.ts
719
+ import chalk from "chalk";
720
+ function report(result, verbose) {
721
+ const { results, summary } = result;
722
+ console.log();
723
+ console.log(chalk.bold.underline("Kanzaki Review Results"));
724
+ console.log();
725
+ let errorCount = 0;
726
+ let warnCount = 0;
727
+ for (const r of results) {
728
+ const isWarn = r.severity === "warn";
729
+ const label = isWarn ? chalk.dim("[warn]") : chalk.dim("[error]");
730
+ if (r.passed) {
731
+ console.log(` ${chalk.green("\u2713")} ${label} ${r.rule}`);
732
+ if (verbose) {
733
+ console.log(` ${chalk.dim(r.reason)}`);
734
+ }
735
+ } else {
736
+ if (isWarn) {
737
+ warnCount++;
738
+ console.log(` ${chalk.yellow("\u26A0")} ${label} ${r.rule}`);
739
+ console.log(` ${chalk.yellow("\u2192")} ${r.reason}`);
740
+ } else {
741
+ errorCount++;
742
+ console.log(` ${chalk.red("\u2717")} ${label} ${r.rule}`);
743
+ console.log(` ${chalk.red("\u2192")} ${r.reason}`);
744
+ }
745
+ }
746
+ }
747
+ console.log();
748
+ const total = results.length;
749
+ const passedCount = total - errorCount - warnCount;
750
+ if (errorCount === 0 && warnCount === 0) {
751
+ console.log(chalk.green.bold(` All ${total} rules passed \u2713`));
752
+ } else {
753
+ const parts = [];
754
+ parts.push(`${passedCount}/${total} passed`);
755
+ if (errorCount > 0) parts.push(chalk.red(`${errorCount} errors`));
756
+ if (warnCount > 0) parts.push(chalk.yellow(`${warnCount} warnings`));
757
+ console.log(` ${parts.join(", ")}`);
758
+ if (errorCount > 0) {
759
+ console.log(chalk.red.bold("\n Commit blocked due to errors."));
760
+ } else {
761
+ console.log(chalk.yellow("\n Warnings found, but commit allowed."));
762
+ }
763
+ }
764
+ if (summary) {
765
+ console.log();
766
+ console.log(chalk.dim(` ${summary}`));
767
+ }
768
+ console.log();
769
+ return { errorCount, warnCount };
770
+ }
771
+
772
+ // src/core/feedback.ts
773
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, existsSync as existsSync4 } from "fs";
774
+ import { join } from "path";
775
+ function writeFeedbackFile(result, rules, staged, outputDir) {
776
+ const failures = result.results.filter((r) => !r.passed);
777
+ if (failures.length === 0) return null;
778
+ if (!existsSync4(outputDir)) {
779
+ mkdirSync2(outputDir, { recursive: true });
780
+ }
781
+ const now = /* @__PURE__ */ new Date();
782
+ const filePath = join(outputDir, `${formatTimestamp(now)}.md`);
783
+ const content = buildFeedbackMarkdown(failures, rules, staged, now, result.summary);
784
+ writeFileSync2(filePath, content, "utf-8");
785
+ return filePath;
786
+ }
787
+ function buildFeedbackMarkdown(failures, rules, staged, now, summary) {
788
+ const ruleMap = /* @__PURE__ */ new Map();
789
+ for (const r of rules) {
790
+ ruleMap.set(r.text.toLowerCase(), r);
791
+ }
792
+ const errorCount = failures.filter((f) => f.severity === "error").length;
793
+ const warnCount = failures.filter((f) => f.severity === "warn").length;
794
+ const lines = [];
795
+ lines.push(`# Kanzaki Review Feedback`);
796
+ lines.push(``);
797
+ lines.push(`Generated: ${now.toISOString()}`);
798
+ lines.push(``);
799
+ lines.push(`## Summary`);
800
+ lines.push(``);
801
+ if (summary) {
802
+ lines.push(summary);
803
+ lines.push(``);
804
+ }
805
+ lines.push(`- \u30A8\u30E9\u30FC: ${errorCount} \u4EF6`);
806
+ lines.push(`- \u8B66\u544A: ${warnCount} \u4EF6`);
807
+ lines.push(`- \u5909\u66F4\u30D5\u30A1\u30A4\u30EB: ${staged.files.join(", ")}`);
808
+ lines.push(``);
809
+ lines.push(`## Instructions for Coding Agents`);
810
+ lines.push(``);
811
+ lines.push(`\u4EE5\u4E0B\u306F \`kanzaki check\` \u304C\u691C\u51FA\u3057\u305F\u30EB\u30FC\u30EB\u9055\u53CD\u3067\u3059\u3002\u5404\u9055\u53CD\u306B\u3064\u3044\u3066\u3001\u6307\u6458\u5185\u5BB9\u3092\u8AAD\u307F\u3001\u5BFE\u8C61\u30D5\u30A1\u30A4\u30EB\u306E\u8A72\u5F53\u7B87\u6240\u3092\u4FEE\u6B63\u3057\u3066\u304F\u3060\u3055\u3044\u3002`);
812
+ lines.push(`\u4FEE\u6B63\u5F8C\u306F \`git add\` \u3067\u518D\u5EA6\u30B9\u30C6\u30FC\u30B8\u3057\u3001\`kanzaki check\` \u3092\u5B9F\u884C\u3057\u3066\u9055\u53CD\u304C\u89E3\u6D88\u3055\u308C\u305F\u3053\u3068\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002`);
813
+ lines.push(`\u30EB\u30FC\u30EB\u5B9A\u7FA9\u305D\u306E\u3082\u306E\u3092\u5909\u66F4\u3059\u308B\u3053\u3068\u3067\u9055\u53CD\u3092\u56DE\u907F\u3059\u308B\u3053\u3068\u306F\u7981\u6B62\u3055\u308C\u3066\u3044\u307E\u3059\uFF08\u30EB\u30FC\u30EB\u306E\u5909\u66F4\u304C\u5FC5\u8981\u306A\u5834\u5408\u306F\u30E6\u30FC\u30B6\u30FC\u306B\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\uFF09\u3002`);
814
+ lines.push(``);
815
+ lines.push(`## Violations`);
816
+ lines.push(``);
817
+ failures.forEach((f, idx) => {
818
+ const rule = ruleMap.get(f.rule.toLowerCase());
819
+ const tag = f.severity === "warn" ? "[WARN]" : "[ERROR]";
820
+ lines.push(`### ${idx + 1}. ${tag} ${f.rule}`);
821
+ lines.push(``);
822
+ if (rule) {
823
+ lines.push(`- **\u30B0\u30EB\u30FC\u30D7**: ${rule.group}`);
824
+ const scope = rule.filePatterns.length > 0 ? rule.filePatterns.join(", ") : "\u5168\u30D5\u30A1\u30A4\u30EB";
825
+ lines.push(`- **\u9069\u7528\u30B9\u30B3\u30FC\u30D7**: ${scope}`);
826
+ if (rule.lineNumber) {
827
+ lines.push(`- **\u30EB\u30FC\u30EB\u5B9A\u7FA9**: rules.md:${rule.lineNumber}`);
828
+ }
829
+ }
830
+ lines.push(`- **\u5909\u66F4\u30D5\u30A1\u30A4\u30EB**: ${staged.files.join(", ")}`);
831
+ lines.push(``);
832
+ lines.push(`**\u9055\u53CD\u7406\u7531**:`);
833
+ lines.push(``);
834
+ const reason = f.reason.trim() || "(no reason provided)";
835
+ for (const line of reason.split("\n")) {
836
+ lines.push(`> ${line}`);
837
+ }
838
+ lines.push(``);
839
+ });
840
+ return lines.join("\n");
841
+ }
842
+ function formatTimestamp(d) {
843
+ const pad = (n) => String(n).padStart(2, "0");
844
+ const y = d.getFullYear();
845
+ const mo = pad(d.getMonth() + 1);
846
+ const da = pad(d.getDate());
847
+ const h = pad(d.getHours());
848
+ const mi = pad(d.getMinutes());
849
+ const s = pad(d.getSeconds());
850
+ return `${y}-${mo}-${da}T${h}-${mi}-${s}`;
851
+ }
852
+
853
+ // src/cli.ts
854
+ var __filename = fileURLToPath(import.meta.url);
855
+ var __dirname = dirname(__filename);
856
+ function createCli() {
857
+ const program2 = new Command();
858
+ program2.name("kanzaki").description("LLM-powered semantic pre-commit linter").version("0.2.0");
859
+ program2.command("init").description("Create .kanzaki/rules.md rules file").action(async () => {
860
+ const cwd = process.cwd();
861
+ const rulesPath = resolve4(cwd, ".kanzaki", "rules.md");
862
+ const rulesDir = dirname(rulesPath);
863
+ if (!existsSync5(rulesDir)) {
864
+ mkdirSync3(rulesDir, { recursive: true });
865
+ }
866
+ if (existsSync5(rulesPath)) {
867
+ console.log(chalk2.yellow("\u26A0 .kanzaki/rules.md already exists, skipping."));
868
+ } else {
869
+ const template = loadTemplate();
870
+ writeFileSync3(rulesPath, template, "utf-8");
871
+ console.log(chalk2.green("\u2713 Created .kanzaki/rules.md"));
872
+ }
873
+ const kanzakiGitignore = resolve4(rulesDir, ".gitignore");
874
+ if (!existsSync5(kanzakiGitignore)) {
875
+ writeFileSync3(kanzakiGitignore, "reviews/\n", "utf-8");
876
+ console.log(chalk2.green("\u2713 Created .kanzaki/.gitignore"));
877
+ }
878
+ console.log();
879
+ console.log(chalk2.dim("Edit .kanzaki/rules.md to customize your review rules."));
880
+ console.log(chalk2.dim("You can run 'kanzaki check' directly, or set it up with husky/lint-staged."));
881
+ console.log(chalk2.dim("Run 'kanzaki login' to authenticate."));
882
+ });
883
+ program2.command("check").description("Review staged changes against rules").option("-p, --provider <provider>", "LLM provider (openai / anthropic)").option("-m, --model <model>", "Model name").option("-r, --rules <path>", "Path to rules file", ".kanzaki/rules.md").option("--api-key <key>", "API key (prefer KANZAKI_API_KEY env var)").option("--no-block", "Warn only, don't block commit").option("-o, --emit-feedback", "Write feedback markdown (for coding agents) to .kanzaki/reviews/").option("-v, --verbose", "Verbose output").action(async (opts) => {
884
+ try {
885
+ if (!hasStagedChanges()) {
886
+ console.log(chalk2.yellow("No staged changes found. Nothing to review."));
887
+ process.exit(0);
888
+ }
889
+ const config = loadConfig({
890
+ provider: opts.provider,
891
+ apiKey: opts.apiKey,
892
+ model: opts.model,
893
+ rulesPath: opts.rules,
894
+ verbose: opts.verbose ?? false,
895
+ noBlock: opts.noBlock === false
896
+ // commander の --no-block は block=false にする
897
+ });
898
+ if (!existsSync5(config.rulesPath)) {
899
+ console.error(chalk2.red(`Rules file not found: ${config.rulesPath}`));
900
+ console.error(chalk2.dim("Run 'kanzaki init' to create one."));
901
+ process.exit(1);
902
+ }
903
+ const { rules, context: rulesContext, errors: parseErrors } = parseRulesFile(config.rulesPath);
904
+ if (parseErrors && parseErrors.length > 0) {
905
+ console.error(chalk2.red.bold(`
906
+ \u274C Found ${parseErrors.length} formatting error(s) in ${config.rulesPath}:`));
907
+ parseErrors.forEach((err) => {
908
+ console.error(chalk2.yellow(` Line ${err.line}: `) + err.message);
909
+ });
910
+ console.error(chalk2.dim("\nPlease fix these errors before committing."));
911
+ process.exit(1);
912
+ }
913
+ if (rules.length === 0) {
914
+ console.log(chalk2.yellow("No rules found in rules file. Skipping review."));
915
+ process.exit(0);
916
+ }
917
+ const errorRules = rules.filter((r) => r.severity === "error").length;
918
+ const warnRules = rules.filter((r) => r.severity === "warn").length;
919
+ if (config.verbose) {
920
+ console.log(chalk2.dim(`Provider: ${config.provider} (${config.model})`));
921
+ console.log(chalk2.dim(`Rules: ${errorRules} errors, ${warnRules} warnings`));
922
+ if (rulesContext) {
923
+ console.log(chalk2.dim(`Context: ${rulesContext.length} chars of additional context`));
924
+ }
925
+ }
926
+ const staged = getStagedChanges();
927
+ const fileContexts = getFileContexts(staged.files);
928
+ const applicableRules = filterRulesByFiles(rules, staged.files);
929
+ if (applicableRules.length === 0) {
930
+ console.log(chalk2.yellow("No applicable rules for changed files. Skipping review."));
931
+ process.exit(0);
932
+ }
933
+ if (config.verbose) {
934
+ console.log(chalk2.dim(`Files changed: ${staged.files.join(", ")}`));
935
+ if (applicableRules.length < rules.length) {
936
+ console.log(chalk2.dim(`Rules filtered: ${applicableRules.length}/${rules.length} applicable`));
937
+ }
938
+ }
939
+ console.log(chalk2.dim("Reviewing changes with LLM..."));
940
+ const result = await review(config, applicableRules, staged, fileContexts, rulesContext);
941
+ const { errorCount } = report(result, config.verbose);
942
+ if (opts.emitFeedback) {
943
+ const rulesDir = dirname(resolve4(config.rulesPath));
944
+ const reviewsDir = resolve4(rulesDir, "reviews");
945
+ const feedbackPath = writeFeedbackFile(result, applicableRules, staged, reviewsDir);
946
+ if (feedbackPath) {
947
+ console.log(chalk2.dim(`\u2192 Feedback written to ${feedbackPath}`));
948
+ }
949
+ }
950
+ if (errorCount > 0 && !config.noBlock) {
951
+ process.exit(1);
952
+ }
953
+ } catch (error) {
954
+ console.error(chalk2.red(`Error: ${error.message}`));
955
+ process.exit(1);
956
+ }
957
+ });
958
+ const SUPPORTED_PROVIDERS = ["openai", "anthropic"];
959
+ program2.command("login").description("Authenticate with a supported LLM provider").option("-p, --provider <provider>", `Provider to use (${SUPPORTED_PROVIDERS.join(" / ")})`).option("--use-chatgpt", "Log in with ChatGPT Plus/Pro subscription (OAuth)").option("--use-claude", "Use the local Claude CLI as a subprocess").action(async (opts) => {
960
+ if (opts.useChatgpt) {
961
+ console.log(chalk2.dim("Starting OAuth Authorization Code Flow..."));
962
+ try {
963
+ const token = await loginWithOAuthPKCE();
964
+ const expiresAt = new Date(Date.now() + token.expires_in * 1e3).toISOString();
965
+ saveCredentials({
966
+ provider: "openai",
967
+ apiKey: "",
968
+ oauthToken: token.access_token,
969
+ refreshToken: token.refresh_token,
970
+ expiresAt
971
+ });
972
+ console.log(chalk2.green("\n\u2713 Authenticated via OAuth"));
973
+ } catch (error) {
974
+ console.error(chalk2.red(`OAuth failed: ${error.message}`));
975
+ console.error(chalk2.dim("Try 'kanzaki login' with an API key instead."));
976
+ process.exit(1);
977
+ }
978
+ } else if (opts.useClaude) {
979
+ console.log(chalk2.dim("Checking local Claude CLI installation..."));
980
+ try {
981
+ const version = execSync2("claude --version", { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim();
982
+ console.log(chalk2.dim(`Found: ${version}`));
983
+ } catch {
984
+ console.error(chalk2.red("Claude CLI not found."));
985
+ console.error(chalk2.dim("Install it from https://docs.claude.com/en/docs/claude-code and run 'claude login' first."));
986
+ process.exit(1);
987
+ }
988
+ saveCredentials({
989
+ provider: "anthropic",
990
+ apiKey: "",
991
+ useClaudeCli: true
992
+ });
993
+ console.log(chalk2.green("\n\u2713 Configured to use local Claude CLI"));
994
+ console.log(chalk2.dim("Kanzaki will invoke 'claude -p' for reviews, using your existing Claude CLI session."));
995
+ console.log(chalk2.dim("Credentials stored in ~/.config/kanzaki/credentials.json"));
996
+ } else {
997
+ if (!opts.provider) {
998
+ console.error(chalk2.red("Authentication method is required."));
999
+ console.error(chalk2.dim("Use one of:"));
1000
+ console.error(chalk2.dim(` kanzaki login --provider <${SUPPORTED_PROVIDERS.join(" | ")}> (API key)`));
1001
+ console.error(chalk2.dim(" kanzaki login --use-chatgpt (ChatGPT OAuth)"));
1002
+ console.error(chalk2.dim(" kanzaki login --use-claude (Claude CLI subprocess)"));
1003
+ process.exit(1);
1004
+ }
1005
+ if (!SUPPORTED_PROVIDERS.includes(opts.provider)) {
1006
+ console.error(chalk2.red(`Unknown provider: ${opts.provider}`));
1007
+ console.error(chalk2.dim(`Supported providers: ${SUPPORTED_PROVIDERS.join(", ")}`));
1008
+ process.exit(1);
1009
+ }
1010
+ const provider = opts.provider;
1011
+ const label = provider === "openai" ? "OpenAI" : "Anthropic";
1012
+ const key = await promptSecret(`Enter your ${label} API key: `);
1013
+ if (!key) {
1014
+ console.error(chalk2.red("No API key provided."));
1015
+ process.exit(1);
1016
+ }
1017
+ saveCredentials({ provider, apiKey: key });
1018
+ console.log(chalk2.green(`\u2713 Saved ${provider} credentials`));
1019
+ console.log(chalk2.dim("Credentials stored in ~/.config/kanzaki/credentials.json"));
1020
+ }
1021
+ });
1022
+ program2.command("logout").description("Remove saved credentials").action(() => {
1023
+ clearCredentials();
1024
+ console.log(chalk2.green("\u2713 Credentials removed"));
1025
+ });
1026
+ program2.command("status").description("Show authentication status").action(() => {
1027
+ const creds = loadCredentials();
1028
+ if (!creds || !creds.apiKey && !creds.oauthToken && !creds.useClaudeCli) {
1029
+ console.log(chalk2.yellow("Not authenticated."));
1030
+ console.log(chalk2.dim("Run 'kanzaki login' to authenticate."));
1031
+ return;
1032
+ }
1033
+ console.log(chalk2.bold("Kanzaki Status"));
1034
+ console.log(` Provider: ${chalk2.cyan(creds.provider)}`);
1035
+ if (creds.useClaudeCli) {
1036
+ console.log(` Auth: ${chalk2.cyan("Claude CLI subprocess")} ${chalk2.green("(active)")}`);
1037
+ } else if (creds.oauthToken) {
1038
+ const expired = creds.expiresAt && new Date(creds.expiresAt) < /* @__PURE__ */ new Date();
1039
+ console.log(` Auth: ${chalk2.cyan("OAuth")}${expired ? chalk2.red(" (expired)") : chalk2.green(" (active)")}`);
1040
+ } else {
1041
+ const masked = creds.apiKey.slice(0, 7) + "..." + creds.apiKey.slice(-4);
1042
+ console.log(` Auth: ${chalk2.cyan("API Key")} (${masked})`);
1043
+ }
1044
+ });
1045
+ return program2;
1046
+ }
1047
+ function loadTemplate() {
1048
+ const templatePath = resolve4(__dirname, "..", "templates", "rules.md");
1049
+ if (existsSync5(templatePath)) {
1050
+ return readFileSync4(templatePath, "utf-8");
1051
+ }
1052
+ return `# Kanzaki \u30EC\u30D3\u30E5\u30FC\u30EB\u30FC\u30EB
1053
+
1054
+ ## \u54C1\u8CEA
1055
+ - [ ] !error \u5909\u66F4\u5185\u5BB9\u304C\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u306E\u65E2\u5B58\u306E\u30B9\u30BF\u30A4\u30EB\u3084\u898F\u7D04\u3068\u4E00\u8CAB\u3057\u3066\u3044\u308B\u3053\u3068
1056
+ - [ ] !error \u30D7\u30EC\u30FC\u30B9\u30DB\u30EB\u30C0\u30FC\u3084TODO\u304C\u6B8B\u3063\u3066\u3044\u306A\u3044\u3053\u3068
1057
+ - [ ] !warn \u5185\u5BB9\u304C\u660E\u78BA\u30FB\u7C21\u6F54\u3067\u3001\u4E0D\u8981\u306A\u7E70\u308A\u8FD4\u3057\u304C\u306A\u3044\u3053\u3068
1058
+
1059
+ ## \u6B63\u78BA\u6027
1060
+ - [ ] !error \u4E8B\u5B9F\u8AA4\u8A8D\u3084\u8AA4\u89E3\u3092\u62DB\u304F\u60C5\u5831\u304C\u542B\u307E\u308C\u3066\u3044\u306A\u3044\u3053\u3068
1061
+ - [ ] !warn \u9069\u5207\u306A\u7B87\u6240\u306B\u51FA\u5178\u30FB\u53C2\u8003\u6587\u732E\u304C\u8A18\u8F09\u3055\u308C\u3066\u3044\u308B\u3053\u3068
1062
+
1063
+ ## \u30BB\u30AD\u30E5\u30EA\u30C6\u30A3 (*.ts, *.js, *.py)
1064
+ - [ ] !error \u30CF\u30FC\u30C9\u30B3\u30FC\u30C9\u3055\u308C\u305F\u30B7\u30FC\u30AF\u30EC\u30C3\u30C8\u30FBAPI\u30AD\u30FC\u30FB\u30D1\u30B9\u30EF\u30FC\u30C9\u304C\u542B\u307E\u308C\u3066\u3044\u306A\u3044\u3053\u3068
1065
+ `;
1066
+ }
1067
+ function promptSecret(prompt) {
1068
+ return new Promise((resolve5) => {
1069
+ const rl = createInterface({
1070
+ input: process.stdin,
1071
+ output: process.stdout
1072
+ });
1073
+ process.stdout.write(prompt);
1074
+ const stdin = process.stdin;
1075
+ const wasRaw = stdin.isRaw;
1076
+ if (stdin.isTTY && stdin.setRawMode) {
1077
+ stdin.setRawMode(true);
1078
+ }
1079
+ let input = "";
1080
+ const onData = (char) => {
1081
+ const c = char.toString();
1082
+ if (c === "\n" || c === "\r") {
1083
+ stdin.removeListener("data", onData);
1084
+ if (stdin.isTTY && stdin.setRawMode) {
1085
+ stdin.setRawMode(wasRaw ?? false);
1086
+ }
1087
+ process.stdout.write("\n");
1088
+ rl.close();
1089
+ resolve5(input);
1090
+ } else if (c === "") {
1091
+ process.exit(1);
1092
+ } else if (c === "\x7F" || c === "\b") {
1093
+ input = input.slice(0, -1);
1094
+ } else {
1095
+ input += c;
1096
+ process.stdout.write("*");
1097
+ }
1098
+ };
1099
+ stdin.on("data", onData);
1100
+ });
1101
+ }
1102
+
1103
+ // src/bin.ts
1104
+ var program = createCli();
1105
+ program.parse();
1106
+ //# sourceMappingURL=bin.js.map