@loicngr/kobo 1.1.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/mcp-server/kobo-tasks-handlers.js +147 -0
  2. package/dist/mcp-server/kobo-tasks-server.js +236 -29
  3. package/dist/server/db/migrations.js +61 -10
  4. package/dist/server/db/schema.js +1 -0
  5. package/dist/server/index.js +10 -4
  6. package/dist/server/routes/images.js +57 -0
  7. package/dist/server/routes/workspaces.js +80 -19
  8. package/dist/server/services/agent-manager.js +91 -5
  9. package/dist/server/services/image-service.js +73 -0
  10. package/dist/server/services/pr-watcher-service.js +61 -0
  11. package/dist/server/services/settings-service.js +75 -10
  12. package/dist/server/services/workspace-service.js +13 -0
  13. package/dist/server/utils/git-ops.js +39 -0
  14. package/package.json +3 -1
  15. package/src/client/dist/spa/assets/{ActivityFeed-CufaRX1M.js → ActivityFeed-Bie-lcn7.js} +7 -7
  16. package/src/client/dist/spa/assets/ActivityFeed-D88GOO2z.css +1 -0
  17. package/src/client/dist/spa/assets/CreatePage-OC-fnNGP.js +2 -0
  18. package/src/client/dist/spa/assets/MainLayout-91cUoVYa.css +1 -0
  19. package/src/client/dist/spa/assets/MainLayout-BIQNJixM.js +1 -0
  20. package/src/client/dist/spa/assets/QBadge-DbE3eSf1.js +1 -0
  21. package/src/client/dist/spa/assets/QDialog-Cd_4PvgW.js +1 -0
  22. package/src/client/dist/spa/assets/QExpansionItem-pMQDDRMv.js +1 -0
  23. package/src/client/dist/spa/assets/QPage-lhV4XbI2.js +1 -0
  24. package/src/client/dist/spa/assets/{QSpinnerDots-DcaNq8uL.js → QSpinnerDots-ByNZaBWw.js} +1 -1
  25. package/src/client/dist/spa/assets/QTooltip-6GSFtFKP.js +1 -0
  26. package/src/client/dist/spa/assets/SettingsPage-BPH70mno.css +1 -0
  27. package/src/client/dist/spa/assets/SettingsPage-s2WJBreM.js +1 -0
  28. package/src/client/dist/spa/assets/WorkspacePage-Dhkuuhf8.css +1 -0
  29. package/src/client/dist/spa/assets/WorkspacePage-XT26aCJE.js +2 -0
  30. package/src/client/dist/spa/assets/_plugin-vue_export-helper-B6FaNy4R.js +1 -0
  31. package/src/client/dist/spa/assets/index-BoQWbZtE.js +5 -0
  32. package/src/client/dist/spa/assets/{nodes-DeIen-kp.js → nodes-CXdiSdC2.js} +1 -1
  33. package/src/client/dist/spa/assets/use-checkbox-Z9pfihkw.js +1 -0
  34. package/src/client/dist/spa/assets/use-quasar-CtCe3LQU.js +1 -0
  35. package/src/client/dist/spa/index.html +2 -2
  36. package/src/mcp-server/README.md +179 -0
  37. package/src/mcp-server/kobo-tasks-handlers.ts +238 -0
  38. package/src/mcp-server/kobo-tasks-server.ts +263 -29
  39. package/src/client/dist/spa/assets/ActivityFeed-DBNn62g_.css +0 -1
  40. package/src/client/dist/spa/assets/CreatePage-Cyl-TRHT.js +0 -2
  41. package/src/client/dist/spa/assets/MainLayout-D_vxGAPn.css +0 -1
  42. package/src/client/dist/spa/assets/MainLayout-Dzy0I8lB.js +0 -1
  43. package/src/client/dist/spa/assets/QBadge-Cb92Ia8-.js +0 -1
  44. package/src/client/dist/spa/assets/QDialog-CMC1Ph52.js +0 -1
  45. package/src/client/dist/spa/assets/QExpansionItem-DDjku8zz.js +0 -1
  46. package/src/client/dist/spa/assets/QPage-DaNo_vcd.js +0 -1
  47. package/src/client/dist/spa/assets/QTabPanels-BKHAAJ2p.js +0 -1
  48. package/src/client/dist/spa/assets/QTooltip-637ruGFc.js +0 -1
  49. package/src/client/dist/spa/assets/SettingsPage-B9VYIQs-.css +0 -1
  50. package/src/client/dist/spa/assets/SettingsPage-Blw7Qk7m.js +0 -1
  51. package/src/client/dist/spa/assets/WorkspacePage-DlnwomOE.js +0 -2
  52. package/src/client/dist/spa/assets/WorkspacePage-HtatyhXN.css +0 -1
  53. package/src/client/dist/spa/assets/_plugin-vue_export-helper-CHpmshS7.js +0 -1
  54. package/src/client/dist/spa/assets/index-DJkEmbBM.js +0 -5
  55. package/src/client/dist/spa/assets/use-quasar-Dq-Vjx_2.js +0 -1
