@@ -4,6 +4,7 @@
|
|||||||
!/.env.example
|
!/.env.example
|
||||||
/logs/
|
/logs/
|
||||||
/runtime/
|
/runtime/
|
||||||
|
/notes/
|
||||||
/dictionary/actions.jsonl
|
/dictionary/actions.jsonl
|
||||||
/dictionary/actions.index.json
|
/dictionary/actions.index.json
|
||||||
/dictionary/model.codebook.json
|
/dictionary/model.codebook.json
|
||||||
|
|||||||
@@ -12,6 +12,9 @@
|
|||||||
- Start llama.cpp: `cargo run -- serve`
|
- Start llama.cpp: `cargo run -- serve`
|
||||||
- Query local model: `cargo run -- query "log hello from codex"`
|
- Query local model: `cargo run -- query "log hello from codex"`
|
||||||
- Direct proof-of-concept dispatch: `cargo run -- run logger --payload '{"message":"hello from codex"}'`
|
- Direct proof-of-concept dispatch: `cargo run -- run logger --payload '{"message":"hello from codex"}'`
|
||||||
|
- Direct math dispatch: `cargo run -- run calculate --payload '{"expression":"2 + 2 * 3"}'`
|
||||||
|
- Direct text dispatch: `cargo run -- run to_slug --payload '{"text":"Hello, World!"}'`
|
||||||
|
- Direct file dispatch: `cargo run -- run write_note --payload '{"filename":"hello.txt","content":"hello"}'`
|
||||||
- Codex setup: `scripts/codex-setup.sh`
|
- Codex setup: `scripts/codex-setup.sh`
|
||||||
- Codex logger smoke test: `scripts/codex-run-logger.sh`
|
- Codex logger smoke test: `scripts/codex-run-logger.sh`
|
||||||
- Precommit/CI check: `scripts/precommit-check.sh`
|
- Precommit/CI check: `scripts/precommit-check.sh`
|
||||||
@@ -38,6 +41,7 @@ Use `.env` for local values and keep it out of git. Update `.env.example`, `READ
|
|||||||
- `dictionary/actions.index.json` is the generated verbose runtime lookup file.
|
- `dictionary/actions.index.json` is the generated verbose runtime lookup file.
|
||||||
- `dictionary/model.codebook.json` is the generated model-facing compact codebook.
|
- `dictionary/model.codebook.json` is the generated model-facing compact codebook.
|
||||||
- `dictionary/model.examples.jsonl` is generated template/example data for future synthetic-data tooling.
|
- `dictionary/model.examples.jsonl` is generated template/example data for future synthetic-data tooling.
|
||||||
|
- Registered action suites are Math & Conversion, Text Utilities, File Operations, and Logger.
|
||||||
- Regenerate dictionary files with `cargo run -- dictionary generate` after changing registered actions.
|
- Regenerate dictionary files with `cargo run -- dictionary generate` after changing registered actions.
|
||||||
- Do not manually edit generated dictionary files.
|
- Do not manually edit generated dictionary files.
|
||||||
- Generated metadata includes `dictionary_version`, `registry_checksum`, `static_base_checksum`, and `codebook_checksum`.
|
- Generated metadata includes `dictionary_version`, `registry_checksum`, `static_base_checksum`, and `codebook_checksum`.
|
||||||
@@ -50,12 +54,14 @@ Use `.env` for local values and keep it out of git. Update `.env.example`, `READ
|
|||||||
- Unknown, malformed, ambiguous, missing, or empty action ids must be rejected without running scripts.
|
- Unknown, malformed, ambiguous, missing, or empty action ids must be rejected without running scripts.
|
||||||
- Script processes are detached. The controller spawns them and does not monitor completion.
|
- Script processes are detached. The controller spawns them and does not monitor completion.
|
||||||
- Keep script payload validation strict and local to the script module.
|
- Keep script payload validation strict and local to the script module.
|
||||||
|
- File operation scripts must remain confined to project-local `notes/` paths and reject absolute paths or path traversal.
|
||||||
|
|
||||||
## Generated Paths
|
## Generated Paths
|
||||||
|
|
||||||
- `target/`
|
- `target/`
|
||||||
- `logs/`
|
- `logs/`
|
||||||
- `runtime/`
|
- `runtime/`
|
||||||
|
- `notes/`
|
||||||
- `dictionary/actions.jsonl`
|
- `dictionary/actions.jsonl`
|
||||||
- `dictionary/actions.index.json`
|
- `dictionary/actions.index.json`
|
||||||
- `dictionary/model.codebook.json`
|
- `dictionary/model.codebook.json`
|
||||||
|
|||||||
Generated
+108
-5
@@ -151,7 +151,7 @@ checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cpufeatures 0.3.0",
|
"cpufeatures 0.3.0",
|
||||||
"rand_core",
|
"rand_core 0.10.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -283,6 +283,12 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "evalexpr"
|
||||||
|
version = "12.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ae893d2d5e908b78f151ed89de3bfc272cdf6d368c7ed866942f98e24dea208a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastrand"
|
name = "fastrand"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
@@ -386,7 +392,7 @@ dependencies = [
|
|||||||
"js-sys",
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"r-efi",
|
"r-efi",
|
||||||
"rand_core",
|
"rand_core 0.10.1",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -695,6 +701,16 @@ dependencies = [
|
|||||||
"regex-automata",
|
"regex-automata",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "md-5"
|
||||||
|
version = "0.10.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.8.3"
|
version = "2.8.3"
|
||||||
@@ -720,14 +736,19 @@ dependencies = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
|
"evalexpr",
|
||||||
|
"md-5",
|
||||||
|
"rand 0.8.7",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"sha1",
|
||||||
"sha2",
|
"sha2",
|
||||||
"shell-words",
|
"shell-words",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -781,6 +802,15 @@ dependencies = [
|
|||||||
"zerovec",
|
"zerovec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ppv-lite86"
|
||||||
|
version = "0.2.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.107"
|
version = "1.0.107"
|
||||||
@@ -819,7 +849,7 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"getrandom 0.4.3",
|
"getrandom 0.4.3",
|
||||||
"lru-slab",
|
"lru-slab",
|
||||||
"rand",
|
"rand 0.10.2",
|
||||||
"rand_pcg",
|
"rand_pcg",
|
||||||
"ring",
|
"ring",
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
@@ -861,6 +891,17 @@ version = "6.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.8.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"rand_chacha",
|
||||||
|
"rand_core 0.6.4",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.10.2"
|
version = "0.10.2"
|
||||||
@@ -869,7 +910,26 @@ checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"chacha20",
|
"chacha20",
|
||||||
"getrandom 0.4.3",
|
"getrandom 0.4.3",
|
||||||
"rand_core",
|
"rand_core 0.10.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_chacha"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||||
|
dependencies = [
|
||||||
|
"ppv-lite86",
|
||||||
|
"rand_core 0.6.4",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.6.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.2.17",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -884,7 +944,7 @@ version = "0.10.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
|
checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rand_core",
|
"rand_core 0.10.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1079,6 +1139,17 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha1"
|
||||||
|
version = "0.10.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures 0.2.17",
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha2"
|
name = "sha2"
|
||||||
version = "0.10.9"
|
version = "0.10.9"
|
||||||
@@ -1438,6 +1509,18 @@ version = "0.2.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uuid"
|
||||||
|
version = "1.24.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.4.3",
|
||||||
|
"js-sys",
|
||||||
|
"serde_core",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "valuable"
|
name = "valuable"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -1719,6 +1802,26 @@ dependencies = [
|
|||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy"
|
||||||
|
version = "0.8.55"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy-derive"
|
||||||
|
version = "0.8.55"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerofrom"
|
name = "zerofrom"
|
||||||
version = "0.1.8"
|
version = "0.1.8"
|
||||||
|
|||||||
@@ -9,13 +9,18 @@ anyhow = "1.0"
|
|||||||
chrono = { version = "0.4", default-features = false, features = ["clock", "serde"] }
|
chrono = { version = "0.4", default-features = false, features = ["clock", "serde"] }
|
||||||
clap = { version = "4.5", features = ["derive"] }
|
clap = { version = "4.5", features = ["derive"] }
|
||||||
dotenvy = "0.15"
|
dotenvy = "0.15"
|
||||||
|
evalexpr = "12.0"
|
||||||
|
md-5 = "0.10"
|
||||||
|
rand = "0.8"
|
||||||
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
|
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
sha1 = "0.10"
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
shell-words = "1.1"
|
shell-words = "1.1"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
uuid = { version = "1.11", features = ["v4", "serde"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.10"
|
tempfile = "3.10"
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ cargo run -- dictionary generate
|
|||||||
cargo run -- serve
|
cargo run -- serve
|
||||||
cargo run -- query "log hello from codex"
|
cargo run -- query "log hello from codex"
|
||||||
cargo run -- run logger --payload '{"message":"hello from direct dispatch"}'
|
cargo run -- run logger --payload '{"message":"hello from direct dispatch"}'
|
||||||
|
cargo run -- run calculate --payload '{"expression":"2 + 2 * 3"}'
|
||||||
|
cargo run -- run to_slug --payload '{"text":"Hello, World!"}'
|
||||||
|
cargo run -- run write_note --payload '{"filename":"hello.txt","content":"hello"}'
|
||||||
scripts/precommit-check.sh
|
scripts/precommit-check.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -67,7 +70,32 @@ Malformed JSON, missing action codes, stale versions/checksums, aliases, and unk
|
|||||||
|
|
||||||
## Initial Script
|
## Initial Script
|
||||||
|
|
||||||
The first registered script is `logger`. It writes JSONL records to `logs/controller-actions.jsonl`. Dispatch is detached: after the controller spawns the script process, it does not monitor completion.
|
The first registered script was `logger`. It writes JSONL records to `logs/controller-actions.jsonl`. Dispatch is detached: after the controller spawns the script process, it does not monitor completion.
|
||||||
|
|
||||||
|
## Action Suites
|
||||||
|
|
||||||
|
Math & Conversion:
|
||||||
|
|
||||||
|
- `calculate`: `{ "expression": "2 + 2 * 3" }`
|
||||||
|
- `convert_units`: `{ "value": 100, "from": "km", "to": "miles" }`
|
||||||
|
- `generate_uuid`: `{}`
|
||||||
|
- `random_number`: `{ "min": 1, "max": 100 }`
|
||||||
|
|
||||||
|
Text Utilities:
|
||||||
|
|
||||||
|
- `count_words`: `{ "text": "..." }`
|
||||||
|
- `to_slug`: `{ "text": "..." }`
|
||||||
|
- `hash_string`: `{ "text": "...", "algorithm": "sha256|md5|sha1" }`
|
||||||
|
- `truncate_text`: `{ "text": "...", "max_chars": 100 }`
|
||||||
|
|
||||||
|
File Operations:
|
||||||
|
|
||||||
|
- `write_note`: `{ "filename": "...", "content": "..." }`
|
||||||
|
- `append_to_file`: `{ "filename": "...", "content": "..." }`
|
||||||
|
- `read_file`: `{ "filename": "..." }`
|
||||||
|
- `delete_file`: `{ "filename": "..." }`
|
||||||
|
|
||||||
|
File operations are confined to project-local `notes/` and reject absolute paths or path traversal.
|
||||||
|
|
||||||
## CI And Hooks
|
## CI And Hooks
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ Commands:
|
|||||||
- `cargo run -- serve`
|
- `cargo run -- serve`
|
||||||
- `cargo run -- query "log hello from codex"`
|
- `cargo run -- query "log hello from codex"`
|
||||||
- `cargo run -- run logger --payload '{"message":"hello from codex"}'`
|
- `cargo run -- run logger --payload '{"message":"hello from codex"}'`
|
||||||
|
- `cargo run -- run calculate --payload '{"expression":"2 + 2 * 3"}'`
|
||||||
|
- `cargo run -- run to_slug --payload '{"text":"Hello, World!"}'`
|
||||||
|
- `cargo run -- run write_note --payload '{"filename":"hello.txt","content":"hello"}'`
|
||||||
- `scripts/codex-setup.sh`
|
- `scripts/codex-setup.sh`
|
||||||
- `scripts/codex-run-logger.sh`
|
- `scripts/codex-run-logger.sh`
|
||||||
- `scripts/precommit-check.sh`
|
- `scripts/precommit-check.sh`
|
||||||
@@ -42,6 +45,15 @@ Environment:
|
|||||||
|
|
||||||
The LLM may return verbose JSON like `{"action_id":"logger","payload":{"message":"hello"}}` or compact codebook JSON like `{"v":"1.0.0","c":"<codebook_checksum>","a":"A001","p":{"F001":"hello"}}`. Compact outputs must match `dictionary/model.codebook.json` version and checksum before expansion. The runtime only executes exact `action_id` matches from `dictionary/actions.index.json`; aliases are not executable. Invalid or unknown outputs are rejected without running anything.
|
The LLM may return verbose JSON like `{"action_id":"logger","payload":{"message":"hello"}}` or compact codebook JSON like `{"v":"1.0.0","c":"<codebook_checksum>","a":"A001","p":{"F001":"hello"}}`. Compact outputs must match `dictionary/model.codebook.json` version and checksum before expansion. The runtime only executes exact `action_id` matches from `dictionary/actions.index.json`; aliases are not executable. Invalid or unknown outputs are rejected without running anything.
|
||||||
|
|
||||||
|
Registered suites:
|
||||||
|
|
||||||
|
- Math & Conversion: `calculate`, `convert_units`, `generate_uuid`, `random_number`.
|
||||||
|
- Text Utilities: `count_words`, `to_slug`, `hash_string`, `truncate_text`.
|
||||||
|
- File Operations: `write_note`, `append_to_file`, `read_file`, `delete_file`.
|
||||||
|
- Logger: `logger`.
|
||||||
|
|
||||||
|
File operations are sandboxed to project-local `notes/`.
|
||||||
|
|
||||||
Codex environment: `.codex/environments/environment.toml` defines setup plus Build, Test, Generate Dictionary, Precommit Check, Run Logger, Query Logger, and Serve Llama actions.
|
Codex environment: `.codex/environments/environment.toml` defines setup plus Build, Test, Generate Dictionary, Precommit Check, Run Logger, Query Logger, and Serve Llama actions.
|
||||||
|
|
||||||
CI and hooks: `.gitea/workflows/ci.yml`, `.woodpecker.yml`, and `.githooks/pre-commit` all use `scripts/precommit-check.sh` to verify formatting, tests, and dictionary generation.
|
CI and hooks: `.gitea/workflows/ci.yml`, `.woodpecker.yml`, and `.githooks/pre-commit` all use `scripts/precommit-check.sh` to verify formatting, tests, and dictionary generation.
|
||||||
|
|||||||
@@ -21,6 +21,9 @@
|
|||||||
"serve": "cargo run -- serve",
|
"serve": "cargo run -- serve",
|
||||||
"query": "cargo run -- query \"log hello from codex\"",
|
"query": "cargo run -- query \"log hello from codex\"",
|
||||||
"run_logger": "cargo run -- run logger --payload '{\"message\":\"hello from codex\"}'",
|
"run_logger": "cargo run -- run logger --payload '{\"message\":\"hello from codex\"}'",
|
||||||
|
"run_calculate": "cargo run -- run calculate --payload '{\"expression\":\"2 + 2 * 3\"}'",
|
||||||
|
"run_to_slug": "cargo run -- run to_slug --payload '{\"text\":\"Hello, World!\"}'",
|
||||||
|
"run_write_note": "cargo run -- run write_note --payload '{\"filename\":\"hello.txt\",\"content\":\"hello\"}'",
|
||||||
"codex_setup": "scripts/codex-setup.sh",
|
"codex_setup": "scripts/codex-setup.sh",
|
||||||
"codex_logger_smoke": "scripts/codex-run-logger.sh",
|
"codex_logger_smoke": "scripts/codex-run-logger.sh",
|
||||||
"precommit_check": "scripts/precommit-check.sh",
|
"precommit_check": "scripts/precommit-check.sh",
|
||||||
@@ -108,8 +111,102 @@
|
|||||||
"runtime_policy": "Verbose outputs require exact action_id matches; compact outputs require matching version/checksum and exact codebook expansion before dispatch."
|
"runtime_policy": "Verbose outputs require exact action_id matches; compact outputs require matching version/checksum and exact codebook expansion before dispatch."
|
||||||
},
|
},
|
||||||
"actions": [
|
"actions": [
|
||||||
|
{
|
||||||
|
"action_id": "calculate",
|
||||||
|
"suite": "Math & Conversion",
|
||||||
|
"payload_example": {
|
||||||
|
"expression": "2 + 2 * 3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action_id": "convert_units",
|
||||||
|
"suite": "Math & Conversion",
|
||||||
|
"payload_example": {
|
||||||
|
"value": 100,
|
||||||
|
"from": "km",
|
||||||
|
"to": "miles"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action_id": "generate_uuid",
|
||||||
|
"suite": "Math & Conversion",
|
||||||
|
"payload_example": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action_id": "random_number",
|
||||||
|
"suite": "Math & Conversion",
|
||||||
|
"payload_example": {
|
||||||
|
"min": 1,
|
||||||
|
"max": 100
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action_id": "count_words",
|
||||||
|
"suite": "Text Utilities",
|
||||||
|
"payload_example": {
|
||||||
|
"text": "hello from codex"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action_id": "to_slug",
|
||||||
|
"suite": "Text Utilities",
|
||||||
|
"payload_example": {
|
||||||
|
"text": "Hello, World!"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action_id": "hash_string",
|
||||||
|
"suite": "Text Utilities",
|
||||||
|
"payload_example": {
|
||||||
|
"text": "hello",
|
||||||
|
"algorithm": "sha256"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action_id": "truncate_text",
|
||||||
|
"suite": "Text Utilities",
|
||||||
|
"payload_example": {
|
||||||
|
"text": "hello from codex",
|
||||||
|
"max_chars": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action_id": "write_note",
|
||||||
|
"suite": "File Operations",
|
||||||
|
"payload_example": {
|
||||||
|
"filename": "hello.txt",
|
||||||
|
"content": "hello"
|
||||||
|
},
|
||||||
|
"sandbox": "notes/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action_id": "append_to_file",
|
||||||
|
"suite": "File Operations",
|
||||||
|
"payload_example": {
|
||||||
|
"filename": "hello.txt",
|
||||||
|
"content": " world"
|
||||||
|
},
|
||||||
|
"sandbox": "notes/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action_id": "read_file",
|
||||||
|
"suite": "File Operations",
|
||||||
|
"payload_example": {
|
||||||
|
"filename": "hello.txt"
|
||||||
|
},
|
||||||
|
"sandbox": "notes/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action_id": "delete_file",
|
||||||
|
"suite": "File Operations",
|
||||||
|
"payload_example": {
|
||||||
|
"filename": "hello.txt"
|
||||||
|
},
|
||||||
|
"sandbox": "notes/"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"action_id": "logger",
|
"action_id": "logger",
|
||||||
|
"suite": "Logger",
|
||||||
"script": "src/scripts/logger.rs",
|
"script": "src/scripts/logger.rs",
|
||||||
"description": "Proof-of-concept script that writes structured JSONL log records.",
|
"description": "Proof-of-concept script that writes structured JSONL log records.",
|
||||||
"payload_example": {
|
"payload_example": {
|
||||||
@@ -122,6 +219,7 @@
|
|||||||
"target/",
|
"target/",
|
||||||
"logs/",
|
"logs/",
|
||||||
"runtime/",
|
"runtime/",
|
||||||
|
"notes/",
|
||||||
"dictionary/actions.jsonl",
|
"dictionary/actions.jsonl",
|
||||||
"dictionary/actions.index.json",
|
"dictionary/actions.index.json",
|
||||||
"dictionary/model.codebook.json",
|
"dictionary/model.codebook.json",
|
||||||
|
|||||||
+15
-8
@@ -426,9 +426,10 @@ mod tests {
|
|||||||
fn adaptive_layer_maps_logger_and_message_deterministically() {
|
fn adaptive_layer_maps_logger_and_message_deterministically() {
|
||||||
let records = sorted_records(®istered_actions());
|
let records = sorted_records(®istered_actions());
|
||||||
let adaptive = build_adaptive_codebook(&records);
|
let adaptive = build_adaptive_codebook(&records);
|
||||||
assert_eq!(adaptive.actions["A001"], "logger");
|
assert_eq!(adaptive.actions["A001"], "append_to_file");
|
||||||
assert_eq!(adaptive.fields["F001"], "message");
|
assert!(adaptive.actions.values().any(|value| value == "logger"));
|
||||||
assert_eq!(adaptive.modules["M001"], "logger");
|
assert!(adaptive.fields.values().any(|value| value == "message"));
|
||||||
|
assert!(adaptive.modules.values().any(|value| value == "logger"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -459,23 +460,29 @@ mod tests {
|
|||||||
let temp = temp_dictionary_dir();
|
let temp = temp_dictionary_dir();
|
||||||
let summary = generate_dictionary_files(®istered_actions(), temp.path()).unwrap();
|
let summary = generate_dictionary_files(®istered_actions(), temp.path()).unwrap();
|
||||||
|
|
||||||
assert_eq!(summary.action_count, 1);
|
assert_eq!(summary.action_count, 13);
|
||||||
assert!(summary.jsonl_path.exists());
|
assert!(summary.jsonl_path.exists());
|
||||||
assert!(summary.index_path.exists());
|
assert!(summary.index_path.exists());
|
||||||
assert!(summary.codebook_path.exists());
|
assert!(summary.codebook_path.exists());
|
||||||
assert!(summary.examples_path.exists());
|
assert!(summary.examples_path.exists());
|
||||||
|
|
||||||
let records = read_jsonl_records(&summary.jsonl_path).unwrap();
|
let records = read_jsonl_records(&summary.jsonl_path).unwrap();
|
||||||
assert_eq!(records.len(), 1);
|
assert_eq!(records.len(), 13);
|
||||||
assert_eq!(records[0].action_id, "logger");
|
assert!(records.iter().any(|record| record.action_id == "logger"));
|
||||||
|
|
||||||
let index = load_action_index(temp.path()).unwrap();
|
let index = load_action_index(temp.path()).unwrap();
|
||||||
assert!(index.actions.contains_key("logger"));
|
assert!(index.actions.contains_key("logger"));
|
||||||
assert!(!index.actions.contains_key("log"));
|
assert!(!index.actions.contains_key("log"));
|
||||||
|
|
||||||
let codebook = load_model_codebook(temp.path()).unwrap();
|
let codebook = load_model_codebook(temp.path()).unwrap();
|
||||||
assert_eq!(codebook.adaptive.actions["A001"], "logger");
|
assert_eq!(codebook.adaptive.actions["A001"], "append_to_file");
|
||||||
assert_eq!(codebook.adaptive.fields["F001"], "message");
|
assert!(
|
||||||
|
codebook
|
||||||
|
.adaptive
|
||||||
|
.fields
|
||||||
|
.values()
|
||||||
|
.any(|value| value == "message")
|
||||||
|
);
|
||||||
assert!(!codebook.codebook_checksum.is_empty());
|
assert!(!codebook.codebook_checksum.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,19 @@ use std::fs::{self, OpenOptions};
|
|||||||
use std::process::{Command, Stdio};
|
use std::process::{Command, Stdio};
|
||||||
|
|
||||||
use crate::dictionary::ActionIndex;
|
use crate::dictionary::ActionIndex;
|
||||||
|
use crate::scripts::file_ops::{
|
||||||
|
FilePayload, WriteFilePayload, run_append_to_file, run_delete_file, run_read_file,
|
||||||
|
run_write_note,
|
||||||
|
};
|
||||||
use crate::scripts::logger::{LoggerPayload, run_logger};
|
use crate::scripts::logger::{LoggerPayload, run_logger};
|
||||||
|
use crate::scripts::math::{
|
||||||
|
CalculatePayload, ConvertUnitsPayload, GenerateUuidPayload, RandomNumberPayload, run_calculate,
|
||||||
|
run_convert_units, run_generate_uuid, run_random_number,
|
||||||
|
};
|
||||||
|
use crate::scripts::text::{
|
||||||
|
CountWordsPayload, HashStringPayload, ToSlugPayload, TruncateTextPayload, run_count_words,
|
||||||
|
run_hash_string, run_to_slug, run_truncate_text,
|
||||||
|
};
|
||||||
|
|
||||||
pub fn dispatch_action(index: &ActionIndex, action_id: &str, payload: Value) -> Result<()> {
|
pub fn dispatch_action(index: &ActionIndex, action_id: &str, payload: Value) -> Result<()> {
|
||||||
let action = index
|
let action = index
|
||||||
@@ -46,6 +58,54 @@ pub fn run_internal_script(action_id: &str, payload: Value) -> Result<()> {
|
|||||||
let payload = serde_json::from_value::<LoggerPayload>(payload)?;
|
let payload = serde_json::from_value::<LoggerPayload>(payload)?;
|
||||||
run_logger(payload)
|
run_logger(payload)
|
||||||
}
|
}
|
||||||
|
"calculate" => {
|
||||||
|
let payload = serde_json::from_value::<CalculatePayload>(payload)?;
|
||||||
|
run_calculate(payload)
|
||||||
|
}
|
||||||
|
"convert_units" => {
|
||||||
|
let payload = serde_json::from_value::<ConvertUnitsPayload>(payload)?;
|
||||||
|
run_convert_units(payload)
|
||||||
|
}
|
||||||
|
"generate_uuid" => {
|
||||||
|
let payload = serde_json::from_value::<GenerateUuidPayload>(payload)?;
|
||||||
|
run_generate_uuid(payload)
|
||||||
|
}
|
||||||
|
"random_number" => {
|
||||||
|
let payload = serde_json::from_value::<RandomNumberPayload>(payload)?;
|
||||||
|
run_random_number(payload)
|
||||||
|
}
|
||||||
|
"count_words" => {
|
||||||
|
let payload = serde_json::from_value::<CountWordsPayload>(payload)?;
|
||||||
|
run_count_words(payload)
|
||||||
|
}
|
||||||
|
"to_slug" => {
|
||||||
|
let payload = serde_json::from_value::<ToSlugPayload>(payload)?;
|
||||||
|
run_to_slug(payload)
|
||||||
|
}
|
||||||
|
"hash_string" => {
|
||||||
|
let payload = serde_json::from_value::<HashStringPayload>(payload)?;
|
||||||
|
run_hash_string(payload)
|
||||||
|
}
|
||||||
|
"truncate_text" => {
|
||||||
|
let payload = serde_json::from_value::<TruncateTextPayload>(payload)?;
|
||||||
|
run_truncate_text(payload)
|
||||||
|
}
|
||||||
|
"write_note" => {
|
||||||
|
let payload = serde_json::from_value::<WriteFilePayload>(payload)?;
|
||||||
|
run_write_note(payload)
|
||||||
|
}
|
||||||
|
"append_to_file" => {
|
||||||
|
let payload = serde_json::from_value::<WriteFilePayload>(payload)?;
|
||||||
|
run_append_to_file(payload)
|
||||||
|
}
|
||||||
|
"read_file" => {
|
||||||
|
let payload = serde_json::from_value::<FilePayload>(payload)?;
|
||||||
|
run_read_file(payload)
|
||||||
|
}
|
||||||
|
"delete_file" => {
|
||||||
|
let payload = serde_json::from_value::<FilePayload>(payload)?;
|
||||||
|
run_delete_file(payload)
|
||||||
|
}
|
||||||
other => bail!("unknown internal script `{other}`"),
|
other => bail!("unknown internal script `{other}`"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-2
@@ -227,9 +227,21 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn expands_valid_compact_action_response() {
|
fn expands_valid_compact_action_response() {
|
||||||
let codebook = test_codebook();
|
let codebook = test_codebook();
|
||||||
|
let logger_code = codebook
|
||||||
|
.adaptive
|
||||||
|
.actions
|
||||||
|
.iter()
|
||||||
|
.find_map(|(code, action)| (action == "logger").then_some(code.as_str()))
|
||||||
|
.unwrap();
|
||||||
|
let message_code = codebook
|
||||||
|
.adaptive
|
||||||
|
.fields
|
||||||
|
.iter()
|
||||||
|
.find_map(|(code, field)| (field == "message").then_some(code.as_str()))
|
||||||
|
.unwrap();
|
||||||
let raw = format!(
|
let raw = format!(
|
||||||
r#"{{"v":"{}","c":"{}","a":"A001","p":{{"F001":"hello"}}}}"#,
|
r#"{{"v":"{}","c":"{}","a":"{}","p":{{"{}":"hello"}}}}"#,
|
||||||
codebook.dictionary_version, codebook.codebook_checksum
|
codebook.dictionary_version, codebook.codebook_checksum, logger_code, message_code
|
||||||
);
|
);
|
||||||
let parsed = parse_action_response(&raw, &codebook).unwrap();
|
let parsed = parse_action_response(&raw, &codebook).unwrap();
|
||||||
assert_eq!(parsed.action_id, "logger");
|
assert_eq!(parsed.action_id, "logger");
|
||||||
|
|||||||
+264
-24
@@ -18,33 +18,273 @@ pub struct ExecutableDefinition {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn registered_actions() -> Vec<ActionDefinition> {
|
pub fn registered_actions() -> Vec<ActionDefinition> {
|
||||||
vec![ActionDefinition {
|
vec![
|
||||||
action_id: "logger".to_string(),
|
action(
|
||||||
aliases: vec![
|
"append_to_file",
|
||||||
"log".to_string(),
|
&["append", "append_file", "add_to_file"],
|
||||||
"write_log".to_string(),
|
"Append content to a project-local notes file.",
|
||||||
"record_message".to_string(),
|
&[
|
||||||
|
"append this line to notes.txt",
|
||||||
|
"add content to a file",
|
||||||
|
"append to a project note",
|
||||||
],
|
],
|
||||||
description: "Write a structured proof-of-concept log message.".to_string(),
|
object_schema(
|
||||||
intent_examples: vec![
|
&[
|
||||||
"log hello from codex".to_string(),
|
("filename", string_schema("Relative notes/ filename.")),
|
||||||
"record this message".to_string(),
|
("content", string_schema("Content to append.")),
|
||||||
"write a logger proof of concept entry".to_string(),
|
|
||||||
],
|
],
|
||||||
payload_schema: serde_json::json!({
|
&["filename", "content"],
|
||||||
"type": "object",
|
),
|
||||||
"required": ["message"],
|
),
|
||||||
"properties": {
|
action(
|
||||||
"message": {
|
"calculate",
|
||||||
"type": "string",
|
&["calc", "evaluate_expression", "math"],
|
||||||
"description": "Message text to write into logs/controller-actions.jsonl."
|
"Evaluate a simple math expression.",
|
||||||
}
|
&[
|
||||||
},
|
"calculate 2 + 2 * 3",
|
||||||
"additionalProperties": false
|
"evaluate this expression",
|
||||||
}),
|
"what is 10 / 2 plus 8",
|
||||||
|
],
|
||||||
|
object_schema(
|
||||||
|
&[("expression", string_schema("Math expression to evaluate."))],
|
||||||
|
&["expression"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
action(
|
||||||
|
"convert_units",
|
||||||
|
&["convert", "unit_conversion", "convert_measurement"],
|
||||||
|
"Convert a numeric distance value between supported units.",
|
||||||
|
&[
|
||||||
|
"convert 100 km to miles",
|
||||||
|
"convert meters to feet",
|
||||||
|
"unit conversion for 12 inches to cm",
|
||||||
|
],
|
||||||
|
object_schema(
|
||||||
|
&[
|
||||||
|
("value", number_schema("Numeric value to convert.")),
|
||||||
|
("from", string_schema("Source unit.")),
|
||||||
|
("to", string_schema("Target unit.")),
|
||||||
|
],
|
||||||
|
&["value", "from", "to"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
action(
|
||||||
|
"count_words",
|
||||||
|
&["word_count", "count_text_words"],
|
||||||
|
"Count whitespace-delimited words in text.",
|
||||||
|
&[
|
||||||
|
"count words in this text",
|
||||||
|
"how many words are here",
|
||||||
|
"word count this sentence",
|
||||||
|
],
|
||||||
|
object_schema(
|
||||||
|
&[("text", string_schema("Text to count words in."))],
|
||||||
|
&["text"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
action(
|
||||||
|
"delete_file",
|
||||||
|
&["delete_note", "remove_file", "remove_note"],
|
||||||
|
"Delete a project-local notes file.",
|
||||||
|
&[
|
||||||
|
"delete note.txt",
|
||||||
|
"remove this note file",
|
||||||
|
"delete a project note",
|
||||||
|
],
|
||||||
|
object_schema(
|
||||||
|
&[("filename", string_schema("Relative notes/ filename."))],
|
||||||
|
&["filename"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
action(
|
||||||
|
"generate_uuid",
|
||||||
|
&["uuid", "new_uuid", "create_uuid"],
|
||||||
|
"Generate a random UUID v4.",
|
||||||
|
&[
|
||||||
|
"generate a uuid",
|
||||||
|
"create a new uuid",
|
||||||
|
"give me a random uuid",
|
||||||
|
],
|
||||||
|
object_schema(&[], &[]),
|
||||||
|
),
|
||||||
|
action(
|
||||||
|
"hash_string",
|
||||||
|
&["hash", "digest_string", "string_hash"],
|
||||||
|
"Hash text with sha256, md5, or sha1.",
|
||||||
|
&[
|
||||||
|
"hash hello with sha256",
|
||||||
|
"md5 this string",
|
||||||
|
"sha1 digest for this text",
|
||||||
|
],
|
||||||
|
object_schema(
|
||||||
|
&[
|
||||||
|
("text", string_schema("Text to hash.")),
|
||||||
|
(
|
||||||
|
"algorithm",
|
||||||
|
enum_schema("Hash algorithm.", &["sha256", "md5", "sha1"]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
&["text", "algorithm"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
action(
|
||||||
|
"logger",
|
||||||
|
&["log", "write_log", "record_message"],
|
||||||
|
"Write a structured proof-of-concept log message.",
|
||||||
|
&[
|
||||||
|
"log hello from codex",
|
||||||
|
"record this message",
|
||||||
|
"write a logger proof of concept entry",
|
||||||
|
],
|
||||||
|
object_schema(
|
||||||
|
&[(
|
||||||
|
"message",
|
||||||
|
string_schema("Message text to write into logs/controller-actions.jsonl."),
|
||||||
|
)],
|
||||||
|
&["message"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
action(
|
||||||
|
"random_number",
|
||||||
|
&["random_int", "roll_number", "number_in_range"],
|
||||||
|
"Generate a random integer in an inclusive range.",
|
||||||
|
&[
|
||||||
|
"random number between 1 and 100",
|
||||||
|
"roll a number in this range",
|
||||||
|
"pick an integer from 5 to 10",
|
||||||
|
],
|
||||||
|
object_schema(
|
||||||
|
&[
|
||||||
|
("min", integer_schema("Inclusive minimum.")),
|
||||||
|
("max", integer_schema("Inclusive maximum.")),
|
||||||
|
],
|
||||||
|
&["min", "max"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
action(
|
||||||
|
"read_file",
|
||||||
|
&["read_note", "open_file", "show_file"],
|
||||||
|
"Read a project-local notes file.",
|
||||||
|
&[
|
||||||
|
"read note.txt",
|
||||||
|
"show the contents of this note",
|
||||||
|
"open a project note file",
|
||||||
|
],
|
||||||
|
object_schema(
|
||||||
|
&[("filename", string_schema("Relative notes/ filename."))],
|
||||||
|
&["filename"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
action(
|
||||||
|
"to_slug",
|
||||||
|
&["slugify", "make_slug", "url_slug"],
|
||||||
|
"Convert text to a URL-safe ASCII slug.",
|
||||||
|
&[
|
||||||
|
"slugify this title",
|
||||||
|
"make a url slug",
|
||||||
|
"convert text to a slug",
|
||||||
|
],
|
||||||
|
object_schema(&[("text", string_schema("Text to slugify."))], &["text"]),
|
||||||
|
),
|
||||||
|
action(
|
||||||
|
"truncate_text",
|
||||||
|
&["truncate", "shorten_text", "clip_text"],
|
||||||
|
"Truncate text to a maximum character count.",
|
||||||
|
&[
|
||||||
|
"truncate this to 100 characters",
|
||||||
|
"shorten this text",
|
||||||
|
"clip text to a max length",
|
||||||
|
],
|
||||||
|
object_schema(
|
||||||
|
&[
|
||||||
|
("text", string_schema("Text to truncate.")),
|
||||||
|
("max_chars", integer_schema("Maximum number of characters.")),
|
||||||
|
],
|
||||||
|
&["text", "max_chars"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
action(
|
||||||
|
"write_note",
|
||||||
|
&["write_file", "save_note", "create_note"],
|
||||||
|
"Create or overwrite a project-local notes file.",
|
||||||
|
&[
|
||||||
|
"write a note file",
|
||||||
|
"save this note",
|
||||||
|
"create a text file with this content",
|
||||||
|
],
|
||||||
|
object_schema(
|
||||||
|
&[
|
||||||
|
("filename", string_schema("Relative notes/ filename.")),
|
||||||
|
("content", string_schema("Content to write.")),
|
||||||
|
],
|
||||||
|
&["filename", "content"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn action(
|
||||||
|
action_id: &str,
|
||||||
|
aliases: &[&str],
|
||||||
|
description: &str,
|
||||||
|
intent_examples: &[&str],
|
||||||
|
payload_schema: Value,
|
||||||
|
) -> ActionDefinition {
|
||||||
|
ActionDefinition {
|
||||||
|
action_id: action_id.to_string(),
|
||||||
|
aliases: aliases.iter().map(|alias| alias.to_string()).collect(),
|
||||||
|
description: description.to_string(),
|
||||||
|
intent_examples: intent_examples
|
||||||
|
.iter()
|
||||||
|
.map(|example| example.to_string())
|
||||||
|
.collect(),
|
||||||
|
payload_schema,
|
||||||
executable: ExecutableDefinition {
|
executable: ExecutableDefinition {
|
||||||
kind: "internal-rust-script".to_string(),
|
kind: "internal-rust-script".to_string(),
|
||||||
script: "logger".to_string(),
|
script: action_id.to_string(),
|
||||||
},
|
},
|
||||||
}]
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn object_schema(properties: &[(&str, Value)], required: &[&str]) -> Value {
|
||||||
|
let properties = properties
|
||||||
|
.iter()
|
||||||
|
.map(|(name, schema)| ((*name).to_string(), schema.clone()))
|
||||||
|
.collect::<serde_json::Map<_, _>>();
|
||||||
|
|
||||||
|
serde_json::json!({
|
||||||
|
"type": "object",
|
||||||
|
"required": required,
|
||||||
|
"properties": properties,
|
||||||
|
"additionalProperties": false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn string_schema(description: &str) -> Value {
|
||||||
|
serde_json::json!({
|
||||||
|
"type": "string",
|
||||||
|
"description": description
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn number_schema(description: &str) -> Value {
|
||||||
|
serde_json::json!({
|
||||||
|
"type": "number",
|
||||||
|
"description": description
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn integer_schema(description: &str) -> Value {
|
||||||
|
serde_json::json!({
|
||||||
|
"type": "integer",
|
||||||
|
"description": description
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enum_schema(description: &str, values: &[&str]) -> Value {
|
||||||
|
serde_json::json!({
|
||||||
|
"type": "string",
|
||||||
|
"description": description,
|
||||||
|
"enum": values
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
use anyhow::{Context, Result, bail};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fs::{self, OpenOptions};
|
||||||
|
use std::io::Write;
|
||||||
|
use std::path::{Component, Path, PathBuf};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct FilePayload {
|
||||||
|
pub filename: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct WriteFilePayload {
|
||||||
|
pub filename: String,
|
||||||
|
pub content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_write_note(payload: WriteFilePayload) -> Result<()> {
|
||||||
|
let path = write_note(payload)?;
|
||||||
|
print_json(
|
||||||
|
"write_note",
|
||||||
|
serde_json::json!({ "path": path.display().to_string() }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_append_to_file(payload: WriteFilePayload) -> Result<()> {
|
||||||
|
let path = append_to_file(payload)?;
|
||||||
|
print_json(
|
||||||
|
"append_to_file",
|
||||||
|
serde_json::json!({ "path": path.display().to_string() }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_read_file(payload: FilePayload) -> Result<()> {
|
||||||
|
let contents = read_file(payload)?;
|
||||||
|
print_json("read_file", serde_json::json!({ "content": contents }))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_delete_file(payload: FilePayload) -> Result<()> {
|
||||||
|
let path = delete_file(payload)?;
|
||||||
|
print_json(
|
||||||
|
"delete_file",
|
||||||
|
serde_json::json!({ "path": path.display().to_string() }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_note(payload: WriteFilePayload) -> Result<PathBuf> {
|
||||||
|
let path = safe_notes_path(&payload.filename)?;
|
||||||
|
ensure_parent(&path)?;
|
||||||
|
fs::write(&path, payload.content).with_context(|| format!("writing {}", path.display()))?;
|
||||||
|
Ok(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn append_to_file(payload: WriteFilePayload) -> Result<PathBuf> {
|
||||||
|
let path = safe_notes_path(&payload.filename)?;
|
||||||
|
ensure_parent(&path)?;
|
||||||
|
let mut file = OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.append(true)
|
||||||
|
.open(&path)
|
||||||
|
.with_context(|| format!("opening {}", path.display()))?;
|
||||||
|
file.write_all(payload.content.as_bytes())?;
|
||||||
|
Ok(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_file(payload: FilePayload) -> Result<String> {
|
||||||
|
let path = safe_notes_path(&payload.filename)?;
|
||||||
|
fs::read_to_string(&path).with_context(|| format!("reading {}", path.display()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_file(payload: FilePayload) -> Result<PathBuf> {
|
||||||
|
let path = safe_notes_path(&payload.filename)?;
|
||||||
|
fs::remove_file(&path).with_context(|| format!("deleting {}", path.display()))?;
|
||||||
|
Ok(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn safe_notes_path(filename: &str) -> Result<PathBuf> {
|
||||||
|
let trimmed = filename.trim();
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
bail!("filename must not be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
let relative = Path::new(trimmed);
|
||||||
|
if relative.is_absolute() {
|
||||||
|
bail!("filename must be relative to notes/");
|
||||||
|
}
|
||||||
|
|
||||||
|
for component in relative.components() {
|
||||||
|
match component {
|
||||||
|
Component::Normal(_) => {}
|
||||||
|
_ => bail!("filename must not contain path traversal or special components"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(PathBuf::from("notes").join(relative))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_parent(path: &Path) -> Result<()> {
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
fs::create_dir_all(parent).with_context(|| format!("creating {}", parent.display()))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_json(action_id: &str, payload: serde_json::Value) -> Result<()> {
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
serde_json::to_string(&serde_json::json!({
|
||||||
|
"action_id": action_id,
|
||||||
|
"output": payload
|
||||||
|
}))?
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_unsafe_paths() {
|
||||||
|
assert!(safe_notes_path("../secret.txt").is_err());
|
||||||
|
assert!(safe_notes_path("/tmp/secret.txt").is_err());
|
||||||
|
assert!(safe_notes_path("").is_err());
|
||||||
|
assert_eq!(
|
||||||
|
safe_notes_path("daily/note.txt").unwrap(),
|
||||||
|
PathBuf::from("notes/daily/note.txt")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn writes_appends_reads_and_deletes_note() {
|
||||||
|
let temp = tempfile::tempdir().unwrap();
|
||||||
|
let previous = std::env::current_dir().unwrap();
|
||||||
|
std::env::set_current_dir(temp.path()).unwrap();
|
||||||
|
|
||||||
|
write_note(WriteFilePayload {
|
||||||
|
filename: "test.txt".to_string(),
|
||||||
|
content: "hello".to_string(),
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
append_to_file(WriteFilePayload {
|
||||||
|
filename: "test.txt".to_string(),
|
||||||
|
content: " world".to_string(),
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
read_file(FilePayload {
|
||||||
|
filename: "test.txt".to_string()
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
"hello world"
|
||||||
|
);
|
||||||
|
delete_file(FilePayload {
|
||||||
|
filename: "test.txt".to_string(),
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
assert!(!Path::new("notes/test.txt").exists());
|
||||||
|
|
||||||
|
std::env::set_current_dir(previous).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
use anyhow::{Result, bail};
|
||||||
|
use evalexpr::eval;
|
||||||
|
use rand::Rng;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub struct CalculatePayload {
|
||||||
|
pub expression: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub struct ConvertUnitsPayload {
|
||||||
|
pub value: f64,
|
||||||
|
pub from: String,
|
||||||
|
pub to: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct GenerateUuidPayload {}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct RandomNumberPayload {
|
||||||
|
pub min: i64,
|
||||||
|
pub max: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_calculate(payload: CalculatePayload) -> Result<()> {
|
||||||
|
let result = calculate(payload)?;
|
||||||
|
print_json("calculate", serde_json::json!({ "result": result }))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_convert_units(payload: ConvertUnitsPayload) -> Result<()> {
|
||||||
|
let result = convert_units(payload)?;
|
||||||
|
print_json("convert_units", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_generate_uuid(_payload: GenerateUuidPayload) -> Result<()> {
|
||||||
|
print_json(
|
||||||
|
"generate_uuid",
|
||||||
|
serde_json::json!({ "uuid": Uuid::new_v4().to_string() }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_random_number(payload: RandomNumberPayload) -> Result<()> {
|
||||||
|
let number = random_number(payload)?;
|
||||||
|
print_json("random_number", serde_json::json!({ "number": number }))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn calculate(payload: CalculatePayload) -> Result<Value> {
|
||||||
|
let expression = payload.expression.trim();
|
||||||
|
if expression.is_empty() {
|
||||||
|
bail!("expression must not be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = eval(expression)?;
|
||||||
|
Ok(match result {
|
||||||
|
evalexpr::Value::Int(value) => serde_json::json!(value),
|
||||||
|
evalexpr::Value::Float(value) => serde_json::json!(value),
|
||||||
|
evalexpr::Value::Boolean(value) => serde_json::json!(value),
|
||||||
|
evalexpr::Value::String(value) => serde_json::json!(value),
|
||||||
|
other => serde_json::json!(other.to_string()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn convert_units(payload: ConvertUnitsPayload) -> Result<Value> {
|
||||||
|
let from = normalize_unit(&payload.from);
|
||||||
|
let to = normalize_unit(&payload.to);
|
||||||
|
let meters = to_meters(payload.value, &from)?;
|
||||||
|
let converted = from_meters(meters, &to)?;
|
||||||
|
Ok(serde_json::json!({
|
||||||
|
"value": converted,
|
||||||
|
"from": payload.from,
|
||||||
|
"to": payload.to
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn random_number(payload: RandomNumberPayload) -> Result<i64> {
|
||||||
|
if payload.min > payload.max {
|
||||||
|
bail!("min must be less than or equal to max");
|
||||||
|
}
|
||||||
|
Ok(rand::thread_rng().gen_range(payload.min..=payload.max))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_unit(unit: &str) -> String {
|
||||||
|
unit.trim().to_ascii_lowercase()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_meters(value: f64, unit: &str) -> Result<f64> {
|
||||||
|
match unit {
|
||||||
|
"m" | "meter" | "meters" => Ok(value),
|
||||||
|
"km" | "kilometer" | "kilometers" => Ok(value * 1000.0),
|
||||||
|
"mi" | "mile" | "miles" => Ok(value * 1609.344),
|
||||||
|
"ft" | "foot" | "feet" => Ok(value * 0.3048),
|
||||||
|
"in" | "inch" | "inches" => Ok(value * 0.0254),
|
||||||
|
"cm" | "centimeter" | "centimeters" => Ok(value * 0.01),
|
||||||
|
"mm" | "millimeter" | "millimeters" => Ok(value * 0.001),
|
||||||
|
other => bail!("unsupported source unit `{other}`"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_meters(value: f64, unit: &str) -> Result<f64> {
|
||||||
|
match unit {
|
||||||
|
"m" | "meter" | "meters" => Ok(value),
|
||||||
|
"km" | "kilometer" | "kilometers" => Ok(value / 1000.0),
|
||||||
|
"mi" | "mile" | "miles" => Ok(value / 1609.344),
|
||||||
|
"ft" | "foot" | "feet" => Ok(value / 0.3048),
|
||||||
|
"in" | "inch" | "inches" => Ok(value / 0.0254),
|
||||||
|
"cm" | "centimeter" | "centimeters" => Ok(value / 0.01),
|
||||||
|
"mm" | "millimeter" | "millimeters" => Ok(value / 0.001),
|
||||||
|
other => bail!("unsupported target unit `{other}`"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_json(action_id: &str, payload: Value) -> Result<()> {
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
serde_json::to_string(&serde_json::json!({
|
||||||
|
"action_id": action_id,
|
||||||
|
"output": payload
|
||||||
|
}))?
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn evaluates_math_expression() {
|
||||||
|
let result = calculate(CalculatePayload {
|
||||||
|
expression: "2 + 2 * 3".to_string(),
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(result, serde_json::json!(8));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn converts_km_to_miles() {
|
||||||
|
let result = convert_units(ConvertUnitsPayload {
|
||||||
|
value: 100.0,
|
||||||
|
from: "km".to_string(),
|
||||||
|
to: "miles".to_string(),
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
let converted = result["value"].as_f64().unwrap();
|
||||||
|
assert!((converted - 62.137119).abs() < 0.0001);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validates_random_range() {
|
||||||
|
assert!(random_number(RandomNumberPayload { min: 1, max: 3 }).unwrap() >= 1);
|
||||||
|
assert!(random_number(RandomNumberPayload { min: 5, max: 1 }).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1,4 @@
|
|||||||
|
pub mod file_ops;
|
||||||
pub mod logger;
|
pub mod logger;
|
||||||
|
pub mod math;
|
||||||
|
pub mod text;
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
use anyhow::{Result, bail};
|
||||||
|
use md5::Md5;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sha1::Sha1;
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct CountWordsPayload {
|
||||||
|
pub text: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct ToSlugPayload {
|
||||||
|
pub text: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct HashStringPayload {
|
||||||
|
pub text: String,
|
||||||
|
pub algorithm: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct TruncateTextPayload {
|
||||||
|
pub text: String,
|
||||||
|
pub max_chars: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_count_words(payload: CountWordsPayload) -> Result<()> {
|
||||||
|
print_json(
|
||||||
|
"count_words",
|
||||||
|
serde_json::json!({ "word_count": count_words(&payload.text) }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_to_slug(payload: ToSlugPayload) -> Result<()> {
|
||||||
|
print_json(
|
||||||
|
"to_slug",
|
||||||
|
serde_json::json!({ "slug": to_slug(&payload.text) }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_hash_string(payload: HashStringPayload) -> Result<()> {
|
||||||
|
let hash = hash_string(payload)?;
|
||||||
|
print_json("hash_string", serde_json::json!({ "hash": hash }))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_truncate_text(payload: TruncateTextPayload) -> Result<()> {
|
||||||
|
let truncated = truncate_text(payload)?;
|
||||||
|
print_json(
|
||||||
|
"truncate_text",
|
||||||
|
serde_json::json!({ "text": truncated.text, "truncated": truncated.truncated }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn count_words(text: &str) -> usize {
|
||||||
|
text.split_whitespace()
|
||||||
|
.filter(|word| !word.is_empty())
|
||||||
|
.count()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_slug(text: &str) -> String {
|
||||||
|
let mut slug = String::new();
|
||||||
|
let mut last_was_dash = false;
|
||||||
|
|
||||||
|
for ch in text.chars().flat_map(char::to_lowercase) {
|
||||||
|
if ch.is_ascii_alphanumeric() {
|
||||||
|
slug.push(ch);
|
||||||
|
last_was_dash = false;
|
||||||
|
} else if !last_was_dash && !slug.is_empty() {
|
||||||
|
slug.push('-');
|
||||||
|
last_was_dash = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
slug.trim_matches('-').to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hash_string(payload: HashStringPayload) -> Result<String> {
|
||||||
|
match payload.algorithm.trim().to_ascii_lowercase().as_str() {
|
||||||
|
"sha256" => Ok(hex_digest(
|
||||||
|
Sha256::digest(payload.text.as_bytes()).as_slice(),
|
||||||
|
)),
|
||||||
|
"sha1" => Ok(hex_digest(Sha1::digest(payload.text.as_bytes()).as_slice())),
|
||||||
|
"md5" => Ok(hex_digest(Md5::digest(payload.text.as_bytes()).as_slice())),
|
||||||
|
other => bail!("unsupported hash algorithm `{other}`"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct TruncatedText {
|
||||||
|
pub text: String,
|
||||||
|
pub truncated: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn truncate_text(payload: TruncateTextPayload) -> Result<TruncatedText> {
|
||||||
|
if payload.max_chars == 0 {
|
||||||
|
bail!("max_chars must be greater than zero");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut chars = payload.text.chars();
|
||||||
|
let text = chars.by_ref().take(payload.max_chars).collect::<String>();
|
||||||
|
let truncated = chars.next().is_some();
|
||||||
|
Ok(TruncatedText { text, truncated })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hex_digest(bytes: &[u8]) -> String {
|
||||||
|
bytes.iter().map(|byte| format!("{byte:02x}")).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_json(action_id: &str, payload: serde_json::Value) -> Result<()> {
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
serde_json::to_string(&serde_json::json!({
|
||||||
|
"action_id": action_id,
|
||||||
|
"output": payload
|
||||||
|
}))?
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn counts_words() {
|
||||||
|
assert_eq!(count_words("hello world\nfrom codex"), 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn creates_slug() {
|
||||||
|
assert_eq!(to_slug("Hello, World! Rust 2026"), "hello-world-rust-2026");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hashes_supported_algorithms() {
|
||||||
|
assert_eq!(
|
||||||
|
hash_string(HashStringPayload {
|
||||||
|
text: "hello".to_string(),
|
||||||
|
algorithm: "sha256".to_string(),
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
hash_string(HashStringPayload {
|
||||||
|
text: "hello".to_string(),
|
||||||
|
algorithm: "md5".to_string(),
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
"5d41402abc4b2a76b9719d911017c592"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
hash_string(HashStringPayload {
|
||||||
|
text: "hello".to_string(),
|
||||||
|
algorithm: "sha1".to_string(),
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
"aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn truncates_by_chars() {
|
||||||
|
let truncated = truncate_text(TruncateTextPayload {
|
||||||
|
text: "abcdef".to_string(),
|
||||||
|
max_chars: 3,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(truncated.text, "abc");
|
||||||
|
assert!(truncated.truncated);
|
||||||
|
assert!(
|
||||||
|
truncate_text(TruncateTextPayload {
|
||||||
|
text: "abc".to_string(),
|
||||||
|
max_chars: 0,
|
||||||
|
})
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user