Skip to content

feat: add standalone Kimi evaluator script#659

Draft
austinchima wants to merge 1 commit into
santifer:mainfrom
austinchima:feat/kimi-standalone-evaluator
Draft

feat: add standalone Kimi evaluator script#659
austinchima wants to merge 1 commit into
santifer:mainfrom
austinchima:feat/kimi-standalone-evaluator

Conversation

@austinchima
Copy link
Copy Markdown

@austinchima austinchima commented May 15, 2026

Description

This PR introduces the standalone Kimi evaluator script (kimi-eval.mjs) as requested by the maintainer.

In alignment with the project's recent architectural shift to a single AGENTS.md file, this PR drops the native Kimi CLI support and .kimi/ directory structure from the previous attempt, focusing entirely on the standalone API script.

Changes

  • Added kimi-eval.mjs: A free-tier alternative to the Claude-based pipeline using Moonshot's API.
  • Updated package.json: Added "kimi:eval": "node kimi-eval.mjs".
  • Updated .env.example: Added Kimi/Moonshot API keys with accurate documentation regarding Tier 0 rate limits (3 RPM) and the $1 initial recharge requirement for sustained workflows.
  • Updated README.md: Replaced old documentation with a new block explaining the standalone script, distinguishing between the flagship 1T MoE kimi-k2.6 model and the moonshot-v1-8k fallback.

Updates PR #508.

Summary by CodeRabbit

  • New Features

    • Integrated Kimi (Moonshot) API for AI-powered job evaluation with automated scoring and result categorization
    • Added standalone CLI evaluation tool supporting job description input via direct arguments or file, with configurable model selection
  • Documentation

    • Updated README with Kimi integration setup and usage instructions
    • Added documented environment variables for API key configuration

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 15, 2026

📝 Walkthrough

Walkthrough

This PR adds a complete Kimi/Moonshot API integration for resume evaluation. A new Node.js CLI script processes job descriptions against candidate profiles, calls Moonshot's API, extracts structured evaluation results, and persists reports with tracking metadata.

Changes

Kimi/Moonshot Evaluator

Layer / File(s) Summary
Environment & Script Entry Points
.env.example, package.json
KIMI_API_KEY and MOONSHOT_API_KEY (alias) are added to .env.example with setup guidance; package.json receives a kimi:eval script entry.
CLI Bootstrap & Argument Parsing
kimi-eval.mjs (lines 1–131)
Entrypoint loads dotenv, imports modules, defines file paths, parses --file, --model, and --no-save flags, accumulates positional job-description text, and validates KIMI_API_KEY or MOONSHOT_API_KEY presence before proceeding.
Context Loading & System Prompt
kimi-eval.mjs (lines 135–203)
Helper functions read evaluation instructions from markdown context files and compute the next report number; system prompt is constructed embedding those contexts, candidate CV, and a precise output format specification for score-summary extraction.
Moonshot API Call & Output Parsing
kimi-eval.mjs (lines 204–279)
Script calls the OpenAI-compatible Moonshot endpoint with constructed prompt and user JD; handles authentication and rate-limit errors; parses response and extracts fields (COMPANY, ROLE, SCORE, ARCHETYPE, LEGITIMACY) from a regex-matched ---SCORE_SUMMARY--- block.
Report Generation & Summary Output
kimi-eval.mjs (lines 284–347)
If saving is enabled, script ensures reports/ exists, generates a dated markdown report with auto-incremented number and sanitized company slug, writes a TSV tracker entry under batch/tracker-additions/, and prints a concise one-line summary to stdout.
User Documentation
README.md (lines 162–181)
New "Kimi Integration" section documents setup (API key, install), usage examples (inline JD and --file mode), and guidance on model selection and usage tiers.

