karajan-code 1.31.1 → 1.32.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/package.json +1 -1
- package/src/orchestrator/post-loop-stages.js +109 -0
- package/src/orchestrator.js +32 -4
- package/src/prompts/coder.js +7 -1
- package/src/prompts/reviewer.js +7 -1
- package/src/prompts/rtk-snippet.js +15 -0
- package/src/roles/coder-role.js +2 -1
- package/src/roles/reviewer-role.js +8 -2
- package/src/utils/rtk-detect.js +18 -0
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="docs/karajan-code-logo
|
|
2
|
+
<img src="docs/karajan-code-logo.svg" alt="Karajan Code" width="180">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<h1 align="center">Karajan Code</h1>
|
|
@@ -46,6 +46,7 @@ Use Karajan when you want:
|
|
|
46
46
|
- **Zero-config operation** — auto-detects test frameworks, starts SonarQube, simplifies pipeline for trivial tasks
|
|
47
47
|
- **Composable role architecture** — define agent behaviors as plain markdown files that travel with your project
|
|
48
48
|
- **Local-first** — your code, your keys, your machine, no data leaves unless you say so
|
|
49
|
+
- **Zero API costs** — Karajan uses AI agent CLIs (Claude Code, Codex, Gemini CLI), not APIs. You pay your existing subscription (Claude Pro, ChatGPT Plus), not per-token API fees. No surprise bills.
|
|
49
50
|
|
|
50
51
|
If Claude Code is a smart pair programmer, Karajan is the CI/CD pipeline for AI-assisted development. They work great together — Karajan is designed to be used as an MCP server inside Claude Code.
|
|
51
52
|
|
|
@@ -64,6 +65,8 @@ That's it. No Docker required (SonarQube uses Docker, but Karajan auto-manages i
|
|
|
64
65
|
kj run "Create a utility function that validates Spanish DNI numbers, with tests"
|
|
65
66
|
```
|
|
66
67
|
|
|
68
|
+
[**▶ Watch the full pipeline demo**](https://karajancode.com#demo) — HU certification, triage, architecture, TDD, SonarQube, code review, Solomon arbitration, security audit.
|
|
69
|
+
|
|
67
70
|
Karajan will:
|
|
68
71
|
1. Triage the task complexity and activate the right roles
|
|
69
72
|
2. Write tests first (TDD)
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { TesterRole } from "../roles/tester-role.js";
|
|
2
2
|
import { SecurityRole } from "../roles/security-role.js";
|
|
3
3
|
import { ImpeccableRole } from "../roles/impeccable-role.js";
|
|
4
|
+
import { AuditRole } from "../roles/audit-role.js";
|
|
4
5
|
import { addCheckpoint, saveSession } from "../session-store.js";
|
|
5
6
|
import { emitProgress, makeEvent } from "../utils/events.js";
|
|
6
7
|
import { invokeSolomon } from "./solomon-escalation.js";
|
|
@@ -290,5 +291,113 @@ export async function runImpeccableStage({ config, logger, emitter, eventBase, s
|
|
|
290
291
|
return { action: "ok", stageResult: { ok: impeccableOutput.ok, verdict, summary: impeccableOutput.summary || "No frontend design issues found" } };
|
|
291
292
|
}
|
|
292
293
|
|
|
294
|
+
export async function runFinalAuditStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget, iteration, task, diff }) {
|
|
295
|
+
logger.setContext({ iteration, stage: "audit" });
|
|
296
|
+
emitProgress(
|
|
297
|
+
emitter,
|
|
298
|
+
makeEvent("audit:start", { ...eventBase, stage: "audit" }, {
|
|
299
|
+
message: "Final audit — verifying code quality"
|
|
300
|
+
})
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
const auditStart = Date.now();
|
|
304
|
+
const { output: auditOutput, provider, attempts } = await runRoleWithFallback(
|
|
305
|
+
AuditRole,
|
|
306
|
+
{ roleName: "audit", config, logger, emitter, eventBase, task, iteration, diff }
|
|
307
|
+
);
|
|
308
|
+
const totalDuration = Date.now() - auditStart;
|
|
309
|
+
|
|
310
|
+
trackBudget({
|
|
311
|
+
role: "audit",
|
|
312
|
+
provider: provider || coderRole.provider,
|
|
313
|
+
model: config?.roles?.audit?.model || coderRole.model,
|
|
314
|
+
result: auditOutput,
|
|
315
|
+
duration_ms: totalDuration
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
await addCheckpoint(session, {
|
|
319
|
+
stage: "audit",
|
|
320
|
+
iteration,
|
|
321
|
+
ok: auditOutput.ok,
|
|
322
|
+
provider: provider || coderRole.provider,
|
|
323
|
+
model: config?.roles?.audit?.model || coderRole.model || null,
|
|
324
|
+
attempts: attempts.length > 1 ? attempts : undefined
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
if (!auditOutput.ok) {
|
|
328
|
+
// Audit agent failed to run — treat as advisory, don't block pipeline
|
|
329
|
+
logger.warn(`Audit agent error (advisory): ${auditOutput.summary}`);
|
|
330
|
+
emitProgress(
|
|
331
|
+
emitter,
|
|
332
|
+
makeEvent("audit:end", { ...eventBase, stage: "audit" }, {
|
|
333
|
+
status: "warn",
|
|
334
|
+
message: `Audit: agent error (advisory), continuing — ${auditOutput.summary}`
|
|
335
|
+
})
|
|
336
|
+
);
|
|
337
|
+
return { action: "ok", stageResult: { ok: false, summary: auditOutput.summary || "Audit agent error (advisory)", auto_continued: true } };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Parse findings from audit result
|
|
341
|
+
const result = auditOutput.result || {};
|
|
342
|
+
const summary = result.summary || {};
|
|
343
|
+
const overallHealth = summary.overallHealth || "fair";
|
|
344
|
+
const criticalCount = summary.critical || 0;
|
|
345
|
+
const highCount = summary.high || 0;
|
|
346
|
+
|
|
347
|
+
// Collect critical and high findings for feedback
|
|
348
|
+
const actionableFindings = [];
|
|
349
|
+
if (result.dimensions) {
|
|
350
|
+
for (const [dimName, dim] of Object.entries(result.dimensions)) {
|
|
351
|
+
for (const finding of (dim.findings || [])) {
|
|
352
|
+
if (finding.severity === "critical" || finding.severity === "high") {
|
|
353
|
+
actionableFindings.push({
|
|
354
|
+
dimension: dimName,
|
|
355
|
+
...finding
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const hasActionableIssues = (overallHealth === "poor" || overallHealth === "critical") && (criticalCount > 0 || highCount > 0);
|
|
363
|
+
|
|
364
|
+
if (hasActionableIssues) {
|
|
365
|
+
// Build feedback string for the coder
|
|
366
|
+
const feedbackLines = actionableFindings.map(f => {
|
|
367
|
+
const loc = f.file ? `${f.file}${f.line ? `:${f.line}` : ""}` : "";
|
|
368
|
+
return `[${f.severity.toUpperCase()}] ${loc} ${f.description}${f.recommendation ? ` — Fix: ${f.recommendation}` : ""}`;
|
|
369
|
+
});
|
|
370
|
+
const feedback = `Audit found ${criticalCount + highCount} critical/high issue(s) that must be fixed:\n${feedbackLines.join("\n")}`;
|
|
371
|
+
|
|
372
|
+
logger.warn(`Audit: ${criticalCount + highCount} actionable issues found, sending back to coder`);
|
|
373
|
+
emitProgress(
|
|
374
|
+
emitter,
|
|
375
|
+
makeEvent("audit:end", { ...eventBase, stage: "audit" }, {
|
|
376
|
+
status: "fail",
|
|
377
|
+
message: `Audit: ${criticalCount + highCount} issue(s) found, sending back to coder`
|
|
378
|
+
})
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
return { action: "retry", feedback, stageResult: { ok: false, summary: auditOutput.summary || `${criticalCount + highCount} actionable issues` } };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Audit passed (good/fair or no critical/high findings)
|
|
385
|
+
const hasAdvisory = (summary.medium || 0) + (summary.low || 0) > 0;
|
|
386
|
+
const certifiedMsg = hasAdvisory
|
|
387
|
+
? `Audit: CERTIFIED (with ${(summary.medium || 0) + (summary.low || 0)} advisory warning(s))`
|
|
388
|
+
: "Audit: CERTIFIED";
|
|
389
|
+
|
|
390
|
+
logger.info(certifiedMsg);
|
|
391
|
+
emitProgress(
|
|
392
|
+
emitter,
|
|
393
|
+
makeEvent("audit:end", { ...eventBase, stage: "audit" }, {
|
|
394
|
+
status: "ok",
|
|
395
|
+
message: certifiedMsg
|
|
396
|
+
})
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
return { action: "ok", stageResult: { ok: true, summary: certifiedMsg } };
|
|
400
|
+
}
|
|
401
|
+
|
|
293
402
|
// Exported for testing
|
|
294
403
|
export { buildFallbackChain, isAgentFailure, runRoleWithFallback };
|
package/src/orchestrator.js
CHANGED
|
@@ -32,10 +32,11 @@ import { invokeSolomon } from "./orchestrator/solomon-escalation.js";
|
|
|
32
32
|
import { PipelineContext } from "./orchestrator/pipeline-context.js";
|
|
33
33
|
import { runTriageStage, runResearcherStage, runArchitectStage, runPlannerStage, runDiscoverStage, runHuReviewerStage } from "./orchestrator/pre-loop-stages.js";
|
|
34
34
|
import { runCoderStage, runRefactorerStage, runTddCheckStage, runSonarStage, runSonarCloudStage, runReviewerStage } from "./orchestrator/iteration-stages.js";
|
|
35
|
-
import { runTesterStage, runSecurityStage, runImpeccableStage } from "./orchestrator/post-loop-stages.js";
|
|
35
|
+
import { runTesterStage, runSecurityStage, runImpeccableStage, runFinalAuditStage } from "./orchestrator/post-loop-stages.js";
|
|
36
36
|
import { waitForCooldown, MAX_STANDBY_RETRIES } from "./orchestrator/standby.js";
|
|
37
37
|
import { detectTestFramework } from "./utils/project-detect.js";
|
|
38
38
|
import { runPreflightChecks } from "./orchestrator/preflight-checks.js";
|
|
39
|
+
import { detectRtk } from "./utils/rtk-detect.js";
|
|
39
40
|
|
|
40
41
|
|
|
41
42
|
// --- Extracted helper functions (pure refactoring, zero behavior change) ---
|
|
@@ -70,8 +71,8 @@ async function handleDryRun({ task, config, flags, emitter, pipelineFlags }) {
|
|
|
70
71
|
const projectDir = config.projectDir || process.cwd();
|
|
71
72
|
const { rules: reviewRules } = await resolveReviewProfile({ mode: config.review_mode, projectDir });
|
|
72
73
|
const coderRules = await loadFirstExisting(resolveRoleMdPath("coder", projectDir));
|
|
73
|
-
const coderPrompt = buildCoderPrompt({ task, coderRules, methodology: config.development?.methodology, serenaEnabled: Boolean(config.serena?.enabled) });
|
|
74
|
-
const reviewerPrompt = buildReviewerPrompt({ task, diff: "(dry-run: no diff)", reviewRules, mode: config.review_mode, serenaEnabled: Boolean(config.serena?.enabled) });
|
|
74
|
+
const coderPrompt = buildCoderPrompt({ task, coderRules, methodology: config.development?.methodology, serenaEnabled: Boolean(config.serena?.enabled), rtkAvailable: Boolean(config.rtk?.available) });
|
|
75
|
+
const reviewerPrompt = buildReviewerPrompt({ task, diff: "(dry-run: no diff)", reviewRules, mode: config.review_mode, serenaEnabled: Boolean(config.serena?.enabled), rtkAvailable: Boolean(config.rtk?.available) });
|
|
75
76
|
|
|
76
77
|
const summary = {
|
|
77
78
|
dry_run: true,
|
|
@@ -669,6 +670,22 @@ async function handlePostLoopStages({ config, session, emitter, eventBase, coder
|
|
|
669
670
|
}
|
|
670
671
|
}
|
|
671
672
|
|
|
673
|
+
// Final audit — last quality gate before declaring success
|
|
674
|
+
const auditResult = await runFinalAuditStage({
|
|
675
|
+
config, logger, emitter, eventBase, session, coderRole, trackBudget,
|
|
676
|
+
iteration: i, task, diff: postLoopDiff
|
|
677
|
+
});
|
|
678
|
+
if (auditResult.stageResult) {
|
|
679
|
+
stageResults.audit = auditResult.stageResult;
|
|
680
|
+
await tryBecariaComment({ config, session, logger, agent: "Audit", body: `Final audit: ${auditResult.stageResult.summary || "completed"}` });
|
|
681
|
+
}
|
|
682
|
+
if (auditResult.action === "retry") {
|
|
683
|
+
// Audit found actionable issues — loop back to coder
|
|
684
|
+
session.last_reviewer_feedback = auditResult.feedback;
|
|
685
|
+
await saveSession(session);
|
|
686
|
+
return { action: "continue" };
|
|
687
|
+
}
|
|
688
|
+
|
|
672
689
|
return { action: "proceed" };
|
|
673
690
|
}
|
|
674
691
|
|
|
@@ -1071,6 +1088,17 @@ async function initFlowContext({ task, config, logger, emitter, askQuestion, pgT
|
|
|
1071
1088
|
ctx.budgetSummary = budgetSummary;
|
|
1072
1089
|
ctx.trackBudget = trackBudget;
|
|
1073
1090
|
|
|
1091
|
+
// --- RTK detection ---
|
|
1092
|
+
const rtkResult = await detectRtk();
|
|
1093
|
+
if (rtkResult.available) {
|
|
1094
|
+
config = { ...config, rtk: { available: true, version: rtkResult.version } };
|
|
1095
|
+
logger.info(`RTK detected (${rtkResult.version}) — instructing agents to prefix Bash commands with rtk`);
|
|
1096
|
+
emitProgress(emitter, makeEvent("rtk:detected", ctx.eventBase, {
|
|
1097
|
+
message: "RTK detected — agent commands will use token optimization",
|
|
1098
|
+
detail: { version: rtkResult.version }
|
|
1099
|
+
}));
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1074
1102
|
ctx.session = await initializeSession({ task, config, flags, pgTaskId, pgProject });
|
|
1075
1103
|
ctx.eventBase.sessionId = ctx.session.id;
|
|
1076
1104
|
|
|
@@ -1199,7 +1227,7 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
1199
1227
|
const checkpointIntervalMs = (ctx.config.session.checkpoint_interval_minutes ?? 5) * 60 * 1000;
|
|
1200
1228
|
let lastCheckpointAt = Date.now();
|
|
1201
1229
|
let checkpointDisabled = false;
|
|
1202
|
-
let lastCheckpointSnapshot =
|
|
1230
|
+
let lastCheckpointSnapshot = takeCheckpointSnapshot(ctx.session);
|
|
1203
1231
|
|
|
1204
1232
|
let i = 0;
|
|
1205
1233
|
while (i < ctx.config.max_iterations) {
|
package/src/prompts/coder.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { RTK_INSTRUCTIONS } from "./rtk-snippet.js";
|
|
2
|
+
|
|
1
3
|
const SUBAGENT_PREAMBLE = [
|
|
2
4
|
"IMPORTANT: You are running as a Karajan sub-agent.",
|
|
3
5
|
"Do NOT ask about using Karajan, do NOT mention Karajan, do NOT suggest orchestration.",
|
|
@@ -29,7 +31,7 @@ const SERENA_INSTRUCTIONS = [
|
|
|
29
31
|
"Fall back to reading files only when Serena tools are not sufficient."
|
|
30
32
|
].join("\n");
|
|
31
33
|
|
|
32
|
-
export function buildCoderPrompt({ task, reviewerFeedback = null, sonarSummary = null, coderRules = null, methodology = "tdd", serenaEnabled = false, deferredContext = null }) {
|
|
34
|
+
export function buildCoderPrompt({ task, reviewerFeedback = null, sonarSummary = null, coderRules = null, methodology = "tdd", serenaEnabled = false, rtkAvailable = false, deferredContext = null }) {
|
|
33
35
|
const sections = [
|
|
34
36
|
serenaEnabled ? SUBAGENT_PREAMBLE_SERENA : SUBAGENT_PREAMBLE,
|
|
35
37
|
`Task:\n${task}`,
|
|
@@ -42,6 +44,10 @@ export function buildCoderPrompt({ task, reviewerFeedback = null, sonarSummary =
|
|
|
42
44
|
sections.push(SERENA_INSTRUCTIONS);
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
if (rtkAvailable) {
|
|
48
|
+
sections.push(RTK_INSTRUCTIONS);
|
|
49
|
+
}
|
|
50
|
+
|
|
45
51
|
if (coderRules) {
|
|
46
52
|
sections.push(`Coder rules (MUST follow):\n${coderRules}`);
|
|
47
53
|
}
|
package/src/prompts/reviewer.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { RTK_INSTRUCTIONS } from "./rtk-snippet.js";
|
|
2
|
+
|
|
1
3
|
const SUBAGENT_PREAMBLE = [
|
|
2
4
|
"IMPORTANT: You are running as a Karajan sub-agent.",
|
|
3
5
|
"Do NOT ask about using Karajan, do NOT mention Karajan, do NOT suggest orchestration.",
|
|
@@ -20,7 +22,7 @@ const SERENA_INSTRUCTIONS = [
|
|
|
20
22
|
"Fall back to reading files only when Serena tools are not sufficient."
|
|
21
23
|
].join("\n");
|
|
22
24
|
|
|
23
|
-
export function buildReviewerPrompt({ task, diff, reviewRules, mode, serenaEnabled = false }) {
|
|
25
|
+
export function buildReviewerPrompt({ task, diff, reviewRules, mode, serenaEnabled = false, rtkAvailable = false }) {
|
|
24
26
|
const truncatedDiff = diff.length > 12000 ? `${diff.slice(0, 12000)}\n\n[TRUNCATED]` : diff;
|
|
25
27
|
|
|
26
28
|
const sections = [
|
|
@@ -37,6 +39,10 @@ export function buildReviewerPrompt({ task, diff, reviewRules, mode, serenaEnabl
|
|
|
37
39
|
sections.push(SERENA_INSTRUCTIONS);
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
if (rtkAvailable) {
|
|
43
|
+
sections.push(RTK_INSTRUCTIONS);
|
|
44
|
+
}
|
|
45
|
+
|
|
40
46
|
sections.push(
|
|
41
47
|
`Task context:\n${task}`,
|
|
42
48
|
`Review rules:\n${reviewRules}`,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RTK (Rust Token Killer) prompt instruction snippet.
|
|
3
|
+
* Injected into coder/reviewer prompts when RTK is detected.
|
|
4
|
+
*/
|
|
5
|
+
export const RTK_INSTRUCTIONS = [
|
|
6
|
+
"## Token Optimization (RTK detected)",
|
|
7
|
+
"RTK is installed. Prefix ALL Bash tool calls with `rtk` to reduce token usage:",
|
|
8
|
+
"- Use `rtk git status` instead of `git status`",
|
|
9
|
+
"- Use `rtk git diff` instead of `git diff`",
|
|
10
|
+
"- Use `rtk ls` instead of `ls`",
|
|
11
|
+
"- Use `rtk find` instead of `find`",
|
|
12
|
+
"- Use `rtk grep` instead of `grep`",
|
|
13
|
+
"- Use `rtk cat` instead of `cat`",
|
|
14
|
+
"This does NOT apply to non-Bash tools (Read, Write, Edit, Glob, Grep)."
|
|
15
|
+
].join("\n");
|
package/src/roles/coder-role.js
CHANGED
|
@@ -41,7 +41,8 @@ export class CoderRole extends BaseRole {
|
|
|
41
41
|
deferredContext: deferredContext || null,
|
|
42
42
|
coderRules: this.instructions,
|
|
43
43
|
methodology: this.config?.development?.methodology || "tdd",
|
|
44
|
-
serenaEnabled: Boolean(this.config?.serena?.enabled)
|
|
44
|
+
serenaEnabled: Boolean(this.config?.serena?.enabled),
|
|
45
|
+
rtkAvailable: Boolean(this.config?.rtk?.available)
|
|
45
46
|
});
|
|
46
47
|
|
|
47
48
|
const coderArgs = { prompt, role: "coder" };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BaseRole } from "./base-role.js";
|
|
2
2
|
import { createAgent as defaultCreateAgent } from "../agents/index.js";
|
|
3
|
+
import { RTK_INSTRUCTIONS } from "../prompts/rtk-snippet.js";
|
|
3
4
|
|
|
4
5
|
const MAX_DIFF_LENGTH = 12000;
|
|
5
6
|
|
|
@@ -24,7 +25,7 @@ function truncateDiff(diff) {
|
|
|
24
25
|
: diff;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
function buildPrompt({ task, diff, reviewRules, reviewMode, instructions }) {
|
|
28
|
+
function buildPrompt({ task, diff, reviewRules, reviewMode, instructions, rtkAvailable = false }) {
|
|
28
29
|
const sections = [];
|
|
29
30
|
|
|
30
31
|
sections.push(SUBAGENT_PREAMBLE);
|
|
@@ -41,6 +42,10 @@ function buildPrompt({ task, diff, reviewRules, reviewMode, instructions }) {
|
|
|
41
42
|
`Task context:\n${task}`
|
|
42
43
|
);
|
|
43
44
|
|
|
45
|
+
if (rtkAvailable) {
|
|
46
|
+
sections.push(RTK_INSTRUCTIONS);
|
|
47
|
+
}
|
|
48
|
+
|
|
44
49
|
if (reviewRules) {
|
|
45
50
|
sections.push(`Review rules:\n${reviewRules}`);
|
|
46
51
|
}
|
|
@@ -78,7 +83,8 @@ export class ReviewerRole extends BaseRole {
|
|
|
78
83
|
diff: diff || "",
|
|
79
84
|
reviewRules: reviewRules || null,
|
|
80
85
|
reviewMode: this.config?.review_mode || "standard",
|
|
81
|
-
instructions: this.instructions
|
|
86
|
+
instructions: this.instructions,
|
|
87
|
+
rtkAvailable: Boolean(this.config?.rtk?.available)
|
|
82
88
|
});
|
|
83
89
|
|
|
84
90
|
const reviewArgs = { prompt, role: "reviewer" };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { runCommand } from "./process.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Detect whether RTK (Rust Token Killer) is installed and available.
|
|
5
|
+
* @returns {Promise<{ available: boolean, version: string|null }>}
|
|
6
|
+
*/
|
|
7
|
+
export async function detectRtk() {
|
|
8
|
+
try {
|
|
9
|
+
const result = await runCommand("rtk", ["--version"]);
|
|
10
|
+
if (result.exitCode === 0) {
|
|
11
|
+
const version = (result.stdout || "").trim() || null;
|
|
12
|
+
return { available: true, version };
|
|
13
|
+
}
|
|
14
|
+
return { available: false, version: null };
|
|
15
|
+
} catch {
|
|
16
|
+
return { available: false, version: null };
|
|
17
|
+
}
|
|
18
|
+
}
|