groove-dev 0.10.10 → 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.
Files changed (61) hide show
  1. package/README.md +24 -16
  2. package/node_modules/@groove-dev/cli/bin/groove.js +32 -0
  3. package/node_modules/@groove-dev/cli/package.json +1 -1
  4. package/node_modules/@groove-dev/cli/src/commands/audit.js +60 -0
  5. package/node_modules/@groove-dev/cli/src/commands/connect.js +279 -0
  6. package/node_modules/@groove-dev/cli/src/commands/disconnect.js +91 -0
  7. package/node_modules/@groove-dev/cli/src/commands/federation.js +84 -0
  8. package/node_modules/@groove-dev/cli/src/commands/start.js +7 -2
  9. package/node_modules/@groove-dev/cli/src/commands/status.js +4 -1
  10. package/node_modules/@groove-dev/daemon/package.json +1 -1
  11. package/node_modules/@groove-dev/daemon/src/api.js +128 -2
  12. package/node_modules/@groove-dev/daemon/src/audit.js +65 -0
  13. package/node_modules/@groove-dev/daemon/src/federation.js +352 -0
  14. package/node_modules/@groove-dev/daemon/src/firstrun.js +27 -2
  15. package/node_modules/@groove-dev/daemon/src/index.js +64 -6
  16. package/node_modules/@groove-dev/daemon/src/indexer.js +324 -0
  17. package/node_modules/@groove-dev/daemon/src/introducer.js +55 -4
  18. package/node_modules/@groove-dev/daemon/src/journalist.js +140 -51
  19. package/node_modules/@groove-dev/daemon/src/process.js +3 -2
  20. package/node_modules/@groove-dev/gui/dist/assets/index-B49YqEXS.js +73 -0
  21. package/{packages/gui/dist/assets/index-CPzm9ZE9.css → node_modules/@groove-dev/gui/dist/assets/index-Gfb8Zxy9.css} +1 -1
  22. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  23. package/node_modules/@groove-dev/gui/package.json +1 -1
  24. package/node_modules/@groove-dev/gui/src/App.jsx +24 -1
  25. package/node_modules/@groove-dev/gui/src/components/AgentNode.jsx +6 -4
  26. package/node_modules/@groove-dev/gui/src/components/AgentStats.jsx +1 -0
  27. package/node_modules/@groove-dev/gui/src/components/SpawnPanel.jsx +71 -1
  28. package/node_modules/@groove-dev/gui/src/stores/groove.js +19 -2
  29. package/node_modules/@groove-dev/gui/src/theme.css +2 -2
  30. package/package.json +1 -1
  31. package/packages/cli/bin/groove.js +32 -0
  32. package/packages/cli/package.json +1 -1
  33. package/packages/cli/src/commands/audit.js +60 -0
  34. package/packages/cli/src/commands/connect.js +279 -0
  35. package/packages/cli/src/commands/disconnect.js +91 -0
  36. package/packages/cli/src/commands/federation.js +84 -0
  37. package/packages/cli/src/commands/start.js +7 -2
  38. package/packages/cli/src/commands/status.js +4 -1
  39. package/packages/daemon/package.json +1 -1
  40. package/packages/daemon/src/api.js +128 -2
  41. package/packages/daemon/src/audit.js +65 -0
  42. package/packages/daemon/src/federation.js +352 -0
  43. package/packages/daemon/src/firstrun.js +27 -2
  44. package/packages/daemon/src/index.js +64 -6
  45. package/packages/daemon/src/indexer.js +324 -0
  46. package/packages/daemon/src/introducer.js +55 -4
  47. package/packages/daemon/src/journalist.js +140 -51
  48. package/packages/daemon/src/process.js +3 -2
  49. package/packages/gui/dist/assets/index-B49YqEXS.js +73 -0
  50. package/{node_modules/@groove-dev/gui/dist/assets/index-CPzm9ZE9.css → packages/gui/dist/assets/index-Gfb8Zxy9.css} +1 -1
  51. package/packages/gui/dist/index.html +2 -2
  52. package/packages/gui/package.json +1 -1
  53. package/packages/gui/src/App.jsx +24 -1
  54. package/packages/gui/src/components/AgentNode.jsx +6 -4
  55. package/packages/gui/src/components/AgentStats.jsx +1 -0
  56. package/packages/gui/src/components/SpawnPanel.jsx +71 -1
  57. package/packages/gui/src/stores/groove.js +19 -2
  58. package/packages/gui/src/theme.css +2 -2
  59. package/groove-logo.png +0 -0
  60. package/node_modules/@groove-dev/gui/dist/assets/index-BPVh7Oqk.js +0 -73
  61. package/packages/gui/dist/assets/index-BPVh7Oqk.js +0 -73
