cli-jaw 2.17.27 → 2.17.28

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.
@@ -31,7 +31,7 @@
31
31
  href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@400;500;600;700&family=Outfit:wght@400;500;600;700&display=swap"
32
32
  rel="stylesheet">
33
33
  <!-- Vite handles module bundling in dev (HMR) and production (build) -->
34
- <script type="module" crossorigin src="/dist/assets/app-DUwWnKh3.js"></script>
34
+ <script type="module" crossorigin src="/dist/assets/app-CYayckBa.js"></script>
35
35
  <link rel="stylesheet" crossorigin href="/dist/assets/vendor-render-kIxHrxDV.css">
36
36
  <link rel="stylesheet" crossorigin href="/dist/assets/app-DuyLYRC6.css">
37
37
  </head>
@@ -6,7 +6,7 @@
6
6
  <link rel="icon" type="image/png" href="/dist/assets/icon-192-CwXPIYF5.png">
7
7
  <title>Jaw Manager</title>
8
8
  <script src="/manager/theme-boot.js"></script>
9
- <script type="module" crossorigin src="/dist/assets/manager-BEvw_3H1.js"></script>
9
+ <script type="module" crossorigin src="/dist/assets/manager-CwqrZ2MH.js"></script>
10
10
  <link rel="stylesheet" crossorigin href="/dist/assets/vendor-render-kIxHrxDV.css">
11
11
  <link rel="stylesheet" crossorigin href="/dist/assets/manager-LkGs6qEJ.css">
12
12
  </head>
