cctrans 0.1.0 → 0.2.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.
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
- // tt — control + test CLI for the Claude Code bilingual (EN->ZH) overlay.
3
+ // cctrans — control + test CLI for the Claude Code bilingual overlay.
4
4
  //
5
- // tt on | off | toggle | status
6
- // tt backend <openai|google>
7
- // tt install | uninstall register/remove the MessageDisplay hook
8
- // tt last [N] translate the latest (or Nth-back) reply -> stdout
9
- // tt test <text...> translate ad-hoc text -> stdout
10
- // tt help
5
+ // cctrans on | off | toggle | status
6
+ // cctrans backend <openai|google>
7
+ // cctrans install | uninstall register/remove the MessageDisplay hook
8
+ // cctrans last [N] translate the latest (or Nth-back) reply -> stdout
9
+ // cctrans test <text...> translate ad-hoc text -> stdout
10
+ // cctrans help
11
11
 
12
12
  const fs = require('fs');
13
13
  const os = require('os');
@@ -36,8 +36,8 @@ function readSettings() {
36
36
  try { return JSON.parse(fs.readFileSync(SETTINGS, 'utf8')); } catch (e) { return {}; }
37
37
  }
38
38
  function writeSettings(s) {
39
- try { fs.copyFileSync(SETTINGS, SETTINGS + '.bak-tt'); } catch (e) {}
40
- const tmp = SETTINGS + '.tt.tmp';
39
+ try { fs.copyFileSync(SETTINGS, SETTINGS + '.bak-cctrans'); } catch (e) {}
40
+ const tmp = SETTINGS + '.cctrans.tmp';
41
41
  fs.writeFileSync(tmp, JSON.stringify(s, null, 2));
42
42
  fs.renameSync(tmp, SETTINGS);
43
43
  }
@@ -62,7 +62,7 @@ function install() {
62
62
  changed = true;
63
63
  }
64
64
  if (!inputHookInstalled(s)) {
65
- // Registered always; the hook exits instantly unless `tt input on`.
65
+ // Registered always; the hook exits instantly unless `cctrans input on`.
66
66
  s.hooks.UserPromptSubmit = s.hooks.UserPromptSubmit || [];
67
67
  s.hooks.UserPromptSubmit.push({ hooks: [{ type: 'command', command: 'node ' + INPUT_HOOK_PATH }] });
68
68
  changed = true;
@@ -73,23 +73,23 @@ function install() {
73
73
  } else {
74
74
  console.log(C.green('✓') + ' hooks already registered in ' + SETTINGS);
75
75
  }
76
- // Make `tt` runnable from anywhere (best-effort symlink on a common PATH dir).
76
+ // Make `cctrans` runnable from anywhere (best-effort symlink on a common PATH dir).
77
77
  const linkDir = path.join(os.homedir(), '.local', 'bin');
78
- const link = path.join(linkDir, 'tt');
78
+ const link = path.join(linkDir, 'cctrans');
79
79
  try {
80
80
  fs.mkdirSync(linkDir, { recursive: true });
81
81
  try { fs.unlinkSync(link); } catch (e) {}
82
- fs.symlinkSync(path.resolve(__dirname, 'tt.js'), link);
83
- fs.chmodSync(path.resolve(__dirname, 'tt.js'), 0o755);
84
- console.log(C.green('✓') + ' linked `tt` -> ' + link + (process.env.PATH.includes(linkDir) ? '' : C.dim(' (add ' + linkDir + ' to PATH)')));
82
+ fs.symlinkSync(path.resolve(__dirname, 'cctrans.js'), link);
83
+ fs.chmodSync(path.resolve(__dirname, 'cctrans.js'), 0o755);
84
+ console.log(C.green('✓') + ' linked `cctrans` -> ' + link + (process.env.PATH.includes(linkDir) ? '' : C.dim(' (add ' + linkDir + ' to PATH)')));
85
85
  } catch (e) {
86
- console.log(C.dim(' (could not symlink tt; add alias: alias tt=\'node ' + path.resolve(__dirname, 'tt.js') + '\')'));
86
+ console.log(C.dim(' (could not symlink cctrans; add alias: alias cctrans=\'node ' + path.resolve(__dirname, 'cctrans.js') + '\')'));
87
87
  }
88
88
  console.log('');
89
89
  console.log('Next:');
90
90
  console.log(' 1. Restart Claude Code (new session) so the hook loads.');
91
91
  console.log(' 2. Send any message — replies now show ' + C.bold('English + 中文') + ' inline.');
92
- console.log(' 3. Toggle anytime: ' + C.bold('!tt off') + ' / ' + C.bold('!tt on') + ' (typed inside Claude Code).');
92
+ console.log(' 3. Toggle anytime: ' + C.bold('!cctrans off') + ' / ' + C.bold('!cctrans on') + ' (typed inside Claude Code).');
93
93
  }
