Background
Recently, our copy team decided to update all UI text from Title Case to sentence case — meaning only the first letter of the first word gets capitalized:
"Confirm Update" -> "Confirm update"
"Edit User" -> "Edit user"
Sounds simple, right? It wasn’t.
Every change came with hidden rules: preserve proper nouns and acronyms, skip variable names, avoid touching technical terms, and make it work across all UI components, labels, and headings.
First try
So I briefly describe my request in the augment VSCode plugin, with the help of “Enhance Prompt” feature, I got a solid, structured set of instructions:

Search through the entire codebase for text strings that are in title case (where each word is captialized, eg. "Create User", "Edit Profile", "Delete Account") and convert them to sentence case (where only the first word is capitalized, e.g. "Create user", "Edit profile", "Delete account").
Specifically:
1. Look for text strings in UI components, labels, buttons, headings, and user-facing messages
2. Focus on strings that appear to be user interface text rather than code identifiers or technical terms
3. Preserve proper nouns and acronyms that should remain capitalized
4. Make the changes across all file types in the repository (JavaScript, TypeScript, JSX, TSX, HTML, etc.)
5. After making changes, run prettier to maintain consistent code formatting
Examples of transformations:
- "Create User" -> "Create user"
- "Edit Profile Settings" -> "Edit profile settings"
- "Delete Account" -> "Delete account"
- "Save Changes" -> "Save changes"
Do not change:
- Code variable names, function names, or class names
- Technical terms or proper nouns that should remain capitalized
- Already correctly formatted sentence case text
So far, so good — except Augment’s first move was to try listing every possible UI string instead of just transforming them.

