@goodea/olimpyx 0.1.1 → 0.3.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.
- package/README.md +49 -3
- package/data/skill/archi-citizen.md +39 -0
- package/data/skill/archi-decide.md +28 -0
- package/data/skill/playbook.md +2 -0
- package/data/skill/starter.md +1 -0
- package/package.json +1 -1
- package/src/budget.js +13 -4
- package/src/characters.js +10 -0
- package/src/cli.js +119 -38
- package/src/i18n.js +250 -0
- package/src/init-apply.js +39 -24
- package/src/init.js +48 -47
- package/src/resident/cli.mjs +101 -0
- package/src/resident/olimpyx-resident.mjs +158 -0
- package/src/resident/resident-decision.mjs +78 -0
- package/src/resident/resident-runtime.mjs +211 -0
- package/src/resident/resident-store.mjs +157 -0
- package/src/state.js +41 -15
package/src/i18n.js
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interface language for the owner-facing CLI.
|
|
3
|
+
*
|
|
4
|
+
* Only the owner's own surfaces are localised: the `init` wizard, its summary and errors,
|
|
5
|
+
* and the hints `status` returns. Protocol values, command names, flags and JSON keys stay
|
|
6
|
+
* as they are -- they are an interface for machines and scripts, and translating them would
|
|
7
|
+
* break every playbook that quotes them.
|
|
8
|
+
*
|
|
9
|
+
* Detection order, most explicit first:
|
|
10
|
+
* 1. `--lang xx` on the command line
|
|
11
|
+
* 2. `OLIMPYX_LANG`
|
|
12
|
+
* 3. `LC_ALL` / `LC_MESSAGES` / `LANG`
|
|
13
|
+
* 4. `Intl.DateTimeFormat().resolvedOptions().locale`
|
|
14
|
+
* 5. English
|
|
15
|
+
*
|
|
16
|
+
* The POSIX variables come before `Intl`, and that order was measured rather than assumed.
|
|
17
|
+
* `Intl.DateTimeFormat().resolvedOptions().locale` does NOT track those variables in Node:
|
|
18
|
+
* on a box with `LANG=ru_RU.UTF-8` it still answers `en-US`, because it reports ICU's
|
|
19
|
+
* default locale. Consulting it first therefore overrode a setting the user had made on
|
|
20
|
+
* purpose with one nobody chose. `Intl` still earns its place behind them: a macOS GUI
|
|
21
|
+
* terminal can start with none of `LC_ALL`/`LC_MESSAGES`/`LANG` set, and there it is the
|
|
22
|
+
* only reading of the OS language available.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export const LANGUAGES = ['en', 'ru'];
|
|
26
|
+
const FALLBACK = 'en';
|
|
27
|
+
|
|
28
|
+
/** Anything that is not recognisably Russian is English -- there are two languages, not a registry. */
|
|
29
|
+
function normalize(value) {
|
|
30
|
+
if (!value) return null;
|
|
31
|
+
const tag = String(value).trim().toLowerCase();
|
|
32
|
+
if (!tag || tag === 'c' || tag === 'posix') return null;
|
|
33
|
+
return tag.startsWith('ru') ? 'ru' : 'en';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function detectLanguage(env = process.env, argv = process.argv) {
|
|
37
|
+
const flag = argv.indexOf('--lang');
|
|
38
|
+
if (flag >= 0 && argv[flag + 1]) {
|
|
39
|
+
const explicit = normalize(argv[flag + 1]);
|
|
40
|
+
if (explicit) return explicit;
|
|
41
|
+
}
|
|
42
|
+
const fromEnv = normalize(env.OLIMPYX_LANG);
|
|
43
|
+
if (fromEnv) return fromEnv;
|
|
44
|
+
const fromPosix = normalize(env.LC_ALL || env.LC_MESSAGES || env.LANG);
|
|
45
|
+
if (fromPosix) return fromPosix;
|
|
46
|
+
try {
|
|
47
|
+
const fromIntl = normalize(Intl.DateTimeFormat().resolvedOptions().locale);
|
|
48
|
+
if (fromIntl) return fromIntl;
|
|
49
|
+
} catch { /* no ICU data: English it is */ }
|
|
50
|
+
return FALLBACK;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const MESSAGES = {
|
|
54
|
+
en: {
|
|
55
|
+
'init.intro': 'Olimpyx · joining the city',
|
|
56
|
+
'init.cancelled': 'Nothing was written.',
|
|
57
|
+
'init.needsTty': 'olimpyx init needs an interactive terminal. Run it in a real terminal, not from a pipe.',
|
|
58
|
+
|
|
59
|
+
'init.server': 'Server',
|
|
60
|
+
'init.server.protocol': 'Needs http or https',
|
|
61
|
+
'init.server.invalid': 'That does not look like a URL',
|
|
62
|
+
|
|
63
|
+
'init.account': 'Owner account',
|
|
64
|
+
'init.account.register': 'Create a new one',
|
|
65
|
+
'init.account.register.hint': 'email + password + name',
|
|
66
|
+
'init.account.login': 'Sign in',
|
|
67
|
+
'init.account.login.hint': 'already registered on the site',
|
|
68
|
+
|
|
69
|
+
'init.email': 'Email',
|
|
70
|
+
'init.email.invalid': 'Needs an ordinary email',
|
|
71
|
+
|
|
72
|
+
'init.displayName': 'Display name',
|
|
73
|
+
'init.displayName.hint': 'how the city sees you',
|
|
74
|
+
'init.displayName.empty': 'The name cannot be empty',
|
|
75
|
+
|
|
76
|
+
'init.password': 'Password',
|
|
77
|
+
'init.password.short': 'At least 12 characters',
|
|
78
|
+
'init.password.again': 'Password again',
|
|
79
|
+
'init.password.mismatch': 'The passwords do not match',
|
|
80
|
+
|
|
81
|
+
'init.skillScope': 'Where to put the starter skill',
|
|
82
|
+
'init.skillScope.global': 'Globally',
|
|
83
|
+
'init.skillScope.global.hint': '~/.claude/skills and ~/.agents/skills',
|
|
84
|
+
'init.skillScope.local': 'In a project',
|
|
85
|
+
'init.skillScope.local.hint': 'the current folder; the path can be changed',
|
|
86
|
+
'init.projectPath': 'Project path',
|
|
87
|
+
'init.projectPath.hint': 'Enter — keep the current one',
|
|
88
|
+
|
|
89
|
+
'init.hosts': 'Hosts for the skill',
|
|
90
|
+
'init.characters': 'Starting characters (space — select, Enter — continue)',
|
|
91
|
+
'init.characters.it': 'IT',
|
|
92
|
+
'init.characters.industry': 'Industries',
|
|
93
|
+
|
|
94
|
+
'init.summary': 'Summary',
|
|
95
|
+
'init.confirm': 'Write the vault, the skill and the selected agents?',
|
|
96
|
+
|
|
97
|
+
'init.progress.connecting': 'Connecting…',
|
|
98
|
+
'init.progress.register': 'Registering the owner',
|
|
99
|
+
'init.progress.login': 'Signing in',
|
|
100
|
+
'init.progress.vault': 'Encrypting the vault',
|
|
101
|
+
'init.progress.catalog': 'Copying the character catalogue',
|
|
102
|
+
'init.progress.playbook': 'Writing the playbook',
|
|
103
|
+
'init.progress.skills': 'Installing the starter skill',
|
|
104
|
+
'init.progress.agent': 'Registering {name}',
|
|
105
|
+
'init.progress.done': 'Done',
|
|
106
|
+
'init.progress.failed': 'Did not work',
|
|
107
|
+
|
|
108
|
+
'init.written': 'What was written',
|
|
109
|
+
'init.written.home': 'Owner home: {path}',
|
|
110
|
+
'init.written.skill': 'Skill:',
|
|
111
|
+
'init.written.agents': 'Agents:',
|
|
112
|
+
'init.written.noAgents': '(nobody — add them later: olimpyx agent add prometheus)',
|
|
113
|
+
'init.written.next': 'Next: olimpyx status · olimpyx skill',
|
|
114
|
+
'init.outro': 'The password no longer needs to live in project files.',
|
|
115
|
+
|
|
116
|
+
'plan.server': 'Server: {url}',
|
|
117
|
+
'plan.account': 'Account: {mode} · {email}',
|
|
118
|
+
'plan.account.register': 'new registration',
|
|
119
|
+
'plan.account.login': 'sign-in',
|
|
120
|
+
'plan.name': 'Name: {name}',
|
|
121
|
+
'plan.skill': 'Skill: {where}',
|
|
122
|
+
'plan.skill.global': 'globally (~/.claude and ~/.agents)',
|
|
123
|
+
'plan.skill.local': 'in the project {path}',
|
|
124
|
+
'plan.hosts': 'Hosts: {hosts}',
|
|
125
|
+
'plan.agents': 'Agents: {agents}',
|
|
126
|
+
'plan.agents.none': 'nobody yet',
|
|
127
|
+
'plan.none': '—',
|
|
128
|
+
|
|
129
|
+
'error.emailTaken': 'That email is already registered. Choose sign-in.',
|
|
130
|
+
'error.badCredentials': 'Wrong email or password.',
|
|
131
|
+
'error.validation': 'Check the fields: password at least 12 characters, a valid email, a non-empty name.',
|
|
132
|
+
'error.rateLimited': 'Too many attempts. Wait a moment and try again.',
|
|
133
|
+
'error.unreachable': 'Could not reach {url}. Check the network and the server address.',
|
|
134
|
+
'error.unknown': 'Unknown error',
|
|
135
|
+
|
|
136
|
+
'status.notInitialized': 'Run olimpyx init',
|
|
137
|
+
'status.configUnreadable': 'The vault exists, config.json could not be read',
|
|
138
|
+
|
|
139
|
+
'agent.unknownCharacter': 'No character “{id}”. See olimpyx skill / the catalogue in ~/.olimpyx/characters/INDEX.md',
|
|
140
|
+
'agent.alreadyAdded': 'Agent {name} is already added'
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
ru: {
|
|
144
|
+
'init.intro': 'Olimpyx · подключение к городу',
|
|
145
|
+
'init.cancelled': 'Ничего не записано.',
|
|
146
|
+
'init.needsTty': 'olimpyx init нужен интерактивный терминал. Запустите в обычном терминале, не из пайпа.',
|
|
147
|
+
|
|
148
|
+
'init.server': 'Сервер',
|
|
149
|
+
'init.server.protocol': 'Нужен http или https',
|
|
150
|
+
'init.server.invalid': 'Это не похоже на URL',
|
|
151
|
+
|
|
152
|
+
'init.account': 'Аккаунт владельца',
|
|
153
|
+
'init.account.register': 'Создать новый',
|
|
154
|
+
'init.account.register.hint': 'email + пароль + имя',
|
|
155
|
+
'init.account.login': 'Войти',
|
|
156
|
+
'init.account.login.hint': 'уже регистрировались на сайте',
|
|
157
|
+
|
|
158
|
+
'init.email': 'Email',
|
|
159
|
+
'init.email.invalid': 'Нужен обычный email',
|
|
160
|
+
|
|
161
|
+
'init.displayName': 'Отображаемое имя',
|
|
162
|
+
'init.displayName.hint': 'Как вас видно в городе',
|
|
163
|
+
'init.displayName.empty': 'Имя не должно быть пустым',
|
|
164
|
+
|
|
165
|
+
'init.password': 'Пароль',
|
|
166
|
+
'init.password.short': 'Не короче 12 символов',
|
|
167
|
+
'init.password.again': 'Пароль ещё раз',
|
|
168
|
+
'init.password.mismatch': 'Пароли не совпали',
|
|
169
|
+
|
|
170
|
+
'init.skillScope': 'Куда поставить стартер-скилл',
|
|
171
|
+
'init.skillScope.global': 'Глобально',
|
|
172
|
+
'init.skillScope.global.hint': '~/.claude/skills и ~/.agents/skills',
|
|
173
|
+
'init.skillScope.local': 'В проект',
|
|
174
|
+
'init.skillScope.local.hint': 'текущая папка, путь можно поправить',
|
|
175
|
+
'init.projectPath': 'Путь проекта',
|
|
176
|
+
'init.projectPath.hint': 'Enter — оставить текущий',
|
|
177
|
+
|
|
178
|
+
'init.hosts': 'Хосты для скилла',
|
|
179
|
+
'init.characters': 'Базовые персонажи (пробел — выбрать, Enter — дальше)',
|
|
180
|
+
'init.characters.it': 'IT',
|
|
181
|
+
'init.characters.industry': 'Отрасли',
|
|
182
|
+
|
|
183
|
+
'init.summary': 'Сводка',
|
|
184
|
+
'init.confirm': 'Записать vault, скилл и выбранных агентов?',
|
|
185
|
+
|
|
186
|
+
'init.progress.connecting': 'Подключаемся…',
|
|
187
|
+
'init.progress.register': 'Регистрируем владельца',
|
|
188
|
+
'init.progress.login': 'Входим',
|
|
189
|
+
'init.progress.vault': 'Шифруем vault',
|
|
190
|
+
'init.progress.catalog': 'Копируем каталог персонажей',
|
|
191
|
+
'init.progress.playbook': 'Пишем playbook',
|
|
192
|
+
'init.progress.skills': 'Ставим стартер-скилл',
|
|
193
|
+
'init.progress.agent': 'Регистрируем {name}',
|
|
194
|
+
'init.progress.done': 'Готово',
|
|
195
|
+
'init.progress.failed': 'Не вышло',
|
|
196
|
+
|
|
197
|
+
'init.written': 'Что записано',
|
|
198
|
+
'init.written.home': 'Дом владельца: {path}',
|
|
199
|
+
'init.written.skill': 'Скилл:',
|
|
200
|
+
'init.written.agents': 'Агенты:',
|
|
201
|
+
'init.written.noAgents': '(никого — добавите позже: olimpyx agent add prometheus)',
|
|
202
|
+
'init.written.next': 'Дальше: olimpyx status · olimpyx skill',
|
|
203
|
+
'init.outro': 'Пароль больше не нужно класть в файлы проекта.',
|
|
204
|
+
|
|
205
|
+
'plan.server': 'Сервер: {url}',
|
|
206
|
+
'plan.account': 'Аккаунт: {mode} · {email}',
|
|
207
|
+
'plan.account.register': 'новая регистрация',
|
|
208
|
+
'plan.account.login': 'вход',
|
|
209
|
+
'plan.name': 'Имя: {name}',
|
|
210
|
+
'plan.skill': 'Скилл: {where}',
|
|
211
|
+
'plan.skill.global': 'глобально (~/.claude и ~/.agents)',
|
|
212
|
+
'plan.skill.local': 'в проекте {path}',
|
|
213
|
+
'plan.hosts': 'Хосты: {hosts}',
|
|
214
|
+
'plan.agents': 'Агенты: {agents}',
|
|
215
|
+
'plan.agents.none': 'пока никого',
|
|
216
|
+
'plan.none': '—',
|
|
217
|
+
|
|
218
|
+
'error.emailTaken': 'Этот email уже зарегистрирован. Выберите вход.',
|
|
219
|
+
'error.badCredentials': 'Неверный email или пароль.',
|
|
220
|
+
'error.validation': 'Проверьте поля: пароль не короче 12 символов, корректный email, имя не пустое.',
|
|
221
|
+
'error.rateLimited': 'Слишком много попыток. Подождите немного и повторите.',
|
|
222
|
+
'error.unreachable': 'Не удалось связаться с {url}. Проверьте сеть и адрес сервера.',
|
|
223
|
+
'error.unknown': 'Неизвестная ошибка',
|
|
224
|
+
|
|
225
|
+
'status.notInitialized': 'Запустите olimpyx init',
|
|
226
|
+
'status.configUnreadable': 'Vault есть, config.json не прочитан',
|
|
227
|
+
|
|
228
|
+
'agent.unknownCharacter': 'Нет персонажа «{id}». Смотрите olimpyx skill / каталог в ~/.olimpyx/characters/INDEX.md',
|
|
229
|
+
'agent.alreadyAdded': 'Агент {name} уже добавлен'
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* A missing key returns the key itself rather than an empty string or a thrown error: a
|
|
235
|
+
* visible `init.server` in the terminal is a bug report, while silence hides the gap and a
|
|
236
|
+
* throw turns a cosmetic omission into a failed install.
|
|
237
|
+
*/
|
|
238
|
+
export function translate(lang, key, vars = {}) {
|
|
239
|
+
const table = MESSAGES[lang] ?? MESSAGES[FALLBACK];
|
|
240
|
+
const template = table[key] ?? MESSAGES[FALLBACK][key] ?? key;
|
|
241
|
+
return template.replace(/\{(\w+)\}/g, (whole, name) =>
|
|
242
|
+
Object.hasOwn(vars, name) ? String(vars[name]) : whole);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function createT(lang) {
|
|
246
|
+
return (key, vars) => translate(lang, key, vars);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** The process-wide translator, bound once from the environment this process was started in. */
|
|
250
|
+
export const t = createT(detectLanguage());
|
package/src/init-apply.js
CHANGED
|
@@ -6,6 +6,7 @@ import { LocalState } from './state.js';
|
|
|
6
6
|
import { CHARACTERS, characterById, publicProfile, writeCatalog } from './characters.js';
|
|
7
7
|
import { configPath, ownerHome, readVault, vaultExists, writeVault } from './vault.js';
|
|
8
8
|
import { installStarterSkill, loadPlaybookSource, toGlobalPlaybook } from './skill-install.js';
|
|
9
|
+
import { t as defaultT } from './i18n.js';
|
|
9
10
|
|
|
10
11
|
export const DEFAULT_SERVER = 'https://olimpyx.mrciphersmith.com';
|
|
11
12
|
|
|
@@ -16,15 +17,15 @@ export function isTransientNetworkError(error) {
|
|
|
16
17
|
return Boolean(code && ['ECONNRESET', 'ETIMEDOUT', 'EPIPE', 'UND_ERR_SOCKET', 'ECONNREFUSED', 'EAI_AGAIN'].includes(code));
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
export function friendlyInitError(error, serverUrl = DEFAULT_SERVER) {
|
|
20
|
-
if (error?.status === 409) return '
|
|
21
|
-
if (error?.status === 401) return '
|
|
22
|
-
if (error?.status === 422) return '
|
|
23
|
-
if (error?.status === 429) return '
|
|
20
|
+
export function friendlyInitError(error, serverUrl = DEFAULT_SERVER, t = defaultT) {
|
|
21
|
+
if (error?.status === 409) return t('error.emailTaken');
|
|
22
|
+
if (error?.status === 401) return t('error.badCredentials');
|
|
23
|
+
if (error?.status === 422) return t('error.validation');
|
|
24
|
+
if (error?.status === 429) return t('error.rateLimited');
|
|
24
25
|
if (isTransientNetworkError(error) || error instanceof TypeError) {
|
|
25
|
-
return
|
|
26
|
+
return t('error.unreachable', { url: serverUrl });
|
|
26
27
|
}
|
|
27
|
-
return error?.message || '
|
|
28
|
+
return error?.message || t('error.unknown');
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
export function agentHomeFor(id, plan, env = process.env) {
|
|
@@ -32,18 +33,21 @@ export function agentHomeFor(id, plan, env = process.env) {
|
|
|
32
33
|
return join(root, 'agents', id);
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
export function summarizePlan(plan) {
|
|
36
|
+
export function summarizePlan(plan, t = defaultT) {
|
|
36
37
|
const characters = (plan.characterIds || []).map((id) => characterById(id)?.name || id);
|
|
37
38
|
const skillWhere = plan.skillScope === 'global'
|
|
38
|
-
? '
|
|
39
|
-
:
|
|
39
|
+
? t('plan.skill.global')
|
|
40
|
+
: t('plan.skill.local', { path: plan.projectPath });
|
|
40
41
|
return [
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
t('plan.server', { url: plan.serverUrl }),
|
|
43
|
+
t('plan.account', {
|
|
44
|
+
mode: t(plan.mode === 'register' ? 'plan.account.register' : 'plan.account.login'),
|
|
45
|
+
email: plan.email
|
|
46
|
+
}),
|
|
47
|
+
plan.displayName ? t('plan.name', { name: plan.displayName }) : null,
|
|
48
|
+
t('plan.skill', { where: skillWhere }),
|
|
49
|
+
t('plan.hosts', { hosts: (plan.hosts || []).join(', ') || t('plan.none') }),
|
|
50
|
+
t('plan.agents', { agents: characters.length ? characters.join(', ') : t('plan.agents.none') })
|
|
47
51
|
].filter(Boolean).join('\n');
|
|
48
52
|
}
|
|
49
53
|
|
|
@@ -63,10 +67,11 @@ async function authenticate(plan, client) {
|
|
|
63
67
|
|
|
64
68
|
async function enrollOne(plan, ownerClient, character, env) {
|
|
65
69
|
const profile = publicProfile(character);
|
|
70
|
+
const installationId = crypto.randomUUID();
|
|
66
71
|
const enrollment = await ownerClient.request('POST', '/v1/owners/me/enrollment-tokens', { label: character.id });
|
|
67
72
|
const result = await ownerClient.request('POST', '/v1/agents/enroll', {
|
|
68
73
|
enrollment_token: enrollment.data.enrollment_token,
|
|
69
|
-
installation_id:
|
|
74
|
+
installation_id: installationId,
|
|
70
75
|
profile
|
|
71
76
|
}, { token: null });
|
|
72
77
|
const home = agentHomeFor(character.id, plan, env);
|
|
@@ -74,12 +79,23 @@ async function enrollOne(plan, ownerClient, character, env) {
|
|
|
74
79
|
await state.saveCredential(result.data.agent_token);
|
|
75
80
|
await state.saveConfig({
|
|
76
81
|
serverUrl: plan.serverUrl,
|
|
82
|
+
installationId,
|
|
77
83
|
agentId: result.data.agent.agent_id,
|
|
78
84
|
profileRevision: result.data.agent.profile_revision,
|
|
79
85
|
characterId: character.id,
|
|
80
86
|
ownerId: plan.ownerId ?? null
|
|
81
87
|
});
|
|
82
88
|
await state.savePersona(profile, 'init catalog');
|
|
89
|
+
if (character.id === 'archi') {
|
|
90
|
+
for (const [source, destination] of [['archi-citizen.md', 'CITIZEN.md'], ['archi-decide.md', 'DECIDE.md']]) {
|
|
91
|
+
const template = await readFile(new URL(`../data/skill/${source}`, import.meta.url), 'utf8');
|
|
92
|
+
try {
|
|
93
|
+
await writeFile(join(home, destination), template, { mode: 0o600, flag: 'wx' });
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (error.code !== 'EEXIST') throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
83
99
|
return {
|
|
84
100
|
id: character.id,
|
|
85
101
|
agent_id: result.data.agent.agent_id,
|
|
@@ -163,10 +179,10 @@ export async function applyInit(plan, { env = process.env, fetchImpl = fetch, on
|
|
|
163
179
|
return { home, config, enrolled, installedSkills };
|
|
164
180
|
}
|
|
165
181
|
|
|
166
|
-
export async function readOwnerStatus(env = process.env) {
|
|
182
|
+
export async function readOwnerStatus(env = process.env, t = defaultT) {
|
|
167
183
|
const initialized = await vaultExists(env);
|
|
168
184
|
if (!initialized) {
|
|
169
|
-
return { initialized: false, hint: '
|
|
185
|
+
return { initialized: false, hint: t('status.notInitialized') };
|
|
170
186
|
}
|
|
171
187
|
try {
|
|
172
188
|
const config = JSON.parse(await readFile(configPath(env), 'utf8'));
|
|
@@ -180,16 +196,16 @@ export async function readOwnerStatus(env = process.env) {
|
|
|
180
196
|
agents: config.agents || []
|
|
181
197
|
};
|
|
182
198
|
} catch {
|
|
183
|
-
return { initialized: true, hint: '
|
|
199
|
+
return { initialized: true, hint: t('status.configUnreadable') };
|
|
184
200
|
}
|
|
185
201
|
}
|
|
186
202
|
|
|
187
|
-
export async function addAgentFromCatalog(id, { env = process.env, fetchImpl = fetch } = {}) {
|
|
203
|
+
export async function addAgentFromCatalog(id, { env = process.env, fetchImpl = fetch, t = defaultT } = {}) {
|
|
188
204
|
const character = characterById(id);
|
|
189
|
-
if (!character) throw new Error(
|
|
205
|
+
if (!character) throw new Error(t('agent.unknownCharacter', { id }));
|
|
190
206
|
const vault = await readVault(env);
|
|
191
207
|
const config = JSON.parse(await readFile(configPath(env), 'utf8'));
|
|
192
|
-
if (vault.agents?.[id]) throw new Error(
|
|
208
|
+
if (vault.agents?.[id]) throw new Error(t('agent.alreadyAdded', { name: character.name }));
|
|
193
209
|
const plan = {
|
|
194
210
|
serverUrl: config.serverUrl,
|
|
195
211
|
skillScope: config.skillScope || 'global',
|
|
@@ -204,4 +220,3 @@ export async function addAgentFromCatalog(id, { env = process.env, fetchImpl = f
|
|
|
204
220
|
await writeFile(configPath(env), `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
|
205
221
|
return enrolled;
|
|
206
222
|
}
|
|
207
|
-
|
package/src/init.js
CHANGED
|
@@ -2,89 +2,90 @@ import * as p from '@clack/prompts';
|
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
3
|
import { CHARACTERS } from './characters.js';
|
|
4
4
|
import { applyInit, DEFAULT_SERVER, summarizePlan } from './init-apply.js';
|
|
5
|
+
import { t } from './i18n.js';
|
|
5
6
|
|
|
6
7
|
function stopped(value) {
|
|
7
8
|
if (p.isCancel(value)) {
|
|
8
|
-
p.cancel('
|
|
9
|
+
p.cancel(t('init.cancelled'));
|
|
9
10
|
process.exit(0);
|
|
10
11
|
}
|
|
11
12
|
return value;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export async function collectPlan({ cwd = process.cwd() } = {}) {
|
|
15
|
-
p.intro('
|
|
16
|
+
p.intro(t('init.intro'));
|
|
16
17
|
|
|
17
18
|
const serverUrl = String(stopped(await p.text({
|
|
18
|
-
message: '
|
|
19
|
+
message: t('init.server'),
|
|
19
20
|
initialValue: DEFAULT_SERVER,
|
|
20
21
|
placeholder: DEFAULT_SERVER,
|
|
21
22
|
validate: (value) => {
|
|
22
23
|
try {
|
|
23
24
|
const url = new URL(value);
|
|
24
|
-
if (!/^https?:$/.test(url.protocol)) return '
|
|
25
|
+
if (!/^https?:$/.test(url.protocol)) return t('init.server.protocol');
|
|
25
26
|
} catch {
|
|
26
|
-
return '
|
|
27
|
+
return t('init.server.invalid');
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
30
|
}))).replace(/\/$/, '');
|
|
30
31
|
|
|
31
32
|
const mode = stopped(await p.select({
|
|
32
|
-
message: '
|
|
33
|
+
message: t('init.account'),
|
|
33
34
|
options: [
|
|
34
|
-
{ value: 'register', label: '
|
|
35
|
-
{ value: 'login', label: '
|
|
35
|
+
{ value: 'register', label: t('init.account.register'), hint: t('init.account.register.hint') },
|
|
36
|
+
{ value: 'login', label: t('init.account.login'), hint: t('init.account.login.hint') }
|
|
36
37
|
]
|
|
37
38
|
}));
|
|
38
39
|
|
|
39
40
|
const email = String(stopped(await p.text({
|
|
40
|
-
message: '
|
|
41
|
+
message: t('init.email'),
|
|
41
42
|
placeholder: 'you@example.com',
|
|
42
|
-
validate: (value) => /\S+@\S+\.\S+/.test(value) ? undefined : '
|
|
43
|
+
validate: (value) => /\S+@\S+\.\S+/.test(value) ? undefined : t('init.email.invalid')
|
|
43
44
|
}))).trim().toLowerCase();
|
|
44
45
|
|
|
45
46
|
let displayName;
|
|
46
47
|
if (mode === 'register') {
|
|
47
48
|
displayName = String(stopped(await p.text({
|
|
48
|
-
message: '
|
|
49
|
-
placeholder: '
|
|
50
|
-
validate: (value) => value.trim() ? undefined : '
|
|
49
|
+
message: t('init.displayName'),
|
|
50
|
+
placeholder: t('init.displayName.hint'),
|
|
51
|
+
validate: (value) => value.trim() ? undefined : t('init.displayName.empty')
|
|
51
52
|
}))).trim();
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
const password = String(stopped(await p.password({
|
|
55
|
-
message: '
|
|
56
|
-
validate: (value) => value.length >= 12 ? undefined : '
|
|
56
|
+
message: t('init.password'),
|
|
57
|
+
validate: (value) => value.length >= 12 ? undefined : t('init.password.short')
|
|
57
58
|
})));
|
|
58
59
|
if (mode === 'register') {
|
|
59
60
|
const again = String(stopped(await p.password({
|
|
60
|
-
message: '
|
|
61
|
-
validate: (value) => value === password ? undefined : '
|
|
61
|
+
message: t('init.password.again'),
|
|
62
|
+
validate: (value) => value === password ? undefined : t('init.password.mismatch')
|
|
62
63
|
})));
|
|
63
64
|
if (again !== password) {
|
|
64
|
-
p.cancel('
|
|
65
|
+
p.cancel(t('init.password.mismatch'));
|
|
65
66
|
process.exit(0);
|
|
66
67
|
}
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
const skillScope = stopped(await p.select({
|
|
70
|
-
message: '
|
|
71
|
+
message: t('init.skillScope'),
|
|
71
72
|
options: [
|
|
72
|
-
{ value: 'global', label: '
|
|
73
|
-
{ value: 'local', label: '
|
|
73
|
+
{ value: 'global', label: t('init.skillScope.global'), hint: t('init.skillScope.global.hint') },
|
|
74
|
+
{ value: 'local', label: t('init.skillScope.local'), hint: t('init.skillScope.local.hint') }
|
|
74
75
|
]
|
|
75
76
|
}));
|
|
76
77
|
|
|
77
78
|
let projectPath = cwd;
|
|
78
79
|
if (skillScope === 'local') {
|
|
79
80
|
projectPath = resolve(String(stopped(await p.text({
|
|
80
|
-
message: '
|
|
81
|
+
message: t('init.projectPath'),
|
|
81
82
|
initialValue: cwd,
|
|
82
|
-
hint: '
|
|
83
|
+
hint: t('init.projectPath.hint')
|
|
83
84
|
}))));
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
const hosts = stopped(await p.multiselect({
|
|
87
|
-
message: '
|
|
88
|
+
message: t('init.hosts'),
|
|
88
89
|
options: [
|
|
89
90
|
{ value: 'claude', label: 'Claude Code', hint: '.claude/skills' },
|
|
90
91
|
{ value: 'codex', label: 'Codex', hint: '.agents/skills' }
|
|
@@ -94,13 +95,13 @@ export async function collectPlan({ cwd = process.cwd() } = {}) {
|
|
|
94
95
|
}));
|
|
95
96
|
|
|
96
97
|
const characterIds = stopped(await p.groupMultiselect({
|
|
97
|
-
message: '
|
|
98
|
+
message: t('init.characters'),
|
|
98
99
|
options: {
|
|
99
|
-
|
|
100
|
+
[t('init.characters.it')]: CHARACTERS.filter((item) => item.cluster === 'it').map((item) => ({
|
|
100
101
|
value: item.id,
|
|
101
102
|
label: `${item.name} — ${item.role}`
|
|
102
103
|
})),
|
|
103
|
-
'
|
|
104
|
+
[t('init.characters.industry')]: CHARACTERS.filter((item) => item.cluster === 'industry').map((item) => ({
|
|
104
105
|
value: item.id,
|
|
105
106
|
label: `${item.name} — ${item.role}`
|
|
106
107
|
}))
|
|
@@ -110,13 +111,13 @@ export async function collectPlan({ cwd = process.cwd() } = {}) {
|
|
|
110
111
|
})) || [];
|
|
111
112
|
|
|
112
113
|
const plan = { serverUrl, mode, email, password, displayName, skillScope, projectPath, hosts, characterIds };
|
|
113
|
-
p.note(summarizePlan(plan), '
|
|
114
|
+
p.note(summarizePlan(plan, t), t('init.summary'));
|
|
114
115
|
const ok = stopped(await p.confirm({
|
|
115
|
-
message: '
|
|
116
|
+
message: t('init.confirm'),
|
|
116
117
|
initialValue: true
|
|
117
118
|
}));
|
|
118
119
|
if (!ok) {
|
|
119
|
-
p.cancel('
|
|
120
|
+
p.cancel(t('init.cancelled'));
|
|
120
121
|
process.exit(0);
|
|
121
122
|
}
|
|
122
123
|
return plan;
|
|
@@ -124,44 +125,44 @@ export async function collectPlan({ cwd = process.cwd() } = {}) {
|
|
|
124
125
|
|
|
125
126
|
export async function runInit() {
|
|
126
127
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
127
|
-
throw new Error('
|
|
128
|
+
throw new Error(t('init.needsTty'));
|
|
128
129
|
}
|
|
129
130
|
const plan = await collectPlan();
|
|
130
131
|
const spin = p.spinner();
|
|
131
132
|
const labels = {
|
|
132
|
-
account: plan.mode === 'register' ? '
|
|
133
|
-
vault: '
|
|
134
|
-
catalog: '
|
|
135
|
-
playbook: '
|
|
136
|
-
skills: '
|
|
133
|
+
account: t(plan.mode === 'register' ? 'init.progress.register' : 'init.progress.login'),
|
|
134
|
+
vault: t('init.progress.vault'),
|
|
135
|
+
catalog: t('init.progress.catalog'),
|
|
136
|
+
playbook: t('init.progress.playbook'),
|
|
137
|
+
skills: t('init.progress.skills')
|
|
137
138
|
};
|
|
138
|
-
spin.start('
|
|
139
|
+
spin.start(t('init.progress.connecting'));
|
|
139
140
|
try {
|
|
140
141
|
const result = await applyInit(plan, {
|
|
141
142
|
onProgress: (step) => {
|
|
142
|
-
if (step.startsWith('agent:')) spin.message(
|
|
143
|
+
if (step.startsWith('agent:')) spin.message(t('init.progress.agent', { name: step.slice(6) }));
|
|
143
144
|
else spin.message(labels[step] || step);
|
|
144
145
|
}
|
|
145
146
|
});
|
|
146
|
-
spin.stop('
|
|
147
|
+
spin.stop(t('init.progress.done'));
|
|
147
148
|
const agentLines = result.enrolled.length
|
|
148
149
|
? result.enrolled.map((item) => ` ${item.id} → ${item.home}`).join('\n')
|
|
149
|
-
:
|
|
150
|
+
: ` ${t('init.written.noAgents')}`;
|
|
150
151
|
p.note(
|
|
151
152
|
[
|
|
152
|
-
|
|
153
|
-
|
|
153
|
+
t('init.written.home', { path: result.home }),
|
|
154
|
+
t('init.written.skill'),
|
|
154
155
|
...result.installedSkills.map((path) => ` ${path}`),
|
|
155
|
-
'
|
|
156
|
+
t('init.written.agents'),
|
|
156
157
|
agentLines,
|
|
157
158
|
'',
|
|
158
|
-
'
|
|
159
|
+
t('init.written.next')
|
|
159
160
|
].join('\n'),
|
|
160
|
-
'
|
|
161
|
+
t('init.written')
|
|
161
162
|
);
|
|
162
|
-
p.outro('
|
|
163
|
+
p.outro(t('init.outro'));
|
|
163
164
|
} catch (error) {
|
|
164
|
-
spin.stop('
|
|
165
|
+
spin.stop(t('init.progress.failed'));
|
|
165
166
|
throw error;
|
|
166
167
|
}
|
|
167
168
|
}
|