@i18n-agent/cli 1.0.1

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.
package/src/index.js ADDED
@@ -0,0 +1,156 @@
1
+ import { Command } from 'commander';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+
6
+ import { loginAction } from './commands/login.js';
7
+ import { logoutAction } from './commands/logout.js';
8
+ import { creditsAction } from './commands/credits.js';
9
+ import { languagesAction } from './commands/languages.js';
10
+ import { translateAction, translateTextAction, translateFileAction } from './commands/translate.js';
11
+ import { statusAction } from './commands/status.js';
12
+ import { downloadAction } from './commands/download.js';
13
+ import { resumeAction } from './commands/resume.js';
14
+ import { uploadAction, uploadsListAction } from './commands/upload.js';
15
+ import { analyzeAction } from './commands/analyze.js';
16
+ import { tuiAction } from './commands/tui.js';
17
+
18
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
19
+
20
+ function getVersion() {
21
+ const pkgPath = path.join(__dirname, '..', 'package.json');
22
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
23
+ return pkg.version;
24
+ }
25
+
26
+ export function createProgram() {
27
+ const program = new Command();
28
+
29
+ program
30
+ .name('i18nagent')
31
+ .description('Terminal client for i18n-agent translation service')
32
+ .version(getVersion());
33
+
34
+ // --- Auth ---
35
+
36
+ program
37
+ .command('login')
38
+ .description('Save API key to config file')
39
+ .option('--key <key>', 'API key (or enter interactively)')
40
+ .option('--force', 'Overwrite existing key')
41
+ .action(loginAction);
42
+
43
+ program
44
+ .command('logout')
45
+ .description('Remove saved API key')
46
+ .action(logoutAction);
47
+
48
+ // --- Translation ---
49
+
50
+ const addTranslateOpts = (cmd) => cmd
51
+ .option('--lang <languages>', 'Target language(s), comma-separated (e.g. es,fr,ja)')
52
+ .option('--source <lang>', 'Source language (auto-detected if omitted)')
53
+ .option('--audience <audience>', 'Target audience (e.g. general, technical)')
54
+ .option('--industry <industry>', 'Industry context (e.g. technology, healthcare)')
55
+ .option('--context <context>', 'Additional instructions for translation')
56
+ .option('--namespace <ns>', 'Namespace for project tracking')
57
+ .option('--pseudo', 'Pseudo-translation mode (no AI cost)')
58
+ .option('--skip-warnings', 'Skip source quality warnings')
59
+ .option('--output <dir>', 'Output directory for file translations')
60
+ .option('--json', 'Output as JSON');
61
+
62
+ const translate = program
63
+ .command('translate [input]')
64
+ .description('Translate text or file (auto-detected). Run without args for interactive mode.');
65
+ addTranslateOpts(translate).action(translateAction);
66
+
67
+ const translateTextCmd = translate
68
+ .command('text [text]')
69
+ .description('Translate text content');
70
+ addTranslateOpts(translateTextCmd).action(translateTextAction);
71
+
72
+ const translateFileCmd = translate
73
+ .command('file [path]')
74
+ .description('Translate a file');
75
+ addTranslateOpts(translateFileCmd).action(translateFileAction);
76
+
77
+ // --- Job management ---
78
+
79
+ program
80
+ .command('status <jobId>')
81
+ .description('Check translation job status')
82
+ .option('--json', 'Output as JSON')
83
+ .option('--page-size <n>', 'Languages per page (default: 50)')
84
+ .action(statusAction);
85
+
86
+ program
87
+ .command('download <jobId>')
88
+ .description('Download completed translations')
89
+ .option('--output <dir>', 'Output directory')
90
+ .option('--json', 'Output as JSON')
91
+ .action(downloadAction);
92
+
93
+ program
94
+ .command('resume <jobId>')
95
+ .description('Resume a failed or interrupted translation job')
96
+ .option('--json', 'Output as JSON')
97
+ .action(resumeAction);
98
+
99
+ // --- Upload ---
100
+
101
+ program
102
+ .command('upload [filePath]')
103
+ .description('Upload existing translations for reuse')
104
+ .requiredOption('--source <lang>', 'Source language code')
105
+ .requiredOption('--target <lang>', 'Target language code')
106
+ .option('--namespace <ns>', 'Namespace for tracking')
107
+ .option('--json', 'Output as JSON')
108
+ .action(uploadAction);
109
+
110
+ const uploads = program
111
+ .command('uploads')
112
+ .description('Manage uploaded translations');
113
+
114
+ uploads
115
+ .command('list')
116
+ .description('List uploaded translations in a namespace')
117
+ .requiredOption('--namespace <ns>', 'Namespace to list')
118
+ .option('--source <lang>', 'Filter by source language')
119
+ .option('--target <lang>', 'Filter by target language')
120
+ .option('--json', 'Output as JSON')
121
+ .action(uploadsListAction);
122
+
123
+ // --- Info ---
124
+
125
+ program
126
+ .command('credits')
127
+ .description('Check translation credit balance')
128
+ .option('--json', 'Output as JSON')
129
+ .action(creditsAction);
130
+
131
+ program
132
+ .command('languages')
133
+ .description('List supported languages')
134
+ .option('--json', 'Output as JSON')
135
+ .option('--no-quality', 'Exclude quality ratings')
136
+ .action(languagesAction);
137
+
138
+ program
139
+ .command('analyze <input>')
140
+ .description('Analyze content for translation readiness')
141
+ .requiredOption('--lang <lang>', 'Target language')
142
+ .option('--source <lang>', 'Source language')
143
+ .option('--audience <audience>', 'Target audience')
144
+ .option('--industry <industry>', 'Industry context')
145
+ .option('--json', 'Output as JSON')
146
+ .action(analyzeAction);
147
+
148
+ // --- Interactive ---
149
+
150
+ program
151
+ .command('tui')
152
+ .description('Launch interactive TUI mode')
153
+ .action(tuiAction);
154
+
155
+ return program;
156
+ }
@@ -0,0 +1,362 @@
1
+ /**
2
+ * Namespace Auto-Detection Utility for MCP Client
3
+ *
4
+ * Analyzes file paths and suggests namespace names based on project structure.
5
+ * Follows the same validation rules as the service-mcp namespace validator.
6
+ */
7
+
8
+ /**
9
+ * Auto-detect namespace from file path
10
+ */
11
+ export function detectNamespaceFromPath(filePath, options = {}) {
12
+ const { includeAlternatives = true, maxAlternatives = 3 } = options;
13
+
14
+ if (!filePath || typeof filePath !== 'string') {
15
+ return {
16
+ suggestion: null,
17
+ confidence: 0,
18
+ source: 'none',
19
+ reasoning: 'No file path provided',
20
+ alternatives: []
21
+ };
22
+ }
23
+
24
+ // Normalize path separators
25
+ const normalizedPath = filePath.replace(/\\/g, '/');
26
+ const pathSegments = normalizedPath.split('/').filter(segment => segment.length > 0);
27
+
28
+ // Pattern detection strategies (ordered by confidence)
29
+ const detectionStrategies = [
30
+ detectServicePattern,
31
+ detectGitRepoPattern,
32
+ detectProjectFolderPattern,
33
+ detectDirectoryPattern
34
+ ];
35
+
36
+ let bestResult = null;
37
+ const allSuggestions = [];
38
+
39
+ for (const strategy of detectionStrategies) {
40
+ const result = strategy(pathSegments, normalizedPath);
41
+ if (result.suggestion && isValidNamespace(result.suggestion)) {
42
+ if (!bestResult || result.confidence > bestResult.confidence) {
43
+ bestResult = result;
44
+ }
45
+ if (includeAlternatives && !allSuggestions.includes(result.suggestion)) {
46
+ allSuggestions.push(result.suggestion);
47
+ }
48
+ }
49
+ }
50
+
51
+ if (!bestResult) {
52
+ return {
53
+ suggestion: null,
54
+ confidence: 0,
55
+ source: 'none',
56
+ reasoning: 'Could not detect a valid namespace from the file path',
57
+ alternatives: []
58
+ };
59
+ }
60
+
61
+ // Generate alternatives
62
+ const alternatives = includeAlternatives
63
+ ? allSuggestions
64
+ .filter(s => s !== bestResult.suggestion)
65
+ .slice(0, maxAlternatives)
66
+ : [];
67
+
68
+ return {
69
+ ...bestResult,
70
+ alternatives
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Detect service-* pattern (highest confidence)
76
+ * Examples:
77
+ * - /path/to/service-platform/file.json → "service-platform"
78
+ * - /path/to/service-auth/config.yaml → "service-auth"
79
+ */
80
+ function detectServicePattern(pathSegments) {
81
+ for (let i = 0; i < pathSegments.length; i++) {
82
+ const segment = pathSegments[i];
83
+ if (segment.startsWith('service-') && segment.length > 8) {
84
+ const serviceName = segment.toLowerCase();
85
+ return {
86
+ suggestion: serviceName,
87
+ confidence: 0.9,
88
+ source: 'service-name',
89
+ reasoning: `Detected service name "${serviceName}" in path`,
90
+ alternatives: []
91
+ };
92
+ }
93
+ }
94
+
95
+ return { suggestion: null, confidence: 0, source: 'none', reasoning: '', alternatives: [] };
96
+ }
97
+
98
+ /**
99
+ * Detect git repository pattern (high confidence)
100
+ * Examples:
101
+ * - /path/to/i18n-agent/service-platform/file.json → "service-platform"
102
+ * - /path/to/my-project/src/file.ts → "my-project"
103
+ */
104
+ function detectGitRepoPattern(pathSegments) {
105
+ // Look for common project root indicators
106
+ const projectRootIndicators = [
107
+ 'package.json', '.git', 'node_modules', 'src', 'lib', 'app',
108
+ 'components', 'pages', 'public', 'assets', 'docs', 'tests'
109
+ ];
110
+
111
+ // Find potential project root by looking for these indicators
112
+ for (let i = pathSegments.length - 1; i >= 0; i--) {
113
+ const segment = pathSegments[i];
114
+
115
+ // If this looks like a project folder, use it
116
+ if (segment.includes('-') || segment.includes('_')) {
117
+ // Check if next segments contain project indicators
118
+ const remainingPath = pathSegments.slice(i + 1);
119
+ const hasProjectIndicators = remainingPath.some(seg =>
120
+ projectRootIndicators.includes(seg) ||
121
+ seg === 'src' ||
122
+ seg === 'lib' ||
123
+ seg.includes('component') ||
124
+ seg.includes('page')
125
+ );
126
+
127
+ if (hasProjectIndicators) {
128
+ const suggestion = normalizeNamespace(segment);
129
+ if (suggestion) {
130
+ return {
131
+ suggestion,
132
+ confidence: 0.7,
133
+ source: 'git-repo',
134
+ reasoning: `Detected project root "${segment}" based on directory structure`,
135
+ alternatives: []
136
+ };
137
+ }
138
+ }
139
+ }
140
+ }
141
+
142
+ return { suggestion: null, confidence: 0, source: 'none', reasoning: '', alternatives: [] };
143
+ }
144
+
145
+ /**
146
+ * Detect project folder pattern (medium confidence)
147
+ * Examples:
148
+ * - /Users/user/Documents/my-awesome-project/file.js → "my-awesome-project"
149
+ * - /workspace/client-docs/locales/en.json → "client-docs"
150
+ */
151
+ function detectProjectFolderPattern(pathSegments) {
152
+ // Look for segments that contain hyphens or underscores (common in project names)
153
+ const projectLikeSegments = pathSegments.filter(segment =>
154
+ (segment.includes('-') || segment.includes('_')) &&
155
+ segment.length >= 3 &&
156
+ !segment.startsWith('.') &&
157
+ segment !== 'node_modules'
158
+ );
159
+
160
+ if (projectLikeSegments.length > 0) {
161
+ // Use the last project-like segment (closest to the file)
162
+ const lastProjectSegment = projectLikeSegments[projectLikeSegments.length - 1];
163
+ const suggestion = normalizeNamespace(lastProjectSegment);
164
+
165
+ if (suggestion) {
166
+ return {
167
+ suggestion,
168
+ confidence: 0.5,
169
+ source: 'project-folder',
170
+ reasoning: `Detected project-like folder "${lastProjectSegment}" in path`,
171
+ alternatives: []
172
+ };
173
+ }
174
+ }
175
+
176
+ return { suggestion: null, confidence: 0, source: 'none', reasoning: '', alternatives: [] };
177
+ }
178
+
179
+ /**
180
+ * Detect directory pattern (low confidence fallback)
181
+ * Uses the immediate parent directory if it looks reasonable
182
+ */
183
+ function detectDirectoryPattern(pathSegments) {
184
+ if (pathSegments.length < 2) {
185
+ return { suggestion: null, confidence: 0, source: 'none', reasoning: '', alternatives: [] };
186
+ }
187
+
188
+ // Use the parent directory of the file
189
+ const parentDir = pathSegments[pathSegments.length - 2];
190
+ const suggestion = normalizeNamespace(parentDir);
191
+
192
+ if (suggestion && suggestion.length >= 3) {
193
+ return {
194
+ suggestion,
195
+ confidence: 0.3,
196
+ source: 'directory-name',
197
+ reasoning: `Using parent directory "${parentDir}" as fallback`,
198
+ alternatives: []
199
+ };
200
+ }
201
+
202
+ return { suggestion: null, confidence: 0, source: 'none', reasoning: '', alternatives: [] };
203
+ }
204
+
205
+ /**
206
+ * Normalize a potential namespace to follow validation rules
207
+ */
208
+ function normalizeNamespace(input) {
209
+ if (!input || typeof input !== 'string') return null;
210
+
211
+ // Convert to lowercase and trim
212
+ let normalized = input.toLowerCase().trim();
213
+
214
+ // Replace invalid characters with hyphens
215
+ normalized = normalized.replace(/[^a-z0-9_-]/g, '-');
216
+
217
+ // Remove multiple consecutive hyphens/underscores
218
+ normalized = normalized.replace(/[-_]{2,}/g, '-');
219
+
220
+ // Remove leading/trailing hyphens or underscores
221
+ normalized = normalized.replace(/^[-_]+|[-_]+$/g, '');
222
+
223
+ // Check length constraints
224
+ if (normalized.length < 3 || normalized.length > 50) {
225
+ return null;
226
+ }
227
+
228
+ // Check if it's a reserved keyword
229
+ const reservedKeywords = [
230
+ 'admin', 'api', 'www', 'system', 'root', 'default',
231
+ 'translations', 'upload', 'download', 'temp', 'cache',
232
+ 'public', 'private', 'test', 'staging', 'production'
233
+ ];
234
+
235
+ if (reservedKeywords.includes(normalized)) {
236
+ return null;
237
+ }
238
+
239
+ return normalized;
240
+ }
241
+
242
+ /**
243
+ * Quick validation using the same rules as service-mcp
244
+ */
245
+ function isValidNamespace(namespace) {
246
+ if (!namespace || typeof namespace !== 'string') return false;
247
+
248
+ // Basic validation
249
+ const pattern = /^[a-z0-9_-]+$/;
250
+ return (
251
+ namespace.length >= 3 &&
252
+ namespace.length <= 50 &&
253
+ pattern.test(namespace) &&
254
+ !['admin', 'api', 'www', 'system', 'root', 'default'].includes(namespace)
255
+ );
256
+ }
257
+
258
+ /**
259
+ * Generate additional namespace suggestions based on file context
260
+ */
261
+ export function generateNamespaceSuggestions(fileName, fileContent) {
262
+ const suggestions = [];
263
+
264
+ if (fileName) {
265
+ // Extract potential namespace from filename
266
+ const baseName = fileName.replace(/\.(json|yaml|yml|ts|js|md)$/i, '');
267
+ const normalized = normalizeNamespace(baseName);
268
+ if (normalized) {
269
+ suggestions.push(normalized);
270
+ }
271
+
272
+ // Look for common patterns in filename
273
+ if (fileName.includes('i18n') || fileName.includes('locale') || fileName.includes('lang')) {
274
+ suggestions.push('localization', 'translations', 'i18n-project');
275
+ }
276
+
277
+ if (fileName.includes('api') || fileName.includes('endpoint')) {
278
+ suggestions.push('api-docs', 'api-project');
279
+ }
280
+
281
+ if (fileName.includes('component') || fileName.includes('ui')) {
282
+ suggestions.push('components', 'ui-library');
283
+ }
284
+ }
285
+
286
+ if (fileContent) {
287
+ // Analyze content for clues (basic implementation)
288
+ const content = fileContent.toLowerCase();
289
+
290
+ if (content.includes('api') || content.includes('endpoint')) {
291
+ suggestions.push('api-docs');
292
+ }
293
+
294
+ if (content.includes('component') || content.includes('react') || content.includes('vue')) {
295
+ suggestions.push('components');
296
+ }
297
+
298
+ if (content.includes('translation') || content.includes('locale')) {
299
+ suggestions.push('translations');
300
+ }
301
+ }
302
+
303
+ // Remove duplicates and filter valid ones
304
+ return [...new Set(suggestions)]
305
+ .filter(s => isValidNamespace(s))
306
+ .slice(0, 5);
307
+ }
308
+
309
+ /**
310
+ * Get helpful namespace suggestions text for user guidance
311
+ */
312
+ export function getNamespaceSuggestionText(filePath, fileName) {
313
+ if (!filePath && !fileName) {
314
+ return `šŸ’” Namespace Suggestions:
315
+ • Use descriptive project names: "my-website", "mobile-app"
316
+ • Include service names: "service-auth", "api-gateway"
317
+ • Use organization prefixes: "company-docs", "team-frontend"
318
+ • Avoid generic names: "app", "project", "files"`;
319
+ }
320
+
321
+ const detection = detectNamespaceFromPath(filePath || fileName);
322
+
323
+ if (detection.suggestion) {
324
+ let text = `šŸŽÆ Auto-detected suggestion: "${detection.suggestion}"`;
325
+ text += `\nšŸ“ Source: ${getSourceDescription(detection.source)}`;
326
+ text += `\nšŸ’Ŗ Confidence: ${Math.round(detection.confidence * 100)}%`;
327
+
328
+ if (detection.alternatives.length > 0) {
329
+ text += `\nšŸ”„ Alternatives: ${detection.alternatives.join(', ')}`;
330
+ }
331
+
332
+ return text;
333
+ }
334
+
335
+ return `šŸ’” Namespace Suggestions for "${fileName || filePath}":
336
+ • Extract from path: "${getPathBasedSuggestion(filePath || fileName)}"
337
+ • Use project context: "docs", "website", "api"
338
+ • Include team/org: "frontend-team", "backend-api"
339
+ • Keep it descriptive: "user-dashboard", "admin-panel"`;
340
+ }
341
+
342
+ function getSourceDescription(source) {
343
+ const descriptions = {
344
+ 'service-name': 'Service name pattern detected',
345
+ 'git-repo': 'Git repository structure analyzed',
346
+ 'project-folder': 'Project folder pattern found',
347
+ 'directory-name': 'Parent directory used as fallback'
348
+ };
349
+ return descriptions[source] || 'Unknown source';
350
+ }
351
+
352
+ function getPathBasedSuggestion(path) {
353
+ if (!path) return 'my-project';
354
+
355
+ const segments = path.replace(/\\/g, '/').split('/').filter(s => s.length > 0);
356
+ for (let i = segments.length - 1; i >= 0; i--) {
357
+ const normalized = normalizeNamespace(segments[i]);
358
+ if (normalized) return normalized;
359
+ }
360
+
361
+ return 'my-project';
362
+ }
package/src/output.js ADDED
@@ -0,0 +1,39 @@
1
+ export function formatOutput(data, opts = {}) {
2
+ if (opts.json) {
3
+ return JSON.stringify(data, null, 2);
4
+ }
5
+ if (opts.template) {
6
+ return opts.template(data);
7
+ }
8
+ return JSON.stringify(data, null, 2);
9
+ }
10
+
11
+ export function formatError(message, opts = {}) {
12
+ if (opts.json) {
13
+ return JSON.stringify({ error: message });
14
+ }
15
+ return `Error: ${message}`;
16
+ }
17
+
18
+ export function formatSuccess(message, opts = {}) {
19
+ if (opts.json) {
20
+ return JSON.stringify({ success: true, message });
21
+ }
22
+ return message;
23
+ }
24
+
25
+ export function formatProgress(current, total, width = 20) {
26
+ const pct = total > 0 ? Math.round((current / total) * 100) : 0;
27
+ const filled = Math.round((pct / 100) * width);
28
+ const empty = width - filled;
29
+ const bar = '#'.repeat(filled) + '.'.repeat(empty);
30
+ return `[${bar}] ${pct}%`;
31
+ }
32
+
33
+ export function print(text) {
34
+ process.stdout.write(text + '\n');
35
+ }
36
+
37
+ export function printErr(text) {
38
+ process.stderr.write(text + '\n');
39
+ }
package/src/prompts.js ADDED
@@ -0,0 +1,65 @@
1
+ import readline from 'readline';
2
+
3
+ function createInterface() {
4
+ return readline.createInterface({
5
+ input: process.stdin,
6
+ output: process.stderr,
7
+ });
8
+ }
9
+
10
+ function ask(rl, question) {
11
+ return new Promise((resolve) => {
12
+ rl.question(question, (answer) => resolve(answer.trim()));
13
+ });
14
+ }
15
+
16
+ export async function promptText(question) {
17
+ const rl = createInterface();
18
+ try {
19
+ const answer = await ask(rl, question);
20
+ return answer || null;
21
+ } finally {
22
+ rl.close();
23
+ }
24
+ }
25
+
26
+ export async function promptApiKey() {
27
+ const rl = createInterface();
28
+ try {
29
+ const key = await ask(rl, 'Enter your API key (get one at https://app.i18nagent.ai): ');
30
+ if (!key) return null;
31
+ if (!key.startsWith('i18n_') || key.length <= 5) {
32
+ console.error('Invalid API key format. Keys should start with "i18n_"');
33
+ return null;
34
+ }
35
+ return key;
36
+ } finally {
37
+ rl.close();
38
+ }
39
+ }
40
+
41
+ export async function promptChoice(question, choices) {
42
+ const rl = createInterface();
43
+ try {
44
+ console.error(question);
45
+ choices.forEach((choice, i) => {
46
+ console.error(` [${i + 1}] ${choice}`);
47
+ });
48
+ const answer = await ask(rl, '\n Choose: ');
49
+ const num = parseInt(answer, 10);
50
+ if (isNaN(num) || num < 1 || num > choices.length) return null;
51
+ return num - 1;
52
+ } finally {
53
+ rl.close();
54
+ }
55
+ }
56
+
57
+ export async function promptConfirm(question) {
58
+ const rl = createInterface();
59
+ try {
60
+ const answer = await ask(rl, `${question} (y/n): `);
61
+ return answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
62
+ } finally {
63
+ rl.close();
64
+ }
65
+ }