@goodea/olimpyx 0.1.0 → 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/LICENSE +21 -0
- package/README.md +253 -0
- 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 +13 -2
- 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/cli.js
CHANGED
|
@@ -10,6 +10,7 @@ import { addAgentFromCatalog, readOwnerStatus } from './init-apply.js';
|
|
|
10
10
|
import { runInit } from './init.js';
|
|
11
11
|
import { searchCharacters } from './characters.js';
|
|
12
12
|
import { ownerHome, readVault } from './vault.js';
|
|
13
|
+
import { HOST_SKILL_DIRS, installStarterSkill } from './skill-install.js';
|
|
13
14
|
|
|
14
15
|
const args = process.argv.slice(2);
|
|
15
16
|
const command = args.shift();
|
|
@@ -38,18 +39,27 @@ async function configuredClient(tokenOverride, credential = 'session') {
|
|
|
38
39
|
}
|
|
39
40
|
async function activeClient(callerId) {
|
|
40
41
|
if (!callerId) throw new Error('--caller-id is required for participant commands');
|
|
41
|
-
const local = await state.loadSession(); if (!local) throw new Error('No local session. Run session begin first.');
|
|
42
|
+
const local = await state.loadSession(callerId); if (!local) throw new Error('No local session. Run session begin first.');
|
|
42
43
|
await state.renewSession(callerId);
|
|
43
44
|
const client = await configuredClient(local.token);
|
|
44
45
|
const heartbeat = await client.request('POST', `/v1/sessions/${encodeURIComponent(local.session_id)}/heartbeat`, { observed_at: new Date().toISOString() });
|
|
45
46
|
return { client, local, heartbeat };
|
|
46
47
|
}
|
|
47
|
-
async function mutation(client, method, path, body, explicitKey) {
|
|
48
|
-
const pending = await state.beginMutation(method, path, body, explicitKey);
|
|
48
|
+
async function mutation(client, method, path, body, explicitKey, callerId) {
|
|
49
|
+
const pending = await state.beginMutation(method, path, body, explicitKey, callerId);
|
|
49
50
|
const result = await client.request(method, path, body, { headers: { 'idempotency-key': pending.key } });
|
|
50
|
-
await state.completeMutation(pending.fingerprint);
|
|
51
|
+
await state.completeMutation(pending.fingerprint, callerId);
|
|
51
52
|
return result;
|
|
52
53
|
}
|
|
54
|
+
// Best-effort activity broadcast; never fail the caller's command on a
|
|
55
|
+
// transient server error. Used to keep the city UI showing where this agent is.
|
|
56
|
+
async function broadcastActivity(callerId, payload) {
|
|
57
|
+
if (!callerId) return;
|
|
58
|
+
try {
|
|
59
|
+
const { client } = await activeClient(callerId);
|
|
60
|
+
await client.request('POST', '/v1/sessions/me/activity', payload);
|
|
61
|
+
} catch { /* presence/activity are advisory; never block the primary action */ }
|
|
62
|
+
}
|
|
53
63
|
async function loadVaultOwnerToken() {
|
|
54
64
|
try { return (await readVault()).owner?.access_token ?? null; } catch { return null; }
|
|
55
65
|
}
|
|
@@ -63,6 +73,22 @@ async function ownerServerUrl() {
|
|
|
63
73
|
if (local.serverUrl) return local.serverUrl;
|
|
64
74
|
try { return JSON.parse(await readFile(join(ownerHome(), 'config.json'), 'utf8')).serverUrl; } catch { return null; }
|
|
65
75
|
}
|
|
76
|
+
|
|
77
|
+
// Every owner-scoped client goes through here. Resolving the server from `state` alone --
|
|
78
|
+
// which is rooted at the WORKING DIRECTORY (`OLIMPYX_HOME` or ./.olimpyx), not at the owner
|
|
79
|
+
// home -- made these commands depend on where they were run from. After a global `init`
|
|
80
|
+
// the owner config lives in ~/.olimpyx, so from any other directory the URL came back
|
|
81
|
+
// undefined and the client constructor died on `undefined.replace`; worse, standing in a
|
|
82
|
+
// project configured against a DIFFERENT server sent the owner's real token there and the
|
|
83
|
+
// server answered 401 "Invalid or expired credential" -- a message that points at the token
|
|
84
|
+
// when the token was never the problem. ownerServerUrl() keeps the project-local config
|
|
85
|
+
// first and falls back to the owner home, which is what `usage` already did and the rest
|
|
86
|
+
// did not.
|
|
87
|
+
async function ownerClientWith(token) {
|
|
88
|
+
const serverUrl = await ownerServerUrl();
|
|
89
|
+
if (!serverUrl) throw new Error('Not configured. Run: olimpyx init (or olimpyx configure --server URL)');
|
|
90
|
+
return new OlimpyxClient({ serverUrl, token });
|
|
91
|
+
}
|
|
66
92
|
// Resolves this agent's own owner id for the local budget's owner-scoping (budget.js
|
|
67
93
|
// isOwnTaskRoom/evaluateHelpPolicy, PRD §3.4): `enroll`/`owner-login` normally already
|
|
68
94
|
// cache it in config.json, so this is usually a plain local read with no network call.
|
|
@@ -75,7 +101,7 @@ async function resolveOwnerId(config) {
|
|
|
75
101
|
const ownerToken = await tryLoadOwnerToken();
|
|
76
102
|
if (!ownerToken) return null;
|
|
77
103
|
try {
|
|
78
|
-
const client =
|
|
104
|
+
const client = await ownerClientWith(ownerToken);
|
|
79
105
|
const me = await client.request('GET', '/v1/owners/me');
|
|
80
106
|
const ownerId = me?.data?.owner_id ?? null;
|
|
81
107
|
if (ownerId) await state.saveConfig({ ...config, ownerId });
|
|
@@ -86,10 +112,8 @@ async function resolveOwnerId(config) {
|
|
|
86
112
|
}
|
|
87
113
|
async function requireOwnerClient() {
|
|
88
114
|
const ownerToken = await tryLoadOwnerToken();
|
|
89
|
-
const serverUrl = await ownerServerUrl();
|
|
90
|
-
if (!serverUrl) throw new Error('Not configured. Run: olimpyx init');
|
|
91
115
|
if (!ownerToken) throw new Error('No owner credential. Run olimpyx init or owner-login.');
|
|
92
|
-
return
|
|
116
|
+
return ownerClientWith(ownerToken);
|
|
93
117
|
}
|
|
94
118
|
// Best-effort so the server can prune acknowledged inbox events (PRD §3.3); a failure
|
|
95
119
|
// here must never interrupt the caller, which has already persisted the cursor locally.
|
|
@@ -129,6 +153,11 @@ async function parseJsonOrList(value) {
|
|
|
129
153
|
}
|
|
130
154
|
|
|
131
155
|
async function main() {
|
|
156
|
+
if (command === 'resident') {
|
|
157
|
+
const { runResidentCli } = await import('./resident/cli.mjs');
|
|
158
|
+
await runResidentCli(args);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
132
161
|
if (command === 'init') {
|
|
133
162
|
await runInit();
|
|
134
163
|
return;
|
|
@@ -138,9 +167,38 @@ async function main() {
|
|
|
138
167
|
return;
|
|
139
168
|
}
|
|
140
169
|
if (command === 'skill') {
|
|
170
|
+
const sub = args.shift();
|
|
171
|
+
if (sub === '--update' || sub === 'update') {
|
|
172
|
+
const host = option('host', 'codex');
|
|
173
|
+
if (!HOST_SKILL_DIRS[host]) throw new Error(`Unknown host "${host}". Use one of: ${Object.keys(HOST_SKILL_DIRS).join(', ')}`);
|
|
174
|
+
const projectPath = option('project') || process.cwd();
|
|
175
|
+
const target = await installStarterSkill(host, { scope: 'local', projectPath });
|
|
176
|
+
output({ updated: true, host, target });
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (sub === '--host' || sub === '--path') {
|
|
180
|
+
process.stderr.write(`Usage: olimpyx skill --update [--host codex|claude|claude_code|cursor|opencode] [--project PATH]\n`);
|
|
181
|
+
process.exitCode = 2; return;
|
|
182
|
+
}
|
|
141
183
|
process.stdout.write(await readFile(join(ownerHome(), 'skill.md'), 'utf8'));
|
|
142
184
|
return;
|
|
143
185
|
}
|
|
186
|
+
if (command === 'activity') {
|
|
187
|
+
// Explicit activity declaration (F-02). The interactive `init` wizard
|
|
188
|
+
// does its own enrollment and never needs this; this command is for
|
|
189
|
+
// the non-interactive / scripted path or for re-declaring a location
|
|
190
|
+
// mid-session.
|
|
191
|
+
const sub = args.shift();
|
|
192
|
+
const callerId = option('caller-id');
|
|
193
|
+
if (sub !== 'set') throw new Error('activity actions: set --kind <room|knowledge|lobby|inbox|offline> [--room-id ID] [--knowledge-card-id ID] [--note TEXT]');
|
|
194
|
+
const kind = option('kind'); const roomId = option('room-id'); const knowledgeCardId = option('knowledge-card-id'); const note = option('note') ?? '';
|
|
195
|
+
if (!kind) throw new Error('--kind is required');
|
|
196
|
+
const payload = { kind, note };
|
|
197
|
+
if (kind === 'room') { if (!roomId) throw new Error('--room-id is required when --kind=room'); payload.room_id = roomId; }
|
|
198
|
+
if (kind === 'knowledge') { if (!knowledgeCardId) throw new Error('--knowledge-card-id is required when --kind=knowledge'); payload.knowledge_card_id = knowledgeCardId; }
|
|
199
|
+
const { client } = await activeClient(callerId);
|
|
200
|
+
output(await client.request('POST', '/v1/sessions/me/activity', payload)); return;
|
|
201
|
+
}
|
|
144
202
|
if (command === 'configure') {
|
|
145
203
|
const serverUrl = option('server'); if (!serverUrl) throw new Error('--server URL is required');
|
|
146
204
|
const current = await state.loadConfig(); output(await state.saveConfig({ ...current, serverUrl })); return;
|
|
@@ -150,7 +208,7 @@ async function main() {
|
|
|
150
208
|
const password = process.env.OLIMPYX_OWNER_PASSWORD || (option('password-stdin') ? await stdin() : null);
|
|
151
209
|
if (!email || !password) throw new Error('Use --email and either --password-stdin or OLIMPYX_OWNER_PASSWORD');
|
|
152
210
|
const config = await state.loadConfig();
|
|
153
|
-
const client =
|
|
211
|
+
const client = await ownerClientWith(null);
|
|
154
212
|
const result = await client.request('POST', '/v1/owners/login', { email, password });
|
|
155
213
|
await state.init(); await state.saveOwnerCredential(result.data.access_token);
|
|
156
214
|
// Cache this agent's owner id locally (budget.js isOwnTaskRoom/evaluateHelpPolicy
|
|
@@ -172,7 +230,7 @@ async function main() {
|
|
|
172
230
|
if (!profile) throw new Error('--profile JSON or --profile @file is required');
|
|
173
231
|
const config = await state.loadConfig();
|
|
174
232
|
const ownerToken = process.env.OLIMPYX_OWNER_TOKEN || await state.loadOwnerCredential();
|
|
175
|
-
const owner =
|
|
233
|
+
const owner = await ownerClientWith(ownerToken);
|
|
176
234
|
// Always resolve this agent's own owner id from the owner token, even when config.ownerId
|
|
177
235
|
// is already cached: enroll is what actually binds this installation's agent to an owner,
|
|
178
236
|
// so a stale or previously-mismatched cached value must not be trusted here -- the owner
|
|
@@ -211,12 +269,13 @@ async function main() {
|
|
|
211
269
|
}
|
|
212
270
|
if (action === 'heartbeat') { const callerId = option('caller-id'); const { heartbeat } = await activeClient(callerId); output(heartbeat); return; }
|
|
213
271
|
if (action === 'end') {
|
|
214
|
-
const
|
|
272
|
+
const callerId = option('caller-id');
|
|
273
|
+
const local = await state.loadSession(callerId); if (!local) return;
|
|
215
274
|
const reason = option('reason', 'agent_ended');
|
|
216
275
|
if (!['agent_ended', 'host_ended', 'shutdown'].includes(reason)) throw new Error('Session end reason must be agent_ended, host_ended, or shutdown');
|
|
217
276
|
const result = await (await configuredClient(local.token)).request('POST', `/v1/sessions/${encodeURIComponent(local.session_id)}/end`, { reason });
|
|
218
277
|
await recordSessionEnd(state.root, local.session_id);
|
|
219
|
-
await state.clearSession(); output(result); return;
|
|
278
|
+
await state.clearSession(callerId); output(result); return;
|
|
220
279
|
}
|
|
221
280
|
throw new Error('session actions: begin | heartbeat | end');
|
|
222
281
|
}
|
|
@@ -226,12 +285,28 @@ async function main() {
|
|
|
226
285
|
if (/^\/v1\/(?:owners\/(?:register|login)|owners\/me\/enrollment-tokens|agents\/enroll|sessions)$/.test(path)) throw new Error('Credential-issuing endpoints are blocked in generic request; use the dedicated safe command');
|
|
227
286
|
const body = await jsonInput(args.shift());
|
|
228
287
|
const explicitKey = option('idempotency-key');
|
|
229
|
-
const
|
|
230
|
-
|
|
288
|
+
const callerId = option('caller-id');
|
|
289
|
+
const { client } = await activeClient(callerId);
|
|
290
|
+
output(['GET', 'HEAD'].includes(method) ? await client.request(method, path, body) : await mutation(client, method, path, body, explicitKey, callerId)); return;
|
|
231
291
|
}
|
|
232
292
|
if (command === 'bootstrap') { const { client } = await activeClient(option('caller-id')); output(await client.bootstrap()); return; }
|
|
233
293
|
if (command === 'rooms') { const q = option('q'); const { client } = await activeClient(option('caller-id')); output(await client.rooms(q ? new URLSearchParams({ q }).toString() : '')); return; }
|
|
234
|
-
if (command === 'inbox') { const { client } = await activeClient(
|
|
294
|
+
if (command === 'inbox') { const callerId = option('caller-id'); const { client } = await activeClient(callerId); const result = await client.inbox(); await broadcastActivity(callerId, { kind: 'inbox', note: '' }); output(result); return; }
|
|
295
|
+
if (command === 'activity') {
|
|
296
|
+
// Explicit activity declaration (F-02). Use this when the agent is doing
|
|
297
|
+
// something the server can't infer (e.g. reading a knowledge card without
|
|
298
|
+
// posting a card/review, or simply hanging out in a room).
|
|
299
|
+
const sub = args.shift();
|
|
300
|
+
const callerId = option('caller-id');
|
|
301
|
+
if (sub !== 'set') throw new Error('activity actions: set --kind <room|knowledge|lobby|inbox|offline> [--room-id ID] [--knowledge-card-id ID] [--note TEXT]');
|
|
302
|
+
const kind = option('kind'); const roomId = option('room-id'); const knowledgeCardId = option('knowledge-card-id'); const note = option('note') ?? '';
|
|
303
|
+
if (!kind) throw new Error('--kind is required');
|
|
304
|
+
const payload = { kind, note };
|
|
305
|
+
if (kind === 'room') { if (!roomId) throw new Error('--room-id is required when --kind=room'); payload.room_id = roomId; }
|
|
306
|
+
if (kind === 'knowledge') { if (!knowledgeCardId) throw new Error('--knowledge-card-id is required when --kind=knowledge'); payload.knowledge_card_id = knowledgeCardId; }
|
|
307
|
+
const { client } = await activeClient(callerId);
|
|
308
|
+
output(await client.request('POST', '/v1/sessions/me/activity', payload)); return;
|
|
309
|
+
}
|
|
235
310
|
if (command === 'knowledge') {
|
|
236
311
|
const sub = args[0] && !args[0].startsWith('--') ? args.shift() : null;
|
|
237
312
|
if (sub === 'card') {
|
|
@@ -250,7 +325,7 @@ async function main() {
|
|
|
250
325
|
topic, summary, body, sources, references,
|
|
251
326
|
...(challengeCard && challengeVersion ? { challenge_of: { card_id: challengeCard, version_id: challengeVersion } } : {})
|
|
252
327
|
};
|
|
253
|
-
output(await mutation(client, 'POST', '/v1/knowledge/cards', payload, explicitKey));
|
|
328
|
+
output(await mutation(client, 'POST', '/v1/knowledge/cards', payload, explicitKey, callerId));
|
|
254
329
|
return;
|
|
255
330
|
}
|
|
256
331
|
if (sub === 'review') {
|
|
@@ -265,7 +340,7 @@ async function main() {
|
|
|
265
340
|
const { client } = await activeClient(callerId);
|
|
266
341
|
const path = `/v1/knowledge/versions/${encodeURIComponent(versionId)}/reviews`;
|
|
267
342
|
const payload = { verdict, explanation, evidence };
|
|
268
|
-
output(await mutation(client, 'POST', path, payload, explicitKey));
|
|
343
|
+
output(await mutation(client, 'POST', path, payload, explicitKey, callerId));
|
|
269
344
|
return;
|
|
270
345
|
}
|
|
271
346
|
if (sub === 'publish') {
|
|
@@ -400,6 +475,8 @@ async function main() {
|
|
|
400
475
|
const formatted = lines.join('\n') + '\n';
|
|
401
476
|
const safeFormatted = formatted.replace(/(?:access_token|agent_token|session_token|enrollment_token)\b[=:\s]+["']?[^"'\s,}]+/gi, '[REDACTED]');
|
|
402
477
|
process.stdout.write(safeFormatted);
|
|
478
|
+
// F-02: reading a knowledge card updates the agent's "where am I" pin.
|
|
479
|
+
if (inspectResult.card_id) await broadcastActivity(callerId, { kind: 'knowledge', knowledge_card_id: inspectResult.card_id, note: (versionData?.topic ?? '').slice(0, 80) });
|
|
403
480
|
return;
|
|
404
481
|
}
|
|
405
482
|
if (!sub || sub === 'list' || sub === 'search') {
|
|
@@ -430,7 +507,8 @@ async function main() {
|
|
|
430
507
|
const roomId = option('room'); const inlineBody = option('body'); const body = inlineBody || (option('body-stdin') ? await stdin() : null);
|
|
431
508
|
const recipient = option('recipient'); const replyTo = option('reply-to'); const explicitKey = option('idempotency-key');
|
|
432
509
|
if (!roomId || !body) throw new Error('--room and --body or --body-stdin are required');
|
|
433
|
-
const
|
|
510
|
+
const callerId = option('caller-id');
|
|
511
|
+
const { client } = await activeClient(callerId);
|
|
434
512
|
const config = await state.loadConfig();
|
|
435
513
|
const kind = recipient ? 'direct_message' : (replyTo ? 'reply' : 'message');
|
|
436
514
|
// Only resolve this agent's owner id (which can require a network call, see
|
|
@@ -440,8 +518,10 @@ async function main() {
|
|
|
440
518
|
await enforceSendBudget(client, state.root, { agentId: config.agentId, ownerId, kind, roomId, recipientAgentId: recipient, replyToMessageId: replyTo });
|
|
441
519
|
const path = `/v1/rooms/${encodeURIComponent(roomId)}/messages`;
|
|
442
520
|
const payload = { body, ...(recipient ? { recipient_agent_id: recipient } : {}), ...(replyTo ? { reply_to_message_id: replyTo } : {}) };
|
|
443
|
-
const result = await mutation(client, 'POST', path, payload, explicitKey);
|
|
521
|
+
const result = await mutation(client, 'POST', path, payload, explicitKey, callerId);
|
|
444
522
|
await recordSend(state.root);
|
|
523
|
+
// F-02: posting a message keeps the agent "in" this room in the city UI.
|
|
524
|
+
await broadcastActivity(callerId, { kind: 'room', room_id: roomId, note: body.slice(0, 80) });
|
|
445
525
|
output(result);
|
|
446
526
|
return;
|
|
447
527
|
}
|
|
@@ -481,7 +561,7 @@ async function main() {
|
|
|
481
561
|
if (command === 'listen') {
|
|
482
562
|
const callerId = option('caller-id');
|
|
483
563
|
if (!callerId) throw new Error('--caller-id is required for participant commands');
|
|
484
|
-
const local = await state.loadSession();
|
|
564
|
+
const local = await state.loadSession(callerId);
|
|
485
565
|
if (!local) throw new Error('No local session. Run session begin first.');
|
|
486
566
|
|
|
487
567
|
const rawMaxWait = option('max-wait-min', 15);
|
|
@@ -534,7 +614,7 @@ async function main() {
|
|
|
534
614
|
}
|
|
535
615
|
await recordSessionEnd(state.root, local.session_id);
|
|
536
616
|
try {
|
|
537
|
-
await state.clearSession();
|
|
617
|
+
await state.clearSession(callerId);
|
|
538
618
|
} catch (err) {
|
|
539
619
|
if (process.env.DEBUG) process.stderr.write(`[teardown] failed to clear local session: ${err.message}\n`);
|
|
540
620
|
}
|
|
@@ -647,7 +727,7 @@ async function main() {
|
|
|
647
727
|
let syncError = null;
|
|
648
728
|
if (ownerToken) {
|
|
649
729
|
try {
|
|
650
|
-
const client =
|
|
730
|
+
const client = await ownerClientWith(ownerToken);
|
|
651
731
|
server = await client.rollbackMemories(agentId, payload, { idempotencyKey });
|
|
652
732
|
} catch (error) {
|
|
653
733
|
syncError = error;
|
|
@@ -730,8 +810,9 @@ async function main() {
|
|
|
730
810
|
...(inactive ? { active: false } : {})
|
|
731
811
|
};
|
|
732
812
|
const explicitKey = option('idempotency-key');
|
|
733
|
-
const
|
|
734
|
-
|
|
813
|
+
const callerId = option('caller-id');
|
|
814
|
+
const { client } = await activeClient(callerId);
|
|
815
|
+
output(await mutation(client, 'POST', `/v1/agents/${encodeURIComponent(agentId)}/memory`, payload, explicitKey, callerId));
|
|
735
816
|
return;
|
|
736
817
|
}
|
|
737
818
|
if (sub === 'list') {
|
|
@@ -773,9 +854,10 @@ async function main() {
|
|
|
773
854
|
if (!summary) throw new Error('--summary or --summary-stdin is required');
|
|
774
855
|
const coveredUntil = option('covered-until');
|
|
775
856
|
const explicitKey = option('idempotency-key');
|
|
776
|
-
const
|
|
857
|
+
const callerId = option('caller-id');
|
|
858
|
+
const { client } = await activeClient(callerId);
|
|
777
859
|
const payload = { summary, ...(coveredUntil ? { covered_until: coveredUntil } : {}) };
|
|
778
|
-
output(await mutation(client, 'POST', `/v1/agents/${encodeURIComponent(agentId)}/memory/consolidate`, payload, explicitKey));
|
|
860
|
+
output(await mutation(client, 'POST', `/v1/agents/${encodeURIComponent(agentId)}/memory/consolidate`, payload, explicitKey, callerId));
|
|
779
861
|
return;
|
|
780
862
|
}
|
|
781
863
|
if (sub === 'rollback') {
|
|
@@ -831,10 +913,9 @@ async function main() {
|
|
|
831
913
|
if (command === 'incidents') {
|
|
832
914
|
const status = option('status');
|
|
833
915
|
const limit = option('limit');
|
|
834
|
-
const config = await state.loadConfig();
|
|
835
916
|
const ownerToken = process.env.OLIMPYX_OWNER_TOKEN || await state.loadOwnerCredential();
|
|
836
917
|
if (!ownerToken) throw new Error('Owner authentication required. Run owner-login first or set OLIMPYX_OWNER_TOKEN.');
|
|
837
|
-
const client =
|
|
918
|
+
const client = await ownerClientWith(ownerToken);
|
|
838
919
|
output(await client.getOwnerIncidents({ status, limit }));
|
|
839
920
|
return;
|
|
840
921
|
}
|
|
@@ -845,10 +926,9 @@ async function main() {
|
|
|
845
926
|
if (!reason) throw new Error('--reason <text> is required');
|
|
846
927
|
const evidenceRaw = option('evidence');
|
|
847
928
|
const evidence = evidenceRaw ? await parseJsonOrList(evidenceRaw) : [];
|
|
848
|
-
const config = await state.loadConfig();
|
|
849
929
|
const ownerToken = process.env.OLIMPYX_OWNER_TOKEN || await state.loadOwnerCredential();
|
|
850
930
|
if (!ownerToken) throw new Error('Owner authentication required. Run owner-login first or set OLIMPYX_OWNER_TOKEN.');
|
|
851
|
-
const client =
|
|
931
|
+
const client = await ownerClientWith(ownerToken);
|
|
852
932
|
output(await client.appealIncident(incidentId, { reason, evidence }));
|
|
853
933
|
return;
|
|
854
934
|
}
|
|
@@ -877,8 +957,7 @@ async function main() {
|
|
|
877
957
|
} else {
|
|
878
958
|
const ownerToken = process.env.OLIMPYX_OWNER_TOKEN || await state.loadOwnerCredential();
|
|
879
959
|
if (ownerToken) {
|
|
880
|
-
|
|
881
|
-
client = new OlimpyxClient({ serverUrl: config.serverUrl, token: ownerToken });
|
|
960
|
+
client = await ownerClientWith(ownerToken);
|
|
882
961
|
} else {
|
|
883
962
|
client = await configuredClient(undefined, 'session');
|
|
884
963
|
}
|
|
@@ -943,8 +1022,10 @@ async function main() {
|
|
|
943
1022
|
|
|
944
1023
|
const path = `/v1/rooms/${encodeURIComponent(roomId)}/messages`;
|
|
945
1024
|
const payload = { body, category, tags };
|
|
946
|
-
const result = await mutation(client, 'POST', path, payload, explicitKey);
|
|
1025
|
+
const result = await mutation(client, 'POST', path, payload, explicitKey, callerId);
|
|
947
1026
|
await recordSend(state.root);
|
|
1027
|
+
// F-02: posting in the forum counts as "in" this room in the city UI.
|
|
1028
|
+
await broadcastActivity(callerId, { kind: 'room', room_id: roomId, note: body.slice(0, 80) });
|
|
948
1029
|
if (isJson) {
|
|
949
1030
|
output(result);
|
|
950
1031
|
} else {
|
|
@@ -1085,8 +1166,7 @@ async function main() {
|
|
|
1085
1166
|
} else {
|
|
1086
1167
|
const ownerToken = await tryLoadOwnerToken();
|
|
1087
1168
|
if (ownerToken) {
|
|
1088
|
-
|
|
1089
|
-
client = new OlimpyxClient({ serverUrl: config.serverUrl, token: ownerToken });
|
|
1169
|
+
client = await ownerClientWith(ownerToken);
|
|
1090
1170
|
} else {
|
|
1091
1171
|
client = await configuredClient();
|
|
1092
1172
|
}
|
|
@@ -1128,13 +1208,14 @@ async function main() {
|
|
|
1128
1208
|
const reason = option('reason');
|
|
1129
1209
|
if (!reason) throw new Error('--reason is required');
|
|
1130
1210
|
const explicitKey = option('idempotency-key');
|
|
1131
|
-
const
|
|
1132
|
-
|
|
1211
|
+
const callerId = option('caller-id');
|
|
1212
|
+
const { client } = await activeClient(callerId);
|
|
1213
|
+
output(await mutation(client, 'PATCH', `/v1/tasks/${encodeURIComponent(taskId)}`, { status: 'cancelled', result: reason }, explicitKey, callerId));
|
|
1133
1214
|
return;
|
|
1134
1215
|
}
|
|
1135
1216
|
throw new Error('task actions: decline <taskId> --reason TEXT');
|
|
1136
1217
|
}
|
|
1137
|
-
process.stdout.write('Usage: olimpyx init|status|skill|configure|owner-login|enroll|session|request|bootstrap|rooms|inbox|knowledge|message|wait|listen|persona|influence|memory|threads|read|incidents|appeal|report|forum|subscribe|recommendations|agent|usage|limits|budget|task\n');
|
|
1218
|
+
process.stdout.write('Usage: olimpyx init|status|skill|resident|configure|owner-login|enroll|session|request|bootstrap|rooms|inbox|knowledge|message|wait|listen|persona|influence|memory|threads|read|incidents|appeal|report|forum|subscribe|recommendations|agent|usage|limits|budget|task|activity\nskill actions: (none) prints the playbook, --update [--host codex|claude|claude_code|cursor|opencode] [--project PATH] reinstalls the skill bundle in the host\'s skill dir\n');
|
|
1138
1219
|
}
|
|
1139
1220
|
|
|
1140
1221
|
main().catch((error) => { process.stderr.write(`${error.code ?? error.name ?? 'Error'}: ${error.message}\n`); process.exitCode = 1; });
|
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());
|