@@ -138,7 +138,10 @@ export async function saveHeartbeatJobs(): Promise<void> {
138
138
  export async function initHeartbeatBadge(): Promise<void> {
139
139
  try {
140
140
  const d = await api<HeartbeatData>('/api/heartbeat');
141
- const active = (d?.jobs || []).map(normalizeHeartbeatJob).filter(j => j.enabled).length;
141
+ // A held job is enabled in the file but never scheduled, so it must not
142
+ // be counted as active. `held` comes from the server, which owns that
143
+ // judgement; the file only carries intent.
144
+ const active = (d?.jobs || []).filter(j => j.enabled !== false && !(j as { held?: string }).held).length;
142
145
  const btn = document.getElementById('hbSidebarBtn');
143
146
  if (btn) btn.innerHTML = `${ICONS.heartPulse} Heartbeat (${active})`;
144
147
  } catch { /* ignore */ }
@@ -45,9 +45,17 @@ export function validateHHMM(value: string): boolean {
45
45
  return HHMM_RE.test(value.trim());
46
46
  }
47
47
 
48
+ /** Per-call, because the timestamp alone is not unique.
49
+ *
50
+ * Two jobs added inside one millisecond both became `hb_<same-ms>`. The server
51
+ * rejects a duplicate id now — two jobs under one id share one mention-watch
52
+ * ledger namespace and the second inherits the first's cursor — so the
53
+ * collision would fail the whole save rather than quietly merging them. */
54
+ let idCounter = 0;
55
+
48
56
  export function makeDefaultJob(now: number = Date.now()): HbJob {
49
57
  return {
50
- id: `hb_${now}`,
58
+ id: `hb_${now}_${idCounter++}`,
51
59
  name: '',
52
60
  enabled: true,
53
61
  schedule: { kind: 'every', minutes: 30 },
@@ -1 +0,0 @@
1
- import{r as e}from"./rolldown-runtime-XQCOJYun.js";import{n as t,t as n}from"./jsx-runtime-CYoNlHAD.js";import{a as r,i,n as a,r as o,t as s}from"./page-shell-DZ1lls_P.js";import{a as c,i as l,s as u}from"./fields-E1L8aEhU.js";import{i as d}from"./agent-meta-_Idad3qq.js";import{t as f}from"./path-utils-QbjARuCC.js";var p=e(t(),1),m=n(),h=[{value:`every`,label:`every (interval)`},{value:`cron`,label:`cron expression`}];function g({job:e,index:t,bodyError:n,timeZoneError:r,onChange:i,onRemove:a}){let o=(0,p.useId)(),s=e.schedule.kind,d=e.schedule.timeZone??``;return(0,m.jsxs)(`fieldset`,{className:`settings-heartbeat-job`,"aria-label":`Heartbeat job ${t+1}${e.name?` (${e.name})`:``}`,children:[(0,m.jsx)(`legend`,{className:`settings-heartbeat-job-legend`,children:e.name||`Job ${t+1}`}),(0,m.jsx)(c,{id:`${o}-enabled`,label:`Enabled`,value:e.enabled,onChange:e=>i({enabled:e})}),(0,m.jsx)(u,{id:`${o}-name`,label:`Name`,value:e.name,placeholder:`Morning summary`,onChange:e=>i({name:e})}),(0,m.jsx)(l,{id:`${o}-kind`,label:`Schedule kind`,value:s,options:h,onChange:e=>{e===`every`&&s!==`every`?i({schedule:{kind:`every`,minutes:30,...d?{timeZone:d}:{}}}):e===`cron`&&s!==`cron`&&i({schedule:{kind:`cron`,cron:`0 9 * * *`,...d?{timeZone:d}:{}}})}}),s===`every`?(0,m.jsx)(u,{id:`${o}-minutes`,label:`Every (minutes)`,value:String(e.schedule.minutes??``),placeholder:`30`,error:n??null,onChange:e=>{let t=e.trim(),n=Number(t);i({schedule:{kind:`every`,minutes:Number.isFinite(n)?n:NaN,...d?{timeZone:d}:{}}})}}):(0,m.jsx)(u,{id:`${o}-cron`,label:`Cron expression`,value:e.schedule.cron??``,placeholder:`0 9 * * *`,error:n??null,onChange:e=>i({schedule:{kind:`cron`,cron:e,...d?{timeZone:d}:{}}})}),(0,m.jsx)(u,{id:`${o}-tz`,label:`Time zone (optional)`,value:d,placeholder:`Asia/Seoul`,error:r??null,onChange:t=>{let n=t.trim();i(s===`every`?{schedule:{kind:`every`,minutes:e.schedule.minutes,...n?{timeZone:n}:{}}}:{schedule:{kind:`cron`,cron:e.schedule.cron,...n?{timeZone:n}:{}}})}}),(0,m.jsxs)(`label`,{className:`settings-field settings-field-textarea`,htmlFor:`${o}-prompt`,children:[(0,m.jsx)(`span`,{className:`settings-field-label`,children:`Prompt`}),(0,m.jsx)(`textarea`,{id:`${o}-prompt`,rows:3,value:e.prompt,placeholder:`What should this heartbeat do?`,onChange:e=>i({prompt:e.target.value})})]}),(0,m.jsx)(`div`,{className:`settings-heartbeat-job-footer`,children:(0,m.jsx)(`button`,{type:`button`,className:`settings-action settings-action-discard`,onClick:a,children:`Remove job`})})]})}var _=/^\d+[smh]$/,v=/^([01]\d|2[0-3]):[0-5]\d$/,y=[`ai-e`,`claude`,`claude-e`,`codex`,`codex-app`,`copilot`,`cursor`,`kiro-code`,`gemini`,`grok`,`opencode`],b=[`heartbeat.enabled`,`heartbeat.every`,`heartbeat.activeHours.start`,`heartbeat.activeHours.end`,`heartbeat.target`],x=[`heartbeat.jobs`,`heartbeat.md`];function S(e){return _.test(e.trim())}function C(e){return v.test(e.trim())}function w(e=Date.now()){return{id:`hb_${e}`,name:``,enabled:!0,schedule:{kind:`every`,minutes:30},prompt:``}}function T(e){return e.startsWith(`heartbeat.`)&&e!==`heartbeat.jobs`&&e!==`heartbeat.md`}function E(e,t){if(!e||typeof e!=`object`)return null;let n=e,r=typeof n.id==`string`&&n.id.trim()?n.id.trim():`hb_unknown_${t}`,i=typeof n.name==`string`?n.name:``,a=n.enabled!==!1,o=typeof n.prompt==`string`?n.prompt:``,s=n.schedule,c;if(s&&typeof s==`object`){let e=s;c=e.kind===`cron`&&typeof e.cron==`string`?{kind:`cron`,cron:e.cron,...typeof e.timeZone==`string`&&e.timeZone?{timeZone:e.timeZone}:{}}:{kind:`every`,minutes:typeof e.minutes==`number`?e.minutes:30,...typeof e.timeZone==`string`&&e.timeZone?{timeZone:e.timeZone}:{}}}else c={kind:`every`,minutes:30};return{id:r,name:i,enabled:a,schedule:c,prompt:o}}function D(e){if(!e||typeof e!=`object`)return[];let t=e.jobs;return Array.isArray(t)?t.map((e,t)=>E(e,t)).filter(e=>e!==null):[]}function O(e){if(e.kind===`every`){let t=e.minutes;return!Number.isFinite(t)||t<=0||!Number.isInteger(t)?`Minutes must be a positive integer`:null}let t=e.cron?.trim()??``;return t?t.split(/\s+/).length<5?`Cron must have at least 5 fields`:null:`Cron expression required`}function k(e){return e.some(e=>O(e.schedule)!==null)}function A({port:e,client:t,dirty:n,snapshot:r}){let[i,a]=(0,p.useState)([]),[c,l]=(0,p.useState)([]),[u,d]=(0,p.useState)(!1),[f,h]=(0,p.useState)(null);(0,p.useEffect)(()=>{if(r.kind!==`ready`)return;let e=D(r.data);a(e),l(e)},[r]),(0,p.useEffect)(()=>{n.set(`heartbeat.jobs`,{value:i,original:c,valid:!k(i)})},[i,c,n]),(0,p.useEffect)(()=>()=>{n.remove(`heartbeat.jobs`)},[n]);let _=(0,p.useCallback)(async()=>{if(k(i)){h(`Fix invalid schedules before saving.`);return}d(!0),h(null);try{let e=D(await t.put(`/api/heartbeat`,{jobs:i}));a(e),l(e),n.remove(`heartbeat.jobs`)}catch(e){h(e instanceof Error?e.message:String(e))}finally{d(!1)}},[t,n,i]);if(r.kind===`loading`)return(0,m.jsx)(`p`,{className:`settings-section-hint`,children:`Loading jobs…`});if(r.kind===`offline`)return(0,m.jsx)(o,{port:e});if(r.kind===`error`)return(0,m.jsx)(s,{message:r.message});let v=n.pending.has(`heartbeat.jobs`),y=k(i);function b(e,t){a(n=>n.map((n,r)=>r===e?{...n,...t}:n))}function x(){a(e=>[...e,w()])}function S(e){a(t=>t.filter((t,n)=>n!==e))}return(0,m.jsxs)(m.Fragment,{children:[i.length===0?(0,m.jsx)(`p`,{className:`settings-section-hint`,children:`No jobs configured. Add one below.`}):(0,m.jsx)(`div`,{className:`settings-heartbeat-jobs-list`,children:i.map((e,t)=>(0,m.jsx)(g,{job:e,index:t,bodyError:O(e.schedule),onChange:e=>b(t,e),onRemove:()=>S(t)},e.id))}),(0,m.jsxs)(`div`,{className:`settings-heartbeat-jobs-footer`,children:[(0,m.jsx)(`button`,{type:`button`,className:`settings-action settings-action-discard`,onClick:x,children:`+ Add job`}),(0,m.jsx)(`button`,{type:`button`,className:`settings-action settings-action-save`,onClick:()=>void _(),disabled:u||!v||y,children:u?`Saving…`:`Apply jobs`})]}),f?(0,m.jsx)(`p`,{className:`settings-field-error`,role:`alert`,children:f}):null]})}function j({port:e,client:t,dirty:n,snapshot:r}){let[i,a]=(0,p.useState)(``),[c,l]=(0,p.useState)(``),[u,d]=(0,p.useState)(!1),[f,h]=(0,p.useState)(null);(0,p.useEffect)(()=>{if(r.kind!==`ready`)return;let e=r.data.content??``;a(e),l(e)},[r]),(0,p.useEffect)(()=>{n.set(`heartbeat.md`,{value:i,original:c,valid:!0})},[i,c,n]),(0,p.useEffect)(()=>()=>{n.remove(`heartbeat.md`)},[n]);let g=(0,p.useCallback)(async()=>{d(!0),h(null);try{await t.put(`/api/heartbeat-md`,{content:i}),l(i),n.remove(`heartbeat.md`)}catch(e){h(e instanceof Error?e.message:String(e))}finally{d(!1)}},[t,n,i]);if(r.kind===`loading`)return(0,m.jsx)(`p`,{className:`settings-section-hint`,children:`Loading template…`});if(r.kind===`offline`)return(0,m.jsx)(o,{port:e});if(r.kind===`error`)return(0,m.jsx)(s,{message:r.message});let _=n.pending.has(`heartbeat.md`);return(0,m.jsxs)(m.Fragment,{children:[(0,m.jsxs)(`label`,{className:`settings-field settings-field-textarea`,htmlFor:`hb-md`,children:[(0,m.jsx)(`span`,{className:`settings-field-label`,children:`Template`}),(0,m.jsx)(`textarea`,{id:`hb-md`,rows:10,spellCheck:!1,value:i,onChange:e=>a(e.target.value),style:{fontFamily:`ui-monospace, monospace`}})]}),(0,m.jsx)(`div`,{className:`settings-heartbeat-md-footer`,children:(0,m.jsx)(`button`,{type:`button`,className:`settings-action settings-action-save`,onClick:()=>void g(),disabled:u||!_,children:u?`Saving…`:`Save template`})}),f?(0,m.jsx)(`p`,{className:`settings-field-error`,role:`alert`,children:f}):null]})}function M({port:e,client:t,dirty:n,registerSave:h}){let g=r(t,`/api/settings`),_=r(t,`/api/heartbeat`),v=r(t,`/api/heartbeat-md`),[x,w]=(0,p.useState)(!1),[E,D]=(0,p.useState)(`30m`),[O,k]=(0,p.useState)(`08:00`),[M,N]=(0,p.useState)(`22:00`),[P,F]=(0,p.useState)(`all`);(0,p.useEffect)(()=>{if(g.state.kind!==`ready`)return;let e=g.state.data.heartbeat||{};w(!!e.enabled),D(typeof e.every==`string`?e.every:`30m`),k(e.activeHours?.start??`08:00`),N(e.activeHours?.end??`22:00`),F(typeof e.target==`string`?e.target:`all`)},[g.state]),(0,p.useEffect)(()=>()=>{for(let e of b)n.remove(e)},[n]);let I=(0,p.useMemo)(()=>g.state.kind===`ready`&&g.state.data.heartbeat||{},[g.state]),L=(0,p.useCallback)((e,t)=>n.set(e,t),[n]),R=(0,p.useRef)(0),z=(0,p.useCallback)(async()=>{let e=n.saveBundle(),r={};for(let[t,n]of Object.entries(e))T(t)&&(r[t]=n);if(Object.keys(r).length===0)return;let i=++R.current,a=f(r),o=await t.put(`/api/settings`,a);if(i!==R.current)return;let s=o&&typeof o==`object`&&`data`in o?o.data:o;for(let e of b)n.remove(e);g.setData(s);let c=s.heartbeat||{};w(!!c.enabled),D(typeof c.every==`string`?c.every:`30m`),k(c.activeHours?.start??`08:00`),N(c.activeHours?.end??`22:00`),F(typeof c.target==`string`?c.target:`all`),await g.refresh()},[t,n,g]);if((0,p.useEffect)(()=>{if(h)return h(z),()=>h(null)},[h,z]),g.state.kind===`loading`)return(0,m.jsx)(a,{});if(g.state.kind===`offline`)return(0,m.jsx)(o,{port:e});if(g.state.kind===`error`)return(0,m.jsx)(s,{message:g.state.message});let B=S(E)?null:"Use the form `<n>s|m|h`, e.g. `30m`",V=C(O)?null:`Use HH:MM (24h)`,H=C(M)?null:`Use HH:MM (24h)`,U=g.state.data.perCli||{},W=Object.keys(U),G=[{value:`all`,label:`all (broadcast)`},...(W.length>0?W:[...y]).map(e=>({value:e,label:d(e).label||e}))];return G.some(e=>e.value===P)||G.push({value:P,label:`${d(P).label||P} (legacy)`}),(0,m.jsxs)(`form`,{className:`settings-page-form`,onSubmit:e=>{e.preventDefault(),z()},children:[(0,m.jsxs)(i,{title:`Heartbeat`,hint:`Background prompts the agent runs on a schedule.`,children:[(0,m.jsx)(c,{id:`hb-enabled`,label:`Heartbeat enabled`,value:x,onChange:e=>{w(e),L(`heartbeat.enabled`,{value:e,original:!!I.enabled,valid:!0})}}),(0,m.jsx)(u,{id:`hb-every`,label:`Default interval`,value:E,placeholder:`30m`,error:B,onChange:e=>{D(e),L(`heartbeat.every`,{value:e,original:I.every??`30m`,valid:S(e)})}}),(0,m.jsx)(u,{id:`hb-start`,label:`Active hours start`,value:O,placeholder:`08:00`,error:V,onChange:e=>{k(e),L(`heartbeat.activeHours.start`,{value:e,original:I.activeHours?.start??`08:00`,valid:C(e)})}}),(0,m.jsx)(u,{id:`hb-end`,label:`Active hours end`,value:M,placeholder:`22:00`,error:H,onChange:e=>{N(e),L(`heartbeat.activeHours.end`,{value:e,original:I.activeHours?.end??`22:00`,valid:C(e)})}}),(0,m.jsx)(l,{id:`hb-target`,label:`Target`,value:P,options:G,onChange:e=>{F(e),L(`heartbeat.target`,{value:e,original:I.target??`all`,valid:!0})}})]}),(0,m.jsx)(i,{title:`Jobs`,hint:`Each job replaces the entire heartbeat.json on save. Concurrent edits from multiple browsers will overwrite — last write wins.`,children:(0,m.jsx)(A,{port:e,client:t,dirty:n,snapshot:_.state})}),(0,m.jsx)(i,{title:`Default heartbeat prompt template`,hint:`Markdown shown to the agent when a heartbeat fires without a job-specific prompt.`,children:(0,m.jsx)(j,{port:e,client:t,dirty:n,snapshot:v.state})})]})}export{x as PAGE_LOCAL_KEYS,b as SECTION_A_KEYS,y as TARGET_FALLBACK,M as default,T as isHeartbeatSettingsKey,O as jobScheduleBodyError,k as jobsHaveErrors,w as makeDefaultJob,D as normalizeJobsResponse,C as validateHHMM,S as validateInterval};