@codacy/gate-cli 0.14.2 → 0.15.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 (2) hide show
  1. package/bin/gate.js +490 -35
  2. package/package.json +1 -1
package/bin/gate.js CHANGED
@@ -10627,7 +10627,18 @@ function rotateIfNeeded(file) {
10627
10627
 
10628
10628
  // src/lib/api-client.ts
10629
10629
  async function apiRequest(options) {
10630
- const { method, path, serviceUrl, token, body, verbose, timeout = 9e4, cmd = "unknown", retry = false } = options;
10630
+ const {
10631
+ method,
10632
+ path,
10633
+ serviceUrl,
10634
+ token,
10635
+ body,
10636
+ verbose,
10637
+ timeout = 9e4,
10638
+ cmd = "unknown",
10639
+ retry = false,
10640
+ encodeBody = false
10641
+ } = options;
10631
10642
  const url = `${serviceUrl}${path}`;
10632
10643
  const headers = {
10633
10644
  "Content-Type": "application/json"
@@ -10636,13 +10647,20 @@ async function apiRequest(options) {
10636
10647
  headers["Authorization"] = `Bearer ${token}`;
10637
10648
  }
10638
10649
  printVerbose(`${method} ${url}`, verbose);
10639
- const serializedBody = body ? JSON.stringify(body) : void 0;
10640
- if (body) {
10650
+ let serializedBody;
10651
+ if (body !== void 0 && body !== null) {
10652
+ const innerJson = JSON.stringify(body);
10653
+ if (encodeBody) {
10654
+ const payload = Buffer.from(innerJson, "utf8").toString("base64");
10655
+ serializedBody = JSON.stringify({ encoding: "base64", payload });
10656
+ } else {
10657
+ serializedBody = innerJson;
10658
+ }
10641
10659
  printVerbose(`Body: ${serializedBody.slice(0, 500)}`, verbose);
10642
10660
  }
10643
10661
  const startedAt = Date.now();
10644
10662
  const bodyBytes = serializedBody ? Buffer.byteLength(serializedBody) : 0;
10645
- const logBase = { cmd, method, url, body_bytes: bodyBytes, retry };
10663
+ const logBase = { cmd, method, url, body_bytes: bodyBytes, retry, encoded: encodeBody };
10646
10664
  let response;
10647
10665
  try {
10648
10666
  response = await fetch(url, {
@@ -10924,6 +10942,9 @@ function registerHooksCommands(program2) {
10924
10942
  });
10925
10943
  }
10926
10944
 
10945
+ // src/commands/intent.ts
10946
+ var import_node_crypto = require("node:crypto");
10947
+
10927
10948
  // src/lib/conversation-buffer.ts
10928
10949
  var import_promises5 = require("node:fs/promises");
10929
10950
  var import_node_fs2 = require("node:fs");
@@ -11052,10 +11073,41 @@ function registerIntentCommands(program2) {
11052
11073
  process.exit(0);
11053
11074
  }
11054
11075
  await appendToConversationBuffer(prompt, event.session_id ?? "");
11076
+ fireClassify(prompt, event.session_id ?? "").catch(() => {
11077
+ });
11055
11078
  } catch {
11056
11079
  }
11057
11080
  process.exit(0);
11058
11081
  });
11082
+ intent.command("classify-body").description("Output JSON body for POST /classify-task (internal, used by hooks)").requiredOption("--session-id <id>", "Session ID").requiredOption("--prompt <text>", "User prompt").requiredOption("--prompt-hash <hash>", "SHA-256 hash of the prompt").action((opts) => {
11083
+ const body = JSON.stringify({
11084
+ session_id: opts.sessionId,
11085
+ user_prompt: opts.prompt.slice(0, 4e3),
11086
+ user_prompt_hash: opts.promptHash
11087
+ });
11088
+ process.stdout.write(body);
11089
+ });
11090
+ }
11091
+ async function fireClassify(prompt, sessionId) {
11092
+ if (!sessionId) return;
11093
+ const tokenResult = await resolveToken();
11094
+ if (!tokenResult.ok) return;
11095
+ const urlResult = await resolveServiceUrl();
11096
+ if (!urlResult.ok) return;
11097
+ const promptHash = (0, import_node_crypto.createHash)("sha256").update(prompt).digest("hex");
11098
+ await apiRequest({
11099
+ method: "POST",
11100
+ path: "/classify-task",
11101
+ serviceUrl: urlResult.data,
11102
+ token: tokenResult.data.token,
11103
+ body: {
11104
+ session_id: sessionId,
11105
+ user_prompt: prompt.slice(0, 4e3),
11106
+ user_prompt_hash: promptHash
11107
+ },
11108
+ timeout: 2e3,
11109
+ cmd: "classify"
11110
+ });
11059
11111
  }
11060
11112
 
11061
11113
  // src/commands/standard.ts
@@ -11234,6 +11286,16 @@ function registerConfigCommands(program2) {
11234
11286
  }
11235
11287
 
11236
11288
  // src/commands/status.ts
11289
+ function timeAgo(isoDate) {
11290
+ const ms = Date.now() - new Date(isoDate).getTime();
11291
+ const mins = Math.floor(ms / 6e4);
11292
+ if (mins < 1) return "just now";
11293
+ if (mins < 60) return `${mins}m ago`;
11294
+ const hrs = Math.floor(mins / 60);
11295
+ if (hrs < 24) return `${hrs}h ago`;
11296
+ const days = Math.floor(hrs / 24);
11297
+ return `${days}d ago`;
11298
+ }
11237
11299
  function registerStatusCommand(program2) {
11238
11300
  program2.command("status").description("Show project quality status").option("--history", "Include recent run history").option("--limit <n>", "Number of history entries", "5").option("--json", "Output raw JSON").action(async (opts) => {
11239
11301
  const globals = program2.opts();
@@ -11306,6 +11368,27 @@ function registerStatusCommand(program2) {
11306
11368
  printInfo(` [${item.priority.toUpperCase()}] ${item.description}`);
11307
11369
  }
11308
11370
  }
11371
+ const recentTasks = mem.recent_tasks;
11372
+ const currentTask = mem.current_task;
11373
+ const contextFiles = mem.context_files;
11374
+ if (recentTasks && recentTasks.length > 0) {
11375
+ printInfo("");
11376
+ printInfo("--- Active Tasks ---");
11377
+ for (const task of recentTasks) {
11378
+ const isCurrent = currentTask && task.id === currentTask.id;
11379
+ const marker = isCurrent ? "\u25CF" : "\u25CB";
11380
+ const runCount = task.run_count ?? 0;
11381
+ const lastAt = task.last_run_at ? timeAgo(task.last_run_at) : "no runs";
11382
+ printInfo(` ${marker} ${task.label} ${runCount} run${runCount === 1 ? "" : "s"}, ${lastAt}`);
11383
+ }
11384
+ if (currentTask) {
11385
+ printInfo("");
11386
+ printInfo(`Current task: ${currentTask.label}`);
11387
+ if (contextFiles && contextFiles.length > 0) {
11388
+ printInfo(` Files in task: ${contextFiles.length} (${contextFiles.slice(0, 3).join(", ")}${contextFiles.length > 3 ? "..." : ""})`);
11389
+ }
11390
+ }
11391
+ }
11309
11392
  if (opts.history) {
11310
11393
  const runsResult = await apiRequest({
11311
11394
  method: "GET",
@@ -11626,7 +11709,7 @@ function collectCodeDelta(files, opts) {
11626
11709
 
11627
11710
  // src/lib/debounce.ts
11628
11711
  var import_node_fs6 = require("node:fs");
11629
- var import_node_crypto = require("node:crypto");
11712
+ var import_node_crypto2 = require("node:crypto");
11630
11713
  function checkDebounce(debounceSeconds = DEBOUNCE_SECONDS) {
11631
11714
  if (!(0, import_node_fs6.existsSync)(DEBOUNCE_FILE)) return null;
11632
11715
  try {
@@ -11664,7 +11747,7 @@ function checkMtime(files, bypassForRecentCommits) {
11664
11747
  return "No files modified since last analysis";
11665
11748
  }
11666
11749
  function computeContentHash(files) {
11667
- const hash = (0, import_node_crypto.createHash)("sha1");
11750
+ const hash = (0, import_node_crypto2.createHash)("sha1");
11668
11751
  const sorted = [...files].sort();
11669
11752
  for (const f of sorted) {
11670
11753
  const resolved = resolveFile(f) ?? f;
@@ -12049,11 +12132,11 @@ function cleanStaleSnapshots(dir, keepSet) {
12049
12132
 
12050
12133
  // src/lib/offline.ts
12051
12134
  var import_node_fs10 = require("node:fs");
12052
- var import_node_crypto2 = require("node:crypto");
12135
+ var import_node_crypto3 = require("node:crypto");
12053
12136
  function cacheRequest(body) {
12054
12137
  try {
12055
12138
  (0, import_node_fs10.mkdirSync)(CACHE_DIR, { recursive: true });
12056
- const suffix = (0, import_node_crypto2.randomBytes)(4).toString("hex");
12139
+ const suffix = (0, import_node_crypto3.randomBytes)(4).toString("hex");
12057
12140
  const filename = `pending-${Math.floor(Date.now() / 1e3)}-${suffix}.json`;
12058
12141
  (0, import_node_fs10.writeFileSync)(`${CACHE_DIR}/${filename}`, JSON.stringify(body));
12059
12142
  } catch {
@@ -12068,6 +12151,90 @@ function buildOfflineFallback(reason, staticResults) {
12068
12151
  };
12069
12152
  }
12070
12153
 
12154
+ // src/lib/context-files.ts
12155
+ var import_node_fs11 = require("node:fs");
12156
+ var MAX_CONTEXT_FILES = 10;
12157
+ var MAX_CONTEXT_FILE_BYTES = 10240;
12158
+ var MAX_CONTEXT_TOTAL_BYTES = 51200;
12159
+ function gatherContextFiles(contextPaths, deltaFiles) {
12160
+ const deltaPaths = new Set(deltaFiles.map((f) => f.path));
12161
+ const result = [];
12162
+ let totalBytes = 0;
12163
+ for (const filePath of contextPaths) {
12164
+ if (result.length >= MAX_CONTEXT_FILES) break;
12165
+ if (deltaPaths.has(filePath)) continue;
12166
+ try {
12167
+ const content = (0, import_node_fs11.readFileSync)(filePath, "utf8");
12168
+ const bytes = Buffer.byteLength(content);
12169
+ if (bytes > MAX_CONTEXT_FILE_BYTES) {
12170
+ logEvent("context_file_skipped", { path: filePath, reason: "too_large", bytes });
12171
+ continue;
12172
+ }
12173
+ if (totalBytes + bytes > MAX_CONTEXT_TOTAL_BYTES) {
12174
+ logEvent("context_file_skipped", { path: filePath, reason: "budget", bytes });
12175
+ continue;
12176
+ }
12177
+ const ext = filePath.split(".").pop() ?? "";
12178
+ const langMap = {
12179
+ ts: "typescript",
12180
+ tsx: "typescript",
12181
+ js: "javascript",
12182
+ jsx: "javascript",
12183
+ py: "python",
12184
+ rb: "ruby",
12185
+ go: "go",
12186
+ rs: "rust",
12187
+ java: "java",
12188
+ yaml: "yaml",
12189
+ yml: "yaml",
12190
+ json: "json",
12191
+ md: "markdown",
12192
+ sql: "sql",
12193
+ sh: "bash",
12194
+ css: "css",
12195
+ html: "html"
12196
+ };
12197
+ result.push({
12198
+ path: filePath,
12199
+ content,
12200
+ language: langMap[ext],
12201
+ role: "context"
12202
+ });
12203
+ totalBytes += bytes;
12204
+ } catch {
12205
+ logEvent("context_file_skipped", { path: filePath, reason: "missing" });
12206
+ }
12207
+ }
12208
+ return result;
12209
+ }
12210
+
12211
+ // src/lib/cache-cleanup.ts
12212
+ var import_node_fs12 = require("node:fs");
12213
+ var import_node_path8 = require("node:path");
12214
+ var CACHE_TTL_DAYS = 7;
12215
+ function pruneStaleCache() {
12216
+ try {
12217
+ const dir = projectPath(CACHE_DIR);
12218
+ const cutoff = Date.now() - CACHE_TTL_DAYS * 24 * 3600 * 1e3;
12219
+ for (const entry of (0, import_node_fs12.readdirSync)(dir)) {
12220
+ if (!entry.startsWith("pending-")) continue;
12221
+ const path = (0, import_node_path8.join)(dir, entry);
12222
+ try {
12223
+ const stat = (0, import_node_fs12.statSync)(path);
12224
+ if (stat.mtimeMs < cutoff) {
12225
+ (0, import_node_fs12.unlinkSync)(path);
12226
+ logEvent("cache_entry_pruned", {
12227
+ path: entry,
12228
+ age_days: Math.round((Date.now() - stat.mtimeMs) / 864e5)
12229
+ });
12230
+ }
12231
+ } catch {
12232
+ }
12233
+ }
12234
+ } catch {
12235
+ }
12236
+ }
12237
+
12071
12238
  // src/lib/analysis-mode.ts
12072
12239
  var DEBUG_PHRASES = [
12073
12240
  "not working",
@@ -12138,7 +12305,7 @@ function detectAnalysisMode(noFilesChanged, assistantResponse, conversationPromp
12138
12305
  }
12139
12306
 
12140
12307
  // src/lib/transcript.ts
12141
- var import_node_fs11 = require("node:fs");
12308
+ var import_node_fs13 = require("node:fs");
12142
12309
  var MAX_READ_BYTES = 256 * 1024;
12143
12310
  var SMALL_FILE_BYTES = 64 * 1024;
12144
12311
  var MAX_FILES_LIST = 20;
@@ -12160,14 +12327,14 @@ async function extractActionSummary(transcriptPath) {
12160
12327
  function readTurnLines(transcriptPath) {
12161
12328
  let size;
12162
12329
  try {
12163
- size = (0, import_node_fs11.statSync)(transcriptPath).size;
12330
+ size = (0, import_node_fs13.statSync)(transcriptPath).size;
12164
12331
  } catch {
12165
12332
  return null;
12166
12333
  }
12167
12334
  if (size === 0) return null;
12168
12335
  let raw;
12169
12336
  if (size <= SMALL_FILE_BYTES) {
12170
- raw = (0, import_node_fs11.readFileSync)(transcriptPath, "utf-8");
12337
+ raw = (0, import_node_fs13.readFileSync)(transcriptPath, "utf-8");
12171
12338
  } else {
12172
12339
  const buf = Buffer.alloc(Math.min(MAX_READ_BYTES, size));
12173
12340
  const fd = require("node:fs").openSync(transcriptPath, "r");
@@ -12484,6 +12651,33 @@ async function runAnalyze(opts, globals) {
12484
12651
  if (!urlResult.ok) {
12485
12652
  passAndExit("Not configured yet \u2014 run /gate-setup in Claude Code to enable quality gates.");
12486
12653
  }
12654
+ const sessionIdForMemory = process.env.CLAUDE_SESSION_ID ?? "";
12655
+ let contextFilePaths = [];
12656
+ try {
12657
+ const memoryPath2 = sessionIdForMemory ? `/memory?session_id=${encodeURIComponent(sessionIdForMemory)}` : "/memory";
12658
+ const memoryResult = await apiRequest({
12659
+ method: "GET",
12660
+ path: memoryPath2,
12661
+ serviceUrl: urlResult.data,
12662
+ token: tokenResult.data.token,
12663
+ verbose: globals.verbose,
12664
+ timeout: 1e4,
12665
+ cmd: "analyze_context"
12666
+ });
12667
+ if (memoryResult.ok && Array.isArray(memoryResult.data.context_files)) {
12668
+ contextFilePaths = memoryResult.data.context_files;
12669
+ }
12670
+ } catch {
12671
+ logEvent("memory_fetch_failed", { reason: "exception" });
12672
+ }
12673
+ const contextFiles = gatherContextFiles(contextFilePaths, codeDelta.files);
12674
+ for (const f of codeDelta.files) {
12675
+ f.role = "delta";
12676
+ }
12677
+ for (const cf of contextFiles) {
12678
+ codeDelta.files.push(cf);
12679
+ }
12680
+ pruneStaleCache();
12487
12681
  const requestBody = {
12488
12682
  static_results: staticResults,
12489
12683
  code_delta: codeDelta,
@@ -12540,7 +12734,8 @@ async function runAnalyze(opts, globals) {
12540
12734
  body: requestBody,
12541
12735
  verbose: globals.verbose,
12542
12736
  timeout: ANALYZE_TIMEOUT_MS,
12543
- cmd: "analyze"
12737
+ cmd: "analyze",
12738
+ encodeBody: true
12544
12739
  });
12545
12740
  if (!result.ok && (result.category === "network" || result.category === "timeout")) {
12546
12741
  logEvent("analyze_warm_retry", { first_error: result.error, category: result.category });
@@ -12562,7 +12757,8 @@ async function runAnalyze(opts, globals) {
12562
12757
  verbose: globals.verbose,
12563
12758
  timeout: ANALYZE_TIMEOUT_MS,
12564
12759
  cmd: "analyze",
12565
- retry: true
12760
+ retry: true,
12761
+ encodeBody: true
12566
12762
  });
12567
12763
  }
12568
12764
  if (!result.ok) {
@@ -12576,6 +12772,26 @@ async function runAnalyze(opts, globals) {
12576
12772
  }
12577
12773
  const response = result.data;
12578
12774
  const decision = response.gate_decision ?? "PASS";
12775
+ const metadata = response.metadata ?? {};
12776
+ const intentAmbiguity = metadata.intent_ambiguity;
12777
+ if (intentAmbiguity != null && intentAmbiguity > 5) {
12778
+ const taskInfo = response.task ?? {};
12779
+ if (intentAmbiguity > 7) {
12780
+ process.stderr.write(`${YELLOW}\u26A0 Vague prompt (${intentAmbiguity}/10) \u2014 Analysis will proceed but findings may not match your intent.${NC}
12781
+ `);
12782
+ } else {
12783
+ process.stderr.write(`${YELLOW}\u26A0 Ambiguous prompt (${intentAmbiguity}/10) \u2014 Consider being more specific about what to change and where.${NC}
12784
+ `);
12785
+ }
12786
+ }
12787
+ const taskMeta = response.task ?? {};
12788
+ if (taskMeta.label) {
12789
+ const taskLabel = taskMeta.label;
12790
+ const isNew = taskMeta.is_new ?? false;
12791
+ const prefix = isNew ? `${BOLD}\u25B6 NEW${NC} ` : "";
12792
+ process.stderr.write(`${DIM}Task: ${prefix}${taskLabel}${NC}
12793
+ `);
12794
+ }
12579
12795
  saveSnapshots(codeDelta.files.map((f) => ({ path: f.path, content: f.content })));
12580
12796
  switch (decision) {
12581
12797
  case "FAIL": {
@@ -12680,7 +12896,7 @@ async function runAnalyze(opts, globals) {
12680
12896
  }
12681
12897
 
12682
12898
  // src/commands/review.ts
12683
- var import_node_fs12 = require("node:fs");
12899
+ var import_node_fs14 = require("node:fs");
12684
12900
  function registerReviewCommand(program2) {
12685
12901
  program2.command("review").description("Run on-demand GATE.md analysis (advisory, never blocks)").requiredOption("--files <paths>", "Comma-separated file list").option("--changed <paths>", "Subset of --files that were modified").option("--intent <text>", "User intent description (max 2000 chars)").option("--specs <paths>", "Comma-separated spec file paths").option("--json", "Output raw JSON response").action(async (opts) => {
12686
12902
  const globals = program2.opts();
@@ -12699,7 +12915,7 @@ async function runReview(opts, globals) {
12699
12915
  const securityFiles = filterSecurity(allFiles);
12700
12916
  let staticResults;
12701
12917
  if (isCodacyAvailable()) {
12702
- const scannable = Array.from(/* @__PURE__ */ new Set([...analyzable, ...securityFiles])).filter((f) => (0, import_node_fs12.existsSync)(f) || resolveFile(f) !== null);
12918
+ const scannable = Array.from(/* @__PURE__ */ new Set([...analyzable, ...securityFiles])).filter((f) => (0, import_node_fs14.existsSync)(f) || resolveFile(f) !== null);
12703
12919
  staticResults = runCodacyAnalysis(scannable);
12704
12920
  } else {
12705
12921
  staticResults = {
@@ -12724,10 +12940,10 @@ async function runReview(opts, globals) {
12724
12940
  const specPaths = opts.specs.split(",").map((f) => f.trim()).filter(Boolean);
12725
12941
  specs = [];
12726
12942
  for (const p of specPaths) {
12727
- if (!(0, import_node_fs12.existsSync)(p)) continue;
12943
+ if (!(0, import_node_fs14.existsSync)(p)) continue;
12728
12944
  try {
12729
- const { readFileSync: readFileSync6 } = await import("node:fs");
12730
- const content = readFileSync6(p, "utf-8");
12945
+ const { readFileSync: readFileSync8 } = await import("node:fs");
12946
+ const content = readFileSync8(p, "utf-8");
12731
12947
  specs.push({ path: p, content: content.slice(0, 10240) });
12732
12948
  } catch {
12733
12949
  }
@@ -12785,21 +13001,21 @@ async function runReview(opts, globals) {
12785
13001
  }
12786
13002
 
12787
13003
  // src/commands/init.ts
12788
- var import_node_fs13 = require("node:fs");
13004
+ var import_node_fs15 = require("node:fs");
12789
13005
  var import_promises8 = require("node:fs/promises");
12790
- var import_node_path8 = require("node:path");
13006
+ var import_node_path9 = require("node:path");
12791
13007
  var import_node_child_process8 = require("node:child_process");
12792
13008
  function resolveDataDir() {
12793
13009
  const candidates = [
12794
- (0, import_node_path8.join)(__dirname, "..", "data"),
13010
+ (0, import_node_path9.join)(__dirname, "..", "data"),
12795
13011
  // installed: node_modules/@codacy/gate-cli/data
12796
- (0, import_node_path8.join)(__dirname, "..", "..", "data"),
13012
+ (0, import_node_path9.join)(__dirname, "..", "..", "data"),
12797
13013
  // edge case: nested resolution
12798
- (0, import_node_path8.join)(process.cwd(), "cli", "data")
13014
+ (0, import_node_path9.join)(process.cwd(), "cli", "data")
12799
13015
  // local dev: running from repo root
12800
13016
  ];
12801
13017
  for (const candidate of candidates) {
12802
- if ((0, import_node_fs13.existsSync)((0, import_node_path8.join)(candidate, "skills"))) {
13018
+ if ((0, import_node_fs15.existsSync)((0, import_node_path9.join)(candidate, "skills"))) {
12803
13019
  return candidate;
12804
13020
  }
12805
13021
  }
@@ -12815,7 +13031,7 @@ function registerInitCommand(program2) {
12815
13031
  program2.command("init").description("Initialize GATE.md in the current project").option("--force", "Overwrite existing skills and hooks").action(async (opts) => {
12816
13032
  const force = opts.force ?? false;
12817
13033
  const projectMarkers = [".git", "package.json", "pyproject.toml", "go.mod", "Cargo.toml", "Gemfile", "pom.xml", "build.gradle"];
12818
- const isProject = projectMarkers.some((m) => (0, import_node_fs13.existsSync)(m));
13034
+ const isProject = projectMarkers.some((m) => (0, import_node_fs15.existsSync)(m));
12819
13035
  if (!isProject) {
12820
13036
  printError("No project detected in the current directory.");
12821
13037
  printInfo('Run "gate init" from your project root.');
@@ -12868,21 +13084,21 @@ function registerInitCommand(program2) {
12868
13084
  console.log("");
12869
13085
  printInfo("Installing skills...");
12870
13086
  const dataDir = resolveDataDir();
12871
- const skillsSource = (0, import_node_path8.join)(dataDir, "skills");
13087
+ const skillsSource = (0, import_node_path9.join)(dataDir, "skills");
12872
13088
  const skillsDest = ".claude/skills";
12873
13089
  const skills = ["gate-setup", "gate-analyze", "gate-status", "gate-feedback"];
12874
13090
  let skillsInstalled = 0;
12875
13091
  for (const skill of skills) {
12876
- const src = (0, import_node_path8.join)(skillsSource, skill);
12877
- const dest = (0, import_node_path8.join)(skillsDest, skill);
12878
- if (!(0, import_node_fs13.existsSync)(src)) {
13092
+ const src = (0, import_node_path9.join)(skillsSource, skill);
13093
+ const dest = (0, import_node_path9.join)(skillsDest, skill);
13094
+ if (!(0, import_node_fs15.existsSync)(src)) {
12879
13095
  printWarn(` Skill data not found: ${skill}`);
12880
13096
  continue;
12881
13097
  }
12882
- if ((0, import_node_fs13.existsSync)(dest) && !force) {
12883
- const srcSkill = (0, import_node_path8.join)(src, "SKILL.md");
12884
- const destSkill = (0, import_node_path8.join)(dest, "SKILL.md");
12885
- if ((0, import_node_fs13.existsSync)(destSkill)) {
13098
+ if ((0, import_node_fs15.existsSync)(dest) && !force) {
13099
+ const srcSkill = (0, import_node_path9.join)(src, "SKILL.md");
13100
+ const destSkill = (0, import_node_path9.join)(dest, "SKILL.md");
13101
+ if ((0, import_node_fs15.existsSync)(destSkill)) {
12886
13102
  try {
12887
13103
  const srcContent = await (0, import_promises8.readFile)(srcSkill, "utf-8");
12888
13104
  const destContent = await (0, import_promises8.readFile)(destSkill, "utf-8");
@@ -12910,7 +13126,7 @@ function registerInitCommand(program2) {
12910
13126
  printInfo(' Run "gate hooks install --force" to overwrite.');
12911
13127
  }
12912
13128
  await (0, import_promises8.mkdir)(GATE_DIR, { recursive: true });
12913
- const globalGateDir = (0, import_node_path8.join)(process.env.HOME ?? "", ".gate");
13129
+ const globalGateDir = (0, import_node_path9.join)(process.env.HOME ?? "", ".gate");
12914
13130
  await (0, import_promises8.mkdir)(globalGateDir, { recursive: true });
12915
13131
  console.log("");
12916
13132
  printInfo("GATE.md initialized!");
@@ -12927,8 +13143,245 @@ function registerInitCommand(program2) {
12927
13143
  });
12928
13144
  }
12929
13145
 
13146
+ // src/commands/task.ts
13147
+ function memoryPath() {
13148
+ const sid = process.env.CLAUDE_SESSION_ID;
13149
+ return sid ? `/memory?session_id=${encodeURIComponent(sid)}` : "/memory";
13150
+ }
13151
+ function timeAgo2(isoDate) {
13152
+ const ms = Date.now() - new Date(isoDate).getTime();
13153
+ const mins = Math.floor(ms / 6e4);
13154
+ if (mins < 1) return "just now";
13155
+ if (mins < 60) return `${mins}m ago`;
13156
+ const hrs = Math.floor(mins / 60);
13157
+ if (hrs < 24) return `${hrs}h ago`;
13158
+ return `${Math.floor(hrs / 24)}d ago`;
13159
+ }
13160
+ async function getAuth(globals) {
13161
+ const tokenResult = await resolveToken(globals.token);
13162
+ if (!tokenResult.ok) {
13163
+ printError(tokenResult.error);
13164
+ process.exit(1);
13165
+ }
13166
+ const urlResult = await resolveServiceUrl(globals.serviceUrl);
13167
+ if (!urlResult.ok) {
13168
+ printError(urlResult.error);
13169
+ process.exit(1);
13170
+ }
13171
+ return { token: tokenResult.data.token, serviceUrl: urlResult.data };
13172
+ }
13173
+ function registerTaskCommands(program2) {
13174
+ const task = program2.command("task").description("Manage tasks");
13175
+ task.command("list").description("List tasks").option("--status <status>", "Filter by status (active, closed, merged)").action(async (opts) => {
13176
+ const globals = program2.opts();
13177
+ const { token, serviceUrl } = await getAuth(globals);
13178
+ const statusParam = opts.status ? `?status=${opts.status}` : "";
13179
+ const result = await apiRequest({
13180
+ method: "GET",
13181
+ path: `/tasks${statusParam}`,
13182
+ serviceUrl,
13183
+ token,
13184
+ verbose: globals.verbose,
13185
+ cmd: "task_list"
13186
+ });
13187
+ if (!result.ok) {
13188
+ printError(result.error);
13189
+ process.exit(1);
13190
+ }
13191
+ const tasks = result.data.tasks;
13192
+ if (tasks.length === 0) {
13193
+ printInfo("No tasks found.");
13194
+ return;
13195
+ }
13196
+ printInfo("Tasks:");
13197
+ for (const t of tasks) {
13198
+ const status = t.status ?? "active";
13199
+ const statusTag = status !== "active" ? ` [${status}]` : "";
13200
+ const runs = t.run_count;
13201
+ const ago = t.last_run_at ? timeAgo2(t.last_run_at) : "";
13202
+ const detail = runs != null ? `${runs} run${runs === 1 ? "" : "s"}${ago ? ", " + ago : ""}` : "";
13203
+ printInfo(` ${t.label}${statusTag}${detail ? " (" + detail + ")" : ""}`);
13204
+ }
13205
+ });
13206
+ task.command("close").description("Close the current task (next run starts a new task)").option("--id <taskId>", "Close a specific task by ID").action(async (opts) => {
13207
+ const globals = program2.opts();
13208
+ const { token, serviceUrl } = await getAuth(globals);
13209
+ let taskId = opts.id;
13210
+ if (!taskId) {
13211
+ const mem = await apiRequest({
13212
+ method: "GET",
13213
+ path: memoryPath(),
13214
+ serviceUrl,
13215
+ token,
13216
+ verbose: globals.verbose,
13217
+ cmd: "task_close_lookup"
13218
+ });
13219
+ if (!mem.ok) {
13220
+ printError(mem.error);
13221
+ process.exit(1);
13222
+ }
13223
+ const ct = mem.data.current_task;
13224
+ if (!ct?.id) {
13225
+ printError("No active task to close.");
13226
+ process.exit(1);
13227
+ }
13228
+ taskId = ct.id;
13229
+ }
13230
+ const result = await apiRequest({
13231
+ method: "POST",
13232
+ path: `/tasks/${taskId}/close`,
13233
+ serviceUrl,
13234
+ token,
13235
+ verbose: globals.verbose,
13236
+ cmd: "task_close"
13237
+ });
13238
+ if (!result.ok) {
13239
+ printError(result.error);
13240
+ process.exit(1);
13241
+ }
13242
+ printInfo(`Closed task: "${result.data.task.label}"`);
13243
+ });
13244
+ task.command("rename <label>").description("Rename the current task").option("--id <taskId>", "Rename a specific task by ID").action(async (label, opts) => {
13245
+ const globals = program2.opts();
13246
+ const { token, serviceUrl } = await getAuth(globals);
13247
+ let taskId = opts.id;
13248
+ if (!taskId) {
13249
+ const mem = await apiRequest({
13250
+ method: "GET",
13251
+ path: memoryPath(),
13252
+ serviceUrl,
13253
+ token,
13254
+ verbose: globals.verbose,
13255
+ cmd: "task_rename_lookup"
13256
+ });
13257
+ if (!mem.ok) {
13258
+ printError(mem.error);
13259
+ process.exit(1);
13260
+ }
13261
+ const ct = mem.data.current_task;
13262
+ if (!ct?.id) {
13263
+ printError("No active task to rename.");
13264
+ process.exit(1);
13265
+ }
13266
+ taskId = ct.id;
13267
+ }
13268
+ const result = await apiRequest({
13269
+ method: "POST",
13270
+ path: `/tasks/${taskId}/rename`,
13271
+ serviceUrl,
13272
+ token,
13273
+ body: { label: label.slice(0, 120) },
13274
+ verbose: globals.verbose,
13275
+ cmd: "task_rename"
13276
+ });
13277
+ if (!result.ok) {
13278
+ printError(result.error);
13279
+ process.exit(1);
13280
+ }
13281
+ printInfo(`Renamed task to: "${result.data.task.label}"`);
13282
+ });
13283
+ task.command("merge <source> <target>").description("Merge source task into target task").action(async (source, target) => {
13284
+ const globals = program2.opts();
13285
+ const { token, serviceUrl } = await getAuth(globals);
13286
+ const result = await apiRequest({
13287
+ method: "POST",
13288
+ path: "/tasks/merge",
13289
+ serviceUrl,
13290
+ token,
13291
+ body: { source_id: source, target_id: target },
13292
+ verbose: globals.verbose,
13293
+ cmd: "task_merge"
13294
+ });
13295
+ if (!result.ok) {
13296
+ printError(result.error);
13297
+ process.exit(1);
13298
+ }
13299
+ printInfo(`Merged task ${source} into ${target}`);
13300
+ });
13301
+ }
13302
+
13303
+ // src/commands/reset.ts
13304
+ var import_node_fs16 = require("node:fs");
13305
+ var import_node_path10 = require("node:path");
13306
+ function registerResetCommand(program2) {
13307
+ program2.command("reset").description("Close the current task and clear transient state").option("--keep-task", "Only purge caches; leave the current task open").option("--all", "Also purge diagnostic logs (.gate/.logs/)").action(async (opts) => {
13308
+ const globals = program2.opts();
13309
+ if (!opts.keepTask) {
13310
+ try {
13311
+ const tokenResult = await resolveToken(globals.token);
13312
+ const urlResult = await resolveServiceUrl(globals.serviceUrl);
13313
+ if (tokenResult.ok && urlResult.ok) {
13314
+ const mem = await apiRequest({
13315
+ method: "GET",
13316
+ path: process.env.CLAUDE_SESSION_ID ? `/memory?session_id=${encodeURIComponent(process.env.CLAUDE_SESSION_ID)}` : "/memory",
13317
+ serviceUrl: urlResult.data,
13318
+ token: tokenResult.data.token,
13319
+ timeout: 1e4,
13320
+ cmd: "reset_lookup"
13321
+ });
13322
+ const ct = mem.ok ? mem.data.current_task : null;
13323
+ if (ct?.id) {
13324
+ const closeResult = await apiRequest({
13325
+ method: "POST",
13326
+ path: `/tasks/${ct.id}/close`,
13327
+ serviceUrl: urlResult.data,
13328
+ token: tokenResult.data.token,
13329
+ timeout: 1e4,
13330
+ cmd: "reset_close"
13331
+ });
13332
+ if (closeResult.ok) {
13333
+ printInfo(`Closed task: "${ct.label}"`);
13334
+ }
13335
+ }
13336
+ }
13337
+ } catch {
13338
+ }
13339
+ }
13340
+ const cacheDir = projectPath(CACHE_DIR);
13341
+ let purged = 0;
13342
+ if ((0, import_node_fs16.existsSync)(cacheDir)) {
13343
+ for (const entry of (0, import_node_fs16.readdirSync)(cacheDir)) {
13344
+ if (entry.startsWith("pending-")) {
13345
+ try {
13346
+ (0, import_node_fs16.unlinkSync)((0, import_node_path10.join)(cacheDir, entry));
13347
+ purged++;
13348
+ } catch {
13349
+ }
13350
+ }
13351
+ }
13352
+ }
13353
+ const filesToClear = [
13354
+ projectPath(INTENT_FILE),
13355
+ projectPath(HASH_FILE),
13356
+ projectPath(`${GATE_DIR}/.iteration-count`),
13357
+ projectPath(`${GATE_DIR}/.last-analysis`)
13358
+ ];
13359
+ for (const file of filesToClear) {
13360
+ if ((0, import_node_fs16.existsSync)(file)) {
13361
+ try {
13362
+ (0, import_node_fs16.writeFileSync)(file, "");
13363
+ } catch {
13364
+ }
13365
+ }
13366
+ }
13367
+ if (opts.all) {
13368
+ const logsDir = projectPath(`${GATE_DIR}/.logs`);
13369
+ if ((0, import_node_fs16.existsSync)(logsDir)) {
13370
+ for (const entry of (0, import_node_fs16.readdirSync)(logsDir)) {
13371
+ try {
13372
+ (0, import_node_fs16.unlinkSync)((0, import_node_path10.join)(logsDir, entry));
13373
+ } catch {
13374
+ }
13375
+ }
13376
+ }
13377
+ printInfo("Cleared diagnostic logs.");
13378
+ }
13379
+ printInfo(`Reset complete. Purged ${purged} cached request${purged !== 1 ? "s" : ""}.${opts.keepTask ? "" : " Next run starts a new task."}`);
13380
+ });
13381
+ }
13382
+
12930
13383
  // src/cli.ts
12931
- program.name("gate").description("CLI for GATE.md quality gate service").version("0.14.2").option("--token <token>", "Override authentication token").option("--service-url <url>", "Override service URL").option("--verbose", "Log HTTP requests/responses to stderr");
13384
+ program.name("gate").description("CLI for GATE.md quality gate service").version("0.15.0").option("--token <token>", "Override authentication token").option("--service-url <url>", "Override service URL").option("--verbose", "Log HTTP requests/responses to stderr");
12932
13385
  registerAuthCommands(program);
12933
13386
  registerHooksCommands(program);
12934
13387
  registerIntentCommands(program);
@@ -12939,4 +13392,6 @@ registerFeedbackCommand(program);
12939
13392
  registerAnalyzeCommand(program);
12940
13393
  registerReviewCommand(program);
12941
13394
  registerInitCommand(program);
13395
+ registerTaskCommands(program);
13396
+ registerResetCommand(program);
12942
13397
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codacy/gate-cli",
3
- "version": "0.14.2",
3
+ "version": "0.15.0",
4
4
  "description": "CLI for GATE.md quality gate service",
5
5
  "bin": {
6
6
  "gate": "./bin/gate.js"