🎯 2 (Simple) | ⏱️ ~10 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately and concisely summarizes the main change: adding a standalone Kimi evaluator script. It is specific, clear, and directly reflects the primary objective of the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@kimi-eval.mjs`:
- Around line 324-335: The TSV row is built from untrusted model outputs
(company, role) which may contain tabs/newlines or spreadsheet-formula prefixes
and can break parsing; before building tsvRow (the variables company and role
used with tsvRow and writeFileSync), sanitize each field by trimming and
replacing any tabs/newlines with a single space and neutralize leading formula
characters (=, +, -, @) by prefixing with a single quote (or otherwise escaping)
and also ensure filename is safe/normalised; then join the sanitized values into
tsvRow and call writeFileSync with tsvPath as before.
- Around line 218-229: The OpenAI call using openai.chat.completions.create (the
block that builds messages with systemPrompt, modelName and jdText) needs a
full-request timeout via AbortSignal rather than only the SDK timeout option;
create an AbortController before the call, set a timer to call
controller.abort() after the desired timeout, pass controller.signal into the
create() options (e.g., signal), and ensure you clear the timer after completion
or on error so you don’t leak timers.

In `@package.json`:
- Around line 18-20: The package.json is missing the openai dependency required
by the new script "kimi:eval" which imports openai in kimi-eval.mjs; add
"openai" to dependencies in package.json (not devDependencies) with an
appropriate semver (e.g., latest compatible version) and run npm install / yarn
install so the kimi:eval script can import openai at runtime.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 02305ac8-e78f-4dc0-bd9a-c27e04a40512

📥 Commits

Reviewing files that changed from the base of the PR and between 44def35 and ed066d6.

📒 Files selected for processing (4)
  • .env.example
  • README.md
  • kimi-eval.mjs
  • package.json

Comment thread kimi-eval.mjs
Comment on lines +218 to +229
const response = await openai.chat.completions.create({
model: modelName,
messages: [
{ role: "system", content: systemPrompt },
{
role: "user",
content: `\n\nJOB DESCRIPTION TO EVALUATE:\n\n${jdText}`,
},
],
temperature: 0.4, // deterministic enough for structured evaluation
max_tokens: 8192,
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

For the currently used OpenAI Node SDK version, what is the correct way to set request timeouts (client-level and per-request) for chat.completions.create?

💡 Result:

In the current OpenAI Node SDK, you can configure request timeouts using the timeout option, but it is important to understand its limitations regarding the request lifecycle [1][2]. Client-Level Configuration You can set a default timeout for all requests when initializing the OpenAI client [2]. This value is in milliseconds [3][2]. const client = new OpenAI({ timeout: 20 * 1000, // 20 seconds (default is 10 minutes) }); Per-Request Configuration You can override this timeout for individual chat.completions.create calls by passing a second argument to the method [2]. await client.chat.completions.create( { model: 'gpt-4o', messages: [...] }, { timeout: 5 * 1000 } // 5 seconds); Important Limitations The timeout option provided by the SDK only covers the time taken to receive the initial response headers (Time to First Byte) [1]. Once the headers are received, the internal timer is cleared, and the subsequent reading of the response body is not guarded by this timeout [1]. Consequently, if the response body stream hangs, the request may remain open indefinitely [1]. Recommended Workaround for Robust Timeouts Because of the limitation mentioned above, if you require a hard timeout that covers the entire duration of the request (including body streaming), the recommended approach is to use an AbortSignal [1]. You can create a signal with a hard timeout and pass it to the signal option in the request configuration [1]. // Example using a hard timeout for the entire request const controller = new AbortController; const timeoutId = setTimeout( => controller.abort, 30000); // 30s hard limit try { const completion = await client.chat.completions.create( { model: 'gpt-4o', messages: [...] }, { signal: controller.signal }); //... process response } catch (err) { if (err.name === 'AbortError') { console.error('Request timed out after 30s'); } } finally { clearTimeout(timeoutId); } This method ensures that the request is aborted regardless of whether the SDK's internal header-arrival timer has already cleared [1].

Citations:


🏁 Script executed:

# First, find the package.json to check the OpenAI SDK version
find . -name "package.json" -type f | head -5 | xargs cat

Repository: santifer/career-ops

Length of output: 1267


🏁 Script executed:

# Search for the OpenAI client initialization in kimi-eval.mjs
rg "new OpenAI" kimi-eval.mjs -A 5 -B 5

Repository: santifer/career-ops

Length of output: 342


🏁 Script executed:

# Get the complete context around lines 218-229 in kimi-eval.mjs
sed -n '200,240p' kimi-eval.mjs

Repository: santifer/career-ops

Length of output: 1366


🏁 Script executed:

# Check for OpenAI import in kimi-eval.mjs
head -50 kimi-eval.mjs | grep -i "import\|require"

Repository: santifer/career-ops

Length of output: 365


🏁 Script executed:

# Also check devDependencies and all dependencies sections
rg "openai" package.json -i

Repository: santifer/career-ops

Length of output: 45


🏁 Script executed:

# Check if there's a node_modules or package-lock.json that might indicate the version
ls -la | grep -E "package-lock|yarn.lock|node_modules"

Repository: santifer/career-ops

Length of output: 45


🏁 Script executed:

# Check if openai appears anywhere in package-lock.json or yarn.lock
find . -name "package-lock.json" -o -name "yarn.lock" | xargs grep -l "openai" 2>/dev/null | head -5

Repository: santifer/career-ops

Length of output: 45


🏁 Script executed:

# Look at the full imports section of kimi-eval.mjs
head -20 kimi-eval.mjs

Repository: santifer/career-ops

Length of output: 711


🏁 Script executed:

# Check if OpenAI is used with any signal/timeout handling elsewhere
rg "AbortController|AbortSignal|timeout" kimi-eval.mjs

Repository: santifer/career-ops

Length of output: 45


🏁 Script executed:

# Check if there's a setup or init file that might install dependencies
rg "npm install|openai" . -l | grep -v node_modules | head -10

Repository: santifer/career-ops

Length of output: 240


The proposed timeout parameter alone is insufficient; implement a complete request timeout using AbortSignal instead.

The external API call can hang indefinitely during response body streaming, even with a client-level timeout. While the timeout option in the OpenAI SDK covers the initial headers (TTFB), the response body stream remains unprotected, meaning hanging reads will not be interrupted [1].

For a robust solution that covers the entire request lifecycle, use AbortSignal:

Recommended fix
let evaluationText;
try {
+  const controller = new AbortController();
+  const timeoutId = setTimeout(() => controller.abort(), 120000);
+  
  const response = await openai.chat.completions.create({
    model: modelName,
    messages: [
      { role: "system", content: systemPrompt },
      {
        role: "user",
        content: `\n\nJOB DESCRIPTION TO EVALUATE:\n\n${jdText}`,
      },
    ],
    temperature: 0.4,
    max_tokens: 8192,
-  });
+  }, { signal: controller.signal });
+  
+  clearTimeout(timeoutId);
} catch (err) {
+  if (err.name === 'AbortError') {
+    console.error("❌  Kimi API timeout after 120s");
+    process.exit(1);
+  }
   console.error("❌  Kimi API error:", err.message || err);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@kimi-eval.mjs` around lines 218 - 229, The OpenAI call using
