kantban-cli 0.1.24 → 0.1.26
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/index.js
CHANGED
|
@@ -31,10 +31,10 @@ async function main() {
|
|
|
31
31
|
}
|
|
32
32
|
case "pipeline": {
|
|
33
33
|
if (args[0] === "stop") {
|
|
34
|
-
const { stopPipeline } = await import("./pipeline-
|
|
34
|
+
const { stopPipeline } = await import("./pipeline-ETDPJOYI.js");
|
|
35
35
|
await stopPipeline(args.slice(1));
|
|
36
36
|
} else {
|
|
37
|
-
const { runPipeline } = await import("./pipeline-
|
|
37
|
+
const { runPipeline } = await import("./pipeline-ETDPJOYI.js");
|
|
38
38
|
await runPipeline(client, args);
|
|
39
39
|
}
|
|
40
40
|
break;
|
|
@@ -22,9 +22,9 @@ import {
|
|
|
22
22
|
} from "./chunk-ZTQJMXJM.js";
|
|
23
23
|
|
|
24
24
|
// src/commands/pipeline.ts
|
|
25
|
-
import { mkdirSync as
|
|
26
|
-
import { homedir } from "os";
|
|
27
|
-
import { join as
|
|
25
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, readFileSync as readFileSync3, unlinkSync as unlinkSync3, existsSync as existsSync3, appendFileSync as appendFileSync2 } from "fs";
|
|
26
|
+
import { homedir as homedir2 } from "os";
|
|
27
|
+
import { join as join3 } from "path";
|
|
28
28
|
|
|
29
29
|
// src/lib/tool-profiles.ts
|
|
30
30
|
function resolveToolRestrictions(builtinTools, allowedTools, disallowedTools) {
|
|
@@ -3448,15 +3448,451 @@ var CodexProvider = class {
|
|
|
3448
3448
|
}
|
|
3449
3449
|
};
|
|
3450
3450
|
|
|
3451
|
+
// src/providers/gemini-provider.ts
|
|
3452
|
+
import { spawn as spawn3, execFileSync as execFileSync3 } from "child_process";
|
|
3453
|
+
import { existsSync as existsSync2, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2 } from "fs";
|
|
3454
|
+
import { join as join2, dirname } from "path";
|
|
3455
|
+
import { homedir } from "os";
|
|
3456
|
+
import { fileURLToPath } from "url";
|
|
3457
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
3458
|
+
|
|
3459
|
+
// src/providers/gemini-jsonl-parser.ts
|
|
3460
|
+
var GeminiJsonlParser = class {
|
|
3461
|
+
buffer = "";
|
|
3462
|
+
/** Accumulates lines that failed individual JSON parse (multi-line object) */
|
|
3463
|
+
jsonAccum = "";
|
|
3464
|
+
toolCallCount = 0;
|
|
3465
|
+
inputTokens = 0;
|
|
3466
|
+
outputTokens = 0;
|
|
3467
|
+
lastOutput = "";
|
|
3468
|
+
onEvent = () => {
|
|
3469
|
+
};
|
|
3470
|
+
onError = () => {
|
|
3471
|
+
};
|
|
3472
|
+
feed(chunk) {
|
|
3473
|
+
this.buffer += chunk;
|
|
3474
|
+
const lines = this.buffer.split("\n");
|
|
3475
|
+
this.buffer = lines.pop() ?? "";
|
|
3476
|
+
for (const line of lines) {
|
|
3477
|
+
this.parseLine(line.trim());
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
3480
|
+
flush() {
|
|
3481
|
+
const trimmed = this.buffer.trim();
|
|
3482
|
+
this.buffer = "";
|
|
3483
|
+
if (trimmed) this.parseLine(trimmed);
|
|
3484
|
+
this.tryFlushAccum();
|
|
3485
|
+
}
|
|
3486
|
+
getToolCallCount() {
|
|
3487
|
+
return this.toolCallCount;
|
|
3488
|
+
}
|
|
3489
|
+
getUsage() {
|
|
3490
|
+
return { inputTokens: this.inputTokens, outputTokens: this.outputTokens };
|
|
3491
|
+
}
|
|
3492
|
+
getLastOutput() {
|
|
3493
|
+
return this.lastOutput;
|
|
3494
|
+
}
|
|
3495
|
+
reset() {
|
|
3496
|
+
this.buffer = "";
|
|
3497
|
+
this.toolCallCount = 0;
|
|
3498
|
+
this.inputTokens = 0;
|
|
3499
|
+
this.outputTokens = 0;
|
|
3500
|
+
this.lastOutput = "";
|
|
3501
|
+
}
|
|
3502
|
+
parseLine(line) {
|
|
3503
|
+
if (!line) return;
|
|
3504
|
+
if (this.jsonAccum) {
|
|
3505
|
+
this.jsonAccum += "\n" + line;
|
|
3506
|
+
if (this.tryFlushAccum()) return;
|
|
3507
|
+
return;
|
|
3508
|
+
}
|
|
3509
|
+
try {
|
|
3510
|
+
const raw = JSON.parse(line);
|
|
3511
|
+
this.translateEvent(raw);
|
|
3512
|
+
} catch {
|
|
3513
|
+
if (line.startsWith("{") || line.startsWith("[")) {
|
|
3514
|
+
this.jsonAccum = line;
|
|
3515
|
+
}
|
|
3516
|
+
}
|
|
3517
|
+
}
|
|
3518
|
+
/** Try to parse the accumulated multi-line buffer as JSON. Returns true if successful. */
|
|
3519
|
+
tryFlushAccum() {
|
|
3520
|
+
if (!this.jsonAccum) return false;
|
|
3521
|
+
try {
|
|
3522
|
+
const raw = JSON.parse(this.jsonAccum);
|
|
3523
|
+
this.jsonAccum = "";
|
|
3524
|
+
this.translateEvent(raw);
|
|
3525
|
+
return true;
|
|
3526
|
+
} catch {
|
|
3527
|
+
return false;
|
|
3528
|
+
}
|
|
3529
|
+
}
|
|
3530
|
+
translateEvent(raw) {
|
|
3531
|
+
const eventType = raw.type;
|
|
3532
|
+
if (eventType === "message") {
|
|
3533
|
+
if (raw.role === "assistant" && typeof raw.content === "string") {
|
|
3534
|
+
this.onEvent({ type: "text", text: raw.content });
|
|
3535
|
+
}
|
|
3536
|
+
} else if (eventType === "tool_use") {
|
|
3537
|
+
this.toolCallCount++;
|
|
3538
|
+
this.onEvent({
|
|
3539
|
+
type: "tool_call",
|
|
3540
|
+
tool: raw.name ?? "unknown",
|
|
3541
|
+
input: raw.args ?? raw.input
|
|
3542
|
+
});
|
|
3543
|
+
} else if (eventType === "tool_result") {
|
|
3544
|
+
this.onEvent({
|
|
3545
|
+
type: "tool_result",
|
|
3546
|
+
tool: raw.name ?? "unknown",
|
|
3547
|
+
output: raw.output ?? raw.result
|
|
3548
|
+
});
|
|
3549
|
+
} else if (eventType === "result" || raw.session_id && raw.stats) {
|
|
3550
|
+
const { inTok, outTok } = this.extractTokens(raw);
|
|
3551
|
+
this.inputTokens += inTok;
|
|
3552
|
+
this.outputTokens += outTok;
|
|
3553
|
+
if (inTok || outTok) {
|
|
3554
|
+
this.onEvent({ type: "usage", inputTokens: inTok, outputTokens: outTok });
|
|
3555
|
+
}
|
|
3556
|
+
const tools = raw.stats?.tools;
|
|
3557
|
+
if (tools?.totalCalls) this.toolCallCount = Math.max(this.toolCallCount, tools.totalCalls);
|
|
3558
|
+
if (typeof raw.response === "string") {
|
|
3559
|
+
this.lastOutput = raw.response;
|
|
3560
|
+
this.onEvent({ type: "done", result: raw.response });
|
|
3561
|
+
}
|
|
3562
|
+
} else if (eventType === "error") {
|
|
3563
|
+
const msg = raw.message ?? "unknown error";
|
|
3564
|
+
this.lastOutput = msg;
|
|
3565
|
+
this.onEvent({ type: "error", message: msg });
|
|
3566
|
+
}
|
|
3567
|
+
}
|
|
3568
|
+
/** Extract token counts from Gemini's nested stats structure */
|
|
3569
|
+
extractTokens(raw) {
|
|
3570
|
+
const stats = raw.stats;
|
|
3571
|
+
if (!stats) return { inTok: 0, outTok: 0 };
|
|
3572
|
+
if (stats.promptTokenCount || stats.input_tokens) {
|
|
3573
|
+
return {
|
|
3574
|
+
inTok: stats.promptTokenCount ?? stats.input_tokens ?? 0,
|
|
3575
|
+
outTok: stats.candidatesTokenCount ?? stats.output_tokens ?? 0
|
|
3576
|
+
};
|
|
3577
|
+
}
|
|
3578
|
+
if (stats.models && typeof stats.models === "object") {
|
|
3579
|
+
let inTok = 0;
|
|
3580
|
+
let outTok = 0;
|
|
3581
|
+
for (const model of Object.values(stats.models)) {
|
|
3582
|
+
const tokens = model?.tokens;
|
|
3583
|
+
if (tokens) {
|
|
3584
|
+
inTok += tokens.prompt ?? tokens.input ?? 0;
|
|
3585
|
+
outTok += tokens.candidates ?? tokens.output ?? 0;
|
|
3586
|
+
}
|
|
3587
|
+
}
|
|
3588
|
+
return { inTok, outTok };
|
|
3589
|
+
}
|
|
3590
|
+
return { inTok: 0, outTok: 0 };
|
|
3591
|
+
}
|
|
3592
|
+
};
|
|
3593
|
+
|
|
3594
|
+
// src/providers/gemini-provider.ts
|
|
3595
|
+
var GEMINI_TIMEOUT_MS = 60 * 60 * 1e3;
|
|
3596
|
+
var TOOL_NAME_MAP = {
|
|
3597
|
+
Write: "write_file",
|
|
3598
|
+
Edit: "replace",
|
|
3599
|
+
Read: "read_file",
|
|
3600
|
+
Bash: "run_shell_command",
|
|
3601
|
+
Grep: "grep_search",
|
|
3602
|
+
Glob: "glob",
|
|
3603
|
+
WebFetch: "web_fetch",
|
|
3604
|
+
WebSearch: "google_web_search",
|
|
3605
|
+
LS: "list_directory"
|
|
3606
|
+
};
|
|
3607
|
+
function translateToolNames(tools) {
|
|
3608
|
+
return tools.map((t) => TOOL_NAME_MAP[t] ?? t);
|
|
3609
|
+
}
|
|
3610
|
+
function resolveHookScriptPath() {
|
|
3611
|
+
try {
|
|
3612
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
3613
|
+
const candidates = [
|
|
3614
|
+
join2(thisDir, "lib", "gemini-hooks.mjs"),
|
|
3615
|
+
// dist/lib/ (flat bundle)
|
|
3616
|
+
join2(thisDir, "..", "lib", "gemini-hooks.mjs"),
|
|
3617
|
+
// dist/../lib/ (nested)
|
|
3618
|
+
join2(thisDir, "..", "src", "lib", "gemini-hooks.mjs"),
|
|
3619
|
+
// source from dist/
|
|
3620
|
+
join2(thisDir, "..", "..", "src", "lib", "gemini-hooks.mjs")
|
|
3621
|
+
// source from dist/providers/
|
|
3622
|
+
];
|
|
3623
|
+
for (const p of candidates) {
|
|
3624
|
+
if (existsSync2(p)) return p;
|
|
3625
|
+
}
|
|
3626
|
+
return null;
|
|
3627
|
+
} catch {
|
|
3628
|
+
return null;
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3631
|
+
var GeminiProvider = class _GeminiProvider {
|
|
3632
|
+
id = "gemini";
|
|
3633
|
+
displayName = "Gemini CLI";
|
|
3634
|
+
capabilities() {
|
|
3635
|
+
return {
|
|
3636
|
+
supportsToolAllowlist: true,
|
|
3637
|
+
supportsToolDenylist: true,
|
|
3638
|
+
supportsBuiltinToolStripping: true,
|
|
3639
|
+
supportsMaxTurns: true,
|
|
3640
|
+
supportsMcpConfigInjection: false,
|
|
3641
|
+
supportsMcpConfigOverride: true,
|
|
3642
|
+
supportsWorktreeFlag: false,
|
|
3643
|
+
supportsSandboxModes: true,
|
|
3644
|
+
supportedModels: [
|
|
3645
|
+
{ id: "gemini-2.5-flash-lite", displayName: "Flash Lite", tier: "fast" },
|
|
3646
|
+
{ id: "gemini-2.5-flash", displayName: "Flash", tier: "default" },
|
|
3647
|
+
{ id: "gemini-2.5-pro", displayName: "Pro", tier: "thorough" }
|
|
3648
|
+
],
|
|
3649
|
+
streamFormat: "jsonl"
|
|
3650
|
+
};
|
|
3651
|
+
}
|
|
3652
|
+
async invoke(request) {
|
|
3653
|
+
const degraded = [];
|
|
3654
|
+
const args = this.buildArgs(request);
|
|
3655
|
+
const startTime = Date.now();
|
|
3656
|
+
if (request.workingDirectory) {
|
|
3657
|
+
if (!existsSync2(request.workingDirectory)) {
|
|
3658
|
+
try {
|
|
3659
|
+
execFileSync3("git", ["worktree", "add", "-b", request.workingDirectory, request.workingDirectory, "HEAD"], {
|
|
3660
|
+
stdio: "pipe"
|
|
3661
|
+
});
|
|
3662
|
+
} catch {
|
|
3663
|
+
try {
|
|
3664
|
+
execFileSync3("git", ["worktree", "add", request.workingDirectory, request.workingDirectory], {
|
|
3665
|
+
stdio: "pipe"
|
|
3666
|
+
});
|
|
3667
|
+
} catch {
|
|
3668
|
+
degraded.push("worktreeCreation");
|
|
3669
|
+
}
|
|
3670
|
+
}
|
|
3671
|
+
} else {
|
|
3672
|
+
try {
|
|
3673
|
+
execFileSync3("git", ["-C", request.workingDirectory, "rev-parse", "--git-dir"], {
|
|
3674
|
+
stdio: "pipe"
|
|
3675
|
+
});
|
|
3676
|
+
} catch {
|
|
3677
|
+
try {
|
|
3678
|
+
execFileSync3("rm", ["-rf", request.workingDirectory], { stdio: "pipe" });
|
|
3679
|
+
execFileSync3("git", ["worktree", "add", "-b", request.workingDirectory, request.workingDirectory, "HEAD"], {
|
|
3680
|
+
stdio: "pipe"
|
|
3681
|
+
});
|
|
3682
|
+
} catch {
|
|
3683
|
+
try {
|
|
3684
|
+
execFileSync3("git", ["worktree", "add", request.workingDirectory, request.workingDirectory], {
|
|
3685
|
+
stdio: "pipe"
|
|
3686
|
+
});
|
|
3687
|
+
} catch {
|
|
3688
|
+
degraded.push("worktreeCreation");
|
|
3689
|
+
}
|
|
3690
|
+
}
|
|
3691
|
+
}
|
|
3692
|
+
}
|
|
3693
|
+
if (!degraded.includes("worktreeCreation") && existsSync2(request.workingDirectory)) {
|
|
3694
|
+
ensureWorktreeRemote(request.workingDirectory);
|
|
3695
|
+
}
|
|
3696
|
+
}
|
|
3697
|
+
const settingsDir = this.writeGeminiSettings(request, degraded);
|
|
3698
|
+
const cwd = request.workingDirectory ?? settingsDir;
|
|
3699
|
+
if (request.workingDirectory && settingsDir) {
|
|
3700
|
+
this.copySettingsToDir(settingsDir, request.workingDirectory);
|
|
3701
|
+
}
|
|
3702
|
+
return new Promise((resolve) => {
|
|
3703
|
+
const child = spawn3("gemini", args, {
|
|
3704
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
3705
|
+
cwd: cwd || void 0
|
|
3706
|
+
});
|
|
3707
|
+
const parser = new GeminiJsonlParser();
|
|
3708
|
+
parser.onEvent = (event) => request.onStreamEvent?.(event);
|
|
3709
|
+
parser.onError = (err) => process.stderr.write(`[gemini-jsonl] ${err.message}
|
|
3710
|
+
`);
|
|
3711
|
+
let stderr = "";
|
|
3712
|
+
child.stdout?.on("data", (chunk) => parser.feed(chunk.toString()));
|
|
3713
|
+
child.stderr?.on("data", (chunk) => {
|
|
3714
|
+
stderr += chunk.toString();
|
|
3715
|
+
});
|
|
3716
|
+
child.stdin?.end();
|
|
3717
|
+
if (request.abortSignal) {
|
|
3718
|
+
request.abortSignal.addEventListener("abort", () => {
|
|
3719
|
+
try {
|
|
3720
|
+
child.kill("SIGTERM");
|
|
3721
|
+
} catch {
|
|
3722
|
+
}
|
|
3723
|
+
}, { once: true });
|
|
3724
|
+
}
|
|
3725
|
+
let killTimer;
|
|
3726
|
+
const timeoutHandle = setTimeout(() => {
|
|
3727
|
+
try {
|
|
3728
|
+
child.kill("SIGTERM");
|
|
3729
|
+
} catch {
|
|
3730
|
+
}
|
|
3731
|
+
killTimer = setTimeout(() => {
|
|
3732
|
+
try {
|
|
3733
|
+
child.kill("SIGKILL");
|
|
3734
|
+
} catch {
|
|
3735
|
+
}
|
|
3736
|
+
}, 5e3);
|
|
3737
|
+
}, GEMINI_TIMEOUT_MS);
|
|
3738
|
+
let resolved = false;
|
|
3739
|
+
const finish = (code, errorMsg) => {
|
|
3740
|
+
if (resolved) return;
|
|
3741
|
+
resolved = true;
|
|
3742
|
+
clearTimeout(timeoutHandle);
|
|
3743
|
+
if (killTimer) clearTimeout(killTimer);
|
|
3744
|
+
parser.flush();
|
|
3745
|
+
const usage = parser.getUsage();
|
|
3746
|
+
const rawOutput = errorMsg ?? (parser.getLastOutput() || _GeminiProvider.stripCliPreamble(stderr));
|
|
3747
|
+
const result = {
|
|
3748
|
+
exitCode: (code === 53 ? 0 : code) ?? 1,
|
|
3749
|
+
output: rawOutput,
|
|
3750
|
+
toolCallCount: parser.getToolCallCount(),
|
|
3751
|
+
usage,
|
|
3752
|
+
durationMs: Date.now() - startTime
|
|
3753
|
+
};
|
|
3754
|
+
if (degraded.length > 0) result.degradedCapabilities = degraded;
|
|
3755
|
+
resolve(result);
|
|
3756
|
+
};
|
|
3757
|
+
child.on("close", (code) => finish(code));
|
|
3758
|
+
child.on("error", (err) => finish(1, err.message));
|
|
3759
|
+
});
|
|
3760
|
+
}
|
|
3761
|
+
async preflight() {
|
|
3762
|
+
try {
|
|
3763
|
+
const whichModule = await import("which");
|
|
3764
|
+
const syncFn = whichModule.default?.sync ?? whichModule.sync;
|
|
3765
|
+
syncFn("gemini");
|
|
3766
|
+
return { available: true, authenticated: true };
|
|
3767
|
+
} catch {
|
|
3768
|
+
return { available: false, authenticated: false, error: "gemini binary not found on PATH" };
|
|
3769
|
+
}
|
|
3770
|
+
}
|
|
3771
|
+
buildArgs(request) {
|
|
3772
|
+
const args = [
|
|
3773
|
+
"-p",
|
|
3774
|
+
request.prompt,
|
|
3775
|
+
"--yolo",
|
|
3776
|
+
"--output-format",
|
|
3777
|
+
"json"
|
|
3778
|
+
];
|
|
3779
|
+
if (request.model) args.push("--model", request.model);
|
|
3780
|
+
return args;
|
|
3781
|
+
}
|
|
3782
|
+
writeGeminiSettings(request, degraded) {
|
|
3783
|
+
try {
|
|
3784
|
+
const baseDir = join2(homedir(), ".kantban", "pipelines", "gemini-tmp");
|
|
3785
|
+
const dir = join2(baseDir, `session-${Date.now()}`);
|
|
3786
|
+
const geminiDir = join2(dir, ".gemini");
|
|
3787
|
+
mkdirSync2(geminiDir, { recursive: true, mode: 448 });
|
|
3788
|
+
const settings = {};
|
|
3789
|
+
if (request.mcpConfig && Object.keys(request.mcpConfig.servers).length > 0) {
|
|
3790
|
+
settings.mcpServers = {};
|
|
3791
|
+
for (const [name, server] of Object.entries(request.mcpConfig.servers)) {
|
|
3792
|
+
settings.mcpServers[name] = {
|
|
3793
|
+
command: server.command,
|
|
3794
|
+
args: server.args,
|
|
3795
|
+
env: server.env,
|
|
3796
|
+
trust: true
|
|
3797
|
+
};
|
|
3798
|
+
}
|
|
3799
|
+
}
|
|
3800
|
+
const hookPath = resolveHookScriptPath();
|
|
3801
|
+
if (hookPath) {
|
|
3802
|
+
const hooks = {};
|
|
3803
|
+
const hookConfig = {};
|
|
3804
|
+
if (request.toolRestrictions) {
|
|
3805
|
+
const tr = request.toolRestrictions;
|
|
3806
|
+
hookConfig.allowedTools = tr.allowedTools ? translateToolNames(tr.allowedTools) : null;
|
|
3807
|
+
hookConfig.disallowedTools = tr.disallowedTools ? translateToolNames(tr.disallowedTools) : null;
|
|
3808
|
+
if (tr.tools !== void 0) hookConfig.builtinToolsMode = tr.tools;
|
|
3809
|
+
const wrapperPath = join2(dir, ".kantban-hook-before-tool.sh");
|
|
3810
|
+
writeFileSync3(wrapperPath, this.generateHookWrapper(hookPath, "BeforeToolSelection", join2(dir, ".kantban-hook-config.json")), { mode: 493 });
|
|
3811
|
+
hooks.BeforeToolSelection = [{ matcher: "*", hooks: [{ type: "command", command: wrapperPath, timeout: 3e4 }] }];
|
|
3812
|
+
}
|
|
3813
|
+
if (request.maxTurns) {
|
|
3814
|
+
hookConfig.maxTurns = request.maxTurns;
|
|
3815
|
+
hookConfig.turnFile = join2(dir, ".kantban-turn-counter");
|
|
3816
|
+
const wrapperPath = join2(dir, ".kantban-hook-after-agent.sh");
|
|
3817
|
+
writeFileSync3(wrapperPath, this.generateHookWrapper(hookPath, "AfterAgent", join2(dir, ".kantban-hook-config.json")), { mode: 493 });
|
|
3818
|
+
hooks.AfterAgent = [{ matcher: "*", hooks: [{ type: "command", command: wrapperPath, timeout: 3e4 }] }];
|
|
3819
|
+
}
|
|
3820
|
+
if (Object.keys(hookConfig).length > 0) {
|
|
3821
|
+
writeFileSync3(join2(dir, ".kantban-hook-config.json"), JSON.stringify(hookConfig), { mode: 384 });
|
|
3822
|
+
}
|
|
3823
|
+
if (Object.keys(hooks).length > 0) {
|
|
3824
|
+
settings.hooks = hooks;
|
|
3825
|
+
}
|
|
3826
|
+
} else {
|
|
3827
|
+
if (request.maxTurns) degraded.push("maxTurns");
|
|
3828
|
+
if (request.toolRestrictions?.allowedTools?.length) degraded.push("toolAllowlist");
|
|
3829
|
+
if (request.toolRestrictions?.disallowedTools?.length) degraded.push("toolDenylist");
|
|
3830
|
+
if (request.toolRestrictions?.tools !== void 0) degraded.push("builtinToolStripping");
|
|
3831
|
+
process.stderr.write(`[gemini] Hook script not found \u2014 tool scoping and turn limits unavailable
|
|
3832
|
+
`);
|
|
3833
|
+
}
|
|
3834
|
+
writeFileSync3(join2(geminiDir, "settings.json"), JSON.stringify(settings, null, 2), { mode: 384 });
|
|
3835
|
+
return dir;
|
|
3836
|
+
} catch (err) {
|
|
3837
|
+
process.stderr.write(`[gemini] Failed to write settings.json: ${err}
|
|
3838
|
+
`);
|
|
3839
|
+
if (request.maxTurns) degraded.push("maxTurns");
|
|
3840
|
+
if (request.toolRestrictions?.allowedTools?.length) degraded.push("toolAllowlist");
|
|
3841
|
+
if (request.toolRestrictions?.disallowedTools?.length) degraded.push("toolDenylist");
|
|
3842
|
+
if (request.toolRestrictions?.tools !== void 0) degraded.push("builtinToolStripping");
|
|
3843
|
+
return null;
|
|
3844
|
+
}
|
|
3845
|
+
}
|
|
3846
|
+
/** Generate a per-session bash wrapper that invokes the Node hook script.
|
|
3847
|
+
* Gemini CLI ignores `args` in hook definitions, so we embed all arguments. */
|
|
3848
|
+
generateHookWrapper(hookScript, event, configFile) {
|
|
3849
|
+
return `#!/bin/bash
|
|
3850
|
+
# KantBan pipeline hook wrapper \u2014 Gemini CLI does not pass args, so we embed them.
|
|
3851
|
+
STDIN_FILE=$(mktemp)
|
|
3852
|
+
trap 'rm -f "$STDIN_FILE"' EXIT
|
|
3853
|
+
cat > "$STDIN_FILE"
|
|
3854
|
+
node "${hookScript}" "${event}" "${configFile}" < "$STDIN_FILE"
|
|
3855
|
+
`;
|
|
3856
|
+
}
|
|
3857
|
+
/** Strip Gemini CLI startup messages from stderr output.
|
|
3858
|
+
* When the JSONL parser finds no result event, the output falls back to stderr
|
|
3859
|
+
* which contains lines like "YOLO mode is enabled..." and "Loaded cached credentials."
|
|
3860
|
+
* that corrupt JSON parsing in downstream consumers (advisor, light-call). */
|
|
3861
|
+
static stripCliPreamble(text) {
|
|
3862
|
+
const preamblePatterns = [
|
|
3863
|
+
/^YOLO mode is enabled\. All tool calls will be automatically approved\.$/,
|
|
3864
|
+
/^Loaded cached credentials\.$/,
|
|
3865
|
+
/^Sandbox mode: .+$/
|
|
3866
|
+
];
|
|
3867
|
+
const lines = text.split("\n");
|
|
3868
|
+
const filtered = lines.filter((line) => {
|
|
3869
|
+
const trimmed = line.trim();
|
|
3870
|
+
return trimmed !== "" && !preamblePatterns.some((p) => p.test(trimmed));
|
|
3871
|
+
});
|
|
3872
|
+
return filtered.join("\n");
|
|
3873
|
+
}
|
|
3874
|
+
copySettingsToDir(settingsDir, targetDir) {
|
|
3875
|
+
try {
|
|
3876
|
+
const sourceFile = join2(settingsDir, ".gemini", "settings.json");
|
|
3877
|
+
const targetGeminiDir = join2(targetDir, ".gemini");
|
|
3878
|
+
mkdirSync2(targetGeminiDir, { recursive: true, mode: 448 });
|
|
3879
|
+
const content = readFileSync2(sourceFile, "utf-8");
|
|
3880
|
+
writeFileSync3(join2(targetGeminiDir, "settings.json"), content, { mode: 384 });
|
|
3881
|
+
} catch {
|
|
3882
|
+
}
|
|
3883
|
+
}
|
|
3884
|
+
};
|
|
3885
|
+
|
|
3451
3886
|
// src/commands/pipeline.ts
|
|
3452
3887
|
function createProviderRegistry() {
|
|
3453
3888
|
const registry = new ProviderRegistry();
|
|
3454
3889
|
registry.register(new ClaudeProvider());
|
|
3455
3890
|
registry.register(new CodexProvider());
|
|
3891
|
+
registry.register(new GeminiProvider());
|
|
3456
3892
|
return registry;
|
|
3457
3893
|
}
|
|
3458
3894
|
function readMcpConfigAsProviderConfig(filePath) {
|
|
3459
|
-
const raw = JSON.parse(
|
|
3895
|
+
const raw = JSON.parse(readFileSync3(filePath, "utf-8"));
|
|
3460
3896
|
return { servers: raw.mcpServers };
|
|
3461
3897
|
}
|
|
3462
3898
|
function parseArgs(args) {
|
|
@@ -3544,7 +3980,7 @@ Flags:
|
|
|
3544
3980
|
--max-iterations <n> Override max iterations per ticket
|
|
3545
3981
|
--max-budget <usd> Per-ticket budget cap (USD)
|
|
3546
3982
|
--model <model> Override model preference
|
|
3547
|
-
--provider <id> Override default provider (claude, codex)
|
|
3983
|
+
--provider <id> Override default provider (claude, codex, gemini)
|
|
3548
3984
|
--concurrency <n> Override concurrency per column
|
|
3549
3985
|
--log-retention <d> Log retention in days (default: 7)
|
|
3550
3986
|
--yes, -y Skip safety confirmation`);
|
|
@@ -3553,28 +3989,28 @@ Flags:
|
|
|
3553
3989
|
return { boardId, once, dryRun, columnFilter, maxIterations, maxBudget, model, provider, concurrency, logRetention, yes };
|
|
3554
3990
|
}
|
|
3555
3991
|
function pidDir(boardId) {
|
|
3556
|
-
return
|
|
3992
|
+
return join3(homedir2(), ".kantban", "pipelines", boardId);
|
|
3557
3993
|
}
|
|
3558
3994
|
function pidFilePath(boardId) {
|
|
3559
|
-
return
|
|
3995
|
+
return join3(pidDir(boardId), "orchestrator.pid");
|
|
3560
3996
|
}
|
|
3561
3997
|
function writePidFile(boardId) {
|
|
3562
3998
|
const dir = pidDir(boardId);
|
|
3563
|
-
|
|
3564
|
-
|
|
3999
|
+
mkdirSync3(dir, { recursive: true, mode: 448 });
|
|
4000
|
+
writeFileSync4(pidFilePath(boardId), String(process.pid), { mode: 384 });
|
|
3565
4001
|
}
|
|
3566
4002
|
function removePidFile(boardId) {
|
|
3567
4003
|
try {
|
|
3568
|
-
|
|
4004
|
+
unlinkSync3(pidFilePath(boardId));
|
|
3569
4005
|
} catch {
|
|
3570
4006
|
}
|
|
3571
4007
|
}
|
|
3572
4008
|
function childManifestPath(boardId) {
|
|
3573
|
-
return
|
|
4009
|
+
return join3(pidDir(boardId), "children.pid");
|
|
3574
4010
|
}
|
|
3575
4011
|
function readChildManifest(boardId) {
|
|
3576
4012
|
try {
|
|
3577
|
-
const contents =
|
|
4013
|
+
const contents = readFileSync3(childManifestPath(boardId), "utf-8");
|
|
3578
4014
|
return contents.split("\n").map((l) => l.trim()).filter((l) => l !== "").map(Number).filter((n) => !isNaN(n) && n > 0);
|
|
3579
4015
|
} catch {
|
|
3580
4016
|
return [];
|
|
@@ -3582,15 +4018,15 @@ function readChildManifest(boardId) {
|
|
|
3582
4018
|
}
|
|
3583
4019
|
function removeChildManifest(boardId) {
|
|
3584
4020
|
try {
|
|
3585
|
-
|
|
4021
|
+
unlinkSync3(childManifestPath(boardId));
|
|
3586
4022
|
} catch {
|
|
3587
4023
|
}
|
|
3588
4024
|
}
|
|
3589
4025
|
function cleanupOrphanedProcesses(boardId) {
|
|
3590
4026
|
const pidPath = pidFilePath(boardId);
|
|
3591
|
-
if (
|
|
4027
|
+
if (existsSync3(pidPath)) {
|
|
3592
4028
|
try {
|
|
3593
|
-
const stalePid = parseInt(
|
|
4029
|
+
const stalePid = parseInt(readFileSync3(pidPath, "utf-8").trim(), 10);
|
|
3594
4030
|
if (stalePid && stalePid !== process.pid) {
|
|
3595
4031
|
try {
|
|
3596
4032
|
process.kill(stalePid, 0);
|
|
@@ -3615,10 +4051,10 @@ function cleanupOrphanedProcesses(boardId) {
|
|
|
3615
4051
|
console.log(`Killed ${String(manifestPids.length)} orphaned child process(es) from manifest`);
|
|
3616
4052
|
removeChildManifest(boardId);
|
|
3617
4053
|
}
|
|
3618
|
-
const staleReaperPath =
|
|
4054
|
+
const staleReaperPath = join3(pidDir(boardId), "reaper.pid");
|
|
3619
4055
|
try {
|
|
3620
|
-
if (
|
|
3621
|
-
const reaperPid = parseInt(
|
|
4056
|
+
if (existsSync3(staleReaperPath)) {
|
|
4057
|
+
const reaperPid = parseInt(readFileSync3(staleReaperPath, "utf-8").trim(), 10);
|
|
3622
4058
|
if (reaperPid && !isNaN(reaperPid)) {
|
|
3623
4059
|
try {
|
|
3624
4060
|
process.kill(reaperPid, 0);
|
|
@@ -3627,7 +4063,7 @@ function cleanupOrphanedProcesses(boardId) {
|
|
|
3627
4063
|
} catch {
|
|
3628
4064
|
}
|
|
3629
4065
|
}
|
|
3630
|
-
|
|
4066
|
+
unlinkSync3(staleReaperPath);
|
|
3631
4067
|
}
|
|
3632
4068
|
} catch {
|
|
3633
4069
|
}
|
|
@@ -3674,15 +4110,15 @@ async function runPipeline(client, args) {
|
|
|
3674
4110
|
return;
|
|
3675
4111
|
}
|
|
3676
4112
|
}
|
|
3677
|
-
const gateFilePath =
|
|
4113
|
+
const gateFilePath = join3(process.cwd(), "pipeline.gates.yaml");
|
|
3678
4114
|
let gateConfig;
|
|
3679
|
-
if (!
|
|
4115
|
+
if (!existsSync3(gateFilePath)) {
|
|
3680
4116
|
console.error(`Error: pipeline.gates.yaml not found in ${process.cwd()}`);
|
|
3681
4117
|
console.error('Run "kantban pipeline init" to generate a starter gate file.');
|
|
3682
4118
|
process.exit(1);
|
|
3683
4119
|
}
|
|
3684
4120
|
try {
|
|
3685
|
-
const gateYaml =
|
|
4121
|
+
const gateYaml = readFileSync3(gateFilePath, "utf-8");
|
|
3686
4122
|
gateConfig = parseGateConfig(gateYaml);
|
|
3687
4123
|
console.log(`Gate config loaded: ${gateConfig.default.length} default gate(s)`);
|
|
3688
4124
|
} catch (err) {
|
|
@@ -3710,7 +4146,7 @@ async function runPipeline(client, args) {
|
|
|
3710
4146
|
intelligence_provider: opts.provider ?? void 0
|
|
3711
4147
|
};
|
|
3712
4148
|
const intelligenceProvider = registry.resolveForIntelligence(boardProviderConfig);
|
|
3713
|
-
const logBaseDir =
|
|
4149
|
+
const logBaseDir = join3(homedir2(), ".kantban", "pipelines");
|
|
3714
4150
|
const logger = new PipelineLogger(logBaseDir, opts.boardId);
|
|
3715
4151
|
logger.pruneOldLogs(opts.logRetention);
|
|
3716
4152
|
let runMemory = null;
|
|
@@ -3747,7 +4183,7 @@ async function runPipeline(client, args) {
|
|
|
3747
4183
|
effectiveConfig.model = registry.resolveModel(columnProvider, effectiveConfig.model);
|
|
3748
4184
|
}
|
|
3749
4185
|
const columnGates = resolveGatesForColumn(gateConfig, resolvedColumnName);
|
|
3750
|
-
const gateCwd = effectiveConfig.worktreeName ?
|
|
4186
|
+
const gateCwd = effectiveConfig.worktreeName ? join3(process.cwd(), effectiveConfig.worktreeName) : void 0;
|
|
3751
4187
|
const effectiveMcpConfigPath = columnGates.length > 0 ? generateGateProxyMcpConfig(
|
|
3752
4188
|
client.baseUrl,
|
|
3753
4189
|
client.token,
|
|
@@ -4196,7 +4632,7 @@ Received ${signal}. Shutting down gracefully...`);
|
|
|
4196
4632
|
cleanupOrphanedProcesses(opts.boardId);
|
|
4197
4633
|
writePidFile(opts.boardId);
|
|
4198
4634
|
logger.orchestrator(`PID file written: ${String(process.pid)}`);
|
|
4199
|
-
const reaperPidPath =
|
|
4635
|
+
const reaperPidPath = join3(pidDir(opts.boardId), "reaper.pid");
|
|
4200
4636
|
const reaperProcess = spawnReaper({
|
|
4201
4637
|
orchestratorPid: process.pid,
|
|
4202
4638
|
manifestPath: childManifestPath(opts.boardId),
|
|
@@ -4359,7 +4795,7 @@ async function stopPipeline(args) {
|
|
|
4359
4795
|
const pidFile = pidFilePath(boardId);
|
|
4360
4796
|
let pid;
|
|
4361
4797
|
try {
|
|
4362
|
-
const content =
|
|
4798
|
+
const content = readFileSync3(pidFile, "utf8").trim();
|
|
4363
4799
|
pid = Number(content);
|
|
4364
4800
|
if (isNaN(pid) || pid <= 0) {
|
|
4365
4801
|
throw new Error("Invalid PID");
|
|
@@ -4390,4 +4826,4 @@ export {
|
|
|
4390
4826
|
runPipeline,
|
|
4391
4827
|
stopPipeline
|
|
4392
4828
|
};
|
|
4393
|
-
//# sourceMappingURL=pipeline-
|
|
4829
|
+
//# sourceMappingURL=pipeline-ETDPJOYI.js.map
|