kanon-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/bin/kanon.js +266 -0
- package/package.json +34 -0
- package/src/commands/attachment.js +98 -0
- package/src/commands/boards.js +39 -0
- package/src/commands/card.js +260 -0
- package/src/commands/cards.js +79 -0
- package/src/commands/checklist.js +129 -0
- package/src/commands/dashboard.js +24 -0
- package/src/commands/init.js +89 -0
- package/src/commands/label.js +61 -0
- package/src/commands/list.js +78 -0
- package/src/commands/login.js +91 -0
- package/src/commands/watch.js +224 -0
- package/src/dashboard/dist/assets/index-Dcbpx-Xz.js +186 -0
- package/src/dashboard/dist/assets/index-DhFfv70f.css +1 -0
- package/src/dashboard/dist/index.html +13 -0
- package/src/dashboard/dist/kanon.png +0 -0
- package/src/dashboard/package.json +26 -0
- package/src/dashboard/server/agent.js +201 -0
- package/src/dashboard/server/index.js +85 -0
- package/src/dashboard/server/proxy.js +54 -0
- package/src/dashboard/server/settings.js +236 -0
- package/src/lib/admin.js +330 -0
- package/src/lib/api.js +225 -0
- package/src/lib/claude.js +161 -0
- package/src/lib/config.js +112 -0
- package/src/lib/pipeline.js +133 -0
- package/src/lib/websocket.js +194 -0
- package/src/prompts/templates.js +127 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:#374151;border-radius:3px}::-webkit-scrollbar-thumb:hover{background:#4b5563}.pointer-events-none{pointer-events:none}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.left-0\.5{left:.125rem}.left-3{left:.75rem}.left-\[18px\]{left:18px}.right-3{right:.75rem}.top-0\.5{top:.125rem}.top-full{top:100%}.z-50{z-index:50}.col-span-2{grid-column:span 2 / span 2}.mx-2{margin-left:.5rem;margin-right:.5rem}.mb-1{margin-bottom:.25rem}.mb-1\.5{margin-bottom:.375rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-3{height:.75rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-7{height:1.75rem}.h-full{height:100%}.max-h-40{max-height:10rem}.max-h-48{max-height:12rem}.max-h-64{max-height:16rem}.max-h-\[60vh\]{max-height:60vh}.max-h-\[70vh\]{max-height:70vh}.w-10{width:2.5rem}.w-12{width:3rem}.w-16{width:4rem}.w-2{width:.5rem}.w-2\.5{width:.625rem}.w-20{width:5rem}.w-3{width:.75rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-56{width:14rem}.w-7{width:1.75rem}.w-9{width:2.25rem}.w-full{width:100%}.min-w-0{min-width:0px}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.rotate-180{--tw-rotate: 180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-90{--tw-rotate: 90deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-pointer{cursor:pointer}.resize-y{resize:vertical}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.space-y-0\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.125rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.125rem * var(--tw-space-y-reverse))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-r{border-right-width:1px}.border-r-2{border-right-width:2px}.border-t{border-top-width:1px}.border-gray-700{--tw-border-opacity: 1;border-color:rgb(55 65 81 / var(--tw-border-opacity, 1))}.border-gray-700\/30{border-color:#3741514d}.border-gray-700\/50{border-color:#37415180}.border-gray-800{--tw-border-opacity: 1;border-color:rgb(31 41 55 / var(--tw-border-opacity, 1))}.border-gray-800\/50{border-color:#1f293780}.border-green-500\/30{border-color:#22c55e4d}.border-kanon-500{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity, 1))}.border-kanon-500\/30{border-color:#3b82f64d}.border-red-500\/30{border-color:#ef44444d}.border-red-800\/40{border-color:#991b1b66}.border-yellow-500\/30{border-color:#eab3084d}.bg-blue-500{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity, 1))}.bg-cyan-500\/10{background-color:#06b6d41a}.bg-gray-500{--tw-bg-opacity: 1;background-color:rgb(107 114 128 / var(--tw-bg-opacity, 1))}.bg-gray-700{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity, 1))}.bg-gray-800{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.bg-gray-800\/30{background-color:#1f29374d}.bg-gray-800\/50{background-color:#1f293780}.bg-gray-900{--tw-bg-opacity: 1;background-color:rgb(17 24 39 / var(--tw-bg-opacity, 1))}.bg-gray-950{--tw-bg-opacity: 1;background-color:rgb(3 7 18 / var(--tw-bg-opacity, 1))}.bg-gray-950\/50{background-color:#03071280}.bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.bg-green-500\/10{background-color:#22c55e1a}.bg-green-500\/20{background-color:#22c55e33}.bg-green-600\/20{background-color:#16a34a33}.bg-kanon-500\/10{background-color:#3b82f61a}.bg-kanon-500\/20{background-color:#3b82f633}.bg-kanon-500\/5{background-color:#3b82f60d}.bg-kanon-600{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity, 1))}.bg-orange-500{--tw-bg-opacity: 1;background-color:rgb(249 115 22 / var(--tw-bg-opacity, 1))}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity, 1))}.bg-red-500\/10{background-color:#ef44441a}.bg-red-500\/20{background-color:#ef444433}.bg-red-600\/20{background-color:#dc262633}.bg-transparent{background-color:transparent}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-yellow-500{--tw-bg-opacity: 1;background-color:rgb(234 179 8 / var(--tw-bg-opacity, 1))}.bg-yellow-500\/10{background-color:#eab3081a}.bg-yellow-500\/20{background-color:#eab30833}.bg-yellow-600\/20{background-color:#ca8a0433}.object-contain{-o-object-fit:contain;object-fit:contain}.p-1{padding:.25rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-3{padding-bottom:.75rem}.pl-3{padding-left:.75rem}.pt-2{padding-top:.5rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-\[10px\]{font-size:10px}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.capitalize{text-transform:capitalize}.italic{font-style:italic}.leading-tight{line-height:1.25}.tracking-wider{letter-spacing:.05em}.text-blue-400{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.text-cyan-400{--tw-text-opacity: 1;color:rgb(34 211 238 / var(--tw-text-opacity, 1))}.text-gray-100{--tw-text-opacity: 1;color:rgb(243 244 246 / var(--tw-text-opacity, 1))}.text-gray-200{--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity, 1))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.text-green-400{--tw-text-opacity: 1;color:rgb(74 222 128 / var(--tw-text-opacity, 1))}.text-green-400\/60{color:#4ade8099}.text-kanon-300{--tw-text-opacity: 1;color:rgb(147 197 253 / var(--tw-text-opacity, 1))}.text-kanon-400{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.text-kanon-400\/60{color:#60a5fa99}.text-purple-400{--tw-text-opacity: 1;color:rgb(192 132 252 / var(--tw-text-opacity, 1))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-transparent{color:transparent}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-yellow-400{--tw-text-opacity: 1;color:rgb(250 204 21 / var(--tw-text-opacity, 1))}.placeholder-gray-600::-moz-placeholder{--tw-placeholder-opacity: 1;color:rgb(75 85 99 / var(--tw-placeholder-opacity, 1))}.placeholder-gray-600::placeholder{--tw-placeholder-opacity: 1;color:rgb(75 85 99 / var(--tw-placeholder-opacity, 1))}.caret-gray-200{caret-color:#e5e7eb}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.last\:border-0:last-child{border-width:0px}.hover\:border-gray-600\/50:hover{border-color:#4b556380}.hover\:bg-gray-700\/50:hover{background-color:#37415180}.hover\:bg-gray-800:hover{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-800\/30:hover{background-color:#1f29374d}.hover\:bg-gray-800\/50:hover{background-color:#1f293780}.hover\:bg-gray-800\/80:hover{background-color:#1f2937cc}.hover\:bg-green-600\/30:hover{background-color:#16a34a4d}.hover\:bg-kanon-500:hover{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity, 1))}.hover\:bg-kanon-500\/20:hover{background-color:#3b82f633}.hover\:bg-red-600\/30:hover{background-color:#dc26264d}.hover\:bg-yellow-600\/30:hover{background-color:#ca8a044d}.hover\:text-gray-200:hover{--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity, 1))}.hover\:text-gray-300:hover{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.hover\:text-gray-400:hover{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.hover\:text-green-300:hover{--tw-text-opacity: 1;color:rgb(134 239 172 / var(--tw-text-opacity, 1))}.hover\:text-kanon-300:hover{--tw-text-opacity: 1;color:rgb(147 197 253 / var(--tw-text-opacity, 1))}.hover\:text-kanon-400:hover{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.hover\:text-red-300:hover{--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity, 1))}.hover\:text-red-400:hover{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.focus\:border-kanon-500:focus{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity, 1))}.focus\:border-kanon-500\/50:focus{border-color:#3b82f680}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-1:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-kanon-500\/30:focus{--tw-ring-color: rgb(59 130 246 / .3)}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" class="h-full bg-gray-950">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Kanon Dashboard</title>
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-Dcbpx-Xz.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DhFfv70f.css">
|
|
9
|
+
</head>
|
|
10
|
+
<body class="h-full text-gray-100">
|
|
11
|
+
<div id="root" class="h-full"></div>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
Binary file
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kanon-dashboard",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"preview": "vite preview"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"lucide-react": "^0.577.0",
|
|
13
|
+
"react": "^19.1.0",
|
|
14
|
+
"react-dom": "^19.1.0",
|
|
15
|
+
"react-router-dom": "^7.6.2"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/react": "^19.1.5",
|
|
19
|
+
"@types/react-dom": "^19.1.5",
|
|
20
|
+
"@vitejs/plugin-react": "^4.5.2",
|
|
21
|
+
"autoprefixer": "^10.4.21",
|
|
22
|
+
"postcss": "^8.5.4",
|
|
23
|
+
"tailwindcss": "^3.4.17",
|
|
24
|
+
"vite": "^6.3.5"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import { getAgentLockPath, getStatusPath } from '../../lib/config.js';
|
|
6
|
+
|
|
7
|
+
export function createAgentRoutes() {
|
|
8
|
+
const router = Router();
|
|
9
|
+
let watchProcess = null;
|
|
10
|
+
|
|
11
|
+
// Helper: find running watch daemon's IPC port
|
|
12
|
+
function getAgentPort() {
|
|
13
|
+
const lockPath = getAgentLockPath();
|
|
14
|
+
if (!fs.existsSync(lockPath)) return null;
|
|
15
|
+
try {
|
|
16
|
+
const lock = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
|
|
17
|
+
// Check if process is still alive
|
|
18
|
+
try { process.kill(lock.pid, 0); } catch { return null; }
|
|
19
|
+
return lock.port;
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isAgentRunning() {
|
|
26
|
+
return getAgentPort() !== null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Proxy to running watch daemon's IPC server
|
|
30
|
+
async function proxyToAgent(path, method = 'GET') {
|
|
31
|
+
const port = getAgentPort();
|
|
32
|
+
if (!port) {
|
|
33
|
+
throw new Error('Watch daemon not running. Start with: kanon watch');
|
|
34
|
+
}
|
|
35
|
+
const res = await fetch(`http://127.0.0.1:${port}${path}`, { method });
|
|
36
|
+
return res.json();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// GET /api/agent/status
|
|
40
|
+
router.get('/status', async (req, res) => {
|
|
41
|
+
try {
|
|
42
|
+
const data = await proxyToAgent('/status');
|
|
43
|
+
res.json(data);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
// Fall back to status file
|
|
46
|
+
try {
|
|
47
|
+
const statusPath = getStatusPath();
|
|
48
|
+
if (fs.existsSync(statusPath)) {
|
|
49
|
+
const status = JSON.parse(fs.readFileSync(statusPath, 'utf8'));
|
|
50
|
+
res.json({ ...status, live: false });
|
|
51
|
+
} else {
|
|
52
|
+
res.json({ activeWorkers: 0, queueLength: 0, workers: [], queue: [], paused: false, live: false });
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
res.json({ activeWorkers: 0, queueLength: 0, workers: [], queue: [], paused: false, live: false });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// GET /api/agent/events
|
|
61
|
+
router.get('/events', async (req, res) => {
|
|
62
|
+
try {
|
|
63
|
+
const data = await proxyToAgent('/events');
|
|
64
|
+
res.json(data);
|
|
65
|
+
} catch {
|
|
66
|
+
res.json([]);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// POST /api/agent/pause
|
|
71
|
+
router.post('/pause', async (req, res) => {
|
|
72
|
+
try {
|
|
73
|
+
const data = await proxyToAgent('/pause', 'POST');
|
|
74
|
+
res.json(data);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
res.status(503).json({ error: err.message });
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// POST /api/agent/resume
|
|
81
|
+
router.post('/resume', async (req, res) => {
|
|
82
|
+
try {
|
|
83
|
+
const data = await proxyToAgent('/resume', 'POST');
|
|
84
|
+
res.json(data);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
res.status(503).json({ error: err.message });
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// POST /api/agent/kill/:cardId
|
|
91
|
+
router.post('/kill/:cardId', async (req, res) => {
|
|
92
|
+
try {
|
|
93
|
+
const data = await proxyToAgent(`/kill/${req.params.cardId}`, 'POST');
|
|
94
|
+
res.json(data);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
res.status(503).json({ error: err.message });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// POST /api/agent/start — spawn watch daemon as child process
|
|
101
|
+
router.post('/start', (req, res) => {
|
|
102
|
+
if (isAgentRunning()) {
|
|
103
|
+
return res.json({ ok: true, message: 'Already running' });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const binPath = path.resolve(import.meta.dirname, '../../../bin/kanon.js');
|
|
107
|
+
const child = spawn(process.execPath, [binPath, 'watch'], {
|
|
108
|
+
stdio: 'ignore',
|
|
109
|
+
detached: true,
|
|
110
|
+
cwd: process.cwd(),
|
|
111
|
+
});
|
|
112
|
+
child.unref();
|
|
113
|
+
watchProcess = child;
|
|
114
|
+
|
|
115
|
+
// Wait briefly for the lock file to appear
|
|
116
|
+
let attempts = 0;
|
|
117
|
+
const check = setInterval(() => {
|
|
118
|
+
attempts++;
|
|
119
|
+
if (isAgentRunning()) {
|
|
120
|
+
clearInterval(check);
|
|
121
|
+
res.json({ ok: true });
|
|
122
|
+
} else if (attempts > 20) {
|
|
123
|
+
clearInterval(check);
|
|
124
|
+
res.status(500).json({ error: 'Watch daemon did not start in time' });
|
|
125
|
+
}
|
|
126
|
+
}, 250);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// POST /api/agent/stop — kill watch daemon
|
|
130
|
+
router.post('/stop', (req, res) => {
|
|
131
|
+
const lockPath = getAgentLockPath();
|
|
132
|
+
if (!fs.existsSync(lockPath)) {
|
|
133
|
+
return res.json({ ok: true, message: 'Not running' });
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
const lock = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
|
|
137
|
+
process.kill(lock.pid, 'SIGTERM');
|
|
138
|
+
watchProcess = null;
|
|
139
|
+
res.json({ ok: true });
|
|
140
|
+
} catch (err) {
|
|
141
|
+
res.status(500).json({ error: err.message });
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Auto-start watcher on server startup
|
|
146
|
+
if (!isAgentRunning()) {
|
|
147
|
+
const binPath = path.resolve(import.meta.dirname, '../../../bin/kanon.js');
|
|
148
|
+
const child = spawn(process.execPath, [binPath, 'watch'], {
|
|
149
|
+
stdio: 'ignore',
|
|
150
|
+
detached: true,
|
|
151
|
+
cwd: process.cwd(),
|
|
152
|
+
});
|
|
153
|
+
child.unref();
|
|
154
|
+
watchProcess = child;
|
|
155
|
+
console.log('Auto-starting watcher daemon…');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// SSE stream: polls agent status and forwards as events
|
|
159
|
+
router.get('/stream', (req, res) => {
|
|
160
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
161
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
162
|
+
res.setHeader('Connection', 'keep-alive');
|
|
163
|
+
res.setHeader('X-Accel-Buffering', 'no');
|
|
164
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
165
|
+
res.flushHeaders();
|
|
166
|
+
|
|
167
|
+
const emptyStatus = { activeWorkers: 0, queueLength: 0, workers: [], queue: [], paused: false, live: false };
|
|
168
|
+
|
|
169
|
+
// Send initial status immediately so client knows connection is alive
|
|
170
|
+
res.write(`event: status\ndata: ${JSON.stringify(emptyStatus)}\n\n`);
|
|
171
|
+
if (typeof res.flush === 'function') res.flush();
|
|
172
|
+
|
|
173
|
+
let lastEventCount = 0;
|
|
174
|
+
|
|
175
|
+
const interval = setInterval(async () => {
|
|
176
|
+
try {
|
|
177
|
+
const status = await proxyToAgent('/status');
|
|
178
|
+
res.write(`event: status\ndata: ${JSON.stringify({ ...status, live: true })}\n\n`);
|
|
179
|
+
|
|
180
|
+
const events = await proxyToAgent('/events');
|
|
181
|
+
if (Array.isArray(events) && events.length > lastEventCount) {
|
|
182
|
+
const newEvents = events.slice(lastEventCount);
|
|
183
|
+
for (const event of newEvents) {
|
|
184
|
+
res.write(`event: event\ndata: ${JSON.stringify(event)}\n\n`);
|
|
185
|
+
}
|
|
186
|
+
lastEventCount = events.length;
|
|
187
|
+
}
|
|
188
|
+
} catch {
|
|
189
|
+
// Agent not running — still send status so client stays connected
|
|
190
|
+
res.write(`event: status\ndata: ${JSON.stringify(emptyStatus)}\n\n`);
|
|
191
|
+
}
|
|
192
|
+
if (typeof res.flush === 'function') res.flush();
|
|
193
|
+
}, 2000);
|
|
194
|
+
|
|
195
|
+
req.on('close', () => {
|
|
196
|
+
clearInterval(interval);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
return router;
|
|
201
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { execSync, spawn } from 'child_process';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import { createProxyRoutes } from './proxy.js';
|
|
7
|
+
import { createSettingsRoutes } from './settings.js';
|
|
8
|
+
import { createAgentRoutes } from './agent.js';
|
|
9
|
+
import { getAgentLockPath } from '../../lib/config.js';
|
|
10
|
+
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
|
|
13
|
+
export function createDashboardServer(port = 3737) {
|
|
14
|
+
const app = express();
|
|
15
|
+
app.use(express.json());
|
|
16
|
+
|
|
17
|
+
// API routes
|
|
18
|
+
app.use('/api/kanon', createProxyRoutes());
|
|
19
|
+
app.use('/api/settings', createSettingsRoutes());
|
|
20
|
+
app.use('/api/agent', createAgentRoutes());
|
|
21
|
+
|
|
22
|
+
// POST /api/restart — restart server, optionally rebuild dashboard first
|
|
23
|
+
app.post('/api/restart', async (req, res) => {
|
|
24
|
+
const rebuild = req.query.rebuild === '1';
|
|
25
|
+
const steps = [];
|
|
26
|
+
|
|
27
|
+
// 1. Optionally rebuild dashboard frontend
|
|
28
|
+
if (rebuild) {
|
|
29
|
+
try {
|
|
30
|
+
const dashboardDir = path.join(__dirname, '..');
|
|
31
|
+
execSync('npm run build', { cwd: dashboardDir, stdio: 'pipe', timeout: 30000 });
|
|
32
|
+
steps.push('Dashboard rebuilt');
|
|
33
|
+
} catch (err) {
|
|
34
|
+
steps.push(`Dashboard build failed: ${err.message}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 2. Stop watch daemon
|
|
39
|
+
try {
|
|
40
|
+
const lockPath = getAgentLockPath();
|
|
41
|
+
if (fs.existsSync(lockPath)) {
|
|
42
|
+
const lock = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
|
|
43
|
+
process.kill(lock.pid, 'SIGTERM');
|
|
44
|
+
steps.push('Watch daemon stopped');
|
|
45
|
+
}
|
|
46
|
+
} catch {}
|
|
47
|
+
|
|
48
|
+
// 3. Respond before restarting ourselves
|
|
49
|
+
res.json({ ok: true, steps });
|
|
50
|
+
|
|
51
|
+
// 4. Restart the server process after a brief delay
|
|
52
|
+
setTimeout(() => {
|
|
53
|
+
const binPath = path.resolve(__dirname, '../../../bin/kanon.js');
|
|
54
|
+
const child = spawn(process.execPath, [binPath, 'dashboard', '--no-browser', '-p', String(port)], {
|
|
55
|
+
stdio: 'ignore',
|
|
56
|
+
detached: true,
|
|
57
|
+
cwd: process.cwd(),
|
|
58
|
+
});
|
|
59
|
+
child.unref();
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}, 500);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Serve built dashboard (production)
|
|
65
|
+
const distDir = path.join(__dirname, '..', 'dist');
|
|
66
|
+
if (fs.existsSync(distDir)) {
|
|
67
|
+
app.use(express.static(distDir));
|
|
68
|
+
app.get('/{*splat}', (req, res) => {
|
|
69
|
+
res.sendFile(path.join(distDir, 'index.html'));
|
|
70
|
+
});
|
|
71
|
+
} else {
|
|
72
|
+
app.get('/', (req, res) => {
|
|
73
|
+
res.json({
|
|
74
|
+
message: 'Dashboard not built yet. Run: cd cli/src/dashboard && npm run build',
|
|
75
|
+
hint: 'For development, run: cd cli/src/dashboard && npm run dev',
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const server = app.listen(port, () => {
|
|
81
|
+
console.log(`Dashboard server running on http://localhost:${port}`);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return server;
|
|
85
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { loadGlobalConfig, getServerUrl } from '../../lib/config.js';
|
|
3
|
+
|
|
4
|
+
export function createProxyRoutes() {
|
|
5
|
+
const router = Router();
|
|
6
|
+
|
|
7
|
+
// Proxy helper: forward requests to Kanon API
|
|
8
|
+
async function proxy(req, res, method, apiPath) {
|
|
9
|
+
const config = loadGlobalConfig();
|
|
10
|
+
const serverUrl = getServerUrl();
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
14
|
+
if (config.token) headers['Authorization'] = `Bearer ${config.token}`;
|
|
15
|
+
|
|
16
|
+
const opts = { method, headers };
|
|
17
|
+
if (req.body && Object.keys(req.body).length) {
|
|
18
|
+
opts.body = JSON.stringify(req.body);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const url = `${serverUrl}/api${apiPath}`;
|
|
22
|
+
const response = await fetch(url, opts);
|
|
23
|
+
const text = await response.text();
|
|
24
|
+
|
|
25
|
+
res.status(response.status);
|
|
26
|
+
res.set('Content-Type', 'application/json');
|
|
27
|
+
res.send(text);
|
|
28
|
+
} catch (err) {
|
|
29
|
+
res.status(502).json({ error: `Proxy error: ${err.message}` });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// GET /api/kanon/boards
|
|
34
|
+
router.get('/boards', (req, res) => proxy(req, res, 'GET', '/teams'));
|
|
35
|
+
|
|
36
|
+
// GET /api/kanon/boards/:id
|
|
37
|
+
router.get('/boards/:id', (req, res) => {
|
|
38
|
+
const { id } = req.params;
|
|
39
|
+
proxy(req, res, 'GET', `/boards/${id}?card_fields=essential&card_limit=20`);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// GET /api/kanon/boards/:id/cards
|
|
43
|
+
router.get('/boards/:id/cards', (req, res) => {
|
|
44
|
+
const { id } = req.params;
|
|
45
|
+
proxy(req, res, 'GET', `/boards/${id}?card_fields=essential&card_limit=50`);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// GET /api/kanon/cards/:id
|
|
49
|
+
router.get('/cards/:id', (req, res) => {
|
|
50
|
+
proxy(req, res, 'GET', `/cards/${req.params.id}`);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return router;
|
|
54
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import {
|
|
3
|
+
loadGlobalConfig,
|
|
4
|
+
saveGlobalConfig,
|
|
5
|
+
loadProjectConfig,
|
|
6
|
+
saveProjectConfig,
|
|
7
|
+
getServerUrl,
|
|
8
|
+
getAgentLockPath,
|
|
9
|
+
getActiveBoardIds,
|
|
10
|
+
getBoardConfig,
|
|
11
|
+
} from '../../lib/config.js';
|
|
12
|
+
import { spawn } from 'child_process';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import fs from 'fs';
|
|
15
|
+
import { DEFAULT_SYSTEM, DEFAULT_TASK, DEFAULT_BUNDLE } from '../../prompts/templates.js';
|
|
16
|
+
|
|
17
|
+
const DEFAULT_PROMPTS = {
|
|
18
|
+
system: DEFAULT_SYSTEM,
|
|
19
|
+
task: DEFAULT_TASK,
|
|
20
|
+
bundle: DEFAULT_BUNDLE,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function restartWatcher() {
|
|
24
|
+
// Stop existing watcher
|
|
25
|
+
const lockPath = getAgentLockPath();
|
|
26
|
+
try {
|
|
27
|
+
if (fs.existsSync(lockPath)) {
|
|
28
|
+
const lock = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
|
|
29
|
+
process.kill(lock.pid, 'SIGTERM');
|
|
30
|
+
}
|
|
31
|
+
} catch {}
|
|
32
|
+
|
|
33
|
+
// Start new watcher after brief delay
|
|
34
|
+
setTimeout(() => {
|
|
35
|
+
const binPath = path.resolve(import.meta.dirname, '../../../bin/kanon.js');
|
|
36
|
+
const child = spawn(process.execPath, [binPath, 'watch'], {
|
|
37
|
+
stdio: 'ignore',
|
|
38
|
+
detached: true,
|
|
39
|
+
cwd: process.cwd(),
|
|
40
|
+
});
|
|
41
|
+
child.unref();
|
|
42
|
+
}, 1000);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function createSettingsRoutes() {
|
|
46
|
+
const router = Router();
|
|
47
|
+
|
|
48
|
+
// GET /api/settings — returns global admin + board summary list
|
|
49
|
+
router.get('/', (req, res) => {
|
|
50
|
+
const config = loadProjectConfig();
|
|
51
|
+
const base = { _cwd: process.cwd() };
|
|
52
|
+
if (!config) {
|
|
53
|
+
return res.json({
|
|
54
|
+
...base,
|
|
55
|
+
_noConfig: true,
|
|
56
|
+
admin: { check_interval_seconds: 60, max_session_minutes: 30, stuck_timeout_minutes: 5, auto_restart_stuck: true, queue_max_size: 20 },
|
|
57
|
+
boards: [],
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
res.json({
|
|
61
|
+
...base,
|
|
62
|
+
admin: config.admin || {},
|
|
63
|
+
boards: (config.boards || []).map(b => ({ id: b.id, name: b.name, active: !!b.active })),
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// GET /api/settings/prompt-defaults — returns hardcoded default prompts
|
|
68
|
+
router.get('/prompt-defaults', (req, res) => {
|
|
69
|
+
res.json({ system: DEFAULT_SYSTEM, task: DEFAULT_TASK, bundle: DEFAULT_BUNDLE });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// PUT /api/settings — save global admin config only
|
|
73
|
+
router.put('/', (req, res) => {
|
|
74
|
+
try {
|
|
75
|
+
const config = loadProjectConfig() || {};
|
|
76
|
+
const { admin } = req.body;
|
|
77
|
+
config.admin = admin;
|
|
78
|
+
saveProjectConfig(config);
|
|
79
|
+
restartWatcher();
|
|
80
|
+
res.json({ ok: true });
|
|
81
|
+
} catch (err) {
|
|
82
|
+
res.status(500).json({ error: err.message });
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// GET /api/settings/boards/:id — full board config
|
|
87
|
+
router.get('/boards/:id', (req, res) => {
|
|
88
|
+
const config = loadProjectConfig();
|
|
89
|
+
if (!config?.boards) return res.status(404).json({ error: 'No boards configured' });
|
|
90
|
+
const board = config.boards.find(b => b.id === req.params.id);
|
|
91
|
+
if (!board) return res.status(404).json({ error: 'Board not found' });
|
|
92
|
+
// Ensure prompts have defaults
|
|
93
|
+
const prompts = {
|
|
94
|
+
system: board.prompts?.system || DEFAULT_PROMPTS.system,
|
|
95
|
+
task: board.prompts?.task || DEFAULT_PROMPTS.task,
|
|
96
|
+
bundle: board.prompts?.bundle || DEFAULT_PROMPTS.bundle,
|
|
97
|
+
};
|
|
98
|
+
res.json({ ...board, prompts, _cwd: process.cwd() });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// PUT /api/settings/boards/:id — save board config + restart watcher
|
|
102
|
+
router.put('/boards/:id', (req, res) => {
|
|
103
|
+
try {
|
|
104
|
+
const config = loadProjectConfig() || {};
|
|
105
|
+
if (!config.boards) config.boards = [];
|
|
106
|
+
const idx = config.boards.findIndex(b => b.id === req.params.id);
|
|
107
|
+
if (idx === -1) return res.status(404).json({ error: 'Board not found' });
|
|
108
|
+
// Preserve id and name if not in body
|
|
109
|
+
const { _cwd, ...boardData } = req.body;
|
|
110
|
+
config.boards[idx] = { ...config.boards[idx], ...boardData, id: req.params.id };
|
|
111
|
+
saveProjectConfig(config);
|
|
112
|
+
restartWatcher();
|
|
113
|
+
res.json({ ok: true });
|
|
114
|
+
} catch (err) {
|
|
115
|
+
res.status(500).json({ error: err.message });
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// POST /api/settings/boards — add a new board
|
|
120
|
+
router.post('/boards', (req, res) => {
|
|
121
|
+
try {
|
|
122
|
+
const config = loadProjectConfig() || {};
|
|
123
|
+
if (!config.boards) config.boards = [];
|
|
124
|
+
const { id, name } = req.body;
|
|
125
|
+
if (!id) return res.status(400).json({ error: 'Board id is required' });
|
|
126
|
+
if (config.boards.some(b => b.id === id)) return res.status(409).json({ error: 'Board already configured' });
|
|
127
|
+
config.boards.push({
|
|
128
|
+
id,
|
|
129
|
+
name: name || 'Untitled',
|
|
130
|
+
active: true,
|
|
131
|
+
bundle_queue: true,
|
|
132
|
+
claude: { command: 'claude', max_concurrent: 1 },
|
|
133
|
+
watch: { cooldown_seconds: 30, ignore_own_events: true },
|
|
134
|
+
rules: { labels: [], events: { agent_assigned: { enabled: true } } },
|
|
135
|
+
prompts: DEFAULT_PROMPTS,
|
|
136
|
+
});
|
|
137
|
+
saveProjectConfig(config);
|
|
138
|
+
restartWatcher();
|
|
139
|
+
res.json({ ok: true });
|
|
140
|
+
} catch (err) {
|
|
141
|
+
res.status(500).json({ error: err.message });
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// DELETE /api/settings/boards/:id — remove a board
|
|
146
|
+
router.delete('/boards/:id', (req, res) => {
|
|
147
|
+
try {
|
|
148
|
+
const config = loadProjectConfig() || {};
|
|
149
|
+
if (!config.boards) return res.status(404).json({ error: 'No boards' });
|
|
150
|
+
config.boards = config.boards.filter(b => b.id !== req.params.id);
|
|
151
|
+
saveProjectConfig(config);
|
|
152
|
+
restartWatcher();
|
|
153
|
+
res.json({ ok: true });
|
|
154
|
+
} catch (err) {
|
|
155
|
+
res.status(500).json({ error: err.message });
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// PATCH /api/settings/boards/:id/active — toggle board active state
|
|
160
|
+
router.patch('/boards/:id/active', (req, res) => {
|
|
161
|
+
try {
|
|
162
|
+
const config = loadProjectConfig() || {};
|
|
163
|
+
if (!config.boards) return res.status(404).json({ error: 'No boards' });
|
|
164
|
+
const board = config.boards.find(b => b.id === req.params.id);
|
|
165
|
+
if (!board) return res.status(404).json({ error: 'Board not found' });
|
|
166
|
+
board.active = !!req.body.active;
|
|
167
|
+
saveProjectConfig(config);
|
|
168
|
+
restartWatcher();
|
|
169
|
+
res.json({ ok: true });
|
|
170
|
+
} catch (err) {
|
|
171
|
+
res.status(500).json({ error: err.message });
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// GET /api/settings/credentials — returns global config (masked password)
|
|
176
|
+
router.get('/credentials', (req, res) => {
|
|
177
|
+
const config = loadGlobalConfig();
|
|
178
|
+
res.json({
|
|
179
|
+
server_url: config.server_url || '',
|
|
180
|
+
email: config.email || '',
|
|
181
|
+
password: config.password ? '********' : '',
|
|
182
|
+
token: config.token || '',
|
|
183
|
+
user_name: config.user_name || '',
|
|
184
|
+
user_id: config.user_id || '',
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// PUT /api/settings/credentials — update global config
|
|
189
|
+
router.put('/credentials', (req, res) => {
|
|
190
|
+
try {
|
|
191
|
+
const current = loadGlobalConfig();
|
|
192
|
+
const update = { ...current };
|
|
193
|
+
|
|
194
|
+
if (req.body.server_url) update.server_url = req.body.server_url;
|
|
195
|
+
if (req.body.email) update.email = req.body.email;
|
|
196
|
+
if (req.body.password && req.body.password !== '********') {
|
|
197
|
+
update.password = req.body.password;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
saveGlobalConfig(update);
|
|
201
|
+
res.json({ ok: true });
|
|
202
|
+
} catch (err) {
|
|
203
|
+
res.status(500).json({ error: err.message });
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// POST /api/settings/test-connection — test login with stored credentials
|
|
208
|
+
router.post('/test-connection', async (req, res) => {
|
|
209
|
+
try {
|
|
210
|
+
const config = loadGlobalConfig();
|
|
211
|
+
const serverUrl = getServerUrl();
|
|
212
|
+
|
|
213
|
+
const response = await fetch(`${serverUrl}/api/auth/login`, {
|
|
214
|
+
method: 'POST',
|
|
215
|
+
headers: { 'Content-Type': 'application/json' },
|
|
216
|
+
body: JSON.stringify({ email: config.email, password: config.password }),
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
if (!response.ok) {
|
|
220
|
+
const data = await response.json().catch(() => ({}));
|
|
221
|
+
return res.status(401).json({ error: data.error || 'Login failed' });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const data = await response.json();
|
|
225
|
+
|
|
226
|
+
// Update token
|
|
227
|
+
saveGlobalConfig({ ...config, token: data.token, user_id: data.user.id, user_name: data.user.name });
|
|
228
|
+
|
|
229
|
+
res.json({ ok: true, user_name: data.user.name });
|
|
230
|
+
} catch (err) {
|
|
231
|
+
res.status(500).json({ error: err.message });
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
return router;
|
|
236
|
+
}
|