@@ -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-BPVh7Oqk.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-CPzm9ZE9.css">
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>
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/gui",
3
- "version": "0.10.10",
3
+ "version": "0.11.0",
4
4
  "description": "GROOVE GUI — visual agent control plane",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -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)',
@@ -35,10 +35,12 @@ export default function AgentNode({ data }) {
35
35
  ? data.tokensUsed > 999 ? `${(data.tokensUsed / 1000).toFixed(1)}k` : `${data.tokensUsed}`
36
36
  : '0';
37
37
 
38
- // Get scope summary for activity text
39
- const activity = data.scope?.length > 0
40
- ? data.scope[0].replace(/\/\*\*$/, '').replace(/^src\//, '')
41
- : data.role;
38
+ // Get scope summary for activity text — prefer workingDir if set
39
+ const activity = data.workingDir
40
+ ? data.workingDir.replace(/^\.\//, '')
41
+ : data.scope?.length > 0
42
+ ? data.scope[0].replace(/\/\*\*$/, '').replace(/^src\//, '')
43
+ : data.role;
42
44
 
43
45
  return (
44
46
  <div style={{
@@ -61,6 +61,7 @@ export default function AgentStats({ agent }) {
61
61
  <InfoRow label="Role" value={agent.role} />
62
62
  <InfoRow label="Provider" value={agent.provider} />
63
63
  <InfoRow label="Model" value={agent.model || 'default'} />
64
+ {agent.workingDir && <InfoRow label="Directory" value={agent.workingDir} />}
64
65
  <InfoRow label="Scope" value={(agent.scope || []).join(', ') || 'unrestricted'} />
65
66
  <InfoRow label="Spawned" value={agent.spawnedAt ? new Date(agent.spawnedAt).toLocaleTimeString() : '-'} />
66
67
  <InfoRow label="Last Active" value={agent.lastActivity ? new Date(agent.lastActivity).toLocaleTimeString() : '-'} />
@@ -34,12 +34,15 @@ export default function SpawnPanel() {
34
34
  const [submitting, setSubmitting] = useState(false);
35
35
  const [error, setError] = useState('');
36
36
  const [showAdvanced, setShowAdvanced] = useState(false);
37
+ const [workingDir, setWorkingDir] = useState('');
38
+ const [workspaces, setWorkspaces] = useState([]);
37
39
  const [connectingProvider, setConnectingProvider] = useState(null);
38
40
  const [apiKeyInput, setApiKeyInput] = useState('');
39
41
  const [keySaving, setKeySaving] = useState(false);
40
42
 
41
43
  useEffect(() => {
42
44
  fetchProviders();
45
+ fetchWorkspaces();
43
46
  }, []);
44
47
 
45
48
  async function fetchProviders() {
@@ -49,6 +52,14 @@ export default function SpawnPanel() {
49
52
  } catch { /* ignore */ }
50
53
  }
51
54
 
55
+ async function fetchWorkspaces() {
56
+ try {
57
+ const res = await fetch('/api/indexer/workspaces');
58
+ const data = await res.json();
59
+ setWorkspaces(data.workspaces || []);
60
+ } catch { /* ignore */ }
61
+ }
62
+
52
63
  const selectedPreset = ROLE_PRESETS.find((p) => p.id === role);
53
64
  const effectiveScope = role === 'custom'
54
65
  ? scope
@@ -113,6 +124,7 @@ export default function SpawnPanel() {
113
124
  model: model || 'auto',
114
125
  provider,
115
126
  permission,
127
+ ...(workingDir.trim() ? { workingDir: workingDir.trim() } : {}),
116
128
  });
117
129
  closeDetail();
118
130
  } catch (err) {
@@ -198,6 +210,42 @@ export default function SpawnPanel() {
198
210
  rows={3}
199
211
  />
200
212
 
213
+ {/* Workspace picker — visible by default when workspaces detected */}
214
+ {workspaces.length > 0 && (
215
+ <>
216
+ <div style={styles.label}>DIRECTORY</div>
217
+ <div style={styles.wsRow}>
218
+ <button
219
+ type="button"
220
+ onClick={() => setWorkingDir('')}
221
+ style={{
222
+ ...styles.wsBtn,
223
+ ...(!workingDir ? { borderColor: 'var(--accent)', color: 'var(--text-bright)' } : {}),
224
+ }}
225
+ >
226
+ project root
227
+ </button>
228
+ {workspaces.map((ws) => (
229
+ <button
230
+ key={ws.path}
231
+ type="button"
232
+ onClick={() => setWorkingDir(ws.path)}
233
+ style={{
234
+ ...styles.wsBtn,
235
+ ...(workingDir === ws.path ? { borderColor: 'var(--accent)', color: 'var(--text-bright)' } : {}),
236
+ }}
237
+ title={`${ws.name} (${ws.files} files)`}
238
+ >
239
+ {ws.path}
240
+ </button>
241
+ ))}
242
+ </div>
243
+ <div style={styles.hint}>
244
+ Agent spawns inside this directory and only sees this subtree
245
+ </div>
246
+ </>
247
+ )}
248
+
201
249
  {/* Permissions */}
202
250
  <div style={styles.label}>PERMISSIONS</div>
203
251
  <div style={styles.permGrid}>
@@ -231,6 +279,18 @@ export default function SpawnPanel() {
231
279
 
232
280
  {showAdvanced && (
233
281
  <>
282
+ {/* Working directory — manual input for custom paths */}
283
+ <div style={styles.label}>WORKING DIRECTORY</div>
284
+ <input
285
+ style={styles.input}
286
+ placeholder="e.g. packages/frontend (default: project root)"
287
+ value={workingDir}
288
+ onChange={(e) => setWorkingDir(e.target.value)}
289
+ />
290
+ <div style={styles.hint}>
291
+ Relative path — or use the directory buttons above
292
+ </div>
293
+
234
294
  {/* Provider selector with connection flow */}
235
295
  <div style={styles.label}>PROVIDER</div>
236
296
  {providerList.map((p) => {
@@ -499,7 +559,17 @@ const styles = {
499
559
  fontFamily: 'var(--font)', resize: 'vertical',
500
560
  },
501
561
  hint: {
502
- fontSize: 10, color: 'var(--text-muted)', marginTop: 3,
562
+ fontSize: 10, color: 'var(--text-dim)', marginTop: 3,
563
+ },
564
+ wsRow: {
565
+ display: 'flex', flexWrap: 'wrap', gap: 4, marginTop: 6,
566
+ },
567
+ wsBtn: {
568
+ background: 'var(--bg-surface)', border: '1px solid var(--border)',
569
+ borderRadius: 2, padding: '3px 8px',
570
+ color: 'var(--text-dim)', fontSize: 10, cursor: 'pointer',
571
+ fontFamily: 'var(--font)',
572
+ transition: 'color 0.1s, border-color 0.1s',
503
573
  },
504
574
  error: {
505
575
  color: 'var(--red)', fontSize: 11, marginTop: 8,
@@ -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 = () => set({ connected: true, ws });
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
 
@@ -14,8 +14,8 @@
14
14
  /* Text */
15
15
  --text-primary: #abb2bf;
16
16
  --text-bright: #e6e6e6;
17
- --text-dim: #5c6370;
18
- --text-muted: #3e4451;
17
+ --text-dim: #7a8394;
18
+ --text-muted: #5c6370;
19
19
  --border: #4b5263;
20
20
 
21
21
  /* Status */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "groove-dev",
3
- "version": "0.10.10",
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);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.10.10",
3
+ "version": "0.11.0",
4
4
  "description": "GROOVE CLI — manage AI coding agents from your terminal",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -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
+ }