diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5b16fa1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out +*.log + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum + +# env file +.env + +bin/ +build/ + +*.tar.gz +*.gz + +build/ +*.log +pkg/handlerfactory/rustclients/target/.rustc_info.json + +target/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index b8c0584..92e5041 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,196 @@ -# heroagent +# HeroAgent + +### Quick Install + +You can install HeroLauncher using our install script: + +```bash +# Install the latest version +curl -fsSL https://raw.githubusercontent.com/freeflowuniverse/heroagent/main/scripts/install.sh | bash + +# Install a specific version +curl -fsSL https://raw.githubusercontent.com/freeflowuniverse/heroagent/main/scripts/install.sh | bash -s 1.0.0 +``` + +The script will: +- Download the appropriate binary for your platform +- Install it to `~/heroagent/bin` +- Add the installation directory to your PATH +- Create symlinks in `/usr/local/bin` if possible + +### Manual Installation + +You can also download the binaries manually from the [Releases](https://github.com/freeflowuniverse/heroagent/releases) page. + +### Building from Source + +```bash +# Clone the repository +git clone https://github.com/freeflowuniverse/heroagent.git +cd heroagent + +# Build the project +go build -o bin/heroagent ./cmd/processmanager +``` + +### Prerequisites + +- Go 1.23 or later +- For IPFS functionality: [IPFS](https://ipfs.io/) installed + +## Usage + +### Running HeroLauncher + +```bash +# Run with default settings +./heroagent + +# Run with web server on a specific port +./heroagent -w -p 9090 + +# Enable IPFS server +./heroagent -i + +# Run in installer mode +./heroagent --install + +# Show help +./heroagent -h +``` + +### Command Line Options + +- `-w, --web`: Enable web server (default: true) +- `-p, --port`: Web server port (default: 9001) +- `--host`: Web server host (default: localhost) +- `-i, --ipfs`: Enable IPFS server +- `--install`: Run in installer mode +- `-h, --help`: Show help message + +## API Documentation + +When the web server is running, you can access the Swagger UI at: + +``` +http://localhost:9001/swagger +``` + +The OpenAPI specification is available at: + +``` +http://localhost:9001/openapi.json +``` + +## Project Structure + +``` +/ +├── modules/ +│ ├── installer/ # Installer module +│ ├── webserver/ # Web server module +│ │ ├── endpoints/ +│ │ │ ├── executor/ # Command execution endpoint +│ │ │ └── packagemanager/ # Package management endpoint +│ └── ipfs/ # IPFS server module +├── main.v # Main application entry point +└── v.mod # V module definition +``` + +## Development + +### Running Tests + +```bash +# Run all tests +./test.sh + +# Run tests with debug output +./test.sh --debug +``` + +The test script will run all Go tests in the project and display a summary of the results at the end. You can exclude specific packages by uncommenting them in the `EXCLUDED_MODULES` array in the test.sh file. + +### Continuous Integration and Deployment + +This project uses GitHub Actions for CI/CD: + +- **Go Tests**: Runs all tests using the test.sh script on every push and pull request +- **Go Lint**: Performs linting using golangci-lint to ensure code quality +- **Build**: Builds the application for multiple platforms (Linux Intel/ARM, macOS Intel/ARM, Windows) and makes the binaries available as artifacts +- **Release**: Creates GitHub releases with binaries for all platforms when a new tag is pushed + +### Downloading Binaries from CI + +The Build workflow creates binaries for multiple platforms and makes them available as artifacts. To download the binaries: + +1. Go to the [Actions](https://github.com/freeflowuniverse/heroagent/actions) tab in the repository +2. Click on the latest successful Build workflow run +3. Scroll down to the Artifacts section +4. Download the artifact for your platform: + - `heroagent-linux-amd64.tar.gz` for Linux (Intel) + - `heroagent-linux-arm64.tar.gz` for Linux (ARM) + - `heroagent-darwin-amd64.tar.gz` for macOS (Intel) + - `heroagent-darwin-arm64.tar.gz` for macOS (ARM) + - `heroagent-windows-amd64.zip` for Windows +5. Extract the archive to get the binaries +6. The archive contains the following executables: + - `pmclient-[platform]`: Process Manager client + - `telnettest-[platform]`: Telnet test utility + - `webdavclient-[platform]`: WebDAV client + - `webdavserver-[platform]`: WebDAV server +7. Run the desired executable from the command line + +### Creating a New Release + +To create a new release: + +1. Use the release script: + +```bash +# Run the release script +./scripts/release.sh +``` + +This script will: +- Get the latest release from GitHub +- Ask for a new version number +- Create a git tag with the new version +- Push the tag to GitHub + +2. Alternatively, you can manually create and push a tag: + +```bash +# Tag a new version +git tag v1.0.0 + +# Push the tag to trigger the release workflow +git push origin v1.0.0 +``` + +3. The Release workflow will automatically: + - Build the binaries for all platforms + - Create a GitHub release with the tag name + - Upload the binaries as assets to the release + - Generate release notes based on the commits since the last release + +4. Once the workflow completes, the release will be available on the [Releases](https://github.com/freeflowuniverse/heroagent/releases) page + +#### Docker + +A Docker image is automatically built and pushed to Docker Hub on each push to main/master and on tag releases. To use the Docker image: + +```bash +# Pull the latest image +docker pull username/heroagent:latest + +# Run the container +docker run -p 9001:9001 username/heroagent:latest +``` + +Replace `username` with the actual Docker Hub username configured in the repository secrets. + +## License + +MIT diff --git a/aiprompts/instructions/instruction_handlers.md b/aiprompts/instructions/instruction_handlers.md new file mode 100644 index 0000000..83bb642 --- /dev/null +++ b/aiprompts/instructions/instruction_handlers.md @@ -0,0 +1,38 @@ + +## handler factor + +a handler factory has a function to register handlers + +each handler has a name (what the actor is called) + + +each handler represents an actor with its actions +to understand more about heroscript which is the way how we can actor and actions see @instructions/knowledge/1_heroscript.md + +and each handler has as function to translate heroscript to the implementation + +the handler calls the required implementation (can be in one or more packages) + +the handler has a play method which uses @pkg/heroscript/playbook to process heroscript and call the required implementation + +create a folder in @pkg/heroscript/handlers which will have all the knowledge how to go from heroscript to implementation + + +## telnet server + +we need a generic telnet server which takes a handler factory as input + +the telnet server is very basic, it get's messages +each message is a heroscript + +when an empty line is sent that means its the end of a heroscript message + +the telnet server needs to be authenticated using a special heroscript message + +!!auth secret:'secret123' + +as long as that authentication has not been done it will not process any heroscript + +the processing of heroscript happens by means of calling the handler factory + +there can be more than one secret on the telnet server diff --git a/aiprompts/instructions/instructions1.md b/aiprompts/instructions/instructions1.md new file mode 100644 index 0000000..f6bd12d --- /dev/null +++ b/aiprompts/instructions/instructions1.md @@ -0,0 +1,435 @@ + +create a golang project + +there will be multiple + +- modules + - one is for installers + - one is for a fiber web server with a web ui, swagger UI and opeapi rest interface (v3.1.0 swagger) + - a generic redis server + +- on the fiber webserver create multiple endpoints nicely structures as separate directories underneith the module + - executor (for executing commands, results in jobs) + - package manager (on basis of apt, brew, scoop) + - create an openapi interface for each of those v3.1.0 + - integrate in generic way the goswagger interface so people can use the rest interface from web + +- create a main server which connects to all the modules + + +### code for the redis server + +```go +package main + +import ( + "fmt" + "log" + "strconv" + "strings" + "sync" + "time" + + "github.com/tidwall/redcon" +) + +// entry represents a stored value. For strings, value is stored as a string. +// For hashes, value is stored as a map[string]string. +type entry struct { + value interface{} + expiration time.Time // zero means no expiration +} + +// Server holds the in-memory datastore and provides thread-safe access. +type Server struct { + mu sync.RWMutex + data map[string]*entry +} + +// NewServer creates a new server instance and starts a cleanup goroutine. +func NewServer() *Server { + s := &Server{ + data: make(map[string]*entry), + } + go s.cleanupExpiredKeys() + return s +} + +// cleanupExpiredKeys periodically removes expired keys. +func (s *Server) cleanupExpiredKeys() { + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + for range ticker.C { + now := time.Now() + s.mu.Lock() + for k, ent := range s.data { + if !ent.expiration.IsZero() && now.After(ent.expiration) { + delete(s.data, k) + } + } + s.mu.Unlock() + } +} + +// set stores a key with a value and an optional expiration duration. +func (s *Server) set(key string, value interface{}, duration time.Duration) { + s.mu.Lock() + defer s.mu.Unlock() + var exp time.Time + if duration > 0 { + exp = time.Now().Add(duration) + } + s.data[key] = &entry{ + value: value, + expiration: exp, + } +} + +// get retrieves the value for a key if it exists and is not expired. +func (s *Server) get(key string) (interface{}, bool) { + s.mu.RLock() + ent, ok := s.data[key] + s.mu.RUnlock() + if !ok { + return nil, false + } + if !ent.expiration.IsZero() && time.Now().After(ent.expiration) { + // Key has expired; remove it. + s.mu.Lock() + delete(s.data, key) + s.mu.Unlock() + return nil, false + } + return ent.value, true +} + +// del deletes a key and returns 1 if the key was present. +func (s *Server) del(key string) int { + s.mu.Lock() + defer s.mu.Unlock() + if _, ok := s.data[key]; ok { + delete(s.data, key) + return 1 + } + return 0 +} + +// keys returns all keys matching the given pattern. +// For simplicity, only "*" is fully supported. +func (s *Server) keys(pattern string) []string { + s.mu.RLock() + defer s.mu.RUnlock() + var result []string + // Simple pattern matching: if pattern is "*", return all nonexpired keys. + if pattern == "*" { + for k, ent := range s.data { + if !ent.expiration.IsZero() && time.Now().After(ent.expiration) { + continue + } + result = append(result, k) + } + } else { + // For any other pattern, do a simple substring match. + for k, ent := range s.data { + if !ent.expiration.IsZero() && time.Now().After(ent.expiration) { + continue + } + if strings.Contains(k, pattern) { + result = append(result, k) + } + } + } + return result +} + +// getHash retrieves the hash map stored at key. +func (s *Server) getHash(key string) (map[string]string, bool) { + v, ok := s.get(key) + if !ok { + return nil, false + } + hash, ok := v.(map[string]string) + return hash, ok +} + +// hset sets a field in the hash stored at key. It returns 1 if the field is new. +func (s *Server) hset(key, field, value string) int { + s.mu.Lock() + defer s.mu.Unlock() + var hash map[string]string + ent, exists := s.data[key] + if exists { + if !ent.expiration.IsZero() && time.Now().After(ent.expiration) { + // expired; recreate a new hash. + hash = make(map[string]string) + s.data[key] = &entry{value: hash} + } else { + var ok bool + hash, ok = ent.value.(map[string]string) + if !ok { + // Overwrite if the key holds a non-hash value. + hash = make(map[string]string) + s.data[key] = &entry{value: hash} + } + } + } else { + hash = make(map[string]string) + s.data[key] = &entry{value: hash} + } + _, fieldExists := hash[field] + hash[field] = value + if fieldExists { + return 0 + } + return 1 +} + +// hget retrieves the value of a field in the hash stored at key. +func (s *Server) hget(key, field string) (string, bool) { + hash, ok := s.getHash(key) + if !ok { + return "", false + } + val, exists := hash[field] + return val, exists +} + +// hdel deletes one or more fields from the hash stored at key. +// Returns the number of fields that were removed. +func (s *Server) hdel(key string, fields []string) int { + hash, ok := s.getHash(key) + if !ok { + return 0 + } + count := 0 + for _, field := range fields { + if _, exists := hash[field]; exists { + delete(hash, field) + count++ + } + } + return count +} + +// hkeys returns all field names in the hash stored at key. +func (s *Server) hkeys(key string) []string { + hash, ok := s.getHash(key) + if !ok { + return nil + } + var keys []string + for field := range hash { + keys = append(keys, field) + } + return keys +} + +// hlen returns the number of fields in the hash stored at key. +func (s *Server) hlen(key string) int { + hash, ok := s.getHash(key) + if !ok { + return 0 + } + return len(hash) +} + +// incr increments the integer value stored at key by one. +// If the key does not exist, it is set to 0 before performing the operation. +func (s *Server) incr(key string) (int64, error) { + s.mu.Lock() + defer s.mu.Unlock() + var current int64 + ent, exists := s.data[key] + if exists { + if !ent.expiration.IsZero() && time.Now().After(ent.expiration) { + current = 0 + } else { + switch v := ent.value.(type) { + case string: + var err error + current, err = strconv.ParseInt(v, 10, 64) + if err != nil { + return 0, err + } + case int: + current = int64(v) + case int64: + current = v + default: + return 0, fmt.Errorf("value is not an integer") + } + } + } + current++ + // Store the new value as a string. + s.data[key] = &entry{ + value: strconv.FormatInt(current, 10), + } + return current, nil +} + +func main() { + server := NewServer() + log.Println("Starting Redis-like server on :6379") + err := redcon.ListenAndServe(":6379", + func(conn redcon.Conn, cmd redcon.Command) { + // Every command is expected to have at least one argument (the command name). + if len(cmd.Args) == 0 { + conn.WriteError("ERR empty command") + return + } + command := strings.ToLower(string(cmd.Args[0])) + switch command { + case "ping": + conn.WriteString("PONG") + case "set": + // Usage: SET key value [EX seconds] + if len(cmd.Args) < 3 { + conn.WriteError("ERR wrong number of arguments for 'set' command") + return + } + key := string(cmd.Args[1]) + value := string(cmd.Args[2]) + duration := time.Duration(0) + // Check for an expiration option (only EX is supported here). + if len(cmd.Args) > 3 { + if strings.ToLower(string(cmd.Args[3])) == "ex" && len(cmd.Args) > 4 { + seconds, err := strconv.Atoi(string(cmd.Args[4])) + if err != nil { + conn.WriteError("ERR invalid expire time") + return + } + duration = time.Duration(seconds) * time.Second + } + } + server.set(key, value, duration) + conn.WriteString("OK") + case "get": + if len(cmd.Args) < 2 { + conn.WriteError("ERR wrong number of arguments for 'get' command") + return + } + key := string(cmd.Args[1]) + v, ok := server.get(key) + if !ok { + conn.WriteNull() + return + } + // Only string type is returned by GET. + switch val := v.(type) { + case string: + conn.WriteBulkString(val) + default: + conn.WriteError("WRONGTYPE Operation against a key holding the wrong kind of value") + } + case "del": + if len(cmd.Args) < 2 { + conn.WriteError("ERR wrong number of arguments for 'del' command") + return + } + count := 0 + for i := 1; i < len(cmd.Args); i++ { + key := string(cmd.Args[i]) + count += server.del(key) + } + conn.WriteInt(count) + case "keys": + if len(cmd.Args) < 2 { + conn.WriteError("ERR wrong number of arguments for 'keys' command") + return + } + pattern := string(cmd.Args[1]) + keys := server.keys(pattern) + res := make([][]byte, len(keys)) + for i, k := range keys { + res[i] = []byte(k) + } + conn.WriteArray(res) + case "hset": + // Usage: HSET key field value + if len(cmd.Args) < 4 { + conn.WriteError("ERR wrong number of arguments for 'hset' command") + return + } + key := string(cmd.Args[1]) + field := string(cmd.Args[2]) + value := string(cmd.Args[3]) + added := server.hset(key, field, value) + conn.WriteInt(added) + case "hget": + // Usage: HGET key field + if len(cmd.Args) < 3 { + conn.WriteError("ERR wrong number of arguments for 'hget' command") + return + } + key := string(cmd.Args[1]) + field := string(cmd.Args[2]) + v, ok := server.hget(key, field) + if !ok { + conn.WriteNull() + return + } + conn.WriteBulkString(v) + case "hdel": + // Usage: HDEL key field [field ...] + if len(cmd.Args) < 3 { + conn.WriteError("ERR wrong number of arguments for 'hdel' command") + return + } + key := string(cmd.Args[1]) + fields := make([]string, 0, len(cmd.Args)-2) + for i := 2; i < len(cmd.Args); i++ { + fields = append(fields, string(cmd.Args[i])) + } + removed := server.hdel(key, fields) + conn.WriteInt(removed) + case "hkeys": + // Usage: HKEYS key + if len(cmd.Args) < 2 { + conn.WriteError("ERR wrong number of arguments for 'hkeys' command") + return + } + key := string(cmd.Args[1]) + fields := server.hkeys(key) + res := make([][]byte, len(fields)) + for i, field := range fields { + res[i] = []byte(field) + } + conn.WriteArray(res) + case "hlen": + // Usage: HLEN key + if len(cmd.Args) < 2 { + conn.WriteError("ERR wrong number of arguments for 'hlen' command") + return + } + key := string(cmd.Args[1]) + length := server.hlen(key) + conn.WriteInt(length) + case "incr": + if len(cmd.Args) < 2 { + conn.WriteError("ERR wrong number of arguments for 'incr' command") + return + } + key := string(cmd.Args[1]) + newVal, err := server.incr(key) + if err != nil { + conn.WriteError("ERR " + err.Error()) + return + } + conn.WriteInt64(newVal) + default: + conn.WriteError("ERR unknown command '" + command + "'") + } + }, + // Accept connection: always allow. + func(conn redcon.Conn) bool { return true }, + // On connection close. + func(conn redcon.Conn, err error) {}, + ) + if err != nil { + log.Fatal(err) + } +} +``` + +test above code, test with a redis client it works \ No newline at end of file diff --git a/aiprompts/instructions/instructions_doctree.md b/aiprompts/instructions/instructions_doctree.md new file mode 100644 index 0000000..d49c6fc --- /dev/null +++ b/aiprompts/instructions/instructions_doctree.md @@ -0,0 +1,78 @@ +there is a module called doctree + +the metadata info of doctree is stored in a redis server + +there is the concept of collection + +a collection has markdown pages +each page has a unique name in the collection + +a collection has files which can be a sstd file or image +each file has a unique name + +the names are lower_cased and all non ascci chars are removed, use the namefix function as used in internal/tools/name_fix_test.go + +its a struct called DocTree +which has as argument a path and a name which is also namefixed + +the init walks over the path and finds all files and .md files + +we remember the relative position of each file and markdown page in a hset + +hset is: + +- key: collections:$name +- hkey: pagename.md (always namefixed) +- hkey: imagename.png ... or any other extension for files (always namefixed) +- val: the relative position in the doctree location + +use redisclient to internal redis to store this + +create following methods on doctree + +- Scan (scan the collection again, remove hset and repopulate) +- PageGet get page from a name (do namefix inside method) return the markdown +- PageGetHtml same as PageGet but make html +- FileGetUrl the url which can then be used in static webserver for downloading this content +- PageGetPath relative path in the collection +- Info (name & path) + +in PageGet implement a simple include function which is done as !!include name:'pagename' this needs to include page as mentioned in this collection + if !!include name:'othercollection:pagename' then pagename comes from other collection do namefix to find + + +## Objects + +#### DocTree + +- has add, get, delete, list functions in relation to underlying Collection + +### Collection + +- has get/set/delete/list for pages +- has get/set/delete/list for files + +namefix used everywhere to make sure + +- in get for page we do the include which an get other pages + +## special functions + +### Include + +``` +!!include collectionname:'pagename' +!!include collectionname:'pagename.md' +!!include 'pagename' +!!include collectionname:pagename +!!include collectionname:pagename.md + +``` + +the include needs to parse the following + +note: + +- pages can have .md or not, check if given if not add +- all is namefixed on collection and page level +- there can be '' around the name but this is optional \ No newline at end of file diff --git a/aiprompts/instructions/instructions_handler_client.md b/aiprompts/instructions/instructions_handler_client.md new file mode 100644 index 0000000..94b2205 --- /dev/null +++ b/aiprompts/instructions/instructions_handler_client.md @@ -0,0 +1,9 @@ +create a client for processmanager over telnet in @pkg/handlerfactory/clients + +this is over tcp or socket + +make a factory in clients where we say to which server we want to connect and then make methods per action which have right parameters which then create the heroscript which is sent to the server + +we always expect json back + +the json gets parsed and returned \ No newline at end of file diff --git a/aiprompts/instructions/instructions_herojobs.md b/aiprompts/instructions/instructions_herojobs.md new file mode 100644 index 0000000..e69de29 diff --git a/aiprompts/instructions/instructions_imap.md b/aiprompts/instructions/instructions_imap.md new file mode 100644 index 0000000..86454bd --- /dev/null +++ b/aiprompts/instructions/instructions_imap.md @@ -0,0 +1,28 @@ +create a pkg/imapserver lib which uses: + +https://github.com/foxcpp/go-imap + +the mails are in redis + +the model for mail is in @pkg/mail/model.go + +## the mails are in redis based on following code, learn from it + +cmd/redis_mail_feeder/main.go + +the redis keys are + +- mail:in:$account:$folder:$uid + +the json is the mail model + +see @instructions_imap_feeder.md for details + +## imap server is using the redis as backedn + +- based on what the feeder put in + +there is no no login/passwd, anything is fine, any authentication is fine, +ignore if user specifies it, try to support any login/passwd/authentication method just accept everything + + diff --git a/aiprompts/instructions/instructions_imap_feeder.md b/aiprompts/instructions/instructions_imap_feeder.md new file mode 100644 index 0000000..f76f457 --- /dev/null +++ b/aiprompts/instructions/instructions_imap_feeder.md @@ -0,0 +1,23 @@ + +## populator of imap + +in @/cmd/ +create a new command called redis_mail_feeder + +this feeder creates 100 mails in different folders and stores them in redis as datastor + +the mail model is in @pkg/mail/model.go + +@uid is epoch in seconds + an incrementing number based on of there was already a mail with the same uid before, so we just increment the number until we get a unique uid (is string(epoch)+string(incrementing number)) + +the mails are stored in + +and stores mail in mail:in:$account:$folder:$uid + +id is the blake192 from the json serialization + +- account is random over pol & jan +- folders chose then random can be upto 3 levels deep + +make random emails, 100x in well chosen folder + diff --git a/aiprompts/instructions/instructions_mcp.md b/aiprompts/instructions/instructions_mcp.md new file mode 100644 index 0000000..c8eaa72 --- /dev/null +++ b/aiprompts/instructions/instructions_mcp.md @@ -0,0 +1,4 @@ +the @pkg/mcpopenapi/cmd/mcpopenapi works very welll + +we want to implement + diff --git a/aiprompts/instructions/instructions_openapi_generation.md b/aiprompts/instructions/instructions_openapi_generation.md new file mode 100644 index 0000000..98da3d2 --- /dev/null +++ b/aiprompts/instructions/instructions_openapi_generation.md @@ -0,0 +1,236 @@ +# OpenAPI Generation Instructions + +## Overview + +The OpenAPI package in `pkg/openapi` provides functionality to generate server code from OpenAPI specifications. This document explains how to use this package to generate and host multiple APIs under a single server with Swagger UI integration. + +## Implementation Status + +We have successfully implemented: + +1. A proper test in `pkg/openapi/examples` that generates code from OpenAPI specifications +2. Code generation for two example APIs: + - `petstoreapi` (from `petstore.yaml`) + - `actionsapi` (from `actions.yaml`) +3. A webserver that hosts multiple generated APIs +4. Swagger UI integration for API documentation +5. A home page with links to the APIs and their documentation + +All APIs are hosted under `$serverurl:$port/api` with a clean navigation structure. + +## Directory Structure + +``` +pkg/openapi/ +├── examples/ +│ ├── actions.yaml # OpenAPI spec for Actions API +│ ├── actionsapi/ # Generated code for Actions API +│ ├── main.go # Main server implementation +│ ├── petstore.yaml # OpenAPI spec for Petstore API +│ ├── petstoreapi/ # Generated code for Petstore API +│ ├── README.md # Documentation for examples +│ ├── run_test.sh # Script to run tests and server +│ └── test/ # Tests for OpenAPI generation +├── generator.go # Server code generator +├── parser.go # OpenAPI spec parser +├── example.go # Example usage +└── templates/ # Code generation templates + └── server.tmpl # Server template +``` + +## How to Use + +### Running the Example + +To run the example implementation: + +1. Navigate to the examples directory: + ```bash + cd pkg/openapi/examples + ``` + +2. Run the test script: + ```bash + ./run_test.sh + ``` + +3. Access the APIs: + - API Home: http://localhost:9091/api + - Petstore API: http://localhost:9091/api/petstore + - Petstore API Documentation: http://localhost:9091/api/swagger/petstore + - Actions API: http://localhost:9091/api/actions + - Actions API Documentation: http://localhost:9091/api/swagger/actions + +### Generating Code from Your Own OpenAPI Spec + +To generate code from your own OpenAPI specification: + +```go +package main + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/freeflowuniverse/heroagent/pkg/openapi" +) + +func main() { + // Parse the OpenAPI specification + spec, err := openapi.ParseFromFile("your-api.yaml") + if err != nil { + fmt.Printf("Failed to parse OpenAPI specification: %v\n", err) + os.Exit(1) + } + + // Create a server generator + generator := openapi.NewServerGenerator(spec) + + // Generate server code + serverCode := generator.GenerateServerCode() + + // Write the server code to a file + outputPath := "generated-server.go" + err = os.WriteFile(outputPath, []byte(serverCode), 0644) + if err != nil { + fmt.Printf("Failed to write server code: %v\n", err) + os.Exit(1) + } + + fmt.Printf("Generated server code in %s\n", outputPath) +} +``` + +### Hosting Multiple APIs + +To host multiple APIs under a single server: + +```go +package main + +import ( + "fmt" + + "github.com/freeflowuniverse/heroagent/pkg/openapi" + "github.com/gofiber/fiber/v2" +) + +func main() { + // Create the main server + app := fiber.New() + + // Setup API routes + app.Get("/api", func(c *fiber.Ctx) error { + return c.SendString("API Home Page") + }) + + // Mount the first API + spec1, _ := openapi.ParseFromFile("api1.yaml") + generator1 := openapi.NewServerGenerator(spec1) + apiServer1 := generator1.GenerateServer() + app.Mount("/api/api1", apiServer1) + + // Mount the second API + spec2, _ := openapi.ParseFromFile("api2.yaml") + generator2 := openapi.NewServerGenerator(spec2) + apiServer2 := generator2.GenerateServer() + app.Mount("/api/api2", apiServer2) + + // Start the server + app.Listen(":8080") +} +``` + +### Adding Swagger UI + +To add Swagger UI for API documentation: + +```go +// Serve OpenAPI specs +app.Static("/api/api1/openapi.yaml", "api1.yaml") +app.Static("/api/api2/openapi.yaml", "api2.yaml") + +// API1 Swagger UI +app.Get("/api/swagger/api1", func(c *fiber.Ctx) error { + return c.SendString(` + + +
+ +u-d&&(s=u-d,a.length=s);for(var f=0;f =a)}}for(var h=this.__startIndex;h n?a:o,h=Math.abs(l.label.y-n);if(h>=u.maxY){var c=l.label.x-e-l.len2*r,p=i+l.len,f=Math.abs(c) t.unconstrainedWidth?null:d:null;i.setStyle("width",f)}var g=i.getBoundingRect();o.width=g.width;var y=(i.style.margin||0)+2.1;o.height=g.height+y,o.y-=(o.height-c)/2}}}function kM(t){return"center"===t.position}function LM(t){var e,n,i=t.getData(),r=[],o=!1,a=(t.get("minShowLabelAngle")||0)*CM,s=i.getLayout("viewRect"),l=i.getLayout("r"),u=s.width,h=s.x,c=s.y,p=s.height;function d(t){t.ignore=!0}i.each((function(t){var s=i.getItemGraphicEl(t),c=s.shape,p=s.getTextContent(),f=s.getTextGuideLine(),g=i.getItemModel(t),y=g.getModel("label"),v=y.get("position")||g.get(["emphasis","label","position"]),m=y.get("distanceToLabelLine"),x=y.get("alignTo"),_=$r(y.get("edgeDistance"),u),b=y.get("bleedMargin"),w=g.getModel("labelLine"),S=w.get("length");S=$r(S,u);var M=w.get("length2");if(M=$r(M,u),Math.abs(c.endAngle-c.startAngle)0?"right":"left":k>0?"left":"right"}var B=Math.PI,F=0,G=y.get("rotate");if(j(G))F=G*(B/180);else if("center"===v)F=0;else if("radial"===G||!0===G){F=k<0?-A+B:-A}else if("tangential"===G&&"outside"!==v&&"outer"!==v){var W=Math.atan2(k,L);W<0&&(W=2*B+W),L>0&&(W=B+W),F=W-B}if(o=!!F,p.x=I,p.y=T,p.rotation=F,p.setStyle({verticalAlign:"middle"}),P){p.setStyle({align:D});var H=p.states.select;H&&(H.x+=p.x,H.y+=p.y)}else{var Y=p.getBoundingRect().clone();Y.applyTransform(p.getComputedTransform());var X=(p.style.margin||0)+2.1;Y.y-=X/2,Y.height+=X,r.push({label:p,labelLine:f,position:v,len:S,len2:M,minTurnAngle:w.get("minTurnAngle"),maxSurfaceAngle:w.get("maxSurfaceAngle"),surfaceNormal:new De(k,L),linePoints:C,textAlign:D,labelDistance:m,labelAlignTo:x,edgeDistance:_,bleedMargin:b,rect:Y,unconstrainedWidth:Y.width,labelStyleWidth:p.style.width})}s.setTextConfig({inside:P})}})),!o&&t.get("avoidLabelOverlap")&&function(t,e,n,i,r,o,a,s){for(var l=[],u=[],h=Number.MAX_VALUE,c=-Number.MAX_VALUE,p=0;p i&&(i=e);var o=i%2?i+2:i+3;r=[];for(var a=0;ah[1]&&(h[1]=y),c[p++]=v}return r._count=p,r._indices=c,r._updateGetRawIdx(),r},t.prototype.each=function(t,e){if(this._count)for(var n=t.length,i=this._chunks,r=0,o=this.count();r1||n>0&&!t.noHeader;return E(t.blocks,(function(t){var n=lg(t);n>=e&&(e=n+ +(i&&(!n||ag(t)&&!t.noHeader)))})),e}return 0}function ug(t,e,n,i){var r,o=e.noHeader,a=(r=lg(e),{html:ig[r],richText:rg[r]}),s=[],l=e.blocks||[];lt(!l||Y(l)),l=l||[];var u=t.orderMode;if(e.sortBlocks&&u){l=l.slice();var h={valueAsc:"asc",valueDesc:"desc"};if(_t(h,u)){var c=new kf(h[u],null);l.sort((function(t,e){return c.evaluate(t.sortParam,e.sortParam)}))}else"seriesDesc"===u&&l.reverse()}E(l,(function(n,r){var o=e.valueFormatter,l=sg(n)(o?A(A({},t),{valueFormatter:o}):t,n,r>0?a.html:0,i);null!=l&&s.push(l)}));var p="richText"===t.renderMode?s.join(a.richText):pg(i,s.join(""),o?n:a.html);if(o)return p;var d=mp(e.header,"ordinal",t.useUTC),f=ng(i,t.renderMode).nameStyle,g=eg(i);return"richText"===t.renderMode?dg(t,d,f)+a.richText+p:pg(i,'5)return;var i=this._model.coordinateSystem.getSlidedAxisExpandWindow([t.offsetX,t.offsetY]);"none"!==i.behavior&&this._dispatchExpand({axisExpandWindow:i.axisExpandWindow})}this._mouseDownPoint=null},mousemove:function(t){if(!this._mouseDownPoint&&Rk(this,"mousemove")){var e=this._model,n=e.coordinateSystem.getSlidedAxisExpandWindow([t.offsetX,t.offsetY]),i=n.behavior;"jump"===i&&this._throttledDispatchExpand.debounceNextCall(e.get("axisExpandDebounce")),this._throttledDispatchExpand("none"===i?null:{axisExpandWindow:n.axisExpandWindow,animation:"jump"===i?null:{duration:0}})}}};function Rk(t,e){var n=t._model;return n.get("axisExpandable")&&n.get("axisExpandTriggerOn")===e}var Nk=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.init=function(){t.prototype.init.apply(this,arguments),this.mergeOption({})},e.prototype.mergeOption=function(t){var e=this.option;t&&C(e,t,!0),this._initDimensions()},e.prototype.contains=function(t,e){var n=t.get("parallelIndex");return null!=n&&e.getComponent("parallel",n)===this},e.prototype.setAxisExpand=function(t){E(["axisExpandable","axisExpandCenter","axisExpandCount","axisExpandWidth","axisExpandWindow"],(function(e){t.hasOwnProperty(e)&&(this.option[e]=t[e])}),this)},e.prototype._initDimensions=function(){var t=this.dimensions=[],e=this.parallelAxisIndex=[];E(B(this.ecModel.queryComponents({mainType:"parallelAxis"}),(function(t){return(t.get("parallelIndex")||0)===this.componentIndex}),this),(function(n){t.push("dim"+n.get("dim")),e.push(n.componentIndex)}))},e.type="parallel",e.dependencies=["parallelAxis"],e.layoutMode="box",e.defaultOption={z:0,left:80,top:60,right:80,bottom:60,layout:"horizontal",axisExpandable:!1,axisExpandCenter:null,axisExpandCount:0,axisExpandWidth:50,axisExpandRate:17,axisExpandDebounce:50,axisExpandSlideTriggerArea:[-.15,.05,.4],axisExpandTriggerOn:"click",parallelAxisDefault:null},e}(zp),Ek=function(t){function e(e,n,i,r,o){var a=t.call(this,e,n,i)||this;return a.type=r||"value",a.axisIndex=o,a}return n(e,t),e.prototype.isHorizontal=function(){return"horizontal"!==this.coordinateSystem.getModel().get("layout")},e}(ab);function zk(t,e,n,i,r,o){t=t||0;var a=n[1]-n[0];if(null!=r&&(r=Bk(r,[0,a])),null!=o&&(o=Math.max(o,null!=r?r:0)),"all"===i){var s=Math.abs(e[1]-e[0]);s=Bk(s,[0,a]),r=o=Bk(s,[r,o]),i=0}e[0]=Bk(e[0],n),e[1]=Bk(e[1],n);var l=Vk(e,i);e[i]+=t;var u,h=r||0,c=n.slice();return l.sign<0?c[0]+=h:c[1]-=h,e[i]=Bk(e[i],c),u=Vk(e,i),null!=r&&(u.sign!==l.sign||u.span