infinicode 2.8.102 → 2.8.104

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.
@@ -26,7 +26,7 @@ export const DASHBOARD_HTML = `<!doctype html>
26
26
  <head>
27
27
  <meta charset="utf-8">
28
28
  <meta name="viewport" content="width=device-width, initial-scale=1">
29
- <meta name="robopark-ui-build" content="voice-media-20260716-3">
29
+ <meta name="robopark-ui-build" content="provisioning-token-20260717-1">
30
30
  <title>RoboPark Control</title>
31
31
  <style>
32
32
  /* ── premium tech glass theme ── dark is the committed default; light is a frosted toggle ── */
@@ -363,20 +363,24 @@ button.act:disabled{opacity:0.5;cursor:default;}
363
363
  .pk-ctl{display:flex;gap:0.45rem;flex-wrap:wrap;}
364
364
  .pk-note{font-family:var(--mono);font-size:0.58rem;color:var(--gold);opacity:0.85;margin-top:0.4rem;}
365
365
  .pk-pipeline{display:grid;gap:0.42rem;margin-top:0.7rem;position:relative;}
366
+ .pk-live-stage-focus{--stage-color:var(--gold-bright);position:relative;overflow:hidden;margin-top:.58rem;padding:.78rem .85rem .78rem 1rem;border:1px solid var(--stage-color);border-radius:13px;background:linear-gradient(105deg,color-mix(in srgb,var(--stage-color) 24%,transparent),rgba(8,8,8,.68));box-shadow:0 10px 30px rgba(0,0,0,.28),inset 4px 0 0 var(--stage-color);}
367
+ .pk-live-stage-focus:before{content:'';position:absolute;width:150px;height:150px;right:-65px;top:-95px;border-radius:50%;background:var(--stage-color);opacity:.12;filter:blur(5px);animation:pulse 1.3s infinite;}.pk-live-stage-focus .eyebrow{display:block;font-family:var(--mono);font-size:.49rem;letter-spacing:.13em;text-transform:uppercase;color:var(--stage-color);}.pk-live-stage-focus b{display:block;margin-top:.2rem;font-size:1rem;letter-spacing:.015em;color:var(--ink);}.pk-live-stage-focus span:last-child{display:block;margin-top:.18rem;font-family:var(--mono);font-size:.57rem;color:var(--muted);}.pk-live-stage-focus.blocked,.pk-live-stage-focus.failed{--stage-color:var(--bad)}.pk-live-stage-focus.waiting,.pk-live-stage-focus.paused{--stage-color:var(--muted)}.pk-live-stage-focus.active,.pk-live-stage-focus.running{--stage-color:var(--warn)}
366
368
  .pk-pipe-step{--phase:var(--gold);display:grid;grid-template-columns:2rem minmax(7.2rem,.72fr) minmax(9rem,1.35fr);gap:0.55rem;align-items:center;padding:0.55rem 0.62rem;border-radius:11px;border:1px solid rgba(255,255,255,.06);background:linear-gradient(110deg,color-mix(in srgb,var(--phase) 7%,transparent),rgba(255,255,255,.018));font-family:var(--mono);font-size:0.58rem;color:var(--muted);transition:transform .18s,border-color .18s,background .18s;position:relative;overflow:hidden;}
367
369
  .pk-pipe-step.phase-hardware{--phase:#63d5c6}.pk-pipe-step.phase-trigger{--phase:#efb94c}.pk-pipe-step.phase-session{--phase:#66a8ff}.pk-pipe-step.phase-media{--phase:#68d48f}.pk-pipe-step.phase-intelligence{--phase:#e38bff}.pk-pipe-step.phase-playback{--phase:#ff9b62}.pk-pipe-step.phase-close{--phase:#a8b0bd}
368
370
  .pk-pipe-step .step-index{display:grid;place-items:center;width:1.65rem;height:1.65rem;border-radius:8px;border:1px solid color-mix(in srgb,var(--phase) 35%,transparent);color:var(--phase);background:color-mix(in srgb,var(--phase) 10%,transparent);font-weight:700;}
369
371
  .pk-pipe-step .stage{color:var(--text);text-transform:uppercase;letter-spacing:.055em;font-weight:650;display:block;}.pk-pipe-step .phase{display:block;color:var(--phase);font-size:.49rem;letter-spacing:.08em;margin-top:.14rem;}
370
- .pk-pipe-step .detail{text-align:right;overflow-wrap:anywhere;line-height:1.35;}.pk-pipe-step .time{display:block;color:color-mix(in srgb,var(--muted) 75%,transparent);font-size:.5rem;margin-top:.12rem;}
372
+ .pk-pipe-step .detail{text-align:right;overflow-wrap:anywhere;line-height:1.35;}.pk-pipe-step .time{display:block;color:color-mix(in srgb,var(--muted) 75%,transparent);font-size:.5rem;margin-top:.12rem;}.pk-pipe-step .step-state{display:inline-block;margin-left:.35rem;padding:.1rem .3rem;border:1px solid currentColor;border-radius:999px;font-size:.43rem;letter-spacing:.07em;vertical-align:1px;color:var(--muted)}
371
373
  .pk-pipe-step.ok{border-color:color-mix(in srgb,var(--ok) 22%,transparent)}
374
+ .pk-pipe-step.ok .step-state{color:var(--ok)}.pk-pipe-step.pending{opacity:.52}.pk-pipe-step.pending .step-index{filter:saturate(.3)}
372
375
  .pk-pipe-step.failed,.pk-pipe-step.blocked{border-color:rgba(255,90,90,.48);background:linear-gradient(110deg,rgba(255,70,70,.14),rgba(255,255,255,.02));}
373
- .pk-pipe-step.current{transform:translateX(4px);border-color:var(--phase);background:linear-gradient(105deg,color-mix(in srgb,var(--phase) 19%,transparent),rgba(255,255,255,.035));box-shadow:0 0 0 1px color-mix(in srgb,var(--phase) 18%,transparent),0 8px 28px rgba(0,0,0,.22);}
376
+ .pk-pipe-step.current{transform:translateX(5px) scale(1.012);border-color:var(--phase);background:linear-gradient(105deg,color-mix(in srgb,var(--phase) 24%,transparent),rgba(255,255,255,.045));box-shadow:0 0 0 1px color-mix(in srgb,var(--phase) 28%,transparent),0 10px 32px rgba(0,0,0,.3);opacity:1;z-index:2;}
374
377
  .pk-pipe-step.current:after{content:'ACTIVE';position:absolute;right:-1.7rem;top:.45rem;transform:rotate(38deg);padding:.12rem 2rem;background:var(--phase);color:#080808;font-size:.42rem;font-weight:800;letter-spacing:.08em;}
375
378
  .pk-pipe-step.current .step-index{box-shadow:0 0 14px color-mix(in srgb,var(--phase) 55%,transparent);animation:pulse 1.15s infinite;}
376
379
  .pk-pipeline-summary{padding:0.7rem 0.75rem;border:1px solid var(--glass-border);border-radius:11px;background:linear-gradient(120deg,rgba(212,170,72,.08),rgba(255,255,255,.018));font-family:var(--mono);font-size:0.62rem;color:var(--muted);line-height:1.45;}
377
380
  .pk-pipeline-summary.ready{color:var(--ok);border-color:rgba(70,220,150,.35);}
378
381
  .pk-pipeline-summary.running{color:var(--warn);border-color:rgba(255,196,70,.35);}
379
382
  .pk-pipeline-summary.blocked{color:var(--bad);border-color:rgba(255,90,90,.4);}
383
+ .fleet-stage-focus{margin-top:.48rem;padding:.48rem .58rem;border:1px solid color-mix(in srgb,var(--warn) 48%,transparent);border-radius:9px;background:linear-gradient(100deg,rgba(255,184,62,.14),rgba(255,255,255,.02));font-family:var(--mono)}.fleet-stage-focus span{display:block;color:var(--warn);font-size:.45rem;letter-spacing:.1em;text-transform:uppercase}.fleet-stage-focus b{display:block;margin-top:.12rem;color:var(--ink);font-size:.66rem}.fleet-stage-focus.blocked,.fleet-stage-focus.failed{border-color:color-mix(in srgb,var(--bad) 55%,transparent)}.fleet-stage-focus.blocked span,.fleet-stage-focus.failed span{color:var(--bad)}.fleet-stage-focus.waiting,.fleet-stage-focus.paused{border-color:var(--glass-border)}.fleet-stage-focus.waiting span,.fleet-stage-focus.paused span{color:var(--muted)}
380
384
  .pk-pipe-history{margin-top:.75rem;border-top:1px solid var(--glass-border);padding-top:.7rem}.pk-pipe-history-head{display:flex;align-items:center;justify-content:space-between;gap:.6rem;margin-bottom:.5rem}.pk-pipe-history-head b{font-family:var(--mono);font-size:.65rem;color:var(--ink);text-transform:uppercase;letter-spacing:.06em}.pk-pipe-history-list{display:grid;gap:.45rem}.pk-run-card{border:1px solid var(--glass-border);border-radius:10px;background:var(--panel-2);padding:.55rem .62rem;cursor:pointer}.pk-run-card:hover{border-color:var(--gold)}.pk-run-top{display:grid;grid-template-columns:1fr auto;gap:.5rem;align-items:start}.pk-run-title{font-family:var(--mono);font-size:.61rem;color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pk-run-meta{font-family:var(--mono);font-size:.53rem;color:var(--muted);margin-top:.17rem}.pk-run-state{font-family:var(--mono);font-size:.5rem;text-transform:uppercase;border:1px solid var(--glass-border);border-radius:999px;padding:.18rem .4rem}.pk-run-state.ok{color:var(--ok);border-color:color-mix(in srgb,var(--ok) 45%,transparent)}.pk-run-state.bad{color:var(--bad);border-color:color-mix(in srgb,var(--bad) 45%,transparent)}.pk-run-state.running{color:var(--warn);border-color:color-mix(in srgb,var(--warn) 45%,transparent)}.pk-run-stages{display:flex;gap:3px;margin-top:.45rem}.pk-run-stages i{height:4px;flex:1;border-radius:3px;background:rgba(255,255,255,.07)}.pk-run-stages i.ok{background:var(--ok)}.pk-run-stages i.failed,.pk-run-stages i.blocked{background:var(--bad)}.pk-history-pager{display:flex;align-items:center;gap:.35rem}.pk-history-pager span{font-family:var(--mono);font-size:.54rem;color:var(--muted)}
381
385
  .pk-mic-suite{margin-top:.6rem;display:grid;gap:.55rem}.pk-mic-health{display:grid;grid-template-columns:repeat(3,1fr);gap:.4rem}.pk-mic-check{padding:.48rem;border:1px solid var(--glass-border);border-radius:9px;background:var(--panel-2)}.pk-mic-check .k{font-family:var(--mono);font-size:.49rem;color:var(--muted);text-transform:uppercase}.pk-mic-check .v{font-family:var(--mono);font-size:.62rem;color:var(--ink);margin-top:.16rem;overflow-wrap:anywhere}.pk-mic-check.ok{border-color:color-mix(in srgb,var(--ok) 30%,transparent)}.pk-mic-check.bad{border-color:color-mix(in srgb,var(--bad) 45%,transparent)}.pk-mic-result{border:1px solid var(--glass-border);border-radius:10px;padding:.58rem;background:var(--panel-2);font-family:var(--mono);font-size:.58rem}.pk-mic-result.ok{border-color:var(--ok);background:rgba(60,180,110,.08)}.pk-mic-result.bad{border-color:var(--bad);background:rgba(220,80,80,.08)}.pk-mic-readings{display:grid;grid-template-columns:repeat(4,1fr);gap:.35rem;margin-top:.45rem}.pk-mic-reading{padding:.35rem;background:rgba(0,0,0,.16);border-radius:7px}.pk-mic-reading span{display:block;color:var(--muted);font-size:.48rem;text-transform:uppercase}.pk-mic-reading b{display:block;color:var(--ink);font-size:.67rem;margin-top:.1rem}
382
386
  .pk-motor-registry{display:grid;gap:.4rem}.pk-motor-row{display:grid;grid-template-columns:1fr 4.5rem 5.5rem auto;gap:.35rem;align-items:end;padding:.45rem;border:1px solid var(--glass-border);border-radius:9px;background:var(--panel-2)}.pk-motor-canvas{display:flex;align-items:stretch;gap:.55rem;overflow-x:auto;padding:.65rem .2rem;min-height:130px}.pk-motor-node{min-width:165px;padding:.55rem;border:1px solid rgba(255,205,93,.35);border-radius:12px;background:linear-gradient(145deg,rgba(201,150,46,.13),rgba(255,255,255,.025));position:relative}.pk-motor-node:not(:last-child):after{content:'';position:absolute;right:-.56rem;top:50%;width:.56rem;border-top:2px solid var(--gold)}.pk-motor-node .node-num{font-family:var(--mono);font-size:.48rem;color:var(--gold);text-transform:uppercase;margin-bottom:.35rem}.pk-motor-node .rp-field{margin-bottom:.3rem}.pk-motor-empty{display:grid;place-items:center;width:100%;min-height:100px;border:1px dashed var(--glass-border);border-radius:10px;color:var(--muted);font-family:var(--mono);font-size:.58rem}.pk-motor-status{font-family:var(--mono);font-size:.58rem;padding:.5rem;border-radius:8px;background:var(--panel-2);border:1px solid var(--glass-border)}.pk-motor-status.ok{color:var(--ok);border-color:var(--ok)}.pk-motor-status.bad{color:var(--bad);border-color:var(--bad)}
@@ -1126,18 +1130,21 @@ button.act:disabled{opacity:0.5;cursor:default;}
1126
1130
  <div class="rp-form" id="rp-addrobot">
1127
1131
  <h3>Add robot (mint enrollment token)</h3>
1128
1132
  <div class="rp-quickrow">
1129
- <div class="rp-field"><label>Robot name</label><input id="rb-name" placeholder="jaguar"></div>
1130
- <div class="rp-field"><label>Character preset</label><select id="rb-char"><option value="">— auto (matched from name) —</option></select></div>
1131
- <button class="rp-btn primary" id="rb-add" style="height:fit-content">Add robot</button>
1133
+ <div class="rp-field"><label>Robot name</label><input id="rb-name" placeholder="jaguar"></div>
1134
+ <div class="rp-field"><label>Character preset</label><select id="rb-char"><option value="">— auto (matched from name) —</option></select></div>
1135
+ <div class="rp-field"><label>Robot network</label><select id="rb-network"><option value="lan">LAN (onsite default)</option><option value="tailscale">Tailscale</option></select></div>
1136
+ <div class="rp-field"><label>Mesh token</label><input id="rb-token" spellcheck="false" autocomplete="off" placeholder="required in generated command"></div>
1137
+ <button class="rp-btn primary" id="rb-add" style="height:fit-content">Add robot</button>
1132
1138
  </div>
1133
1139
  <div class="rp-autonote" id="rb-autonote">the robot self-reports its IP and hardware on first heartbeat — you only need a name and (optionally) a character preset to get started.</div>
1134
1140
  <div class="rp-quickrow" id="rb-clicmd-row" style="display:none;">
1135
- <div class="rp-field" style="flex:1;"><label>run this ON THE ROBOT (no need to click Add — this mints its own token)</label><input id="rb-clicmd" readonly style="cursor:text;"></div>
1141
+ <div class="rp-field" style="flex:1;"><label>complete production one-liner — run this ON THE ROBOT</label><input id="rb-clicmd" readonly style="cursor:text;"></div>
1136
1142
  <button class="rp-btn" id="rb-clicmd-copy" style="height:fit-content">📋 Copy</button>
1137
1143
  </div>
1138
1144
  <details class="rp-advanced">
1139
- <summary>Advanced — pre-set network/voice config before the robot connects</summary>
1140
- <div class="rp-advanced-grid">
1145
+ <summary>Advanced — pre-set network/voice config before the robot connects</summary>
1146
+ <div class="rp-advanced-grid">
1147
+ <div class="rp-field"><label>Site</label><input id="rb-site" placeholder="Tel Aviv"></div>
1141
1148
  <div class="rp-field"><label>Tailscale IP</label><input id="rb-tsip" placeholder="auto-detected once connected"></div>
1142
1149
  <div class="rp-field"><label>LAN IP</label><input id="rb-lanip" placeholder="auto-detected once connected"></div>
1143
1150
  <div class="rp-field"><label>Motor server URL</label><input id="rb-motor" placeholder="http://...:5000"></div>
@@ -1168,7 +1175,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
1168
1175
  <div class="rp-config-hero-actions"><button class="rp-btn" id="rp-command-reset">Reset environment</button><button class="rp-btn primary" id="rp-command-copy-all">Copy visible commands</button></div>
1169
1176
  </div>
1170
1177
  <div class="rp-command-env">
1171
- <div class="rp-field"><label>Hub LAN IP</label><input id="rp-command-lan" value="192.168.1.12" spellcheck="false"></div>
1178
+ <div class="rp-field"><label>Hub LAN IP</label><input id="rp-command-lan" value="192.168.0.9" spellcheck="false"></div>
1172
1179
  <div class="rp-field"><label>Hub Tailscale IP / DNS</label><input id="rp-command-tail" value="100.84.147.24" spellcheck="false"></div>
1173
1180
  <div class="rp-field"><label>Site</label><input id="rp-command-site" value="Tel Aviv"></div>
1174
1181
  <div class="rp-field"><label>Robot name</label><input id="rp-command-robot" value="bmw" spellcheck="false"></div>
@@ -1241,6 +1248,10 @@ button.act:disabled{opacity:0.5;cursor:default;}
1241
1248
  (function(){
1242
1249
  var TOKEN = new URLSearchParams(location.search).get('token') || '';
1243
1250
  var QS = TOKEN ? ('?token=' + encodeURIComponent(TOKEN)) : '';
1251
+ function dashboardMeshToken(){
1252
+ if(TOKEN)return TOKEN;
1253
+ try{var saved=JSON.parse(localStorage.getItem('rp-command-env-v1')||'null');return saved&&String(saved.token||'').trim()||'';}catch(e){return '';}
1254
+ }
1244
1255
  var UI_BUILD=(document.querySelector('meta[name="robopark-ui-build"]')||{}).content||'unknown';
1245
1256
  fetch('/fed/ui-build'+QS,{cache:'no-store'}).then(function(r){return r.ok?r.json():null;}).then(function(info){
1246
1257
  if(!info||!info.build||info.build===UI_BUILD)return;
@@ -1532,6 +1543,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
1532
1543
  + '<div class="meta">'+esc(n.platform||'')+' '+esc((hw&&hw.cpu&&hw.cpu.model)?hw.cpu.model.replace(/\\s+/g,' ').slice(0,22):'')+site+' <span class="mono">v'+esc(n.version||'?')+'</span></div>'
1533
1544
  + (avatar?('<div class="node-hero"><img class="node-avatar" alt="'+esc(n.displayName)+'" src="'+esc(avatar+QS)+'"><div class="node-health"><b>'+esc(String(readiness).replace(/_/g,' '))+'</b><br>'
1534
1545
  +(tele?('sessions '+esc(tele.sessions_total||0)+' / triggers '+esc(tele.trigger_count||0)+'<br>latency '+esc(tele.avg_latency_ms!=null?Math.round(tele.avg_latency_ms)+'ms':'n/a')+' / drops '+esc(tele.drop_rate!=null?Math.round(tele.drop_rate*100)+'%':'n/a')+'<br>heartbeat '+esc(hb!=null?hb+'s ago':'unavailable')):'scheduler telemetry pending')+'</div></div>'):'');
1546
+ if(tele){var fleetStage=tele.current_stage||{},fleetStageStatus=fleetStage.status||tele.current_stage_status||'waiting',fleetStageLabel=fleetStage.label||tele.current_stage_label||'Waiting for pipeline telemetry';html+='<div class="fleet-stage-focus '+esc(fleetStageStatus)+'"><span>Live production stage</span><b>'+esc(fleetStageLabel)+'</b></div>';}
1535
1547
 
1536
1548
  if((tele||String(n.role||'').toLowerCase()==='satellite'||String(n.role||'').toLowerCase()==='robot')&&cameraRef){
1537
1549
  html += '<div class="fleet-live-camera" data-fleet-camera-host="'+esc(cameraRef)+'"><img alt="Live camera for '+esc(n.displayName)+'" data-fleet-camera="'+esc(cameraRef)+'"><span class="state">'+(up?'connecting camera':'robot offline')+'</span></div>';
@@ -2248,16 +2260,21 @@ button.act:disabled{opacity:0.5;cursor:default;}
2248
2260
 
2249
2261
  // ── robust CLI one-liner generation, live as you type — no API call
2250
2262
  // needed to see it; 'robopark add-robot' mints its own token when run ──
2251
- function buildAddRobotCommand(name, characterId){
2252
- if(!name) return '';
2253
- var hubUrl=location.origin;
2254
- var schedulerUrl=(lastStatus && lastStatus.control && lastStatus.control.schedulerUrl) || '';
2255
- var argv=['robopark','add-robot',name];
2256
- if(characterId) argv.push('--character', characterId);
2257
- if(hubUrl) argv.push('--hub-url', hubUrl);
2258
- if(TOKEN) argv.push('--token', TOKEN);
2259
- if(schedulerUrl) argv.push('--scheduler-url', schedulerUrl);
2260
- argv.push('--start');
2263
+ function buildAddRobotCommand(name, characterId){
2264
+ if(!name) return '';
2265
+ var network=$('rb-network')&&$('rb-network').value||'lan';
2266
+ var commandEnv=rpCommandEnv();
2267
+ var hubHost=network==='tailscale'?commandEnv.tail:commandEnv.lan;
2268
+ var hubUrl=/^https?:\/\//i.test(hubHost)?hubHost.replace(/\\/+$/,''):'http://'+hubHost.replace(/\\/+$/,'')+':47913';
2269
+ var schedulerUrl=hubUrl+'/robopark';
2270
+ var argv=['robopark','add-robot',name];
2271
+ if(characterId) argv.push('--character', characterId);
2272
+ var site=$('rb-site')&&$('rb-site').value.trim();if(site)argv.push('--site',site);
2273
+ if(hubUrl) argv.push('--hub-url', hubUrl);
2274
+ var meshToken=$('rb-token')&&$('rb-token').value.trim()||dashboardMeshToken()||'<mesh-token>';
2275
+ argv.push('--token', meshToken);
2276
+ if(schedulerUrl) argv.push('--scheduler-url', schedulerUrl);
2277
+ argv.push(network==='tailscale'?'--tailscale':'--lan','--start','--auto-start','--yes');
2261
2278
  return argv.map(function(a){ return /[^a-zA-Z0-9_./:=,-]/.test(a) ? '"'+a.replace(/"/g,'\\"')+'"' : a; }).join(' ');
2262
2279
  }
2263
2280
  function refreshRobotCliCommand(){
@@ -2267,8 +2284,11 @@ button.act:disabled{opacity:0.5;cursor:default;}
2267
2284
  inp.value=buildAddRobotCommand(name, cid);
2268
2285
  row.style.display='flex';
2269
2286
  }
2270
- var rbNameEl=$('rb-name'); if(rbNameEl) rbNameEl.addEventListener('input', refreshRobotCliCommand);
2271
- var rbCharEl=$('rb-char'); if(rbCharEl) rbCharEl.addEventListener('change', refreshRobotCliCommand);
2287
+ var rbNameEl=$('rb-name'); if(rbNameEl) rbNameEl.addEventListener('input', refreshRobotCliCommand);
2288
+ var rbCharEl=$('rb-char'); if(rbCharEl) rbCharEl.addEventListener('change', refreshRobotCliCommand);
2289
+ var rbNetworkEl=$('rb-network'); if(rbNetworkEl) rbNetworkEl.addEventListener('change', refreshRobotCliCommand);
2290
+ var rbSiteEl=$('rb-site'); if(rbSiteEl) rbSiteEl.addEventListener('input', refreshRobotCliCommand);
2291
+ var rbTokenEl=$('rb-token'); if(rbTokenEl){ rbTokenEl.value=dashboardMeshToken(); rbTokenEl.addEventListener('input', refreshRobotCliCommand); }
2272
2292
  var rbCliCopy=$('rb-clicmd-copy'); if(rbCliCopy) rbCliCopy.addEventListener('click', function(){ var i=$('rb-clicmd'); i.select(); try{document.execCommand('copy');}catch(e){} showOut('command copied — run it on the robot itself'); });
2273
2293
 
2274
2294
  function addRobot(){
@@ -2653,7 +2673,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
2653
2673
  var RP_COMMAND_ENV_KEY='rp-command-env-v1';
2654
2674
  var RP_CUSTOM_COMMANDS_KEY='rp-custom-commands-v1';
2655
2675
  var rpVisibleCommands=[];
2656
- function rpCommandDefaults(){return {lan:'192.168.1.12',tail:'100.84.147.24',site:'Tel Aviv',robot:'bmw',enrollment:'',token:TOKEN};}
2676
+ function rpCommandDefaults(){return {lan:'192.168.0.9',tail:'100.84.147.24',site:'Tel Aviv',robot:'bmw',enrollment:'',token:dashboardMeshToken()};}
2657
2677
  function rpLoadStoredJson(key,fallback){try{var value=JSON.parse(localStorage.getItem(key)||'null');return value&&typeof value==='object'?value:fallback;}catch(e){return fallback;}}
2658
2678
  function rpCommandEnv(){
2659
2679
  var d=rpCommandDefaults(),s=rpLoadStoredJson(RP_COMMAND_ENV_KEY,{});
@@ -2705,7 +2725,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
2705
2725
  }
2706
2726
  function initCommandSheet(){
2707
2727
  var fields=['lan','tail','site','robot','enrollment','token'],defaults=rpCommandDefaults(),saved=rpLoadStoredJson(RP_COMMAND_ENV_KEY,{});
2708
- fields.forEach(function(name){var input=$('rp-command-'+name);if(!input)return;input.value=saved[name]!=null?saved[name]:defaults[name];input.addEventListener('input',function(){rpSaveCommandEnv();renderCommandSheet();});});
2728
+ fields.forEach(function(name){var input=$('rp-command-'+name);if(!input)return;input.value=name==='token'&&TOKEN?TOKEN:(saved[name]!=null?saved[name]:defaults[name]);input.addEventListener('input',function(){rpSaveCommandEnv();renderCommandSheet();});});
2709
2729
  var search=$('rp-command-search'),category=$('rp-command-category'),grid=$('rp-command-grid'),reset=$('rp-command-reset'),copyAll=$('rp-command-copy-all'),form=$('rp-command-custom-form');
2710
2730
  if(search)search.addEventListener('input',renderCommandSheet);if(category)category.addEventListener('change',renderCommandSheet);
2711
2731
  if(reset)reset.addEventListener('click',function(){try{localStorage.removeItem(RP_COMMAND_ENV_KEY);}catch(e){}fields.forEach(function(name){var input=$('rp-command-'+name);if(input)input.value=defaults[name];});renderCommandSheet();showOut('command environment reset');});
@@ -6517,13 +6537,24 @@ button.act:disabled{opacity:0.5;cursor:default;}
6517
6537
  // cars that arrived at the boundary together don't land on
6518
6538
  // top of each other.
6519
6539
  if(car.dir > 0 && car.x > PARK_W + 0.5){
6540
+ // Outbound (right-bound) car reached the right edge —
6541
+ // re-enter on the left. We swap to the OTHER lane so two
6542
+ // lanes of traffic are visible at all times; the wrap dir
6543
+ // is explicitly set to +1 so the car continues in its
6544
+ // original direction on the new lane. (Previous version
6545
+ // computed dir from the *post-wrap* y, which always
6546
+ // flipped the car to the opposite direction — cars would
6547
+ // oscillate at the road edge instead of crossing it.)
6520
6548
  car.x = -0.5 - Math.random() * (PARK_W * 0.5);
6521
6549
  car.y = (car.y < -0.9) ? -0.65 : -1.18;
6522
- car.dir = (car.y < -0.9) ? +1 : -1;
6550
+ car.dir = +1;
6523
6551
  } else if(car.dir < 0 && car.x < -0.5){
6552
+ // Inbound (left-bound) car reached the left edge —
6553
+ // re-enter on the right, swap to the other lane, keep
6554
+ // direction at -1. (See note above about the prior bug.)
6524
6555
  car.x = PARK_W + 0.5 + Math.random() * (PARK_W * 0.5);
6525
6556
  car.y = (car.y < -0.9) ? -0.65 : -1.18;
6526
- car.dir = (car.y < -0.9) ? +1 : -1;
6557
+ car.dir = -1;
6527
6558
  }
6528
6559
  }
6529
6560
  }
@@ -7429,7 +7460,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
7429
7460
  +'<div class="pk-note" id="pk-camnote">seeking live feed…</div>'
7430
7461
  +'<div class="pk-ctl" style="margin-top:0.5rem"><button class="rp-btn" id="pk-golive" data-robot="'+esc(n.name)+'">↻ Reconnect camera</button><button class="rp-btn primary" id="pk-camera-fullscreen">Fullscreen operations view</button><span class="pk-note" id="pk-golivenote" style="margin:0">always-on RoboVision relay</span></div>'
7431
7462
  +'<label class="pk-prodtoggle"><input type="checkbox" id="pk-prodmode"'+(devForPm.production_mode?' checked':'')+'><span>Production mode — continuous motion-triggered loop, camera always live</span></label>')
7432
- + sect('Remote production test','<div id="pk-pipeline-summary" class="pk-pipeline-summary">Checking robot, camera, scheduler and LiveKit...</div>'
7463
+ + sect('Remote production test','<div id="pk-live-stage" class="pk-live-stage-focus waiting"><span class="eyebrow">Current production stage</span><b>Checking robot state...</b><span>Waiting for scheduler telemetry</span></div><div id="pk-pipeline-summary" class="pk-pipeline-summary">Checking robot, camera, scheduler and LiveKit...</div>'
7433
7464
  + '<div class="pk-ctl" style="margin-top:0.55rem"><button class="rp-btn primary" id="pk-unattended-test">Run unattended readiness</button><button class="rp-btn" id="pk-pipeline-start">Start full test</button><button class="rp-btn danger" id="pk-pipeline-stop">Stop conversation</button></div>'
7434
7465
  + '<div class="pk-note" id="pk-unattended-result">Unattended readiness runs an acoustic speaker/mic round-trip, triggers the production motion path, and requires greeting playback on the robot.</div>'
7435
7466
  + '<div class="pk-pipeline" id="pk-pipeline"></div>')
@@ -7581,7 +7612,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
7581
7612
  function motorStatus(message,kind){var host=$('pk-motor-status');if(host){host.textContent=message;host.className='pk-motor-status '+(kind||'');}}
7582
7613
  function loadMotorProfile(n){
7583
7614
  var robotId=schedId(n);motorStatus('Loading saved motor profile...');
7584
- getJson('/robopark/api/robots/'+encodeURIComponent(robotId)+'/motor-profile').then(function(data){motorProfile={registry:data.registry||[],sequences:data.sequences||[],greeting_sequence_id:data.greeting_sequence_id||null,motor_server_url:(schedulerDevice(n)||{}).motor_server_url||'http://127.0.0.1:8001'};if(!motorProfile.sequences.length)motorProfile.sequences=[{id:'greeting-motion',name:'Greeting motion',steps:[]}];motorSequenceId=motorProfile.greeting_sequence_id||motorProfile.sequences[0].id;renderMotorEditor();motorStatus('Saved profile loaded | '+motorProfile.registry.length+' relay(s), '+motorProfile.sequences.length+' sequence(s)','ok');}).catch(function(error){motorStatus('Motor profile unavailable: '+error.message,'bad');});
7615
+ getJson('/robopark/api/robots/'+encodeURIComponent(robotId)+'/motor-profile').then(function(data){motorProfile={registry:data.registry||[],sequences:data.sequences||[],greeting_sequence_id:data.greeting_sequence_id||null,motor_server_url:(schedulerDevice(n)||{}).motor_server_url||data.motor_server_url||'http://127.0.0.1:8001'};if(!motorProfile.sequences.length)motorProfile.sequences=[{id:'greeting-motion',name:'Greeting motion',steps:[]}];motorSequenceId=motorProfile.greeting_sequence_id||motorProfile.sequences[0].id;renderMotorEditor();if(motorProfile.registry.length){motorStatus('Saved profile loaded | '+motorProfile.registry.length+' relay(s), '+motorProfile.sequences.length+' sequence(s)','ok');return;}motorStatus('No central registry; discovering robot-local relays...');return discoverMotorRelays(n,true);}).catch(function(error){motorStatus('Motor profile unavailable: '+error.message,'bad');});
7585
7616
  }
7586
7617
  function saveMotorProfile(n){
7587
7618
  captureMotorEditor();var robotId=schedId(n),url=motorProfile.motor_server_url;
@@ -7598,11 +7629,17 @@ button.act:disabled{opacity:0.5;cursor:default;}
7598
7629
  captureMotorEditor();var motor=motorProfile.registry[index];if(!motor)return;var robotId=schedId(n);motorStatus('Saving profile before safe 300 ms '+motor.name+' test...');
7599
7630
  saveMotorProfile(n).then(function(){return rpAction('POST','/robopark/api/robots/'+encodeURIComponent(robotId)+'/motors/'+encodeURIComponent(motor.id)+'/test?duration_ms=300',{});}).then(function(q){return _shellWaitForResult(robotId,q.request_id,q.device_id||robotId,'','motor-test');}).then(function(result){motorStatus(result.ok?motor.name+' relay test completed.':'Relay test failed: '+(result.error||'unknown error'),result.ok?'ok':'bad');}).catch(function(error){motorStatus('Relay test failed: '+error.message,'bad');});
7600
7631
  }
7632
+ function discoverMotorRelays(n,automatic){
7633
+ var robotId=schedId(n);motorStatus((automatic?'Automatically discovering':'Discovering')+' relays from the robot motor server...');
7634
+ return rpAction('POST','/robopark/api/robots/'+encodeURIComponent(robotId)+'/motors/discover',{}).then(function(q){return _shellWaitForResult(robotId,q.request_id,q.device_id||robotId,'','motor-discover');}).then(function(result){
7635
+ if(!result.ok)throw new Error(result.error||'robot motor discovery failed');
7636
+ if(!result.registry||!result.registry.length)throw new Error('Robot motor server has no registered relays. Register the wired BCM pins once; unknown pins will not be pulsed.');
7637
+ motorProfile.registry=result.registry;renderMotorEditor();motorStatus('Discovered '+result.registry.length+' robot-local relay(s); saving centrally...','ok');return saveMotorProfile(n);
7638
+ });
7639
+ }
7601
7640
  function testAllMotorRelays(n){
7602
- captureMotorEditor();if(!motorProfile.registry.length){motorStatus('Register at least one relay before running the GPIO test.','bad');return;}
7603
- if(!window.confirm('Pulse all '+motorProfile.registry.length+' registered relays one at a time? Ensure the motor area is clear.'))return;
7604
- var robotId=schedId(n);motorStatus('Saving profile and testing '+motorProfile.registry.length+' registered GPIO relay(s), one at a time...');
7605
- saveMotorProfile(n).then(function(){return rpAction('POST','/robopark/api/robots/'+encodeURIComponent(robotId)+'/motors/test-all?duration_ms=300&pause_ms=200',{});}).then(function(q){return _shellWaitForResult(robotId,q.request_id,q.device_id||robotId,'','gpio-test-all');}).then(function(result){motorStatus(result.ok?'Full registered GPIO test completed: '+(result.completed_steps||[]).length+' relay(s).':'GPIO test failed: '+(result.error||'unknown error'),result.ok?'ok':'bad');}).catch(function(error){motorStatus('GPIO test failed: '+error.message,'bad');});
7641
+ captureMotorEditor();var ready=motorProfile.registry.length?Promise.resolve():discoverMotorRelays(n,true);
7642
+ ready.then(function(){if(!window.confirm('Pulse all '+motorProfile.registry.length+' registered relays one at a time? Ensure the motor area is clear.'))return null;var robotId=schedId(n);motorStatus('Saving profile and testing '+motorProfile.registry.length+' registered GPIO relay(s), one at a time...');return saveMotorProfile(n).then(function(){return rpAction('POST','/robopark/api/robots/'+encodeURIComponent(robotId)+'/motors/test-all?duration_ms=300&pause_ms=200',{});}).then(function(q){return _shellWaitForResult(robotId,q.request_id,q.device_id||robotId,'','gpio-test-all');}).then(function(result){motorStatus(result.ok?'Full registered GPIO test completed: '+(result.completed_steps||[]).length+' relay(s).':'GPIO test failed: '+(result.error||'unknown error'),result.ok?'ok':'bad');});}).catch(function(error){motorStatus('GPIO test failed: '+error.message,'bad');});
7606
7643
  }
7607
7644
 
7608
7645
  var pipelineHistoryPage=1;
@@ -7621,7 +7658,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
7621
7658
  return stages[Math.min(stages.length-1,lastOk+1)];
7622
7659
  }
7623
7660
  function renderPipeline(data){
7624
- var host=$('pk-pipeline'), summary=$('pk-pipeline-summary'); if(!host||!summary) return;
7661
+ var host=$('pk-pipeline'), summary=$('pk-pipeline-summary'),focus=$('pk-live-stage'); if(!host||!summary) return;
7625
7662
  var latest={}, prereqs=data.prerequisites||{}, session=data.latest_session||{}, sid=session.id||null;
7626
7663
  (data.events||[]).forEach(function(ev){
7627
7664
  if(ev.session_id && sid && ev.session_id!==sid) return;
@@ -7630,15 +7667,16 @@ button.act:disabled{opacity:0.5;cursor:default;}
7630
7667
  ['robot_online','camera_ready','microphone_ready','speaker_ready'].forEach(function(stage){
7631
7668
  if(!latest[stage]) latest[stage]={stage:stage,status:prereqs[stage]?'ok':'blocked',message:prereqs[stage]?'Ready':'Not reported'};
7632
7669
  });
7633
- var running=data.overall==='running',stages=data.stages||Object.keys(PIPELINE_LABELS),current=pipelineCurrentStage(stages,latest,running);
7670
+ var running=data.overall==='running',stages=data.stages||Object.keys(PIPELINE_LABELS),authoritative=data.current_stage||{},current=authoritative.stage||pipelineCurrentStage(stages,latest,running);
7634
7671
  host.innerHTML=stages.map(function(stage,index){
7635
- var ev=latest[stage],isCurrent=stage===current,status=ev&&ev.status||(isCurrent?'running':'pending');
7636
- var detail=ev&&(ev.message||ev.timestamp)||'waiting';
7672
+ var ev=latest[stage],isCurrent=stage===current,status=(isCurrent&&authoritative.status)||ev&&ev.status||(isCurrent?'running':'pending');
7673
+ var detail=isCurrent&&(authoritative.message||authoritative.label)||ev&&(ev.message||ev.timestamp)||'waiting';
7637
7674
  var time=ev&&ev.timestamp?new Date(ev.timestamp+'Z').toLocaleString():'';
7638
7675
  var phase=PIPELINE_PHASES[stage]||'session';
7639
- return '<div class="pk-pipe-step phase-'+phase+' '+esc(status)+(isCurrent?' current':'')+'"><span class="step-index">'+String(index+1).padStart(2,'0')+'</span><span><span class="stage">'+esc(PIPELINE_LABELS[stage]||stage)+'</span><span class="phase">'+esc(phase)+'</span></span><span class="detail">'+esc(detail)+(time?'<span class="time">'+esc(time)+'</span>':'')+'</span></div>';
7676
+ return '<div class="pk-pipe-step phase-'+phase+' '+esc(status)+(isCurrent?' current':'')+'"'+(isCurrent?' aria-current="step"':'')+'><span class="step-index">'+String(index+1).padStart(2,'0')+'</span><span><span class="stage">'+esc(PIPELINE_LABELS[stage]||stage)+'<em class="step-state">'+esc(isCurrent?'ACTIVE':status)+'</em></span><span class="phase">'+esc(phase)+'</span></span><span class="detail">'+esc(detail)+(time?'<span class="time">'+esc(time)+'</span>':'')+'</span></div>';
7640
7677
  }).join('');
7641
7678
  var blockers=data.blockers||[];
7679
+ if(focus){var focusStatus=authoritative.status||blockers.length?'blocked':running?'active':'waiting';if(authoritative.status)focusStatus=authoritative.status;focus.className='pk-live-stage-focus '+esc(focusStatus);focus.innerHTML='<span class="eyebrow">Current production stage</span><b>'+esc(authoritative.label||PIPELINE_LABELS[current]||current||'Standing by')+'</b><span>'+esc(authoritative.message||blockers[0]||'Robot is armed and waiting for motion')+'</span>';}
7642
7680
  summary.className='pk-pipeline-summary '+esc(data.overall||'blocked');
7643
7681
  summary.textContent=blockers.length?('BLOCKED: '+blockers[0]):(running?('ACTIVE: '+(PIPELINE_LABELS[current]||current||'starting')+' | session '+(sid||'allocating')):'READY: camera-to-speaker remote test can start');
7644
7682
  var micHealth=$('pk-mic-health');if(micHealth){
@@ -7784,11 +7822,12 @@ button.act:disabled{opacity:0.5;cursor:default;}
7784
7822
  function refreshCameraOperations(){
7785
7823
  var n=cameraOpsNode;if(!n)return;var row=schedRow(n)||{},device=schedulerDevice(n)||{},hw=hwByNode[n.nodeId]||{};
7786
7824
  var readiness=String(row.readiness_code||(!n.connected?'offline':'mesh_online')),active=!!(row.active_session||row.in_session||row.session_active||row.current_session_id||row.status==='running'||row.status==='connecting');
7825
+ var currentStage=row.current_stage||{},currentStageLabel=currentStage.label||row.current_stage_label||(active?'Conversation starting':device.production_mode?'Armed - waiting for motion':'Production paused'),currentStageStatus=currentStage.status||row.current_stage_status||(active?'active':'waiting');
7787
7826
  var img=$('pk-camera-ops-img'),cameraLive=!!(img&&img.dataset.live==='true'),clockEl=$('pk-camera-ops-clock');if(clockEl)clockEl.textContent=clock(Date.now());
7788
7827
  var rec=$('pk-camera-ops-rec');if(rec)rec.textContent=(cameraLive?'REC / LIVE':'REC / CONNECTING')+' / '+String(n.displayName||n.name||'robot').toUpperCase();
7789
- var overlay=$('pk-camera-ops-overlay');if(overlay)overlay.innerHTML='<div class="metric"><b>'+esc(readiness.replace(/_/g,' '))+'</b><span>readiness</span></div><div class="metric"><b>'+esc(active?'in session':'standing by')+'</b><span>conversation</span></div><div class="metric"><b>'+(row.avg_latency_ms!=null?Math.round(row.avg_latency_ms)+'ms':'--')+'</b><span>average latency</span></div><div class="metric"><b>'+(row.drop_rate!=null?Math.round(row.drop_rate*100)+'%':'--')+'</b><span>session drops</span></div>';
7828
+ var overlay=$('pk-camera-ops-overlay');if(overlay)overlay.innerHTML='<div class="metric"><b>'+esc(currentStageLabel)+'</b><span>active stage</span></div><div class="metric"><b>'+esc(readiness.replace(/_/g,' '))+'</b><span>readiness</span></div><div class="metric"><b>'+(row.avg_latency_ms!=null?Math.round(row.avg_latency_ms)+'ms':'--')+'</b><span>average latency</span></div><div class="metric"><b>'+(row.drop_rate!=null?Math.round(row.drop_rate*100)+'%':'--')+'</b><span>session drops</span></div>';
7790
7829
  var insights=$('pk-camera-ops-insights');if(insights)insights.innerHTML='<span>Camera relay</span><b class="'+(cameraLive?'ok':'bad')+'">'+(cameraLive?'LIVE':'CONNECTING')+'</b><span>Mesh node</span><b class="'+(n.connected?'ok':'bad')+'">'+(n.connected?'connected':'offline')+'</b><span>Production mode</span><b class="'+(device.production_mode?'ok':'')+'">'+(device.production_mode?'armed':'disabled')+'</b><span>Readiness</span><b>'+esc(readiness.replace(/_/g,' '))+'</b><span>Heartbeat</span><b>'+(row.heartbeat_age_seconds!=null?Math.round(row.heartbeat_age_seconds)+'s ago':'--')+'</b><span>Current room</span><b>'+esc(String(row.current_session_id||row.active_session_id||'none').slice(0,24))+'</b><span>Triggers</span><b>'+esc(row.trigger_count!=null?row.trigger_count:0)+'</b><span>Sessions</span><b>'+esc(row.sessions_total!=null?row.sessions_total:0)+'</b>';
7791
- var stages=[['Robot online',!!n.connected],['Camera streaming',cameraLive],['Motion detector armed',!!device.production_mode],['Scheduler session',active],['Voice pipeline',readiness==='in_session'||active]],pipe=$('pk-camera-ops-pipeline');if(pipe)pipe.innerHTML=stages.map(function(s){return '<div><i class="'+(s[1]?'ok':'warn')+'"></i><span>'+esc(s[0])+'</span><b>'+(s[1]?'ready':'waiting')+'</b></div>';}).join('');
7830
+ var stages=[[currentStageLabel,true,'ACTIVE'],['Robot online',!!n.connected,'ready'],['Camera streaming',cameraLive,'ready'],['Motion detector armed',!!device.production_mode,'armed'],['Scheduler session',active,'active']],pipe=$('pk-camera-ops-pipeline');if(pipe)pipe.innerHTML=stages.map(function(s,index){return '<div class="'+(index===0?'fleet-stage-focus '+esc(currentStageStatus):'')+'"><i class="'+(s[1]?'ok':'warn')+'"></i><span>'+esc(s[0])+'</span><b>'+(s[1]?s[2]:'waiting')+'</b></div>';}).join('');
7792
7831
  var inv=device.device_inventory||{},mh=inv.media_health||{},hardware=$('pk-camera-ops-hardware');if(hardware)hardware.innerHTML='<span>CPU</span><b>'+(hw.cpu&&hw.cpu.percent!=null?Math.round(hw.cpu.percent)+'%':'--')+'</b><span>Memory</span><b>'+(hw.memory&&hw.memory.percent!=null?Math.round(hw.memory.percent)+'%':'--')+'</b><span>Temperature</span><b>'+(hw.temperatureC!=null?Math.round(hw.temperatureC)+'C':'--')+'</b><span>Camera inputs</span><b>'+((inv.video||[]).length||0)+'</b><span>Microphones</span><b>'+((inv.audio_input||[]).length||0)+'</b><span>Speaker outputs</span><b>'+((inv.audio_output||[]).length||0)+'</b><span>Camera permission</span><b class="'+(mh.camera_access===false?'bad':'ok')+'">'+(mh.camera_access===false?'DENIED':mh.camera_access===true?'granted':'--')+'</b><span>Audio permission</span><b class="'+(mh.audio_access===false?'bad':'ok')+'">'+(mh.audio_access===false?'DENIED':mh.audio_access===true?'granted':'--')+'</b><span>Camera worker</span><b class="'+(mh.camera_stalled?'bad':'ok')+'">'+(mh.camera_stalled?'STALLED':mh.camera_worker?'streaming':'--')+'</b><span>Frame age</span><b>'+(mh.camera_frame_age_seconds!=null?Number(mh.camera_frame_age_seconds).toFixed(1)+'s':'--')+'</b><span>Selected camera</span><b>'+esc(device.video_device||'auto')+'</b><span>Selected microphone</span><b>'+esc(device.audio_device||'default')+'</b>';
7793
7832
  var prod=$('pk-camera-ops-production');if(prod){prod.textContent=device.production_mode?'Disable production':'Enable production';prod.classList.toggle('primary',!!device.production_mode);}
7794
7833
  }
@@ -4,6 +4,8 @@ import { type ExplicitContextOpts } from './profile.js';
4
4
  export interface AddRobotOptions extends ExplicitContextOpts {
5
5
  name: string;
6
6
  start?: boolean;
7
+ autoStart?: boolean;
8
+ yes?: boolean;
7
9
  /** Force a specific park-map pad/character preset id instead of the
8
10
  * fuzzy name-based match below — e.g. `--character jaguar`. */
9
11
  character?: string;
@@ -24,23 +24,54 @@
24
24
  import chalk from 'chalk';
25
25
  import { resolveContext } from './profile.js';
26
26
  import { roboparkSetup } from './setup.js';
27
- async function mintEnrollmentToken(schedulerUrl, name, characterId) {
28
- const url = `${schedulerUrl.replace(/\/$/, '')}/api/devices`;
29
- const res = await fetch(url, {
30
- method: 'POST',
31
- headers: { 'content-type': 'application/json' },
32
- body: JSON.stringify({ name, character_id: characterId }),
33
- });
27
+ function robotReachableSchedulerUrl(schedulerUrl, hubUrl) {
28
+ if (!hubUrl)
29
+ return schedulerUrl;
30
+ try {
31
+ // Port 8080 is intentionally private to the hub. Enrollment, character
32
+ // lookup, heartbeats, and robot sessions must all use the authenticated
33
+ // mesh proxy, whether the hidden scheduler is loopback or a 192.168.x.x
34
+ // address that is only meaningful on the hub's own LAN.
35
+ new URL(schedulerUrl);
36
+ new URL(hubUrl);
37
+ return `${hubUrl.replace(/\/+$/, '')}/robopark`;
38
+ }
39
+ catch {
40
+ // Keep the original value so the existing request reports a useful error.
41
+ }
42
+ return schedulerUrl;
43
+ }
44
+ function schedulerApiUrl(schedulerUrl, path, token) {
45
+ const url = new URL(`${schedulerUrl.replace(/\/+$/, '')}${path}`);
46
+ if (token && url.pathname.startsWith('/robopark/')) {
47
+ url.searchParams.set('token', token);
48
+ }
49
+ return url.toString();
50
+ }
51
+ async function mintEnrollmentToken(schedulerUrl, name, characterId, token) {
52
+ const url = schedulerApiUrl(schedulerUrl, '/api/devices', token);
53
+ let res;
54
+ try {
55
+ res = await fetch(url, {
56
+ method: 'POST',
57
+ headers: { 'content-type': 'application/json' },
58
+ body: JSON.stringify({ name, character_id: characterId }),
59
+ });
60
+ }
61
+ catch (error) {
62
+ const cause = error.cause;
63
+ throw new Error(`cannot reach scheduler through ${schedulerUrl}: ${cause?.message ?? error.message}`);
64
+ }
34
65
  if (!res.ok) {
35
66
  const text = await res.text().catch(() => '');
36
67
  throw new Error(`${res.status}: ${text || res.statusText}`);
37
68
  }
38
69
  return (await res.json());
39
70
  }
40
- async function pickCharacterPreset(schedulerUrl, name) {
71
+ async function pickCharacterPreset(schedulerUrl, name, token) {
41
72
  const lower = name.toLowerCase();
42
73
  try {
43
- const res = await fetch(`${schedulerUrl.replace(/\/$/, '')}/api/character-presets`);
74
+ const res = await fetch(schedulerApiUrl(schedulerUrl, '/api/character-presets', token));
44
75
  if (!res.ok)
45
76
  return undefined;
46
77
  const presets = (await res.json());
@@ -83,11 +114,16 @@ export async function roboparkAddRobot(config, opts) {
83
114
  console.log(chalk.red(' ✗ no scheduler URL resolved. Pass --scheduler-url/--hub-url, run `robopark hub-use` first, or run this on/near the hub.'));
84
115
  process.exit(1);
85
116
  }
86
- console.log(` scheduler: ${chalk.cyan(ctx.schedulerUrl)}`);
117
+ const schedulerUrl = robotReachableSchedulerUrl(ctx.schedulerUrl, ctx.hubUrl);
118
+ if (schedulerUrl.endsWith('/robopark') && !ctx.token) {
119
+ console.log(chalk.red(' ✗ hub mesh token is required to use the scheduler proxy. Pass --token or run `robopark hub-use` first.'));
120
+ process.exit(1);
121
+ }
122
+ console.log(` scheduler: ${chalk.cyan(schedulerUrl)}`);
87
123
  console.log(` name: ${chalk.cyan(opts.name)}${ctx.site ? chalk.dim(' @ ' + ctx.site) : ''}`);
88
124
  console.log(` network: ${chalk.cyan(network)}`);
89
125
  console.log();
90
- const characterId = opts.character?.trim() || await pickCharacterPreset(ctx.schedulerUrl, opts.name);
126
+ const characterId = opts.character?.trim() || await pickCharacterPreset(schedulerUrl, opts.name, ctx.token);
91
127
  if (characterId) {
92
128
  console.log(` character: ${chalk.cyan(characterId)}${opts.character ? chalk.dim(' (forced via --character)') : chalk.dim(' (auto-matched)')}`);
93
129
  }
@@ -96,7 +132,7 @@ export async function roboparkAddRobot(config, opts) {
96
132
  }
97
133
  let minted;
98
134
  try {
99
- minted = await mintEnrollmentToken(ctx.schedulerUrl, opts.name, characterId);
135
+ minted = await mintEnrollmentToken(schedulerUrl, opts.name, characterId, ctx.token);
100
136
  }
101
137
  catch (e) {
102
138
  console.log(chalk.red(` ✗ failed to mint enrollment token: ${e.message}`));
@@ -120,6 +156,8 @@ export async function roboparkAddRobot(config, opts) {
120
156
  schedulerPort: ctx.schedulerUrl ? String(new URL(ctx.schedulerUrl).port || '8080') : undefined,
121
157
  enrollmentToken: minted.enrollment_token,
122
158
  start: true,
159
+ autoStart: opts.autoStart !== false,
160
+ yes: opts.yes !== false,
123
161
  lan: opts.lan,
124
162
  tailscale: opts.tailscale,
125
163
  });
@@ -140,7 +178,7 @@ export async function roboparkAddRobot(config, opts) {
140
178
  argv.push('--scheduler-port', schedulerPort);
141
179
  }
142
180
  argv.push(network === 'tailscale' ? '--tailscale' : '--lan');
143
- argv.push('--enrollment-token', minted.enrollment_token, '--start', '--auto-start');
181
+ argv.push('--enrollment-token', minted.enrollment_token, '--start', '--auto-start', '--yes');
144
182
  console.log(chalk.dim(` run this ON THE ROBOT${characterId ? ` (binds to pad: ${characterId})` : ''}:`));
145
183
  console.log(' ' + chalk.cyan(commandString(argv)));
146
184
  if (!ctx.hubUrl) {
@@ -146,7 +146,7 @@ export async function roboparkRobotRuntime(opts) {
146
146
  // only while audible robot speech is playing, then hold briefly for
147
147
  // room echo decay so VAD cannot interrupt the robot with its own voice.
148
148
  ROBOPARK_HALF_DUPLEX: 'true',
149
- ROBOPARK_ECHO_TAIL_MS: '700',
149
+ ROBOPARK_ECHO_TAIL_MS: '350',
150
150
  ROBOPARK_ECHO_GATE_PEAK: '96',
151
151
  // Keep ordinary speaker echo muted but reopen the microphone when a
152
152
  // nearby voice rises decisively above the learned acoustic echo floor.
@@ -284,6 +284,8 @@ program
284
284
  .option('--lan', 'generate a robot command for the same local network (default)')
285
285
  .option('--tailscale', 'generate a robot command for a Tailscale-reachable hub')
286
286
  .option('--start', 'start the robot node on this machine now, instead of printing the one-liner')
287
+ .option('--auto-start', 'register the robot runtime to start on boot')
288
+ .option('--yes', 'non-interactive production setup')
287
289
  .action(async (name, opts) => {
288
290
  await roboparkAddRobot(config, { name, ...opts });
289
291
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infinicode",
3
- "version": "2.8.102",
3
+ "version": "2.8.104",
4
4
  "description": "OpenKernel — provider-agnostic AI execution kernel. Native coding agent + mission-driven execution runtime.",
5
5
  "type": "module",
6
6
  "main": "./dist/kernel/index.js",