rootr-cli 0.17.1 → 0.17.2

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/lib/client.js CHANGED
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import { currentRateLimitSink } from './context.js';
10
+ import { t as tr } from './i18n.js';
10
11
 
11
12
  /** Copy the API's X-RateLimit-* headers into the ambient per-call sink, if any. */
12
13
  function captureRateLimit(res) {
@@ -40,10 +41,17 @@ function hintFor(status, body) {
40
41
  const detail = body && (body.message || body.error || '');
41
42
  if (status === 403 && typeof detail === 'string' && /does not belong to this workspace/i.test(detail)) {
42
43
  return (
43
- '이 키는 다른 워크스페이스 전용 키입니다. 목록에 보인다고 접근되는 것은 아닙니다.\n' +
44
- '조치: rootr.io > 설정 > API 키에서 계정 키(PAT)를 발급해 `rootr config --api-key <키>` 로 등록하거나, ' +
45
- '그 워크스페이스용 키를 `rootr config --add-key <키> --workspace <워크스페이스 id>` 로 추가하세요. ' +
46
- '(rootr_workspaces 로 어떤 워크스페이스가 실제로 열리는지 확인할 수 있습니다.)'
44
+ tr(
45
+ '이 키는 다른 워크스페이스 전용 키입니다. 목록에 보인다고 접근되는 것은 아닙니다.\n' +
46
+ '조치: rootr.io > 설정 > API 키에서 계정 키(PAT)를 발급해 `rootr config --api-key <키>` 로 등록하거나, ' +
47
+ '그 워크스페이스용 키를 `rootr config --add-key <키> --workspace <워크스페이스 id>` 로 추가하세요. ' +
48
+ '(rootr_workspaces 로 어떤 워크스페이스가 실제로 열리는지 확인할 수 있습니다.)',
49
+ 'This key belongs to a different workspace. Being listed is not the same as being reachable.\n' +
50
+ 'Fix: create an account key (PAT) under rootr.io > Settings > API keys and register it with ' +
51
+ '`rootr config --api-key <key>`, or add a key for that workspace with ' +
52
+ '`rootr config --add-key <key> --workspace <workspace id>`. ' +
53
+ '(rootr_workspaces shows which workspaces actually open.)',
54
+ )
47
55
  );
48
56
  }
49
57
  // HINT-WRONG-CAUSE — 서버가 이미 원인을 정확히 말했으면 **덮어쓰지 않는다**.
@@ -54,9 +62,14 @@ function hintFor(status, body) {
54
62
  const msg = String((body && (body.message || body.error)) || '');
55
63
  if (/workspace/i.test(msg) && /(at most|limit|한도|최대)/i.test(msg)) {
56
64
  return (
57
- '한도 문제이지 키 문제가 아닙니다. 안 쓰는 워크스페이스를 rootr.io > 설정 > 워크스페이스에서 ' +
58
- '지우거나, 기존 워크스페이스 **안에** 폴더로 지으세요(rootr_scaffold_apply 에 rootPath 를 주면 됩니다). ' +
59
- '요금제를 올려도 됩니다.'
65
+ tr(
66
+ '한도 문제이지 키 문제가 아닙니다. 안 쓰는 워크스페이스를 rootr.io > 설정 > 워크스페이스에서 ' +
67
+ '지우거나, 기존 워크스페이스 **안에** 폴더로 지으세요(rootr_scaffold_apply 에 rootPath 를 주면 됩니다). ' +
68
+ '요금제를 올려도 됩니다.',
69
+ 'This is a quota problem, not a key problem. Delete unused workspaces under ' +
70
+ 'rootr.io > Settings > Workspaces, or build **inside** an existing workspace as a folder ' +
71
+ '(pass rootPath to rootr_scaffold_apply). Upgrading the plan also works.',
72
+ )
60
73
  );
61
74
  }
62
75
  // 서버가 무슨 일인지 이미 구체적으로 말했으면 그대로 둔다.
@@ -64,16 +77,21 @@ function hintFor(status, body) {
64
77
  return undefined;
65
78
  }
66
79
  return (
67
- 'API 키 또는 워크스페이스 스코프를 확인하세요 (rootr config로 현재 설정 확인). ' +
68
- '워크스페이스 생성(rootr_create_workspace)은 계정 키(PAT, workspaces:create 스코프)가 필요합니다 — ' +
69
- '워크스페이스 키(docs:read/docs:write/graph:read/ask/webhooks:manage 스코프)로는 할 수 없습니다.'
80
+ tr(
81
+ 'API 키 또는 워크스페이스 스코프를 확인하세요 (rootr config로 현재 설정 확인). ' +
82
+ '워크스페이스 생성(rootr_create_workspace)은 계정 키(PAT, workspaces:create 스코프)가 필요합니다 — ' +
83
+ '워크스페이스 키(docs:read/docs:write/graph:read/ask/webhooks:manage 스코프)로는 할 수 없습니다.',
84
+ 'Check the API key and the workspace scopes (`rootr config` shows the current setup). ' +
85
+ 'Creating a workspace (rootr_create_workspace) needs an account key (PAT with workspaces:create) — ' +
86
+ 'a workspace key (docs:read/docs:write/graph:read/ask/webhooks:manage) cannot do it.',
87
+ )
70
88
  );
71
89
  }
72
90
  if (status === 412) {
73
- return '문서가 그 사이 변경되었습니다: 다시 읽고 재시도하세요.';
91
+ return tr('문서가 그 사이 변경되었습니다: 다시 읽고 재시도하세요.', 'The document changed in the meantime: read it again and retry.');
74
92
  }
75
93
  if (status === 409) {
76
- return body && (body.message || body.error) ? undefined : '충돌이 발생했습니다.';
94
+ return body && (body.message || body.error) ? undefined : tr('충돌이 발생했습니다.', 'Conflict.');
77
95
  }
78
96
  return undefined;
79
97
  }
@@ -90,16 +108,22 @@ export class RootrClient {
90
108
 
91
109
  _requireWorkspace() {
92
110
  if (!this.workspace) {
93
- throw new RootrApiError('워크스페이스가 설정되지 않았습니다.', {
94
- hint: 'rootr config --workspace <id> 로 설정하거나 ROOTR_WORKSPACE 환경변수를 지정하세요.',
111
+ throw new RootrApiError(tr('워크스페이스가 설정되지 않았습니다.', 'No workspace is configured.'), {
112
+ hint: tr(
113
+ 'rootr config --workspace <id> 로 설정하거나 ROOTR_WORKSPACE 환경변수를 지정하세요.',
114
+ 'Set one with `rootr config --workspace <id>`, or the ROOTR_WORKSPACE environment variable.',
115
+ ),
95
116
  });
96
117
  }
97
118
  }
98
119
 
99
120
  _requireApiKey() {
100
121
  if (!this.apiKey) {
101
- throw new RootrApiError('API 키가 설정되지 않았습니다.', {
102
- hint: 'rootr config --api-key <key> 로 설정하거나 ROOTR_API_KEY 환경변수를 지정하세요.',
122
+ throw new RootrApiError(tr('API 키가 설정되지 않았습니다.', 'No API key is configured.'), {
123
+ hint: tr(
124
+ 'rootr config --api-key <key> 로 설정하거나 ROOTR_API_KEY 환경변수를 지정하세요.',
125
+ 'Set one with `rootr config --api-key <key>`, or the ROOTR_API_KEY environment variable.',
126
+ ),
103
127
  });
104
128
  }
105
129
  }
@@ -138,7 +162,7 @@ export class RootrClient {
138
162
  body,
139
163
  });
140
164
  } catch (err) {
141
- throw new RootrApiError(`네트워크 오류: ${err.message}`, { hint: `요청 URL: ${url}` });
165
+ throw new RootrApiError(tr(`네트워크 오류: ${err.message}`, `Network error: ${err.message}`), { hint: tr(`요청 URL: ${url}`, `Request URL: ${url}`) });
142
166
  }
143
167
 
144
168
  captureRateLimit(res);
@@ -665,7 +689,7 @@ export class RootrClient {
665
689
  });
666
690
  if (data && Array.isArray(data.errors) && data.errors.length) {
667
691
  const msg = data.errors.map((e) => e.message || String(e)).join('; ');
668
- throw new RootrApiError(`ask 오류: ${msg}`, { body: data.errors });
692
+ throw new RootrApiError(tr(`ask 오류: ${msg}`, `ask failed: ${msg}`), { body: data.errors });
669
693
  }
670
694
  return (data && data.data && data.data.ask) || { text: '', citations: [] };
671
695
  }
@@ -1265,7 +1289,7 @@ export class RootrClient {
1265
1289
  const url = this.attachmentRawUrl(attachmentId);
1266
1290
  const res = await fetch(url);
1267
1291
  if (!res.ok) {
1268
- throw new RootrApiError(`오디오 다운로드 실패 (HTTP ${res.status})`, { status: res.status });
1292
+ throw new RootrApiError(tr(`오디오 다운로드 실패 (HTTP ${res.status})`, `Audio download failed (HTTP ${res.status})`), { status: res.status });
1269
1293
  }
1270
1294
  return Buffer.from(await res.arrayBuffer());
1271
1295
  }
@@ -1724,7 +1748,7 @@ export class RootrClient {
1724
1748
  body: JSON.stringify({ body, title, kind, contactEmail, reporter, context, workspaceId }),
1725
1749
  });
1726
1750
  } catch (err) {
1727
- throw new RootrApiError(`네트워크 오류: ${err.message}`, { hint: `요청 URL: ${url}` });
1751
+ throw new RootrApiError(tr(`네트워크 오류: ${err.message}`, `Network error: ${err.message}`), { hint: tr(`요청 URL: ${url}`, `Request URL: ${url}`) });
1728
1752
  }
