groove-dev 0.11.0 → 0.12.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/node_modules/@groove-dev/cli/bin/groove.js +32 -0
- package/node_modules/@groove-dev/cli/src/commands/audit.js +60 -0
- package/node_modules/@groove-dev/cli/src/commands/connect.js +279 -0
- package/node_modules/@groove-dev/cli/src/commands/disconnect.js +91 -0
- package/node_modules/@groove-dev/cli/src/commands/federation.js +84 -0
- package/node_modules/@groove-dev/cli/src/commands/start.js +7 -2
- package/node_modules/@groove-dev/cli/src/commands/status.js +4 -1
- package/node_modules/@groove-dev/daemon/src/api.js +106 -2
- package/node_modules/@groove-dev/daemon/src/audit.js +65 -0
- package/node_modules/@groove-dev/daemon/src/federation.js +352 -0
- package/node_modules/@groove-dev/daemon/src/firstrun.js +27 -2
- package/node_modules/@groove-dev/daemon/src/index.js +59 -6
- package/node_modules/@groove-dev/gui/dist/assets/{index-BqZnnVJF.js → index-B49YqEXS.js} +17 -17
- package/node_modules/@groove-dev/gui/dist/assets/{index-CPzm9ZE9.css → index-Gfb8Zxy9.css} +1 -1
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/src/App.jsx +24 -1
- package/node_modules/@groove-dev/gui/src/components/SpawnPanel.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/stores/groove.js +19 -2
- package/node_modules/@groove-dev/gui/src/theme.css +2 -2
- package/package.json +1 -1
- package/packages/cli/bin/groove.js +32 -0
- package/packages/cli/src/commands/audit.js +60 -0
- package/packages/cli/src/commands/connect.js +279 -0
- package/packages/cli/src/commands/disconnect.js +91 -0
- package/packages/cli/src/commands/federation.js +84 -0
- package/packages/cli/src/commands/start.js +7 -2
- package/packages/cli/src/commands/status.js +4 -1
- package/packages/daemon/src/api.js +106 -2
- package/packages/daemon/src/audit.js +65 -0
- package/packages/daemon/src/federation.js +352 -0
- package/packages/daemon/src/firstrun.js +27 -2
- package/packages/daemon/src/index.js +59 -6
- package/packages/gui/dist/assets/{index-BqZnnVJF.js → index-B49YqEXS.js} +17 -17
- package/packages/gui/dist/assets/{index-CPzm9ZE9.css → index-Gfb8Zxy9.css} +1 -1
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/src/App.jsx +24 -1
- package/packages/gui/src/components/SpawnPanel.jsx +1 -1
- package/packages/gui/src/stores/groove.js +19 -2
- package/packages/gui/src/theme.css +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
@import"https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700;800&display=swap";:root{--bg-base: #24282f;--bg-chrome: #282c34;--bg-surface: #2c313a;--bg-hover: #333842;--bg-active: #3a3f4b;--text-primary: #abb2bf;--text-bright: #e6e6e6;--text-dim: #5c6370;--text-muted: #3e4451;--border: #4b5263;--accent: #33afbc;--green: #4ae168;--amber: #e5c07b;--red: #e06c75;--purple: #c678dd;--blue: #61afef;--font: "JetBrains Mono", "SF Mono", "Fira Code", "Cascadia Code", Consolas, monospace}*{margin:0;padding:0;box-sizing:border-box}body{font-family:var(--font);background:var(--bg-base);color:var(--text-primary);font-size:12px;-webkit-font-smoothing:antialiased}#root{width:100vw;height:100vh}::-webkit-scrollbar{width:6px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--bg-hover);border-radius:2px}::selection{background:#33afbc40}@keyframes pulse{0%,to{opacity:1}50%{opacity:.5}}@keyframes neuralFlow{0%{transform:translate(-50%)}to{transform:translate(0)}}.react-flow__edge-path{stroke-width:1!important}.react-flow__edge.animated path{stroke-dasharray:6 4!important;stroke-width:1!important}.react-flow{direction:ltr;--xy-edge-stroke-default: #b1b1b7;--xy-edge-stroke-width-default: 1;--xy-edge-stroke-selected-default: #555;--xy-connectionline-stroke-default: #b1b1b7;--xy-connectionline-stroke-width-default: 1;--xy-attribution-background-color-default: rgba(255, 255, 255, .5);--xy-minimap-background-color-default: #fff;--xy-minimap-mask-background-color-default: rgba(240, 240, 240, .6);--xy-minimap-mask-stroke-color-default: transparent;--xy-minimap-mask-stroke-width-default: 1;--xy-minimap-node-background-color-default: #e2e2e2;--xy-minimap-node-stroke-color-default: transparent;--xy-minimap-node-stroke-width-default: 2;--xy-background-color-default: transparent;--xy-background-pattern-dots-color-default: #91919a;--xy-background-pattern-lines-color-default: #eee;--xy-background-pattern-cross-color-default: #e2e2e2;background-color:var(--xy-background-color, var(--xy-background-color-default));--xy-node-color-default: inherit;--xy-node-border-default: 1px solid #1a192b;--xy-node-background-color-default: #fff;--xy-node-group-background-color-default: rgba(240, 240, 240, .25);--xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(0, 0, 0, .08);--xy-node-boxshadow-selected-default: 0 0 0 .5px #1a192b;--xy-node-border-radius-default: 3px;--xy-handle-background-color-default: #1a192b;--xy-handle-border-color-default: #fff;--xy-selection-background-color-default: rgba(0, 89, 220, .08);--xy-selection-border-default: 1px dotted rgba(0, 89, 220, .8);--xy-controls-button-background-color-default: #fefefe;--xy-controls-button-background-color-hover-default: #f4f4f4;--xy-controls-button-color-default: inherit;--xy-controls-button-color-hover-default: inherit;--xy-controls-button-border-color-default: #eee;--xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, .08);--xy-edge-label-background-color-default: #ffffff;--xy-edge-label-color-default: inherit;--xy-resize-background-color-default: #3367d9}.react-flow.dark{--xy-edge-stroke-default: #3e3e3e;--xy-edge-stroke-width-default: 1;--xy-edge-stroke-selected-default: #727272;--xy-connectionline-stroke-default: #b1b1b7;--xy-connectionline-stroke-width-default: 1;--xy-attribution-background-color-default: rgba(150, 150, 150, .25);--xy-minimap-background-color-default: #141414;--xy-minimap-mask-background-color-default: rgba(60, 60, 60, .6);--xy-minimap-mask-stroke-color-default: transparent;--xy-minimap-mask-stroke-width-default: 1;--xy-minimap-node-background-color-default: #2b2b2b;--xy-minimap-node-stroke-color-default: transparent;--xy-minimap-node-stroke-width-default: 2;--xy-background-color-default: #141414;--xy-background-pattern-dots-color-default: #777;--xy-background-pattern-lines-color-default: #777;--xy-background-pattern-cross-color-default: #777;--xy-node-color-default: #f8f8f8;--xy-node-border-default: 1px solid #3c3c3c;--xy-node-background-color-default: #1e1e1e;--xy-node-group-background-color-default: rgba(240, 240, 240, .25);--xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(255, 255, 255, .08);--xy-node-boxshadow-selected-default: 0 0 0 .5px #999;--xy-handle-background-color-default: #bebebe;--xy-handle-border-color-default: #1e1e1e;--xy-selection-background-color-default: rgba(200, 200, 220, .08);--xy-selection-border-default: 1px dotted rgba(200, 200, 220, .8);--xy-controls-button-background-color-default: #2b2b2b;--xy-controls-button-background-color-hover-default: #3e3e3e;--xy-controls-button-color-default: #f8f8f8;--xy-controls-button-color-hover-default: #fff;--xy-controls-button-border-color-default: #5b5b5b;--xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, .08);--xy-edge-label-background-color-default: #141414;--xy-edge-label-color-default: #f8f8f8}.react-flow__background{background-color:var(--xy-background-color-props, var(--xy-background-color, var(--xy-background-color-default)));pointer-events:none;z-index:-1}.react-flow__container{position:absolute;width:100%;height:100%;top:0;left:0}.react-flow__pane{z-index:1}.react-flow__pane.draggable{cursor:grab}.react-flow__pane.dragging{cursor:grabbing}.react-flow__pane.selection{cursor:pointer}.react-flow__viewport{transform-origin:0 0;z-index:2;pointer-events:none}.react-flow__renderer{z-index:4}.react-flow__selection{z-index:6}.react-flow__nodesselection-rect:focus,.react-flow__nodesselection-rect:focus-visible{outline:none}.react-flow__edge-path{stroke:var(--xy-edge-stroke, var(--xy-edge-stroke-default));stroke-width:var(--xy-edge-stroke-width, var(--xy-edge-stroke-width-default));fill:none}.react-flow__connection-path{stroke:var(--xy-connectionline-stroke, var(--xy-connectionline-stroke-default));stroke-width:var(--xy-connectionline-stroke-width, var(--xy-connectionline-stroke-width-default));fill:none}.react-flow .react-flow__edges{position:absolute}.react-flow .react-flow__edges svg{overflow:visible;position:absolute;pointer-events:none}.react-flow__edge{pointer-events:visibleStroke}.react-flow__edge.selectable{cursor:pointer}.react-flow__edge.animated path{stroke-dasharray:5;animation:dashdraw .5s linear infinite}.react-flow__edge.animated path.react-flow__edge-interaction{stroke-dasharray:none;animation:none}.react-flow__edge.inactive{pointer-events:none}.react-flow__edge.selected,.react-flow__edge:focus,.react-flow__edge:focus-visible{outline:none}.react-flow__edge.selected .react-flow__edge-path,.react-flow__edge.selectable:focus .react-flow__edge-path,.react-flow__edge.selectable:focus-visible .react-flow__edge-path{stroke:var(--xy-edge-stroke-selected, var(--xy-edge-stroke-selected-default))}.react-flow__edge-textwrapper{pointer-events:all}.react-flow__edge .react-flow__edge-text{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__arrowhead polyline{stroke:var(--xy-edge-stroke, var(--xy-edge-stroke-default))}.react-flow__arrowhead polyline.arrowclosed{fill:var(--xy-edge-stroke, var(--xy-edge-stroke-default))}.react-flow__connection{pointer-events:none}.react-flow__connection .animated{stroke-dasharray:5;animation:dashdraw .5s linear infinite}svg.react-flow__connectionline{z-index:1001;overflow:visible;position:absolute}.react-flow__nodes{pointer-events:none;transform-origin:0 0}.react-flow__node{position:absolute;-webkit-user-select:none;-moz-user-select:none;user-select:none;pointer-events:all;transform-origin:0 0;box-sizing:border-box;cursor:default}.react-flow__node.selectable{cursor:pointer}.react-flow__node.draggable{cursor:grab;pointer-events:all}.react-flow__node.draggable.dragging{cursor:grabbing}.react-flow__nodesselection{z-index:3;transform-origin:left top;pointer-events:none}.react-flow__nodesselection-rect{position:absolute;pointer-events:all;cursor:grab}.react-flow__handle{position:absolute;pointer-events:none;min-width:5px;min-height:5px;width:6px;height:6px;background-color:var(--xy-handle-background-color, var(--xy-handle-background-color-default));border:1px solid var(--xy-handle-border-color, var(--xy-handle-border-color-default));border-radius:100%}.react-flow__handle.connectingfrom{pointer-events:all}.react-flow__handle.connectionindicator{pointer-events:all;cursor:crosshair}.react-flow__handle-bottom{top:auto;left:50%;bottom:0;transform:translate(-50%,50%)}.react-flow__handle-top{top:0;left:50%;transform:translate(-50%,-50%)}.react-flow__handle-left{top:50%;left:0;transform:translate(-50%,-50%)}.react-flow__handle-right{top:50%;right:0;transform:translate(50%,-50%)}.react-flow__edgeupdater{cursor:move;pointer-events:all}.react-flow__pane.selection .react-flow__panel{pointer-events:none}.react-flow__panel{position:absolute;z-index:5;margin:15px}.react-flow__panel.top{top:0}.react-flow__panel.bottom{bottom:0}.react-flow__panel.top.center,.react-flow__panel.bottom.center{left:50%;transform:translate(-15px) translate(-50%)}.react-flow__panel.left{left:0}.react-flow__panel.right{right:0}.react-flow__panel.left.center,.react-flow__panel.right.center{top:50%;transform:translateY(-15px) translateY(-50%)}.react-flow__attribution{font-size:10px;background:var(--xy-attribution-background-color, var(--xy-attribution-background-color-default));padding:2px 3px;margin:0}.react-flow__attribution a{text-decoration:none;color:#999}@keyframes dashdraw{0%{stroke-dashoffset:10}}.react-flow__edgelabel-renderer{position:absolute;width:100%;height:100%;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;left:0;top:0}.react-flow__viewport-portal{position:absolute;width:100%;height:100%;left:0;top:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__minimap{background:var( --xy-minimap-background-color-props, var(--xy-minimap-background-color, var(--xy-minimap-background-color-default)) )}.react-flow__minimap-svg{display:block}.react-flow__minimap-mask{fill:var( --xy-minimap-mask-background-color-props, var(--xy-minimap-mask-background-color, var(--xy-minimap-mask-background-color-default)) );stroke:var( --xy-minimap-mask-stroke-color-props, var(--xy-minimap-mask-stroke-color, var(--xy-minimap-mask-stroke-color-default)) );stroke-width:var( --xy-minimap-mask-stroke-width-props, var(--xy-minimap-mask-stroke-width, var(--xy-minimap-mask-stroke-width-default)) )}.react-flow__minimap-node{fill:var( --xy-minimap-node-background-color-props, var(--xy-minimap-node-background-color, var(--xy-minimap-node-background-color-default)) );stroke:var( --xy-minimap-node-stroke-color-props, var(--xy-minimap-node-stroke-color, var(--xy-minimap-node-stroke-color-default)) );stroke-width:var( --xy-minimap-node-stroke-width-props, var(--xy-minimap-node-stroke-width, var(--xy-minimap-node-stroke-width-default)) )}.react-flow__background-pattern.dots{fill:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-dots-color-default)) )}.react-flow__background-pattern.lines{stroke:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-lines-color-default)) )}.react-flow__background-pattern.cross{stroke:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-cross-color-default)) )}.react-flow__controls{display:flex;flex-direction:column;box-shadow:var(--xy-controls-box-shadow, var(--xy-controls-box-shadow-default))}.react-flow__controls.horizontal{flex-direction:row}.react-flow__controls-button{display:flex;justify-content:center;align-items:center;height:26px;width:26px;padding:4px;border:none;background:var(--xy-controls-button-background-color, var(--xy-controls-button-background-color-default));border-bottom:1px solid var( --xy-controls-button-border-color-props, var(--xy-controls-button-border-color, var(--xy-controls-button-border-color-default)) );color:var( --xy-controls-button-color-props, var(--xy-controls-button-color, var(--xy-controls-button-color-default)) );cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__controls-button svg{width:100%;max-width:12px;max-height:12px;fill:currentColor}.react-flow__edge.updating .react-flow__edge-path{stroke:#777}.react-flow__edge-text{font-size:10px}.react-flow__node.selectable:focus,.react-flow__node.selectable:focus-visible{outline:none}.react-flow__node-input,.react-flow__node-default,.react-flow__node-output,.react-flow__node-group{padding:10px;border-radius:var(--xy-node-border-radius, var(--xy-node-border-radius-default));width:150px;font-size:12px;color:var(--xy-node-color, var(--xy-node-color-default));text-align:center;border:var(--xy-node-border, var(--xy-node-border-default));background-color:var(--xy-node-background-color, var(--xy-node-background-color-default))}.react-flow__node-input.selectable:hover,.react-flow__node-default.selectable:hover,.react-flow__node-output.selectable:hover,.react-flow__node-group.selectable:hover{box-shadow:var(--xy-node-boxshadow-hover, var(--xy-node-boxshadow-hover-default))}.react-flow__node-input.selectable.selected,.react-flow__node-input.selectable:focus,.react-flow__node-input.selectable:focus-visible,.react-flow__node-default.selectable.selected,.react-flow__node-default.selectable:focus,.react-flow__node-default.selectable:focus-visible,.react-flow__node-output.selectable.selected,.react-flow__node-output.selectable:focus,.react-flow__node-output.selectable:focus-visible,.react-flow__node-group.selectable.selected,.react-flow__node-group.selectable:focus,.react-flow__node-group.selectable:focus-visible{box-shadow:var(--xy-node-boxshadow-selected, var(--xy-node-boxshadow-selected-default))}.react-flow__node-group{background-color:var(--xy-node-group-background-color, var(--xy-node-group-background-color-default))}.react-flow__nodesselection-rect,.react-flow__selection{background:var(--xy-selection-background-color, var(--xy-selection-background-color-default));border:var(--xy-selection-border, var(--xy-selection-border-default))}.react-flow__nodesselection-rect:focus,.react-flow__nodesselection-rect:focus-visible,.react-flow__selection:focus,.react-flow__selection:focus-visible{outline:none}.react-flow__controls-button:hover{background:var( --xy-controls-button-background-color-hover-props, var(--xy-controls-button-background-color-hover, var(--xy-controls-button-background-color-hover-default)) );color:var( --xy-controls-button-color-hover-props, var(--xy-controls-button-color-hover, var(--xy-controls-button-color-hover-default)) )}.react-flow__controls-button:disabled{pointer-events:none}.react-flow__controls-button:disabled svg{fill-opacity:.4}.react-flow__controls-button:last-child{border-bottom:none}.react-flow__controls.horizontal .react-flow__controls-button{border-bottom:none;border-right:1px solid var( --xy-controls-button-border-color-props, var(--xy-controls-button-border-color, var(--xy-controls-button-border-color-default)) )}.react-flow__controls.horizontal .react-flow__controls-button:last-child{border-right:none}.react-flow__resize-control{position:absolute}.react-flow__resize-control.left,.react-flow__resize-control.right{cursor:ew-resize}.react-flow__resize-control.top,.react-flow__resize-control.bottom{cursor:ns-resize}.react-flow__resize-control.top.left,.react-flow__resize-control.bottom.right{cursor:nwse-resize}.react-flow__resize-control.bottom.left,.react-flow__resize-control.top.right{cursor:nesw-resize}.react-flow__resize-control.handle{width:5px;height:5px;border:1px solid #fff;border-radius:1px;background-color:var(--xy-resize-background-color, var(--xy-resize-background-color-default));translate:-50% -50%}.react-flow__resize-control.handle.left{left:0;top:50%}.react-flow__resize-control.handle.right{left:100%;top:50%}.react-flow__resize-control.handle.top{left:50%;top:0}.react-flow__resize-control.handle.bottom{left:50%;top:100%}.react-flow__resize-control.handle.top.left,.react-flow__resize-control.handle.bottom.left{left:0}.react-flow__resize-control.handle.top.right,.react-flow__resize-control.handle.bottom.right{left:100%}.react-flow__resize-control.line{border-color:var(--xy-resize-background-color, var(--xy-resize-background-color-default));border-width:0;border-style:solid}.react-flow__resize-control.line.left,.react-flow__resize-control.line.right{width:1px;transform:translate(-50%);top:0;height:100%}.react-flow__resize-control.line.left{left:0;border-left-width:1px}.react-flow__resize-control.line.right{left:100%;border-right-width:1px}.react-flow__resize-control.line.top,.react-flow__resize-control.line.bottom{height:1px;transform:translateY(-50%);left:0;width:100%}.react-flow__resize-control.line.top{top:0;border-top-width:1px}.react-flow__resize-control.line.bottom{border-bottom-width:1px;top:100%}.react-flow__edge-textbg{fill:var(--xy-edge-label-background-color, var(--xy-edge-label-background-color-default))}.react-flow__edge-text{fill:var(--xy-edge-label-color, var(--xy-edge-label-color-default))}
|
|
1
|
+
@import"https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700;800&display=swap";:root{--bg-base: #24282f;--bg-chrome: #282c34;--bg-surface: #2c313a;--bg-hover: #333842;--bg-active: #3a3f4b;--text-primary: #abb2bf;--text-bright: #e6e6e6;--text-dim: #7a8394;--text-muted: #5c6370;--border: #4b5263;--accent: #33afbc;--green: #4ae168;--amber: #e5c07b;--red: #e06c75;--purple: #c678dd;--blue: #61afef;--font: "JetBrains Mono", "SF Mono", "Fira Code", "Cascadia Code", Consolas, monospace}*{margin:0;padding:0;box-sizing:border-box}body{font-family:var(--font);background:var(--bg-base);color:var(--text-primary);font-size:12px;-webkit-font-smoothing:antialiased}#root{width:100vw;height:100vh}::-webkit-scrollbar{width:6px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--bg-hover);border-radius:2px}::selection{background:#33afbc40}@keyframes pulse{0%,to{opacity:1}50%{opacity:.5}}@keyframes neuralFlow{0%{transform:translate(-50%)}to{transform:translate(0)}}.react-flow__edge-path{stroke-width:1!important}.react-flow__edge.animated path{stroke-dasharray:6 4!important;stroke-width:1!important}.react-flow{direction:ltr;--xy-edge-stroke-default: #b1b1b7;--xy-edge-stroke-width-default: 1;--xy-edge-stroke-selected-default: #555;--xy-connectionline-stroke-default: #b1b1b7;--xy-connectionline-stroke-width-default: 1;--xy-attribution-background-color-default: rgba(255, 255, 255, .5);--xy-minimap-background-color-default: #fff;--xy-minimap-mask-background-color-default: rgba(240, 240, 240, .6);--xy-minimap-mask-stroke-color-default: transparent;--xy-minimap-mask-stroke-width-default: 1;--xy-minimap-node-background-color-default: #e2e2e2;--xy-minimap-node-stroke-color-default: transparent;--xy-minimap-node-stroke-width-default: 2;--xy-background-color-default: transparent;--xy-background-pattern-dots-color-default: #91919a;--xy-background-pattern-lines-color-default: #eee;--xy-background-pattern-cross-color-default: #e2e2e2;background-color:var(--xy-background-color, var(--xy-background-color-default));--xy-node-color-default: inherit;--xy-node-border-default: 1px solid #1a192b;--xy-node-background-color-default: #fff;--xy-node-group-background-color-default: rgba(240, 240, 240, .25);--xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(0, 0, 0, .08);--xy-node-boxshadow-selected-default: 0 0 0 .5px #1a192b;--xy-node-border-radius-default: 3px;--xy-handle-background-color-default: #1a192b;--xy-handle-border-color-default: #fff;--xy-selection-background-color-default: rgba(0, 89, 220, .08);--xy-selection-border-default: 1px dotted rgba(0, 89, 220, .8);--xy-controls-button-background-color-default: #fefefe;--xy-controls-button-background-color-hover-default: #f4f4f4;--xy-controls-button-color-default: inherit;--xy-controls-button-color-hover-default: inherit;--xy-controls-button-border-color-default: #eee;--xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, .08);--xy-edge-label-background-color-default: #ffffff;--xy-edge-label-color-default: inherit;--xy-resize-background-color-default: #3367d9}.react-flow.dark{--xy-edge-stroke-default: #3e3e3e;--xy-edge-stroke-width-default: 1;--xy-edge-stroke-selected-default: #727272;--xy-connectionline-stroke-default: #b1b1b7;--xy-connectionline-stroke-width-default: 1;--xy-attribution-background-color-default: rgba(150, 150, 150, .25);--xy-minimap-background-color-default: #141414;--xy-minimap-mask-background-color-default: rgba(60, 60, 60, .6);--xy-minimap-mask-stroke-color-default: transparent;--xy-minimap-mask-stroke-width-default: 1;--xy-minimap-node-background-color-default: #2b2b2b;--xy-minimap-node-stroke-color-default: transparent;--xy-minimap-node-stroke-width-default: 2;--xy-background-color-default: #141414;--xy-background-pattern-dots-color-default: #777;--xy-background-pattern-lines-color-default: #777;--xy-background-pattern-cross-color-default: #777;--xy-node-color-default: #f8f8f8;--xy-node-border-default: 1px solid #3c3c3c;--xy-node-background-color-default: #1e1e1e;--xy-node-group-background-color-default: rgba(240, 240, 240, .25);--xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(255, 255, 255, .08);--xy-node-boxshadow-selected-default: 0 0 0 .5px #999;--xy-handle-background-color-default: #bebebe;--xy-handle-border-color-default: #1e1e1e;--xy-selection-background-color-default: rgba(200, 200, 220, .08);--xy-selection-border-default: 1px dotted rgba(200, 200, 220, .8);--xy-controls-button-background-color-default: #2b2b2b;--xy-controls-button-background-color-hover-default: #3e3e3e;--xy-controls-button-color-default: #f8f8f8;--xy-controls-button-color-hover-default: #fff;--xy-controls-button-border-color-default: #5b5b5b;--xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, .08);--xy-edge-label-background-color-default: #141414;--xy-edge-label-color-default: #f8f8f8}.react-flow__background{background-color:var(--xy-background-color-props, var(--xy-background-color, var(--xy-background-color-default)));pointer-events:none;z-index:-1}.react-flow__container{position:absolute;width:100%;height:100%;top:0;left:0}.react-flow__pane{z-index:1}.react-flow__pane.draggable{cursor:grab}.react-flow__pane.dragging{cursor:grabbing}.react-flow__pane.selection{cursor:pointer}.react-flow__viewport{transform-origin:0 0;z-index:2;pointer-events:none}.react-flow__renderer{z-index:4}.react-flow__selection{z-index:6}.react-flow__nodesselection-rect:focus,.react-flow__nodesselection-rect:focus-visible{outline:none}.react-flow__edge-path{stroke:var(--xy-edge-stroke, var(--xy-edge-stroke-default));stroke-width:var(--xy-edge-stroke-width, var(--xy-edge-stroke-width-default));fill:none}.react-flow__connection-path{stroke:var(--xy-connectionline-stroke, var(--xy-connectionline-stroke-default));stroke-width:var(--xy-connectionline-stroke-width, var(--xy-connectionline-stroke-width-default));fill:none}.react-flow .react-flow__edges{position:absolute}.react-flow .react-flow__edges svg{overflow:visible;position:absolute;pointer-events:none}.react-flow__edge{pointer-events:visibleStroke}.react-flow__edge.selectable{cursor:pointer}.react-flow__edge.animated path{stroke-dasharray:5;animation:dashdraw .5s linear infinite}.react-flow__edge.animated path.react-flow__edge-interaction{stroke-dasharray:none;animation:none}.react-flow__edge.inactive{pointer-events:none}.react-flow__edge.selected,.react-flow__edge:focus,.react-flow__edge:focus-visible{outline:none}.react-flow__edge.selected .react-flow__edge-path,.react-flow__edge.selectable:focus .react-flow__edge-path,.react-flow__edge.selectable:focus-visible .react-flow__edge-path{stroke:var(--xy-edge-stroke-selected, var(--xy-edge-stroke-selected-default))}.react-flow__edge-textwrapper{pointer-events:all}.react-flow__edge .react-flow__edge-text{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__arrowhead polyline{stroke:var(--xy-edge-stroke, var(--xy-edge-stroke-default))}.react-flow__arrowhead polyline.arrowclosed{fill:var(--xy-edge-stroke, var(--xy-edge-stroke-default))}.react-flow__connection{pointer-events:none}.react-flow__connection .animated{stroke-dasharray:5;animation:dashdraw .5s linear infinite}svg.react-flow__connectionline{z-index:1001;overflow:visible;position:absolute}.react-flow__nodes{pointer-events:none;transform-origin:0 0}.react-flow__node{position:absolute;-webkit-user-select:none;-moz-user-select:none;user-select:none;pointer-events:all;transform-origin:0 0;box-sizing:border-box;cursor:default}.react-flow__node.selectable{cursor:pointer}.react-flow__node.draggable{cursor:grab;pointer-events:all}.react-flow__node.draggable.dragging{cursor:grabbing}.react-flow__nodesselection{z-index:3;transform-origin:left top;pointer-events:none}.react-flow__nodesselection-rect{position:absolute;pointer-events:all;cursor:grab}.react-flow__handle{position:absolute;pointer-events:none;min-width:5px;min-height:5px;width:6px;height:6px;background-color:var(--xy-handle-background-color, var(--xy-handle-background-color-default));border:1px solid var(--xy-handle-border-color, var(--xy-handle-border-color-default));border-radius:100%}.react-flow__handle.connectingfrom{pointer-events:all}.react-flow__handle.connectionindicator{pointer-events:all;cursor:crosshair}.react-flow__handle-bottom{top:auto;left:50%;bottom:0;transform:translate(-50%,50%)}.react-flow__handle-top{top:0;left:50%;transform:translate(-50%,-50%)}.react-flow__handle-left{top:50%;left:0;transform:translate(-50%,-50%)}.react-flow__handle-right{top:50%;right:0;transform:translate(50%,-50%)}.react-flow__edgeupdater{cursor:move;pointer-events:all}.react-flow__pane.selection .react-flow__panel{pointer-events:none}.react-flow__panel{position:absolute;z-index:5;margin:15px}.react-flow__panel.top{top:0}.react-flow__panel.bottom{bottom:0}.react-flow__panel.top.center,.react-flow__panel.bottom.center{left:50%;transform:translate(-15px) translate(-50%)}.react-flow__panel.left{left:0}.react-flow__panel.right{right:0}.react-flow__panel.left.center,.react-flow__panel.right.center{top:50%;transform:translateY(-15px) translateY(-50%)}.react-flow__attribution{font-size:10px;background:var(--xy-attribution-background-color, var(--xy-attribution-background-color-default));padding:2px 3px;margin:0}.react-flow__attribution a{text-decoration:none;color:#999}@keyframes dashdraw{0%{stroke-dashoffset:10}}.react-flow__edgelabel-renderer{position:absolute;width:100%;height:100%;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;left:0;top:0}.react-flow__viewport-portal{position:absolute;width:100%;height:100%;left:0;top:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__minimap{background:var( --xy-minimap-background-color-props, var(--xy-minimap-background-color, var(--xy-minimap-background-color-default)) )}.react-flow__minimap-svg{display:block}.react-flow__minimap-mask{fill:var( --xy-minimap-mask-background-color-props, var(--xy-minimap-mask-background-color, var(--xy-minimap-mask-background-color-default)) );stroke:var( --xy-minimap-mask-stroke-color-props, var(--xy-minimap-mask-stroke-color, var(--xy-minimap-mask-stroke-color-default)) );stroke-width:var( --xy-minimap-mask-stroke-width-props, var(--xy-minimap-mask-stroke-width, var(--xy-minimap-mask-stroke-width-default)) )}.react-flow__minimap-node{fill:var( --xy-minimap-node-background-color-props, var(--xy-minimap-node-background-color, var(--xy-minimap-node-background-color-default)) );stroke:var( --xy-minimap-node-stroke-color-props, var(--xy-minimap-node-stroke-color, var(--xy-minimap-node-stroke-color-default)) );stroke-width:var( --xy-minimap-node-stroke-width-props, var(--xy-minimap-node-stroke-width, var(--xy-minimap-node-stroke-width-default)) )}.react-flow__background-pattern.dots{fill:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-dots-color-default)) )}.react-flow__background-pattern.lines{stroke:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-lines-color-default)) )}.react-flow__background-pattern.cross{stroke:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-cross-color-default)) )}.react-flow__controls{display:flex;flex-direction:column;box-shadow:var(--xy-controls-box-shadow, var(--xy-controls-box-shadow-default))}.react-flow__controls.horizontal{flex-direction:row}.react-flow__controls-button{display:flex;justify-content:center;align-items:center;height:26px;width:26px;padding:4px;border:none;background:var(--xy-controls-button-background-color, var(--xy-controls-button-background-color-default));border-bottom:1px solid var( --xy-controls-button-border-color-props, var(--xy-controls-button-border-color, var(--xy-controls-button-border-color-default)) );color:var( --xy-controls-button-color-props, var(--xy-controls-button-color, var(--xy-controls-button-color-default)) );cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__controls-button svg{width:100%;max-width:12px;max-height:12px;fill:currentColor}.react-flow__edge.updating .react-flow__edge-path{stroke:#777}.react-flow__edge-text{font-size:10px}.react-flow__node.selectable:focus,.react-flow__node.selectable:focus-visible{outline:none}.react-flow__node-input,.react-flow__node-default,.react-flow__node-output,.react-flow__node-group{padding:10px;border-radius:var(--xy-node-border-radius, var(--xy-node-border-radius-default));width:150px;font-size:12px;color:var(--xy-node-color, var(--xy-node-color-default));text-align:center;border:var(--xy-node-border, var(--xy-node-border-default));background-color:var(--xy-node-background-color, var(--xy-node-background-color-default))}.react-flow__node-input.selectable:hover,.react-flow__node-default.selectable:hover,.react-flow__node-output.selectable:hover,.react-flow__node-group.selectable:hover{box-shadow:var(--xy-node-boxshadow-hover, var(--xy-node-boxshadow-hover-default))}.react-flow__node-input.selectable.selected,.react-flow__node-input.selectable:focus,.react-flow__node-input.selectable:focus-visible,.react-flow__node-default.selectable.selected,.react-flow__node-default.selectable:focus,.react-flow__node-default.selectable:focus-visible,.react-flow__node-output.selectable.selected,.react-flow__node-output.selectable:focus,.react-flow__node-output.selectable:focus-visible,.react-flow__node-group.selectable.selected,.react-flow__node-group.selectable:focus,.react-flow__node-group.selectable:focus-visible{box-shadow:var(--xy-node-boxshadow-selected, var(--xy-node-boxshadow-selected-default))}.react-flow__node-group{background-color:var(--xy-node-group-background-color, var(--xy-node-group-background-color-default))}.react-flow__nodesselection-rect,.react-flow__selection{background:var(--xy-selection-background-color, var(--xy-selection-background-color-default));border:var(--xy-selection-border, var(--xy-selection-border-default))}.react-flow__nodesselection-rect:focus,.react-flow__nodesselection-rect:focus-visible,.react-flow__selection:focus,.react-flow__selection:focus-visible{outline:none}.react-flow__controls-button:hover{background:var( --xy-controls-button-background-color-hover-props, var(--xy-controls-button-background-color-hover, var(--xy-controls-button-background-color-hover-default)) );color:var( --xy-controls-button-color-hover-props, var(--xy-controls-button-color-hover, var(--xy-controls-button-color-hover-default)) )}.react-flow__controls-button:disabled{pointer-events:none}.react-flow__controls-button:disabled svg{fill-opacity:.4}.react-flow__controls-button:last-child{border-bottom:none}.react-flow__controls.horizontal .react-flow__controls-button{border-bottom:none;border-right:1px solid var( --xy-controls-button-border-color-props, var(--xy-controls-button-border-color, var(--xy-controls-button-border-color-default)) )}.react-flow__controls.horizontal .react-flow__controls-button:last-child{border-right:none}.react-flow__resize-control{position:absolute}.react-flow__resize-control.left,.react-flow__resize-control.right{cursor:ew-resize}.react-flow__resize-control.top,.react-flow__resize-control.bottom{cursor:ns-resize}.react-flow__resize-control.top.left,.react-flow__resize-control.bottom.right{cursor:nwse-resize}.react-flow__resize-control.bottom.left,.react-flow__resize-control.top.right{cursor:nesw-resize}.react-flow__resize-control.handle{width:5px;height:5px;border:1px solid #fff;border-radius:1px;background-color:var(--xy-resize-background-color, var(--xy-resize-background-color-default));translate:-50% -50%}.react-flow__resize-control.handle.left{left:0;top:50%}.react-flow__resize-control.handle.right{left:100%;top:50%}.react-flow__resize-control.handle.top{left:50%;top:0}.react-flow__resize-control.handle.bottom{left:50%;top:100%}.react-flow__resize-control.handle.top.left,.react-flow__resize-control.handle.bottom.left{left:0}.react-flow__resize-control.handle.top.right,.react-flow__resize-control.handle.bottom.right{left:100%}.react-flow__resize-control.line{border-color:var(--xy-resize-background-color, var(--xy-resize-background-color-default));border-width:0;border-style:solid}.react-flow__resize-control.line.left,.react-flow__resize-control.line.right{width:1px;transform:translate(-50%);top:0;height:100%}.react-flow__resize-control.line.left{left:0;border-left-width:1px}.react-flow__resize-control.line.right{left:100%;border-right-width:1px}.react-flow__resize-control.line.top,.react-flow__resize-control.line.bottom{height:1px;transform:translateY(-50%);left:0;width:100%}.react-flow__resize-control.line.top{top:0;border-top-width:1px}.react-flow__resize-control.line.bottom{border-bottom-width:1px;top:100%}.react-flow__edge-textbg{fill:var(--xy-edge-label-background-color, var(--xy-edge-label-background-color-default))}.react-flow__edge-text{fill:var(--xy-edge-label-color, var(--xy-edge-label-color-default))}
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>GROOVE</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-B49YqEXS.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Gfb8Zxy9.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
|
@@ -25,6 +25,8 @@ export default function App() {
|
|
|
25
25
|
const activeTab = useGrooveStore((s) => s.activeTab);
|
|
26
26
|
const detailPanel = useGrooveStore((s) => s.detailPanel);
|
|
27
27
|
const statusMessage = useGrooveStore((s) => s.statusMessage);
|
|
28
|
+
const daemonHost = useGrooveStore((s) => s.daemonHost);
|
|
29
|
+
const tunneled = useGrooveStore((s) => s.tunneled);
|
|
28
30
|
const connect = useGrooveStore((s) => s.connect);
|
|
29
31
|
const setActiveTab = useGrooveStore((s) => s.setActiveTab);
|
|
30
32
|
const openDetail = useGrooveStore((s) => s.openDetail);
|
|
@@ -41,6 +43,9 @@ export default function App() {
|
|
|
41
43
|
<header style={styles.header}>
|
|
42
44
|
<div style={styles.headerLeft}>
|
|
43
45
|
<img src="/groove-logo-short.png" alt="GROOVE" style={{ height: 18, marginTop: 3, opacity: 0.85 }} />
|
|
46
|
+
{daemonHost && (
|
|
47
|
+
<span style={styles.hostBadge}>{daemonHost}</span>
|
|
48
|
+
)}
|
|
44
49
|
</div>
|
|
45
50
|
|
|
46
51
|
|
|
@@ -112,8 +117,19 @@ export default function App() {
|
|
|
112
117
|
fontFamily: 'var(--font)',
|
|
113
118
|
animation: 'pulse 3s infinite',
|
|
114
119
|
}}>
|
|
115
|
-
{connected ? 'connected' : 'offline'}
|
|
120
|
+
{connected ? (tunneled ? 'tunneled' : daemonHost ? 'remote' : 'connected') : 'offline'}
|
|
116
121
|
</span>
|
|
122
|
+
{connected && tunneled && (
|
|
123
|
+
<span
|
|
124
|
+
title="Connected via SSH tunnel. Run 'groove disconnect' in your terminal to close."
|
|
125
|
+
style={{
|
|
126
|
+
fontSize: 9, color: 'var(--text-dim)', fontFamily: 'var(--font)',
|
|
127
|
+
cursor: 'help', marginLeft: 2,
|
|
128
|
+
}}
|
|
129
|
+
>
|
|
130
|
+
(via ssh)
|
|
131
|
+
</span>
|
|
132
|
+
)}
|
|
117
133
|
</div>
|
|
118
134
|
|
|
119
135
|
{/* Main row */}
|
|
@@ -162,6 +178,13 @@ const styles = {
|
|
|
162
178
|
headerLeft: {
|
|
163
179
|
display: 'flex', alignItems: 'center', gap: 8,
|
|
164
180
|
},
|
|
181
|
+
hostBadge: {
|
|
182
|
+
fontSize: 9, fontWeight: 600, letterSpacing: 0.5,
|
|
183
|
+
color: 'var(--text-dim)', background: 'var(--bg-active)',
|
|
184
|
+
padding: '2px 6px', borderRadius: 3,
|
|
185
|
+
border: '1px solid var(--border)',
|
|
186
|
+
fontFamily: 'var(--font)',
|
|
187
|
+
},
|
|
165
188
|
logo: {
|
|
166
189
|
fontSize: 13, fontWeight: 600, letterSpacing: 1.5,
|
|
167
190
|
color: 'var(--text-bright)',
|
|
@@ -559,7 +559,7 @@ const styles = {
|
|
|
559
559
|
fontFamily: 'var(--font)', resize: 'vertical',
|
|
560
560
|
},
|
|
561
561
|
hint: {
|
|
562
|
-
fontSize: 10, color: 'var(--text-
|
|
562
|
+
fontSize: 10, color: 'var(--text-dim)', marginTop: 3,
|
|
563
563
|
},
|
|
564
564
|
wsRow: {
|
|
565
565
|
display: 'flex', flexWrap: 'wrap', gap: 4, marginTop: 6,
|
|
@@ -11,6 +11,8 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
11
11
|
agents: [],
|
|
12
12
|
connected: false,
|
|
13
13
|
ws: null,
|
|
14
|
+
daemonHost: null, // bound host IP (null = localhost)
|
|
15
|
+
tunneled: false, // true when accessed via SSH tunnel (port mismatch)
|
|
14
16
|
|
|
15
17
|
// UI state — unified panel model
|
|
16
18
|
activeTab: 'agents', // 'agents' | 'stats' | 'teams' | 'approvals'
|
|
@@ -28,7 +30,22 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
28
30
|
|
|
29
31
|
const ws = new WebSocket(WS_URL);
|
|
30
32
|
|
|
31
|
-
ws.onopen = () =>
|
|
33
|
+
ws.onopen = () => {
|
|
34
|
+
set({ connected: true, ws });
|
|
35
|
+
// Fetch daemon info for instance badge + tunnel detection
|
|
36
|
+
fetch(`${API_BASE}/api/status`).then((r) => r.json()).then((s) => {
|
|
37
|
+
const updates = {};
|
|
38
|
+
if (s.host && s.host !== '127.0.0.1') {
|
|
39
|
+
updates.daemonHost = s.host;
|
|
40
|
+
}
|
|
41
|
+
// Detect tunnel: browser port differs from daemon's actual port
|
|
42
|
+
const browserPort = window.location.port || '80';
|
|
43
|
+
if (String(s.port) !== browserPort) {
|
|
44
|
+
updates.tunneled = true;
|
|
45
|
+
}
|
|
46
|
+
if (Object.keys(updates).length > 0) set(updates);
|
|
47
|
+
}).catch(() => {});
|
|
48
|
+
};
|
|
32
49
|
|
|
33
50
|
ws.onmessage = (event) => {
|
|
34
51
|
const msg = JSON.parse(event.data);
|
|
@@ -113,7 +130,7 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
113
130
|
};
|
|
114
131
|
|
|
115
132
|
ws.onclose = () => {
|
|
116
|
-
set({ connected: false, ws: null });
|
|
133
|
+
set({ connected: false, ws: null, daemonHost: null, tunneled: false });
|
|
117
134
|
setTimeout(() => get().connect(), 2000);
|
|
118
135
|
};
|
|
119
136
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "groove-dev",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Open-source agent orchestration layer for AI coding tools. GUI dashboard, multi-agent coordination, zero cold-start (Journalist), infinite sessions (adaptive context rotation), AI Project Manager, Quick Launch. Works with Claude Code, Codex, Gemini CLI, Ollama.",
|
|
5
5
|
"license": "FSL-1.1-Apache-2.0",
|
|
6
6
|
"author": "Groove Dev <hello@groovedev.ai> (https://groovedev.ai)",
|
|
@@ -16,6 +16,10 @@ import { teamSave, teamLoad, teamList, teamDelete, teamExport, teamImport } from
|
|
|
16
16
|
import { approvals, approve, reject } from '../src/commands/approve.js';
|
|
17
17
|
import { providers, setKey } from '../src/commands/providers.js';
|
|
18
18
|
import { configShow, configSet } from '../src/commands/config.js';
|
|
19
|
+
import { connect } from '../src/commands/connect.js';
|
|
20
|
+
import { disconnect } from '../src/commands/disconnect.js';
|
|
21
|
+
import { audit } from '../src/commands/audit.js';
|
|
22
|
+
import { federationPair, federationUnpair, federationList, federationStatus } from '../src/commands/federation.js';
|
|
19
23
|
|
|
20
24
|
program
|
|
21
25
|
.name('groove')
|
|
@@ -26,6 +30,7 @@ program
|
|
|
26
30
|
.command('start')
|
|
27
31
|
.description('Start the GROOVE daemon')
|
|
28
32
|
.option('-p, --port <port>', 'Port to run on', '31415')
|
|
33
|
+
.option('-H, --host <host>', 'Host/IP to bind to (use "tailscale" for auto-detect)', '127.0.0.1')
|
|
29
34
|
.action(start);
|
|
30
35
|
|
|
31
36
|
program
|
|
@@ -90,6 +95,33 @@ program
|
|
|
90
95
|
program.command('providers').description('List available AI providers').action(providers);
|
|
91
96
|
program.command('set-key <provider> <key>').description('Set API key for a provider').action(setKey);
|
|
92
97
|
|
|
98
|
+
// Remote
|
|
99
|
+
program
|
|
100
|
+
.command('connect <target>')
|
|
101
|
+
.description('Connect to a remote GROOVE daemon via SSH tunnel')
|
|
102
|
+
.option('-i, --identity <keyfile>', 'SSH private key file')
|
|
103
|
+
.option('--no-browser', 'Don\'t open browser automatically')
|
|
104
|
+
.action(connect);
|
|
105
|
+
|
|
106
|
+
program
|
|
107
|
+
.command('disconnect')
|
|
108
|
+
.description('Disconnect from remote GROOVE daemon')
|
|
109
|
+
.action(disconnect);
|
|
110
|
+
|
|
111
|
+
// Audit
|
|
112
|
+
program
|
|
113
|
+
.command('audit')
|
|
114
|
+
.description('View audit log of state-changing operations')
|
|
115
|
+
.option('-n, --limit <count>', 'Number of entries to show', '25')
|
|
116
|
+
.action(audit);
|
|
117
|
+
|
|
118
|
+
// Federation
|
|
119
|
+
const federation = program.command('federation').description('Manage daemon-to-daemon federation');
|
|
120
|
+
federation.command('pair <target>').description('Pair with a remote GROOVE daemon (ip or ip:port)').action(federationPair);
|
|
121
|
+
federation.command('unpair <id>').description('Remove a paired peer').action(federationUnpair);
|
|
122
|
+
federation.command('list').description('List paired peers').action(federationList);
|
|
123
|
+
federation.command('status').description('Show federation status').action(federationStatus);
|
|
124
|
+
|
|
93
125
|
// Config
|
|
94
126
|
const config = program.command('config').description('View and modify configuration');
|
|
95
127
|
config.command('show').description('Show current configuration').action(configShow);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// GROOVE CLI — audit command
|
|
2
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
|
+
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { apiCall } from '../client.js';
|
|
6
|
+
|
|
7
|
+
const ACTION_COLORS = {
|
|
8
|
+
'agent.spawn': 'green',
|
|
9
|
+
'agent.kill': 'red',
|
|
10
|
+
'agent.kill_all': 'red',
|
|
11
|
+
'agent.rotate': 'yellow',
|
|
12
|
+
'agent.instruct': 'cyan',
|
|
13
|
+
'team.save': 'blue',
|
|
14
|
+
'team.load': 'blue',
|
|
15
|
+
'team.delete': 'red',
|
|
16
|
+
'team.import': 'blue',
|
|
17
|
+
'team.launch': 'green',
|
|
18
|
+
'config.set': 'yellow',
|
|
19
|
+
'credential.set': 'yellow',
|
|
20
|
+
'credential.delete': 'red',
|
|
21
|
+
'approval.approve': 'green',
|
|
22
|
+
'approval.reject': 'red',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function formatEntry(entry) {
|
|
26
|
+
const time = entry.t ? new Date(entry.t).toLocaleTimeString() : '??:??:??';
|
|
27
|
+
const color = ACTION_COLORS[entry.action] || 'white';
|
|
28
|
+
const action = chalk[color](entry.action.padEnd(20));
|
|
29
|
+
|
|
30
|
+
// Build detail string from remaining fields
|
|
31
|
+
const { t, action: _, ...detail } = entry;
|
|
32
|
+
const detailStr = Object.entries(detail)
|
|
33
|
+
.map(([k, v]) => `${k}=${typeof v === 'string' ? v : JSON.stringify(v)}`)
|
|
34
|
+
.join(' ');
|
|
35
|
+
|
|
36
|
+
return ` ${chalk.dim(time)} ${action} ${chalk.dim(detailStr)}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function audit(options) {
|
|
40
|
+
try {
|
|
41
|
+
const limit = parseInt(options.limit, 10) || 25;
|
|
42
|
+
const entries = await apiCall('GET', `/api/audit?limit=${limit}`);
|
|
43
|
+
|
|
44
|
+
console.log('');
|
|
45
|
+
if (entries.length === 0) {
|
|
46
|
+
console.log(chalk.dim(' No audit entries yet.'));
|
|
47
|
+
} else {
|
|
48
|
+
console.log(chalk.bold(` Audit Log`) + chalk.dim(` (${entries.length} entries, newest first)`));
|
|
49
|
+
console.log('');
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
console.log(formatEntry(entry));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
console.log('');
|
|
55
|
+
} catch {
|
|
56
|
+
console.log('');
|
|
57
|
+
console.log(chalk.yellow(' Daemon not running.'));
|
|
58
|
+
console.log('');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
// GROOVE CLI — connect command (SSH tunnel to remote daemon)
|
|
2
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
|
+
|
|
4
|
+
import { execFileSync, spawn } from 'child_process';
|
|
5
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
|
|
6
|
+
import { resolve } from 'path';
|
|
7
|
+
import { createConnection } from 'net';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
|
|
10
|
+
const REMOTE_PORT = 31415;
|
|
11
|
+
const DEFAULT_LOCAL_PORT = 31416;
|
|
12
|
+
const MAX_PORT_ATTEMPTS = 10;
|
|
13
|
+
|
|
14
|
+
// Allow user@host OR plain hostname (SSH config aliases like "groove-ai")
|
|
15
|
+
const SSH_TARGET_PATTERN = /^([a-zA-Z0-9._-]+@)?[a-zA-Z0-9._-]+$/;
|
|
16
|
+
|
|
17
|
+
function validateTarget(target) {
|
|
18
|
+
if (!target || typeof target !== 'string') {
|
|
19
|
+
throw new Error('SSH target is required (e.g., user@host or ssh-config-alias)');
|
|
20
|
+
}
|
|
21
|
+
// Block injection characters
|
|
22
|
+
if (/[;|&`$(){}[\]<>!#\n\r\\]/.test(target)) {
|
|
23
|
+
throw new Error('Invalid characters in SSH target');
|
|
24
|
+
}
|
|
25
|
+
if (!SSH_TARGET_PATTERN.test(target)) {
|
|
26
|
+
throw new Error('Invalid SSH target format. Expected: user@hostname or ssh-config-alias');
|
|
27
|
+
}
|
|
28
|
+
if (target.length > 253) {
|
|
29
|
+
throw new Error('SSH target too long');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function grooveDir() {
|
|
34
|
+
const dir = resolve(process.cwd(), '.groove');
|
|
35
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
36
|
+
return dir;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function pidFile() {
|
|
40
|
+
return resolve(grooveDir(), 'tunnel.pid');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function tunnelInfoFile() {
|
|
44
|
+
return resolve(grooveDir(), 'tunnel.json');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isPortInUse(port) {
|
|
48
|
+
return new Promise((resolve) => {
|
|
49
|
+
const conn = createConnection({ host: '127.0.0.1', port });
|
|
50
|
+
conn.setTimeout(3000);
|
|
51
|
+
conn.on('connect', () => { conn.destroy(); resolve(true); });
|
|
52
|
+
conn.on('error', () => resolve(false));
|
|
53
|
+
conn.on('timeout', () => { conn.destroy(); resolve(false); });
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function findAvailablePort() {
|
|
58
|
+
for (let port = DEFAULT_LOCAL_PORT; port < DEFAULT_LOCAL_PORT + MAX_PORT_ATTEMPTS; port++) {
|
|
59
|
+
if (!(await isPortInUse(port))) return port;
|
|
60
|
+
}
|
|
61
|
+
throw new Error(`No available local port found (tried ${DEFAULT_LOCAL_PORT}-${DEFAULT_LOCAL_PORT + MAX_PORT_ATTEMPTS - 1})`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function preflight(target, keyFile) {
|
|
65
|
+
// SSH in and check if daemon is listening on remote
|
|
66
|
+
// Use curl to health endpoint — works on both Linux and macOS
|
|
67
|
+
const args = [
|
|
68
|
+
...(keyFile ? ['-i', keyFile] : []),
|
|
69
|
+
'-o', 'ConnectTimeout=10',
|
|
70
|
+
'-o', 'StrictHostKeyChecking=accept-new',
|
|
71
|
+
'-o', 'BatchMode=yes',
|
|
72
|
+
target,
|
|
73
|
+
`curl -sf http://localhost:${REMOTE_PORT}/api/health >/dev/null 2>&1 || echo __GROOVE_NOT_RUNNING__`,
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const result = execFileSync('ssh', args, {
|
|
78
|
+
encoding: 'utf8',
|
|
79
|
+
timeout: 15000,
|
|
80
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
if (result.includes('__GROOVE_NOT_RUNNING__')) {
|
|
84
|
+
return { running: false };
|
|
85
|
+
}
|
|
86
|
+
return { running: true };
|
|
87
|
+
} catch (err) {
|
|
88
|
+
const stderr = err.stderr?.toString() || '';
|
|
89
|
+
if (stderr.includes('Permission denied')) {
|
|
90
|
+
throw new Error('SSH authentication failed. Check your key or credentials.');
|
|
91
|
+
}
|
|
92
|
+
if (stderr.includes('Connection refused') || stderr.includes('Connection timed out') || stderr.includes('No route to host')) {
|
|
93
|
+
throw new Error(`Cannot reach ${target}. Check the hostname and that SSH is running.`);
|
|
94
|
+
}
|
|
95
|
+
throw new Error(`SSH preflight failed: ${stderr.trim() || err.message}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isSshProcess(pid) {
|
|
100
|
+
// Verify the PID belongs to an SSH process (not a random reused PID)
|
|
101
|
+
try {
|
|
102
|
+
const cmd = execFileSync('ps', ['-p', String(pid), '-o', 'command='], {
|
|
103
|
+
encoding: 'utf8',
|
|
104
|
+
timeout: 3000,
|
|
105
|
+
}).trim();
|
|
106
|
+
return cmd.includes('ssh');
|
|
107
|
+
} catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function existingTunnel() {
|
|
113
|
+
const pf = pidFile();
|
|
114
|
+
if (!existsSync(pf)) return null;
|
|
115
|
+
const pid = parseInt(readFileSync(pf, 'utf8').trim(), 10);
|
|
116
|
+
if (isNaN(pid)) return null;
|
|
117
|
+
// Check if process is still alive AND is an SSH process
|
|
118
|
+
try {
|
|
119
|
+
process.kill(pid, 0);
|
|
120
|
+
if (!isSshProcess(pid)) {
|
|
121
|
+
// PID was reused by a different process — stale tunnel files
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
// Read tunnel info if available
|
|
125
|
+
const infoPath = tunnelInfoFile();
|
|
126
|
+
if (existsSync(infoPath)) {
|
|
127
|
+
const info = JSON.parse(readFileSync(infoPath, 'utf8'));
|
|
128
|
+
return { pid, ...info };
|
|
129
|
+
}
|
|
130
|
+
return { pid };
|
|
131
|
+
} catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function openBrowser(url) {
|
|
137
|
+
const platform = process.platform;
|
|
138
|
+
try {
|
|
139
|
+
if (platform === 'darwin') {
|
|
140
|
+
execFileSync('open', [url], { stdio: 'ignore' });
|
|
141
|
+
} else if (platform === 'win32') {
|
|
142
|
+
execFileSync('cmd', ['/c', 'start', '', url], { stdio: 'ignore' });
|
|
143
|
+
} else {
|
|
144
|
+
execFileSync('xdg-open', [url], { stdio: 'ignore' });
|
|
145
|
+
}
|
|
146
|
+
} catch {
|
|
147
|
+
// Browser open is best-effort
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function connect(target, options) {
|
|
152
|
+
console.log('');
|
|
153
|
+
|
|
154
|
+
// Validate SSH target
|
|
155
|
+
try {
|
|
156
|
+
validateTarget(target);
|
|
157
|
+
} catch (err) {
|
|
158
|
+
console.log(chalk.red(' Error: ') + err.message);
|
|
159
|
+
console.log('');
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Check for existing tunnel
|
|
164
|
+
const existing = existingTunnel();
|
|
165
|
+
if (existing) {
|
|
166
|
+
console.log(chalk.yellow(' Tunnel already active') + ` (PID ${existing.pid})`);
|
|
167
|
+
if (existing.target) {
|
|
168
|
+
console.log(` Target: ${existing.target}`);
|
|
169
|
+
}
|
|
170
|
+
if (existing.localPort) {
|
|
171
|
+
console.log(` GUI: ${chalk.cyan(`http://localhost:${existing.localPort}`)}`);
|
|
172
|
+
}
|
|
173
|
+
console.log('');
|
|
174
|
+
console.log(` Run ${chalk.bold('groove disconnect')} first to close it.`);
|
|
175
|
+
console.log('');
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Preflight — check daemon is running on remote
|
|
180
|
+
console.log(chalk.dim(' Checking remote daemon...'));
|
|
181
|
+
try {
|
|
182
|
+
const check = preflight(target, options.identity);
|
|
183
|
+
if (!check.running) {
|
|
184
|
+
console.log(chalk.red(' Daemon not running on remote.'));
|
|
185
|
+
console.log(` SSH into ${chalk.bold(target)} and run ${chalk.bold('groove start')} first.`);
|
|
186
|
+
console.log('');
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
} catch (err) {
|
|
190
|
+
console.log(chalk.red(' ' + err.message));
|
|
191
|
+
console.log('');
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
console.log(chalk.dim(' Remote daemon is running.'));
|
|
196
|
+
|
|
197
|
+
// Find available local port
|
|
198
|
+
let localPort;
|
|
199
|
+
try {
|
|
200
|
+
localPort = await findAvailablePort();
|
|
201
|
+
} catch (err) {
|
|
202
|
+
console.log(chalk.red(' ' + err.message));
|
|
203
|
+
console.log('');
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (localPort !== DEFAULT_LOCAL_PORT) {
|
|
208
|
+
console.log(chalk.yellow(` Port ${DEFAULT_LOCAL_PORT} in use, using ${localPort} instead.`));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Spawn SSH tunnel
|
|
212
|
+
const sshArgs = [
|
|
213
|
+
'-N', // No remote command
|
|
214
|
+
'-L', `127.0.0.1:${localPort}:localhost:${REMOTE_PORT}`, // Local bind to 127.0.0.1 only
|
|
215
|
+
'-o', 'ServerAliveInterval=30', // Keepalive every 30s
|
|
216
|
+
'-o', 'ServerAliveCountMax=3', // Die after 3 missed keepalives
|
|
217
|
+
'-o', 'ExitOnForwardFailure=yes', // Fail if port forward fails
|
|
218
|
+
'-o', 'StrictHostKeyChecking=accept-new',
|
|
219
|
+
...(options.identity ? ['-i', options.identity] : []),
|
|
220
|
+
target,
|
|
221
|
+
];
|
|
222
|
+
|
|
223
|
+
const tunnel = spawn('ssh', sshArgs, {
|
|
224
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
225
|
+
detached: true,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Capture stderr for early errors
|
|
229
|
+
let stderrBuf = '';
|
|
230
|
+
tunnel.stderr.on('data', (chunk) => { stderrBuf += chunk.toString(); });
|
|
231
|
+
|
|
232
|
+
// Wait a moment to catch immediate failures (bad key, connection refused)
|
|
233
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
234
|
+
|
|
235
|
+
// Check if tunnel is still alive
|
|
236
|
+
if (tunnel.exitCode !== null) {
|
|
237
|
+
console.log(chalk.red(' Tunnel failed to start.'));
|
|
238
|
+
if (stderrBuf.trim()) {
|
|
239
|
+
console.log(chalk.dim(' ' + stderrBuf.trim()));
|
|
240
|
+
}
|
|
241
|
+
console.log('');
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Verify the tunnel is actually forwarding
|
|
246
|
+
const tunnelUp = await isPortInUse(localPort);
|
|
247
|
+
if (!tunnelUp) {
|
|
248
|
+
console.log(chalk.red(' Tunnel started but port forward not active.'));
|
|
249
|
+
try { process.kill(tunnel.pid); } catch { /* ignore */ }
|
|
250
|
+
console.log('');
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Detach and save PID
|
|
255
|
+
tunnel.unref();
|
|
256
|
+
writeFileSync(pidFile(), String(tunnel.pid), { mode: 0o600 });
|
|
257
|
+
writeFileSync(tunnelInfoFile(), JSON.stringify({
|
|
258
|
+
target,
|
|
259
|
+
localPort,
|
|
260
|
+
remotePort: REMOTE_PORT,
|
|
261
|
+
startedAt: new Date().toISOString(),
|
|
262
|
+
}), { mode: 0o600 });
|
|
263
|
+
|
|
264
|
+
const url = `http://localhost:${localPort}`;
|
|
265
|
+
|
|
266
|
+
console.log('');
|
|
267
|
+
console.log(chalk.green(' Connected!'));
|
|
268
|
+
console.log('');
|
|
269
|
+
console.log(` Target: ${chalk.bold(target)}`);
|
|
270
|
+
console.log(` Tunnel: localhost:${localPort} → ${target}:${REMOTE_PORT}`);
|
|
271
|
+
console.log(` GUI: ${chalk.cyan(url)}`);
|
|
272
|
+
console.log(` PID: ${tunnel.pid}`);
|
|
273
|
+
console.log('');
|
|
274
|
+
|
|
275
|
+
// Open browser (Commander: --no-browser sets options.browser = false)
|
|
276
|
+
if (options.browser !== false) {
|
|
277
|
+
openBrowser(url);
|
|
278
|
+
}
|
|
279
|
+
}
|