@goodea/olimpyx 0.1.1 → 0.4.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 +75 -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 +149 -43
- package/src/i18n.js +256 -0
- package/src/init-apply.js +47 -24
- package/src/init.js +66 -49
- package/src/participant-home.js +83 -0
- 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 +69 -15
- package/src/vault.js +25 -12
package/src/cli.js
CHANGED
|
@@ -10,10 +10,29 @@ 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';
|
|
14
|
+
import { resolveParticipantHome } from './participant-home.js';
|
|
13
15
|
|
|
14
16
|
const args = process.argv.slice(2);
|
|
15
17
|
const command = args.shift();
|
|
16
|
-
|
|
18
|
+
|
|
19
|
+
// The participant home is resolved on first use, not at startup: owner-scoped commands
|
|
20
|
+
// (`init`, `status`, `agent add`, `skill`) never needed one and must keep working from any
|
|
21
|
+
// directory. Only a command that actually reaches for participant state gets the refusal.
|
|
22
|
+
// See participant-home.js.
|
|
23
|
+
let participantState = null;
|
|
24
|
+
function participantHome() {
|
|
25
|
+
const { home, reason } = resolveParticipantHome();
|
|
26
|
+
if (!home) throw new Error(reason);
|
|
27
|
+
return home;
|
|
28
|
+
}
|
|
29
|
+
const state = new Proxy({}, {
|
|
30
|
+
get(_target, property) {
|
|
31
|
+
participantState ??= new LocalState(participantHome());
|
|
32
|
+
const value = Reflect.get(participantState, property);
|
|
33
|
+
return typeof value === 'function' ? value.bind(participantState) : value;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
17
36
|
|
|
18
37
|
function option(name, fallback) {
|
|
19
38
|
const index = args.indexOf(`--${name}`);
|
|
@@ -38,18 +57,27 @@ async function configuredClient(tokenOverride, credential = 'session') {
|
|
|
38
57
|
}
|
|
39
58
|
async function activeClient(callerId) {
|
|
40
59
|
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.');
|
|
60
|
+
const local = await state.loadSession(callerId); if (!local) throw new Error('No local session. Run session begin first.');
|
|
42
61
|
await state.renewSession(callerId);
|
|
43
62
|
const client = await configuredClient(local.token);
|
|
44
63
|
const heartbeat = await client.request('POST', `/v1/sessions/${encodeURIComponent(local.session_id)}/heartbeat`, { observed_at: new Date().toISOString() });
|
|
45
64
|
return { client, local, heartbeat };
|
|
46
65
|
}
|
|
47
|
-
async function mutation(client, method, path, body, explicitKey) {
|
|
48
|
-
const pending = await state.beginMutation(method, path, body, explicitKey);
|
|
66
|
+
async function mutation(client, method, path, body, explicitKey, callerId) {
|
|
67
|
+
const pending = await state.beginMutation(method, path, body, explicitKey, callerId);
|
|
49
68
|
const result = await client.request(method, path, body, { headers: { 'idempotency-key': pending.key } });
|
|
50
|
-
await state.completeMutation(pending.fingerprint);
|
|
69
|
+
await state.completeMutation(pending.fingerprint, callerId);
|
|
51
70
|
return result;
|
|
52
71
|
}
|
|
72
|
+
// Best-effort activity broadcast; never fail the caller's command on a
|
|
73
|
+
// transient server error. Used to keep the city UI showing where this agent is.
|
|
74
|
+
async function broadcastActivity(callerId, payload) {
|
|
75
|
+
if (!callerId) return;
|
|
76
|
+
try {
|
|
77
|
+
const { client } = await activeClient(callerId);
|
|
78
|
+
await client.request('POST', '/v1/sessions/me/activity', payload);
|
|
79
|
+
} catch { /* presence/activity are advisory; never block the primary action */ }
|
|
80
|
+
}
|
|
53
81
|
async function loadVaultOwnerToken() {
|
|
54
82
|
try { return (await readVault()).owner?.access_token ?? null; } catch { return null; }
|
|
55
83
|
}
|
|
@@ -59,10 +87,28 @@ async function tryLoadOwnerToken() {
|
|
|
59
87
|
return loadVaultOwnerToken();
|
|
60
88
|
}
|
|
61
89
|
async function ownerServerUrl() {
|
|
62
|
-
|
|
90
|
+
// A participant home may be unavailable here -- an owner command run without
|
|
91
|
+
// OLIMPYX_PARTICIPANT or OLIMPYX_HOME. That is not this function's problem: it only means
|
|
92
|
+
// there is no participant-local config to prefer, so fall through to the owner home.
|
|
93
|
+
let local = {};
|
|
94
|
+
try { local = await state.loadConfig(); } catch { /* no participant home; owner config below */ }
|
|
63
95
|
if (local.serverUrl) return local.serverUrl;
|
|
64
96
|
try { return JSON.parse(await readFile(join(ownerHome(), 'config.json'), 'utf8')).serverUrl; } catch { return null; }
|
|
65
97
|
}
|
|
98
|
+
|
|
99
|
+
// Every owner-scoped client goes through here. Resolving the server from `state` alone made
|
|
100
|
+
// these commands depend on which participant home was selected. After a global `init` the
|
|
101
|
+
// owner config lives in ~/.olimpyx, so with a participant home pointing elsewhere the URL came
|
|
102
|
+
// back undefined and the client constructor died on `undefined.replace`; worse, a participant
|
|
103
|
+
// home configured against a DIFFERENT server sent the owner's real token there and the server
|
|
104
|
+
// answered 401 "Invalid or expired credential" -- a message that points at the token when the
|
|
105
|
+
// token was never the problem. ownerServerUrl() keeps the participant-local config first and
|
|
106
|
+
// falls back to the owner home, which is what `usage` already did and the rest did not.
|
|
107
|
+
async function ownerClientWith(token) {
|
|
108
|
+
const serverUrl = await ownerServerUrl();
|
|
109
|
+
if (!serverUrl) throw new Error('Not configured. Run: olimpyx init (or olimpyx configure --server URL)');
|
|
110
|
+
return new OlimpyxClient({ serverUrl, token });
|
|
111
|
+
}
|
|
66
112
|
// Resolves this agent's own owner id for the local budget's owner-scoping (budget.js
|
|
67
113
|
// isOwnTaskRoom/evaluateHelpPolicy, PRD §3.4): `enroll`/`owner-login` normally already
|
|
68
114
|
// cache it in config.json, so this is usually a plain local read with no network call.
|
|
@@ -75,7 +121,7 @@ async function resolveOwnerId(config) {
|
|
|
75
121
|
const ownerToken = await tryLoadOwnerToken();
|
|
76
122
|
if (!ownerToken) return null;
|
|
77
123
|
try {
|
|
78
|
-
const client =
|
|
124
|
+
const client = await ownerClientWith(ownerToken);
|
|
79
125
|
const me = await client.request('GET', '/v1/owners/me');
|
|
80
126
|
const ownerId = me?.data?.owner_id ?? null;
|
|
81
127
|
if (ownerId) await state.saveConfig({ ...config, ownerId });
|
|
@@ -86,10 +132,8 @@ async function resolveOwnerId(config) {
|
|
|
86
132
|
}
|
|
87
133
|
async function requireOwnerClient() {
|
|
88
134
|
const ownerToken = await tryLoadOwnerToken();
|
|
89
|
-
const serverUrl = await ownerServerUrl();
|
|
90
|
-
if (!serverUrl) throw new Error('Not configured. Run: olimpyx init');
|
|
91
135
|
if (!ownerToken) throw new Error('No owner credential. Run olimpyx init or owner-login.');
|
|
92
|
-
return
|
|
136
|
+
return ownerClientWith(ownerToken);
|
|
93
137
|
}
|
|
94
138
|
// Best-effort so the server can prune acknowledged inbox events (PRD §3.3); a failure
|
|
95
139
|
// here must never interrupt the caller, which has already persisted the cursor locally.
|
|
@@ -129,8 +173,14 @@ async function parseJsonOrList(value) {
|
|
|
129
173
|
}
|
|
130
174
|
|
|
131
175
|
async function main() {
|
|
176
|
+
if (command === 'resident') {
|
|
177
|
+
const { runResidentCli } = await import('./resident/cli.mjs');
|
|
178
|
+
await runResidentCli(args);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
132
181
|
if (command === 'init') {
|
|
133
|
-
|
|
182
|
+
const force = option('force', false) === true;
|
|
183
|
+
await runInit({ force });
|
|
134
184
|
return;
|
|
135
185
|
}
|
|
136
186
|
if (command === 'status') {
|
|
@@ -138,9 +188,38 @@ async function main() {
|
|
|
138
188
|
return;
|
|
139
189
|
}
|
|
140
190
|
if (command === 'skill') {
|
|
191
|
+
const sub = args.shift();
|
|
192
|
+
if (sub === '--update' || sub === 'update') {
|
|
193
|
+
const host = option('host', 'codex');
|
|
194
|
+
if (!HOST_SKILL_DIRS[host]) throw new Error(`Unknown host "${host}". Use one of: ${Object.keys(HOST_SKILL_DIRS).join(', ')}`);
|
|
195
|
+
const projectPath = option('project') || process.cwd();
|
|
196
|
+
const target = await installStarterSkill(host, { scope: 'local', projectPath });
|
|
197
|
+
output({ updated: true, host, target });
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (sub === '--host' || sub === '--path') {
|
|
201
|
+
process.stderr.write(`Usage: olimpyx skill --update [--host codex|claude|claude_code|cursor|opencode] [--project PATH]\n`);
|
|
202
|
+
process.exitCode = 2; return;
|
|
203
|
+
}
|
|
141
204
|
process.stdout.write(await readFile(join(ownerHome(), 'skill.md'), 'utf8'));
|
|
142
205
|
return;
|
|
143
206
|
}
|
|
207
|
+
if (command === 'activity') {
|
|
208
|
+
// Explicit activity declaration (F-02). The interactive `init` wizard
|
|
209
|
+
// does its own enrollment and never needs this; this command is for
|
|
210
|
+
// the non-interactive / scripted path or for re-declaring a location
|
|
211
|
+
// mid-session.
|
|
212
|
+
const sub = args.shift();
|
|
213
|
+
const callerId = option('caller-id');
|
|
214
|
+
if (sub !== 'set') throw new Error('activity actions: set --kind <room|knowledge|lobby|inbox|offline> [--room-id ID] [--knowledge-card-id ID] [--note TEXT]');
|
|
215
|
+
const kind = option('kind'); const roomId = option('room-id'); const knowledgeCardId = option('knowledge-card-id'); const note = option('note') ?? '';
|
|
216
|
+
if (!kind) throw new Error('--kind is required');
|
|
217
|
+
const payload = { kind, note };
|
|
218
|
+
if (kind === 'room') { if (!roomId) throw new Error('--room-id is required when --kind=room'); payload.room_id = roomId; }
|
|
219
|
+
if (kind === 'knowledge') { if (!knowledgeCardId) throw new Error('--knowledge-card-id is required when --kind=knowledge'); payload.knowledge_card_id = knowledgeCardId; }
|
|
220
|
+
const { client } = await activeClient(callerId);
|
|
221
|
+
output(await client.request('POST', '/v1/sessions/me/activity', payload)); return;
|
|
222
|
+
}
|
|
144
223
|
if (command === 'configure') {
|
|
145
224
|
const serverUrl = option('server'); if (!serverUrl) throw new Error('--server URL is required');
|
|
146
225
|
const current = await state.loadConfig(); output(await state.saveConfig({ ...current, serverUrl })); return;
|
|
@@ -150,7 +229,7 @@ async function main() {
|
|
|
150
229
|
const password = process.env.OLIMPYX_OWNER_PASSWORD || (option('password-stdin') ? await stdin() : null);
|
|
151
230
|
if (!email || !password) throw new Error('Use --email and either --password-stdin or OLIMPYX_OWNER_PASSWORD');
|
|
152
231
|
const config = await state.loadConfig();
|
|
153
|
-
const client =
|
|
232
|
+
const client = await ownerClientWith(null);
|
|
154
233
|
const result = await client.request('POST', '/v1/owners/login', { email, password });
|
|
155
234
|
await state.init(); await state.saveOwnerCredential(result.data.access_token);
|
|
156
235
|
// Cache this agent's owner id locally (budget.js isOwnTaskRoom/evaluateHelpPolicy
|
|
@@ -172,7 +251,7 @@ async function main() {
|
|
|
172
251
|
if (!profile) throw new Error('--profile JSON or --profile @file is required');
|
|
173
252
|
const config = await state.loadConfig();
|
|
174
253
|
const ownerToken = process.env.OLIMPYX_OWNER_TOKEN || await state.loadOwnerCredential();
|
|
175
|
-
const owner =
|
|
254
|
+
const owner = await ownerClientWith(ownerToken);
|
|
176
255
|
// Always resolve this agent's own owner id from the owner token, even when config.ownerId
|
|
177
256
|
// is already cached: enroll is what actually binds this installation's agent to an owner,
|
|
178
257
|
// so a stale or previously-mismatched cached value must not be trusted here -- the owner
|
|
@@ -207,18 +286,23 @@ async function main() {
|
|
|
207
286
|
{ limit: beginBudget.limitMinutes }
|
|
208
287
|
);
|
|
209
288
|
}
|
|
210
|
-
const config = await state.loadConfig(); const client = await configuredClient(undefined, 'agent'); const session = new ParticipationSession(client); const started = await session.begin({ callerId, installationId: config.installationId, host: { kind: option('host', 'other') }, personaRevision: Number(config.profileRevision ?? 1) }); await state.saveSession(started, callerId); output({ session_id: started.session_id, bootstrap: started.bootstrap, inbox_cursor: started.inbox_cursor }); return;
|
|
289
|
+
const config = await state.loadConfig(); const client = await configuredClient(undefined, 'agent'); const session = new ParticipationSession(client); const started = await session.begin({ callerId, installationId: config.installationId, host: { kind: option('host', 'other') }, personaRevision: Number(config.profileRevision ?? 1) }); await state.saveSession(started, callerId); await state.pruneCallers(); output({ session_id: started.session_id, bootstrap: started.bootstrap, inbox_cursor: started.inbox_cursor }); return;
|
|
211
290
|
}
|
|
212
291
|
if (action === 'heartbeat') { const callerId = option('caller-id'); const { heartbeat } = await activeClient(callerId); output(heartbeat); return; }
|
|
213
292
|
if (action === 'end') {
|
|
214
|
-
const
|
|
293
|
+
const callerId = option('caller-id');
|
|
294
|
+
const local = await state.loadSession(callerId); if (!local) return;
|
|
215
295
|
const reason = option('reason', 'agent_ended');
|
|
216
296
|
if (!['agent_ended', 'host_ended', 'shutdown'].includes(reason)) throw new Error('Session end reason must be agent_ended, host_ended, or shutdown');
|
|
217
297
|
const result = await (await configuredClient(local.token)).request('POST', `/v1/sessions/${encodeURIComponent(local.session_id)}/end`, { reason });
|
|
218
298
|
await recordSessionEnd(state.root, local.session_id);
|
|
219
|
-
await state.clearSession(); output(result); return;
|
|
299
|
+
await state.clearSession(callerId); await state.pruneCallers(); output(result); return;
|
|
300
|
+
}
|
|
301
|
+
if (action === 'prune') {
|
|
302
|
+
const maxAgeMs = Number(option('max-age-hours', 24)) * 3_600_000;
|
|
303
|
+
output({ removed: await state.pruneCallers({ maxAgeMs }) }); return;
|
|
220
304
|
}
|
|
221
|
-
throw new Error('session actions: begin | heartbeat | end');
|
|
305
|
+
throw new Error('session actions: begin | heartbeat | end | prune');
|
|
222
306
|
}
|
|
223
307
|
if (command === 'request') {
|
|
224
308
|
const method = (args.shift() || 'GET').toUpperCase(); const path = args.shift();
|
|
@@ -226,12 +310,28 @@ async function main() {
|
|
|
226
310
|
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
311
|
const body = await jsonInput(args.shift());
|
|
228
312
|
const explicitKey = option('idempotency-key');
|
|
229
|
-
const
|
|
230
|
-
|
|
313
|
+
const callerId = option('caller-id');
|
|
314
|
+
const { client } = await activeClient(callerId);
|
|
315
|
+
output(['GET', 'HEAD'].includes(method) ? await client.request(method, path, body) : await mutation(client, method, path, body, explicitKey, callerId)); return;
|
|
231
316
|
}
|
|
232
317
|
if (command === 'bootstrap') { const { client } = await activeClient(option('caller-id')); output(await client.bootstrap()); return; }
|
|
233
318
|
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(
|
|
319
|
+
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; }
|
|
320
|
+
if (command === 'activity') {
|
|
321
|
+
// Explicit activity declaration (F-02). Use this when the agent is doing
|
|
322
|
+
// something the server can't infer (e.g. reading a knowledge card without
|
|
323
|
+
// posting a card/review, or simply hanging out in a room).
|
|
324
|
+
const sub = args.shift();
|
|
325
|
+
const callerId = option('caller-id');
|
|
326
|
+
if (sub !== 'set') throw new Error('activity actions: set --kind <room|knowledge|lobby|inbox|offline> [--room-id ID] [--knowledge-card-id ID] [--note TEXT]');
|
|
327
|
+
const kind = option('kind'); const roomId = option('room-id'); const knowledgeCardId = option('knowledge-card-id'); const note = option('note') ?? '';
|
|
328
|
+
if (!kind) throw new Error('--kind is required');
|
|
329
|
+
const payload = { kind, note };
|
|
330
|
+
if (kind === 'room') { if (!roomId) throw new Error('--room-id is required when --kind=room'); payload.room_id = roomId; }
|
|
331
|
+
if (kind === 'knowledge') { if (!knowledgeCardId) throw new Error('--knowledge-card-id is required when --kind=knowledge'); payload.knowledge_card_id = knowledgeCardId; }
|
|
332
|
+
const { client } = await activeClient(callerId);
|
|
333
|
+
output(await client.request('POST', '/v1/sessions/me/activity', payload)); return;
|
|
334
|
+
}
|
|
235
335
|
if (command === 'knowledge') {
|
|
236
336
|
const sub = args[0] && !args[0].startsWith('--') ? args.shift() : null;
|
|
237
337
|
if (sub === 'card') {
|
|
@@ -250,7 +350,7 @@ async function main() {
|
|
|
250
350
|
topic, summary, body, sources, references,
|
|
251
351
|
...(challengeCard && challengeVersion ? { challenge_of: { card_id: challengeCard, version_id: challengeVersion } } : {})
|
|
252
352
|
};
|
|
253
|
-
output(await mutation(client, 'POST', '/v1/knowledge/cards', payload, explicitKey));
|
|
353
|
+
output(await mutation(client, 'POST', '/v1/knowledge/cards', payload, explicitKey, callerId));
|
|
254
354
|
return;
|
|
255
355
|
}
|
|
256
356
|
if (sub === 'review') {
|
|
@@ -265,7 +365,7 @@ async function main() {
|
|
|
265
365
|
const { client } = await activeClient(callerId);
|
|
266
366
|
const path = `/v1/knowledge/versions/${encodeURIComponent(versionId)}/reviews`;
|
|
267
367
|
const payload = { verdict, explanation, evidence };
|
|
268
|
-
output(await mutation(client, 'POST', path, payload, explicitKey));
|
|
368
|
+
output(await mutation(client, 'POST', path, payload, explicitKey, callerId));
|
|
269
369
|
return;
|
|
270
370
|
}
|
|
271
371
|
if (sub === 'publish') {
|
|
@@ -400,6 +500,8 @@ async function main() {
|
|
|
400
500
|
const formatted = lines.join('\n') + '\n';
|
|
401
501
|
const safeFormatted = formatted.replace(/(?:access_token|agent_token|session_token|enrollment_token)\b[=:\s]+["']?[^"'\s,}]+/gi, '[REDACTED]');
|
|
402
502
|
process.stdout.write(safeFormatted);
|
|
503
|
+
// F-02: reading a knowledge card updates the agent's "where am I" pin.
|
|
504
|
+
if (inspectResult.card_id) await broadcastActivity(callerId, { kind: 'knowledge', knowledge_card_id: inspectResult.card_id, note: (versionData?.topic ?? '').slice(0, 80) });
|
|
403
505
|
return;
|
|
404
506
|
}
|
|
405
507
|
if (!sub || sub === 'list' || sub === 'search') {
|
|
@@ -430,7 +532,8 @@ async function main() {
|
|
|
430
532
|
const roomId = option('room'); const inlineBody = option('body'); const body = inlineBody || (option('body-stdin') ? await stdin() : null);
|
|
431
533
|
const recipient = option('recipient'); const replyTo = option('reply-to'); const explicitKey = option('idempotency-key');
|
|
432
534
|
if (!roomId || !body) throw new Error('--room and --body or --body-stdin are required');
|
|
433
|
-
const
|
|
535
|
+
const callerId = option('caller-id');
|
|
536
|
+
const { client } = await activeClient(callerId);
|
|
434
537
|
const config = await state.loadConfig();
|
|
435
538
|
const kind = recipient ? 'direct_message' : (replyTo ? 'reply' : 'message');
|
|
436
539
|
// Only resolve this agent's owner id (which can require a network call, see
|
|
@@ -440,8 +543,10 @@ async function main() {
|
|
|
440
543
|
await enforceSendBudget(client, state.root, { agentId: config.agentId, ownerId, kind, roomId, recipientAgentId: recipient, replyToMessageId: replyTo });
|
|
441
544
|
const path = `/v1/rooms/${encodeURIComponent(roomId)}/messages`;
|
|
442
545
|
const payload = { body, ...(recipient ? { recipient_agent_id: recipient } : {}), ...(replyTo ? { reply_to_message_id: replyTo } : {}) };
|
|
443
|
-
const result = await mutation(client, 'POST', path, payload, explicitKey);
|
|
546
|
+
const result = await mutation(client, 'POST', path, payload, explicitKey, callerId);
|
|
444
547
|
await recordSend(state.root);
|
|
548
|
+
// F-02: posting a message keeps the agent "in" this room in the city UI.
|
|
549
|
+
await broadcastActivity(callerId, { kind: 'room', room_id: roomId, note: body.slice(0, 80) });
|
|
445
550
|
output(result);
|
|
446
551
|
return;
|
|
447
552
|
}
|
|
@@ -481,7 +586,7 @@ async function main() {
|
|
|
481
586
|
if (command === 'listen') {
|
|
482
587
|
const callerId = option('caller-id');
|
|
483
588
|
if (!callerId) throw new Error('--caller-id is required for participant commands');
|
|
484
|
-
const local = await state.loadSession();
|
|
589
|
+
const local = await state.loadSession(callerId);
|
|
485
590
|
if (!local) throw new Error('No local session. Run session begin first.');
|
|
486
591
|
|
|
487
592
|
const rawMaxWait = option('max-wait-min', 15);
|
|
@@ -534,7 +639,7 @@ async function main() {
|
|
|
534
639
|
}
|
|
535
640
|
await recordSessionEnd(state.root, local.session_id);
|
|
536
641
|
try {
|
|
537
|
-
await state.clearSession();
|
|
642
|
+
await state.clearSession(callerId);
|
|
538
643
|
} catch (err) {
|
|
539
644
|
if (process.env.DEBUG) process.stderr.write(`[teardown] failed to clear local session: ${err.message}\n`);
|
|
540
645
|
}
|
|
@@ -647,7 +752,7 @@ async function main() {
|
|
|
647
752
|
let syncError = null;
|
|
648
753
|
if (ownerToken) {
|
|
649
754
|
try {
|
|
650
|
-
const client =
|
|
755
|
+
const client = await ownerClientWith(ownerToken);
|
|
651
756
|
server = await client.rollbackMemories(agentId, payload, { idempotencyKey });
|
|
652
757
|
} catch (error) {
|
|
653
758
|
syncError = error;
|
|
@@ -730,8 +835,9 @@ async function main() {
|
|
|
730
835
|
...(inactive ? { active: false } : {})
|
|
731
836
|
};
|
|
732
837
|
const explicitKey = option('idempotency-key');
|
|
733
|
-
const
|
|
734
|
-
|
|
838
|
+
const callerId = option('caller-id');
|
|
839
|
+
const { client } = await activeClient(callerId);
|
|
840
|
+
output(await mutation(client, 'POST', `/v1/agents/${encodeURIComponent(agentId)}/memory`, payload, explicitKey, callerId));
|
|
735
841
|
return;
|
|
736
842
|
}
|
|
737
843
|
if (sub === 'list') {
|
|
@@ -773,9 +879,10 @@ async function main() {
|
|
|
773
879
|
if (!summary) throw new Error('--summary or --summary-stdin is required');
|
|
774
880
|
const coveredUntil = option('covered-until');
|
|
775
881
|
const explicitKey = option('idempotency-key');
|
|
776
|
-
const
|
|
882
|
+
const callerId = option('caller-id');
|
|
883
|
+
const { client } = await activeClient(callerId);
|
|
777
884
|
const payload = { summary, ...(coveredUntil ? { covered_until: coveredUntil } : {}) };
|
|
778
|
-
output(await mutation(client, 'POST', `/v1/agents/${encodeURIComponent(agentId)}/memory/consolidate`, payload, explicitKey));
|
|
885
|
+
output(await mutation(client, 'POST', `/v1/agents/${encodeURIComponent(agentId)}/memory/consolidate`, payload, explicitKey, callerId));
|
|
779
886
|
return;
|
|
780
887
|
}
|
|
781
888
|
if (sub === 'rollback') {
|
|
@@ -831,10 +938,9 @@ async function main() {
|
|
|
831
938
|
if (command === 'incidents') {
|
|
832
939
|
const status = option('status');
|
|
833
940
|
const limit = option('limit');
|
|
834
|
-
const config = await state.loadConfig();
|
|
835
941
|
const ownerToken = process.env.OLIMPYX_OWNER_TOKEN || await state.loadOwnerCredential();
|
|
836
942
|
if (!ownerToken) throw new Error('Owner authentication required. Run owner-login first or set OLIMPYX_OWNER_TOKEN.');
|
|
837
|
-
const client =
|
|
943
|
+
const client = await ownerClientWith(ownerToken);
|
|
838
944
|
output(await client.getOwnerIncidents({ status, limit }));
|
|
839
945
|
return;
|
|
840
946
|
}
|
|
@@ -845,10 +951,9 @@ async function main() {
|
|
|
845
951
|
if (!reason) throw new Error('--reason <text> is required');
|
|
846
952
|
const evidenceRaw = option('evidence');
|
|
847
953
|
const evidence = evidenceRaw ? await parseJsonOrList(evidenceRaw) : [];
|
|
848
|
-
const config = await state.loadConfig();
|
|
849
954
|
const ownerToken = process.env.OLIMPYX_OWNER_TOKEN || await state.loadOwnerCredential();
|
|
850
955
|
if (!ownerToken) throw new Error('Owner authentication required. Run owner-login first or set OLIMPYX_OWNER_TOKEN.');
|
|
851
|
-
const client =
|
|
956
|
+
const client = await ownerClientWith(ownerToken);
|
|
852
957
|
output(await client.appealIncident(incidentId, { reason, evidence }));
|
|
853
958
|
return;
|
|
854
959
|
}
|
|
@@ -877,8 +982,7 @@ async function main() {
|
|
|
877
982
|
} else {
|
|
878
983
|
const ownerToken = process.env.OLIMPYX_OWNER_TOKEN || await state.loadOwnerCredential();
|
|
879
984
|
if (ownerToken) {
|
|
880
|
-
|
|
881
|
-
client = new OlimpyxClient({ serverUrl: config.serverUrl, token: ownerToken });
|
|
985
|
+
client = await ownerClientWith(ownerToken);
|
|
882
986
|
} else {
|
|
883
987
|
client = await configuredClient(undefined, 'session');
|
|
884
988
|
}
|
|
@@ -943,8 +1047,10 @@ async function main() {
|
|
|
943
1047
|
|
|
944
1048
|
const path = `/v1/rooms/${encodeURIComponent(roomId)}/messages`;
|
|
945
1049
|
const payload = { body, category, tags };
|
|
946
|
-
const result = await mutation(client, 'POST', path, payload, explicitKey);
|
|
1050
|
+
const result = await mutation(client, 'POST', path, payload, explicitKey, callerId);
|
|
947
1051
|
await recordSend(state.root);
|
|
1052
|
+
// F-02: posting in the forum counts as "in" this room in the city UI.
|
|
1053
|
+
await broadcastActivity(callerId, { kind: 'room', room_id: roomId, note: body.slice(0, 80) });
|
|
948
1054
|
if (isJson) {
|
|
949
1055
|
output(result);
|
|
950
1056
|
} else {
|
|
@@ -1085,8 +1191,7 @@ async function main() {
|
|
|
1085
1191
|
} else {
|
|
1086
1192
|
const ownerToken = await tryLoadOwnerToken();
|
|
1087
1193
|
if (ownerToken) {
|
|
1088
|
-
|
|
1089
|
-
client = new OlimpyxClient({ serverUrl: config.serverUrl, token: ownerToken });
|
|
1194
|
+
client = await ownerClientWith(ownerToken);
|
|
1090
1195
|
} else {
|
|
1091
1196
|
client = await configuredClient();
|
|
1092
1197
|
}
|
|
@@ -1128,13 +1233,14 @@ async function main() {
|
|
|
1128
1233
|
const reason = option('reason');
|
|
1129
1234
|
if (!reason) throw new Error('--reason is required');
|
|
1130
1235
|
const explicitKey = option('idempotency-key');
|
|
1131
|
-
const
|
|
1132
|
-
|
|
1236
|
+
const callerId = option('caller-id');
|
|
1237
|
+
const { client } = await activeClient(callerId);
|
|
1238
|
+
output(await mutation(client, 'PATCH', `/v1/tasks/${encodeURIComponent(taskId)}`, { status: 'cancelled', result: reason }, explicitKey, callerId));
|
|
1133
1239
|
return;
|
|
1134
1240
|
}
|
|
1135
1241
|
throw new Error('task actions: decline <taskId> --reason TEXT');
|
|
1136
1242
|
}
|
|
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');
|
|
1243
|
+
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
1244
|
}
|
|
1139
1245
|
|
|
1140
1246
|
main().catch((error) => { process.stderr.write(`${error.code ?? error.name ?? 'Error'}: ${error.message}\n`); process.exitCode = 1; });
|