1729
1753
  const text = await res.text();
1730
1754
  let data;
@@ -1734,7 +1758,7 @@ export class RootrClient {
1734
1758
  data = { message: text };
1735
1759
  }
1736
1760
  if (!res.ok) {
1737
- throw new RootrApiError(data.message || `제보를 보내지 못했습니다 (HTTP ${res.status})`, {
1761
+ throw new RootrApiError(data.message || tr(`제보를 보내지 못했습니다 (HTTP ${res.status})`, `Could not send feedback (HTTP ${res.status})`), {
1738
1762
  status: res.status,
1739
1763
  });
1740
1764
  }
@@ -1,6 +1,7 @@
1
1
  import { loadConfig, saveConfig, upsertKey } from './config.js';
2
2
  import { RootrClient } from './client.js';
3
3
  import { parseRootrUrl } from './resolve.js';
4
+ import { t as tr } from './i18n.js';
4
5
 
5
6
  /**
6
7
  * Every credential this machine can use, default key first. An account key
@@ -90,9 +91,13 @@ export async function resolveWorkspaceSwitch(wantedRaw, cfg = loadConfig()) {
90
91
  const candidates = candidateClients(cfg);
91
92
  if (!candidates.length) {
92
93
  return {
93
- error:
94
+ error: tr(
94
95
  '이 컴퓨터에 등록된 Rootr 키가 없습니다.\n' +
95
- '조치: rootr.io > 설정 > API 키에서 계정 키(PAT)를 발급받아 `rootr config --api-key <키>` 로 등록하세요.',
96
+ '조치: rootr.io > 설정 > API 키에서 계정 키(PAT)를 발급받아 `rootr config --api-key <키>` 로 등록하세요.',
97
+ 'No Rootr key is registered on this machine.\n' +
98
+ 'Fix: create an account key (PAT) under rootr.io > Settings > API keys, then register it with ' +
99
+ '`rootr config --api-key <key>`.',
100
+ ),
96
101
  };
97
102
  }
98
103
 
@@ -151,28 +156,48 @@ export async function resolveWorkspaceSwitch(wantedRaw, cfg = loadConfig()) {
151
156
  if (target) {
152
157
  const scoped = lastRejection && lastRejection.status === 403;
153
158
  return {
154
- error:
159
+ error: tr(
155
160
  `"${target.name || target.id}" (${target.id}) 는 접근 권한 목록에는 있지만, 등록된 키로는 열 수 없습니다` +
156
- `${target.role ? ` (내 역할: ${target.role})` : ''}.\n` +
157
- (scoped
158
- ? '원인: 지금 등록된 키가 다른 워크스페이스 전용 키입니다 (워크스페이스 키는 자기 워크스페이스 밖에서 403).\n'
159
- : `원인: ${lastRejection ? lastRejection.message : '알 수 없음'}\n`) +
160
- '조치 (둘 중 하나, 사용자가 직접):\n' +
161
- '1) rootr.io > 설정 > API 키에서 계정 키(PAT)를 발급 → `rootr config --api-key <키>` (모든 워크스페이스에 통함)\n' +
162
- `2) 그 워크스페이스용 키를 발급 → \`rootr config --add-key <키> --workspace ${target.id}\``,
161
+ `${target.role ? ` (내 역할: ${target.role})` : ''}.\n` +
162
+ (scoped
163
+ ? '원인: 지금 등록된 키가 다른 워크스페이스 전용 키입니다 (워크스페이스 키는 자기 워크스페이스 밖에서 403).\n'
164
+ : `원인: ${lastRejection ? lastRejection.message : '알 수 없음'}\n`) +
165
+ '조치 (둘 중 하나, 사용자가 직접):\n' +
166
+ '1) rootr.io > 설정 > API 키에서 계정 키(PAT)를 발급 → `rootr config --api-key <키>` (모든 워크스페이스에 통함)\n' +
167
+ `2) 그 워크스페이스용 키를 발급 → \`rootr config --add-key <키> --workspace ${target.id}\``,
168
+ `"${target.name || target.id}" (${target.id}) is on your access list, but none of the registered keys opens it` +
169
+ `${target.role ? ` (your role: ${target.role})` : ''}.\n` +
170
+ (scoped
171
+ ? 'Cause: the registered key belongs to a different workspace (a workspace key gets 403 outside its own).\n'
172
+ : `Cause: ${lastRejection ? lastRejection.message : 'unknown'}\n`) +
173
+ 'Fix (either one, done by you):\n' +
174
+ '1) Create an account key (PAT) under rootr.io > Settings > API keys → `rootr config --api-key <key>` (works everywhere)\n' +
175
+ `2) Create a key for that workspace → \`rootr config --add-key <key> --workspace ${target.id}\``,
176
+ ),
163
177
  };
164
178
  }
179
+ const visibleList = visible.map((w) => `${w.name || w.slug || ''}(${w.id})`).join(', ');
165
180
  const seenList = visible.length
166
- ? `\n지금 보이는 워크스페이스: ${visible.map((w) => `${w.name || w.slug || ''}(${w.id})`).join(', ')}`
181
+ ? tr(`\n지금 보이는 워크스페이스: ${visibleList}`, `\nWorkspaces visible to you: ${visibleList}`)
167
182
  : '';
168
183
  return {
169
184
  error:
170
185
  listed === 0
171
- ? '등록된 키로 워크스페이스 목록을 조회하지 못했습니다 (키가 만료/취소되었거나 목록 권한이 없음).\n' +
172
- '조치: rootr.io > 설정 > API 키에서 계정 키(PAT)를 새로 발급해 `rootr config --api-key <키>` 로 등록하세요.'
173
- : `"${wanted}" 워크스페이스가 내 소속 목록에 없습니다.${seenList}\n` +
174
- '조치: 주소가 맞는지 확인하고, 다른 계정의 워크스페이스라면 그 계정으로 발급한 키를 ' +
175
- '`rootr config --add-key <키>` 로 등록하세요.',
186
+ ? tr(
187
+ '등록된 키로 워크스페이스 목록을 조회하지 못했습니다 (키가 만료/취소되었거나 목록 권한이 없음).\n' +
188
+ '조치: rootr.io > 설정 > API 키에서 계정 키(PAT)를 새로 발급해 `rootr config --api-key <키>` 로 등록하세요.',
189
+ 'Could not list workspaces with the registered keys (expired/revoked, or no listing scope).\n' +
190
+ 'Fix: create a fresh account key (PAT) under rootr.io > Settings > API keys and register it with ' +
191
+ '`rootr config --api-key <key>`.',
192
+ )
193
+ : tr(
194
+ `"${wanted}" 워크스페이스가 내 소속 목록에 없습니다.${seenList}\n` +
195
+ '조치: 주소가 맞는지 확인하고, 다른 계정의 워크스페이스라면 그 계정으로 발급한 키를 ' +
196
+ '`rootr config --add-key <키>` 로 등록하세요.',
197
+ `"${wanted}" is not in your workspace list.${seenList}\n` +
198
+ 'Fix: check the address, and if it belongs to another account, register a key issued by that ' +
199
+ 'account with `rootr config --add-key <key>`.',
200
+ ),
176
201
  };
177
202
  }
178
203
 
package/lib/i18n.js ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * CLI 출력 언어.
3
+ *
4
+ * 왜: npm 페이지를 영문으로 바꾸고 나니, 그걸 읽고 설치한 사람이 `rootr --help`에서
5
+ * 한글 화면을 만난다. 도구 설명(에이전트가 읽는 스키마)은 원래 영어인데 사람이 보는
6
+ * 쪽만 한국어였다.
7
+ *
8
+ * 규칙 (위가 우선):
9
+ * 1. ROOTR_LANG=ko|en — 명시 지정
10
+ * 2. LC_ALL / LC_MESSAGES / LANG 이 ko 로 시작하면 한국어
11
+ * 3. 그 외에는 영어
12
+ *
13
+ * 한국어 사용자는 대개 로케일이 ko_KR 이라 그대로 한국어를 보고, 로케일이 C/en 인
14
+ * 서버에서 한국어를 원하면 ROOTR_LANG=ko 를 주면 된다.
15
+ */
16
+
17
+ function detect() {
18
+ const explicit = (process.env.ROOTR_LANG || '').trim().toLowerCase();
19
+ if (explicit.startsWith('ko')) return 'ko';
20
+ if (explicit.startsWith('en')) return 'en';
21
+ const locale = process.env.LC_ALL || process.env.LC_MESSAGES || process.env.LANG || '';
22
+ return locale.toLowerCase().startsWith('ko') ? 'ko' : 'en';
23
+ }
24
+
25
+ let cached = null;
26
+
27
+ export function lang() {
28
+ if (cached === null) cached = detect();
29
+ return cached;
30
+ }
31
+
32
+ /** 테스트에서 언어를 고정할 때만 쓴다. */
33
+ export function setLang(value) {
34
+ cached = value === null ? null : value;
35
+ }
36
+
37
+ /** t('한국어', 'English') — 현재 언어에 맞는 쪽을 돌려준다. */
38
+ export function t(ko, en) {
39
+ return lang() === 'ko' ? ko : en;
40
+ }
@@ -2,6 +2,25 @@ import { z } from 'zod';
2
2
 
3
3
  import { jsonResult, textResult, errorResult, makeClient } from './shared.js';
4
4
 
5
+ /**
6
+ * 저장되는 값은 한국어 네 가지로 고정 — 화면·CLI·이미 쌓인 행이 전부 그 값을 쓴다.
7
+ * 도구 설명은 전부 영어인데 이 enum만 한국어라 영어권 에이전트가 걸려 넘어졌다.
8
+ * 입력만 영어로 받아 한국어로 바꿔 넘긴다(저장·출력은 그대로).
9
+ */
10
+ const MEMORY_KIND_ALIASES = {
11
+ instruction: '지시',
12
+ person: '사람',
13
+ people: '사람',
14
+ project: '프로젝트',
15
+ reference: '참고',
16
+ ref: '참고',
17
+ };
18
+
19
+ const MEMORY_KIND = z.preprocess(
20
+ (v) => (typeof v === 'string' ? (MEMORY_KIND_ALIASES[v.trim().toLowerCase()] ?? v) : v),
21
+ z.enum(['지시', '사람', '프로젝트', '참고']),
22
+ );
23
+
5
24
  /**
6
25
  * MEM-001 — 에이전트 메모리.
7
26
  *
@@ -52,12 +71,13 @@ export function registerMemoryTools(server) {
52
71
  'what you do next time. Required for a new memory; may be omitted with `memoryId` to edit ' +
53
72
  'only kind/source/related.',
54
73
  ),
55
- kind: z
56
- .enum(['지시', '사람', '프로젝트', '참고'])
74
+ kind: MEMORY_KIND
57
75
  .optional()
58
76
  .describe(
59
- 'What kind of fact: 지시 = how the team wants work done, 사람 = someone\'s role/preference, ' +
60
- '프로젝트 = ongoing work or constraints, 참고 = a pointer to an external resource',
77
+ 'What kind of fact. Korean values are canonical and what gets stored; the English aliases in ' +
78
+ 'parentheses are accepted and normalized. 지시 (instruction) = how the team wants work done, ' +
79
+ '사람 (person) = someone\'s role/preference, 프로젝트 (project) = ongoing work or constraints, ' +
80
+ '참고 (reference) = a pointer to an external resource',
61
81
  ),
62
82
  source: z
63
83
  .string()
@@ -122,7 +142,7 @@ export function registerMemoryTools(server) {
122
142
  .string()
123
143
  .optional()
124
144
  .describe('Optional keyword filter. Omit to get everything — the list is meant to be read whole.'),
125
- kind: z.enum(['지시', '사람', '프로젝트', '참고']).optional().describe('Only this kind'),
145
+ kind: MEMORY_KIND.optional().describe('Only this kind (English aliases accepted)'),
126
146
  limit: z.number().optional().describe('Max memories to return'),
127
147
  workspace: z
128
148
  .string()
package/lib/mcp.js CHANGED
@@ -24,6 +24,7 @@ import { registerFeedbackTools } from './mcp-tools/feedback.js';
24
24
  import { registerMemoryTools } from './mcp-tools/memory.js';
25
25
  import { withRateLimitSink } from './context.js';
26
26
  import { makeClient } from './mcp-tools/shared.js';
27
+ import { t as tr } from './i18n.js';
27
28
 
28
29
  /** Warn once the workspace has burned this share of its quota. */
29
30
  const QUOTA_WARN_AT = 0.8;
@@ -32,12 +33,18 @@ function quotaNotice({ limit, remaining, window, upgradeUrl }) {
32
33
  if (!Number.isFinite(limit) || !Number.isFinite(remaining) || limit <= 0) return null;
33
34
  if (remaining <= 0) return null; // already blocked — the 429 says it better
34
35
  if (remaining > Math.ceil(limit * (1 - QUOTA_WARN_AT))) return null;
35
- const per = window === 'day' ? '오늘' : '이번 시간';
36
- const refill = window === 'day' ? '내일 자정(KST)' : '다음 정시';
37
- return (
36
+ const per = tr(window === 'day' ? '오늘' : '이번 시간', window === 'day' ? 'today' : 'this hour');
37
+ const refill = tr(
38
+ window === 'day' ? '내일 자정(KST)' : '다음 정시',
39
+ window === 'day' ? 'midnight KST' : 'the top of the next hour',
40
+ );
41
+ return tr(
38
42
  `\n\n⚠️ ${per} 남은 조회 ${remaining}/${limit}회. ` +
39
- `다 쓰면 ${refill}까지 조회가 멈춥니다` +
40
- (upgradeUrl ? `. 한도를 늘리려면: ${upgradeUrl}` : '.')
43
+ `다 쓰면 ${refill}까지 조회가 멈춥니다` +
44
+ (upgradeUrl ? `. 한도를 늘리려면: ${upgradeUrl}` : '.'),
45
+ `\n\n⚠️ ${remaining}/${limit} reads left ${per}. ` +
46
+ `Once they run out, reads pause until ${refill}` +
47
+ (upgradeUrl ? `. To raise the limit: ${upgradeUrl}` : '.'),
41
48
  );
42
49
  }
43
50
 
@@ -95,16 +102,21 @@ function memoryBriefing(memories) {
95
102
  if (!live.length) return null;
96
103
  const line = (m) => {
97
104
  const flags = [];
98
- if (m.status === '확인필요') flags.push('확인필요');
105
+ if (m.status === '확인필요') flags.push(tr('확인필요', 'needs review'));
99
106
  const tail = flags.length ? ` [${flags.join('·')}]` : '';
100
- const src = m.source ? ` (출처: ${m.source})` : '';
107
+ const src = m.source ? tr(` (출처: ${m.source})`, ` (source: ${m.source})`) : '';
101
108
  return `- ${m.content}${tail}${src}`;
102
109
  };
103
- return (
110
+ const body = live.map(line).join('\n');
111
+ return tr(
104
112
  `\n\n---\n🧠 이 워크스페이스가 이미 아는 것 (${live.length}건) — 일을 시작하기 전에 읽으세요.\n` +
105
- live.map(line).join('\n') +
106
- `\n"확인필요"는 출처 문서가 바뀌었다는 뜻입니다 — 맞는지 확인하고 쓰세요.\n` +
107
- `새로 알게 된 규칙·결정·함정이 있으면 rootr_remember로 한 줄 남겨 두세요.`
113
+ body +
114
+ `\n"확인필요"는 출처 문서가 바뀌었다는 뜻입니다 — 맞는지 확인하고 쓰세요.\n` +
115
+ `새로 알게 된 규칙·결정·함정이 있으면 rootr_remember로 한 줄 남겨 두세요.`,
116
+ `\n\n---\n🧠 What this workspace already knows (${live.length}) — read this before starting work.\n` +
117
+ body +
118
+ `\n"needs review" means the source document changed — check it before relying on it.\n` +
119
+ `When you learn a new rule, decision or gotcha, leave one line with rootr_remember.`,
108
120
  );
109
121
  }
110
122
 
package/lib/resolve.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { RootrApiError } from './client.js';
2
+ import { t as tr } from './i18n.js';
2
3
 
3
4
  /**
4
5
  * Web-app route segments that address a single node: rootr.io/{locale?}/w/{ws}/{kind}/{id}
@@ -43,19 +44,34 @@ export async function resolveTarget(client, target) {
43
44
  const url = parseRootrUrl(target);
44
45
  if (url) {
45
46
  if (!url.nodeId) {
46
- throw new RootrApiError(`이 주소는 문서가 아니라 워크스페이스 화면을 가리킵니다: ${target}`, {
47
- hint: `해당 워크스페이스로 전환하려면 rootr_use_workspace 에 ${url.workspaceId} 를 넘기세요.`,
47
+ throw new RootrApiError(
48
+ tr(
49
+ `이 주소는 문서가 아니라 워크스페이스 화면을 가리킵니다: ${target}`,
50
+ `This URL points at a workspace screen, not a document: ${target}`,
51
+ ),
52
+ {
53
+ hint: tr(
54
+ `해당 워크스페이스로 전환하려면 rootr_use_workspace 에 ${url.workspaceId} 를 넘기세요.`,
55
+ `To switch to that workspace, pass ${url.workspaceId} to rootr_use_workspace.`,
56
+ ),
48
57
  });
49
58
  }
50
59
  // The URL carries the workspace it lives in, so a mismatch is knowable
51
60
  // BEFORE the API answers 404 — say so instead of letting it look missing.
52
61
  if (client.workspace && url.workspaceId !== client.workspace) {
53
62
  throw new RootrApiError(
54
- `이 주소는 다른 워크스페이스(${url.workspaceId})의 노드입니다. 현재 활성 워크스페이스는 ${client.workspace} 입니다.`,
63
+ tr(
64
+ `이 주소는 다른 워크스페이스(${url.workspaceId})의 노드입니다. 현재 활성 워크스페이스는 ${client.workspace} 입니다.`,
65
+ `This URL is a node in another workspace (${url.workspaceId}). The active workspace is ${client.workspace}.`,
66
+ ),
55
67
  {
56
68
  hint:
57
- `먼저 rootr_use_workspace 로 ${url.workspaceId} 로 전환하세요. ` +
58
- '전환이 안 되면 그 워크스페이스에 쓸 수 있는 키가 없는 것입니다 (rootr_workspaces 로 확인).',
69
+ tr(
70
+ `먼저 rootr_use_workspace 로 ${url.workspaceId} 로 전환하세요. ` +
71
+ '전환이 안 되면 그 워크스페이스에 쓸 수 있는 키가 없는 것입니다 (rootr_workspaces 로 확인).',
72
+ `Switch to ${url.workspaceId} with rootr_use_workspace first. ` +
73
+ 'If the switch fails, you have no key that opens that workspace (check with rootr_workspaces).',
74
+ ),
59
75
  },
60
76
  );
61
77
  }
@@ -76,7 +92,7 @@ export async function resolveTarget(client, target) {
76
92
 
77
93
  export function requireResolved(resolved) {
78
94
  if (!resolved.existed || !resolved.id) {
79
- throw new RootrApiError(`경로를 찾을 수 없습니다: ${resolved.path}`);
95
+ throw new RootrApiError(tr(`경로를 찾을 수 없습니다: ${resolved.path}`, `Path not found: ${resolved.path}`));
80
96
  }
81
97
  return resolved.id;
82
98
  }
@@ -107,13 +123,13 @@ export async function resolveMoveDestination(client, dest) {
107
123
  return { parentId: existingNode.id };
108
124
  }
109
125
  if (trailingSlash) {
110
- throw new RootrApiError(`대상 폴더를 찾을 수 없습니다: ${destPath}`);
126
+ throw new RootrApiError(tr(`대상 폴더를 찾을 수 없습니다: ${destPath}`, `Destination folder not found: ${destPath}`));
111
127
  }
112
128
 
113
129
  const lastSlash = destPath.lastIndexOf('/');
114
130
  const parentPath = lastSlash > 0 ? destPath.slice(0, lastSlash) : '';
115
131
  const name = destPath.slice(lastSlash + 1);
116
- if (!name) throw new RootrApiError(`잘못된 대상 경로: ${dest}`);
132
+ if (!name) throw new RootrApiError(tr(`잘못된 대상 경로: ${dest}`, `Invalid destination path: ${dest}`));
117
133
 
118
134
  if (!parentPath) {
119
135
  return { parentId: null, name };
@@ -121,7 +137,7 @@ export async function resolveMoveDestination(client, dest) {
121
137
  const parent = await client.getNodeByPath(parentPath);
122
138
  const parentNode = parent && (parent.node || parent);
123
139
  if (!parentNode) {
124
- throw new RootrApiError(`대상 폴더를 찾을 수 없습니다: ${parentPath}`);
140
+ throw new RootrApiError(tr(`대상 폴더를 찾을 수 없습니다: ${parentPath}`, `Destination folder not found: ${parentPath}`));
125
141
  }
126
142
  return { parentId: parentNode.id, name };
127
143
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rootr-cli",
3
- "version": "0.17.1",
3
+ "version": "0.17.2",
4
4
  "description": "Official Rootr CLI + MCP server — read, write, edit and search Rootr documents from the shell or from an AI agent.",
5
5
  "type": "module",
6
6
  "bin": {