kantban-cli 0.1.23 → 0.1.25
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-IYKEQZWN.js");
|
|
35
35
|
await stopPipeline(args.slice(1));
|
|
36
36
|
} else {
|
|
37
|
-
const { runPipeline } = await import("./pipeline-
|
|
37
|
+
const { runPipeline } = await import("./pipeline-IYKEQZWN.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) {
|
|
@@ -913,6 +913,7 @@ var PipelineOrchestrator = class {
|
|
|
913
913
|
* Handle a pipeline event (typically from WebSocket via EventQueue).
|
|
914
914
|
*/
|
|
915
915
|
async handleEvent(event) {
|
|
916
|
+
await this.refreshBoardScope();
|
|
916
917
|
switch (event.type) {
|
|
917
918
|
case "ticket:moved":
|
|
918
919
|
case "ticket:created": {
|
|
@@ -3447,15 +3448,433 @@ var CodexProvider = class {
|
|
|
3447
3448
|
}
|
|
3448
3449
|
};
|
|
3449
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 {
|
|
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 result = {
|
|
3747
|
+
exitCode: (code === 53 ? 0 : code) ?? 1,
|
|
3748
|
+
output: errorMsg ?? (parser.getLastOutput() || stderr),
|
|
3749
|
+
toolCallCount: parser.getToolCallCount(),
|
|
3750
|
+
usage,
|
|
3751
|
+
durationMs: Date.now() - startTime
|
|
3752
|
+
};
|
|
3753
|
+
if (degraded.length > 0) result.degradedCapabilities = degraded;
|
|
3754
|
+
resolve(result);
|
|
3755
|
+
};
|
|
3756
|
+
child.on("close", (code) => finish(code));
|
|
3757
|
+
child.on("error", (err) => finish(1, err.message));
|
|
3758
|
+
});
|
|
3759
|
+
}
|
|
3760
|
+
async preflight() {
|
|
3761
|
+
try {
|
|
3762
|
+
const whichModule = await import("which");
|
|
3763
|
+
const syncFn = whichModule.default?.sync ?? whichModule.sync;
|
|
3764
|
+
syncFn("gemini");
|
|
3765
|
+
return { available: true, authenticated: true };
|
|
3766
|
+
} catch {
|
|
3767
|
+
return { available: false, authenticated: false, error: "gemini binary not found on PATH" };
|
|
3768
|
+
}
|
|
3769
|
+
}
|
|
3770
|
+
buildArgs(request) {
|
|
3771
|
+
const args = [
|
|
3772
|
+
"-p",
|
|
3773
|
+
request.prompt,
|
|
3774
|
+
"--yolo",
|
|
3775
|
+
"--output-format",
|
|
3776
|
+
"json"
|
|
3777
|
+
];
|
|
3778
|
+
if (request.model) args.push("--model", request.model);
|
|
3779
|
+
return args;
|
|
3780
|
+
}
|
|
3781
|
+
writeGeminiSettings(request, degraded) {
|
|
3782
|
+
try {
|
|
3783
|
+
const baseDir = join2(homedir(), ".kantban", "pipelines", "gemini-tmp");
|
|
3784
|
+
const dir = join2(baseDir, `session-${Date.now()}`);
|
|
3785
|
+
const geminiDir = join2(dir, ".gemini");
|
|
3786
|
+
mkdirSync2(geminiDir, { recursive: true, mode: 448 });
|
|
3787
|
+
const settings = {};
|
|
3788
|
+
if (request.mcpConfig && Object.keys(request.mcpConfig.servers).length > 0) {
|
|
3789
|
+
settings.mcpServers = {};
|
|
3790
|
+
for (const [name, server] of Object.entries(request.mcpConfig.servers)) {
|
|
3791
|
+
settings.mcpServers[name] = {
|
|
3792
|
+
command: server.command,
|
|
3793
|
+
args: server.args,
|
|
3794
|
+
env: server.env,
|
|
3795
|
+
trust: true
|
|
3796
|
+
};
|
|
3797
|
+
}
|
|
3798
|
+
}
|
|
3799
|
+
const hookPath = resolveHookScriptPath();
|
|
3800
|
+
if (hookPath) {
|
|
3801
|
+
const hooks = {};
|
|
3802
|
+
const hookConfig = {};
|
|
3803
|
+
if (request.toolRestrictions) {
|
|
3804
|
+
const tr = request.toolRestrictions;
|
|
3805
|
+
hookConfig.allowedTools = tr.allowedTools ? translateToolNames(tr.allowedTools) : null;
|
|
3806
|
+
hookConfig.disallowedTools = tr.disallowedTools ? translateToolNames(tr.disallowedTools) : null;
|
|
3807
|
+
if (tr.tools !== void 0) hookConfig.builtinToolsMode = tr.tools;
|
|
3808
|
+
const wrapperPath = join2(dir, ".kantban-hook-before-tool.sh");
|
|
3809
|
+
writeFileSync3(wrapperPath, this.generateHookWrapper(hookPath, "BeforeToolSelection", join2(dir, ".kantban-hook-config.json")), { mode: 493 });
|
|
3810
|
+
hooks.BeforeToolSelection = [{ matcher: "*", hooks: [{ type: "command", command: wrapperPath, timeout: 3e4 }] }];
|
|
3811
|
+
}
|
|
3812
|
+
if (request.maxTurns) {
|
|
3813
|
+
hookConfig.maxTurns = request.maxTurns;
|
|
3814
|
+
hookConfig.turnFile = join2(dir, ".kantban-turn-counter");
|
|
3815
|
+
const wrapperPath = join2(dir, ".kantban-hook-after-agent.sh");
|
|
3816
|
+
writeFileSync3(wrapperPath, this.generateHookWrapper(hookPath, "AfterAgent", join2(dir, ".kantban-hook-config.json")), { mode: 493 });
|
|
3817
|
+
hooks.AfterAgent = [{ matcher: "*", hooks: [{ type: "command", command: wrapperPath, timeout: 3e4 }] }];
|
|
3818
|
+
}
|
|
3819
|
+
if (Object.keys(hookConfig).length > 0) {
|
|
3820
|
+
writeFileSync3(join2(dir, ".kantban-hook-config.json"), JSON.stringify(hookConfig), { mode: 384 });
|
|
3821
|
+
}
|
|
3822
|
+
if (Object.keys(hooks).length > 0) {
|
|
3823
|
+
settings.hooks = hooks;
|
|
3824
|
+
}
|
|
3825
|
+
} else {
|
|
3826
|
+
if (request.maxTurns) degraded.push("maxTurns");
|
|
3827
|
+
if (request.toolRestrictions?.allowedTools?.length) degraded.push("toolAllowlist");
|
|
3828
|
+
if (request.toolRestrictions?.disallowedTools?.length) degraded.push("toolDenylist");
|
|
3829
|
+
if (request.toolRestrictions?.tools !== void 0) degraded.push("builtinToolStripping");
|
|
3830
|
+
process.stderr.write(`[gemini] Hook script not found \u2014 tool scoping and turn limits unavailable
|
|
3831
|
+
`);
|
|
3832
|
+
}
|
|
3833
|
+
writeFileSync3(join2(geminiDir, "settings.json"), JSON.stringify(settings, null, 2), { mode: 384 });
|
|
3834
|
+
return dir;
|
|
3835
|
+
} catch (err) {
|
|
3836
|
+
process.stderr.write(`[gemini] Failed to write settings.json: ${err}
|
|
3837
|
+
`);
|
|
3838
|
+
if (request.maxTurns) degraded.push("maxTurns");
|
|
3839
|
+
if (request.toolRestrictions?.allowedTools?.length) degraded.push("toolAllowlist");
|
|
3840
|
+
if (request.toolRestrictions?.disallowedTools?.length) degraded.push("toolDenylist");
|
|
3841
|
+
if (request.toolRestrictions?.tools !== void 0) degraded.push("builtinToolStripping");
|
|
3842
|
+
return null;
|
|
3843
|
+
}
|
|
3844
|
+
}
|
|
3845
|
+
/** Generate a per-session bash wrapper that invokes the Node hook script.
|
|
3846
|
+
* Gemini CLI ignores `args` in hook definitions, so we embed all arguments. */
|
|
3847
|
+
generateHookWrapper(hookScript, event, configFile) {
|
|
3848
|
+
return `#!/bin/bash
|
|
3849
|
+
# KantBan pipeline hook wrapper \u2014 Gemini CLI does not pass args, so we embed them.
|
|
3850
|
+
STDIN_FILE=$(mktemp)
|
|
3851
|
+
trap 'rm -f "$STDIN_FILE"' EXIT
|
|
3852
|
+
cat > "$STDIN_FILE"
|
|
3853
|
+
node "${hookScript}" "${event}" "${configFile}" < "$STDIN_FILE"
|
|
3854
|
+
`;
|
|
3855
|
+
}
|
|
3856
|
+
copySettingsToDir(settingsDir, targetDir) {
|
|
3857
|
+
try {
|
|
3858
|
+
const sourceFile = join2(settingsDir, ".gemini", "settings.json");
|
|
3859
|
+
const targetGeminiDir = join2(targetDir, ".gemini");
|
|
3860
|
+
mkdirSync2(targetGeminiDir, { recursive: true, mode: 448 });
|
|
3861
|
+
const content = readFileSync2(sourceFile, "utf-8");
|
|
3862
|
+
writeFileSync3(join2(targetGeminiDir, "settings.json"), content, { mode: 384 });
|
|
3863
|
+
} catch {
|
|
3864
|
+
}
|
|
3865
|
+
}
|
|
3866
|
+
};
|
|
3867
|
+
|
|
3450
3868
|
// src/commands/pipeline.ts
|
|
3451
3869
|
function createProviderRegistry() {
|
|
3452
3870
|
const registry = new ProviderRegistry();
|
|
3453
3871
|
registry.register(new ClaudeProvider());
|
|
3454
3872
|
registry.register(new CodexProvider());
|
|
3873
|
+
registry.register(new GeminiProvider());
|
|
3455
3874
|
return registry;
|
|
3456
3875
|
}
|
|
3457
3876
|
function readMcpConfigAsProviderConfig(filePath) {
|
|
3458
|
-
const raw = JSON.parse(
|
|
3877
|
+
const raw = JSON.parse(readFileSync3(filePath, "utf-8"));
|
|
3459
3878
|
return { servers: raw.mcpServers };
|
|
3460
3879
|
}
|
|
3461
3880
|
function parseArgs(args) {
|
|
@@ -3543,7 +3962,7 @@ Flags:
|
|
|
3543
3962
|
--max-iterations <n> Override max iterations per ticket
|
|
3544
3963
|
--max-budget <usd> Per-ticket budget cap (USD)
|
|
3545
3964
|
--model <model> Override model preference
|
|
3546
|
-
--provider <id> Override default provider (claude, codex)
|
|
3965
|
+
--provider <id> Override default provider (claude, codex, gemini)
|
|
3547
3966
|
--concurrency <n> Override concurrency per column
|
|
3548
3967
|
--log-retention <d> Log retention in days (default: 7)
|
|
3549
3968
|
--yes, -y Skip safety confirmation`);
|
|
@@ -3552,28 +3971,28 @@ Flags:
|
|
|
3552
3971
|
return { boardId, once, dryRun, columnFilter, maxIterations, maxBudget, model, provider, concurrency, logRetention, yes };
|
|
3553
3972
|
}
|
|
3554
3973
|
function pidDir(boardId) {
|
|
3555
|
-
return
|
|
3974
|
+
return join3(homedir2(), ".kantban", "pipelines", boardId);
|
|
3556
3975
|
}
|
|
3557
3976
|
function pidFilePath(boardId) {
|
|
3558
|
-
return
|
|
3977
|
+
return join3(pidDir(boardId), "orchestrator.pid");
|
|
3559
3978
|
}
|
|
3560
3979
|
function writePidFile(boardId) {
|
|
3561
3980
|
const dir = pidDir(boardId);
|
|
3562
|
-
|
|
3563
|
-
|
|
3981
|
+
mkdirSync3(dir, { recursive: true, mode: 448 });
|
|
3982
|
+
writeFileSync4(pidFilePath(boardId), String(process.pid), { mode: 384 });
|
|
3564
3983
|
}
|
|
3565
3984
|
function removePidFile(boardId) {
|
|
3566
3985
|
try {
|
|
3567
|
-
|
|
3986
|
+
unlinkSync3(pidFilePath(boardId));
|
|
3568
3987
|
} catch {
|
|
3569
3988
|
}
|
|
3570
3989
|
}
|
|
3571
3990
|
function childManifestPath(boardId) {
|
|
3572
|
-
return
|
|
3991
|
+
return join3(pidDir(boardId), "children.pid");
|
|
3573
3992
|
}
|
|
3574
3993
|
function readChildManifest(boardId) {
|
|
3575
3994
|
try {
|
|
3576
|
-
const contents =
|
|
3995
|
+
const contents = readFileSync3(childManifestPath(boardId), "utf-8");
|
|
3577
3996
|
return contents.split("\n").map((l) => l.trim()).filter((l) => l !== "").map(Number).filter((n) => !isNaN(n) && n > 0);
|
|
3578
3997
|
} catch {
|
|
3579
3998
|
return [];
|
|
@@ -3581,15 +4000,15 @@ function readChildManifest(boardId) {
|
|
|
3581
4000
|
}
|
|
3582
4001
|
function removeChildManifest(boardId) {
|
|
3583
4002
|
try {
|
|
3584
|
-
|
|
4003
|
+
unlinkSync3(childManifestPath(boardId));
|
|
3585
4004
|
} catch {
|
|
3586
4005
|
}
|
|
3587
4006
|
}
|
|
3588
4007
|
function cleanupOrphanedProcesses(boardId) {
|
|
3589
4008
|
const pidPath = pidFilePath(boardId);
|
|
3590
|
-
if (
|
|
4009
|
+
if (existsSync3(pidPath)) {
|
|
3591
4010
|
try {
|
|
3592
|
-
const stalePid = parseInt(
|
|
4011
|
+
const stalePid = parseInt(readFileSync3(pidPath, "utf-8").trim(), 10);
|
|
3593
4012
|
if (stalePid && stalePid !== process.pid) {
|
|
3594
4013
|
try {
|
|
3595
4014
|
process.kill(stalePid, 0);
|
|
@@ -3614,10 +4033,10 @@ function cleanupOrphanedProcesses(boardId) {
|
|
|
3614
4033
|
console.log(`Killed ${String(manifestPids.length)} orphaned child process(es) from manifest`);
|
|
3615
4034
|
removeChildManifest(boardId);
|
|
3616
4035
|
}
|
|
3617
|
-
const staleReaperPath =
|
|
4036
|
+
const staleReaperPath = join3(pidDir(boardId), "reaper.pid");
|
|
3618
4037
|
try {
|
|
3619
|
-
if (
|
|
3620
|
-
const reaperPid = parseInt(
|
|
4038
|
+
if (existsSync3(staleReaperPath)) {
|
|
4039
|
+
const reaperPid = parseInt(readFileSync3(staleReaperPath, "utf-8").trim(), 10);
|
|
3621
4040
|
if (reaperPid && !isNaN(reaperPid)) {
|
|
3622
4041
|
try {
|
|
3623
4042
|
process.kill(reaperPid, 0);
|
|
@@ -3626,7 +4045,7 @@ function cleanupOrphanedProcesses(boardId) {
|
|
|
3626
4045
|
} catch {
|
|
3627
4046
|
}
|
|
3628
4047
|
}
|
|
3629
|
-
|
|
4048
|
+
unlinkSync3(staleReaperPath);
|
|
3630
4049
|
}
|
|
3631
4050
|
} catch {
|
|
3632
4051
|
}
|
|
@@ -3673,15 +4092,15 @@ async function runPipeline(client, args) {
|
|
|
3673
4092
|
return;
|
|
3674
4093
|
}
|
|
3675
4094
|
}
|
|
3676
|
-
const gateFilePath =
|
|
4095
|
+
const gateFilePath = join3(process.cwd(), "pipeline.gates.yaml");
|
|
3677
4096
|
let gateConfig;
|
|
3678
|
-
if (!
|
|
4097
|
+
if (!existsSync3(gateFilePath)) {
|
|
3679
4098
|
console.error(`Error: pipeline.gates.yaml not found in ${process.cwd()}`);
|
|
3680
4099
|
console.error('Run "kantban pipeline init" to generate a starter gate file.');
|
|
3681
4100
|
process.exit(1);
|
|
3682
4101
|
}
|
|
3683
4102
|
try {
|
|
3684
|
-
const gateYaml =
|
|
4103
|
+
const gateYaml = readFileSync3(gateFilePath, "utf-8");
|
|
3685
4104
|
gateConfig = parseGateConfig(gateYaml);
|
|
3686
4105
|
console.log(`Gate config loaded: ${gateConfig.default.length} default gate(s)`);
|
|
3687
4106
|
} catch (err) {
|
|
@@ -3709,7 +4128,7 @@ async function runPipeline(client, args) {
|
|
|
3709
4128
|
intelligence_provider: opts.provider ?? void 0
|
|
3710
4129
|
};
|
|
3711
4130
|
const intelligenceProvider = registry.resolveForIntelligence(boardProviderConfig);
|
|
3712
|
-
const logBaseDir =
|
|
4131
|
+
const logBaseDir = join3(homedir2(), ".kantban", "pipelines");
|
|
3713
4132
|
const logger = new PipelineLogger(logBaseDir, opts.boardId);
|
|
3714
4133
|
logger.pruneOldLogs(opts.logRetention);
|
|
3715
4134
|
let runMemory = null;
|
|
@@ -3746,7 +4165,7 @@ async function runPipeline(client, args) {
|
|
|
3746
4165
|
effectiveConfig.model = registry.resolveModel(columnProvider, effectiveConfig.model);
|
|
3747
4166
|
}
|
|
3748
4167
|
const columnGates = resolveGatesForColumn(gateConfig, resolvedColumnName);
|
|
3749
|
-
const gateCwd = effectiveConfig.worktreeName ?
|
|
4168
|
+
const gateCwd = effectiveConfig.worktreeName ? join3(process.cwd(), effectiveConfig.worktreeName) : void 0;
|
|
3750
4169
|
const effectiveMcpConfigPath = columnGates.length > 0 ? generateGateProxyMcpConfig(
|
|
3751
4170
|
client.baseUrl,
|
|
3752
4171
|
client.token,
|
|
@@ -4195,7 +4614,7 @@ Received ${signal}. Shutting down gracefully...`);
|
|
|
4195
4614
|
cleanupOrphanedProcesses(opts.boardId);
|
|
4196
4615
|
writePidFile(opts.boardId);
|
|
4197
4616
|
logger.orchestrator(`PID file written: ${String(process.pid)}`);
|
|
4198
|
-
const reaperPidPath =
|
|
4617
|
+
const reaperPidPath = join3(pidDir(opts.boardId), "reaper.pid");
|
|
4199
4618
|
const reaperProcess = spawnReaper({
|
|
4200
4619
|
orchestratorPid: process.pid,
|
|
4201
4620
|
manifestPath: childManifestPath(opts.boardId),
|
|
@@ -4358,7 +4777,7 @@ async function stopPipeline(args) {
|
|
|
4358
4777
|
const pidFile = pidFilePath(boardId);
|
|
4359
4778
|
let pid;
|
|
4360
4779
|
try {
|
|
4361
|
-
const content =
|
|
4780
|
+
const content = readFileSync3(pidFile, "utf8").trim();
|
|
4362
4781
|
pid = Number(content);
|
|
4363
4782
|
if (isNaN(pid) || pid <= 0) {
|
|
4364
4783
|
throw new Error("Invalid PID");
|
|
@@ -4389,4 +4808,4 @@ export {
|
|
|
4389
4808
|
runPipeline,
|
|
4390
4809
|
stopPipeline
|
|
4391
4810
|
};
|
|
4392
|
-
//# sourceMappingURL=pipeline-
|
|
4811
|
+
//# sourceMappingURL=pipeline-IYKEQZWN.js.map
|