@@ -0,0 +1 @@
1
+ import{B as e,C as t,Ct as n,E as r,Et as i,F as a,H as o,I as s,It as c,J as l,K as u,L as d,N as f,Nt as p,Ot as m,Q as h,Rt as g,T as _,V as v,Vt as y,W as b,Wt as x,Z as S,b as C,bt as w,et as T,f as E,kt as D,o as O,q as k,st as A,ut as j,wt as M,x as N,xt as P,y as F,yt as I,z as ee}from"./nodes-CXdiSdC2.js";import{l as te,m as ne,u as L}from"./index-BoQWbZtE.js";import{h as R,k as re,m as z}from"./_plugin-vue_export-helper-B6FaNy4R.js";import{C as B,S as V,c as H,u as U,v as ie,y as W}from"./QDialog-Cd_4PvgW.js";var G={left:!0,right:!0,up:!0,down:!0,horizontal:!0,vertical:!0},ae=Object.keys(G);G.all=!0;function K(e){let t={};for(let n of ae)e[n]===!0&&(t[n]=!0);return Object.keys(t).length===0?G:(t.horizontal===!0?t.left=t.right=!0:t.left===!0&&t.right===!0&&(t.horizontal=!0),t.vertical===!0?t.up=t.down=!0:t.up===!0&&t.down===!0&&(t.vertical=!0),t.horizontal===!0&&t.vertical===!0&&(t.all=!0),t)}var q=[`INPUT`,`TEXTAREA`];function J(e,t){return t.event===void 0&&e.target!==void 0&&e.target.draggable!==!0&&typeof t.handler==`function`&&q.includes(e.target.nodeName.toUpperCase())===!1&&(e.qClonedBy===void 0||e.qClonedBy.indexOf(t.uid)===-1)}var oe=0,se=[`click`,`keydown`],Y={icon:String,label:[Number,String],alert:[Boolean,String],alertIcon:String,name:{type:[Number,String],default:()=>`t_${oe++}`},noCaps:Boolean,tabindex:[String,Number],disable:Boolean,contentClass:String,ripple:{type:[Boolean,Object],default:!0}};function ce(e,t,n,r){let o=P(ne,L);if(o===L)return console.error(`QTab/QRouteTab component needs to be child of QTabs`),L;let{proxy:s}=I(),c=y(null),l=y(null),d=y(null),p=j(()=>e.disable===!0||e.ripple===!1?!1:Object.assign({keyCodes:[13,32],early:!0},e.ripple===!0?{}:e.ripple)),m=j(()=>o.currentModel.value===e.name),h=j(()=>`q-tab relative-position self-stretch flex flex-center text-center`+(m.value===!0?` q-tab--active`+(o.tabProps.value.activeClass?` `+o.tabProps.value.activeClass:``)+(o.tabProps.value.activeColor?` text-${o.tabProps.value.activeColor}`:``)+(o.tabProps.value.activeBgColor?` bg-${o.tabProps.value.activeBgColor}`:``):` q-tab--inactive`)+(e.icon&&e.label&&o.tabProps.value.inlineLabel===!1?` q-tab--full`:``)+(e.noCaps===!0||o.tabProps.value.noCaps===!0?` q-tab--no-caps`:``)+(e.disable===!0?` disabled`:` q-focusable q-hoverable cursor-pointer`)+(r===void 0?``:r.linkClass.value)),_=j(()=>`q-tab__content self-stretch flex-center relative-position q-anchor--skip non-selectable `+(o.tabProps.value.inlineLabel===!0?`row no-wrap q-tab__content--inline`:`column`)+(e.contentClass===void 0?``:` ${e.contentClass}`)),v=j(()=>e.disable===!0||o.hasFocus.value===!0||m.value===!1&&o.hasActiveTab.value===!0?-1:e.tabindex||0);function b(t,i){if(i!==!0&&t?.qAvoidFocus!==!0&&c.value?.focus(),e.disable===!0){r?.hasRouterLink.value===!0&&u(t);return}if(r===void 0){o.updateModel({name:e.name}),n(`click`,t);return}if(r.hasRouterLink.value===!0){let i=(n={})=>{let i,a=n.to===void 0||te(n.to,e.to)===!0?o.avoidRouteWatcher=W():null;return r.navigateToRouterLink(t,{...n,returnRouterError:!0}).catch(e=>{i=e}).then(t=>{if(a===o.avoidRouteWatcher&&(o.avoidRouteWatcher=!1,i===void 0&&(t===void 0||t.message?.startsWith(`Avoided redundant navigation`)===!0)&&o.updateModel({name:e.name})),n.returnRouterError===!0)return i===void 0?t:Promise.reject(i)})};n(`click`,t,i),t.defaultPrevented!==!0&&i();return}n(`click`,t)}function x(e){f(e,[13,32])?b(e,!0):a(e)!==!0&&e.keyCode>=35&&e.keyCode<=40&&e.altKey!==!0&&e.metaKey!==!0&&o.onKbdNavigate(e.keyCode,s.$el)===!0&&u(e),n(`keydown`,e)}function S(){let n=o.tabProps.value.narrowIndicator,r=[],i=w(`div`,{ref:d,class:[`q-tab__indicator`,o.tabProps.value.indicatorClass]});e.icon!==void 0&&r.push(w(F,{class:`q-tab__icon`,name:e.icon})),e.label!==void 0&&r.push(w(`div`,{class:`q-tab__label`},e.label)),e.alert!==!1&&r.push(e.alertIcon===void 0?w(`div`,{class:`q-tab__alert`+(e.alert===!0?``:` text-${e.alert}`)}):w(F,{class:`q-tab__alert-icon`,color:e.alert===!0?void 0:e.alert,name:e.alertIcon})),n===!0&&r.push(i);let a=[w(`div`,{class:`q-focus-helper`,tabindex:-1,ref:c}),w(`div`,{class:_.value},N(t.default,r))];return n===!1&&a.push(i),a}let C={name:j(()=>e.name),rootRef:l,tabIndicatorRef:d,routeData:r};i(()=>{o.unregisterTab(C)}),D(()=>{o.registerTab(C)});function T(t,n){return g(w(t,{ref:l,class:h.value,tabindex:v.value,role:`tab`,"aria-selected":m.value===!0?`true`:`false`,"aria-disabled":e.disable===!0?`true`:void 0,onClick:b,onKeydown:x,...n},S()),[[E,p.value]])}return{renderTab:T,$tabs:o}}var le=k({name:`QTab`,props:Y,emits:se,setup(e,{slots:t,emit:n}){let{renderTab:r}=ce(e,t,n);return()=>r(`div`)}});function ue(){let e=y(!h.value);return e.value===!1&&D(()=>{e.value=!0}),{isHydrated:e}}var X=typeof ResizeObserver<`u`,de=X===!0?{}:{style:`display:block;position:absolute;top:0;left:0;right:0;bottom:0;height:100%;width:100%;overflow:hidden;pointer-events:none;z-index:-1;`,url:`about:blank`},fe=k({name:`QResizeObserver`,props:{debounce:{type:[String,Number],default:100}},emits:[`resize`],setup(t,{emit:r}){let a=null,o,s={width:-1,height:-1};function c(e){e===!0||t.debounce===0||t.debounce===`0`?l():a===null&&(a=setTimeout(l,t.debounce))}function l(){if(a!==null&&(clearTimeout(a),a=null),o){let{offsetWidth:e,offsetHeight:t}=o;(e!==s.width||t!==s.height)&&(s={width:e,height:t},r(`resize`,s))}}let{proxy:u}=I();if(u.trigger=c,X===!0){let e,t=r=>{o=u.$el.parentNode,o?(e=new ResizeObserver(c),e.observe(o),l()):r!==!0&&n(()=>{t(!0)})};return D(()=>{t()}),i(()=>{a!==null&&clearTimeout(a),e!==void 0&&(e.disconnect===void 0?o&&e.unobserve(o):e.disconnect())}),v}else{let{isHydrated:t}=ue(),r;function s(){a!==null&&(clearTimeout(a),a=null),r!==void 0&&(r.removeEventListener!==void 0&&r.removeEventListener(`resize`,c,e.passive),r=void 0)}function d(){s(),o?.contentDocument&&(r=o.contentDocument.defaultView,r.addEventListener(`resize`,c,e.passive),l())}return D(()=>{n(()=>{o=u.$el,o&&d()})}),i(s),()=>{if(t.value===!0)return w(`object`,{class:`q--avoid-card-border`,style:de.style,tabindex:-1,type:`text/html`,data:de.url,"aria-hidden":`true`,onLoad:d})}}}});function pe(e,t,n){let r=n===!0?[`left`,`right`]:[`top`,`bottom`];return`absolute-${t===!0?r[0]:r[1]}${e?` text-${e}`:``}`}var me=[`left`,`center`,`right`,`justify`],he=k({name:`QTabs`,props:{modelValue:[Number,String],align:{type:String,default:`center`,validator:e=>me.includes(e)},breakpoint:{type:[String,Number],default:600},vertical:Boolean,shrink:Boolean,stretch:Boolean,activeClass:String,activeColor:String,activeBgColor:String,indicatorColor:String,leftIcon:String,rightIcon:String,outsideArrows:Boolean,mobileArrows:Boolean,switchIndicator:Boolean,narrowIndicator:Boolean,inlineLabel:Boolean,noCaps:Boolean,dense:Boolean,contentClass:String,"onUpdate:modelValue":[Function,Array]},setup(e,{slots:n,emit:r}){let{proxy:a}=I(),{$q:o}=a,{registerTick:s}=R(),{registerTick:l}=R(),{registerTick:u}=R(),{registerTimeout:d,removeTimeout:f}=z(),{registerTimeout:h,removeTimeout:g}=z(),_=y(null),v=y(null),b=y(e.modelValue),x=y(!1),S=y(!0),C=y(!1),T=y(!1),E=[],D=y(0),O=y(!1),k=null,A=null,N,P=j(()=>({activeClass:e.activeClass,activeColor:e.activeColor,activeBgColor:e.activeBgColor,indicatorClass:pe(e.indicatorColor,e.switchIndicator,e.vertical),narrowIndicator:e.narrowIndicator,inlineLabel:e.inlineLabel,noCaps:e.noCaps})),ee=j(()=>{let e=D.value,t=b.value;for(let n=0;n<e;n++)if(E[n].name.value===t)return!0;return!1}),te=j(()=>`q-tabs__content--align-${x.value===!0?`left`:T.value===!0?`justify`:e.align}`),L=j(()=>`q-tabs row no-wrap items-center q-tabs--${x.value===!0?``:`not-`}scrollable q-tabs--${e.vertical===!0?`vertical`:`horizontal`} q-tabs__arrows--${e.outsideArrows===!0?`outside`:`inside`} q-tabs--mobile-with${e.mobileArrows===!0?``:`out`}-arrows`+(e.dense===!0?` q-tabs--dense`:``)+(e.shrink===!0?` col-shrink`:``)+(e.stretch===!0?` self-stretch`:``)),re=j(()=>`q-tabs__content scroll--mobile row no-wrap items-center self-stretch hide-scrollbar relative-position `+te.value+(e.contentClass===void 0?``:` ${e.contentClass}`)),B=j(()=>e.vertical===!0?{container:`height`,content:`offsetHeight`,scroll:`scrollHeight`}:{container:`width`,content:`offsetWidth`,scroll:`scrollWidth`}),V=j(()=>e.vertical!==!0&&o.lang.rtl===!0),H=j(()=>ie===!1&&V.value===!0);c(V,q),c(()=>e.modelValue,e=>{U({name:e,setCurrent:!0,skipEmit:!0})}),c(()=>e.outsideArrows,W);function U({name:t,setCurrent:n,skipEmit:i}){b.value!==t&&(i!==!0&&e[`onUpdate:modelValue`]!==void 0&&r(`update:modelValue`,t),(n===!0||e[`onUpdate:modelValue`]===void 0)&&(ae(b.value,t),b.value=t))}function W(){s(()=>{_.value&&G({width:_.value.offsetWidth,height:_.value.offsetHeight})})}function G(t){if(B.value===void 0||v.value===null)return;let n=t[B.value.container],r=Math.min(v.value[B.value.scroll],Array.prototype.reduce.call(v.value.children,(e,t)=>e+(t[B.value.content]||0),0)),i=n>0&&r>n;x.value=i,i===!0&&l(q),T.value=n<parseInt(e.breakpoint,10)}function ae(t,n){let r=t!=null&&t!==``?E.find(e=>e.name.value===t):null,i=n!=null&&n!==``?E.find(e=>e.name.value===n):null;if($===!0)$=!1;else if(r&&i){let t=r.tabIndicatorRef.value,n=i.tabIndicatorRef.value;k!==null&&(clearTimeout(k),k=null),t.style.transition=`none`,t.style.transform=`none`,n.style.transition=`none`,n.style.transform=`none`;let a=t.getBoundingClientRect(),o=n.getBoundingClientRect();n.style.transform=e.vertical===!0?`translate3d(0,${a.top-o.top}px,0) scale3d(1,${o.height?a.height/o.height:1},1)`:`translate3d(${a.left-o.left}px,0,0) scale3d(${o.width?a.width/o.width:1},1,1)`,u(()=>{k=setTimeout(()=>{k=null,n.style.transition=`transform .25s cubic-bezier(.4, 0, .2, 1)`,n.style.transform=`none`},70)})}i&&x.value===!0&&K(i.rootRef.value)}function K(t){let{left:n,width:r,top:i,height:a}=v.value.getBoundingClientRect(),o=t.getBoundingClientRect(),s=e.vertical===!0?o.top-i:o.left-n;if(s<0){v.value[e.vertical===!0?`scrollTop`:`scrollLeft`]+=Math.floor(s),q();return}s+=e.vertical===!0?o.height-a:o.width-r,s>0&&(v.value[e.vertical===!0?`scrollTop`:`scrollLeft`]+=Math.ceil(s),q())}function q(){let t=v.value;if(t===null)return;let n=t.getBoundingClientRect(),r=e.vertical===!0?t.scrollTop:Math.abs(t.scrollLeft);V.value===!0?(S.value=Math.ceil(r+n.width)<t.scrollWidth-1,C.value=r>0):(S.value=r>0,C.value=e.vertical===!0?Math.ceil(r+n.height)<t.scrollHeight:Math.ceil(r+n.width)<t.scrollWidth)}function J(e){A!==null&&clearInterval(A),A=setInterval(()=>{ue(e)===!0&&Y()},5)}function oe(){J(H.value===!0?2**53-1:0)}function se(){J(H.value===!0?0:2**53-1)}function Y(){A!==null&&(clearInterval(A),A=null)}function ce(t,n){let r=Array.prototype.filter.call(v.value.children,e=>e===n||e.matches&&e.matches(`.q-tab.q-focusable`)===!0),i=r.length;if(i===0)return;if(t===36)return K(r[0]),r[0].focus(),!0;if(t===35)return K(r[i-1]),r[i-1].focus(),!0;let a=t===(e.vertical===!0?38:37),o=t===(e.vertical===!0?40:39),s=a===!0?-1:o===!0?1:void 0;if(s!==void 0){let e=V.value===!0?-1:1,t=r.indexOf(n)+s*e;return t>=0&&t<i&&(K(r[t]),r[t].focus({preventScroll:!0})),!0}}let le=j(()=>H.value===!0?{get:e=>Math.abs(e.scrollLeft),set:(e,t)=>{e.scrollLeft=-t}}:e.vertical===!0?{get:e=>e.scrollTop,set:(e,t)=>{e.scrollTop=t}}:{get:e=>e.scrollLeft,set:(e,t)=>{e.scrollLeft=t}});function ue(e){let t=v.value,{get:n,set:r}=le.value,i=!1,a=n(t),o=e<a?-1:1;return a+=o*5,a<0?(i=!0,a=0):(o===-1&&a<=e||o===1&&a>=e)&&(i=!0,a=e),r(t,a),q(),i}function X(e,t){for(let n in e)if(e[n]!==t[n])return!1;return!0}function de(){let e=null,t={matchedLen:0,queryDiff:9999,hrefLen:0},n=E.filter(e=>e.routeData?.hasRouterLink.value===!0),{hash:r,query:i}=a.$route,o=Object.keys(i).length;for(let a of n){let n=a.routeData.exact.value===!0;if(a.routeData[n===!0?`linkIsExactActive`:`linkIsActive`].value!==!0)continue;let{hash:s,query:c,matched:l,href:u}=a.routeData.resolvedLink.value,d=Object.keys(c).length;if(n===!0){if(s!==r||d!==o||X(i,c)===!1)continue;e=a.name.value;break}if(s!==``&&s!==r||d!==0&&X(c,i)===!1)continue;let f={matchedLen:l.length,queryDiff:o-d,hrefLen:u.length-s.length};if(f.matchedLen>t.matchedLen){e=a.name.value,t=f;continue}else if(f.matchedLen!==t.matchedLen)continue;if(f.queryDiff<t.queryDiff)e=a.name.value,t=f;else if(f.queryDiff!==t.queryDiff)continue;f.hrefLen>t.hrefLen&&(e=a.name.value,t=f)}if(e===null&&E.some(e=>e.routeData===void 0&&e.name.value===b.value)===!0){$=!1;return}U({name:e,setCurrent:!0})}function me(e){if(f(),O.value!==!0&&_.value!==null&&e.target&&typeof e.target.closest==`function`){let t=e.target.closest(`.q-tab`);t&&_.value.contains(t)===!0&&(O.value=!0,x.value===!0&&K(t))}}function he(){d(()=>{O.value=!1},30)}function Z(){Q.avoidRouteWatcher===!1?h(de):g()}function ge(){if(N===void 0){let e=c(()=>a.$route.fullPath,Z);N=()=>{e(),N=void 0}}}function _e(e){E.push(e),D.value++,W(),e.routeData===void 0||a.$route===void 0?h(()=>{if(x.value===!0){let e=b.value,t=e!=null&&e!==``?E.find(t=>t.name.value===e):null;t&&K(t.rootRef.value)}}):(ge(),e.routeData.hasRouterLink.value===!0&&Z())}function ve(e){E.splice(E.indexOf(e),1),D.value--,W(),N!==void 0&&e.routeData!==void 0&&(E.every(e=>e.routeData===void 0)===!0&&N(),Z())}let Q={currentModel:b,tabProps:P,hasFocus:O,hasActiveTab:ee,registerTab:_e,unregisterTab:ve,verifyRouteModel:Z,updateModel:U,onKbdNavigate:ce,avoidRouteWatcher:!1};p(ne,Q);function ye(){k!==null&&clearTimeout(k),Y(),N?.()}let be,$;return i(ye),m(()=>{be=N!==void 0,ye()}),M(()=>{be===!0&&(ge(),$=!0,Z()),W()}),()=>w(`div`,{ref:_,class:L.value,role:`tablist`,onFocusin:me,onFocusout:he},[w(fe,{onResize:G}),w(`div`,{ref:v,class:re.value,onScroll:q},t(n.default)),w(F,{class:`q-tabs__arrow q-tabs__arrow--left absolute q-tab__icon`+(S.value===!0?``:` q-tabs__arrow--faded`),name:e.leftIcon||o.iconSet.tabs[e.vertical===!0?`up`:`left`],onMousedownPassive:oe,onTouchstartPassive:oe,onMouseupPassive:Y,onMouseleavePassive:Y,onTouchendPassive:Y}),w(F,{class:`q-tabs__arrow q-tabs__arrow--right absolute q-tab__icon`+(C.value===!0?``:` q-tabs__arrow--faded`),name:e.rightIcon||o.iconSet.tabs[e.vertical===!0?`down`:`right`],onMousedownPassive:se,onTouchstartPassive:se,onMouseupPassive:Y,onMouseleavePassive:Y,onTouchendPassive:Y})])}});function Z(e){let t=[.06,6,50];return typeof e==`string`&&e.length&&e.split(`:`).forEach((e,n)=>{let r=parseFloat(e);r&&(t[n]=r)}),t}var ge=l({name:`touch-swipe`,beforeMount(e,{value:t,arg:n,modifiers:r}){if(r.mouse!==!0&&S.has.touch!==!0)return;let i=r.mouseCapture===!0?`Capture`:``,a={handler:t,sensitivity:Z(n),direction:K(r),noop:v,mouseStart(e){J(e,a)&&ee(e)&&(s(a,`temp`,[[document,`mousemove`,`move`,`notPassive${i}`],[document,`mouseup`,`end`,`notPassiveCapture`]]),a.start(e,!0))},touchStart(e){if(J(e,a)){let t=e.target;s(a,`temp`,[[t,`touchmove`,`move`,`notPassiveCapture`],[t,`touchcancel`,`end`,`notPassiveCapture`],[t,`touchend`,`end`,`notPassiveCapture`]]),a.start(e)}},start(t,n){S.is.firefox===!0&&b(e,!0);let r=o(t);a.event={x:r.left,y:r.top,time:Date.now(),mouse:n===!0,dir:!1}},move(e){if(a.event===void 0)return;if(a.event.dir!==!1){u(e);return}let t=Date.now()-a.event.time;if(t===0)return;let n=o(e),r=n.left-a.event.x,i=Math.abs(r),s=n.top-a.event.y,c=Math.abs(s);if(a.event.mouse!==!0){if(i<a.sensitivity[1]&&c<a.sensitivity[1]){a.end(e);return}}else if(window.getSelection().toString()!==``){a.end(e);return}else if(i<a.sensitivity[2]&&c<a.sensitivity[2])return;let l=i/t,d=c/t;a.direction.vertical===!0&&i<c&&i<100&&d>a.sensitivity[0]&&(a.event.dir=s<0?`up`:`down`),a.direction.horizontal===!0&&i>c&&c<100&&l>a.sensitivity[0]&&(a.event.dir=r<0?`left`:`right`),a.direction.up===!0&&i<c&&s<0&&i<100&&d>a.sensitivity[0]&&(a.event.dir=`up`),a.direction.down===!0&&i<c&&s>0&&i<100&&d>a.sensitivity[0]&&(a.event.dir=`down`),a.direction.left===!0&&i>c&&r<0&&c<100&&l>a.sensitivity[0]&&(a.event.dir=`left`),a.direction.right===!0&&i>c&&r>0&&c<100&&l>a.sensitivity[0]&&(a.event.dir=`right`),a.event.dir===!1?a.end(e):(u(e),a.event.mouse===!0&&(document.body.classList.add(`no-pointer-events--children`),document.body.classList.add(`non-selectable`),re(),a.styleCleanup=e=>{a.styleCleanup=void 0,document.body.classList.remove(`non-selectable`);let t=()=>{document.body.classList.remove(`no-pointer-events--children`)};e===!0?setTimeout(t,50):t()}),a.handler({evt:e,touch:a.event.mouse!==!0,mouse:a.event.mouse,direction:a.event.dir,duration:t,distance:{x:i,y:c}}))},end(t){a.event!==void 0&&(d(a,`temp`),S.is.firefox===!0&&b(e,!1),a.styleCleanup?.(!0),t!==void 0&&a.event.dir!==!1&&u(t),a.event=void 0)}};e.__qtouchswipe=a,r.mouse===!0&&s(a,`main`,[[e,`mousedown`,`mouseStart`,`passive${r.mouseCapture===!0||r.mousecapture===!0?`Capture`:``}`]]),S.has.touch===!0&&s(a,`main`,[[e,`touchstart`,`touchStart`,`passive${r.capture===!0?`Capture`:``}`],[e,`touchmove`,`noop`,`notPassiveCapture`]])},updated(e,t){let n=e.__qtouchswipe;n!==void 0&&(t.oldValue!==t.value&&(typeof t.value!=`function`&&n.end(),n.handler=t.value),n.direction=K(t.modifiers))},beforeUnmount(e){let t=e.__qtouchswipe;t!==void 0&&(d(t,`main`),d(t,`temp`),S.is.firefox===!0&&b(e,!1),t.styleCleanup?.(),delete e.__qtouchswipe)}});function _e(){let e=Object.create(null);return{getCache:(t,n)=>e[t]===void 0?e[t]=typeof n==`function`?n():n:e[t],setCache(t,n){e[t]=n},hasCache(t){return Object.hasOwnProperty.call(e,t)},clearCache(t){t===void 0?e=Object.create(null):delete e[t]}}}var ve={name:{required:!0},disable:Boolean},Q={setup(e,{slots:n}){return()=>w(`div`,{class:`q-panel scroll`,role:`tabpanel`},t(n.default))}},ye={modelValue:{required:!0},animated:Boolean,infinite:Boolean,swipeable:Boolean,vertical:Boolean,transitionPrev:String,transitionNext:String,transitionDuration:{type:[String,Number],default:300},keepAlive:Boolean,keepAliveInclude:[String,Array,RegExp],keepAliveExclude:[String,Array,RegExp],keepAliveMax:Number},be=[`update:modelValue`,`beforeTransition`,`transition`];function $(){let{props:e,emit:n,proxy:r}=I(),{getCache:i}=_e(),{registerTimeout:a}=z(),o,s,l=y(null),u={value:null};function d(t){let n=e.vertical===!0?`up`:`left`;M((r.$q.lang.rtl===!0?-1:1)*(t.direction===n?1:-1))}let f=j(()=>[[ge,d,void 0,{horizontal:e.vertical!==!0,vertical:e.vertical,mouse:!0}]]),p=j(()=>e.transitionPrev||`slide-${e.vertical===!0?`down`:`right`}`),m=j(()=>e.transitionNext||`slide-${e.vertical===!0?`up`:`left`}`),h=j(()=>`--q-transition-duration: ${e.transitionDuration}ms`),g=j(()=>typeof e.modelValue==`string`||typeof e.modelValue==`number`?e.modelValue:String(e.modelValue)),_=j(()=>({include:e.keepAliveInclude,exclude:e.keepAliveExclude,max:e.keepAliveMax})),v=j(()=>e.keepAliveInclude!==void 0||e.keepAliveExclude!==void 0);c(()=>e.modelValue,(t,r)=>{let i=C(t)===!0?E(t):-1;s!==!0&&k(i===-1?0:i<E(r)?-1:1),u.value!==i&&(u.value=i,n(`beforeTransition`,t,r),a(()=>{n(`transition`,t,r)},e.transitionDuration))});function b(){M(1)}function x(){M(-1)}function S(e){n(`update:modelValue`,e)}function C(e){return e!=null&&e!==``}function E(e){return o.findIndex(t=>t.props.name===e&&t.props.disable!==``&&t.props.disable!==!0)}function D(){return o.filter(e=>e.props.disable!==``&&e.props.disable!==!0)}function k(t){let n=t!==0&&e.animated===!0&&u.value!==-1?`q-transition--`+(t===-1?p.value:m.value):null;l.value!==n&&(l.value=n)}function M(t,r=u.value){let i=r+t;for(;i!==-1&&i<o.length;){let e=o[i];if(e!==void 0&&e.props.disable!==``&&e.props.disable!==!0){k(t),s=!0,n(`update:modelValue`,e.props.name),setTimeout(()=>{s=!1});return}i+=t}e.infinite===!0&&o.length!==0&&r!==-1&&r!==o.length&&M(t,t===-1?o.length:-1)}function N(){let t=E(e.modelValue);return u.value!==t&&(u.value=t),!0}function P(){let t=C(e.modelValue)===!0&&N()&&o[u.value];return e.keepAlive===!0?[w(A,_.value,[w(v.value===!0?i(g.value,()=>({...Q,name:g.value})):Q,{key:g.value,style:h.value},()=>t)])]:[w(`div`,{class:`q-panel scroll`,style:h.value,key:g.value,role:`tabpanel`},[t])]}function F(){if(o.length!==0)return e.animated===!0?[w(T,{name:l.value},P)]:P()}function ee(e){return o=O(t(e.default,[])).filter(e=>e.props!==null&&e.props.slot===void 0&&C(e.props.name)===!0),o.length}function te(){return o}return Object.assign(r,{next:b,previous:x,goTo:S}),{panelIndex:u,panelDirectives:f,updatePanelsList:ee,updatePanelIndex:N,getPanelContent:F,getEnabledPanels:D,getPanels:te,isValidPanelName:C,keepAliveProps:_,needsUniqueKeepAliveWrapper:v,goToPanelByOffset:M,goToPanel:S,nextPanel:b,previousPanel:x}}var xe=k({name:`QTabPanel`,props:ve,setup(e,{slots:n}){return()=>w(`div`,{class:`q-tab-panel`,role:`tabpanel`},t(n.default))}}),Se=k({name:`QTabPanels`,props:{...ye,...B},emits:be,setup(e,{slots:t}){let n=V(e,I().proxy.$q),{updatePanelsList:r,getPanelContent:i,panelDirectives:a}=$(),o=j(()=>`q-tab-panels q-panel-parent`+(n.value===!0?` q-tab-panels--dark q-dark`:``));return()=>(r(t),C(`div`,{class:o.value},i(),`pan`,e.swipeable,()=>a.value))}});function Ce(e,t){let n=y(null),r=j(()=>e.disable===!0?null:w(`span`,{ref:n,class:`no-outline`,tabindex:-1}));function i(e){let r=t.value;e?.qAvoidFocus!==!0&&(e?.type.indexOf(`key`)===0?document.activeElement!==r&&r?.contains(document.activeElement)===!0&&r.focus():n.value!==null&&(e===void 0||r?.contains(e.target)===!0)&&n.value.focus())}return{refocusTargetEl:r,refocusTarget:i}}var we={xs:30,sm:35,md:40,lg:50,xl:60},Te={...B,...r,...U,modelValue:{required:!0,default:null},val:{},trueValue:{default:!0},falseValue:{default:!1},indeterminateValue:{default:null},checkedIcon:String,uncheckedIcon:String,indeterminateIcon:String,toggleOrder:{type:String,validator:e=>e===`tf`||e===`ft`},toggleIndeterminate:Boolean,label:String,leftLabel:Boolean,color:String,keepColor:Boolean,dense:Boolean,disable:Boolean,tabindex:[String,Number]},Ee=[`update:modelValue`];function De(e,n){let{props:r,slots:i,emit:a,proxy:o}=I(),{$q:s}=o,c=V(r,s),l=y(null),{refocusTargetEl:d,refocusTarget:f}=Ce(r,l),p=_(r,we),m=j(()=>r.val!==void 0&&Array.isArray(r.modelValue)),h=j(()=>{let e=x(r.val);return m.value===!0?r.modelValue.findIndex(t=>x(t)===e):-1}),g=j(()=>m.value===!0?h.value!==-1:x(r.modelValue)===x(r.trueValue)),v=j(()=>m.value===!0?h.value===-1:x(r.modelValue)===x(r.falseValue)),b=j(()=>g.value===!1&&v.value===!1),S=j(()=>r.disable===!0?-1:r.tabindex||0),C=j(()=>`q-${e} cursor-pointer no-outline row inline no-wrap items-center`+(r.disable===!0?` disabled`:``)+(c.value===!0?` q-${e}--dark`:``)+(r.dense===!0?` q-${e}--dense`:``)+(r.leftLabel===!0?` reverse`:``)),T=j(()=>`q-${e}__inner relative-position non-selectable q-${e}__inner--${g.value===!0?`truthy`:v.value===!0?`falsy`:`indet`}${r.color!==void 0&&(r.keepColor===!0||(e===`toggle`?g.value===!0:v.value!==!0))?` text-${r.color}`:``}`),E=H(j(()=>{let e={type:`checkbox`};return r.name!==void 0&&Object.assign(e,{".checked":g.value,"^checked":g.value===!0?`checked`:void 0,name:r.name,value:m.value===!0?r.val:r.trueValue}),e})),D=j(()=>{let t={tabindex:S.value,role:e===`toggle`?`switch`:`checkbox`,"aria-label":r.label,"aria-checked":b.value===!0?`mixed`:g.value===!0?`true`:`false`};return r.disable===!0&&(t[`aria-disabled`]=`true`),t});function O(e){e!==void 0&&(u(e),f(e)),r.disable!==!0&&a(`update:modelValue`,k(),e)}function k(){if(m.value===!0){if(g.value===!0){let e=r.modelValue.slice();return e.splice(h.value,1),e}return r.modelValue.concat([r.val])}if(g.value===!0){if(r.toggleOrder!==`ft`||r.toggleIndeterminate===!1)return r.falseValue}else if(v.value===!0){if(r.toggleOrder===`ft`||r.toggleIndeterminate===!1)return r.trueValue}else return r.toggleOrder===`ft`?r.falseValue:r.trueValue;return r.indeterminateValue}function A(e){(e.keyCode===13||e.keyCode===32)&&u(e)}function M(e){(e.keyCode===13||e.keyCode===32)&&O(e)}let P=n(g,b);return Object.assign(o,{toggle:O}),()=>{let n=P();r.disable!==!0&&E(n,`unshift`,` q-${e}__native absolute q-ma-none q-pa-none`);let a=[w(`div`,{class:T.value,style:p.value,"aria-hidden":`true`},n)];d.value!==null&&a.push(d.value);let o=r.label===void 0?t(i.default):N(i.default,[r.label]);return o!==void 0&&a.push(w(`div`,{class:`q-${e}__label q-anchor--skip`},o)),w(`div`,{ref:l,class:C.value,...D.value,onClick:O,onKeydown:A,onKeyup:M},a)}}export{xe as a,le as c,Se as i,K as l,Ee as n,he as o,Te as r,fe as s,De as t,J as u};
@@ -0,0 +1 @@
1
+ import{xt as e}from"./nodes-CXdiSdC2.js";function t(){return e(`_q_`)}export{t};
@@ -1,4 +1,4 @@
1
- <!DOCTYPE html><html><head><title>Kōbō</title><meta charset=utf-8><meta name=description content="Kōbō — multi-workspace agent manager for Claude Code"><meta name=format-detection content="telephone=no"><meta name=msapplication-tap-highlight content=no><meta name=viewport content="user-scalable=no,initial-scale=1,maximum-scale=1,minimum-scale=1,width=device-width,height=device-height"> <script type="module" crossorigin src="/assets/index-DJkEmbBM.js"></script>
2
- <link rel="modulepreload" crossorigin href="/assets/nodes-DeIen-kp.js">
1
+ <!DOCTYPE html><html><head><title>Kōbō</title><meta charset=utf-8><meta name=description content="Kōbō — multi-workspace agent manager for Claude Code"><meta name=format-detection content="telephone=no"><meta name=msapplication-tap-highlight content=no><meta name=viewport content="user-scalable=no,initial-scale=1,maximum-scale=1,minimum-scale=1,width=device-width,height=device-height"> <script type="module" crossorigin src="/assets/index-BoQWbZtE.js"></script>
2
+ <link rel="modulepreload" crossorigin href="/assets/nodes-CXdiSdC2.js">
3
3
  <link rel="stylesheet" crossorigin href="/assets/index-BThMCiY7.css">
