UNPKG

crowdin-context-harvester

Version:
118 lines (98 loc) 4.93 kB
// @ts-nocheck import ora from 'ora'; import chalk from 'chalk'; import { tool } from '@langchain/core/tools'; import { z } from 'zod'; import { isToolMessage } from '@langchain/core/messages'; import { ChatPromptTemplate } from '@langchain/core/prompts'; import { createReactAgent } from '@langchain/langgraph/prebuilt'; import { globTool, grepTool, lsTool, readTool } from './agent/tools/index.js'; import { SYSTEM_PROMPT } from './agent/prompts/system.js'; import { getCrowdin, getPrompt, validateAiProviderFields, formatDuration, getChatModel } from './utils.js'; const spinner = ora(); const DEFAULT_USER_PROMPT = `You are generating a translator-oriented description by analyzing the local project. Goals: - Help translators quickly understand what this project is about and how to translate it safely and consistently. - Prefer concrete facts found in code, configurations, and scripts; if uncertain, omit. - Do not reference specific string keys/texts or file paths; keep the description general and product-level. Deliverable: - One cohesive description in plain prose (6–12 sentences, 1–2 short paragraphs). No lists, no headings, no bullet points. Cover, when evident from the project, in natural prose: - What the project does, who uses it, and its main features/workflows at a high level. - Tech stack and any i18n-relevant libraries/frameworks (e.g., ICU, i18next, formatjs), only if clearly present. - Placeholders/formatting that translators must preserve: variable tokens (e.g., {{name}}, %s), HTML/Markdown, ICU MessageFormat, dates/numbers. - Plurals/gender, capitalization, punctuation, length/space constraints, or RTL/localization specifics if applicable. - Tone/voice and terminology cues; mention product/brand names and items that must not be translated. - Any configuration/run details that help understand where user-facing text originates (keep high-level). When ready, call the return_description tool with the final text.`; const returnDescriptionTool = tool( input => { return typeof input?.description === 'string' ? input.description.trim() : ''; }, { name: 'return_description', description: 'Return the final project description text.', schema: z.object({ description: z.string().describe('Project description text'), }), returnDirect: true, }, ); async function invokeAgent({ agent, prompt }) { const result = await agent.invoke(prompt, { recursionLimit: 200 }); const lastMessage = result.messages[result.messages.length - 1]; const tokensUsed = result.messages.reduce((totalTokens, message) => totalTokens + (message.usage_metadata?.total_tokens ?? 0), 0); if (!lastMessage || !isToolMessage(lastMessage) || lastMessage.name !== 'return_description' || lastMessage.content.length === 0) { return { description: null, tokensUsed }; } return { description: lastMessage.content, tokensUsed }; } function createAgentAndPrompt(options) { const llm = getChatModel(options); const agent = createReactAgent({ llm, tools: [globTool, grepTool, lsTool, readTool, returnDescriptionTool] }); const promptTemplate = ChatPromptTemplate.fromMessages([ ['system', SYSTEM_PROMPT], ['user', getPrompt({ options, defaultPrompt: DEFAULT_USER_PROMPT })], ]); return { agent, promptTemplate }; } async function describeProject(_name, commandOptions, _command) { const startedAt = Date.now(); try { const options = commandOptions.opts(); if (!['terminal', 'crowdin'].includes(options.output)) { console.error('Wrong value provided for --output option. terminal and crowdin values are available.'); process.exit(1); } validateAiProviderFields(options); const apiClient = await getCrowdin(options); spinner.start('Generating project description...'); const { agent, promptTemplate } = createAgentAndPrompt(options); const prompt = await promptTemplate.invoke({ model: options.model, working_dir: process.cwd(), date: new Date().toISOString(), }); const { description } = await invokeAgent({ agent, prompt }); spinner.succeed(); if (!description || description.trim().length === 0) { console.error('No description was generated.'); process.exit(1); } if (options.output === 'terminal') { console.log('\n'); console.log(chalk.bold('Project Description:\n')); console.log(description); } else if (options.output === 'crowdin') { spinner.start('Updating Crowdin project description...'); await apiClient.projectsGroupsApi.editProject(options.project, [{ op: 'replace', path: '/description', value: description }]); spinner.succeed(); } } catch (error) { console.error('error:', error); } finally { const elapsedMs = Date.now() - startedAt; console.log(`\nTotal execution time: ${chalk.green(formatDuration(elapsedMs))}\n`); } } export default describeProject;