karajan-code 1.15.0 → 1.17.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.
Files changed (69) hide show
  1. package/package.json +1 -1
  2. package/src/activity-log.js +13 -13
  3. package/src/agents/availability.js +2 -3
  4. package/src/agents/claude-agent.js +42 -21
  5. package/src/agents/model-registry.js +1 -1
  6. package/src/becaria/dispatch.js +1 -1
  7. package/src/becaria/repo.js +3 -3
  8. package/src/cli.js +6 -2
  9. package/src/commands/doctor.js +154 -108
  10. package/src/commands/init.js +101 -90
  11. package/src/commands/plan.js +1 -1
  12. package/src/commands/report.js +77 -71
  13. package/src/commands/roles.js +0 -1
  14. package/src/commands/run.js +2 -3
  15. package/src/config.js +157 -89
  16. package/src/git/automation.js +3 -4
  17. package/src/guards/policy-resolver.js +3 -3
  18. package/src/mcp/orphan-guard.js +1 -2
  19. package/src/mcp/progress.js +4 -3
  20. package/src/mcp/run-kj.js +2 -0
  21. package/src/mcp/server-handlers.js +294 -241
  22. package/src/mcp/server.js +4 -3
  23. package/src/mcp/tools.js +19 -0
  24. package/src/orchestrator/agent-fallback.js +1 -3
  25. package/src/orchestrator/iteration-stages.js +206 -170
  26. package/src/orchestrator/pre-loop-stages.js +266 -34
  27. package/src/orchestrator/solomon-rules.js +2 -2
  28. package/src/orchestrator.js +820 -739
  29. package/src/planning-game/adapter.js +23 -20
  30. package/src/planning-game/architect-adrs.js +45 -0
  31. package/src/planning-game/client.js +15 -1
  32. package/src/planning-game/decomposition.js +7 -5
  33. package/src/prompts/architect.js +88 -0
  34. package/src/prompts/discover.js +228 -0
  35. package/src/prompts/planner.js +53 -33
  36. package/src/prompts/triage.js +8 -16
  37. package/src/review/parser.js +18 -19
  38. package/src/review/profiles.js +2 -2
  39. package/src/review/schema.js +3 -3
  40. package/src/review/scope-filter.js +3 -4
  41. package/src/roles/architect-role.js +122 -0
  42. package/src/roles/commiter-role.js +2 -2
  43. package/src/roles/discover-role.js +122 -0
  44. package/src/roles/index.js +2 -0
  45. package/src/roles/planner-role.js +54 -38
  46. package/src/roles/refactorer-role.js +8 -7
  47. package/src/roles/researcher-role.js +6 -7
  48. package/src/roles/reviewer-role.js +4 -5
  49. package/src/roles/security-role.js +3 -4
  50. package/src/roles/solomon-role.js +6 -18
  51. package/src/roles/sonar-role.js +5 -1
  52. package/src/roles/tester-role.js +8 -5
  53. package/src/roles/triage-role.js +2 -2
  54. package/src/session-cleanup.js +29 -24
  55. package/src/session-store.js +1 -1
  56. package/src/sonar/api.js +1 -1
  57. package/src/sonar/manager.js +1 -1
  58. package/src/sonar/project-key.js +5 -5
  59. package/src/sonar/scanner.js +34 -65
  60. package/src/utils/display.js +312 -272
  61. package/src/utils/git.js +3 -3
  62. package/src/utils/logger.js +6 -1
  63. package/src/utils/model-selector.js +5 -5
  64. package/src/utils/process.js +80 -102
  65. package/src/utils/rate-limit-detector.js +13 -13
  66. package/src/utils/run-log.js +55 -52
  67. package/templates/roles/architect.md +62 -0
  68. package/templates/roles/discover.md +167 -0
  69. package/templates/roles/planner.md +1 -0
@@ -85,18 +85,7 @@ async function runWizard(config, logger) {
85
85
  return config;
86
86
  }
87
87
 
88
- export async function initCommand({ logger, flags = {} }) {
89
- const karajanHome = getKarajanHome();
90
- await ensureDir(karajanHome);
91
- logger.info(`Ensured ${karajanHome} exists`);
92
-
93
- const configPath = getConfigPath();
94
- const reviewRulesPath = path.resolve(process.cwd(), "review-rules.md");
95
- const coderRulesPath = path.resolve(process.cwd(), "coder-rules.md");
96
-
97
- const { config, exists: configExists } = await loadConfig();
98
- const interactive = flags.noInteractive !== true && isTTY();
99
-
88
+ async function handleConfigSetup({ config, configExists, interactive, configPath, logger }) {
100
89
  if (configExists && interactive) {
101
90
  const wizard = createWizard();
102
91
  try {
@@ -119,95 +108,117 @@ export async function initCommand({ logger, flags = {} }) {
119
108
  await writeConfig(configPath, config);
120
109
  logger.info(`Created ${configPath}`);
121
110
  }
111
+ }
122
112
 
123
- if (!(await exists(reviewRulesPath))) {
124
- await fs.writeFile(
125
- reviewRulesPath,
126
- "# Review Rules\n\n- Focus on security, correctness, and test coverage.\n",
127
- "utf8"
128
- );
129
- logger.info("Created review-rules.md");
113
+ async function ensureReviewRules(reviewRulesPath, logger) {
114
+ if (await exists(reviewRulesPath)) return;
115
+ await fs.writeFile(
116
+ reviewRulesPath,
117
+ "# Review Rules\n\n- Focus on security, correctness, and test coverage.\n",
118
+ "utf8"
119
+ );
120
+ logger.info("Created review-rules.md");
121
+ }
122
+
123
+ async function ensureCoderRules(coderRulesPath, logger) {
124
+ if (await exists(coderRulesPath)) return;
125
+ const templatePath = path.resolve(import.meta.dirname, "../../templates/coder-rules.md");
126
+ let content;
127
+ try {
128
+ content = await fs.readFile(templatePath, "utf8");
129
+ } catch {
130
+ content = [
131
+ "# Coder Rules",
132
+ "",
133
+ "## File modification safety",
134
+ "",
135
+ "- NEVER overwrite existing files entirely. Always make targeted, minimal edits.",
136
+ "- After each edit, verify with `git diff` that ONLY the intended lines changed.",
137
+ "- Do not modify code unrelated to the task.",
138
+ ""
139
+ ].join("\n");
130
140
  }
141
+ await fs.writeFile(coderRulesPath, content, "utf8");
142
+ logger.info("Created coder-rules.md");
143
+ }
131
144
 
132
- if (!(await exists(coderRulesPath))) {
133
- const templatePath = path.resolve(import.meta.dirname, "../../templates/coder-rules.md");
134
- let content;
135
- try {
136
- content = await fs.readFile(templatePath, "utf8");
137
- } catch {
138
- content = [
139
- "# Coder Rules",
140
- "",
141
- "## File modification safety",
142
- "",
143
- "- NEVER overwrite existing files entirely. Always make targeted, minimal edits.",
144
- "- After each edit, verify with `git diff` that ONLY the intended lines changed.",
145
- "- Do not modify code unrelated to the task.",
146
- ""
147
- ].join("\n");
145
+ async function setupSonarQube(config, logger) {
146
+ if (config.sonarqube?.enabled === false) {
147
+ logger.info("SonarQube disabled — skipping container setup.");
148
+ return;
149
+ }
150
+ const vmCheck = await checkVmMaxMapCount(os.platform());
151
+ if (!vmCheck.ok) {
152
+ logger.warn(`vm.max_map_count check failed: ${vmCheck.reason}`);
153
+ if (vmCheck.fix) {
154
+ logger.warn(`Fix: ${vmCheck.fix}`);
148
155
  }
149
- await fs.writeFile(coderRulesPath, content, "utf8");
150
- logger.info("Created coder-rules.md");
151
156
  }
152
157
 
153
- if (config.sonarqube?.enabled !== false) {
154
- const vmCheck = await checkVmMaxMapCount(os.platform());
155
- if (!vmCheck.ok) {
156
- logger.warn(`vm.max_map_count check failed: ${vmCheck.reason}`);
157
- if (vmCheck.fix) {
158
- logger.warn(`Fix: ${vmCheck.fix}`);
159
- }
160
- }
158
+ const sonar = await sonarUp();
159
+ if (sonar.exitCode !== 0) {
160
+ throw new Error(`Failed to start SonarQube: ${sonar.stderr || sonar.stdout}`);
161
+ }
161
162
 
162
- const sonar = await sonarUp();
163
- if (sonar.exitCode !== 0) {
164
- throw new Error(`Failed to start SonarQube: ${sonar.stderr || sonar.stdout}`);
165
- }
163
+ logger.info("SonarQube container started");
164
+ logger.info("");
165
+ logger.info("To configure the SonarQube token:");
166
+ logger.info(" 1. Open http://localhost:9000");
167
+ logger.info(" 2. Log in (default credentials: admin / admin)");
168
+ logger.info(" 3. Go to: My Account > Security > Generate Token");
169
+ logger.info(" 4. Name: karajan-cli, Type: Global Analysis Token");
170
+ logger.info(" 5. Set the token in ~/.karajan/kj.config.yml under sonarqube.token");
171
+ logger.info(' or export KJ_SONAR_TOKEN="<your-token>"');
172
+ }
166
173
 
167
- logger.info("SonarQube container started");
174
+ async function scaffoldBecariaGateway(config, flags, logger) {
175
+ if (!config.becaria?.enabled && !flags.scaffoldBecaria) return;
176
+ const projectDir = process.cwd();
177
+ const workflowDir = path.join(projectDir, ".github", "workflows");
178
+ await ensureDir(workflowDir);
168
179
 
169
- logger.info("");
170
- logger.info("To configure the SonarQube token:");
171
- logger.info(" 1. Open http://localhost:9000");
172
- logger.info(" 2. Log in (default credentials: admin / admin)");
173
- logger.info(" 3. Go to: My Account > Security > Generate Token");
174
- logger.info(" 4. Name: karajan-cli, Type: Global Analysis Token");
175
- logger.info(" 5. Set the token in ~/.karajan/kj.config.yml under sonarqube.token");
176
- logger.info(' or export KJ_SONAR_TOKEN="<your-token>"');
177
- } else {
178
- logger.info("SonarQube disabled — skipping container setup.");
179
- }
180
+ const templatesDir = path.resolve(import.meta.dirname, "../../templates/workflows");
181
+ const workflows = ["becaria-gateway.yml", "automerge.yml", "houston-override.yml"];
180
182
 
181
- // --- BecarIA Gateway scaffolding ---
182
- if (config.becaria?.enabled || flags.scaffoldBecaria) {
183
- const projectDir = process.cwd();
184
- const workflowDir = path.join(projectDir, ".github", "workflows");
185
- await ensureDir(workflowDir);
186
-
187
- const templatesDir = path.resolve(import.meta.dirname, "../../templates/workflows");
188
- const workflows = ["becaria-gateway.yml", "automerge.yml", "houston-override.yml"];
189
-
190
- for (const wf of workflows) {
191
- const destPath = path.join(workflowDir, wf);
192
- if (!(await exists(destPath))) {
193
- const srcPath = path.join(templatesDir, wf);
194
- try {
195
- const content = await fs.readFile(srcPath, "utf8");
196
- await fs.writeFile(destPath, content, "utf8");
197
- logger.info(`Created ${path.relative(projectDir, destPath)}`);
198
- } catch (err) {
199
- logger.warn(`Could not scaffold ${wf}: ${err.message}`);
200
- }
201
- } else {
202
- logger.info(`${wf} already exists — skipping`);
183
+ for (const wf of workflows) {
184
+ const destPath = path.join(workflowDir, wf);
185
+ if (await exists(destPath)) {
186
+ logger.info(`${wf} already exists skipping`);
187
+ } else {
188
+ const srcPath = path.join(templatesDir, wf);
189
+ try {
190
+ const content = await fs.readFile(srcPath, "utf8");
191
+ await fs.writeFile(destPath, content, "utf8");
192
+ logger.info(`Created ${path.relative(projectDir, destPath)}`);
193
+ } catch (err) {
194
+ logger.warn(`Could not scaffold ${wf}: ${err.message}`);
203
195
  }
204
196
  }
205
-
206
- logger.info("");
207
- logger.info("BecarIA Gateway scaffolded. Next steps:");
208
- logger.info(" 1. Create a GitHub App named 'becaria-reviewer' with pull_request write permissions");
209
- logger.info(" 2. Install the App on your repository");
210
- logger.info(" 3. Add secrets: BECARIA_APP_ID and BECARIA_APP_PRIVATE_KEY");
211
- logger.info(" 4. Push the workflow files and enable 'kj run --enable-becaria'");
212
197
  }
198
+
199
+ logger.info("");
200
+ logger.info("BecarIA Gateway scaffolded. Next steps:");
201
+ logger.info(" 1. Create a GitHub App named 'becaria-reviewer' with pull_request write permissions");
202
+ logger.info(" 2. Install the App on your repository");
203
+ logger.info(" 3. Add secrets: BECARIA_APP_ID and BECARIA_APP_PRIVATE_KEY");
204
+ logger.info(" 4. Push the workflow files and enable 'kj run --enable-becaria'");
205
+ }
206
+
207
+ export async function initCommand({ logger, flags = {} }) {
208
+ const karajanHome = getKarajanHome();
209
+ await ensureDir(karajanHome);
210
+ logger.info(`Ensured ${karajanHome} exists`);
211
+
212
+ const configPath = getConfigPath();
213
+ const reviewRulesPath = path.resolve(process.cwd(), "review-rules.md");
214
+ const coderRulesPath = path.resolve(process.cwd(), "coder-rules.md");
215
+
216
+ const { config, exists: configExists } = await loadConfig();
217
+ const interactive = flags.noInteractive !== true && isTTY();
218
+
219
+ await handleConfigSetup({ config, configExists, interactive, configPath, logger });
220
+ await ensureReviewRules(reviewRulesPath, logger);
221
+ await ensureCoderRules(coderRulesPath, logger);
222
+ await setupSonarQube(config, logger);
223
+ await scaffoldBecariaGateway(config, flags, logger);
213
224
  }
@@ -65,7 +65,7 @@ export async function planCommand({ task, config, logger, json, context }) {
65
65
  return;
66
66
  }
67
67
 
68
- if (parsed && parsed.approach) {
68
+ if (parsed?.approach) {
69
69
  console.log(formatPlan(parsed));
70
70
  } else {
71
71
  console.log(result.output);
@@ -9,7 +9,7 @@ function parseBudgetFromActivityLog(logText) {
9
9
  return { consumed_usd: null, limit_usd: null };
10
10
  }
11
11
 
12
- const regex = /Budget:\s*\$([0-9]+(?:\.[0-9]+)?)\s*\/\s*\$([0-9]+(?:\.[0-9]+)?)/g;
12
+ const regex = /Budget:\s*\$(\d+(?:\.\d+)?)\s*\/\s*\$(\d+(?:\.\d+)?)/g;
13
13
  let match;
14
14
  let last = null;
15
15
  while ((match = regex.exec(logText)) !== null) {
@@ -61,7 +61,7 @@ function summarizePlan(checkpoints = []) {
61
61
 
62
62
  const uniqueOrdered = [];
63
63
  for (const stage of stages) {
64
- if (uniqueOrdered[uniqueOrdered.length - 1] !== stage) {
64
+ if (uniqueOrdered.at(-1) !== stage) {
65
65
  uniqueOrdered.push(stage);
66
66
  }
67
67
  }
@@ -79,7 +79,7 @@ function summarizeSonar(checkpoints = []) {
79
79
  }
80
80
 
81
81
  const initial = sonarPoints[0];
82
- const final = sonarPoints[sonarPoints.length - 1];
82
+ const final = sonarPoints.at(-1);
83
83
  return {
84
84
  initial,
85
85
  final,
@@ -156,14 +156,13 @@ async function buildReport(dir, sessionId) {
156
156
  }
157
157
 
158
158
  function printTextReport(report) {
159
- const budgetText =
160
- typeof report.budget_consumed?.consumed_usd === "number"
161
- ? `$${report.budget_consumed.consumed_usd.toFixed(2)}${
162
- typeof report.budget_consumed?.limit_usd === "number"
163
- ? ` / $${report.budget_consumed.limit_usd.toFixed(2)}`
164
- : ""
165
- }`
166
- : "N/A";
159
+ let budgetText = "N/A";
160
+ if (typeof report.budget_consumed?.consumed_usd === "number") {
161
+ const limitSuffix = typeof report.budget_consumed?.limit_usd === "number"
162
+ ? ` / $${report.budget_consumed.limit_usd.toFixed(2)}`
163
+ : "";
164
+ budgetText = `$${report.budget_consumed.consumed_usd.toFixed(2)}${limitSuffix}`;
165
+ }
167
166
 
168
167
  const planText = report.plan_executed.length > 0 ? report.plan_executed.join(" -> ") : "N/A";
169
168
  const iterationText =
@@ -318,62 +317,60 @@ async function findSessionsByPgTask(dir, pgTask) {
318
317
  // skip malformed sessions
319
318
  }
320
319
  }
321
- return matches.sort();
320
+ return matches.sort((a, b) => a.localeCompare(b));
322
321
  }
323
322
 
324
- export async function reportCommand({ list = false, sessionId = null, format = "text", trace = false, currency = "usd", pgTask = null }) {
325
- const dir = getSessionRoot();
326
- if (!(await exists(dir))) {
327
- console.log("No reports yet");
328
- return;
329
- }
323
+ async function resolveTraceOptions(currency) {
324
+ const { config } = await loadConfig();
325
+ const cur = currency?.toLowerCase() || config?.budget?.currency || "usd";
326
+ const rate = config?.budget?.exchange_rate_eur ?? 0.92;
327
+ return { cur, rate };
328
+ }
330
329
 
331
- const entries = await fs.readdir(dir);
330
+ function printTraceReport(report, currency, exchangeRate) {
331
+ console.log(`Session: ${report.session_id}`);
332
+ if (report.pg_task_id) {
333
+ const projectLabel = report.pg_project_id ? ` (${report.pg_project_id})` : "";
334
+ console.log(`Planning Game Card: ${report.pg_task_id}${projectLabel}`);
335
+ }
336
+ console.log(`Status: ${report.status}`);
337
+ console.log(`Task: ${report.task_description || "N/A"}`);
338
+ console.log("");
339
+ printTraceTable(report.budget_trace, { currency, exchangeRate });
340
+ }
332
341
 
333
- if (pgTask) {
334
- const matches = await findSessionsByPgTask(dir, pgTask);
335
- if (matches.length === 0) {
336
- console.log(`No sessions found for card: ${pgTask}`);
337
- return;
338
- }
339
- if (list) {
340
- for (const item of matches) console.log(item);
341
- return;
342
- }
343
- // Show all reports for this card, or the latest if no list flag
344
- const targetId = sessionId || matches.at(-1);
345
- const report = await buildReport(dir, targetId);
346
- if (format === "json") {
347
- console.log(JSON.stringify({ card: pgTask, sessions: matches, report }, null, 2));
348
- return;
349
- }
350
- console.log(`Card ${pgTask}: ${matches.length} session${matches.length === 1 ? "" : "s"}`);
351
- for (const m of matches) {
352
- const marker = m === targetId ? " <--" : "";
353
- console.log(` ${m}${marker}`);
354
- }
355
- console.log("");
356
- if (trace) {
357
- const { config } = await loadConfig();
358
- const cur = currency?.toLowerCase() || config?.budget?.currency || "usd";
359
- const rate = config?.budget?.exchange_rate_eur ?? 0.92;
360
- console.log(`Session: ${report.session_id}`);
361
- console.log(`Status: ${report.status}`);
362
- console.log(`Task: ${report.task_description || "N/A"}`);
363
- console.log("");
364
- printTraceTable(report.budget_trace, { currency: cur, exchangeRate: rate });
365
- } else {
366
- printTextReport(report);
367
- }
342
+ async function handlePgTaskReport({ dir, pgTask, list, sessionId, format, trace, currency }) {
343
+ const matches = await findSessionsByPgTask(dir, pgTask);
344
+ if (matches.length === 0) {
345
+ console.log(`No sessions found for card: ${pgTask}`);
368
346
  return;
369
347
  }
370
-
371
348
  if (list) {
372
- for (const item of entries) console.log(item);
349
+ for (const item of matches) console.log(item);
350
+ return;
351
+ }
352
+ const targetId = sessionId || matches.at(-1);
353
+ const report = await buildReport(dir, targetId);
354
+ if (format === "json") {
355
+ console.log(JSON.stringify({ card: pgTask, sessions: matches, report }, null, 2));
373
356
  return;
374
357
  }
358
+ console.log(`Card ${pgTask}: ${matches.length} session${matches.length === 1 ? "" : "s"}`);
359
+ for (const m of matches) {
360
+ const marker = m === targetId ? " <--" : "";
361
+ console.log(` ${m}${marker}`);
362
+ }
363
+ console.log("");
364
+ if (trace) {
365
+ const { cur, rate } = await resolveTraceOptions(currency);
366
+ printTraceReport(report, cur, rate);
367
+ } else {
368
+ printTextReport(report);
369
+ }
370
+ }
375
371
 
376
- const ids = [...entries].sort();
372
+ async function handleSingleSessionReport({ dir, entries, sessionId, format, trace, currency }) {
373
+ const ids = [...entries].sort((a, b) => a.localeCompare(b));
377
374
  const selectedSessionId = sessionId || ids.at(-1);
378
375
  if (!selectedSessionId) {
379
376
  console.log("No reports yet");
@@ -388,24 +385,33 @@ export async function reportCommand({ list = false, sessionId = null, format = "
388
385
  console.log(JSON.stringify(report, null, 2));
389
386
  return;
390
387
  }
391
-
392
388
  if (trace) {
393
- const { config } = await loadConfig();
394
- const cur = currency?.toLowerCase() || config?.budget?.currency || "usd";
395
- const rate = config?.budget?.exchange_rate_eur ?? 0.92;
396
- console.log(`Session: ${report.session_id}`);
397
- if (report.pg_task_id) {
398
- const projectLabel = report.pg_project_id ? ` (${report.pg_project_id})` : "";
399
- console.log(`Planning Game Card: ${report.pg_task_id}${projectLabel}`);
400
- }
401
- console.log(`Status: ${report.status}`);
402
- console.log(`Task: ${report.task_description || "N/A"}`);
403
- console.log("");
404
- printTraceTable(report.budget_trace, { currency: cur, exchangeRate: rate });
389
+ const { cur, rate } = await resolveTraceOptions(currency);
390
+ printTraceReport(report, cur, rate);
405
391
  return;
406
392
  }
407
-
408
393
  printTextReport(report);
409
394
  }
410
395
 
396
+ export async function reportCommand({ list = false, sessionId = null, format = "text", trace = false, currency = "usd", pgTask = null }) {
397
+ const dir = getSessionRoot();
398
+ if (!(await exists(dir))) {
399
+ console.log("No reports yet");
400
+ return;
401
+ }
402
+
403
+ const entries = await fs.readdir(dir);
404
+
405
+ if (pgTask) {
406
+ return handlePgTaskReport({ dir, pgTask, list, sessionId, format, trace, currency });
407
+ }
408
+
409
+ if (list) {
410
+ for (const item of entries) console.log(item);
411
+ return;
412
+ }
413
+
414
+ return handleSingleSessionReport({ dir, entries, sessionId, format, trace, currency });
415
+ }
416
+
411
417
  export { formatDuration, convertCost, formatCost, printTraceTable, buildReport, findSessionsByPgTask };
@@ -1,5 +1,4 @@
1
1
  import fs from "node:fs/promises";
2
- import path from "node:path";
3
2
  import { resolveRoleMdPath, loadFirstExisting } from "../roles/base-role.js";
4
3
  import { resolveRole } from "../config.js";
5
4
  import { exists } from "../utils/fs.js";
@@ -25,7 +25,6 @@ export async function runCommandHandler({ task, config, logger, flags }) {
25
25
  // --- Planning Game: resolve card ID ---
26
26
  const pgCardId = flags?.pgTask || parseCardId(task);
27
27
  const pgProject = flags?.pgProject || config.planning_game?.project_id || null;
28
- const enrichedTask = task;
29
28
 
30
29
  const jsonMode = flags?.json;
31
30
 
@@ -48,10 +47,10 @@ export async function runCommandHandler({ task, config, logger, flags }) {
48
47
  });
49
48
 
50
49
  if (!jsonMode) {
51
- printHeader({ task: enrichedTask, config });
50
+ printHeader({ task: task, config });
52
51
  }
53
52
 
54
- const result = await runFlow({ task: enrichedTask, config, logger, flags, emitter, pgTaskId: pgCardId || null, pgProject: pgProject || null });
53
+ const result = await runFlow({ task: task, config, logger, flags, emitter, pgTaskId: pgCardId || null, pgProject: pgProject || null });
55
54
 
56
55
  if (jsonMode) {
57
56
  console.log(JSON.stringify(result, null, 2));