4
4
  </head><body><div id=q-app></div></body></html>
@@ -0,0 +1,179 @@
1
+ # Kōbō Tasks MCP Server
2
+
3
+ Standalone MCP (Model Context Protocol) server spawned by Kōbō for each Claude Code agent running inside a workspace. Exposes workspace-scoped tools that the agent can invoke to interact with Kōbō state: tasks, settings, dev server, images, git, etc.
4
+
5
+ ## How it runs
6
+
7
+ Kōbō's `agent-manager.ts` writes a `.mcp.json` file into each worktree and passes it to Claude Code via `--mcp-config`. Claude spawns this server as a child process with stdio transport and injects these environment variables:
8
+
9
+ | Env var | Purpose |
10
+ |---|---|
11
+ | `KOBO_WORKSPACE_ID` | ID of the current workspace — scopes all queries. **Required**. |
12
+ | `KOBO_DB_PATH` | Absolute path to Kōbō's SQLite DB. **Required**. |
13
+ | `KOBO_SETTINGS_PATH` | Absolute path to Kōbō's `settings.json`. Optional — `get_settings` returns an error shape if absent. |
14
+ | `KOBO_BACKEND_URL` | Base URL of the running Kōbō HTTP backend. Default: `http://localhost:3000`. Used by tools that need runtime state (dev server, git info, workspace transitions). |
15
+
16
+ The server reads the DB directly for read-only queries, writes directly for task CRUD, and calls the backend HTTP API for anything that touches runtime processes (dev server) or state transitions requiring validation.
17
+
18
+ ## Tools
19
+
20
+ ### Tasks
21
+
22
+ #### `list_tasks`
23
+ List all tasks and acceptance criteria for the current workspace with their IDs and current status. Call this first to discover task IDs.
24
+
25
+ **Input:** none
26
+ **Output:** `TaskDto[]` — `{ id, title, status, is_acceptance_criterion }`
27
+
28
+ ---
29
+
30
+ #### `mark_task_done`
31
+ Mark a task or acceptance criterion as done. Use when you have completed and validated the work.
32
+
33
+ **Input:**
34
+ - `task_id` (string, required) — ID from `list_tasks`
35
+
36
+ **Output:** `{ success: true, task: TaskDto }`
37
+ **Side effect:** emits `task:updated` WS event (via backend `notify-done`).
38
+
39
+ ---
40
+
41
+ #### `create_task`
42
+ Create a new task or acceptance criterion for the current workspace. Appended at the end of the list.
43
+
44
+ **Input:**
45
+ - `title` (string, required)
46
+ - `is_acceptance_criterion` (boolean, optional) — default `false`
47
+
48
+ **Output:** `TaskDto`
49
+ **Side effect:** emits `task:updated` WS event.
50
+
51
+ ---
52
+
53
+ #### `update_task`
54
+ Update an existing task — change title, status, or `is_acceptance_criterion` flag. At least one field is required.
55
+
56
+ **Input:**
57
+ - `task_id` (string, required)
58
+ - `title` (string, optional)
59
+ - `status` (string, optional) — `pending | in_progress | done`
60
+ - `is_acceptance_criterion` (boolean, optional)
61
+
62
+ **Output:** `TaskDto`
63
+ **Side effect:** emits `task:updated` WS event.
64
+
65
+ ---
66
+
67
+ #### `delete_task`
68
+ Delete a task from the current workspace permanently.
69
+
70
+ **Input:**
71
+ - `task_id` (string, required)
72
+
73
+ **Output:** `{ success: true, task_id: string }`
74
+ **Side effect:** emits `task:updated` WS event.
75
+
76
+ ---
77
+
78
+ ### Workspace
79
+
80
+ #### `get_workspace_info`
81
+ Get all metadata about the current workspace in a single call: name, project path, branches, model, Notion URL, worktree path, status, timestamps.
82
+
83
+ **Input:** none
84
+ **Output:**
85
+ ```ts
86
+ {
87
+ id, name, projectPath, sourceBranch, workingBranch,
88
+ worktreePath, status, model, notionUrl, notionPageId,
89
+ devServerStatus, createdAt, updatedAt
90
+ }
91
+ ```
92
+
93
+ ---
94
+
95
+ #### `set_workspace_status`
96
+ Update the current workspace status. Transitions are validated by the backend against the state machine.
97
+
98
+ **Input:**
99
+ - `status` (string, required) — e.g. `idle`, `completed`, `error`
100
+
101
+ **Output:** updated `Workspace`
102
+
103
+ ---
104
+
105
+ #### `get_git_info`
106
+ Get git stats for the current workspace: commit count, files changed, insertions, deletions, and PR URL if one exists for the branch.
107
+
108
+ **Input:** none
109
+ **Output:** `{ commitCount, filesChanged, insertions, deletions, prUrl }`
110
+
111
+ ---
112
+
113
+ ### Dev server
114
+
115
+ #### `get_dev_server_status`
116
+ Check whether the dev server is running for the current workspace. Reads `dev_server_status` from the DB.
117
+
118
+ **Input:** none
119
+ **Output:** `{ workspaceId, status }`
120
+
121
+ ---
122
+
123
+ #### `start_dev_server`
124
+ Start the dev server configured for the current workspace (via backend).
125
+
126
+ **Input:** none
127
+ **Output:** `DevServerStatus`
128
+
129
+ ---
130
+
131
+ #### `stop_dev_server`
132
+ Stop the dev server of the current workspace (via backend).
133
+
134
+ **Input:** none
135
+ **Output:** `DevServerStatus`
136
+
137
+ ---
138
+
139
+ #### `get_dev_server_logs`
140
+ Fetch the last N lines of the dev server logs for the current workspace.
141
+
142
+ **Input:**
143
+ - `tail` (number, optional) — default `200`
144
+
145
+ **Output:** `{ logs: string[] }`
146
+
147
+ ---
148
+
149
+ ### Settings
150
+
151
+ #### `get_settings`
152
+ Read Kōbō settings (global and/or per-project). Reads `KOBO_SETTINGS_PATH` directly from disk.
153
+
154
+ **Input:**
155
+ - `project_path` (string, optional) — if provided, returns the specific project entry alongside global
156
+
157
+ **Output (with `project_path`):** `{ global, project }`
158
+ **Output (without):** `{ global, projects }`
159
+ **Output (settings unavailable):** `{ global: null, project: null, error }`
160
+
161
+ ---
162
+
163
+ ### Images
164
+
165
+ #### `list_workspace_images`
166
+ List all images uploaded to the current workspace via Kōbō's chat paste/upload flow. Reads `.ai/images/index.json` from the worktree.
167
+
168
+ **Input:** none
169
+ **Output:** `Array<{ uid, originalName, relativePath, createdAt }>`
170
+
171
+ ---
172
+
173
+ ## Implementation notes
174
+
175
+ - **Handlers** live in `kobo-tasks-handlers.ts` as pure functions taking the DB handle (and sometimes paths) as arguments. This keeps them unit-testable in isolation — see `src/__tests__/kobo-tasks-server.test.ts`.
176
+ - **Backend HTTP helper** `backendRequest()` in `kobo-tasks-server.ts` wraps fetch calls to `KOBO_BACKEND_URL` for tools needing runtime state. On non-2xx it throws — the top-level dispatcher catches and returns an `isError` content.
177
+ - **Notifications**: `mark_task_done` hits `POST /tasks/:id/notify-done`, while `create_task` / `update_task` / `delete_task` hit `POST /tasks/notify-updated`. Both cause the backend to emit a `task:updated` WS event so the Vue UI refreshes.
178
+ - **Workspace scoping**: every handler that touches tasks uses `WHERE workspace_id = ?` to prevent cross-workspace access, even if the LLM passes a task_id from another workspace.
179
+ - **Error handling**: the MCP dispatcher wraps every tool call in a `try/catch` and returns `{ isError: true, content: [{ type: 'text', text: 'Error: ...' }] }` on failure. Handlers should throw with descriptive messages.
@@ -1,4 +1,10 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
1
3
  import type Database from 'better-sqlite3'
