openzoo 0.50.52 → 0.50.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -40,6 +40,7 @@ import { encodeAvailableModels, encodeForMethod, encodeEnsureSandBox, encodeGetG
40
40
  import {
41
41
  accountPodPath, accountAgentsPath, rosterForAccount, rosterForEvent,
42
42
  readHouseRoster, houseAgentsPath, shapeAgent, agentBrief, briefFromName,
43
+ preferNamedAgent, looksLikeAgentId,
43
44
  readWakeups, writeWakeups, shapeWakeup, parseWakeupEvery, wantsWakeupCron,
44
45
  DEFAULT_WAKEUP_PROMPT, addDeletedIds, filterDeleted,
45
46
  } from './grokbotAccount.js';
@@ -365,7 +366,12 @@ function useHouseRoster() {
365
366
  function loadAgents() {
366
367
  const house = filterDeleted(readHouseRoster(HOME, activeAccountId) || [], HOME);
367
368
  const shaped = house.map(shapeAgent);
368
- const dirty = shaped.some((a, i) => a.brief && a.brief !== String(house[i]?.brief || house[i]?.description || ''));
369
+ const dirty = shaped.some((a, i) => {
370
+ const prev = house[i] || {};
371
+ return (a.brief && a.brief !== String(prev.brief || prev.description || ''))
372
+ || (a.name && a.name !== String(prev.name || prev.title || ''))
373
+ || looksLikeAgentId(prev.name, prev.id);
374
+ });
369
375
  if (dirty && shaped.length) {
370
376
  writeJsonFile(houseAgentsPath(HOME), shaped);
371
377
  if (activeAccountId) {
@@ -944,7 +950,7 @@ function bumpAgent(id, { preview = '', notify = true } = {}) {
944
950
  agentActivity.set(id, a);
945
951
  const list = cachedAgentList() || [];
946
952
  const idx = list.findIndex((x) => x.id === id);
947
- const base = idx >= 0 ? list[idx] : { id, name: id };
953
+ const base = idx >= 0 ? list[idx] : { id, name: 'chat' };
948
954
  const agent = shapeAgent(stampActivity({ ...base, updatedAt: now }));
949
955
  if (idx >= 0) list[idx] = agent;
950
956
  else list.unshift(agent);
@@ -1533,12 +1539,17 @@ async function runGroupQueue({ agentId, humanPrompt, parsed, nonce, log }) {
1533
1539
  }
1534
1540
  function mergeAgentLists(remote) {
1535
1541
  const local = cachedAgentList() || [];
1536
- const seen = new Set();
1542
+ const seen = new Map();
1537
1543
  const out = [];
1538
1544
  for (const a of [...local, ...(Array.isArray(remote) ? remote : [])]) {
1539
- if (!a?.id || seen.has(a.id)) continue;
1540
- seen.add(a.id);
1541
- out.push(stampActivity(a));
1545
+ if (!a?.id) continue;
1546
+ const idx = seen.get(a.id);
1547
+ if (idx == null) {
1548
+ seen.set(a.id, out.length);
1549
+ out.push(stampActivity(a));
1550
+ continue;
1551
+ }
1552
+ out[idx] = stampActivity(preferNamedAgent(out[idx], a));
1542
1553
  }
1543
1554
  return sortAgentsByActivity(filterDeleted(out, HOME));
1544
1555
  }
@@ -3113,7 +3124,7 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
3113
3124
  setHostSettings: { ok: true },
3114
3125
  setBoxSecrets: { ok: true },
3115
3126
  listAgents: rosterForEvent(cachedAgentList()
3116
- || [...new Set([...transcripts.keys(), ...tailedAgents])].map((id) => ({ id, name: id, status: 'ready' })), agentActivity),
3127
+ || [...new Set([...transcripts.keys(), ...tailedAgents])].map((id) => ({ id, name: 'chat', status: 'ready' })), agentActivity),
3117
3128
  countAgents: (cachedAgentList() || [...new Set([...transcripts.keys(), ...tailedAgents])]).length,
3118
3129
  searchAgents: rosterForEvent(cachedAgentList() || [], agentActivity),
3119
3130
  getAgentTranscriptTail: { tail: '', lines: [], dropped: false, ok: true },
@@ -41,16 +41,21 @@ function readJsonFile(p) {
41
41
  try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; }
42
42
  }
43
43
 
44
- /** First-seen id wins. Account file, then house file, then other account dirs. */
44
+ /** First-seen id wins. A later pile can only *upgrade* a UUID name/stub brief. */
45
45
  export function mergeAgentRecords(piles) {
46
- const seen = new Set();
46
+ const seen = new Map();
47
47
  const out = [];
48
48
  for (const pile of piles) {
49
49
  if (!Array.isArray(pile)) continue;
50
50
  for (const a of pile) {
51
- if (!a?.id || seen.has(a.id)) continue;
52
- seen.add(a.id);
53
- out.push(a);
51
+ if (!a?.id) continue;
52
+ const idx = seen.get(a.id);
53
+ if (idx == null) {
54
+ seen.set(a.id, out.length);
55
+ out.push(a);
56
+ continue;
57
+ }
58
+ out[idx] = preferNamedAgent(out[idx], a);
54
59
  }
55
60
  }
56
61
  return out;
@@ -111,10 +116,94 @@ function activityTs(agent, activity) {
111
116
  * nulls, then clears the whole persisted tray. That is how a group vanished
112
117
  * after the first send: bumpAgent wrote a partial row, restore returned null.
113
118
  */
119
+ const AGENT_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
120
+
121
+ /** bumpAgent used `{id, name:id}` and 1340 listAgents often echoes that — sidebar then paints UUIDs. */
122
+ export function looksLikeAgentId(s, id) {
123
+ const n = String(s || '').trim();
124
+ if (!n) return true;
125
+ if (id && n === String(id)) return true;
126
+ return AGENT_UUID_RE.test(n);
127
+ }
128
+
129
+ export function isStubBrief(brief, id) {
130
+ const b = String(brief || '').trim();
131
+ if (!b) return true;
132
+ if (id && b.startsWith(`You are ${id}. Your job is ${id}`)) return true;
133
+ return /^You are [0-9a-f-]{36}\. Your job is [0-9a-f-]{36}/i.test(b);
134
+ }
135
+
136
+ /** Pull a human name out of a standing brief: "You are 6 · Content Studio…" / "Job: Product Simplification". */
137
+ export function nameFromBrief(brief, id) {
138
+ const b = String(brief || '').replace(/^\[brief\]\s*/i, '').trim();
139
+ if (!b) return '';
140
+ const job = b.match(/\bJob:\s*([^\n.]+)/i);
141
+ if (job) {
142
+ const n = job[1].trim();
143
+ if (n && !looksLikeAgentId(n, id)) return n.slice(0, 80);
144
+ }
145
+ const numbered = b.match(/^You are\s+(\d+\s*[·.•.\-—–]+\s*[^\n.]+)/i);
146
+ if (numbered) {
147
+ const n = numbered[1].replace(/\s+for\s+.*$/i, '').trim();
148
+ if (n && !looksLikeAgentId(n, id)) return n.slice(0, 80);
149
+ }
150
+ const you = b.match(/^You are\s+(.+?)(?:\.\s|$)/i);
151
+ if (you) {
152
+ const n = you[1].replace(/\s+for\s+Stacc(?:'s)?(?:\s+LLC)?$/i, '').trim();
153
+ if (n && !looksLikeAgentId(n, id)) return n.slice(0, 80);
154
+ }
155
+ return '';
156
+ }
157
+
158
+ export function nameQuality(a) {
159
+ const n = String(a?.name || a?.title || '').trim();
160
+ if (!n || looksLikeAgentId(n, a?.id)) return 0;
161
+ if (/^(chat|group)$/i.test(n)) return 1;
162
+ if (/^new bot$/i.test(n)) return 2;
163
+ return 3;
164
+ }
165
+
166
+ /** Keep the record that still has a human name / real brief. */
167
+ export function preferNamedAgent(keep, incoming) {
168
+ if (!keep) return incoming;
169
+ if (!incoming) return keep;
170
+ const kq = nameQuality(keep);
171
+ const iq = nameQuality(incoming);
172
+ const keepBrief = String(keep.brief || keep.instructions || '').trim();
173
+ const inBrief = String(incoming.brief || incoming.instructions || '').trim();
174
+ const keepStub = isStubBrief(keepBrief, keep.id);
175
+ const inStub = isStubBrief(inBrief, incoming.id);
176
+ if (iq <= kq && !(keepStub && !inStub)) return keep;
177
+ const nameSrc = iq > kq ? incoming : keep;
178
+ const briefSrc = (!inStub && keepStub) ? incoming : keep;
179
+ return {
180
+ ...keep,
181
+ ...incoming,
182
+ name: nameSrc.name || nameSrc.title,
183
+ title: nameSrc.title || nameSrc.name,
184
+ brief: briefSrc.brief || briefSrc.instructions || keepBrief || inBrief,
185
+ };
186
+ }
187
+
188
+ export function displayName(raw = {}) {
189
+ const a = raw && typeof raw === 'object' ? raw : {};
190
+ const id = String(a.id || '');
191
+ const isGroup = a.isGroup === true
192
+ || (Array.isArray(a.memberIds) && a.memberIds.length > 0)
193
+ || (Array.isArray(a.memberAgentIds) && a.memberAgentIds.length > 0);
194
+ for (const cand of [a.name, a.title]) {
195
+ const n = String(cand || '').trim();
196
+ if (n && !looksLikeAgentId(n, id)) return n.slice(0, 80);
197
+ }
198
+ const fromBrief = nameFromBrief(a.brief || a.instructions || a.description, id);
199
+ if (fromBrief) return fromBrief;
200
+ return isGroup ? 'group' : 'chat';
201
+ }
202
+
114
203
  /** Standing job from a sidebar name like "6 · Content Studio" or "Bot 1 — Marketing (X)". */
115
204
  export function briefFromName(name) {
116
205
  const n = String(name || '').trim();
117
- if (!n || /^(new bot|chat|group)$/i.test(n)) return '';
206
+ if (!n || /^(new bot|chat|group)$/i.test(n) || looksLikeAgentId(n)) return '';
118
207
  const role = n.replace(/^\d+\s*[·.•.\-—–]+\s*/, '').trim() || n;
119
208
  if (role.length < 2) return '';
120
209
  return `You are ${n}. Your job is ${role}. Do that job. Do not ask the human to re-brief you. Coordinate with list_agents and message_agent.`;
@@ -123,13 +212,21 @@ export function briefFromName(name) {
123
212
  /** Standing job text. shapeAgent used to drop this, so every restart was amnesia. */
124
213
  export function agentBrief(raw = {}) {
125
214
  const a = raw && typeof raw === 'object' ? raw : {};
215
+ const id = String(a.id || '');
126
216
  for (const k of ['brief', 'instructions', 'customInstructions', 'systemPrompt']) {
127
217
  const v = a[k];
128
- if (typeof v === 'string' && v.trim()) return v.trim().slice(0, 8000);
218
+ if (typeof v === 'string' && v.trim() && !isStubBrief(v, id)) {
219
+ const name = displayName(a);
220
+ const text = v.trim();
221
+ if (id && name && !looksLikeAgentId(name, id) && text.startsWith(`You are ${id}`)) {
222
+ return (`You are ${name}` + text.slice(`You are ${id}`.length)).slice(0, 8000);
223
+ }
224
+ return text.slice(0, 8000);
225
+ }
129
226
  }
130
227
  const d = String(a.description || '').trim();
131
- if (d) return d.slice(0, 8000);
132
- return briefFromName(a.name || a.title).slice(0, 8000);
228
+ if (d && !isStubBrief(d, id)) return d.slice(0, 8000);
229
+ return briefFromName(displayName(a)).slice(0, 8000);
133
230
  }
134
231
 
135
232
  export function shapeAgent(raw = {}) {
@@ -139,14 +236,14 @@ export function shapeAgent(raw = {}) {
139
236
  ? a.memberIds.map((x) => String(x)).filter(Boolean)
140
237
  : (Array.isArray(a.memberAgentIds) ? a.memberAgentIds.map((x) => String(x)).filter(Boolean) : []);
141
238
  const isGroup = a.isGroup === true || memberIds.length > 0;
142
- const name = String(a.name || a.title || (isGroup ? 'group' : 'chat'));
143
- const brief = agentBrief(a);
239
+ const name = displayName({ ...a, isGroup, memberIds });
240
+ const brief = agentBrief({ ...a, name, title: a.title || name });
144
241
  return {
145
242
  id,
146
243
  name,
147
244
  brief,
148
- description: String(a.description || brief || ''),
149
- title: String(a.title || name),
245
+ description: String((a.description && !looksLikeAgentId(a.description, id) && a.description) || brief || ''),
246
+ title: String((!looksLikeAgentId(a.title, id) && a.title) || name),
150
247
  origin: String(a.origin || 'user'),
151
248
  path: String(a.path || (id ? `/local/${id}` : '/local')),
152
249
  createdAt: Number(a.createdAt) || Date.now(),
@@ -159,6 +159,7 @@ function ozEnsureSpendCss() {
159
159
  }
160
160
  s.textContent = [
161
161
  '.oz-spend{margin:.55rem 0 0;font-size:12px;color:inherit;opacity:.82;max-width:36em}',
162
+ '#oz-spend-float>summary{cursor:grab}',
162
163
  '.oz-spend>summary{cursor:help;list-style:none;display:inline-flex;align-items:center;gap:.35rem;',
163
164
  'padding:3px 9px;border-radius:999px;border:1px solid rgba(255,255,255,.18);white-space:nowrap;',
164
165
  'max-width:100%;overflow:hidden;text-overflow:ellipsis}',
@@ -339,27 +340,78 @@ function ozEnsureFloatSpend() {
339
340
  document.body.appendChild(el);
340
341
  }
341
342
  ozPlaceFloat(el);
343
+ ozDragFloat(el);
342
344
  const sum = el.querySelector('summary');
343
345
  if (sum) sum.textContent = 'ⓘ ' + label;
344
346
  }
345
347
 
348
+ function ozSavedFloatPos() {
349
+ try {
350
+ const p = JSON.parse(localStorage.getItem('oz-spend-float-pos') || 'null');
351
+ if (p && Number.isFinite(+p.left) && Number.isFinite(+p.top)) return { left: +p.left, top: +p.top };
352
+ } catch (e) {}
353
+ return null;
354
+ }
355
+
346
356
  function ozPlaceFloat(el) {
347
357
  if (!el) return;
348
- const ta = document.querySelector('textarea, [contenteditable="true"]');
349
- const box = ta && ta.getBoundingClientRect();
350
358
  el.style.position = 'fixed';
351
- el.style.right = 'auto';
352
359
  el.style.zIndex = '2147483646';
353
360
  el.style.opacity = '0.95';
354
- if (box && box.width > 80 && box.top > 48) {
355
- el.style.left = Math.round(box.left) + 'px';
356
- el.style.top = Math.round(Math.max(8, box.top - 56)) + 'px';
361
+ const saved = ozSavedFloatPos();
362
+ if (saved) {
363
+ el.style.left = Math.max(8, saved.left) + 'px';
364
+ el.style.top = Math.max(8, saved.top) + 'px';
365
+ el.style.right = 'auto';
357
366
  el.style.bottom = 'auto';
358
- } else {
359
- el.style.left = '96px';
360
- el.style.bottom = '72px';
361
- el.style.top = 'auto';
367
+ return;
362
368
  }
369
+ el.style.left = 'auto';
370
+ el.style.right = '16px';
371
+ el.style.top = '48px';
372
+ el.style.bottom = 'auto';
373
+ }
374
+
375
+ function ozDragFloat(el) {
376
+ if (!el || el.dataset.ozDrag === '1') return;
377
+ el.dataset.ozDrag = '1';
378
+ const sum = el.querySelector('summary') || el;
379
+ let dragging = false;
380
+ let moved = false;
381
+ let dx = 0;
382
+ let dy = 0;
383
+ sum.addEventListener('pointerdown', (ev) => {
384
+ if (ev.button !== 0) return;
385
+ dragging = true;
386
+ moved = false;
387
+ const r = el.getBoundingClientRect();
388
+ dx = ev.clientX - r.left;
389
+ dy = ev.clientY - r.top;
390
+ el.style.cursor = 'grabbing';
391
+ try { sum.setPointerCapture(ev.pointerId); } catch (e) {}
392
+ ev.preventDefault();
393
+ ev.stopPropagation();
394
+ });
395
+ sum.addEventListener('pointermove', (ev) => {
396
+ if (!dragging) return;
397
+ moved = true;
398
+ el.style.left = Math.max(8, ev.clientX - dx) + 'px';
399
+ el.style.top = Math.max(8, ev.clientY - dy) + 'px';
400
+ el.style.right = 'auto';
401
+ el.style.bottom = 'auto';
402
+ });
403
+ const end = () => {
404
+ if (!dragging) return;
405
+ dragging = false;
406
+ el.style.cursor = 'grab';
407
+ const r = el.getBoundingClientRect();
408
+ try { localStorage.setItem('oz-spend-float-pos', JSON.stringify({ left: r.left, top: r.top })); } catch (e) {}
409
+ };
410
+ sum.addEventListener('pointerup', end);
411
+ sum.addEventListener('pointercancel', end);
412
+ sum.addEventListener('click', (ev) => {
413
+ if (moved) { ev.preventDefault(); ev.stopPropagation(); }
414
+ }, true);
363
415
  }
364
416
 
365
417
  function ozWatchSpend() {
@@ -385,20 +437,8 @@ export function spendChipSource() {
385
437
  return [
386
438
  '(function ozSpendChip(){',
387
439
  "'use strict';",
388
- 'if (window.__OZ_SPEND_CHIP__ === 11) {',
389
- ' const el = document.getElementById("oz-spend-float");',
390
- ' const ta = document.querySelector("textarea, [contenteditable=\\"true\\"]");',
391
- ' const box = ta && ta.getBoundingClientRect();',
392
- ' if (el && box && box.width > 80) {',
393
- ' el.style.position = "fixed";',
394
- ' el.style.left = Math.round(box.left) + "px";',
395
- ' el.style.top = Math.round(Math.max(8, box.top - 56)) + "px";',
396
- ' el.style.right = "auto";',
397
- ' el.style.bottom = "auto";',
398
- ' }',
399
- ' return;',
400
- '}',
401
- 'window.__OZ_SPEND_CHIP__ = 11;',
440
+ 'if (window.__OZ_SPEND_CHIP__ === 12) return;',
441
+ 'window.__OZ_SPEND_CHIP__ = 12;',
402
442
  chipUsd.toString(),
403
443
  labelFromSpendBody.toString(),
404
444
  spendLinesOnly.toString(),
@@ -414,7 +454,9 @@ export function spendChipSource() {
414
454
  ozAttachSpendChip.toString(),
415
455
  ozCollapseSpend.toString(),
416
456
  ozEnsureFloatSpend.toString(),
457
+ ozSavedFloatPos.toString(),
417
458
  ozPlaceFloat.toString(),
459
+ ozDragFloat.toString(),
418
460
  ozWatchSpend.toString(),
419
461
  'ozWatchSpend();',
420
462
  '})();',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.50.52",
3
+ "version": "0.50.54",
4
4
  "description": "Local x402-paying proxy + MCP server for openzoo.fun — point any OpenAI-compatible harness (Cursor, Claude Code, aider, SDKs) at localhost and it pays per call from a local burner wallet. Solana and Base rails live; Robinhood experimental.",
5
5
  "license": "MIT",
6
6
  "type": "module",