openai.chat.completions.create (the block that builds messages with
systemPrompt, modelName and jdText) needs a full-request timeout via AbortSignal
rather than only the SDK timeout option; create an AbortController before the
call, set a timer to call controller.abort() after the desired timeout, pass
controller.signal into the create() options (e.g., signal), and ensure you clear
the timer after completion or on error so you don’t leak timers.

Comment thread kimi-eval.mjs
Comment on lines +324 to +335
const tsvRow = [
num,
today,
company,
role,
"Evaluated",
score,
"❌",
`reports/${filename}`,
"",
].join("\t");
writeFileSync(tsvPath, tsvRow, "utf-8");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Sanitize TSV fields before writing tracker rows.

company and role come from model output and may include tabs/newlines (or spreadsheet formula prefixes), which can corrupt TSV parsing and downstream tooling.

Proposed fix
+    const sanitizeTsv = (value) => {
+      const s = String(value ?? "").replace(/[\t\r\n]+/g, " ").trim();
+      return /^[=+\-@]/.test(s) ? `'${s}` : s;
+    };
+
     const tsvRow = [
       num,
       today,
-      company,
-      role,
+      sanitizeTsv(company),
+      sanitizeTsv(role),
       "Evaluated",
-      score,
+      sanitizeTsv(score),
       "❌",
       `reports/${filename}`,
       "",
     ].join("\t");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const tsvRow = [
num,
today,
company,
role,
"Evaluated",
score,
"❌",
`reports/${filename}`,
"",
].join("\t");
writeFileSync(tsvPath, tsvRow, "utf-8");
const sanitizeTsv = (value) => {
const s = String(value ?? "").replace(/[\t\r\n]+/g, " ").trim();
return /^[=+\-@]/.test(s) ? `'${s}` : s;
};
const tsvRow = [
num,
today,
sanitizeTsv(company),
sanitizeTsv(role),
"Evaluated",
sanitizeTsv(score),
"❌",
`reports/${filename}`,
"",
].join("\t");
writeFileSync(tsvPath, tsvRow, "utf-8");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@kimi-eval.mjs` around lines 324 - 335, The TSV row is built from untrusted
model outputs (company, role) which may contain tabs/newlines or
spreadsheet-formula prefixes and can break parsing; before building tsvRow (the
variables company and role used with tsvRow and writeFileSync), sanitize each
field by trimming and replacing any tabs/newlines with a single space and
neutralize leading formula characters (=, +, -, @) by prefixing with a single
quote (or otherwise escaping) and also ensure filename is safe/normalised; then
join the sanitized values into tsvRow and call writeFileSync with tsvPath as
before.

Comment thread package.json
Comment on lines +18 to 20
"gemini:eval": "node gemini-eval.mjs",
"kimi:eval": "node kimi-eval.mjs"
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) Verify script entry exists"
rg -n '"kimi:eval"\s*:\s*"node kimi-eval\.mjs"' package.json

echo "2) Verify OpenAI import exists in evaluator"
rg -n 'import OpenAI from "openai"' kimi-eval.mjs

echo "3) Verify dependency declaration"
python - <<'PY'
import json
p=json.load(open("package.json"))
deps=p.get("dependencies",{})
dev=p.get("devDependencies",{})
print("openai in dependencies:", "openai" in deps)
print("openai in devDependencies:", "openai" in dev)
PY

Repository: santifer/career-ops

Length of output: 306


Add missing openai dependency to dependencies.

The new kimi:eval script imports openai (line 34 of kimi-eval.mjs) but the package is not declared in dependencies or devDependencies. This will cause the command to fail at runtime with a module not found error.

Proposed fix
   "dependencies": {
     "@google/generative-ai": "^0.24.1",
     "dotenv": "^17.0.0",
     "js-yaml": "^4.1.1",
+    "openai": "^4.0.0",
     "playwright": "^1.58.1"
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` around lines 18 - 20, The package.json is missing the openai
dependency required by the new script "kimi:eval" which imports openai in
kimi-eval.mjs; add "openai" to dependencies in package.json (not
devDependencies) with an appropriate semver (e.g., latest compatible version)
and run npm install / yarn install so the kimi:eval script can import openai at
runtime.

@austinchima austinchima marked this pull request as draft May 17, 2026 20:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant