pinglet-cli 0.5.2 → 0.5.3

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.
@@ -40,6 +40,7 @@ exports.isClaudeIntegrationInstalled = isClaudeIntegrationInstalled;
40
40
  exports.installClaudeIntegration = installClaudeIntegration;
41
41
  exports.uninstallClaudeIntegration = uninstallClaudeIntegration;
42
42
  const fs = __importStar(require("fs"));
43
+ const i18n_1 = require("../i18n");
43
44
  const os = __importStar(require("os"));
44
45
  const path = __importStar(require("path"));
45
46
  const child_process_1 = require("child_process");
@@ -110,38 +111,38 @@ function installSlashCommand() {
110
111
  const cli = (0, config_1.cliPath)();
111
112
  const files = {
112
113
  "pinglet.md": `---
113
- description: 메시지를 Pinglet에 등록해 다른 개발자들의 터미널에 표시
114
+ description: ${(0, i18n_1.t)("slash.postDesc")}
114
115
  allowed-tools: Bash(node:*)
115
116
  ---
116
117
  <!-- ${COMMAND_MARKER} -->
117
118
 
118
- 등록 결과: !\`node "${cli}" post --quiet "$ARGUMENTS"\`
119
+ Result: !\`node "${cli}" post --quiet "$ARGUMENTS"\`
119
120
 
120
- 위 등록 결과 줄을 아무것도 덧붙이거나 바꾸지 말고 그대로 한 줄만 출력하세요.
121
- 다른 설명, 안내, 작업을 일절 하지 마세요.
121
+ Output the result line above exactly as-is, as a single line, without adding or changing anything.
122
+ Do not add any explanation, guidance, or perform any other action.
122
123
  `,
123
124
  "pinglet-login.md": `---
124
- description: Pinglet 계정 연결 (브라우저에서 GitHub 또는 Google 로그인, --github/--google로 지정 가능)
125
+ description: ${(0, i18n_1.t)("slash.loginDesc")}
125
126
  allowed-tools: Bash(node:*)
126
127
  ---
127
128
  <!-- ${COMMAND_MARKER} -->
128
129
 
129
- 로그인 결과:
130
+ Result:
130
131
  !\`node "${cli}" login --quiet $ARGUMENTS\`
131
132
 
132
- 위 로그인 결과를 아무것도 덧붙이거나 바꾸지 말고 그대로 출력하세요.
133
- 다른 설명, 안내, 작업을 일절 하지 마세요.
133
+ Output the result above exactly as-is, without adding or changing anything.
134
+ Do not add any explanation, guidance, or perform any other action.
134
135
  `,
135
136
  "pinglet-logout.md": `---
136
- description: Pinglet 로그인 해제 (이 기기의 로그인 토큰만 제거)
137
+ description: ${(0, i18n_1.t)("slash.logoutDesc")}
137
138
  allowed-tools: Bash(node:*)
138
139
  ---
139
140
  <!-- ${COMMAND_MARKER} -->
140
141
 
141
- 로그아웃 결과: !\`node "${cli}" logout\`
142
+ Result: !\`node "${cli}" logout\`
142
143
 
143
- 위 로그아웃 결과 줄을 아무것도 덧붙이거나 바꾸지 말고 그대로 한 줄만 출력하세요.
144
- 다른 설명, 안내, 작업을 일절 하지 마세요.
144
+ Output the result line above exactly as-is, as a single line, without adding or changing anything.
145
+ Do not add any explanation, guidance, or perform any other action.
145
146
  `,
146
147
  };
147
148
  for (const [name, body] of Object.entries(files)) {
@@ -363,7 +364,7 @@ function installClaudeIntegration(config, options = {}) {
363
364
  return {
364
365
  ok: false,
365
366
  needsForce: true,
366
- reason: "기존 statusLine 설정이 있습니다 (백업 후 교체하려면 --force).",
367
+ reason: (0, i18n_1.t)("claude.existingStatusLine"),
367
368
  };
368
369
  }
369
370
  config.adapters.claude = {
@@ -396,7 +397,7 @@ function installClaudeIntegration(config, options = {}) {
396
397
  }
397
398
  return {
398
399
  ok: false,
399
- reason: "settings.json이 계속 변경되고 있어 설치하지 못했습니다. 잠시 후 다시 시도하세요.",
400
+ reason: (0, i18n_1.t)("claude.settingsBusy"),
400
401
  };
401
402
  }
402
403
  function uninstallClaudeIntegration(config) {
@@ -38,6 +38,7 @@ exports.isCodexIntegrationInstalled = isCodexIntegrationInstalled;
38
38
  exports.installCodexIntegration = installCodexIntegration;
39
39
  exports.uninstallCodexIntegration = uninstallCodexIntegration;
40
40
  const fs = __importStar(require("fs"));
41
+ const i18n_1 = require("../i18n");
41
42
  const os = __importStar(require("os"));
42
43
  const path = __importStar(require("path"));
43
44
  const child_process_1 = require("child_process");
@@ -127,7 +128,7 @@ function installCodexIntegration(config, options = {}) {
127
128
  return {
128
129
  ok: false,
129
130
  needsForce: true,
130
- reason: "기존 notify 설정이 있습니다 (백업 후 교체하려면 --force).",
131
+ reason: (0, i18n_1.t)("codex.existingNotify"),
131
132
  };
132
133
  }
133
134
  config.adapters.codex = {
package/dist/cli.js CHANGED
@@ -13,27 +13,8 @@ const flush_1 = require("./commands/flush");
13
13
  const refresh_1 = require("./commands/refresh");
14
14
  const ping_1 = require("./commands/ping");
15
15
  const post_1 = require("./commands/post");
16
- const HELP = `pinglet v${config_1.VERSION} — AI가 생각하는 동안 만나는 개발자 메시지 네트워크
17
-
18
- 사용법: pinglet <command>
19
-
20
- install [--api <url>] [--force] Claude Code integration 설치
21
- [--claude | --codex] Codex는 OS 알림 방식이라 --codex로 opt-in (experimental)
22
- uninstall [--purge] integration 제거 (--purge: ~/.pinglet까지 삭제)
23
- login [--github | --google] 계정 연결 (GitHub 또는 Google, 생략 시 브라우저에서 선택)
24
- [--token <jwt>]
25
- logout 로그인 해제 (설치·캐시는 유지)
26
- (Claude Code 안에서는 /pinglet-login, /pinglet-logout)
27
- post "메시지" [--category <c>] 메시지 작성 (로그인 필요, 읽기는 익명 가능)
28
- doctor 설치/캐시/서버 상태 진단
29
- ping 메시지 미리보기
30
-
31
- 내부 명령 (integration이 호출):
32
- statusline Claude Code statusLine hook
33
- notify Codex notify hook
34
- flush 이벤트 큐 batch 전송
35
- refresh feed 캐시 갱신 + heartbeat
36
- `;
16
+ const i18n_1 = require("./i18n");
17
+ const HELP = (0, i18n_1.t)("cli.help", { version: config_1.VERSION });
37
18
  async function main() {
38
19
  const [command, ...rest] = process.argv.slice(2);
39
20
  switch (command) {
@@ -80,7 +61,7 @@ async function main() {
80
61
  if (!command) {
81
62
  const config = (0, config_1.loadConfig)();
82
63
  if (!config.adapters.claude && !config.adapters.codex) {
83
- console.log("pinglet: 아직 설정 전입니다 — 첫 설정을 시작합니다.");
64
+ console.log((0, i18n_1.t)("cli.firstSetup"));
84
65
  await (0, install_1.runInstall)([]);
85
66
  break;
86
67
  }
@@ -7,6 +7,7 @@ const cache_1 = require("../cache");
7
7
  const queue_1 = require("../queue");
8
8
  const claude_1 = require("../adapters/claude");
9
9
  const codex_1 = require("../adapters/codex");
10
+ const i18n_1 = require("../i18n");
10
11
  const ok = (label) => console.log(`✓ ${label}`);
11
12
  const warn = (label) => console.log(`○ ${label}`);
12
13
  const bad = (label) => console.log(`✗ ${label}`);
@@ -17,7 +18,7 @@ async function runDoctor() {
17
18
  if (nodeMajor >= 18)
18
19
  ok(`Node.js ${process.versions.node}`);
19
20
  else
20
- bad(`Node.js ${process.versions.node} — 18 이상이 필요합니다`);
21
+ bad((0, i18n_1.t)("doctor.nodeOld", { version: process.versions.node }));
21
22
  // Claude
22
23
  if ((0, claude_1.detectClaude)()) {
23
24
  ok("Claude Code detected");
@@ -25,14 +26,14 @@ async function runDoctor() {
25
26
  ok("Claude integration installed");
26
27
  const spinner = (0, claude_1.spinnerVerbsStatus)();
27
28
  if (spinner.count > 0) {
28
- ok(`Spinner 메시지: ${spinner.count}개 등록됨`);
29
+ ok((0, i18n_1.t)("doctor.spinnerCount", { n: spinner.count }));
29
30
  }
30
31
  else {
31
- warn("Spinner 메시지 미등록 — `pinglet refresh` 실행");
32
+ warn((0, i18n_1.t)("doctor.spinnerMissing"));
32
33
  }
33
34
  }
34
35
  else
35
- warn("Claude integration not installed — `pinglet install` 실행");
36
+ warn((0, i18n_1.t)("doctor.claudeNotInstalled"));
36
37
  }
37
38
  else {
38
39
  warn("Claude Code not found");
@@ -43,7 +44,7 @@ async function runDoctor() {
43
44
  if ((0, codex_1.isCodexIntegrationInstalled)())
44
45
  ok("Codex integration installed");
45
46
  else
46
- warn("Codex integration not installed — `pinglet install` 실행");
47
+ warn((0, i18n_1.t)("doctor.codexNotInstalled"));
47
48
  }
48
49
  else {
49
50
  warn("Codex not found");
@@ -55,32 +56,32 @@ async function runDoctor() {
55
56
  ok(`${agentType} installation registered (${record.installationId})`);
56
57
  }
57
58
  if (Object.keys(config.installations).length === 0) {
58
- warn("서버에 등록된 installation이 없습니다 (오프라인 설치 상태)");
59
+ warn((0, i18n_1.t)("doctor.noInstallation"));
59
60
  }
60
61
  // 로컬 캐시 / 큐
61
62
  const messages = (0, cache_1.loadFeedMessages)();
62
63
  const age = (0, cache_1.feedAgeMs)();
63
64
  if (messages.length === 0) {
64
- warn("Feed cache 비어 있음 — 기본 spinner로 표시 중 (서버 feed 수신 후 Ping 표시)");
65
+ warn((0, i18n_1.t)("doctor.feedEmpty"));
65
66
  }
66
67
  else {
67
68
  const ageMin = age === null ? "?" : Math.round(age / 60_000);
68
- ok(`Feed cache: ${messages.length}개 (${ageMin}분 전 갱신)`);
69
+ ok((0, i18n_1.t)("doctor.feedCache", { n: messages.length, min: ageMin }));
69
70
  }
70
71
  const queued = (0, queue_1.countEvents)();
71
72
  if (queued < 200)
72
- ok(`Event queue: ${queued}개 대기`);
73
+ ok((0, i18n_1.t)("doctor.queue", { n: queued }));
73
74
  else
74
- warn(`Event queue: ${queued}개 대기 — 서버 전송이 밀려 있습니다`);
75
+ warn((0, i18n_1.t)("doctor.queueBacklog", { n: queued }));
75
76
  // API
76
77
  const online = await (0, api_1.checkHealth)(config);
77
78
  if (online)
78
79
  ok(`API reachable (${config.apiBaseUrl})`);
79
80
  else
80
- warn(`API unreachable (${config.apiBaseUrl}) — 오프라인 모드로 동작 중`);
81
+ warn((0, i18n_1.t)("doctor.apiUnreachable", { api: config.apiBaseUrl }));
81
82
  if (config.userToken)
82
- ok("로그인됨");
83
+ ok((0, i18n_1.t)("doctor.loggedIn"));
83
84
  else
84
- warn("로그인 안 됨 — 읽기는 가능, 메시지 작성/반응은 `pinglet login` 후");
85
+ warn((0, i18n_1.t)("doctor.notLoggedIn"));
85
86
  console.log("");
86
87
  }
@@ -40,6 +40,7 @@ const api_1 = require("../api");
40
40
  const cache_1 = require("../cache");
41
41
  const claude_1 = require("../adapters/claude");
42
42
  const codex_1 = require("../adapters/codex");
43
+ const i18n_1 = require("../i18n");
43
44
  /**
44
45
  * 기존 설정 교체 확인. Enter 또는 y면 진행(기본 Yes — 설치 의도가 명확하므로).
45
46
  * non-TTY(postinstall, CI)에서는 묻지 않고 false — 사용자 설정을 말없이
@@ -81,17 +82,17 @@ async function runInstall(args) {
81
82
  console.log(hasCodex ? "✓ Codex detected" : "○ Codex not found");
82
83
  }
83
84
  else if ((0, codex_1.detectCodex)()) {
84
- console.log("○ Codex 감지됨 — OS 알림 방식이라 기본 설치에서 제외합니다 (원하면 pinglet install --codex)");
85
+ console.log((0, i18n_1.t)("install.codexOptOut"));
85
86
  }
86
87
  if (!hasClaude && !hasCodex) {
87
- console.log("\n설치할 대상이 없습니다. Claude Code 또는 Codex를 먼저 설치해 주세요.");
88
+ console.log((0, i18n_1.t)("install.noTarget"));
88
89
  process.exitCode = 1;
89
90
  return;
90
91
  }
91
92
  const online = await (0, api_1.checkHealth)(config);
92
93
  if (!online) {
93
- console.log(`○ Pinglet 서버(${config.apiBaseUrl})에 연결할 수 없어 오프라인 모드로 설치합니다.`);
94
- console.log(" 서버 등록과 feed 수신은 온라인 상태가 되면 자동으로 재시도됩니다.");
94
+ console.log((0, i18n_1.t)("install.offline", { api: config.apiBaseUrl }));
95
+ console.log((0, i18n_1.t)("install.offlineNote"));
95
96
  }
96
97
  const register = async (agentType) => {
97
98
  if (config.installations[agentType] || !online)
@@ -106,7 +107,7 @@ async function runInstall(args) {
106
107
  await register("CLAUDE");
107
108
  let result = (0, claude_1.installClaudeIntegration)(config, { force });
108
109
  if (!result.ok && result.needsForce) {
109
- const yes = await confirmReplace(" 기존 statusLine 설정이 있습니다. 백업 후 교체할까요? (uninstall 시 복원) (Y/n) ");
110
+ const yes = await confirmReplace((0, i18n_1.t)("install.askReplaceStatusLine"));
110
111
  if (yes)
111
112
  result = (0, claude_1.installClaudeIntegration)(config, { force: true });
112
113
  }
@@ -118,7 +119,7 @@ async function runInstall(args) {
118
119
  await register("CODEX");
119
120
  let result = (0, codex_1.installCodexIntegration)(config, { force });
120
121
  if (!result.ok && result.needsForce) {
121
- const yes = await confirmReplace(" 기존 notify 설정이 있습니다. 백업 후 교체할까요? (uninstall 시 복원) (Y/n) ");
122
+ const yes = await confirmReplace((0, i18n_1.t)("install.askReplaceNotify"));
122
123
  if (yes)
123
124
  result = (0, codex_1.installCodexIntegration)(config, { force: true });
124
125
  }
@@ -142,9 +143,9 @@ async function runInstall(args) {
142
143
  }
143
144
  const targets = [hasClaude && "claude", hasCodex && "codex"]
144
145
  .filter(Boolean)
145
- .join(" 또는 ");
146
- console.log(`\n설치 완료! 평소처럼 ${targets}를 실행하면 Ping이 표시됩니다.`);
147
- console.log(" 상태 확인: pinglet doctor");
148
- console.log(' 메시지 작성: pinglet login 후 pinglet post "메시지"');
149
- console.log(" 계정 연결: pinglet login (메시지 작성/반응에 필요)");
146
+ .join((0, i18n_1.t)("install.or"));
147
+ console.log((0, i18n_1.t)("install.done", { targets }));
148
+ console.log((0, i18n_1.t)("install.doneDoctor"));
149
+ console.log((0, i18n_1.t)("install.donePost"));
150
+ console.log((0, i18n_1.t)("install.doneLogin"));
150
151
  }
@@ -39,6 +39,7 @@ const http = __importStar(require("http"));
39
39
  const readline = __importStar(require("readline"));
40
40
  const config_1 = require("../config");
41
41
  const api_1 = require("../api");
42
+ const i18n_1 = require("../i18n");
42
43
  const LOGIN_TIMEOUT_MS = 180_000;
43
44
  function prompt(question) {
44
45
  const rl = readline.createInterface({
@@ -65,18 +66,31 @@ function openBrowser(url) {
65
66
  }
66
67
  }
67
68
  const RESULT_HTML = (ok) => `<!doctype html>
68
- <html lang="ko"><head><meta charset="utf-8"><title>Pinglet</title>
69
+ <html lang="${(0, i18n_1.currentLocale)()}"><head><meta charset="utf-8"><title>Pinglet</title>
69
70
  <style>body{font-family:-apple-system,sans-serif;display:flex;align-items:center;
70
71
  justify-content:center;height:100vh;margin:0;background:#0f1115;color:#e6e9ef}
71
72
  div{text-align:center}p{color:#8b93a3}</style></head><body><div>
72
- <h1>${ok ? "💌 로그인 완료" : "✗ 로그인 실패"}</h1>
73
- <p>${ok ? "이 창을 닫고 터미널로 돌아가세요." : "터미널에서 다시 시도해 주세요."}</p>
73
+ <h1>${ok ? (0, i18n_1.t)("login.html.ok") : (0, i18n_1.t)("login.html.fail")}</h1>
74
+ <p>${ok ? (0, i18n_1.t)("login.html.okSub") : (0, i18n_1.t)("login.html.failSub")}</p>
74
75
  </div></body></html>`;
75
76
  /** provider가 없으면 백엔드의 로그인 방식 선택 페이지(/auth/login)를 연다. */
76
77
  function loginPath(provider) {
77
78
  return provider ? `/auth/${provider}` : "/auth/login";
78
79
  }
79
- function loginViaLoopback(apiBaseUrl, provider) {
80
+ function providerLabel(provider) {
81
+ if (provider === "google")
82
+ return "Google";
83
+ if (provider === "github")
84
+ return "GitHub";
85
+ return (0, i18n_1.t)("login.anyProvider");
86
+ }
87
+ /**
88
+ * 루프백 로그인: 일회용 로컬 서버를 띄우고 브라우저에서 OAuth를 마치면
89
+ * 서버가 http://127.0.0.1:{port}/callback?token=... 으로 토큰을 넘겨준다.
90
+ * 타임아웃/실패 시 null — 호출부가 수동 붙여넣기로 폴백한다.
91
+ * quiet(슬래시 명령)에서는 출력이 완료 후에야 한꺼번에 보이므로 진행 안내를 찍지 않는다.
92
+ */
93
+ function loginViaLoopback(apiBaseUrl, provider, quiet) {
80
94
  return new Promise((resolve) => {
81
95
  let finished = false;
82
96
  const done = (token) => {
@@ -108,10 +122,10 @@ function loginViaLoopback(apiBaseUrl, provider) {
108
122
  server.listen(0, "127.0.0.1", () => {
109
123
  const port = server.address().port;
110
124
  const url = `${apiBaseUrl}${loginPath(provider)}?cli_port=${port}`;
111
- console.log(provider
112
- ? `브라우저에서 ${provider === "google" ? "Google" : "GitHub"} 로그인을 완료해 주세요…`
113
- : "브라우저에서 GitHub 또는 Google 로그인을 완료해 주세요…");
114
- console.log(` ${url}\n`);
125
+ if (!quiet) {
126
+ console.log((0, i18n_1.t)("login.openBrowser", { provider: providerLabel(provider) }));
127
+ console.log(` ${url}\n`);
128
+ }
115
129
  openBrowser(url);
116
130
  });
117
131
  });
@@ -121,6 +135,7 @@ function loginViaLoopback(apiBaseUrl, provider) {
121
135
  * `--github` / `--google`로 방식을 고정하고, 없으면 브라우저에서 고른다.
122
136
  * `--quiet`: /pinglet-login slash command용 — 터미널 입력을 받을 수 없으므로
123
137
  * 붙여넣기 폴백 대신 안내만 출력하고, 실패해도 exit 0 (에러 덤프 방지).
138
+ * 약관 동의 안내는 브라우저의 로그인 선택 페이지가 대신 보여준다.
124
139
  */
125
140
  async function runLogin(args) {
126
141
  const config = (0, config_1.loadConfig)();
@@ -136,32 +151,34 @@ async function runLogin(args) {
136
151
  ? "github"
137
152
  : undefined;
138
153
  if (!token) {
139
- console.log(`\n로그인하면 이용약관(${config.apiBaseUrl}/terms)과 개인정보처리방침(${config.apiBaseUrl}/privacy)에 동의하는 것으로 간주됩니다.`);
140
- token = (await loginViaLoopback(config.apiBaseUrl, provider)) ?? undefined;
154
+ if (!quiet)
155
+ console.log((0, i18n_1.t)("login.terms", { api: config.apiBaseUrl }));
156
+ token =
157
+ (await loginViaLoopback(config.apiBaseUrl, provider, quiet)) ?? undefined;
141
158
  if (!token && quiet) {
142
- console.log("✗ 로그인이 완료되지 않았습니다 (제한 시간 3분). 터미널에서 `pinglet login`으로 다시 시도하세요.");
159
+ console.log((0, i18n_1.t)("login.quietFailed"));
143
160
  return;
144
161
  }
145
162
  if (!token) {
146
163
  // SSH 등 브라우저와 터미널이 다른 기계인 경우를 위한 폴백.
147
- console.log("자동 로그인에 실패했습니다. 아래 URL에서 로그인 후 응답 JSON의 token 값을 붙여넣어 주세요.");
164
+ console.log((0, i18n_1.t)("login.fallback"));
148
165
  console.log(` ${config.apiBaseUrl}${loginPath(provider)}\n`);
149
166
  token = (await prompt("token: ")).trim();
150
167
  }
151
168
  }
152
169
  if (!token) {
153
- console.log("✗ token이 비어 있습니다.");
170
+ console.log((0, i18n_1.t)("login.emptyToken"));
154
171
  if (!quiet)
155
172
  process.exitCode = 1;
156
173
  return;
157
174
  }
158
175
  config.userToken = token;
159
176
  (0, config_1.saveConfig)(config);
160
- console.log("✓ 로그인 정보 저장 완료");
177
+ console.log((0, i18n_1.t)("login.saved"));
161
178
  for (const [agentType, record] of Object.entries(config.installations)) {
162
179
  const linked = await (0, api_1.linkInstallation)(config, record.token);
163
180
  console.log(linked
164
- ? `✓ ${agentType} installation을 계정에 연결했습니다`
165
- : `○ ${agentType} installation 연결 실패 — 서버 연결 후 다시 시도하세요`);
181
+ ? (0, i18n_1.t)("login.linked", { agent: agentType })
182
+ : (0, i18n_1.t)("login.linkFailed", { agent: agentType }));
166
183
  }
167
184
  }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.runLogout = runLogout;
4
4
  const config_1 = require("../config");
5
+ const i18n_1 = require("../i18n");
5
6
  /**
6
7
  * `pinglet logout` — 로컬에 저장된 로그인 토큰만 지운다.
7
8
  * 통합 설정·installation·캐시는 그대로라 읽기는 계속 되고, 다시 `pinglet login`으로
@@ -10,10 +11,10 @@ const config_1 = require("../config");
10
11
  function runLogout() {
11
12
  const config = (0, config_1.loadConfig)();
12
13
  if (!config.userToken) {
13
- console.log("○ 로그인 상태가 아닙니다.");
14
+ console.log((0, i18n_1.t)("logout.notLoggedIn"));
14
15
  return;
15
16
  }
16
17
  delete config.userToken;
17
18
  (0, config_1.saveConfig)(config);
18
- console.log("✓ 로그아웃했습니다. 읽기는 계속 되고, 메시지 작성은 `pinglet login` 후 가능합니다.");
19
+ console.log((0, i18n_1.t)("logout.done"));
19
20
  }
@@ -4,11 +4,12 @@ exports.runPing = runPing;
4
4
  const cache_1 = require("../cache");
5
5
  const picker_1 = require("../picker");
6
6
  const render_1 = require("../render");
7
+ const i18n_1 = require("../i18n");
7
8
  /** 미리보기: 이벤트 기록 없이 메시지 하나를 골라 출력한다. */
8
9
  function runPing() {
9
10
  const message = (0, picker_1.pickMessage)((0, cache_1.loadFeedMessages)(), (0, cache_1.loadState)());
10
11
  if (!message) {
11
- console.log("표시할 Ping이 없습니다 — 서버 feed를 받으면 표시됩니다.");
12
+ console.log((0, i18n_1.t)("ping.none"));
12
13
  return;
13
14
  }
14
15
  console.log((0, render_1.formatPing)(message));
@@ -4,13 +4,14 @@ exports.runPost = runPost;
4
4
  const config_1 = require("../config");
5
5
  const api_1 = require("../api");
6
6
  const cache_1 = require("../cache");
7
- const REASON_LABELS = {
8
- EMPTY: "빈 메시지는 등록할 수 없습니다",
9
- TOO_LONG: "메시지가 너무 깁니다 (기본 100자 제한)",
10
- URL_NOT_ALLOWED: "URL은 허용되지 않습니다",
11
- PII_DETECTED: "이메일/전화번호 등 개인정보가 포함되어 있습니다",
12
- BANNED_WORD: "정책상 검토가 필요한 표현이 포함되어 있습니다",
13
- CONTROL_CHARS: "제어 문자(escape sequence)는 허용되지 않습니다",
7
+ const i18n_1 = require("../i18n");
8
+ const REASON_KEYS = {
9
+ EMPTY: "reason.EMPTY",
10
+ TOO_LONG: "reason.TOO_LONG",
11
+ URL_NOT_ALLOWED: "reason.URL_NOT_ALLOWED",
12
+ PII_DETECTED: "reason.PII_DETECTED",
13
+ BANNED_WORD: "reason.BANNED_WORD",
14
+ CONTROL_CHARS: "reason.CONTROL_CHARS",
14
15
  };
15
16
  /**
16
17
  * `pinglet post "메시지" [--category <카테고리>] [--quiet]`
@@ -33,35 +34,31 @@ async function runPost(args) {
33
34
  }
34
35
  const text = rest.join(" ").trim();
35
36
  if (!text) {
36
- console.log(quiet
37
- ? "✗ 메시지가 비어 있습니다. /pinglet 뒤에 보낼 메시지를 적어주세요."
38
- : '사용법: pinglet post "메시지" [--category <카테고리>]');
37
+ console.log(quiet ? (0, i18n_1.t)("post.emptyQuiet") : (0, i18n_1.t)("post.usage"));
39
38
  if (!quiet)
40
39
  process.exitCode = 1;
41
40
  return;
42
41
  }
43
42
  const config = (0, config_1.loadConfig)();
44
43
  const result = await (0, api_1.createMessage)(config, text, category);
44
+ // 슬래시 명령 안에서는 같은 방식의 슬래시 명령을 안내한다.
45
+ const loginCmd = quiet ? "/pinglet-login" : "pinglet login";
45
46
  if (!result.ok) {
46
47
  switch (result.error) {
47
48
  case "LOGIN_REQUIRED":
48
- console.log(quiet
49
- ? "✗ 메시지 작성에는 로그인(GitHub 또는 Google)이 필요합니다. `/pinglet-login`을 실행하세요 (읽기는 로그인 없이 가능)."
50
- : "✗ 메시지 작성에는 로그인(GitHub 또는 Google)이 필요합니다. `pinglet login`을 실행하세요 (읽기는 로그인 없이 가능).");
49
+ console.log((0, i18n_1.t)("post.loginRequired", { cmd: loginCmd }));
51
50
  break;
52
51
  case "UNAUTHORIZED":
53
- console.log(quiet
54
- ? "✗ 로그인이 만료됐습니다. `/pinglet-login`으로 다시 로그인하세요."
55
- : "✗ 로그인이 만료됐습니다. `pinglet login`으로 다시 로그인하세요.");
52
+ console.log((0, i18n_1.t)("post.expired", { cmd: loginCmd }));
56
53
  break;
57
54
  case "RATE_LIMITED":
58
- console.log("✗ 작성 제한(분당 5개)에 걸렸습니다. 잠시 후 다시 시도하세요.");
55
+ console.log((0, i18n_1.t)("post.rateLimited"));
59
56
  break;
60
57
  case "NETWORK":
61
- console.log(`✗ 서버(${config.apiBaseUrl})에 연결할 수 없습니다.`);
58
+ console.log((0, i18n_1.t)("post.network", { api: config.apiBaseUrl }));
62
59
  break;
63
60
  default:
64
- console.log("✗ 메시지 등록에 실패했습니다.");
61
+ console.log((0, i18n_1.t)("post.failed"));
65
62
  }
66
63
  // --quiet(/pinglet slash command)에서는 exit 0 유지 — exit 1이면 Claude Code가
67
64
  // 안내 문구 대신 "Shell command failed" 에러 덤프를 보여준다.
@@ -73,23 +70,28 @@ async function runPost(args) {
73
70
  if (result.id && result.status !== "REJECTED") {
74
71
  (0, cache_1.saveMyPost)({ id: result.id, text, postedAt: new Date().toISOString() });
75
72
  }
76
- const reason = result.moderationReason
77
- ? REASON_LABELS[result.moderationReason] ?? result.moderationReason
73
+ const reasonKey = result.moderationReason
74
+ ? REASON_KEYS[result.moderationReason]
78
75
  : undefined;
76
+ const reason = result.moderationReason
77
+ ? reasonKey
78
+ ? (0, i18n_1.t)(reasonKey)
79
+ : result.moderationReason
80
+ : "";
79
81
  switch (result.status) {
80
82
  case "APPROVED":
81
- console.log("✓ 메시지가 등록됐습니다. 곧 다른 개발자들의 터미널에 표시됩니다.");
83
+ console.log((0, i18n_1.t)("post.approved"));
82
84
  if (!quiet) {
83
85
  console.log(` id: ${result.id}`);
84
86
  }
85
87
  break;
86
88
  case "PENDING_REVIEW":
87
- console.log(`○ 메시지가 검토 대기 중입니다${reason ? ` — ${reason}` : ""}.`);
89
+ console.log((0, i18n_1.t)("post.pending", { reason }));
88
90
  if (!quiet)
89
- console.log(" 승인되면 feed에 노출됩니다.");
91
+ console.log((0, i18n_1.t)("post.pendingNote"));
90
92
  break;
91
93
  case "REJECTED":
92
- console.log(`✗ 메시지가 거절됐습니다${reason ? ` — ${reason}` : ""}.`);
94
+ console.log((0, i18n_1.t)("post.rejected", { reason }));
93
95
  if (!quiet)
94
96
  process.exitCode = 1;
95
97
  break;
@@ -38,6 +38,7 @@ const fs = __importStar(require("fs"));
38
38
  const config_1 = require("../config");
39
39
  const claude_1 = require("../adapters/claude");
40
40
  const codex_1 = require("../adapters/codex");
41
+ const i18n_1 = require("../i18n");
41
42
  function runUninstall(args) {
42
43
  const config = (0, config_1.loadConfig)();
43
44
  const claudeRemoved = (0, claude_1.uninstallClaudeIntegration)(config);
@@ -53,6 +54,6 @@ function runUninstall(args) {
53
54
  console.log("✓ ~/.pinglet removed (config / cache / event queue)");
54
55
  }
55
56
  else {
56
- console.log("○ ~/.pinglet은 유지됩니다. 전부 지우려면 --purge를 사용하세요.");
57
+ console.log((0, i18n_1.t)("uninstall.keep"));
57
58
  }
58
59
  }
package/dist/i18n.js ADDED
@@ -0,0 +1,293 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.t = t;
4
+ exports.currentLocale = currentLocale;
5
+ const render_1 = require("./render");
6
+ const en = {
7
+ // ---- cli
8
+ "cli.help": (p) => `pinglet v${p.version} — a developer message network you meet while the AI thinks
9
+
10
+ Usage: pinglet <command>
11
+
12
+ install [--api <url>] [--force] Install the Claude Code integration
13
+ [--claude | --codex] Codex uses OS notifications; opt in with --codex (experimental)
14
+ uninstall [--purge] Remove the integration (--purge: also delete ~/.pinglet)
15
+ login [--github | --google] Link your account (GitHub or Google; pick in the browser if omitted)
16
+ [--token <jwt>]
17
+ logout Sign out (keeps the integration and cache)
18
+ (inside Claude Code: /pinglet-login, /pinglet-logout)
19
+ post "message" [--category <c>] Post a message (login required; reading is anonymous)
20
+ doctor Diagnose install / cache / server status
21
+ ping Preview a message
22
+
23
+ Internal commands (called by the integration):
24
+ statusline Claude Code statusLine hook
25
+ notify Codex notify hook
26
+ flush Send the event queue in batches
27
+ refresh Refresh the feed cache + heartbeat
28
+ `,
29
+ "cli.firstSetup": "pinglet: not set up yet — starting first-time setup.",
30
+ // ---- login
31
+ "login.terms": (p) => `\nBy logging in you agree to the Terms (${p.api}/terms) and Privacy Policy (${p.api}/privacy).`,
32
+ "login.anyProvider": "GitHub or Google",
33
+ "login.openBrowser": (p) => `Complete the ${p.provider} login in your browser…`,
34
+ "login.quietFailed": "✗ Login was not completed (3-minute limit). Run `pinglet login` in a terminal to try again.",
35
+ "login.fallback": "Automatic login failed. Log in at the URL below and paste the token value from the JSON response.",
36
+ "login.emptyToken": "✗ token is empty.",
37
+ "login.saved": "✓ Login saved",
38
+ "login.linked": (p) => `✓ Linked the ${p.agent} installation to your account`,
39
+ "login.linkFailed": (p) => `○ Failed to link the ${p.agent} installation — retry once the server is reachable`,
40
+ "login.html.ok": "💌 Logged in",
41
+ "login.html.okSub": "You can close this window and return to the terminal.",
42
+ "login.html.fail": "✗ Login failed",
43
+ "login.html.failSub": "Please try again from the terminal.",
44
+ // ---- logout
45
+ "logout.notLoggedIn": "○ You are not logged in.",
46
+ "logout.done": "✓ Logged out. Reading still works; run `pinglet login` again to post.",
47
+ // ---- post
48
+ "reason.EMPTY": "empty messages cannot be posted",
49
+ "reason.TOO_LONG": "message is too long (100 characters by default)",
50
+ "reason.URL_NOT_ALLOWED": "URLs are not allowed",
51
+ "reason.PII_DETECTED": "contains personal data such as an email or phone number",
52
+ "reason.BANNED_WORD": "contains wording that needs policy review",
53
+ "reason.CONTROL_CHARS": "control characters (escape sequences) are not allowed",
54
+ "post.emptyQuiet": "✗ The message is empty. Write your message after /pinglet.",
55
+ "post.usage": 'Usage: pinglet post "message" [--category <category>]',
56
+ "post.loginRequired": (p) => `✗ Posting requires a login (GitHub or Google). Run \`${p.cmd}\` (reading works without login).`,
57
+ "post.expired": (p) => `✗ Your login has expired. Run \`${p.cmd}\` to log in again.`,
58
+ "post.rateLimited": "✗ Posting limit reached (5 per minute). Try again shortly.",
59
+ "post.network": (p) => `✗ Cannot reach the server (${p.api}).`,
60
+ "post.failed": "✗ Failed to post the message.",
61
+ "post.approved": "✓ Message posted. It will show up in other developers' terminals soon.",
62
+ "post.pending": (p) => `○ Message is pending review${p.reason ? ` — ${p.reason}` : ""}.`,
63
+ "post.pendingNote": " It will appear in the feed once approved.",
64
+ "post.rejected": (p) => `✗ Message rejected${p.reason ? ` — ${p.reason}` : ""}.`,
65
+ // ---- install
66
+ "install.codexOptOut": "○ Codex detected — excluded from the default install because it uses OS notifications (opt in with pinglet install --codex)",
67
+ "install.noTarget": "\nNothing to install. Please install Claude Code or Codex first.",
68
+ "install.offline": (p) => `○ Cannot reach the Pinglet server (${p.api}); installing in offline mode.`,
69
+ "install.offlineNote": " Server registration and feed sync will retry automatically once online.",
70
+ "install.askReplaceStatusLine": " An existing statusLine setting was found. Back it up and replace it? (restored on uninstall) (Y/n) ",
71
+ "install.askReplaceNotify": " An existing notify setting was found. Back it up and replace it? (restored on uninstall) (Y/n) ",
72
+ "install.or": " or ",
73
+ "install.done": (p) => `\nAll set! Run ${p.targets} as usual and Pings will appear.`,
74
+ "install.doneDoctor": " Check status: pinglet doctor",
75
+ "install.donePost": ' Post: pinglet login, then pinglet post "message"',
76
+ "install.doneLogin": " Link account: pinglet login (required to post / react)",
77
+ // ---- doctor
78
+ "doctor.nodeOld": (p) => `Node.js ${p.version} — 18 or newer is required`,
79
+ "doctor.spinnerCount": (p) => `Spinner messages: ${p.n} registered`,
80
+ "doctor.spinnerMissing": "No spinner message registered — run `pinglet refresh`",
81
+ "doctor.claudeNotInstalled": "Claude integration not installed — run `pinglet install`",
82
+ "doctor.codexNotInstalled": "Codex integration not installed — run `pinglet install`",
83
+ "doctor.noInstallation": "No installation registered on the server (offline install)",
84
+ "doctor.feedEmpty": "Feed cache is empty — showing the default spinner (Pings appear after the feed arrives)",
85
+ "doctor.feedCache": (p) => `Feed cache: ${p.n} messages (updated ${p.min} min ago)`,
86
+ "doctor.queue": (p) => `Event queue: ${p.n} pending`,
87
+ "doctor.queueBacklog": (p) => `Event queue: ${p.n} pending — uploads are falling behind`,
88
+ "doctor.apiUnreachable": (p) => `API unreachable (${p.api}) — running in offline mode`,
89
+ "doctor.loggedIn": "Logged in",
90
+ "doctor.notLoggedIn": "Not logged in — reading works; run `pinglet login` to post / react",
91
+ // ---- misc commands
92
+ "uninstall.keep": "○ ~/.pinglet is kept. Use --purge to remove everything.",
93
+ "ping.none": "No Ping to show — one will appear once the feed is received.",
94
+ "postinstall.start": "\npinglet: running first-time setup automatically (`pinglet install`)",
95
+ "postinstall.failed": "pinglet: automatic setup failed. Please run `pinglet install` yourself.",
96
+ // ---- adapters
97
+ "claude.existingStatusLine": "An existing statusLine setting was found (use --force to back it up and replace).",
98
+ "claude.settingsBusy": "settings.json keeps changing; could not install. Try again shortly.",
99
+ "codex.existingNotify": "An existing notify setting was found (use --force to back it up and replace).",
100
+ "slash.postDesc": "Post a message to Pinglet so it shows in other developers' terminals",
101
+ "slash.loginDesc": "Link your Pinglet account (GitHub or Google login in the browser; --github/--google to choose)",
102
+ "slash.logoutDesc": "Sign out of Pinglet (removes the login token on this machine only)",
103
+ };
104
+ const ko = {
105
+ "cli.help": (p) => `pinglet v${p.version} — AI가 생각하는 동안 만나는 개발자 메시지 네트워크
106
+
107
+ 사용법: pinglet <command>
108
+
109
+ install [--api <url>] [--force] Claude Code integration 설치
110
+ [--claude | --codex] Codex는 OS 알림 방식이라 --codex로 opt-in (experimental)
111
+ uninstall [--purge] integration 제거 (--purge: ~/.pinglet까지 삭제)
112
+ login [--github | --google] 계정 연결 (GitHub 또는 Google, 생략 시 브라우저에서 선택)
113
+ [--token <jwt>]
114
+ logout 로그인 해제 (설치·캐시는 유지)
115
+ (Claude Code 안에서는 /pinglet-login, /pinglet-logout)
116
+ post "메시지" [--category <c>] 메시지 작성 (로그인 필요, 읽기는 익명 가능)
117
+ doctor 설치/캐시/서버 상태 진단
118
+ ping 메시지 미리보기
119
+
120
+ 내부 명령 (integration이 호출):
121
+ statusline Claude Code statusLine hook
122
+ notify Codex notify hook
123
+ flush 이벤트 큐 batch 전송
124
+ refresh feed 캐시 갱신 + heartbeat
125
+ `,
126
+ "cli.firstSetup": "pinglet: 아직 설정 전입니다 — 첫 설정을 시작합니다.",
127
+ "login.terms": (p) => `\n로그인하면 이용약관(${p.api}/terms)과 개인정보처리방침(${p.api}/privacy)에 동의하는 것으로 간주됩니다.`,
128
+ "login.anyProvider": "GitHub 또는 Google",
129
+ "login.openBrowser": (p) => `브라우저에서 ${p.provider} 로그인을 완료해 주세요…`,
130
+ "login.quietFailed": "✗ 로그인이 완료되지 않았습니다 (제한 시간 3분). 터미널에서 `pinglet login`으로 다시 시도하세요.",
131
+ "login.fallback": "자동 로그인에 실패했습니다. 아래 URL에서 로그인 후 응답 JSON의 token 값을 붙여넣어 주세요.",
132
+ "login.emptyToken": "✗ token이 비어 있습니다.",
133
+ "login.saved": "✓ 로그인 정보 저장 완료",
134
+ "login.linked": (p) => `✓ ${p.agent} installation을 계정에 연결했습니다`,
135
+ "login.linkFailed": (p) => `○ ${p.agent} installation 연결 실패 — 서버 연결 후 다시 시도하세요`,
136
+ "login.html.ok": "💌 로그인 완료",
137
+ "login.html.okSub": "이 창을 닫고 터미널로 돌아가세요.",
138
+ "login.html.fail": "✗ 로그인 실패",
139
+ "login.html.failSub": "터미널에서 다시 시도해 주세요.",
140
+ "logout.notLoggedIn": "○ 로그인 상태가 아닙니다.",
141
+ "logout.done": "✓ 로그아웃했습니다. 읽기는 계속 되고, 메시지 작성은 `pinglet login` 후 가능합니다.",
142
+ "reason.EMPTY": "빈 메시지는 등록할 수 없습니다",
143
+ "reason.TOO_LONG": "메시지가 너무 깁니다 (기본 100자 제한)",
144
+ "reason.URL_NOT_ALLOWED": "URL은 허용되지 않습니다",
145
+ "reason.PII_DETECTED": "이메일/전화번호 등 개인정보가 포함되어 있습니다",
146
+ "reason.BANNED_WORD": "정책상 검토가 필요한 표현이 포함되어 있습니다",
147
+ "reason.CONTROL_CHARS": "제어 문자(escape sequence)는 허용되지 않습니다",
148
+ "post.emptyQuiet": "✗ 메시지가 비어 있습니다. /pinglet 뒤에 보낼 메시지를 적어주세요.",
149
+ "post.usage": '사용법: pinglet post "메시지" [--category <카테고리>]',
150
+ "post.loginRequired": (p) => `✗ 메시지 작성에는 로그인(GitHub 또는 Google)이 필요합니다. \`${p.cmd}\`을 실행하세요 (읽기는 로그인 없이 가능).`,
151
+ "post.expired": (p) => `✗ 로그인이 만료됐습니다. \`${p.cmd}\`으로 다시 로그인하세요.`,
152
+ "post.rateLimited": "✗ 작성 제한(분당 5개)에 걸렸습니다. 잠시 후 다시 시도하세요.",
153
+ "post.network": (p) => `✗ 서버(${p.api})에 연결할 수 없습니다.`,
154
+ "post.failed": "✗ 메시지 등록에 실패했습니다.",
155
+ "post.approved": "✓ 메시지가 등록됐습니다. 곧 다른 개발자들의 터미널에 표시됩니다.",
156
+ "post.pending": (p) => `○ 메시지가 검토 대기 중입니다${p.reason ? ` — ${p.reason}` : ""}.`,
157
+ "post.pendingNote": " 승인되면 feed에 노출됩니다.",
158
+ "post.rejected": (p) => `✗ 메시지가 거절됐습니다${p.reason ? ` — ${p.reason}` : ""}.`,
159
+ "install.codexOptOut": "○ Codex 감지됨 — OS 알림 방식이라 기본 설치에서 제외합니다 (원하면 pinglet install --codex)",
160
+ "install.noTarget": "\n설치할 대상이 없습니다. Claude Code 또는 Codex를 먼저 설치해 주세요.",
161
+ "install.offline": (p) => `○ Pinglet 서버(${p.api})에 연결할 수 없어 오프라인 모드로 설치합니다.`,
162
+ "install.offlineNote": " 서버 등록과 feed 수신은 온라인 상태가 되면 자동으로 재시도됩니다.",
163
+ "install.askReplaceStatusLine": " 기존 statusLine 설정이 있습니다. 백업 후 교체할까요? (uninstall 시 복원) (Y/n) ",
164
+ "install.askReplaceNotify": " 기존 notify 설정이 있습니다. 백업 후 교체할까요? (uninstall 시 복원) (Y/n) ",
165
+ "install.or": " 또는 ",
166
+ "install.done": (p) => `\n설치 완료! 평소처럼 ${p.targets}를 실행하면 Ping이 표시됩니다.`,
167
+ "install.doneDoctor": " 상태 확인: pinglet doctor",
168
+ "install.donePost": ' 메시지 작성: pinglet login 후 pinglet post "메시지"',
169
+ "install.doneLogin": " 계정 연결: pinglet login (메시지 작성/반응에 필요)",
170
+ "doctor.nodeOld": (p) => `Node.js ${p.version} — 18 이상이 필요합니다`,
171
+ "doctor.spinnerCount": (p) => `Spinner 메시지: ${p.n}개 등록됨`,
172
+ "doctor.spinnerMissing": "Spinner 메시지 미등록 — `pinglet refresh` 실행",
173
+ "doctor.claudeNotInstalled": "Claude integration not installed — `pinglet install` 실행",
174
+ "doctor.codexNotInstalled": "Codex integration not installed — `pinglet install` 실행",
175
+ "doctor.noInstallation": "서버에 등록된 installation이 없습니다 (오프라인 설치 상태)",
176
+ "doctor.feedEmpty": "Feed cache 비어 있음 — 기본 spinner로 표시 중 (서버 feed 수신 후 Ping 표시)",
177
+ "doctor.feedCache": (p) => `Feed cache: ${p.n}개 (${p.min}분 전 갱신)`,
178
+ "doctor.queue": (p) => `Event queue: ${p.n}개 대기`,
179
+ "doctor.queueBacklog": (p) => `Event queue: ${p.n}개 대기 — 서버 전송이 밀려 있습니다`,
180
+ "doctor.apiUnreachable": (p) => `API unreachable (${p.api}) — 오프라인 모드로 동작 중`,
181
+ "doctor.loggedIn": "로그인됨",
182
+ "doctor.notLoggedIn": "로그인 안 됨 — 읽기는 가능, 메시지 작성/반응은 `pinglet login` 후",
183
+ "uninstall.keep": "○ ~/.pinglet은 유지됩니다. 전부 지우려면 --purge를 사용하세요.",
184
+ "ping.none": "표시할 Ping이 없습니다 — 서버 feed를 받으면 표시됩니다.",
185
+ "postinstall.start": "\npinglet: 첫 설정을 자동으로 진행합니다 (`pinglet install`)",
186
+ "postinstall.failed": "pinglet: 자동 설정에 실패했습니다. `pinglet install`을 직접 실행해 주세요.",
187
+ "claude.existingStatusLine": "기존 statusLine 설정이 있습니다 (백업 후 교체하려면 --force).",
188
+ "claude.settingsBusy": "settings.json이 계속 변경되고 있어 설치하지 못했습니다. 잠시 후 다시 시도하세요.",
189
+ "codex.existingNotify": "기존 notify 설정이 있습니다 (백업 후 교체하려면 --force).",
190
+ "slash.postDesc": "메시지를 Pinglet에 등록해 다른 개발자들의 터미널에 표시",
191
+ "slash.loginDesc": "Pinglet 계정 연결 (브라우저에서 GitHub 또는 Google 로그인, --github/--google로 지정 가능)",
192
+ "slash.logoutDesc": "Pinglet 로그인 해제 (이 기기의 로그인 토큰만 제거)",
193
+ };
194
+ const ja = {
195
+ "cli.help": (p) => `pinglet v${p.version} — AIが考えている間に出会う開発者メッセージネットワーク
196
+
197
+ 使い方: pinglet <command>
198
+
199
+ install [--api <url>] [--force] Claude Code integration をインストール
200
+ [--claude | --codex] Codex は OS 通知方式のため --codex で opt-in (experimental)
201
+ uninstall [--purge] integration を削除 (--purge: ~/.pinglet も削除)
202
+ login [--github | --google] アカウント連携 (GitHub または Google、省略時はブラウザで選択)
203
+ [--token <jwt>]
204
+ logout ログアウト (連携設定・キャッシュは維持)
205
+ (Claude Code 内では /pinglet-login, /pinglet-logout)
206
+ post "メッセージ" [--category <c>] メッセージ投稿 (ログイン必要、閲覧は匿名可)
207
+ doctor インストール/キャッシュ/サーバー状態を診断
208
+ ping メッセージのプレビュー
209
+
210
+ 内部コマンド (integration が呼び出す):
211
+ statusline Claude Code statusLine hook
212
+ notify Codex notify hook
213
+ flush イベントキューをバッチ送信
214
+ refresh feed キャッシュ更新 + heartbeat
215
+ `,
216
+ "cli.firstSetup": "pinglet: まだ設定されていません — 初期設定を開始します。",
217
+ "login.terms": (p) => `\nログインすると利用規約(${p.api}/terms)とプライバシーポリシー(${p.api}/privacy)に同意したものとみなされます。`,
218
+ "login.anyProvider": "GitHub または Google",
219
+ "login.openBrowser": (p) => `ブラウザで ${p.provider} ログインを完了してください…`,
220
+ "login.quietFailed": "✗ ログインが完了しませんでした (制限時間3分)。ターミナルで `pinglet login` を実行して再試行してください。",
221
+ "login.fallback": "自動ログインに失敗しました。以下の URL でログイン後、レスポンス JSON の token の値を貼り付けてください。",
222
+ "login.emptyToken": "✗ token が空です。",
223
+ "login.saved": "✓ ログイン情報を保存しました",
224
+ "login.linked": (p) => `✓ ${p.agent} installation をアカウントに連携しました`,
225
+ "login.linkFailed": (p) => `○ ${p.agent} installation の連携に失敗 — サーバー接続後に再試行してください`,
226
+ "login.html.ok": "💌 ログイン完了",
227
+ "login.html.okSub": "このウィンドウを閉じてターミナルに戻ってください。",
228
+ "login.html.fail": "✗ ログイン失敗",
229
+ "login.html.failSub": "ターミナルからもう一度お試しください。",
230
+ "logout.notLoggedIn": "○ ログインしていません。",
231
+ "logout.done": "✓ ログアウトしました。閲覧は引き続き可能で、投稿は `pinglet login` 後にできます。",
232
+ "reason.EMPTY": "空のメッセージは投稿できません",
233
+ "reason.TOO_LONG": "メッセージが長すぎます (デフォルト100文字まで)",
234
+ "reason.URL_NOT_ALLOWED": "URL は許可されていません",
235
+ "reason.PII_DETECTED": "メールアドレス/電話番号などの個人情報が含まれています",
236
+ "reason.BANNED_WORD": "ポリシー上の確認が必要な表現が含まれています",
237
+ "reason.CONTROL_CHARS": "制御文字 (escape sequence) は許可されていません",
238
+ "post.emptyQuiet": "✗ メッセージが空です。/pinglet の後に送るメッセージを書いてください。",
239
+ "post.usage": '使い方: pinglet post "メッセージ" [--category <カテゴリ>]',
240
+ "post.loginRequired": (p) => `✗ 投稿にはログイン (GitHub または Google) が必要です。\`${p.cmd}\` を実行してください (閲覧はログイン不要)。`,
241
+ "post.expired": (p) => `✗ ログインの有効期限が切れました。\`${p.cmd}\` で再度ログインしてください。`,
242
+ "post.rateLimited": "✗ 投稿制限 (1分あたり5件) に達しました。しばらくしてから再試行してください。",
243
+ "post.network": (p) => `✗ サーバー (${p.api}) に接続できません。`,
244
+ "post.failed": "✗ メッセージの投稿に失敗しました。",
245
+ "post.approved": "✓ メッセージを投稿しました。まもなく他の開発者のターミナルに表示されます。",
246
+ "post.pending": (p) => `○ メッセージは審査待ちです${p.reason ? ` — ${p.reason}` : ""}。`,
247
+ "post.pendingNote": " 承認されると feed に表示されます。",
248
+ "post.rejected": (p) => `✗ メッセージは拒否されました${p.reason ? ` — ${p.reason}` : ""}。`,
249
+ "install.codexOptOut": "○ Codex を検出 — OS 通知方式のためデフォルトのインストールからは除外します (必要なら pinglet install --codex)",
250
+ "install.noTarget": "\nインストール対象がありません。先に Claude Code または Codex をインストールしてください。",
251
+ "install.offline": (p) => `○ Pinglet サーバー (${p.api}) に接続できないため、オフラインモードでインストールします。`,
252
+ "install.offlineNote": " サーバー登録と feed の受信はオンラインになると自動的に再試行されます。",
253
+ "install.askReplaceStatusLine": " 既存の statusLine 設定があります。バックアップして置き換えますか? (uninstall 時に復元) (Y/n) ",
254
+ "install.askReplaceNotify": " 既存の notify 設定があります。バックアップして置き換えますか? (uninstall 時に復元) (Y/n) ",
255
+ "install.or": " または ",
256
+ "install.done": (p) => `\nインストール完了! いつも通り ${p.targets} を実行すると Ping が表示されます。`,
257
+ "install.doneDoctor": " 状態確認: pinglet doctor",
258
+ "install.donePost": ' メッセージ投稿: pinglet login の後に pinglet post "メッセージ"',
259
+ "install.doneLogin": " アカウント連携: pinglet login (投稿/リアクションに必要)",
260
+ "doctor.nodeOld": (p) => `Node.js ${p.version} — 18 以上が必要です`,
261
+ "doctor.spinnerCount": (p) => `Spinner メッセージ: ${p.n}件登録済み`,
262
+ "doctor.spinnerMissing": "Spinner メッセージ未登録 — `pinglet refresh` を実行",
263
+ "doctor.claudeNotInstalled": "Claude integration not installed — `pinglet install` を実行",
264
+ "doctor.codexNotInstalled": "Codex integration not installed — `pinglet install` を実行",
265
+ "doctor.noInstallation": "サーバーに登録された installation がありません (オフラインインストール状態)",
266
+ "doctor.feedEmpty": "Feed cache が空です — デフォルトの spinner を表示中 (サーバーから feed を受信すると Ping を表示)",
267
+ "doctor.feedCache": (p) => `Feed cache: ${p.n}件 (${p.min}分前に更新)`,
268
+ "doctor.queue": (p) => `Event queue: ${p.n}件待機中`,
269
+ "doctor.queueBacklog": (p) => `Event queue: ${p.n}件待機中 — サーバーへの送信が滞っています`,
270
+ "doctor.apiUnreachable": (p) => `API unreachable (${p.api}) — オフラインモードで動作中`,
271
+ "doctor.loggedIn": "ログイン済み",
272
+ "doctor.notLoggedIn": "未ログイン — 閲覧は可能、投稿/リアクションは `pinglet login` 後",
273
+ "uninstall.keep": "○ ~/.pinglet は維持されます。すべて削除するには --purge を使ってください。",
274
+ "ping.none": "表示する Ping がありません — サーバーから feed を受信すると表示されます。",
275
+ "postinstall.start": "\npinglet: 初期設定を自動的に行います (`pinglet install`)",
276
+ "postinstall.failed": "pinglet: 自動設定に失敗しました。`pinglet install` を手動で実行してください。",
277
+ "claude.existingStatusLine": "既存の statusLine 設定があります (バックアップして置き換えるには --force)。",
278
+ "claude.settingsBusy": "settings.json が変更され続けているためインストールできませんでした。しばらくしてから再試行してください。",
279
+ "codex.existingNotify": "既存の notify 設定があります (バックアップして置き換えるには --force)。",
280
+ "slash.postDesc": "メッセージを Pinglet に投稿して他の開発者のターミナルに表示",
281
+ "slash.loginDesc": "Pinglet アカウント連携 (ブラウザで GitHub または Google ログイン、--github/--google で指定可)",
282
+ "slash.logoutDesc": "Pinglet ログアウト (このマシンのログイントークンのみ削除)",
283
+ };
284
+ const tables = { en, ko, ja };
285
+ function t(key, params = {}) {
286
+ const table = tables[(0, render_1.detectLocale)()] ?? en;
287
+ const msg = table[key] ?? en[key];
288
+ return typeof msg === "function" ? msg(params) : msg;
289
+ }
290
+ /** HTML lang 속성 등에 쓰는 현재 표시 언어. */
291
+ function currentLocale() {
292
+ return (0, render_1.detectLocale)();
293
+ }
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const install_1 = require("./commands/install");
4
+ const i18n_1 = require("./i18n");
4
5
  /**
5
6
  * npm install -g pinglet-cli 직후 자동으로 `pinglet install`을 수행한다.
6
7
  * - 글로벌 설치(npm_config_global=true)일 때만 동작 — 개발용 npm install,
@@ -12,11 +13,11 @@ const install_1 = require("./commands/install");
12
13
  async function main() {
13
14
  if (process.env.npm_config_global !== "true")
14
15
  return;
15
- console.log("\npinglet: 첫 설정을 자동으로 진행합니다 (`pinglet install`)");
16
+ console.log((0, i18n_1.t)("postinstall.start"));
16
17
  await (0, install_1.runInstall)([]);
17
18
  }
18
19
  main()
19
20
  .catch(() => {
20
- console.log("pinglet: 자동 설정에 실패했습니다. `pinglet install`을 직접 실행해 주세요.");
21
+ console.log((0, i18n_1.t)("postinstall.failed"));
21
22
  })
22
23
  .finally(() => process.exit(0));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinglet-cli",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
4
4
  "description": "Short messages from other developers shown in your Claude Code spinner while the AI thinks — zero tokens, never reads your code. Claude Code / Codex integration",
5
5
  "license": "MIT",
6
6
  "repository": {