After a few nudges, it got the idea… but tapped out after about 20 files. Turns out agent is not designed to process such a tedious job.
Break down the task
Given the bad outcome from Augment in first round,I fell back to the classic codemod approach, which means I need to write a script to walk through the AST file by file, and handle the transformations myself.
If this is 5 years ago, normally I would open up https://astexplorer.net/, copy a random code snippet into the editor, and write the visitor function to get string literals in each file. This could take me around 2 hours to finish the job. But now since we are doing vibe coding, I’ll let Augment do the chores, I just send my instructions like this:
In codemod.js, use `typescript` as an AST parser and `fast-glob` to get all "*.ts(x)"files, get all kinds of strings in each file.
After ~ 5 rounds of talking, Augment handed me a working visitor script. I tightened it up with a couple of rules:
- Only match strings with at least one uppercase letter.
- Skip strings in import statements.
This is the script I get:
import {
isImportDeclaration,
isStringLiteral,
isNoSubstitutionTemplateLiteral,
isTemplateExpression,
isJsxText,
isJsxAttribute,
forEachChild,
createSourceFile,
ScriptTarget,
ScriptKind,
} from 'typescript';
import glob from 'fast-glob';
import { readFileSync, writeFileSync } from 'fs';
// Function to extract all string literals from a TypeScript AST node
function extractStrings(sourceFile) {
const stringsWithLineNumbers = [];
// Helper function to check if string contains at least one capital letter
function hasCapitalLetter(str) {
return /[A-Z]/.test(str);
}
// Helper function to check if a node is part of an import statement
function isInImportStatement(node) {
let parent = node.parent;
while (parent) {
if (isImportDeclaration(parent)) {
return true;
}
parent = parent.parent;
}
return false;
}
// Helper function to get line number from node position
function getLineNumber(node) {
const lineAndChar = sourceFile.getLineAndCharacterOfPosition(
node.getStart(),
);
return lineAndChar.line + 1;
}
function visit(node) {
// Handle regular string literals
if (isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node)) {
if (hasCapitalLetter(node.text) && !isInImportStatement(node)) {
stringsWithLineNumbers.push({
text: node.text,
line: getLineNumber(node),
});
}
}
// Handle template expressions (template literals with ${} expressions)
else if (isTemplateExpression(node)) {
if (node.head && hasCapitalLetter(node.head.text)) {
stringsWithLineNumbers.push({
text: node.head.text,
line: getLineNumber(node.head),
});
}
node.templateSpans.forEach(span => {
if (span.literal && hasCapitalLetter(span.literal.text)) {
stringsWithLineNumbers.push({
text: span.literal.text,
line: getLineNumber(span.literal),
});
}
});
}
// Handle JSX text content
else if (isJsxText(node)) {
const text = node.text.trim();
if (text && hasCapitalLetter(text)) {
stringsWithLineNumbers.push({
text: text,
line: getLineNumber(node),
});
}
}
// Handle JSX string literal attributes
else if (
isJsxAttribute(node) &&
node.initializer &&
isStringLiteral(node.initializer)
) {
if (hasCapitalLetter(node.initializer.text)) {
stringsWithLineNumbers.push({
text: node.initializer.text,
line: getLineNumber(node.initializer),
});
}
}
// Continue traversing child nodes
forEachChild(node, visit);
}
// Start the traversal from the source file
visit(sourceFile);
return stringsWithLineNumbers;
}
// Function to process a single TypeScript file
function processFile(filePath) {
try {
const sourceCode = readFileSync(filePath, 'utf8');
// Debug: Log first few characters to see if file is read correctly
console.log(`Processing ${filePath} {${sourceCode.length} chars}`);
// Create a TypeScript source file with more explicit options
const sourceFile = createSourceFile(
filePath,
sourceCode,
ScriptTarget.Latest,
true,
filePath.endsWith('tsx') ? ScriptKind.TSX : ScriptKind.TS,
);
// Check if parsing was successful
if (sourceFile.parseDiagnostics && sourceFile.parseDiagnostics.length > 0) {
console.warn(
`Parse warnings/errors in ${filePath}:`,
sourceFile.parseDiagnostics.map(d => d.messageText),
);
}
// Extract all strings from the file
const stringsWithLineNumbers = extractStrings(sourceFile);
console.log(` -> Found ${stringsWithLineNumbers.length} strings`);
return {
filePath,
strings: stringsWithLineNumbers,
stringCount: stringsWithLineNumbers.length,
};
} catch (error) {
console.error(`Error processing file ${filePath}:`, error.message);
console.error('Stack trace:', error.stack);
return {
filePath,
strings: [],
stringCount: 0,
error: error.message,
};
}
}
// Main function to process all TypeScript files
async function processAllTypeScriptFiles() {
try {
console.log('Starting file discovery...');
// Get all TypeScript files using fast-glob
const files = await glob(['**/*.ts', '**/*.tsx'], {
ignore: ['node_modules/**', 'dist/**', 'build/**', '**/*.d.ts'],
cwd: 'app',
absolute: true,
});
console.log(`Found ${files.length} TypeScript files:`);
files.slice(0, 5).forEach(file => console.log(` - ${file}`));
if (files.length > 5) {
console.log(` ... and ${files.length - 5} more`);
}
const results = [];
let totalStrings = 0;
// Process each file
for (let i = 0; i < files.length; i++) {
const file = files[i];
console.log(`\n[${i + 1}/${files.length}] Processing: ${file}`);
const result = processFile(file);
if (result.stringCount > 0) {
results.push(result);
}
totalStrings += result.stringCount;
}
console.log(`\n=== SUMMARY ===`);
console.log(`Total files processed: ${files.length}`);
console.log(`Total strings found: ${totalStrings}`);
return results;
} catch (error) {
console.error('Error processing TypeScript files:', error);
console.error('Stack trace:', error.stack);
throw error;
}
}
// Run the codemod
processAllTypeScriptFiles()
.then(results => {
console.log('\nProcessing complete!');
// Optional: Save results to a JSON file
writeFileSync(
'string-extraction-results.json',
JSON.stringify(results, null, 2),
);
console.log('Results saved to string-extraction-results.json');
})
.catch(error => {
console.error('Codemod failed:', error);
process.exit(1);
});
After running the script, it generated a manifest file of all string occurrence like this:
[
{
"filePath": "/src/app/login/route.ts",
"strings": [
{
"text": "Invalid Login Request",
"line": 9
},
{
"text": "&.Mui-selected",
"line": 21
}
],
"stringCount": 2
}
]
“Smart” replacement
The rough AST scan still yields false positives (e.g., CSS selectors). To safely convert strings, I used Auggie CLI to process each file individually. This allowed AI to decide case changes file by file without overwhelming context.
The most powerful AI software development platform with the industry-leading context engine.

Install the auggie cli by running:
npm install -g @augmentcode/auggie
Then loop through files:
fs.writeFileSync(tempFileName, JSON.stringify([fileResult], null, 2));
try {
// Use Auggie CLI to convert title case to sentence case
const auggieCMD = `auggie 'check this file ${tempFileName} and do ...' --print`;
console.log(Running Auggie for file ${i + 1}/${testFiles}...);
await new Promise((resolve, reject) => {
const cp = spawn("sh", ["-c", auggieCMD], { stdio: "inherit" });
cp.on("exit", (code) => {
if (code === 0) {
resolve();
} else {
reject(new Error(Auggie process exited with code ${code}));
}
});
cp.on("error", reject);
});
// Clean up temp file
fs.unlinkSync(tempFileName);
} catch (error) {
console.error(Error processing ${filePath}:, error.message);
// Clean up temp file even on error
if (fs.existsSync(tempFileName)) {
fs.unlinkSync(tempFileName);
}
}
The —print flag can instruct auggie to run in non-interactive mode.
Running the script by:
node ./scripts/codemd.js
Now enjoy the smart agent working for you.
We can see Auggie completely understand the requirement and handles case conversion intelligently. It successfully ignores false positives, leaving only valid UI text updated.

Summary
Auggie not only saved hours of manual work but also ensured consistency across the entire application. Sometimes, all it takes is breaking a task into smaller steps and using old-school scripts to orchestrate AI tools across a series of sub-tasks.