94
94
 
95
95
  function uninstall() {
@@ -114,11 +114,11 @@ function status() {
114
114
  const lang = getLang(st.target);
115
115
  console.log(C.bold('cctranslate status'));
116
116
  console.log(' enabled : ' + (st.enabled ? C.green('ON') : C.red('OFF')));
117
- console.log(' hook : ' + (installed ? C.green('installed') : C.red('not installed') + C.dim(' (run: tt install)')));
117
+ console.log(' hook : ' + (installed ? C.green('installed') : C.red('not installed') + C.dim(' (run: cctrans install)')));
118
118
  console.log(' backend : ' + st.backend + (b ? (b.available() ? C.green(' (ready)') : C.red(' (missing: ' + b.needs + ')')) : C.red(' (unknown backend)')));
119
- console.log(' lang : ' + st.target + (lang ? C.dim(' (' + lang.name + ')') : C.red(' (unsupported — see: tt lang)')));
120
- console.log(' input : ' + (st.inputEn ? C.green('ON') : 'off') + C.dim(' (prompt -> English; toggle: tt input on|off)'));
121
- console.log(' keys : ' + Object.keys(keys.readKeys()).length + ' in ' + keys.KEYS_FILE + C.dim(' (manage: tt key)'));
119
+ console.log(' lang : ' + st.target + (lang ? C.dim(' (' + lang.name + ')') : C.red(' (unsupported — see: cctrans lang)')));
120
+ console.log(' input : ' + (st.inputEn ? C.green('ON') : 'off') + C.dim(' (prompt -> English; toggle: cctrans input on|off)'));
121
+ console.log(' keys : ' + Object.keys(keys.readKeys()).length + ' in ' + keys.KEYS_FILE + C.dim(' (manage: cctrans key)'));
122
122
  console.log(' state : ' + STATE_FILE);
123
123
  }
124
124
 
@@ -141,7 +141,7 @@ function keyCmd(rest) {
141
141
 
142
142
  function backends() {
143
143
  const st = getState();
144
- console.log(C.bold('backends') + C.dim(' (switch: tt backend <id>)'));
144
+ console.log(C.bold('backends') + C.dim(' (switch: cctrans backend <id>)'));
145
145
  for (const b of listBackends()) {
146
146
  const mark = b.id === st.backend ? C.cyan('▶ ') : ' ';
147
147
  const ok = b.available() ? C.green('ready ') : C.red('missing ');
@@ -175,29 +175,29 @@ async function last(nBack) {
175
175
  }
176
176
 
177
177
  function help() {
178
- console.log(`${C.bold('tt')} — cctranslate: bilingual overlay for Claude Code
178
+ console.log(`${C.bold('cctrans')} — bilingual overlay for Claude Code
179
179
 
180
180
  ${C.bold('Control')}
181
- tt on | off | toggle turn the inline translation on/off
182
- tt input on | off translate non-English input to English (as context)
183
- tt status show current state
184
- tt lang [code] show/set target language (zh-Hans, zh-Hant, ja, ko, ru, hi)
185
- tt backend <id> choose translation engine
186
- tt backends list engines + availability
181
+ cctrans on | off | toggle turn the inline translation on/off
182
+ cctrans input on | off translate non-English input to English (as context)
183
+ cctrans status show current state
184
+ cctrans lang [code] show/set target language (zh-Hans, zh-Hant, ja, ko, ru, hi)
185
+ cctrans backend <id> choose translation engine
186
+ cctrans backends list engines + availability
187
187
 
188
188
  ${C.bold('Setup')}
189
- tt install register hooks (+ link tt), then run setup
190
- tt setup interactive wizard: language, backend, API keys
189
+ cctrans install register hooks (+ link cctrans), then run setup
190
+ cctrans setup interactive wizard: language, backend, API keys
191
191
  (flags: --lang --backend --key --yes)
192
- tt key [id] [value] manage API keys in ~/.cc-translate/keys.json
192
+ cctrans key [id] [value] manage API keys in ~/.cc-translate/keys.json
193
193
  (ids: openai, anthropic, deepl, azure, azure-region)
194
- tt uninstall remove the hooks
194
+ cctrans uninstall remove the hooks
195
195
 
196
196
  ${C.bold('Manual / test')}
197
- tt last [N] translate the latest (or N-back) assistant reply
198
- tt test <text...> translate ad-hoc English text
197
+ cctrans last [N] translate the latest (or N-back) assistant reply
198
+ cctrans test <text...> translate ad-hoc English text
199
199
 
200
- ${C.dim('Tip: toggle from inside Claude Code by typing !tt off / !tt on')}`);
200
+ ${C.dim('Tip: toggle from inside Claude Code by typing !cctrans off / !cctrans on')}`);
201
201
  }
202
202
 
203
203
  async function main() {
@@ -210,7 +210,7 @@ async function main() {
210
210
  const id = rest[0];
211
211
  const b = id && getBackend(id);
212
212
  if (!b) {
213
- console.error('usage: tt backend <' + listBackends().map((x) => x.id).join('|') + '>');
213
+ console.error('usage: cctrans backend <' + listBackends().map((x) => x.id).join('|') + '>');
214
214
  process.exit(1);
215
215
  }
216
216
  setState({ backend: id });
@@ -253,7 +253,7 @@ async function main() {
253
253
  else if (sub === 'toggle') setState({ inputEn: !getState().inputEn });
254
254
  const st = getState();
255
255
  console.log('input translation (prompt -> English): ' + (st.inputEn ? C.green('ON') : C.red('OFF')) +
256
- (inputHookInstalled() ? '' : C.red(' (hook not installed — run: tt install)')));
256
+ (inputHookInstalled() ? '' : C.red(' (hook not installed — run: cctrans install)')));
257
257
  break;
258
258
  }
259
259
  case 'uninstall': uninstall(); break;
@@ -261,7 +261,7 @@ async function main() {
261
261
  case 'last': await last(parseInt(rest[0], 10) || 0); break;
262
262
  case 'test': {
263
263
  const text = rest.join(' ');
264
- if (!text) { console.error('usage: tt test <text>'); process.exit(1); }
264
+ if (!text) { console.error('usage: cctrans test <text>'); process.exit(1); }
265
265
  await renderText(text); break;
266
266
  }
267
267
  case 'help': case '--help': case '-h': case undefined: help(); break;
@@ -42,8 +42,8 @@ function saveFence(id, index, inFence, final) {
42
42
  let data = '';
43
43
  process.stdin.on('data', (d) => (data += d));
44
44
  process.stdin.on('end', async () => {
45
- if (process.env.TT_DEBUG_STDIN) {
46
- try { fs.appendFileSync(process.env.TT_DEBUG_STDIN, '\n===== delta =====\n' + data + '\n'); } catch (e) {}
45
+ if (process.env.CCTRANS_DEBUG_STDIN) {
46
+ try { fs.appendFileSync(process.env.CCTRANS_DEBUG_STDIN, '\n===== delta =====\n' + data + '\n'); } catch (e) {}
47
47
  }
48
48
  let inp = {};
49
49
  try { inp = JSON.parse(data); } catch (e) { return showOriginal(); }
@@ -52,8 +52,8 @@ process.stdin.on('end', async () => {
52
52
  if (!delta) return showOriginal();
53
53
 
54
54
  // Recursion guard: the claude-code backend spawns `claude -p` with
55
- // TT_DISABLE=1 so a child Claude process can never re-enter this hook.
56
- if (process.env.TT_DISABLE) return showOriginal();
55
+ // CCTRANS_DISABLE=1 so a child Claude process can never re-enter this hook.
56
+ if (process.env.CCTRANS_DISABLE) return showOriginal();
57
57
 
58
58
  let st;
59
59
  try { st = getState(); } catch (e) { return showOriginal(); }
@@ -23,10 +23,10 @@ function passThrough() { process.exit(0); }
23
23
  let data = '';
24
24
  process.stdin.on('data', (d) => (data += d));
25
25
  process.stdin.on('end', async () => {
26
- if (process.env.TT_DEBUG_STDIN) {
27
- try { require('fs').appendFileSync(process.env.TT_DEBUG_STDIN, '\n===== prompt =====\n' + data + '\n'); } catch (e) {}
26
+ if (process.env.CCTRANS_DEBUG_STDIN) {
27
+ try { require('fs').appendFileSync(process.env.CCTRANS_DEBUG_STDIN, '\n===== prompt =====\n' + data + '\n'); } catch (e) {}
28
28
  }
29
- if (process.env.TT_DISABLE) return passThrough();
29
+ if (process.env.CCTRANS_DISABLE) return passThrough();
30
30
 
31
31
  let inp = {};
32
32
  try { inp = JSON.parse(data); } catch (e) { return passThrough(); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cctrans",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Bilingual inline translation overlay for Claude Code: a translated line (zh/ja/ko/ru/hi) under each English line, right in the conversation — non-destructive, zero main-loop tokens.",
5
5
  "license": "MIT",
6
6
  "author": "Roy Jiang",
@@ -24,7 +24,7 @@
24
24
  "korean"
25
25
  ],
26
26
  "bin": {
27
- "tt": "bin/tt.js"
27
+ "cctrans": "bin/cctrans.js"
28
28
  },
29
29
  "files": [
30
30
  "bin/",
@@ -32,7 +32,8 @@
32
32
  "src/",
33
33
  "README*.md",
34
34
  "ROADMAP.md",
35
- "LICENSE"
35
+ "LICENSE",
36
+ "MOTIVATION.md"
36
37
  ],
37
38
  "engines": {
38
39
  "node": ">=18"
@@ -16,7 +16,7 @@ const SCHEMA = {
16
16
  module.exports = {
17
17
  id: 'anthropic',
18
18
  kind: 'llm',
19
- needs: 'anthropic key (tt key anthropic <value>)',
19
+ needs: 'anthropic key (cctrans key anthropic <value>)',
20
20
  available() { return !!getKey('anthropic'); },
21
21
  async translate(lines, langCode, opts) {
22
22
  opts = opts || {};
@@ -8,7 +8,7 @@ const { getKey } = require('../keys');
8
8
  module.exports = {
9
9
  id: 'azure',
10
10
  kind: 'mt',
11
- needs: 'azure key (tt key azure <value>; region: tt key azure-region <value>)',
11
+ needs: 'azure key (cctrans key azure <value>; region: cctrans key azure-region <value>)',
12
12
  available() { return !!getKey('azure'); },
13
13
  async translate(lines, langCode) {
14
14
  const key = getKey('azure');
@@ -3,7 +3,7 @@
3
3
  // on the user's Claude subscription (no separate API key). Measured ~3s per
4
4
  // call (CLI startup) — usable within the hook's 10s budget but noticeably
5
5
  // slower than HTTP backends; offered as the no-key option, not the default.
6
- // TT_DISABLE=1 is set on the child as a recursion guard (the hook exits early
6
+ // CCTRANS_DISABLE=1 is set on the child as a recursion guard (the hook exits early
7
7
  // when it sees it), and --settings {} -style hook loading is avoided by -p
8
8
  // print mode having no display path.
9
9
  const { execFile } = require('child_process');
@@ -16,7 +16,7 @@ function runClaude(prompt, timeoutMs) {
16
16
  ['-p', '--model', 'claude-haiku-4-5', '--output-format', 'text'],
17
17
  {
18
18
  timeout: timeoutMs,
19
- env: Object.assign({}, process.env, { TT_DISABLE: '1' }),
19
+ env: Object.assign({}, process.env, { CCTRANS_DISABLE: '1' }),
20
20
  maxBuffer: 1024 * 1024,
21
21
  },
22
22
  (err, stdout) => (err ? reject(err) : resolve(stdout)),
@@ -29,7 +29,7 @@ module.exports = {
29
29
  id: 'claude-code',
30
30
  kind: 'cli',
31
31
  needs: 'claude CLI logged in (uses your subscription; ~3s/call)',
32
- available() { return !process.env.TT_DISABLE; },
32
+ available() { return !process.env.CCTRANS_DISABLE; },
33
33
  async translate(lines, langCode, opts) {
34
34
  opts = opts || {};
35
35
  const lang = getLang(langCode);
@@ -8,7 +8,7 @@ const { getKey } = require('../keys');
8
8
  module.exports = {
9
9
  id: 'deepl',
10
10
  kind: 'mt',
11
- needs: 'deepl key (tt key deepl <value>)',
11
+ needs: 'deepl key (cctrans key deepl <value>)',
12
12
  available() { return !!getKey('deepl'); },
13
13
  async translate(lines, langCode) {
14
14
  const key = getKey('deepl');
@@ -2,7 +2,7 @@
2
2
  // Backend registry. Each backend implements:
3
3
  // id - string
4
4
  // kind - 'llm' | 'mt' | 'cli' (informational)
5
- // needs - human-readable requirement shown by `tt backends`
5
+ // needs - human-readable requirement shown by `cctrans backends`
6
6
  // available() -> boolean (are its prerequisites present?)
7
7
  // translate(lines, langCode, opts) -> Promise<string[]> (same length/order)
8
8
 
@@ -6,7 +6,7 @@ const { getKey } = require('../keys');
6
6
  module.exports = {
7
7
  id: 'openai',
8
8
  kind: 'llm',
9
- needs: 'openai key (tt key openai <value>)',
9
+ needs: 'openai key (cctrans key openai <value>)',
10
10
  available() { return !!getKey('openai'); },
11
11
  async translate(lines, langCode, opts) {
12
12
  opts = opts || {};
package/src/config.js CHANGED
@@ -1,9 +1,9 @@
1
1
  'use strict';
2
2
  // Persistent settings for the translator. Everything user-configurable lives
3
3
  // in files under ~/.cc-translate/ — never in shell environment variables:
4
- // state.json — settings (this module); edit by hand or via tt commands
4
+ // state.json — settings (this module); edit by hand or via cctrans commands
5
5
  // keys.json — API secrets (src/keys.js); chmod 600
6
- // TT_HOME (test plumbing) and TT_DISABLE/TT_DEBUG_STDIN (hook internals) are
6
+ // CCTRANS_HOME (test plumbing) and CCTRANS_DISABLE/CCTRANS_DEBUG_STDIN (hook internals) are
7
7
  // the only env vars the tool reads.
8
8
 
9
9
  const fs = require('fs');
@@ -11,7 +11,7 @@ const os = require('os');
11
11
  const path = require('path');
12
12
 
13
13
  const HOME = os.homedir();
14
- const BASE = process.env.TT_HOME || path.join(HOME, '.cc-translate');
14
+ const BASE = process.env.CCTRANS_HOME || path.join(HOME, '.cc-translate');
15
15
  const STATE_FILE = path.join(BASE, 'state.json');
16
16
  const CACHE_DIR = path.join(BASE, 'cache');
17
17
 
package/src/keys.js CHANGED
@@ -1,17 +1,17 @@
1
1
  'use strict';
2
2
  // API-key store. Keys come from exactly ONE place: ~/.cc-translate/keys.json
3
- // (chmod 600), written by `tt setup` / `tt key` or edited by hand. Shell
3
+ // (chmod 600), written by `cctrans setup` / `cctrans key` or edited by hand. Shell
4
4
  // environment variables are never consulted — this tool's keys and the
5
5
  // terminal's keys cannot contaminate each other.
6
6
  //
7
7
  // NOTE: keys.js must not require config.js (config.js requires us for the
8
- // default-backend decision). TT_HOME is internal plumbing for tests only.
8
+ // default-backend decision). CCTRANS_HOME is internal plumbing for tests only.
9
9
 
10
10
  const fs = require('fs');
11
11
  const os = require('os');
12
12
  const path = require('path');
13
13
 
14
- const BASE = process.env.TT_HOME || path.join(os.homedir(), '.cc-translate');
14
+ const BASE = process.env.CCTRANS_HOME || path.join(os.homedir(), '.cc-translate');
15
15
  const KEYS_FILE = path.join(BASE, 'keys.json');
16
16
 
17
17
  const KEY_IDS = ['openai', 'anthropic', 'deepl', 'azure', 'azure-region'];
package/src/setup.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
  // Interactive setup wizard: language -> backend -> API-key entry -> live
3
- // verification -> save. Re-runnable via `tt setup`; non-interactive with
3
+ // verification -> save. Re-runnable via `cctrans setup`; non-interactive with
4
4
  // flags (--lang, --backend, --key, --yes). Keys go to keys.json only — the
5
5
  // shell environment is never read.
6
6
 
@@ -32,7 +32,7 @@ async function runSetup(opts) {
32
32
  };
33
33
 
34
34
  try {
35
- console.log(C.bold('cctranslate setup') + C.dim(' (re-run anytime: tt setup)'));
35
+ console.log(C.bold('cctranslate setup') + C.dim(' (re-run anytime: cctrans setup)'));
36
36
 
37
37
  // 1. Target language
38
38
  let lang = opts.lang;
@@ -89,7 +89,7 @@ async function runSetup(opts) {
89
89
  console.log(C.red('verification failed: ' + e.message));
90
90
  }
91
91
 
92
- console.log(C.dim('\nNext: restart Claude Code (new session). Toggle with `!tt off` / `!tt on`; input translation: `tt input on`.'));
92
+ console.log(C.dim('\nNext: restart Claude Code (new session). Toggle with `!cctrans off` / `!cctrans on`; input translation: `cctrans input on`.'));
93
93
  return true;
94
94
  } finally {
95
95
  if (rl) rl.close();
package/src/transcript.js CHANGED
@@ -49,7 +49,7 @@ function newestJsonlIn(dir) {
49
49
  // 3) globally newest .jsonl across all projects (the active session is
50
50
  // almost always the most recently written one).
51
51
  function findTranscript(cwd) {
52
- if (process.env.TT_TRANSCRIPT) return process.env.TT_TRANSCRIPT;
52
+ if (process.env.CCTRANS_TRANSCRIPT) return process.env.CCTRANS_TRANSCRIPT;
53
53
 
54
54
  const root = projectsRoot();
55
55
  const dir = path.join(root, slugForCwd(cwd || process.cwd()));