4
+ import { nanoid } from 'nanoid'
5
+
6
+ export const VALID_TASK_STATUSES = ['pending', 'in_progress', 'done'] as const
7
+ export type TaskStatus = (typeof VALID_TASK_STATUSES)[number]
2
8
 
3
9
  export interface TaskDto {
4
10
  id: string
@@ -12,6 +18,11 @@ export interface MarkDoneResult {
12
18
  task: TaskDto
13
19
  }
14
20
 
21
+ export interface DevServerStatusDto {
22
+ workspaceId: string
23
+ status: string
24
+ }
25
+
15
26
  interface TaskRow {
16
27
  id: string
17
28
  title: string
@@ -52,3 +63,230 @@ export function markTaskDoneHandler(db: Database.Database, workspaceId: string,
52
63
  .get(taskId) as TaskRow
53
64
  return { success: true, task: rowToDto(row) }
54
65
  }
66
+
67
+ export function createTaskHandler(
68
+ db: Database.Database,
69
+ workspaceId: string,
70
+ data: { title: string; is_acceptance_criterion?: boolean },
71
+ ): TaskDto {
72
+ if (!data.title?.trim()) {
73
+ throw new Error('title is required')
74
+ }
75
+
76
+ // Verify workspace exists
77
+ const ws = db.prepare('SELECT id FROM workspaces WHERE id = ?').get(workspaceId) as { id: string } | undefined
78
+ if (!ws) {
79
+ throw new Error(`Workspace '${workspaceId}' not found`)
80
+ }
81
+
82
+ const id = nanoid()
83
+ const now = new Date().toISOString()
84
+ const isAC = data.is_acceptance_criterion ? 1 : 0
85
+
86
+ // Append at the end: max(sort_order) + 1
87
+ const maxRow = db
88
+ .prepare('SELECT COALESCE(MAX(sort_order), -1) AS max FROM tasks WHERE workspace_id = ?')
89
+ .get(workspaceId) as { max: number }
90
+ const sortOrder = maxRow.max + 1
91
+
92
+ db.prepare(
93
+ 'INSERT INTO tasks (id, workspace_id, title, status, is_acceptance_criterion, sort_order, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
94
+ ).run(id, workspaceId, data.title.trim(), 'pending', isAC, sortOrder, now, now)
95
+
96
+ const row = db.prepare('SELECT id, title, status, is_acceptance_criterion FROM tasks WHERE id = ?').get(id) as TaskRow
97
+ return rowToDto(row)
98
+ }
99
+
100
+ export function updateTaskHandler(
101
+ db: Database.Database,
102
+ workspaceId: string,
103
+ taskId: string,
104
+ data: { title?: string; status?: string; is_acceptance_criterion?: boolean },
105
+ ): TaskDto {
106
+ // Verify task belongs to workspace
107
+ const existing = db.prepare('SELECT id FROM tasks WHERE id = ? AND workspace_id = ?').get(taskId, workspaceId) as
108
+ | { id: string }
109
+ | undefined
110
+ if (!existing) {
111
+ throw new Error(`Task '${taskId}' not found in workspace '${workspaceId}'`)
112
+ }
113
+
114
+ const sets: string[] = []
115
+ const values: unknown[] = []
116
+
117
+ if (data.title !== undefined) {
118
+ if (!data.title.trim()) throw new Error('title cannot be empty')
119
+ sets.push('title = ?')
120
+ values.push(data.title.trim())
121
+ }
122
+ if (data.status !== undefined) {
123
+ if (!(VALID_TASK_STATUSES as readonly string[]).includes(data.status)) {
124
+ throw new Error(`Invalid status '${data.status}'. Must be one of: ${VALID_TASK_STATUSES.join(', ')}`)
125
+ }
126
+ sets.push('status = ?')
127
+ values.push(data.status)
128
+ }
129
+ if (data.is_acceptance_criterion !== undefined) {
130
+ sets.push('is_acceptance_criterion = ?')
131
+ values.push(data.is_acceptance_criterion ? 1 : 0)
132
+ }
133
+
134
+ if (sets.length === 0) {
135
+ throw new Error('No fields to update (provide title, status, or is_acceptance_criterion)')
136
+ }
137
+
138
+ sets.push('updated_at = ?')
139
+ values.push(new Date().toISOString())
140
+ values.push(taskId)
141
+
142
+ db.prepare(`UPDATE tasks SET ${sets.join(', ')} WHERE id = ?`).run(...values)
143
+
144
+ const row = db
145
+ .prepare('SELECT id, title, status, is_acceptance_criterion FROM tasks WHERE id = ?')
146
+ .get(taskId) as TaskRow
147
+ return rowToDto(row)
148
+ }
149
+
150
+ export function deleteTaskHandler(
151
+ db: Database.Database,
152
+ workspaceId: string,
153
+ taskId: string,
154
+ ): { success: true; task_id: string } {
155
+ const result = db.prepare('DELETE FROM tasks WHERE id = ? AND workspace_id = ?').run(taskId, workspaceId)
156
+ if (result.changes === 0) {
157
+ throw new Error(`Task '${taskId}' not found in workspace '${workspaceId}'`)
158
+ }
159
+ return { success: true, task_id: taskId }
160
+ }
161
+
162
+ export function getDevServerStatusHandler(db: Database.Database, workspaceId: string): DevServerStatusDto {
163
+ const row = db.prepare('SELECT dev_server_status FROM workspaces WHERE id = ?').get(workspaceId) as
164
+ | { dev_server_status: string }
165
+ | undefined
166
+ if (!row) {
167
+ throw new Error(`Workspace '${workspaceId}' not found`)
168
+ }
169
+ return { workspaceId, status: row.dev_server_status }
170
+ }
171
+
172
+ export function getSettingsHandler(settingsPath: string | undefined, projectPath?: string): Record<string, unknown> {
173
+ // Shape is determined solely by whether projectPath was provided:
174
+ // - with projectPath → { global, project }
175
+ // - without → { global, projects }
176
+ // The `error` field is added on top when settings are unavailable.
177
+ if (!settingsPath || !fs.existsSync(settingsPath)) {
178
+ const base = projectPath ? { global: null, project: null } : { global: null, projects: [] }
179
+ return { ...base, error: 'Settings file not available' }
180
+ }
181
+ let parsed: Record<string, unknown>
182
+ try {
183
+ parsed = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'))
184
+ } catch (err) {
185
+ throw new Error(`Failed to read settings: ${err instanceof Error ? err.message : String(err)}`)
186
+ }
187
+
188
+ const global = parsed.global ?? null
189
+ const projects = Array.isArray(parsed.projects) ? (parsed.projects as Array<Record<string, unknown>>) : []
190
+
191
+ if (projectPath) {
192
+ const project = projects.find((p) => p.path === projectPath) ?? null
193
+ return { global, project }
194
+ }
195
+ return { global, projects }
196
+ }
197
+
198
+ // ── Workspace info ─────────────────────────────────────────────────────────────
199
+
200
+ export interface WorkspaceInfoDto {
201
+ id: string
202
+ name: string
203
+ projectPath: string
204
+ sourceBranch: string
205
+ workingBranch: string
206
+ worktreePath: string
207
+ status: string
208
+ model: string
209
+ notionUrl: string | null
210
+ notionPageId: string | null
211
+ devServerStatus: string
212
+ createdAt: string
213
+ updatedAt: string
214
+ }
215
+
216
+ interface WorkspaceRow {
217
+ id: string
218
+ name: string
219
+ project_path: string
220
+ source_branch: string
221
+ working_branch: string
222
+ status: string
223
+ notion_url: string | null
224
+ notion_page_id: string | null
225
+ model: string
226
+ dev_server_status: string
227
+ created_at: string
228
+ updated_at: string
229
+ }
230
+
231
+ export function getWorkspaceInfoHandler(db: Database.Database, workspaceId: string): WorkspaceInfoDto {
232
+ const row = db
233
+ .prepare(
234
+ 'SELECT id, name, project_path, source_branch, working_branch, status, notion_url, notion_page_id, model, dev_server_status, created_at, updated_at FROM workspaces WHERE id = ?',
235
+ )
236
+ .get(workspaceId) as WorkspaceRow | undefined
237
+
238
+ if (!row) {
239
+ throw new Error(`Workspace '${workspaceId}' not found`)
240
+ }
241
+
242
+ return {
243
+ id: row.id,
244
+ name: row.name,
245
+ projectPath: row.project_path,
246
+ sourceBranch: row.source_branch,
247
+ workingBranch: row.working_branch,
248
+ worktreePath: path.join(row.project_path, '.worktrees', row.working_branch),
249
+ status: row.status,
250
+ model: row.model,
251
+ notionUrl: row.notion_url,
252
+ notionPageId: row.notion_page_id,
253
+ devServerStatus: row.dev_server_status,
254
+ createdAt: row.created_at,
255
+ updatedAt: row.updated_at,
256
+ }
257
+ }
258
+
259
+ // ── Workspace images (read from .ai/images/index.json in worktree) ─────────────
260
+
261
+ export interface WorkspaceImageDto {
262
+ uid: string
263
+ originalName: string
264
+ relativePath: string
265
+ createdAt: string
266
+ }
267
+
268
+ export function listWorkspaceImagesHandler(worktreePath: string): WorkspaceImageDto[] {
269
+ const imagesDir = path.join(worktreePath, '.ai', 'images')
270
+ const indexPath = path.join(imagesDir, 'index.json')
271
+ if (!fs.existsSync(indexPath)) return []
272
+
273
+ let entries: Array<{ uid: string; originalName: string; createdAt: string }>
274
+ try {
275
+ entries = JSON.parse(fs.readFileSync(indexPath, 'utf-8'))
276
+ } catch {
277
+ return []
278
+ }
279
+
280
+ // Read directory once — imagesDir is guaranteed to exist because indexPath does
281
+ const files = fs.readdirSync(imagesDir)
282
+
283
+ return entries.map((e) => {
284
+ const match = files.find((f) => f.startsWith(`${e.uid}.`))
285
+ return {
286
+ uid: e.uid,
287
+ originalName: e.originalName,
288
+ relativePath: match ? path.join('.ai', 'images', match) : '',
289
+ createdAt: e.createdAt,
290
+ }
291
+ })
292
+ }