Compare commits
462 Commits
v0.4.0
..
ba665528ed
| Author | SHA1 | Date | |
|---|---|---|---|
|
ba665528ed
|
|||
|
1440e23748
|
|||
|
8ff9d84a85
|
|||
|
dc8e831f27
|
|||
|
985ae11fcf
|
|||
|
b758b17dbb
|
|||
|
aef26013cb
|
|||
|
b1fc199a5f
|
|||
|
7e801b80d0
|
|||
|
7cd7abe469
|
|||
|
6a5561edba
|
|||
|
d8a92f4e62
|
|||
|
6330d7dd95
|
|||
|
c63eb0a9f9
|
|||
|
d927a9b99f
|
|||
|
766684615b
|
|||
|
5be12e90dc
|
|||
|
7325ad7b32
|
|||
|
2e224948d4
|
|||
|
fa424bde34
|
|||
|
42c88fa2a3
|
|||
|
84c6f88cf2
|
|||
|
f401c637cc
|
|||
|
3239c5d990
|
|||
|
1dff08893a
|
|||
|
f65ec9e9fe
|
|||
|
3ce5ab4fe7
|
|||
| fd21431c2f | |||
| 61a698f9eb | |||
|
518a39c143
|
|||
|
160ee5d5ae
|
|||
|
e4819ff9db
|
|||
|
ecb3cdfcc2
|
|||
|
364cf29296
|
|||
|
b2fa8ebb71
|
|||
| 48031d592f | |||
|
4c7a0a7a77
|
|||
|
8f960fdbbf
|
|||
|
feefe45ed2
|
|||
| 9958eeee8f | |||
|
aa31db7e07
|
|||
|
8c6dde7d86
|
|||
|
738e29059b
|
|||
|
2eb81c4a8b
|
|||
|
88998d1019
|
|||
|
a1fc099c24
|
|||
|
2fe365bef8
|
|||
|
efb1b7b96b
|
|||
|
d915f9e3c1
|
|||
|
11ebf3c155
|
|||
|
abf5d425fd
|
|||
|
1e3d52482a
|
|||
|
0fb72f8226
|
|||
|
0701c370b4
|
|||
|
0bdaa9441f
|
|||
|
4ba1bd8a24
|
|||
|
b484242e4c
|
|||
|
a756394e30
|
|||
|
0faf7b850d
|
|||
|
eeb9f7083b
|
|||
|
b6a5b340f1
|
|||
|
209257c7b1
|
|||
|
4e88cebe28
|
|||
|
738b600fa6
|
|||
|
f67538e5ab
|
|||
|
18bb3d3440
|
|||
|
04cd3c890b
|
|||
|
ef8f5865e2
|
|||
|
493e9bb2a5
|
|||
|
3eff135349
|
|||
|
7ac753d824
|
|||
|
9add71ff13
|
|||
|
7154c3a652
|
|||
|
36ac924d77
|
|||
|
5e4d3ff011
|
|||
|
fd287b09b0
|
|||
|
07c1f70df3
|
|||
|
8c398b6360
|
|||
|
e43c2e477a
|
|||
|
6078072915
|
|||
|
1902e2d040
|
|||
|
702e6f2f63
|
|||
|
01938a0f28
|
|||
|
5d017fbb48
|
|||
|
a3ed9476ae
|
|||
|
a22faad992
|
|||
|
06fe1f9471
|
|||
|
e2ff2c03f8
|
|||
|
7ca9a19d3b
|
|||
|
f5b69d6b4d
|
|||
|
4f244618ca
|
|||
|
b7a20a000a
|
|||
|
0094be475f
|
|||
|
3e508c9337
|
|||
|
edd3c08247
|
|||
|
bf6b2f718c
|
|||
|
984a073730
|
|||
|
aa4babff56
|
|||
|
7f620d469b
|
|||
|
33782c59a8
|
|||
|
5669830510
|
|||
|
99c6cff068
|
|||
|
9b395a304d
|
|||
|
01912bcef3
|
|||
|
e0b85fc936
|
|||
|
73f6e07e47
|
|||
|
534b9923ae
|
|||
|
c66faa22dc
|
|||
|
cf666eb2c6
|
|||
|
76861508c9
|
|||
|
beebb39050
|
|||
|
3d7ba424f1
|
|||
|
84eb82b355
|
|||
|
1fbdcd66d1
|
|||
|
5b65496684
|
|||
|
ca808b4c08
|
|||
|
84c1753ed5
|
|||
|
c8d9f89d59
|
|||
|
e3531b4dcf
|
|||
|
b939868d28
|
|||
|
7630b3e75c
|
|||
| 3292f8e0a5 | |||
|
cf1c06e632
|
|||
|
49f2932b30
|
|||
|
5fd786dd3d
|
|||
|
f5967c7771
|
|||
|
eee0e86131
|
|||
|
51dfd2a655
|
|||
|
d9cf0c4b08
|
|||
|
b4c65f7a19
|
|||
|
1c0e836a92
|
|||
|
2da196c091
|
|||
|
69648afe27
|
|||
|
454f5c03f3
|
|||
|
406642723e
|
|||
|
2469b713c7
|
|||
|
b6ad7a575d
|
|||
|
f3b410d146
|
|||
|
095d0f3d8a
|
|||
|
5f445e046f
|
|||
|
96ab2bdc1b
|
|||
|
cb175e3b51
|
|||
|
7965b970d9
|
|||
|
0a21f10b04
|
|||
|
49aa9fad41
|
|||
|
8f7d3bd13c
|
|||
|
f7fb249d43
|
|||
|
d9498ffb21
|
|||
|
0177fa6906
|
|||
|
c3f6cb8f46
|
|||
|
7facdce6b6
|
|||
|
c11eb352fe
|
|||
|
0e427dc4ba
|
|||
|
f1914f6bd4
|
|||
|
dba6304f51
|
|||
|
e40a8bba72
|
|||
|
c057249e52
|
|||
|
d906713d7d
|
|||
| ff3419a714 | |||
|
a5899da4fb
|
|||
|
dedcef8ac5
|
|||
|
d658f1d2fe
|
|||
|
6b4a45874f
|
|||
|
7839e1dbd9
|
|||
|
78c3932f36
|
|||
|
11334149b0
|
|||
|
4caa035528
|
|||
|
f30e81af08
|
|||
|
4c75655f58
|
|||
|
f865892c28
|
|||
|
ebeb9c9b7d
|
|||
|
ab2b927fcb
|
|||
|
7e5ff2ba1f
|
|||
|
ed59051f3d
|
|||
| e98bf56a2b | |||
| fb510b1a4f | |||
|
6c17462040
|
|||
|
1536cf384c
|
|||
|
d6842d7e29
|
|||
|
fbc0acda2a
|
|||
|
0327d041b6
|
|||
|
6a01fd4fbd
|
|||
| d822180205 | |||
|
89d0fdce26
|
|||
|
b3ecdce979
|
|||
|
3873821a31
|
|||
|
9c2801b643
|
|||
|
d78820dcd4
|
|||
|
d43c4232a2
|
|||
|
f41c85b703
|
|||
|
9e056bdcf0
|
|||
|
d6022b9f98
|
|||
|
6fc1abf94a
|
|||
|
92ea0f624e
|
|||
|
c3fd8fbc1c
|
|||
|
7fd3f7761c
|
|||
|
05e19098b2
|
|||
|
60067ae757
|
|||
|
c72003b0b6
|
|||
|
7c9d500116
|
|||
|
6b2c87b562
|
|||
|
b2dbdfb4b1
|
|||
|
063e198f96
|
|||
|
73cbe16ec1
|
|||
|
bdea854a9f
|
|||
|
9b4c800597
|
|||
|
eb4d1c02f4
|
|||
|
c428990900
|
|||
|
03b9cc70b9
|
|||
|
3fa0eb832c
|
|||
|
83f66e1061
|
|||
|
741b9c364c
|
|||
|
b6f6f456db
|
|||
|
00a6cf74d7
|
|||
|
d35ca352ca
|
|||
| 57dc1cb252 | |||
|
101a9cdd6e
|
|||
|
c5f52e1efb
|
|||
|
470149b606
|
|||
|
02062c5a50
|
|||
|
e6e99b6926
|
|||
|
15a293204f
|
|||
|
ecf3780aed
|
|||
|
e798747135
|
|||
|
60493728a0
|
|||
|
25d6370b20
|
|||
|
d67f845af5
|
|||
|
920a14cabe
|
|||
|
58bdd2e584
|
|||
|
ce6f53ad05
|
|||
|
96f8007d53
|
|||
|
32a55652fe
|
|||
|
2b92e6c98b
|
|||
|
cfa654bcd8
|
|||
|
d0f5ae39e2
|
|||
|
2bb8cf5f73
|
|||
|
fbac446859
|
|||
|
f91cf2e346
|
|||
|
b6b33ab7e3
|
|||
|
c1902a69d1
|
|||
|
812a8e101c
|
|||
|
655ee2a599
|
|||
|
128a8f9a9c
|
|||
|
b1be9443e7
|
|||
|
7b12c69ebf
|
|||
|
69ad584137
|
|||
|
313058e70a
|
|||
|
ea96d9ba3d
|
|||
|
7884adc7c1
|
|||
|
948466d771
|
|||
|
3894c98b5b
|
|||
|
5e9c31595e
|
|||
|
39d9b25e47
|
|||
|
b86f76ddb9
|
|||
|
7f267a10a1
|
|||
|
cdafdff281
|
|||
|
60ad83d6d9
|
|||
|
44c03ccf4f
|
|||
|
af933bbb29
|
|||
|
1f127ee990
|
|||
|
88a9a7709f
|
|||
| e8d92d1b01 | |||
| ddbfd03e75 | |||
|
d1c7f09015
|
|||
|
d2f8f995f0
|
|||
|
5ef9a397ca
|
|||
|
325ab1f45e
|
|||
|
4cfaa2dc77
|
|||
|
6abe2c5536
|
|||
|
03cfd59962
|
|||
|
4d7d5e5e53
|
|||
|
3779b940ae
|
|||
|
d2e541c5c0
|
|||
|
621c90427c
|
|||
|
486001ee85
|
|||
|
c7a2ec084f
|
|||
|
d4e0d48198
|
|||
|
07f23bab5e
|
|||
|
b11797ea1c
|
|||
|
70c2d411ae
|
|||
|
f82c9aff40
|
|||
|
a935add2a7
|
|||
|
8a37a88ffd
|
|||
|
8f66cac680
|
|||
|
0a40ddd2e4
|
|||
|
d5e0728532
|
|||
|
25c0885dcc
|
|||
|
f56ed7d005
|
|||
|
d79e4b9dff
|
|||
|
cdd829199f
|
|||
|
e3c644b8ca
|
|||
|
5cb8070da1
|
|||
|
66801b5d07
|
|||
|
f2de196e22
|
|||
|
2eba530895
|
|||
| 3baa3102a3 | |||
| 2d4fad596c | |||
| 7259e59d2a | |||
| cec04c4597 | |||
| a7f5677195 | |||
| 6075f0a190 | |||
| 15310a9e2c | |||
| f7df54f2f7 | |||
| 212d4bace4 | |||
| f4b3267c89 | |||
| 9eeeb11871 | |||
| b8db3f689d | |||
| 3b21ce2aa5 | |||
| 9bf4fcd943 | |||
| c1f5cfbbda | |||
| 46517a4e15 | |||
| efbe76e1fc | |||
| 245c567d30 | |||
| cbb3d2c34a | |||
| bddec85fa5 | |||
| 96acbc6bf0 | |||
| 0735a31190 | |||
| 986c64ff13 | |||
| 831426d418 | |||
| b99e3fc030 | |||
| 012734f70a | |||
| f591a9635e | |||
| 7c099bf589 | |||
| 32d3cee907 | |||
| 86539c4bb8 | |||
| 14549afd52 | |||
| 667c843fc0 | |||
| 680a52982c | |||
| 52efb1a775 | |||
| c88931d318 | |||
| 2183ed62d1 | |||
| cc8bd040b9 | |||
| a2a464151f | |||
| c9a3f247e7 | |||
| d167502b7b | |||
| 0d9927bb99 | |||
| c9858ce615 | |||
| cccaa1dbe7 | |||
| acd951e981 | |||
| 10d80d58fd | |||
| f196c375d6 | |||
| cc62c89b05 | |||
| 3266cdeb08 | |||
| 6605c62015 | |||
| 704fdbd145 | |||
| 93e76a65a1 | |||
| b3ca7ebddb | |||
| 091fc0b7b7 | |||
| 874f5ba08e | |||
| 5fdfe94b88 | |||
| c02b168749 | |||
| 6ababd919d | |||
| 86b2b2d772 | |||
| 2aa2c3ccee | |||
| 70645a8431 | |||
| ca4b2f2637 | |||
| 7fce8f9b23 | |||
| e5b3b332f6 | |||
| 3e59762443 | |||
| 2ea8a48f28 | |||
| 3c07471620 | |||
| 23e2c1144f | |||
| 313f5e2dda | |||
| 26c35e55d8 | |||
| 878adc0eb7 | |||
| d353767b2c | |||
| 33baeaa62d | |||
| 591b7a5bf1 | |||
| 0bc993532b | |||
| 09379e7231 | |||
| 1a45ce9dc1 | |||
| 95df054dfb | |||
| 5b49553c6d | |||
| 6508940d11 | |||
| 71d89eaaba | |||
| 9619b7908f | |||
| 304129d793 | |||
| 5df435c21a | |||
| 2719c7320a | |||
| a84bae189c | |||
| d82c7c2535 | |||
| 2bc832ed95 | |||
| b5a0f0635b | |||
| 7426aa4bcb | |||
| ba9649382e | |||
| 9c64e97d8b | |||
| 4b1cd3cf44 | |||
| 4a0f002503 | |||
| c4f8c6e102 | |||
| 421308423f | |||
| 0550de2093 | |||
| dddf72e1da | |||
| e23820adf2 | |||
| fea4411aa6 | |||
| b814a38c59 | |||
| 1a3476e4fb | |||
| ecd4d6587c | |||
| 0938119e99 | |||
| 9f15f01871 | |||
| f09cbd2b32 | |||
| 77c1a06277 | |||
| 600f5d1484 | |||
| 7f71317acd | |||
| 865ef5827b | |||
| e5d5bf6c53 | |||
| 7b08d1ef96 | |||
| 9d363b38c7 | |||
| 2f3586cbbf | |||
| 843abe0621 | |||
| 474c5bc76f | |||
| b49a27f886 | |||
| 6f77b3f46e | |||
| a835012673 | |||
| 3f1e8003f8 | |||
| 8475707e75 | |||
| 8a240b1c3f | |||
| 59a3e3012b | |||
| c13142f971 | |||
| a468ee1154 | |||
| 1b504e211a | |||
| 29536f6291 | |||
| 4ef483126d | |||
| 8d2961f3ee | |||
| f1146bb2b9 | |||
| 2daa014c99 | |||
| ebe642f44a | |||
| 25ad254e84 | |||
| 947a7871c2 | |||
| 6421a677eb | |||
| 950893f4a2 | |||
| a10948614d | |||
| 39fc863e22 | |||
| df8b326d89 | |||
| 591f204b67 | |||
| 316ebd6d25 | |||
| 4e707ae08e | |||
| 1ef554c759 | |||
| 367e7d90fd | |||
| 6e7a89763c | |||
| 9dd3836802 | |||
| f822546971 | |||
| 4bf338f91a | |||
| 16577ddc5e | |||
| 384ae73c80 | |||
| d4c932b8ac | |||
| 743e42d4f8 | |||
| 6be2651106 | |||
| 2a2d20a25c | |||
| 882942385b | |||
| 0aa908c8d3 | |||
| 4c179c9269 | |||
| a4fe91ffda | |||
| dc500207ef | |||
| c1e3c3699b | |||
| 52e9f5fc70 | |||
| c85cddb5b4 | |||
| 477b53124d | |||
| 650dbd92e0 | |||
| 88288a98b6 | |||
| 377ab91af7 | |||
| acfc7685f4 | |||
| 5636010e1e |
@@ -21,25 +21,25 @@ body:
|
|||||||
value: |
|
value: |
|
||||||
I tried this:
|
I tried this:
|
||||||
|
|
||||||
1. `loki`
|
1. `coyote`
|
||||||
|
|
||||||
I expected this to happen:
|
I expected this to happen:
|
||||||
|
|
||||||
Instead, this happened:
|
Instead, this happened:
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: loki-log
|
id: coyote-log
|
||||||
attributes:
|
attributes:
|
||||||
label: Loki log
|
label: Coyote log
|
||||||
description: Include the Loki log file to help diagnose the issue. (`loki --info` to see the log_path)
|
description: Include the Coyote log file to help diagnose the issue. (`coyote --info` to see the log_path)
|
||||||
value: |
|
value: |
|
||||||
| OS | Log file location |
|
| OS | Log file location |
|
||||||
| ------- | ----------------------------------------------------- |
|
| ------- | ----------------------------------------------------- |
|
||||||
| Linux | `~/.cache/loki/loki.log` |
|
| Linux | `~/.cache/coyote/coyote.log` |
|
||||||
| Mac | `~/Library/Logs/loki/loki.log` |
|
| Mac | `~/Library/Logs/coyote/coyote.log` |
|
||||||
| Windows | `C:\Users\<User>\AppData\Local\loki\loki.log` |
|
| Windows | `C:\Users\<User>\AppData\Local\coyote\coyote.log` |
|
||||||
|
|
||||||
```
|
```
|
||||||
please provide a copy of your loki log file here if possible; you may need to redact some of the lines
|
please provide a copy of your coyote log file here if possible; you may need to redact some of the lines
|
||||||
```
|
```
|
||||||
|
|
||||||
- type: input
|
- type: input
|
||||||
@@ -57,13 +57,13 @@ body:
|
|||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: input
|
- type: input
|
||||||
id: loki-version
|
id: coyote-version
|
||||||
attributes:
|
attributes:
|
||||||
label: Loki Version
|
label: Coyote Version
|
||||||
description: >
|
description: >
|
||||||
Loki version (`loki --version` if using a release, `git describe` if building
|
Coyote version (`coyote --version` if using a release, `git describe` if building
|
||||||
from main).
|
from main).
|
||||||
**Make sure that you are using the [latest loki release](https://github.com/Dark-Alex-17/loki/releases) or a newer main build**
|
**Make sure that you are using the [latest coyote release](https://github.com/Dark-Alex-17/coyote/releases) or a newer main build**
|
||||||
placeholder: "loki 0.1.0"
|
placeholder: "coyote 0.1.0"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -98,9 +98,9 @@ jobs:
|
|||||||
# Ignore Act's local artifact dir noise
|
# Ignore Act's local artifact dir noise
|
||||||
echo artifacts/ >> .git/info/exclude || true
|
echo artifacts/ >> .git/info/exclude || true
|
||||||
|
|
||||||
# Edit the version line right after name="loki"
|
# Edit the version line right after name="coyote"
|
||||||
sed -E -i '
|
sed -E -i '
|
||||||
/^[[:space:]]*name[[:space:]]*=[[:space:]]*"loki"[[:space:]]*$/ {
|
/^[[:space:]]*name[[:space:]]*=[[:space:]]*"coyote"[[:space:]]*$/ {
|
||||||
n
|
n
|
||||||
s|^[[:space:]]*version[[:space:]]*=[[:space:]]*"[^"]*"|version = "'"$VERSION"'"|
|
s|^[[:space:]]*version[[:space:]]*=[[:space:]]*"[^"]*"|version = "'"$VERSION"'"|
|
||||||
}
|
}
|
||||||
@@ -278,7 +278,7 @@ jobs:
|
|||||||
- name: Verify file
|
- name: Verify file
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
file target/${{ matrix.target }}/release/loki
|
file target/${{ matrix.target }}/release/coyote
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
if: matrix.target != 'aarch64-apple-darwin' && matrix.target != 'aarch64-pc-windows-msvc'
|
if: matrix.target != 'aarch64-apple-darwin' && matrix.target != 'aarch64-pc-windows-msvc'
|
||||||
@@ -382,11 +382,11 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
# Set environment variables
|
# Set environment variables
|
||||||
macos_sha="$(cat ./artifacts/loki-x86_64-apple-darwin.sha256 | awk '{print $1}')"
|
macos_sha="$(cat ./artifacts/coyote-x86_64-apple-darwin.sha256 | awk '{print $1}')"
|
||||||
echo "MACOS_SHA=$macos_sha" >> $GITHUB_ENV
|
echo "MACOS_SHA=$macos_sha" >> $GITHUB_ENV
|
||||||
macos_sha_arm="$(cat ./artifacts/loki-aarch64-apple-darwin.sha256 | awk '{print $1}')"
|
macos_sha_arm="$(cat ./artifacts/coyote-aarch64-apple-darwin.sha256 | awk '{print $1}')"
|
||||||
echo "MACOS_SHA_ARM=$macos_sha_arm" >> $GITHUB_ENV
|
echo "MACOS_SHA_ARM=$macos_sha_arm" >> $GITHUB_ENV
|
||||||
linux_sha="$(cat ./artifacts/loki-x86_64-unknown-linux-musl.sha256 | awk '{print $1}')"
|
linux_sha="$(cat ./artifacts/coyote-x86_64-unknown-linux-musl.sha256 | awk '{print $1}')"
|
||||||
echo "LINUX_SHA=$linux_sha" >> $GITHUB_ENV
|
echo "LINUX_SHA=$linux_sha" >> $GITHUB_ENV
|
||||||
release_version="$(cat ./artifacts/release-version)"
|
release_version="$(cat ./artifacts/release-version)"
|
||||||
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
||||||
@@ -402,23 +402,23 @@ jobs:
|
|||||||
if: env.ACT != 'true'
|
if: env.ACT != 'true'
|
||||||
run: |
|
run: |
|
||||||
# run packaging script
|
# run packaging script
|
||||||
python "./deployment/homebrew/packager.py" ${{ env.RELEASE_VERSION }} "./deployment/homebrew/loki.rb.template" "./loki.rb" ${{ env.MACOS_SHA }} ${{ env.MACOS_SHA_ARM }} ${{ env.LINUX_SHA }}
|
python "./deployment/homebrew/packager.py" ${{ env.RELEASE_VERSION }} "./deployment/homebrew/coyote.rb.template" "./coyote.rb" ${{ env.MACOS_SHA }} ${{ env.MACOS_SHA_ARM }} ${{ env.LINUX_SHA }}
|
||||||
|
|
||||||
- name: Push changes to Homebrew tap
|
- name: Push changes to Homebrew tap
|
||||||
if: env.ACT != 'true'
|
if: env.ACT != 'true'
|
||||||
env:
|
env:
|
||||||
TOKEN: ${{ secrets.LOKI_GITHUB_TOKEN }}
|
TOKEN: ${{ secrets.COYOTE_GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
# push to Git
|
# push to Git
|
||||||
git config --global user.name "Dark-Alex-17"
|
git config --global user.name "Dark-Alex-17"
|
||||||
git config --global user.email "alex.j.tusa@gmail.com"
|
git config --global user.email "alex.j.tusa@gmail.com"
|
||||||
git clone https://Dark-Alex-17:${{ secrets.LOKI_GITHUB_TOKEN }}@github.com/Dark-Alex-17/homebrew-loki.git
|
git clone https://Dark-Alex-17:${{ secrets.COYOTE_GITHUB_TOKEN }}@github.com/Dark-Alex-17/homebrew-coyote.git
|
||||||
rm homebrew-loki/Formula/loki.rb
|
rm homebrew-coyote/Formula/coyote.rb
|
||||||
cp loki.rb homebrew-loki/Formula
|
cp coyote.rb homebrew-coyote/Formula
|
||||||
cd homebrew-loki
|
cd homebrew-coyote
|
||||||
git add .
|
git add .
|
||||||
git diff-index --quiet HEAD || git commit -am "Update formula for Loki release ${{ env.RELEASE_VERSION }}"
|
git diff-index --quiet HEAD || git commit -am "Update formula for Coyote release ${{ env.RELEASE_VERSION }}"
|
||||||
git push https://$TOKEN@github.com/Dark-Alex-17/homebrew-loki.git
|
git push https://$TOKEN@github.com/Dark-Alex-17/homebrew-coyote.git
|
||||||
|
|
||||||
publish-crate:
|
publish-crate:
|
||||||
needs: publish-github-release
|
needs: publish-github-release
|
||||||
|
|||||||
+1
-1
@@ -3,5 +3,5 @@
|
|||||||
/.env
|
/.env
|
||||||
!cli/**
|
!cli/**
|
||||||
.idea/
|
.idea/
|
||||||
/loki.iml
|
/coyote.iml
|
||||||
/.idea/
|
/.idea/
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
{"type":"rust","build":"cargo build","test":"cargo test","check":"cargo check","_detected_by":"heuristic","_cached_at":"2026-04-13T13:36:33-06:00"}
|
|
||||||
+20
-8
@@ -1,3 +1,15 @@
|
|||||||
|
## v0.5.0 (2026-05-27)
|
||||||
|
|
||||||
|
### Feat
|
||||||
|
|
||||||
|
- rename Loki to Coyote
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- bash-based user interactions in agents accidentally regressed in graph implementation
|
||||||
|
- Claude function calling in agent contexts
|
||||||
|
- Claude code rate limit error per new Claude changes
|
||||||
|
|
||||||
## v0.4.0 (2026-05-23)
|
## v0.4.0 (2026-05-23)
|
||||||
|
|
||||||
### Feat
|
### Feat
|
||||||
@@ -5,7 +17,7 @@
|
|||||||
- LLM node failures propgate up
|
- LLM node failures propgate up
|
||||||
- Added .install remote tab completions to the REPL
|
- Added .install remote tab completions to the REPL
|
||||||
- feature complete install remote with category selection
|
- feature complete install remote with category selection
|
||||||
- Support to interactively add secrets to Loki that are missing from MCP configs when merging
|
- Support to interactively add secrets to Coyote that are missing from MCP configs when merging
|
||||||
- Added MCP config merging support for remote asset installations
|
- Added MCP config merging support for remote asset installations
|
||||||
- install remote now writes files to disk
|
- install remote now writes files to disk
|
||||||
- Created basic install_remote functions
|
- Created basic install_remote functions
|
||||||
@@ -25,7 +37,7 @@
|
|||||||
- validation support for parallel graph execution; restricted map nodes to only run for nodes without next targets and not supporting chained map nodes
|
- validation support for parallel graph execution; restricted map nodes to only run for nodes without next targets and not supporting chained map nodes
|
||||||
- created the staging area for state merges per super-step and created the built-in reducers (and their application) for the state merge phase of a super step
|
- created the staging area for state merges per super-step and created the built-in reducers (and their application) for the state merge phase of a super step
|
||||||
- scaffolding work for fan-out nodes for parallel branch execution support and stubbed out Map node types
|
- scaffolding work for fan-out nodes for parallel branch execution support and stubbed out Map node types
|
||||||
- Loki can now update itself via .update and --update commands
|
- Coyote can now update itself via .update and --update commands
|
||||||
- added a .edit command for editing the MCP configuration file
|
- added a .edit command for editing the MCP configuration file
|
||||||
- Created a new .install command to install bundled assets on-demand
|
- Created a new .install command to install bundled assets on-demand
|
||||||
- migrated llm node validation to graph loading time instead of graph runtime
|
- migrated llm node validation to graph loading time instead of graph runtime
|
||||||
@@ -40,7 +52,7 @@
|
|||||||
- wired together graph execution and agent graph dispatch
|
- wired together graph execution and agent graph dispatch
|
||||||
- implemented support for the graph executor
|
- implemented support for the graph executor
|
||||||
- created the approval node executor and the input node executor for user interaction
|
- created the approval node executor and the input node executor for user interaction
|
||||||
- Added initial support for native Loki agent nodes in the graph-based agent system
|
- Added initial support for native Coyote agent nodes in the graph-based agent system
|
||||||
- Added direct script invocation support for graph-based agents
|
- Added direct script invocation support for graph-based agents
|
||||||
- Added graph validation
|
- Added graph validation
|
||||||
- Implemented state management for agent graphs
|
- Implemented state management for agent graphs
|
||||||
@@ -73,7 +85,7 @@
|
|||||||
- check for an existing session before starting up MCP servers when switching to a role
|
- check for an existing session before starting up MCP servers when switching to a role
|
||||||
- do not switch to agent if a session is active.
|
- do not switch to agent if a session is active.
|
||||||
- Do not append todo instructions when function calling is disabled
|
- Do not append todo instructions when function calling is disabled
|
||||||
- a bug in the dynamic completions because the crate name is loki-ai but the binary is named loki
|
- a bug in the dynamic completions because the crate name is coyote-ai but the binary is named coyote
|
||||||
- bug found by copilot that would create a lock on the PollSender for sse-based MCP servers
|
- bug found by copilot that would create a lock on the PollSender for sse-based MCP servers
|
||||||
- Accidental shadow of temp_file function for Windows function calling
|
- Accidental shadow of temp_file function for Windows function calling
|
||||||
- upgraded to newer rmcp version to get native-tls support
|
- upgraded to newer rmcp version to get native-tls support
|
||||||
@@ -120,7 +132,7 @@
|
|||||||
- Created a CodeRabbit-style code-reviewer agent
|
- Created a CodeRabbit-style code-reviewer agent
|
||||||
- Added configuration option in agents to indicate the timeout for user input before proceeding (defaults to 5 minutes)
|
- Added configuration option in agents to indicate the timeout for user input before proceeding (defaults to 5 minutes)
|
||||||
- Added support for sub-agents to escalate user interaction requests from any depth to the parent agents for user interactions
|
- Added support for sub-agents to escalate user interaction requests from any depth to the parent agents for user interactions
|
||||||
- built-in user interaction tools to remove the need for the list/confirm/etc prompts in prompt tools and to enhance user interactions in Loki
|
- built-in user interaction tools to remove the need for the list/confirm/etc prompts in prompt tools and to enhance user interactions in Coyote
|
||||||
- Experimental update to sisyphus to use the new parallel agent spawning system
|
- Experimental update to sisyphus to use the new parallel agent spawning system
|
||||||
- Added an agent configuration property that allows auto-injecting sub-agent spawning instructions (when using the built-in sub-agent spawning system)
|
- Added an agent configuration property that allows auto-injecting sub-agent spawning instructions (when using the built-in sub-agent spawning system)
|
||||||
- Auto-dispatch support of sub-agents and support for the teammate pattern between subagents
|
- Auto-dispatch support of sub-agents and support for the teammate pattern between subagents
|
||||||
@@ -174,7 +186,7 @@
|
|||||||
|
|
||||||
- Simplified sisyphus prompt to improve functionality
|
- Simplified sisyphus prompt to improve functionality
|
||||||
- Supported the injection of RAG sources into the prompt, not just via the `.sources rag` command in the REPL so models can directly reference the documents that supported their responses
|
- Supported the injection of RAG sources into the prompt, not just via the `.sources rag` command in the REPL so models can directly reference the documents that supported their responses
|
||||||
- Created the Sisyphus agent to make Loki function like Claude Code, Gemini, Codex, etc.
|
- Created the Sisyphus agent to make Coyote function like Claude Code, Gemini, Codex, etc.
|
||||||
- Created the Oracle agent to handle high-level architectural decisions and design questions about a given codebase
|
- Created the Oracle agent to handle high-level architectural decisions and design questions about a given codebase
|
||||||
- Updated the coder agent to be much more task-focused and to be delegated to by Sisyphus
|
- Updated the coder agent to be much more task-focused and to be delegated to by Sisyphus
|
||||||
- Created the explore agent for exploring codebases to help answer questions
|
- Created the explore agent for exploring codebases to help answer questions
|
||||||
@@ -234,8 +246,8 @@
|
|||||||
- Support for secret injection into the global config file (API keys, for example)
|
- Support for secret injection into the global config file (API keys, for example)
|
||||||
- Improved MCP handling toggle handling
|
- Improved MCP handling toggle handling
|
||||||
- Secret injection into the MCP configuration
|
- Secret injection into the MCP configuration
|
||||||
- added REPL support for interacting with the Loki vault
|
- added REPL support for interacting with the Coyote vault
|
||||||
- Integrated gman with Loki to create a vault and added flags to configure the Loki vault
|
- Integrated gman with Coyote to create a vault and added flags to configure the Coyote vault
|
||||||
- Added a default session to the jira helper to make interaction more natural
|
- Added a default session to the jira helper to make interaction more natural
|
||||||
- Created the repo-analyzer role
|
- Created the repo-analyzer role
|
||||||
- Created the coder and sql agents
|
- Created the coder and sql agents
|
||||||
|
|||||||
+2
-2
@@ -2,7 +2,7 @@
|
|||||||
Contributors are very welcome! **No contribution is too small and all contributions are valued.**
|
Contributors are very welcome! **No contribution is too small and all contributions are valued.**
|
||||||
|
|
||||||
## Rust
|
## Rust
|
||||||
You'll need to have the stable Rust toolchain installed in order to develop Loki.
|
You'll need to have the stable Rust toolchain installed in order to develop Coyote.
|
||||||
|
|
||||||
The Rust toolchain (stable) can be installed via rustup using the following command:
|
The Rust toolchain (stable) can be installed via rustup using the following command:
|
||||||
|
|
||||||
@@ -84,5 +84,5 @@ Claude, etc.) is not permitted unless explicitly disclosed and approved.
|
|||||||
Submissions must certify that the contributor understands and can maintain the code they submit.
|
Submissions must certify that the contributor understands and can maintain the code they submit.
|
||||||
|
|
||||||
## Questions? Reach out to me!
|
## Questions? Reach out to me!
|
||||||
If you encounter any questions while developing Loki, please don't hesitate to reach out to me at
|
If you encounter any questions while developing Coyote, please don't hesitate to reach out to me at
|
||||||
alex.j.tusa@gmail.com. I'm happy to help contributors in any way I can, regardless of if they're new or experienced!
|
alex.j.tusa@gmail.com. I'm happy to help contributors in any way I can, regardless of if they're new or experienced!
|
||||||
|
|||||||
+6
-6
@@ -1,19 +1,19 @@
|
|||||||
# Credits
|
# Credits
|
||||||
|
|
||||||
## AIChat
|
## AIChat
|
||||||
Loki originally started as a fork of the fantastic
|
Coyote originally started as a fork of the fantastic
|
||||||
[AIChat CLI](https://github.com/sigoden/aichat). The initial goal was simply
|
[AIChat CLI](https://github.com/sigoden/aichat). The initial goal was simply
|
||||||
to fix a bug in how MCP servers worked with AIChat, allowing different MCP
|
to fix a bug in how MCP servers worked with AIChat, allowing different MCP
|
||||||
servers to be specified per agent. Since then, Loki has evolved far beyond
|
servers to be specified per agent. Since then, Coyote has evolved far beyond
|
||||||
its original scope and grown into a passion project with a life of its own.
|
its original scope and grown into a passion project with a life of its own.
|
||||||
|
|
||||||
Today, Loki includes first-class MCP server support (for both local and remote
|
Today, Coyote includes first-class MCP server support (for both local and remote
|
||||||
servers), a built-in vault for interpolating secrets in configuration files,
|
servers), a built-in vault for interpolating secrets in configuration files,
|
||||||
built-in agents and macros, dynamic tab completions, integrated custom
|
built-in agents and macros, dynamic tab completions, integrated custom
|
||||||
functions (no external `argc` dependency), improved documentation, and much
|
functions (no external `argc` dependency), improved documentation, and much
|
||||||
more with many more ideas planned for the future.
|
more with many more ideas planned for the future.
|
||||||
|
|
||||||
Loki is now developed and maintained as an independent project. Full credit
|
Coyote is now developed and maintained as an independent project. Full credit
|
||||||
for the original foundation goes to the developers of the wonderful
|
for the original foundation goes to the developers of the wonderful
|
||||||
AIChat project.
|
AIChat project.
|
||||||
|
|
||||||
@@ -21,10 +21,10 @@ This project is not affiliated with or endorsed by the AIChat maintainers.
|
|||||||
|
|
||||||
## AIChat
|
## AIChat
|
||||||
|
|
||||||
Loki originally began as a fork of [AIChat CLI](https://github.com/sigoden/aichat),
|
Coyote originally began as a fork of [AIChat CLI](https://github.com/sigoden/aichat),
|
||||||
created and maintained by the AIChat contributors.
|
created and maintained by the AIChat contributors.
|
||||||
|
|
||||||
While Loki has since diverged significantly and is now developed as an
|
While Coyote has since diverged significantly and is now developed as an
|
||||||
independent project, its early foundation and inspiration came from the
|
independent project, its early foundation and inspiration came from the
|
||||||
AIChat project.
|
AIChat project.
|
||||||
|
|
||||||
|
|||||||
Generated
+138
-138
@@ -298,7 +298,7 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"fastrand",
|
"fastrand",
|
||||||
"hex",
|
"hex",
|
||||||
"http 1.4.0",
|
"http 1.4.1",
|
||||||
"sha1",
|
"sha1",
|
||||||
"time",
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -371,7 +371,7 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"bytes-utils",
|
"bytes-utils",
|
||||||
"fastrand",
|
"fastrand",
|
||||||
"http 1.4.0",
|
"http 1.4.1",
|
||||||
"http-body 1.0.1",
|
"http-body 1.0.1",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
@@ -398,7 +398,7 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"fastrand",
|
"fastrand",
|
||||||
"http 0.2.12",
|
"http 0.2.12",
|
||||||
"http 1.4.0",
|
"http 1.4.1",
|
||||||
"regex-lite",
|
"regex-lite",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
@@ -422,7 +422,7 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"fastrand",
|
"fastrand",
|
||||||
"http 0.2.12",
|
"http 0.2.12",
|
||||||
"http 1.4.0",
|
"http 1.4.1",
|
||||||
"regex-lite",
|
"regex-lite",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
@@ -446,7 +446,7 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"fastrand",
|
"fastrand",
|
||||||
"http 0.2.12",
|
"http 0.2.12",
|
||||||
"http 1.4.0",
|
"http 1.4.1",
|
||||||
"regex-lite",
|
"regex-lite",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
@@ -471,7 +471,7 @@ dependencies = [
|
|||||||
"aws-types",
|
"aws-types",
|
||||||
"fastrand",
|
"fastrand",
|
||||||
"http 0.2.12",
|
"http 0.2.12",
|
||||||
"http 1.4.0",
|
"http 1.4.1",
|
||||||
"regex-lite",
|
"regex-lite",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
@@ -491,7 +491,7 @@ dependencies = [
|
|||||||
"hex",
|
"hex",
|
||||||
"hmac 0.13.0",
|
"hmac 0.13.0",
|
||||||
"http 0.2.12",
|
"http 0.2.12",
|
||||||
"http 1.4.0",
|
"http 1.4.1",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"sha2 0.11.0",
|
"sha2 0.11.0",
|
||||||
"time",
|
"time",
|
||||||
@@ -532,7 +532,7 @@ dependencies = [
|
|||||||
"bytes-utils",
|
"bytes-utils",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http 1.4.0",
|
"http 1.4.1",
|
||||||
"http-body 1.0.1",
|
"http-body 1.0.1",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
@@ -553,10 +553,10 @@ dependencies = [
|
|||||||
"h2 0.3.27",
|
"h2 0.3.27",
|
||||||
"h2 0.4.14",
|
"h2 0.4.14",
|
||||||
"http 0.2.12",
|
"http 0.2.12",
|
||||||
"http 1.4.0",
|
"http 1.4.1",
|
||||||
"http-body 0.4.6",
|
"http-body 0.4.6",
|
||||||
"hyper 0.14.32",
|
"hyper 0.14.32",
|
||||||
"hyper 1.9.0",
|
"hyper 1.10.0",
|
||||||
"hyper-rustls 0.24.2",
|
"hyper-rustls 0.24.2",
|
||||||
"hyper-rustls 0.27.9",
|
"hyper-rustls 0.27.9",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
@@ -617,7 +617,7 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"fastrand",
|
"fastrand",
|
||||||
"http 0.2.12",
|
"http 0.2.12",
|
||||||
"http 1.4.0",
|
"http 1.4.1",
|
||||||
"http-body 0.4.6",
|
"http-body 0.4.6",
|
||||||
"http-body 1.0.1",
|
"http-body 1.0.1",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
@@ -638,7 +638,7 @@ dependencies = [
|
|||||||
"aws-smithy-types",
|
"aws-smithy-types",
|
||||||
"bytes",
|
"bytes",
|
||||||
"http 0.2.12",
|
"http 0.2.12",
|
||||||
"http 1.4.0",
|
"http 1.4.1",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
@@ -664,7 +664,7 @@ checksum = "7442cb268338f0eb8278140a107c046756aa01093d8ef5e99628d34ae09c94f5"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-smithy-runtime-api",
|
"aws-smithy-runtime-api",
|
||||||
"aws-smithy-types",
|
"aws-smithy-types",
|
||||||
"http 1.4.0",
|
"http 1.4.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -678,7 +678,7 @@ dependencies = [
|
|||||||
"bytes-utils",
|
"bytes-utils",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"http 0.2.12",
|
"http 0.2.12",
|
||||||
"http 1.4.0",
|
"http 1.4.1",
|
||||||
"http-body 0.4.6",
|
"http-body 0.4.6",
|
||||||
"http-body 1.0.1",
|
"http-body 1.0.1",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
@@ -726,7 +726,7 @@ dependencies = [
|
|||||||
"axum-core",
|
"axum-core",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http 1.4.0",
|
"http 1.4.1",
|
||||||
"http-body 1.0.1",
|
"http-body 1.0.1",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"itoa",
|
"itoa",
|
||||||
@@ -750,7 +750,7 @@ checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"http 1.4.0",
|
"http 1.4.1",
|
||||||
"http-body 1.0.1",
|
"http-body 1.0.1",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"mime",
|
"mime",
|
||||||
@@ -1396,6 +1396,83 @@ version = "0.8.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "coyote-ai"
|
||||||
|
version = "0.5.0"
|
||||||
|
dependencies = [
|
||||||
|
"ansi_colours",
|
||||||
|
"anyhow",
|
||||||
|
"arboard",
|
||||||
|
"argc",
|
||||||
|
"async-recursion",
|
||||||
|
"async-trait",
|
||||||
|
"aws-smithy-eventstream",
|
||||||
|
"base64",
|
||||||
|
"bincode 2.0.1",
|
||||||
|
"bitflags",
|
||||||
|
"bm25",
|
||||||
|
"bytes",
|
||||||
|
"chrono",
|
||||||
|
"clap",
|
||||||
|
"clap_complete",
|
||||||
|
"clap_complete_nushell",
|
||||||
|
"colored",
|
||||||
|
"crossterm",
|
||||||
|
"dirs",
|
||||||
|
"duct",
|
||||||
|
"dunce",
|
||||||
|
"eventsource-stream",
|
||||||
|
"fancy-regex",
|
||||||
|
"futures-util",
|
||||||
|
"fuzzy-matcher",
|
||||||
|
"gman",
|
||||||
|
"hmac 0.12.1",
|
||||||
|
"hnsw_rs",
|
||||||
|
"html_to_markdown",
|
||||||
|
"http 1.4.1",
|
||||||
|
"indexmap 2.14.0",
|
||||||
|
"indoc",
|
||||||
|
"inquire",
|
||||||
|
"is-terminal",
|
||||||
|
"json-patch",
|
||||||
|
"log",
|
||||||
|
"log4rs",
|
||||||
|
"nu-ansi-term",
|
||||||
|
"num_cpus",
|
||||||
|
"open",
|
||||||
|
"os_info",
|
||||||
|
"parking_lot",
|
||||||
|
"path-absolutize",
|
||||||
|
"pretty_assertions",
|
||||||
|
"rand 0.10.1",
|
||||||
|
"reedline",
|
||||||
|
"reqwest 0.13.4",
|
||||||
|
"rmcp",
|
||||||
|
"rust-embed",
|
||||||
|
"scraper",
|
||||||
|
"self_update",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_yaml",
|
||||||
|
"serial_test",
|
||||||
|
"sha2 0.10.9",
|
||||||
|
"shell-words",
|
||||||
|
"strum_macros",
|
||||||
|
"syntect",
|
||||||
|
"sys-locale",
|
||||||
|
"terminal-colorsaurus",
|
||||||
|
"textwrap",
|
||||||
|
"tokio",
|
||||||
|
"tree-sitter",
|
||||||
|
"tree-sitter-python",
|
||||||
|
"tree-sitter-typescript",
|
||||||
|
"unicode-width",
|
||||||
|
"url",
|
||||||
|
"urlencoding",
|
||||||
|
"uuid",
|
||||||
|
"which",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cpu-time"
|
name = "cpu-time"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
@@ -1795,9 +1872,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "displaydoc"
|
name = "displaydoc"
|
||||||
version = "0.2.5"
|
version = "0.2.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -2256,7 +2333,7 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
"futures",
|
"futures",
|
||||||
"hyper 1.9.0",
|
"hyper 1.10.0",
|
||||||
"jsonwebtoken",
|
"jsonwebtoken",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"prost",
|
"prost",
|
||||||
@@ -2425,7 +2502,7 @@ dependencies = [
|
|||||||
"fnv",
|
"fnv",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"http 1.4.0",
|
"http 1.4.1",
|
||||||
"indexmap 2.14.0",
|
"indexmap 2.14.0",
|
||||||
"slab",
|
"slab",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -2577,9 +2654,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "1.4.0"
|
version = "1.4.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
|
checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"itoa",
|
"itoa",
|
||||||
@@ -2603,7 +2680,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
|
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"http 1.4.0",
|
"http 1.4.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2614,7 +2691,7 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"http 1.4.0",
|
"http 1.4.1",
|
||||||
"http-body 1.0.1",
|
"http-body 1.0.1",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
@@ -2688,16 +2765,16 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper"
|
name = "hyper"
|
||||||
version = "1.9.0"
|
version = "1.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca"
|
checksum = "eb92f162bf56536459fc83c79b974bb12837acfed43d6bc370a7916d0ae15ecc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atomic-waker",
|
"atomic-waker",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"h2 0.4.14",
|
"h2 0.4.14",
|
||||||
"http 1.4.0",
|
"http 1.4.1",
|
||||||
"http-body 1.0.1",
|
"http-body 1.0.1",
|
||||||
"httparse",
|
"httparse",
|
||||||
"httpdate",
|
"httpdate",
|
||||||
@@ -2729,8 +2806,8 @@ version = "0.27.9"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
|
checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"http 1.4.0",
|
"http 1.4.1",
|
||||||
"hyper 1.9.0",
|
"hyper 1.10.0",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
"rustls 0.23.40",
|
"rustls 0.23.40",
|
||||||
"rustls-native-certs",
|
"rustls-native-certs",
|
||||||
@@ -2745,7 +2822,7 @@ version = "0.5.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0"
|
checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hyper 1.9.0",
|
"hyper 1.10.0",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -2760,7 +2837,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"hyper 1.9.0",
|
"hyper 1.10.0",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
"native-tls",
|
"native-tls",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -2778,9 +2855,9 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http 1.4.0",
|
"http 1.4.1",
|
||||||
"http-body 1.0.1",
|
"http-body 1.0.1",
|
||||||
"hyper 1.9.0",
|
"hyper 1.10.0",
|
||||||
"ipnet",
|
"ipnet",
|
||||||
"libc",
|
"libc",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
@@ -3075,9 +3152,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jiff"
|
name = "jiff"
|
||||||
version = "0.2.24"
|
version = "0.2.27"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d"
|
checksum = "392c70591e8749fe235ddaf513e6f58b26bce3dcc16524cecc8936f75afa161e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"jiff-static",
|
"jiff-static",
|
||||||
"log",
|
"log",
|
||||||
@@ -3088,9 +3165,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jiff-static"
|
name = "jiff-static"
|
||||||
version = "0.2.24"
|
version = "0.2.27"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7"
|
checksum = "47b605b0c050d845fc355bb11eb3f9a8deddc218ea60c76e61aa1f2adfb2c96a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -3237,9 +3314,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libredox"
|
name = "libredox"
|
||||||
version = "0.1.16"
|
version = "0.1.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c"
|
checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
@@ -3273,9 +3350,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.29"
|
version = "0.4.30"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
@@ -3315,83 +3392,6 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "loki-ai"
|
|
||||||
version = "0.4.0"
|
|
||||||
dependencies = [
|
|
||||||
"ansi_colours",
|
|
||||||
"anyhow",
|
|
||||||
"arboard",
|
|
||||||
"argc",
|
|
||||||
"async-recursion",
|
|
||||||
"async-trait",
|
|
||||||
"aws-smithy-eventstream",
|
|
||||||
"base64",
|
|
||||||
"bincode 2.0.1",
|
|
||||||
"bitflags",
|
|
||||||
"bm25",
|
|
||||||
"bytes",
|
|
||||||
"chrono",
|
|
||||||
"clap",
|
|
||||||
"clap_complete",
|
|
||||||
"clap_complete_nushell",
|
|
||||||
"colored",
|
|
||||||
"crossterm",
|
|
||||||
"dirs",
|
|
||||||
"duct",
|
|
||||||
"dunce",
|
|
||||||
"eventsource-stream",
|
|
||||||
"fancy-regex",
|
|
||||||
"futures-util",
|
|
||||||
"fuzzy-matcher",
|
|
||||||
"gman",
|
|
||||||
"hmac 0.12.1",
|
|
||||||
"hnsw_rs",
|
|
||||||
"html_to_markdown",
|
|
||||||
"http 1.4.0",
|
|
||||||
"indexmap 2.14.0",
|
|
||||||
"indoc",
|
|
||||||
"inquire",
|
|
||||||
"is-terminal",
|
|
||||||
"json-patch",
|
|
||||||
"log",
|
|
||||||
"log4rs",
|
|
||||||
"nu-ansi-term",
|
|
||||||
"num_cpus",
|
|
||||||
"open",
|
|
||||||
"os_info",
|
|
||||||
"parking_lot",
|
|
||||||
"path-absolutize",
|
|
||||||
"pretty_assertions",
|
|
||||||
"rand 0.10.1",
|
|
||||||
"reedline",
|
|
||||||
"reqwest 0.13.3",
|
|
||||||
"rmcp",
|
|
||||||
"rust-embed",
|
|
||||||
"scraper",
|
|
||||||
"self_update",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"serde_yaml",
|
|
||||||
"serial_test",
|
|
||||||
"sha2 0.10.9",
|
|
||||||
"shell-words",
|
|
||||||
"strum_macros",
|
|
||||||
"syntect",
|
|
||||||
"sys-locale",
|
|
||||||
"terminal-colorsaurus",
|
|
||||||
"textwrap",
|
|
||||||
"tokio",
|
|
||||||
"tree-sitter",
|
|
||||||
"tree-sitter-python",
|
|
||||||
"tree-sitter-typescript",
|
|
||||||
"unicode-width",
|
|
||||||
"url",
|
|
||||||
"urlencoding",
|
|
||||||
"uuid",
|
|
||||||
"which",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lru-slab"
|
name = "lru-slab"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
@@ -3472,9 +3472,9 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.8.0"
|
version = "2.8.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mime"
|
name = "mime"
|
||||||
@@ -4664,10 +4664,10 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http 1.4.0",
|
"http 1.4.1",
|
||||||
"http-body 1.0.1",
|
"http-body 1.0.1",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"hyper 1.9.0",
|
"hyper 1.10.0",
|
||||||
"hyper-rustls 0.27.9",
|
"hyper-rustls 0.27.9",
|
||||||
"hyper-tls",
|
"hyper-tls",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
@@ -4700,9 +4700,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest"
|
name = "reqwest"
|
||||||
version = "0.13.3"
|
version = "0.13.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0"
|
checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -4710,10 +4710,10 @@ dependencies = [
|
|||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"h2 0.4.14",
|
"h2 0.4.14",
|
||||||
"http 1.4.0",
|
"http 1.4.1",
|
||||||
"http-body 1.0.1",
|
"http-body 1.0.1",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"hyper 1.9.0",
|
"hyper 1.10.0",
|
||||||
"hyper-rustls 0.27.9",
|
"hyper-rustls 0.27.9",
|
||||||
"hyper-tls",
|
"hyper-tls",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
@@ -4778,11 +4778,11 @@ dependencies = [
|
|||||||
"base64",
|
"base64",
|
||||||
"chrono",
|
"chrono",
|
||||||
"futures",
|
"futures",
|
||||||
"http 1.4.0",
|
"http 1.4.1",
|
||||||
"pastey",
|
"pastey",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"process-wrap",
|
"process-wrap",
|
||||||
"reqwest 0.13.3",
|
"reqwest 0.13.4",
|
||||||
"rmcp-macros",
|
"rmcp-macros",
|
||||||
"schemars 1.2.1",
|
"schemars 1.2.1",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -5207,12 +5207,12 @@ checksum = "2e79722b5a505d4ddc77527455a97244e9e8c4c07533ff44cf4421cce7bb6d17"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"either",
|
"either",
|
||||||
"flate2",
|
"flate2",
|
||||||
"http 1.4.0",
|
"http 1.4.1",
|
||||||
"indicatif",
|
"indicatif",
|
||||||
"log",
|
"log",
|
||||||
"quick-xml 0.38.4",
|
"quick-xml 0.38.4",
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest 0.13.3",
|
"reqwest 0.13.4",
|
||||||
"self-replace",
|
"self-replace",
|
||||||
"semver",
|
"semver",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -6126,10 +6126,10 @@ dependencies = [
|
|||||||
"base64",
|
"base64",
|
||||||
"bytes",
|
"bytes",
|
||||||
"h2 0.4.14",
|
"h2 0.4.14",
|
||||||
"http 1.4.0",
|
"http 1.4.1",
|
||||||
"http-body 1.0.1",
|
"http-body 1.0.1",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"hyper 1.9.0",
|
"hyper 1.10.0",
|
||||||
"hyper-timeout",
|
"hyper-timeout",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
@@ -6187,7 +6187,7 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http 1.4.0",
|
"http 1.4.1",
|
||||||
"http-body 1.0.1",
|
"http-body 1.0.1",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
@@ -6473,7 +6473,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c"
|
checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"http 1.4.0",
|
"http 1.4.1",
|
||||||
"httparse",
|
"httparse",
|
||||||
"log",
|
"log",
|
||||||
]
|
]
|
||||||
@@ -7440,18 +7440,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "zerocopy"
|
||||||
version = "0.8.48"
|
version = "0.8.49"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
|
checksum = "bce33a6288fa3f072a8c2c7d0f2fdbb90e28298f0135c1f99b96c3db2efcc60b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zerocopy-derive",
|
"zerocopy-derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy-derive"
|
name = "zerocopy-derive"
|
||||||
version = "0.8.48"
|
version = "0.8.49"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
|
checksum = "8fd425244944f4ab65ccff928e7323354c5a018c75838362fdce749dfad2ee1e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|||||||
+5
-5
@@ -1,12 +1,12 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "loki-ai"
|
name = "coyote-ai"
|
||||||
version = "0.4.0"
|
version = "0.5.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors = ["Alex Clarke <alex.j.tusa@gmail.com>"]
|
authors = ["Alex Clarke <alex.j.tusa@gmail.com>"]
|
||||||
description = "An all-in-one, batteries included LLM CLI Tool"
|
description = "An all-in-one, batteries included LLM CLI Tool"
|
||||||
keywords = ["chatgpt", "llm", "cli", "ai", "repl"]
|
keywords = ["chatgpt", "llm", "cli", "ai", "repl"]
|
||||||
homepage = "https://github.com/Dark-Alex-17/loki"
|
homepage = "https://github.com/Dark-Alex-17/coyote"
|
||||||
repository = "https://github.com/Dark-Alex-17/loki"
|
repository = "https://github.com/Dark-Alex-17/coyote"
|
||||||
categories = ["command-line-utilities"]
|
categories = ["command-line-utilities"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
@@ -138,7 +138,7 @@ pretty_assertions = "1.4.0"
|
|||||||
serial_test = "3"
|
serial_test = "3"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "loki"
|
name = "coyote"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
|||||||
@@ -1,54 +1,55 @@
|
|||||||
# Loki: All-in-one, batteries-included LLM CLI Tool
|
# Coyote: All-in-one, batteries-included LLM CLI Tool
|
||||||
|
|
||||||

|

|
||||||
[](https://crates.io/crates/loki-ai)
|
[](https://crates.io/crates/coyote-ai)
|
||||||

|

|
||||||

|

|
||||||
[](https://github.com/Dark-Alex-17/loki/releases)
|
[](https://github.com/Dark-Alex-17/coyote/releases)
|
||||||
|
|
||||||
Loki is an all-in-one, batteries-included, LLM CLI tool featuring Shell Assistant, CLI & REPL Mode, RAG, AI Tools &
|
Coyote is an all-in-one, batteries-included, LLM CLI tool featuring Shell Assistant, CLI & REPL Mode, RAG, AI Tools &
|
||||||
Agents, and More.
|
Agents, and More.
|
||||||
|
|
||||||
It is designed to include a number of useful agents, roles, macros, and more so users can get up and running with Loki
|
It is designed to include a number of useful agents, roles, macros, and more so users can get up and running with Coyote
|
||||||
in as little time as possible. You can also install entire bundles of agents, roles, macros, tools, and MCP servers from
|
in as little time as possible. You can also install entire bundles of agents, roles, macros, tools, and MCP servers from
|
||||||
any git repository. See [Sharing Configurations](https://github.com/Dark-Alex-17/loki/wiki/Sharing-Configurations) for more information.
|
any git repository. See [Sharing Configurations](https://github.com/Dark-Alex-17/coyote/wiki/Sharing-Configurations) for more information.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Coming from [AIChat](https://github.com/sigoden/aichat)? Follow the [migration guide](https://github.com/Dark-Alex-17/loki/wiki/AIChat-Migration) to get started.
|
Coming from [AIChat](https://github.com/sigoden/aichat)? Follow the [migration guide](https://github.com/Dark-Alex-17/coyote/wiki/AIChat-Migration) to get started.
|
||||||
|
|
||||||
## Quick Links
|
## Quick Links
|
||||||
* [AIChat Migration Guide](https://github.com/Dark-Alex-17/loki/wiki/AIChat-Migration): Coming from AIChat? Follow the migration guide to get started.
|
* [AIChat Migration Guide](https://github.com/Dark-Alex-17/coyote/wiki/AIChat-Migration): Coming from AIChat? Follow the migration guide to get started.
|
||||||
* [Installation](#install): Install Loki
|
* [Installation](#install): Install Coyote
|
||||||
* [Getting Started](#getting-started): Get started with Loki by doing first-run setup steps.
|
* [Getting Started](#getting-started): Get started with Coyote by doing first-run setup steps.
|
||||||
* [Sharing Configurations](https://github.com/Dark-Alex-17/loki/wiki/Sharing-Configurations): Install bundles of agents, roles, macros, tools, and MCP servers from any git repo, and share your own.
|
* [Sharing Configurations](https://github.com/Dark-Alex-17/coyote/wiki/Sharing-Configurations): Install bundles of agents, roles, macros, tools, and MCP servers from any git repo, and share your own.
|
||||||
* [REPL](https://github.com/Dark-Alex-17/loki/wiki/REPL): Interactive Read-Eval-Print Loop for conversational interactions with LLMs and Loki.
|
* [REPL](https://github.com/Dark-Alex-17/coyote/wiki/REPL): Interactive Read-Eval-Print Loop for conversational interactions with LLMs and Coyote.
|
||||||
* [Custom REPL Prompt](https://github.com/Dark-Alex-17/loki/wiki/REPL-Prompt): Customize the REPL prompt to provide useful contextual information.
|
* [Custom REPL Prompt](https://github.com/Dark-Alex-17/coyote/wiki/REPL-Prompt): Customize the REPL prompt to provide useful contextual information.
|
||||||
* [Vault](https://github.com/Dark-Alex-17/loki/wiki/Vault): Securely store and manage sensitive information such as API keys and credentials.
|
* [Vault](https://github.com/Dark-Alex-17/coyote/wiki/Vault): Securely store and manage sensitive information such as API keys and credentials.
|
||||||
* [Shell Integrations](https://github.com/Dark-Alex-17/loki/wiki/Shell-Integrations): Seamlessly integrate Loki with your shell environment for enhanced command-line assistance.
|
* [Shell Integrations](https://github.com/Dark-Alex-17/coyote/wiki/Shell-Integrations): Seamlessly integrate Coyote with your shell environment for enhanced command-line assistance.
|
||||||
* [Function Calling](https://github.com/Dark-Alex-17/loki/wiki/Tools): Leverage function calling capabilities to extend Loki's functionality with custom tools
|
* [Function Calling](https://github.com/Dark-Alex-17/coyote/wiki/Tools): Leverage function calling capabilities to extend Coyote's functionality with custom tools
|
||||||
* [Creating Custom Tools](https://github.com/Dark-Alex-17/loki/wiki/Custom-Tools): You can create your own custom tools to enhance Loki's capabilities.
|
* [Creating Custom Tools](https://github.com/Dark-Alex-17/coyote/wiki/Custom-Tools): You can create your own custom tools to enhance Coyote's capabilities.
|
||||||
* [Create Custom Python Tools](https://github.com/Dark-Alex-17/loki/wiki/Custom-Tools#custom-python-based-tools)
|
* [Create Custom Python Tools](https://github.com/Dark-Alex-17/coyote/wiki/Custom-Tools#custom-python-based-tools)
|
||||||
* [Create Custom TypeScript Tools](https://github.com/Dark-Alex-17/loki/wiki/Custom-Tools#custom-typescript-based-tools)
|
* [Create Custom TypeScript Tools](https://github.com/Dark-Alex-17/coyote/wiki/Custom-Tools#custom-typescript-based-tools)
|
||||||
* [Create Custom Bash Tools](https://github.com/Dark-Alex-17/loki/wiki/Custom-Bash-Tools)
|
* [Create Custom Bash Tools](https://github.com/Dark-Alex-17/coyote/wiki/Custom-Bash-Tools)
|
||||||
* [Bash Prompt Utilities](https://github.com/Dark-Alex-17/loki/wiki/Bash-Prompt-Helpers)
|
* [Bash Prompt Utilities](https://github.com/Dark-Alex-17/coyote/wiki/Bash-Prompt-Helpers)
|
||||||
* [First-Class MCP Server Support](https://github.com/Dark-Alex-17/loki/wiki/MCP-Servers): Easily connect and interact with MCP servers for advanced functionality.
|
* [First-Class MCP Server Support](https://github.com/Dark-Alex-17/coyote/wiki/MCP-Servers): Easily connect and interact with MCP servers for advanced functionality.
|
||||||
* [Macros](https://github.com/Dark-Alex-17/loki/wiki/Macros): Automate repetitive tasks and workflows with Loki "scripts" (macros).
|
* [Macros](https://github.com/Dark-Alex-17/coyote/wiki/Macros): Automate repetitive tasks and workflows with Coyote "scripts" (macros).
|
||||||
* [RAG](https://github.com/Dark-Alex-17/loki/wiki/RAG): Retrieval-Augmented Generation for enhanced information retrieval and generation.
|
* [RAG](https://github.com/Dark-Alex-17/coyote/wiki/RAG): Retrieval-Augmented Generation for enhanced information retrieval and generation.
|
||||||
* [Sessions](https://github.com/Dark-Alex-17/loki/wiki/Sessions): Manage and persist conversational contexts and settings across multiple interactions.
|
* [Sessions](https://github.com/Dark-Alex-17/coyote/wiki/Sessions): Manage and persist conversational contexts and settings across multiple interactions.
|
||||||
* [Roles](https://github.com/Dark-Alex-17/loki/wiki/Roles): Customize model behavior for specific tasks or domains.
|
* [Roles](https://github.com/Dark-Alex-17/coyote/wiki/Roles): Customize model behavior for specific tasks or domains.
|
||||||
* [Agents](https://github.com/Dark-Alex-17/loki/wiki/Agents): Leverage AI agents to perform complex tasks and workflows, including sub-agent spawning, teammate messaging, and user interaction tools.
|
* [Skills](https://github.com/Dark-Alex-17/coyote/wiki/Skills): Modular knowledge or capability packs the LLM can load and unload mid-conversation. Multiple skills compose; instructions stack, tools and MCPs union.
|
||||||
* [Graph Agents](https://github.com/Dark-Alex-17/loki/wiki/Graph-Agents): Define an agent as a declarative, YAML-driven workflow. A directed graph of typed nodes (LLM calls, scripts, approvals, user input, RAG retrieval, sub-agent spawns).
|
* [Agents](https://github.com/Dark-Alex-17/coyote/wiki/Agents): Leverage AI agents to perform complex tasks and workflows, including sub-agent spawning, teammate messaging, and user interaction tools.
|
||||||
* [Todo System](https://github.com/Dark-Alex-17/loki/wiki/TODO-System): Built-in task tracking for improved LLM reliability with smaller models.
|
* [Graph Agents](https://github.com/Dark-Alex-17/coyote/wiki/Graph-Agents): Define an agent as a declarative, YAML-driven workflow. A directed graph of typed nodes (LLM calls, scripts, approvals, user input, RAG retrieval, sub-agent spawns).
|
||||||
* [Environment Variables](https://github.com/Dark-Alex-17/loki/wiki/Environment-Variables): Override and customize your Loki configuration at runtime with environment variables.
|
* [Todo System](https://github.com/Dark-Alex-17/coyote/wiki/TODO-System): Built-in task tracking for improved LLM reliability with smaller models.
|
||||||
* [Client Configurations](https://github.com/Dark-Alex-17/loki/wiki/Clients): Configuration instructions for various LLM providers.
|
* [Environment Variables](https://github.com/Dark-Alex-17/coyote/wiki/Environment-Variables): Override and customize your Coyote configuration at runtime with environment variables.
|
||||||
* [Authentication (API Key & OAuth)](https://github.com/Dark-Alex-17/loki/wiki/Clients#authentication): Authenticate with API keys or OAuth for subscription-based access.
|
* [Client Configurations](https://github.com/Dark-Alex-17/coyote/wiki/Clients): Configuration instructions for various LLM providers.
|
||||||
* [Patching API Requests](https://github.com/Dark-Alex-17/loki/wiki/Patches): Learn how to patch API requests for advanced customization.
|
* [Authentication (API Key & OAuth)](https://github.com/Dark-Alex-17/coyote/wiki/Clients#authentication): Authenticate with API keys or OAuth for subscription-based access.
|
||||||
* [Custom Themes](https://github.com/Dark-Alex-17/loki/wiki/Themes): Change the look and feel of Loki to your preferences with custom themes.
|
* [Patching API Requests](https://github.com/Dark-Alex-17/coyote/wiki/Patches): Learn how to patch API requests for advanced customization.
|
||||||
* [History](#history): A history of how Loki came to be.
|
* [Custom Themes](https://github.com/Dark-Alex-17/coyote/wiki/Themes): Change the look and feel of Coyote to your preferences with custom themes.
|
||||||
|
* [History](#history): A history of how Coyote came to be.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
Loki requires the following tools to be installed on your system:
|
Coyote requires the following tools to be installed on your system:
|
||||||
* [jq](https://github.com/jqlang/jq)
|
* [jq](https://github.com/jqlang/jq)
|
||||||
* `brew install jq`
|
* `brew install jq`
|
||||||
* [usql](https://github.com/xo/usql) (For the `sql` agent)
|
* [usql](https://github.com/xo/usql) (For the `sql` agent)
|
||||||
@@ -57,57 +58,57 @@ Loki requires the following tools to be installed on your system:
|
|||||||
* [uv](https://docs.astral.sh/uv/getting-started/installation/)
|
* [uv](https://docs.astral.sh/uv/getting-started/installation/)
|
||||||
* `curl -LsSf https://astral.sh/uv/install.sh | sh`
|
* `curl -LsSf https://astral.sh/uv/install.sh | sh`
|
||||||
|
|
||||||
These tools are used to provide various functionalities within Loki, such as document processing, JSON manipulation,
|
These tools are used to provide various functionalities within Coyote, such as document processing, JSON manipulation,
|
||||||
etc., and they are used within agents and tools.
|
etc., and they are used within agents and tools.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
### Cargo
|
### Cargo
|
||||||
If you have Cargo installed, then you can install `loki` from Crates.io:
|
If you have Cargo installed, then you can install `coyote` from Crates.io:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
cargo install loki-ai # Binary name is `loki`
|
cargo install coyote-ai # Binary name is `coyote`
|
||||||
|
|
||||||
# If you encounter issues installing, try installing with '--locked'
|
# If you encounter issues installing, try installing with '--locked'
|
||||||
cargo install --locked loki-ai
|
cargo install --locked coyote-ai
|
||||||
```
|
```
|
||||||
|
|
||||||
### Homebrew (Mac/Linux)
|
### Homebrew (Mac/Linux)
|
||||||
To install Loki from Homebrew, install the `loki` tap. Then you'll be able to install `loki`:
|
To install Coyote from Homebrew, install the `coyote` tap. Then you'll be able to install `coyote`:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
brew tap Dark-Alex-17/loki
|
brew tap Dark-Alex-17/coyote
|
||||||
brew install loki
|
brew install coyote
|
||||||
|
|
||||||
# If you need to be more specific, use:
|
# If you need to be more specific, use:
|
||||||
brew install Dark-Alex-17/loki/loki
|
brew install Dark-Alex-17/coyote/coyote
|
||||||
```
|
```
|
||||||
|
|
||||||
To upgrade `loki` using Homebrew:
|
To upgrade `coyote` using Homebrew:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
brew upgrade loki
|
brew upgrade coyote
|
||||||
```
|
```
|
||||||
|
|
||||||
### Scripts
|
### Scripts
|
||||||
#### Linux/MacOS (`bash`)
|
#### Linux/MacOS (`bash`)
|
||||||
You can use the following command to run a bash script that downloads and installs the latest version of `loki` for your
|
You can use the following command to run a bash script that downloads and installs the latest version of `coyote` for your
|
||||||
OS (Linux/MacOS) and architecture (x86_64/arm64):
|
OS (Linux/MacOS) and architecture (x86_64/arm64):
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
curl -fsSL https://raw.githubusercontent.com/Dark-Alex-17/loki/main/install_loki.sh | bash
|
curl -fsSL https://raw.githubusercontent.com/Dark-Alex-17/coyote/main/install_coyote.sh | bash
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Windows/Linux/MacOS (`PowerShell`)
|
#### Windows/Linux/MacOS (`PowerShell`)
|
||||||
You can use the following command to run a PowerShell script that downloads and installs the latest version of `loki`
|
You can use the following command to run a PowerShell script that downloads and installs the latest version of `coyote`
|
||||||
for your OS (Windows/Linux/MacOS) and architecture (x86_64/arm64):
|
for your OS (Windows/Linux/MacOS) and architecture (x86_64/arm64):
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell -NoProfile -ExecutionPolicy Bypass -Command "iwr -useb https://raw.githubusercontent.com/Dark-Alex-17/loki/main/scripts/install_loki.ps1 | iex"
|
powershell -NoProfile -ExecutionPolicy Bypass -Command "iwr -useb https://raw.githubusercontent.com/Dark-Alex-17/coyote/main/scripts/install_coyote.ps1 | iex"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Manual
|
### Manual
|
||||||
Binaries are available on the [releases](https://github.com/Dark-Alex-17/loki/releases) page for the following platforms:
|
Binaries are available on the [releases](https://github.com/Dark-Alex-17/coyote/releases) page for the following platforms:
|
||||||
|
|
||||||
| Platform | Architecture(s) |
|
| Platform | Architecture(s) |
|
||||||
|----------------|-----------------|
|
|----------------|-----------------|
|
||||||
@@ -118,58 +119,58 @@ Binaries are available on the [releases](https://github.com/Dark-Alex-17/loki/re
|
|||||||
#### Windows Instructions
|
#### Windows Instructions
|
||||||
To use a binary from the releases page on Windows, do the following:
|
To use a binary from the releases page on Windows, do the following:
|
||||||
|
|
||||||
1. Download the latest [binary](https://github.com/Dark-Alex-17/loki/releases) for your OS.
|
1. Download the latest [binary](https://github.com/Dark-Alex-17/coyote/releases) for your OS.
|
||||||
2. Use 7-Zip or TarTool to unpack the Tar file.
|
2. Use 7-Zip or TarTool to unpack the Tar file.
|
||||||
3. Run the executable `loki.exe`!
|
3. Run the executable `coyote.exe`!
|
||||||
|
|
||||||
#### Linux/MacOS Instructions
|
#### Linux/MacOS Instructions
|
||||||
To use a binary from the releases page on Linux/MacOS, do the following:
|
To use a binary from the releases page on Linux/MacOS, do the following:
|
||||||
|
|
||||||
1. Download the latest [binary](https://github.com/Dark-Alex-17/loki/releases) for your OS.
|
1. Download the latest [binary](https://github.com/Dark-Alex-17/coyote/releases) for your OS.
|
||||||
2. `cd` to the directory where you downloaded the binary.
|
2. `cd` to the directory where you downloaded the binary.
|
||||||
3. Extract the binary with `tar -C /usr/local/bin -xzf loki-<arch>.tar.gz` (Note: This may require `sudo`)
|
3. Extract the binary with `tar -C /usr/local/bin -xzf coyote-<arch>.tar.gz` (Note: This may require `sudo`)
|
||||||
4. Now you can run `loki`!
|
4. Now you can run `coyote`!
|
||||||
|
|
||||||
## Updating
|
## Updating
|
||||||
Loki can update itself in place to the latest GitHub release. Run `loki --update`
|
Coyote can update itself in place to the latest GitHub release. Run `coyote --update`
|
||||||
for the newest release, or `loki --update v0.4.0` for a specific version:
|
for the newest release, or `coyote --update v0.4.0` for a specific version:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
loki --update
|
coyote --update
|
||||||
loki --update v0.4.0
|
coyote --update v0.4.0
|
||||||
```
|
```
|
||||||
|
|
||||||
The same is available from within the REPL via `.update` and `.update v0.4.0`.
|
The same is available from within the REPL via `.update` and `.update v0.4.0`.
|
||||||
|
|
||||||
If Loki was installed with a package manager, prefer that package manager so its
|
If Coyote was installed with a package manager, prefer that package manager so its
|
||||||
records stay in sync with the binary on disk; i.e. `brew upgrade loki` for Homebrew,
|
records stay in sync with the binary on disk; i.e. `brew upgrade coyote` for Homebrew,
|
||||||
or `cargo install --locked loki-ai` for Cargo.
|
or `cargo install --locked coyote-ai` for Cargo.
|
||||||
|
|
||||||
When Loki detects a package-manager install it prints a warning and asks for
|
When Coyote detects a package-manager install it prints a warning and asks for
|
||||||
confirmation. In a non-interactive shell (no TTY), pass `--force` to update
|
confirmation. In a non-interactive shell (no TTY), pass `--force` to update
|
||||||
anyway:
|
anyway:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
loki --update --force
|
coyote --update --force
|
||||||
```
|
```
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
After installation, you can generate the configuration files and directories by simply running:
|
After installation, you can generate the configuration files and directories by simply running:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
loki --info
|
coyote --info
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, you need to set up the Loki vault by creating a vault password file. Loki will do this for you automatically and
|
Then, you need to set up the Coyote vault by creating a vault password file. Coyote will do this for you automatically and
|
||||||
guide you through the process when you first attempt to access the vault. So, to get started, you can run:
|
guide you through the process when you first attempt to access the vault. So, to get started, you can run:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
loki --list-secrets
|
coyote --list-secrets
|
||||||
```
|
```
|
||||||
|
|
||||||
### Authentication
|
### Authentication
|
||||||
Each client in your configuration needs authentication (with a few exceptions; e.g. ollama). Most clients use an API key
|
Each client in your configuration needs authentication (with a few exceptions; e.g. ollama). Most clients use an API key
|
||||||
(set via `api_key` in the config or through the [vault](https://github.com/Dark-Alex-17/loki/wiki/Vault)). For providers that support OAuth (e.g. Claude Pro/Max
|
(set via `api_key` in the config or through the [vault](https://github.com/Dark-Alex-17/coyote/wiki/Vault)). For providers that support OAuth (e.g. Claude Pro/Max
|
||||||
subscribers, Google Gemini), you can authenticate with your existing subscription instead:
|
subscribers, Google Gemini), you can authenticate with your existing subscription instead:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -181,40 +182,40 @@ clients:
|
|||||||
```
|
```
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
loki --authenticate my-claude-oauth
|
coyote --authenticate my-claude-oauth
|
||||||
# Or via the REPL: .authenticate
|
# Or via the REPL: .authenticate
|
||||||
```
|
```
|
||||||
|
|
||||||
For full details, see the [authentication documentation](https://github.com/Dark-Alex-17/loki/wiki/Clients#authentication).
|
For full details, see the [authentication documentation](https://github.com/Dark-Alex-17/coyote/wiki/Clients#authentication).
|
||||||
|
|
||||||
### Tab-Completions
|
### Tab-Completions
|
||||||
You can also enable tab completions to make using Loki easier. To do so, add the following to your shell profile:
|
You can also enable tab completions to make using Coyote easier. To do so, add the following to your shell profile:
|
||||||
```shell
|
```shell
|
||||||
# Bash
|
# Bash
|
||||||
# (add to: `~/.bashrc`)
|
# (add to: `~/.bashrc`)
|
||||||
source <(COMPLETE=bash loki)
|
source <(COMPLETE=bash coyote)
|
||||||
|
|
||||||
# Zsh
|
# Zsh
|
||||||
# (add to: `~/.zshrc`)
|
# (add to: `~/.zshrc`)
|
||||||
source <(COMPLETE=zsh loki)
|
source <(COMPLETE=zsh coyote)
|
||||||
|
|
||||||
# Fish
|
# Fish
|
||||||
# (add to: `~/.config/fish/config.fish`)
|
# (add to: `~/.config/fish/config.fish`)
|
||||||
source <(COMPLETE=fish loki | psub)
|
source <(COMPLETE=fish coyote | psub)
|
||||||
|
|
||||||
# Elvish
|
# Elvish
|
||||||
# (add to: `~/.elvish/rc.elv`)
|
# (add to: `~/.elvish/rc.elv`)
|
||||||
eval (E:COMPLETE=elvish loki | slurp)
|
eval (E:COMPLETE=elvish coyote | slurp)
|
||||||
|
|
||||||
# PowerShell
|
# PowerShell
|
||||||
# (add to: `$PROFILE`)
|
# (add to: `$PROFILE`)
|
||||||
$env:COMPLETE = "powershell"
|
$env:COMPLETE = "powershell"
|
||||||
loki | Out-String | Invoke-Expression
|
coyote | Out-String | Invoke-Expression
|
||||||
```
|
```
|
||||||
|
|
||||||
### Shell Integration
|
### Shell Integration
|
||||||
You can integrate Loki's Shell Assistant into your shell for enhanced command-line assistance. Add the code in the
|
You can integrate Coyote's Shell Assistant into your shell for enhanced command-line assistance. Add the code in the
|
||||||
corresponding [shell integration script](./scripts/shell-integration) to your shell. Then, you can invoke Loki to convert natural language to
|
corresponding [shell integration script](./scripts/shell-integration) to your shell. Then, you can invoke Coyote to convert natural language to
|
||||||
shell commands by pressing `Alt-e`. For example:
|
shell commands by pressing `Alt-e`. For example:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
@@ -224,18 +225,18 @@ find . -name "*.md"
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
The location of the global Loki configuration varies between systems, so you can use the following command to find your
|
The location of the global Coyote configuration varies between systems, so you can use the following command to find your
|
||||||
`config.yaml` file:
|
`config.yaml` file:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
loki --info | grep 'config_file' | awk '{print $2}'
|
coyote --info | grep 'config_file' | awk '{print $2}'
|
||||||
```
|
```
|
||||||
|
|
||||||
The configuration file consists of a number of settings. To see a full example configuration file with every setting
|
The configuration file consists of a number of settings. To see a full example configuration file with every setting
|
||||||
defined, refer to the [example configuration file](./config.example.yaml).
|
defined, refer to the [example configuration file](./config.example.yaml).
|
||||||
|
|
||||||
### Default LLM
|
### Default LLM
|
||||||
The following settings are available to configure the default LLM that is used when you start Loki, and its
|
The following settings are available to configure the default LLM that is used when you start Coyote, and its
|
||||||
hyperparameters:
|
hyperparameters:
|
||||||
|
|
||||||
| Setting | Description |
|
| Setting | Description |
|
||||||
@@ -245,34 +246,34 @@ hyperparameters:
|
|||||||
| `top_p` | The default `top_p` hyperparameter value to use for all models, with a range of (0,1) (or (0,2) for some models); <br>Used unless explicitly overridden |
|
| `top_p` | The default `top_p` hyperparameter value to use for all models, with a range of (0,1) (or (0,2) for some models); <br>Used unless explicitly overridden |
|
||||||
|
|
||||||
### CLI Behavior
|
### CLI Behavior
|
||||||
You can use the following settings to modify the behavior of Loki:
|
You can use the following settings to modify the behavior of Coyote:
|
||||||
|
|
||||||
| Setting | Default Value | Description |
|
| Setting | Default Value | Description |
|
||||||
|---------------|---------------|-------------------------------------------------------------------------------------------------------------------------------------|
|
|---------------|---------------|-------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `stream` | `true` | Controls whether to use stream-style APIs when querying for completions from LLM providers |
|
| `stream` | `true` | Controls whether to use stream-style APIs when querying for completions from LLM providers |
|
||||||
| `save` | `true` | Controls whether to save each query/response to every model to `messages.md` for posterity; Useful for debugging |
|
| `save` | `true` | Controls whether to save each query/response to every model to `messages.md` for posterity; Useful for debugging |
|
||||||
| `keybindings` | `emacs` | Specifies which keybinding schema to use; can either be `emacs` or `vi` |
|
| `keybindings` | `emacs` | Specifies which keybinding schema to use; can either be `emacs` or `vi` |
|
||||||
| `editor` | `null` | What text editor Loki should use to edit the input buffer or session (e.g. `vim`, `emacs`, `nano`, `hx`); <br>Defaults to `$EDITOR` |
|
| `editor` | `null` | What text editor Coyote should use to edit the input buffer or session (e.g. `vim`, `emacs`, `nano`, `hx`); <br>Defaults to `$EDITOR` |
|
||||||
| `wrap` | `no` | Controls whether text is wrapped (can be `no`, `auto`, or some `<max_width>` |
|
| `wrap` | `no` | Controls whether text is wrapped (can be `no`, `auto`, or some `<max_width>` |
|
||||||
| `wrap_code` | `false` | Enables or disables the wrapping of code blocks |
|
| `wrap_code` | `false` | Enables or disables the wrapping of code blocks |
|
||||||
|
|
||||||
### Preludes
|
### Preludes
|
||||||
Preludes let you define the default behavior for the different operating modes of Loki. The available settings are
|
Preludes let you define the default behavior for the different operating modes of Coyote. The available settings are
|
||||||
shown below:
|
shown below:
|
||||||
|
|
||||||
| Setting | Description |
|
| Setting | Description |
|
||||||
|-----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|-----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `repl_prelude` | This setting lets you specify a default `session` or `role` to use when starting Loki in [REPL](https://github.com/Dark-Alex-17/loki/wiki/REPL) mode. <br>Values can be <ul><li>`role:<name>` to define a role</li><li>`session:<name>` to define a session</li><li>`<session>:<role>` to define both a session and a role to use</li></ul> |
|
| `repl_prelude` | This setting lets you specify a default `session` or `role` to use when starting Coyote in [REPL](https://github.com/Dark-Alex-17/coyote/wiki/REPL) mode. <br>Values can be <ul><li>`role:<name>` to define a role</li><li>`session:<name>` to define a session</li><li>`<session>:<role>` to define both a session and a role to use</li></ul> |
|
||||||
| `cmd_prelude` | This setting lets you specify a default `session` or `role` to use when running one-off queries in Loki via the CLI. <br>Values can be <ul><li>`role:<name>` to define a role</li><li>`session:<name>` to define a session</li><li>`<session>:<role>` to define both a session and a role to use</li></ul> |
|
| `cmd_prelude` | This setting lets you specify a default `session` or `role` to use when running one-off queries in Coyote via the CLI. <br>Values can be <ul><li>`role:<name>` to define a role</li><li>`session:<name>` to define a session</li><li>`<session>:<role>` to define both a session and a role to use</li></ul> |
|
||||||
| `agent_session` | This setting is used to specify a default session that all agents should start into, unless otherwise specified in the agent configuration. (e.g. `temp`, `default`) |
|
| `agent_session` | This setting is used to specify a default session that all agents should start into, unless otherwise specified in the agent configuration. (e.g. `temp`, `default`) |
|
||||||
|
|
||||||
### Appearance
|
### Appearance
|
||||||
The appearance of Loki can be modified using the following settings:
|
The appearance of Coyote can be modified using the following settings:
|
||||||
|
|
||||||
| Setting | Default Value | Description |
|
| Setting | Default Value | Description |
|
||||||
|---------------|---------------|------------------------------------------------------|
|
|---------------|---------------|------------------------------------------------------|
|
||||||
| `highlight` | `true` | This setting enables or disables syntax highlighting |
|
| `highlight` | `true` | This setting enables or disables syntax highlighting |
|
||||||
| `light_theme` | `false` | This setting toggles light mode in Loki |
|
| `light_theme` | `false` | This setting toggles light mode in Coyote |
|
||||||
|
|
||||||
### Miscellaneous Settings
|
### Miscellaneous Settings
|
||||||
| Setting | Default Value | Description |
|
| Setting | Default Value | Description |
|
||||||
@@ -284,7 +285,7 @@ The appearance of Loki can be modified using the following settings:
|
|||||||
|
|
||||||
## History
|
## History
|
||||||
|
|
||||||
Loki began as a fork of [AIChat CLI](https://github.com/sigoden/aichat) and has since evolved into an independent project.
|
Coyote began as a fork of [AIChat CLI](https://github.com/sigoden/aichat) and has since evolved into an independent project.
|
||||||
|
|
||||||
See [CREDITS.md](./CREDITS.md) for full attribution and background.
|
See [CREDITS.md](./CREDITS.md) for full attribution and background.
|
||||||
|
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ set -euo pipefail
|
|||||||
#######################
|
#######################
|
||||||
|
|
||||||
# Cache file name for detected project info
|
# Cache file name for detected project info
|
||||||
_LOKI_PROJECT_CACHE=".loki-project.json"
|
_COYOTE_PROJECT_CACHE=".coyote-project.json"
|
||||||
|
|
||||||
# Read cached project detection if valid
|
# Read cached project detection if valid
|
||||||
# Usage: _read_project_cache "/path/to/project"
|
# Usage: _read_project_cache "/path/to/project"
|
||||||
# Returns: cached JSON on stdout (exit 0) or nothing (exit 1)
|
# Returns: cached JSON on stdout (exit 0) or nothing (exit 1)
|
||||||
_read_project_cache() {
|
_read_project_cache() {
|
||||||
local dir="$1"
|
local dir="$1"
|
||||||
local cache_file="${dir}/${_LOKI_PROJECT_CACHE}"
|
local cache_file="${dir}/${_COYOTE_PROJECT_CACHE}"
|
||||||
|
|
||||||
if [[ -f "${cache_file}" ]]; then
|
if [[ -f "${cache_file}" ]]; then
|
||||||
local cached
|
local cached
|
||||||
@@ -32,7 +32,7 @@ _read_project_cache() {
|
|||||||
_write_project_cache() {
|
_write_project_cache() {
|
||||||
local dir="$1"
|
local dir="$1"
|
||||||
local json="$2"
|
local json="$2"
|
||||||
local cache_file="${dir}/${_LOKI_PROJECT_CACHE}"
|
local cache_file="${dir}/${_COYOTE_PROJECT_CACHE}"
|
||||||
|
|
||||||
echo "${json}" > "${cache_file}" 2>/dev/null || true
|
echo "${json}" > "${cache_file}" 2>/dev/null || true
|
||||||
}
|
}
|
||||||
@@ -238,7 +238,7 @@ _detect_with_llm() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
local llm_response
|
local llm_response
|
||||||
llm_response=$(loki --no-stream "${prompt}" 2>/dev/null) || return 1
|
llm_response=$(coyote --no-stream "${prompt}" 2>/dev/null) || return 1
|
||||||
|
|
||||||
llm_response=$(echo "${llm_response}" | sed 's/^```json//;s/^```//;s/```$//' | tr -d '\n' | sed 's/^[[:space:]]*//')
|
llm_response=$(echo "${llm_response}" | sed 's/^```json//;s/^```//;s/```$//' | tr -d '\n' | sed 's/^[[:space:]]*//')
|
||||||
llm_response=$(echo "${llm_response}" | grep -o '{[^}]*}' | head -1)
|
llm_response=$(echo "${llm_response}" | grep -o '{[^}]*}' | head -1)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ A graph-based implementation agent. Plans, implements, and runs build +
|
|||||||
tests in a bounded fix-loop until verified. Designed to be delegated to by
|
tests in a bounded fix-loop until verified. Designed to be delegated to by
|
||||||
the **[Sisyphus](../sisyphus/README.md)** agent.
|
the **[Sisyphus](../sisyphus/README.md)** agent.
|
||||||
|
|
||||||
Coder is a [graph agent](https://github.com/Dark-Alex-17/loki/wiki/Graph-Agents): its workflow is
|
Coder is a [graph agent](https://github.com/Dark-Alex-17/coyote/wiki/Graph-Agents): its workflow is
|
||||||
defined declaratively in `graph.yaml`, with verification and the
|
defined declaratively in `graph.yaml`, with verification and the
|
||||||
implement-fix loop enforced as graph edges rather than prose.
|
implement-fix loop enforced as graph edges rather than prose.
|
||||||
|
|
||||||
@@ -42,10 +42,10 @@ so it accepts the runtime override flag:
|
|||||||
```sh
|
```sh
|
||||||
# Invoke from inside the project (project_dir defaults to ".")
|
# Invoke from inside the project (project_dir defaults to ".")
|
||||||
cd /path/to/your/project
|
cd /path/to/your/project
|
||||||
loki -a coder "Add a foo() function..."
|
coyote -a coder "Add a foo() function..."
|
||||||
|
|
||||||
# Or invoke from anywhere with an explicit override
|
# Or invoke from anywhere with an explicit override
|
||||||
loki -a coder --agent-variable project_dir /path/to/your/project "Add..."
|
coyote -a coder --agent-variable project_dir /path/to/your/project "Add..."
|
||||||
```
|
```
|
||||||
|
|
||||||
`graph.yaml` `initial_state` exposes:
|
`graph.yaml` `initial_state` exposes:
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ variables:
|
|||||||
- name: project_dir
|
- name: project_dir
|
||||||
description: |
|
description: |
|
||||||
Absolute path to the project directory. Defaults to "." which is the
|
Absolute path to the project directory. Defaults to "." which is the
|
||||||
directory you invoked `loki` from. Override at runtime with
|
directory you invoked `coyote` from. Override at runtime with
|
||||||
`loki -a coder --agent-variable project_dir /abs/path "..."`.
|
`coyote -a coder --agent-variable project_dir /abs/path "..."`.
|
||||||
default: "."
|
default: "."
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
@@ -70,7 +70,7 @@ nodes:
|
|||||||
MUST be absolute. The project root is {{project_dir}}. Prefer paths
|
MUST be absolute. The project root is {{project_dir}}. Prefer paths
|
||||||
like "{{project_dir}}/src/foo.rs" over "src/foo.rs". The implementer
|
like "{{project_dir}}/src/foo.rs" over "src/foo.rs". The implementer
|
||||||
uses these paths directly with fs_write and fs_patch tools, which
|
uses these paths directly with fs_write and fs_patch tools, which
|
||||||
resolve relative paths against the loki invocation directory (NOT
|
resolve relative paths against the coyote invocation directory (NOT
|
||||||
the project dir). Empty arrays are fine if no files in that category.
|
the project dir). Empty arrays are fine if no files in that category.
|
||||||
|
|
||||||
`risks` is a list of short strings. Anything that could derail the
|
`risks` is a list of short strings. Anything that could derail the
|
||||||
@@ -155,7 +155,7 @@ nodes:
|
|||||||
2. Use `fs_write` for new files or full rewrites.
|
2. Use `fs_write` for new files or full rewrites.
|
||||||
3. NEVER output code to chat. Always use tools.
|
3. NEVER output code to chat. Always use tools.
|
||||||
4. ALWAYS pass ABSOLUTE paths to fs_write and fs_patch. Relative
|
4. ALWAYS pass ABSOLUTE paths to fs_write and fs_patch. Relative
|
||||||
paths resolve against the loki invocation directory (not the
|
paths resolve against the coyote invocation directory (not the
|
||||||
project dir), which is rarely what you want. The project root
|
project dir), which is rarely what you want. The project root
|
||||||
is {{project_dir}}.
|
is {{project_dir}}.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# deep-research
|
# deep-research
|
||||||
|
|
||||||
A deep web research agent, built as a Loki graph agent. It plans an
|
A deep web research agent, built as a Coyote graph agent. It plans an
|
||||||
investigation, decomposes it into sub-questions researched in
|
investigation, decomposes it into sub-questions researched in
|
||||||
parallel, grounds the work in a local knowledge corpus, vets the
|
parallel, grounds the work in a local knowledge corpus, vets the
|
||||||
credibility of cited sources, runs a reflexion self-critique loop to
|
credibility of cited sources, runs a reflexion self-critique loop to
|
||||||
@@ -13,12 +13,12 @@ this agent runs a fixed graph: every request goes through the same
|
|||||||
`plan -> parallel research -> vet -> critique -> synthesize -> verify -> approve`
|
`plan -> parallel research -> vet -> critique -> synthesize -> verify -> approve`
|
||||||
pipeline.
|
pipeline.
|
||||||
|
|
||||||
This agent is also the **canonical reference for the Loki graph
|
This agent is also the **canonical reference for the Coyote graph
|
||||||
system**: it exercises every node type (`script`, `llm`, `rag`, `map`,
|
system**: it exercises every node type (`script`, `llm`, `rag`, `map`,
|
||||||
`agent`, `input`, `approval`, `end`) and both static fan-out and
|
`agent`, `input`, `approval`, `end`) and both static fan-out and
|
||||||
dynamic `map` fan-out. If you are learning how to build a graph
|
dynamic `map` fan-out. If you are learning how to build a graph
|
||||||
agent, this is the file to read alongside the
|
agent, this is the file to read alongside the
|
||||||
[Graph-Agents wiki](https://github.com/Dark-Alex-17/loki/wiki/Graph-Agents).
|
[Graph-Agents wiki](https://github.com/Dark-Alex-17/coyote/wiki/Graph-Agents).
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ incorporate_feedback (script) -> research_each_question (the human-feedbac
|
|||||||
### Node-type breakdown
|
### Node-type breakdown
|
||||||
|
|
||||||
| Type | Nodes |
|
| Type | Nodes |
|
||||||
|---|---|
|
|-----------------------------|-----------------------------------------------------------------------------------------------------------------------|
|
||||||
| `script` (Python) | `parse_request`, `bootstrap_research`, `combine_findings`, `reflexion_gate`, `verify_sources`, `incorporate_feedback` |
|
| `script` (Python) | `parse_request`, `bootstrap_research`, `combine_findings`, `reflexion_gate`, `verify_sources`, `incorporate_feedback` |
|
||||||
| `llm` (tools: `[]`) | `plan`, `critique` |
|
| `llm` (tools: `[]`) | `plan`, `critique` |
|
||||||
| `llm` (with tool whitelist) | `research_one_question`, `vet_sources` |
|
| `llm` (with tool whitelist) | `research_one_question`, `vet_sources` |
|
||||||
@@ -62,7 +62,7 @@ incorporate_feedback (script) -> research_each_question (the human-feedbac
|
|||||||
|
|
||||||
## Parallel execution
|
## Parallel execution
|
||||||
|
|
||||||
The graph has two parallel super-steps where Loki's BSP scheduler runs
|
The graph has two parallel super-steps where Coyote's BSP scheduler runs
|
||||||
branches concurrently.
|
branches concurrently.
|
||||||
|
|
||||||
**1. Context loading (`plan` ‖ `knowledge_lookup`)** — after
|
**1. Context loading (`plan` ‖ `knowledge_lookup`)** — after
|
||||||
@@ -96,7 +96,7 @@ PDFs, or text files into `knowledge/` to bias the research toward
|
|||||||
your local context.
|
your local context.
|
||||||
|
|
||||||
The knowledge base is built once, at agent-load time, into
|
The knowledge base is built once, at agent-load time, into
|
||||||
`~/.config/loki/agents/deep-research/knowledge_lookup.yaml`. Because
|
`~/.config/coyote/agents/deep-research/knowledge_lookup.yaml`. Because
|
||||||
the node fully specifies its build config (`embedding_model`,
|
the node fully specifies its build config (`embedding_model`,
|
||||||
`chunk_size`, `chunk_overlap`), the build is non-interactive. Delete
|
`chunk_size`, `chunk_overlap`), the build is non-interactive. Delete
|
||||||
that cached file after adding or changing knowledge to force a
|
that cached file after adding or changing knowledge to force a
|
||||||
@@ -119,13 +119,13 @@ for details.
|
|||||||
|
|
||||||
## Tools and tool scoping
|
## Tools and tool scoping
|
||||||
|
|
||||||
This agent demonstrates Loki's three tool sources and how an `llm`
|
This agent demonstrates Coyote's three tool sources and how an `llm`
|
||||||
node's `tools:` whitelist scopes them per node.
|
node's `tools:` whitelist scopes them per node.
|
||||||
|
|
||||||
The agent's full tool universe, declared in `graph.yaml`:
|
The agent's full tool universe, declared in `graph.yaml`:
|
||||||
|
|
||||||
- **Global tools** (`global_tools`): `web_search_loki`,
|
- **Global tools** (`global_tools`): `web_search_coyote`,
|
||||||
`fetch_url_via_curl`, `search_arxiv` - Loki's built-in tool scripts.
|
`fetch_url_via_curl`, `search_arxiv` - Coyote's built-in tool scripts.
|
||||||
- **MCP server** (`mcp_servers`): `ddg-search` - a DuckDuckGo web
|
- **MCP server** (`mcp_servers`): `ddg-search` - a DuckDuckGo web
|
||||||
search MCP server. Referenced in a whitelist as `mcp:ddg-search`.
|
search MCP server. Referenced in a whitelist as `mcp:ddg-search`.
|
||||||
- **Custom agent tool** (`tools.sh`): `classify_source` - a
|
- **Custom agent tool** (`tools.sh`): `classify_source` - a
|
||||||
@@ -135,9 +135,9 @@ No node receives all of these. Each `llm` node's `tools:` whitelist
|
|||||||
narrows the universe to exactly what that step needs:
|
narrows the universe to exactly what that step needs:
|
||||||
|
|
||||||
| Node | `tools:` whitelist | Draws from |
|
| Node | `tools:` whitelist | Draws from |
|
||||||
|---|---|---|
|
|-------------------------|-----------------------------------------------------------------------------|--------------------------|
|
||||||
| `plan`, `critique` | `[]` | nothing - pure reasoning |
|
| `plan`, `critique` | `[]` | nothing - pure reasoning |
|
||||||
| `research_one_question` | `web_search_loki`, `fetch_url_via_curl`, `search_arxiv`, `mcp:ddg-search` | global tools + MCP |
|
| `research_one_question` | `web_search_coyote`, `fetch_url_via_curl`, `search_arxiv`, `mcp:ddg-search` | global tools + MCP |
|
||||||
| `vet_sources` | `classify_source` | the custom tool only |
|
| `vet_sources` | `classify_source` | the custom tool only |
|
||||||
|
|
||||||
`research_one_question` (each parallel branch of the map) can search
|
`research_one_question` (each parallel branch of the map) can search
|
||||||
@@ -153,21 +153,21 @@ deterministic - exactly the kind of logic a tool should own rather than
|
|||||||
the LLM guessing.
|
the LLM guessing.
|
||||||
|
|
||||||
Web search may require API-key configuration; see the
|
Web search may require API-key configuration; see the
|
||||||
[Tools](https://github.com/Dark-Alex-17/loki/wiki/Tools) docs.
|
[Tools](https://github.com/Dark-Alex-17/coyote/wiki/Tools) docs.
|
||||||
`fetch_url_via_curl`, `search_arxiv`, and `classify_source` work
|
`fetch_url_via_curl`, `search_arxiv`, and `classify_source` work
|
||||||
without a key.
|
without a key.
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
`research_one_question` (each parallel branch of the `map`) uses the
|
`research_one_question` (each parallel branch of the `map`) uses the
|
||||||
`ddg-search` MCP server via `mcp:ddg-search`. It is one of Loki's
|
`ddg-search` MCP server via `mcp:ddg-search`. It is one of Coyote's
|
||||||
default MCP servers; make sure it is registered in
|
default MCP servers; make sure it is registered in
|
||||||
`~/.config/loki/mcp.json` (run `loki --install mcp_config` to restore
|
`~/.config/coyote/mcp.json` (run `coyote --install mcp_config` to restore
|
||||||
the default template if it is missing). If `ddg-search` is unavailable,
|
the default template if it is missing). If `ddg-search` is unavailable,
|
||||||
the branches still have their global web-search tools to fall back on.
|
the branches still have their global web-search tools to fall back on.
|
||||||
|
|
||||||
The `synthesize` node spawns the `report-writer` sub-agent. Both
|
The `synthesize` node spawns the `report-writer` sub-agent. Both
|
||||||
agents ship with `loki agents install`; if you install one manually,
|
agents ship with `coyote agents install`; if you install one manually,
|
||||||
install both so the agent reference resolves.
|
install both so the agent reference resolves.
|
||||||
|
|
||||||
## Reflexion
|
## Reflexion
|
||||||
@@ -205,10 +205,10 @@ backstop: it caps the total visits to any single node.
|
|||||||
## Running
|
## Running
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
loki agents install # ships deep-research
|
coyote agents install # ships deep-research
|
||||||
loki -a deep-research "How does HTTP/3 differ from HTTP/2?"
|
coyote -a deep-research "How does HTTP/3 differ from HTTP/2?"
|
||||||
loki -a deep-research "Recent advances in solid-state batteries"
|
coyote -a deep-research "Recent advances in solid-state batteries"
|
||||||
loki -a deep-research # no prompt -> triggers ask_topic
|
coyote -a deep-research # no prompt -> triggers ask_topic
|
||||||
```
|
```
|
||||||
|
|
||||||
## Anti-hallucination
|
## Anti-hallucination
|
||||||
@@ -240,7 +240,7 @@ loki -a deep-research # no prompt -> triggers ask_topic
|
|||||||
`report-writer` sub-agent.
|
`report-writer` sub-agent.
|
||||||
- **Tool scope.** Narrow the `research_one_question` node's `tools:`
|
- **Tool scope.** Narrow the `research_one_question` node's `tools:`
|
||||||
list to constrain where each branch looks (for example, drop
|
list to constrain where each branch looks (for example, drop
|
||||||
`web_search_loki` and `mcp:ddg-search` to force arXiv-only
|
`web_search_coyote` and `mcp:ddg-search` to force arXiv-only
|
||||||
research).
|
research).
|
||||||
- **Local knowledge.** Drop files into `knowledge/` to bias every
|
- **Local knowledge.** Drop files into `knowledge/` to bias every
|
||||||
research branch toward your local context (see the *Local
|
research branch toward your local context (see the *Local
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ description: |
|
|||||||
approval. A reviewer's free-form feedback at the approval step feeds
|
approval. A reviewer's free-form feedback at the approval step feeds
|
||||||
back into another research pass.
|
back into another research pass.
|
||||||
|
|
||||||
This is the canonical Loki graph-agent reference: it exercises every
|
This is the canonical Coyote graph-agent reference: it exercises every
|
||||||
node type (script, llm, rag, map, agent, input, approval, end) and
|
node type (script, llm, rag, map, agent, input, approval, end) and
|
||||||
both static fan-out and dynamic map fan-out.
|
both static fan-out and dynamic map fan-out.
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ version: "1.0"
|
|||||||
temperature: 0.0
|
temperature: 0.0
|
||||||
|
|
||||||
global_tools:
|
global_tools:
|
||||||
- web_search_loki.sh
|
- web_search_coyote.sh
|
||||||
- fetch_url_via_curl.sh
|
- fetch_url_via_curl.sh
|
||||||
- search_arxiv.sh
|
- search_arxiv.sh
|
||||||
|
|
||||||
@@ -147,7 +147,7 @@ nodes:
|
|||||||
|
|
||||||
{{research_feedback}}
|
{{research_feedback}}
|
||||||
tools:
|
tools:
|
||||||
- web_search_loki
|
- web_search_coyote
|
||||||
- fetch_url_via_curl
|
- fetch_url_via_curl
|
||||||
- search_arxiv
|
- search_arxiv
|
||||||
- mcp:ddg-search
|
- mcp:ddg-search
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ hybrid (vector + keyword) retrieval over every file in this directory.
|
|||||||
Drop your own notes, papers (PDFs), Markdown docs, or text files here
|
Drop your own notes, papers (PDFs), Markdown docs, or text files here
|
||||||
and they will be indexed into a per-agent knowledge base on first run.
|
and they will be indexed into a per-agent knowledge base on first run.
|
||||||
|
|
||||||
Loki supports common file types out of the box: `.md`, `.txt`, `.pdf`,
|
Coyote supports common file types out of the box: `.md`, `.txt`, `.pdf`,
|
||||||
`.html`, and others. Subdirectories are walked recursively.
|
`.html`, and others. Subdirectories are walked recursively.
|
||||||
|
|
||||||
A small starter file (`research-style-notes.md`) ships so the RAG
|
A small starter file (`research-style-notes.md`) ships so the RAG
|
||||||
@@ -17,7 +17,7 @@ To force the knowledge base to rebuild after you add or change files,
|
|||||||
delete the cached index:
|
delete the cached index:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
rm ~/.config/loki/agents/deep-research/knowledge_lookup.yaml
|
rm ~/.config/coyote/agents/deep-research/knowledge_lookup.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
The next run will rebuild from the current contents of this directory.
|
The next run will rebuild from the current contents of this directory.
|
||||||
|
|||||||
@@ -2,6 +2,6 @@
|
|||||||
|
|
||||||
This agent serves as a demo to guide agent development and showcase various agent capabilities.
|
This agent serves as a demo to guide agent development and showcase various agent capabilities.
|
||||||
|
|
||||||
To enable tools, Loki will look for the first `tools.py` or `tools.sh` file it finds in this directory.
|
To enable tools, Coyote will look for the first `tools.py` or `tools.sh` file it finds in this directory.
|
||||||
|
|
||||||
The base configuration using `tools.py`. To switch to using `tools.sh`, rename or remove `tools.py`.
|
The base configuration using `tools.py`. To switch to using `tools.sh`, rename or remove `tools.py`.
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ It can also be used as a standalone tool for understanding codebases and finding
|
|||||||
## Pro-Tip: Use an IDE MCP Server for Improved Performance
|
## Pro-Tip: Use an IDE MCP Server for Improved Performance
|
||||||
Many modern IDEs now include MCP servers that let LLMs perform operations within the IDE itself and use IDE tools. Using
|
Many modern IDEs now include MCP servers that let LLMs perform operations within the IDE itself and use IDE tools. Using
|
||||||
an IDE's MCP server dramatically improves the performance of coding agents. So if you have an IDE, try adding that MCP
|
an IDE's MCP server dramatically improves the performance of coding agents. So if you have an IDE, try adding that MCP
|
||||||
server to your config (see the [MCP Server docs](../../../docs/function-calling/MCP-SERVERS.md) to see how to configure
|
server to your config (see the [MCP Server docs](https://github.com/Dark-Alex-17/loki/wiki/MCP-Servers) to see how to configure
|
||||||
them), and modify the agent definition to look like this:
|
them), and modify the agent definition to look like this:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -31,7 +31,7 @@ global_tools:
|
|||||||
- fs_grep.sh
|
- fs_grep.sh
|
||||||
- fs_glob.sh
|
- fs_glob.sh
|
||||||
- fs_ls.sh
|
- fs_ls.sh
|
||||||
- web_search_loki.sh
|
- web_search_coyote.sh
|
||||||
|
|
||||||
# ...
|
# ...
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ It can also be used as a standalone tool for design reviews and solving difficul
|
|||||||
## Pro-Tip: Use an IDE MCP Server for Improved Performance
|
## Pro-Tip: Use an IDE MCP Server for Improved Performance
|
||||||
Many modern IDEs now include MCP servers that let LLMs perform operations within the IDE itself and use IDE tools. Using
|
Many modern IDEs now include MCP servers that let LLMs perform operations within the IDE itself and use IDE tools. Using
|
||||||
an IDE's MCP server dramatically improves the performance of coding agents. So if you have an IDE, try adding that MCP
|
an IDE's MCP server dramatically improves the performance of coding agents. So if you have an IDE, try adding that MCP
|
||||||
server to your config (see the [MCP Server docs](../../../docs/function-calling/MCP-SERVERS.md) to see how to configure
|
server to your config (see the [MCP Server docs](https://github.com/Dark-Alex-17/loki/wiki/MCP-Servers) to see how to configure
|
||||||
them), and modify the agent definition to look like this:
|
them), and modify the agent definition to look like this:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -33,7 +33,7 @@ global_tools:
|
|||||||
- fs_grep.sh
|
- fs_grep.sh
|
||||||
- fs_glob.sh
|
- fs_glob.sh
|
||||||
- fs_ls.sh
|
- fs_ls.sh
|
||||||
- web_search_loki.sh
|
- web_search_coyote.sh
|
||||||
|
|
||||||
# ...
|
# ...
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ You can also use this agent directly if you have a set of findings you
|
|||||||
want polished:
|
want polished:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
loki -a report-writer "Topic: X. Findings: <paste findings here>"
|
coyote -a report-writer "Topic: X. Findings: <paste findings here>"
|
||||||
```
|
```
|
||||||
|
|
||||||
It will produce a single Markdown report following the rules in its
|
It will produce a single Markdown report following the rules in its
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Sisyphus
|
# Sisyphus
|
||||||
|
|
||||||
The main coordinator agent for the Loki coding ecosystem, providing a powerful CLI interface for code generation and
|
The main coordinator agent for the Coyote coding ecosystem, providing a powerful CLI interface for code generation and
|
||||||
project management similar to OpenCode, ClaudeCode, Codex, or Gemini CLI.
|
project management similar to OpenCode, ClaudeCode, Codex, or Gemini CLI.
|
||||||
|
|
||||||
_Inspired by the Sisyphus and Oracle agents of OpenCode._
|
_Inspired by the Sisyphus and Oracle agents of OpenCode._
|
||||||
@@ -19,8 +19,8 @@ Sisyphus acts as the primary entry point, capable of handling complex tasks by c
|
|||||||
|
|
||||||
## Pro-Tip: Use an IDE MCP Server for Improved Performance
|
## Pro-Tip: Use an IDE MCP Server for Improved Performance
|
||||||
Many modern IDEs (JetBrains, VS Code, Cursor, Zed, etc.) expose MCP servers that let LLMs use IDE tools directly. Using
|
Many modern IDEs (JetBrains, VS Code, Cursor, Zed, etc.) expose MCP servers that let LLMs use IDE tools directly. Using
|
||||||
one dramatically improves the performance of coding agents. If you have one, add it to your loki config (see the
|
one dramatically improves the performance of coding agents. If you have one, add it to your coyote config (see the
|
||||||
[MCP Server docs](../../../docs/function-calling/MCP-SERVERS.md)) and reference it in this agent's `mcp_servers:` list:
|
[MCP Server docs](https://github.com/Dark-Alex-17/loki/wiki/MCP-Servers)) and reference it in this agent's `mcp_servers:` list:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# ...
|
# ...
|
||||||
@@ -33,7 +33,7 @@ global_tools:
|
|||||||
- fs_grep.sh
|
- fs_grep.sh
|
||||||
- fs_glob.sh
|
- fs_glob.sh
|
||||||
- fs_ls.sh
|
- fs_ls.sh
|
||||||
- web_search_loki.sh
|
- web_search_coyote.sh
|
||||||
- execute_command.sh
|
- execute_command.sh
|
||||||
|
|
||||||
# ...
|
# ...
|
||||||
|
|||||||
-1106
File diff suppressed because it is too large
Load Diff
+6
-6
@@ -6,11 +6,11 @@ set -e
|
|||||||
|
|
||||||
# @option --query! The search query.
|
# @option --query! The search query.
|
||||||
|
|
||||||
# @meta require-tools loki
|
# @meta require-tools coyote
|
||||||
|
|
||||||
# @env WEB_SEARCH_MODEL=gemini:gemini-2.5-flash The model for web-searching.
|
# @env WEB_SEARCH_MODEL=gemini:gemini-2.5-flash The model for web-searching.
|
||||||
#
|
#
|
||||||
# supported loki models:
|
# supported coyote models:
|
||||||
# - gemini:gemini-2.0-*
|
# - gemini:gemini-2.0-*
|
||||||
# - vertexai:gemini-*
|
# - vertexai:gemini-*
|
||||||
# - perplexity:*
|
# - perplexity:*
|
||||||
@@ -22,15 +22,15 @@ main() {
|
|||||||
client="${WEB_SEARCH_MODEL%%:*}"
|
client="${WEB_SEARCH_MODEL%%:*}"
|
||||||
|
|
||||||
if [[ "$client" == "gemini" ]]; then
|
if [[ "$client" == "gemini" ]]; then
|
||||||
export LOKI_PATCH_GEMINI_CHAT_COMPLETIONS='{".*":{"body":{"tools":[{"google_search":{}}]}}}'
|
export COYOTE_PATCH_GEMINI_CHAT_COMPLETIONS='{".*":{"body":{"tools":[{"google_search":{}}]}}}'
|
||||||
elif [[ "$client" == "vertexai" ]]; then
|
elif [[ "$client" == "vertexai" ]]; then
|
||||||
export LOKI_PATCH_VERTEXAI_CHAT_COMPLETIONS='{
|
export COYOTE_PATCH_VERTEXAI_CHAT_COMPLETIONS='{
|
||||||
"gemini-1.5-.*":{"body":{"tools":[{"googleSearchRetrieval":{}}]}},
|
"gemini-1.5-.*":{"body":{"tools":[{"googleSearchRetrieval":{}}]}},
|
||||||
"gemini-2.0-.*":{"body":{"tools":[{"google_search":{}}]}}
|
"gemini-2.0-.*":{"body":{"tools":[{"google_search":{}}]}}
|
||||||
}'
|
}'
|
||||||
elif [[ "$client" == "ernie" ]]; then
|
elif [[ "$client" == "ernie" ]]; then
|
||||||
export LOKI_PATCH_ERNIE_CHAT_COMPLETIONS='{".*":{"body":{"web_search":{"enable":true}}}}'
|
export COYOTE_PATCH_ERNIE_CHAT_COMPLETIONS='{".*":{"body":{"web_search":{"enable":true}}}}'
|
||||||
fi
|
fi
|
||||||
|
|
||||||
loki -m "$WEB_SEARCH_MODEL" "$argc_query" >> "$LLM_OUTPUT"
|
coyote -m "$WEB_SEARCH_MODEL" "$argc_query" >> "$LLM_OUTPUT"
|
||||||
}
|
}
|
||||||
@@ -506,7 +506,6 @@ open_link() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
guard_operation() {
|
guard_operation() {
|
||||||
if [[ -t 1 ]]; then
|
|
||||||
if [[ -z "$AUTO_CONFIRM" && -z "$LLM_AGENT_VAR_AUTO_CONFIRM" ]]; then
|
if [[ -z "$AUTO_CONFIRM" && -z "$LLM_AGENT_VAR_AUTO_CONFIRM" ]]; then
|
||||||
ans="$(confirm "${1:-Are you sure you want to continue?}")"
|
ans="$(confirm "${1:-Are you sure you want to continue?}")"
|
||||||
|
|
||||||
@@ -515,7 +514,6 @@ guard_operation() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Here is an example of a patch block that can be applied to modify the file to request the user's name:
|
# Here is an example of a patch block that can be applied to modify the file to request the user's name:
|
||||||
@@ -655,7 +653,6 @@ guard_path() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -t 1 ]]; then
|
|
||||||
path="$(_to_real_path "$1")"
|
path="$(_to_real_path "$1")"
|
||||||
confirmation_prompt="$2"
|
confirmation_prompt="$2"
|
||||||
|
|
||||||
@@ -667,7 +664,6 @@ guard_path() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_to_real_path() {
|
_to_real_path() {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,7 @@ security/configuration settings. The analysis aims to ensure a thorough understa
|
|||||||
structured and operates, enabling the creation of new files, maintaining consistency with existing practices, and the
|
structured and operates, enabling the creation of new files, maintaining consistency with existing practices, and the
|
||||||
potential implementation of best practices.
|
potential implementation of best practices.
|
||||||
|
|
||||||
Should the root directory contain a `LOKI.md` file, this was generated by Loki and should be used as a reference
|
Should the root directory contain a `COYOTE.md` file, this was generated by Coyote and should be used as a reference
|
||||||
point for all analysis, style questions, etc.
|
point for all analysis, style questions, etc.
|
||||||
|
|
||||||
**Objective:** Enable the AI to thoroughly analyze a software repository, providing detailed insights and guidelines on
|
**Objective:** Enable the AI to thoroughly analyze a software repository, providing detailed insights and guidelines on
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
description: Detect and remove AI slop from code and prose; produce output indistinguishable from a senior engineer's.
|
||||||
|
---
|
||||||
|
You are reviewing or generating content. Apply these standards strictly. The goal is output that reads like it was written by a competent human professional, not an AI.
|
||||||
|
|
||||||
|
## Code
|
||||||
|
|
||||||
|
**No useless comments.** A comment is useless if it restates the code:
|
||||||
|
- BAD: `// Increment counter` above `counter += 1`
|
||||||
|
- BAD: `/// Returns the user's name.` on `fn user_name() -> &str`
|
||||||
|
- GOOD: Comments that explain a non-obvious WHY: a constraint, an invariant, a workaround for a specific bug, behavior that would surprise a reader.
|
||||||
|
|
||||||
|
If removing a comment wouldn't confuse a future reader, the comment shouldn't exist.
|
||||||
|
|
||||||
|
**No emojis** unless the user explicitly asked for them.
|
||||||
|
|
||||||
|
**No defensive handling for impossible cases.** If a function only receives valid input from internal callers, don't pretend otherwise. Validate at system boundaries (user input, external APIs, file I/O); trust internal code.
|
||||||
|
|
||||||
|
**No over-engineering for hypothetical futures.** Three similar lines of code is fine. Premature abstractions are worse than duplication.
|
||||||
|
|
||||||
|
**No backwards-compatibility cruft for unreleased code.** If a function isn't called yet, just change it. Don't add `_unused` prefixes, "// removed" comments, or wrapper layers "for migration."
|
||||||
|
|
||||||
|
**Names should be honest.** A function called `get_user` should not mutate state. A field called `count` should not be a function. A method that can fail should return `Result`, not panic.
|
||||||
|
|
||||||
|
## Prose
|
||||||
|
|
||||||
|
**No flattery.** Don't start with "Great question!" or "That's a really good idea!" Just respond.
|
||||||
|
|
||||||
|
**No filler.** "It's important to note that" — delete. "Let me explain" — just explain. "I'll go ahead and" — just do it.
|
||||||
|
|
||||||
|
**No status updates.** "I'm going to help you with that" — just help.
|
||||||
|
|
||||||
|
**Match the user's terseness.** Brief user, brief reply. Detailed user, detailed reply.
|
||||||
|
|
||||||
|
**No multi-paragraph docstrings.** One short line max. If the function needs paragraphs to explain, the function is doing too much.
|
||||||
|
|
||||||
|
## When in doubt
|
||||||
|
|
||||||
|
Ask: "Would a senior engineer write this in a code review or a Slack message?" If not, cut it.
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
---
|
||||||
|
description: Conduct a thorough code review focused on correctness, clarity, tests, and footguns. Grants read-only filesystem access for inspecting code.
|
||||||
|
enabled_tools: fs_read, fs_grep, fs_glob, fs_cat, fs_ls
|
||||||
|
---
|
||||||
|
You are reviewing code. Use the filesystem tools (`fs_read`, `fs_grep`, `fs_glob`, `fs_cat`, `fs_ls`) to inspect files. Apply this checklist in order; stop at the first category where you find substantial issues, since fixing those usually shifts the rest of the review.
|
||||||
|
|
||||||
|
## Investigation workflow
|
||||||
|
|
||||||
|
Before reviewing the diff, build a mental model of the surrounding code:
|
||||||
|
|
||||||
|
- `fs_ls` the directories that contain the changed files.
|
||||||
|
- `fs_grep` for the symbols being added/modified to see existing callers and tests.
|
||||||
|
- `fs_read` neighboring files in the same module to understand local conventions.
|
||||||
|
- `fs_glob` for test files that might cover this area.
|
||||||
|
|
||||||
|
A review without context is just a syntax check.
|
||||||
|
|
||||||
|
## 1. Correctness
|
||||||
|
|
||||||
|
- Does the change actually do what it claims? Does it solve the stated problem?
|
||||||
|
- Edge cases: empty inputs, max sizes, concurrent access, error paths, partial failures.
|
||||||
|
- Off-by-one errors, type confusion, null/None handling, integer overflow.
|
||||||
|
- Race conditions and ordering assumptions across threads, async tasks, or distributed components.
|
||||||
|
- Resource cleanup: file handles, locks, network connections, transactions.
|
||||||
|
|
||||||
|
## 2. Tests
|
||||||
|
|
||||||
|
- Do the tests test BEHAVIOR, not implementation? (Tests of `private_helper()` are usually a smell.)
|
||||||
|
- Will they fail when the code regresses? Or are they tautological (e.g., `assert!(x.is_empty() || !x.is_empty())`)?
|
||||||
|
- Do they cover the unhappy paths, not just the happy ones?
|
||||||
|
- Is there a missing test for the specific bug or feature being added? `fs_grep` for the function name in test files to check.
|
||||||
|
|
||||||
|
## 3. Clarity
|
||||||
|
|
||||||
|
- Are names accurate? `get_user` that mutates is a lie; rename or split.
|
||||||
|
- Could a competent reader understand this without comments?
|
||||||
|
- Is there a simpler way to express the same logic?
|
||||||
|
- Is the function doing one thing, or several things glued together?
|
||||||
|
|
||||||
|
## 4. Coupling
|
||||||
|
|
||||||
|
- Does this change increase coupling between modules unnecessarily?
|
||||||
|
- Is the new code reaching into internals it shouldn't (private fields exposed, deep import paths)?
|
||||||
|
- Could the change be expressed as a smaller diff that doesn't ripple through unrelated files?
|
||||||
|
|
||||||
|
## 5. Footguns
|
||||||
|
|
||||||
|
- Could a future maintainer easily misuse this API?
|
||||||
|
- Are invariants enforced by types, or just by convention?
|
||||||
|
- Are error types specific enough to be actionable?
|
||||||
|
- Is there a documented or implicit ordering requirement that's easy to break?
|
||||||
|
|
||||||
|
## What to flag
|
||||||
|
|
||||||
|
- Correctness bugs.
|
||||||
|
- Missing error handling at trust boundaries.
|
||||||
|
- Race conditions.
|
||||||
|
- Tests that won't catch regressions.
|
||||||
|
- Security issues (injection, auth, exposed secrets).
|
||||||
|
|
||||||
|
## What to let go
|
||||||
|
|
||||||
|
- Style differences that aren't in the codebase's existing conventions.
|
||||||
|
- "I would have done it differently" preferences.
|
||||||
|
- Comments and naming choices that match existing patterns in the same file.
|
||||||
|
- Micro-optimizations in code that isn't on a hot path.
|
||||||
|
|
||||||
|
## Tone
|
||||||
|
|
||||||
|
Direct, specific, focused on the code. No flattery, no padding. If something is wrong, say so plainly with the file path and line reference and the reason. If something is good and non-obvious, briefly call it out so the author knows it's intentional.
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
description: Designer-turned-developer who crafts stunning UI/UX even without design mockups. Grants filesystem read/write access for editing component files.
|
||||||
|
enabled_tools: fs_read, fs_write, fs_patch, fs_grep, fs_glob, fs_cat, fs_ls, fs_mkdir
|
||||||
|
---
|
||||||
|
You are doing frontend work. Use the filesystem tools to read, write, and patch component files. Treat UI/UX as a discipline, not a polish step at the end.
|
||||||
|
|
||||||
|
## Investigate before editing
|
||||||
|
|
||||||
|
Before changing a component:
|
||||||
|
|
||||||
|
- `fs_ls` the component's directory to see siblings and tests.
|
||||||
|
- `fs_read` the component itself.
|
||||||
|
- `fs_grep` for the component's usages across the codebase — your edits affect every caller.
|
||||||
|
- `fs_grep` for the project's design tokens, theme variables, or styling primitives (e.g., `--color-`, `theme.spacing`, `tw-`).
|
||||||
|
- Read existing similar components to match conventions.
|
||||||
|
|
||||||
|
## Visual hierarchy
|
||||||
|
|
||||||
|
Every screen has a focal point. Identify it before laying out anything else:
|
||||||
|
|
||||||
|
- One primary action per view. Make it visually dominant.
|
||||||
|
- Secondary actions are present but visibly subordinate.
|
||||||
|
- Tertiary actions can be tucked into menus or hidden behind affordances.
|
||||||
|
|
||||||
|
## Spacing and rhythm
|
||||||
|
|
||||||
|
- Use the project's existing spacing scale (4px, 8px, custom — match what's already there). Don't introduce one-off values.
|
||||||
|
- Larger spacing = stronger grouping break. Inside a card, tight; between cards, looser.
|
||||||
|
- White space is not wasted space. It's the difference between "professional" and "cramped."
|
||||||
|
|
||||||
|
## Typography
|
||||||
|
|
||||||
|
- Two or three sizes per view, max. More than that is noise.
|
||||||
|
- Line-height: 1.4-1.6 for body, tighter for headlines.
|
||||||
|
- Don't center long paragraphs. Left-align (or right-align for RTL).
|
||||||
|
|
||||||
|
## Color
|
||||||
|
|
||||||
|
- Use the project's existing palette. If you need a color that isn't there, you're probably overdesigning.
|
||||||
|
- Contrast matters: aim for WCAG AA at minimum (4.5:1 for body text, 3:1 for large text).
|
||||||
|
- Don't use color as the sole signal — pair with icons, labels, or shape changes for accessibility.
|
||||||
|
|
||||||
|
## Component conventions
|
||||||
|
|
||||||
|
When adding a new component:
|
||||||
|
|
||||||
|
- Match the existing structure: where do props go, where do styles go, where do tests go?
|
||||||
|
- `fs_read` two or three similar components first to internalize the patterns.
|
||||||
|
- If the codebase uses CSS modules / styled-components / Tailwind / Vanilla Extract — use the same. Don't introduce a new system.
|
||||||
|
- Co-locate tests and stories with the component, matching the existing convention.
|
||||||
|
|
||||||
|
## Forms
|
||||||
|
|
||||||
|
- Label every input. Placeholder text is not a label.
|
||||||
|
- Show validation errors near the field, not in a banner at the top.
|
||||||
|
- Validate on blur, not on every keystroke. Show success states only after the user has interacted.
|
||||||
|
- Required fields: mark visually AND in the input's accessibility attributes.
|
||||||
|
|
||||||
|
## Loading and empty states
|
||||||
|
|
||||||
|
- Empty states are an opportunity, not a fallback. Tell the user what they can do, not "no data."
|
||||||
|
- Loading: show structure (skeletons) when you know what's coming. Spinners are for indeterminate waits.
|
||||||
|
- Errors: explain WHAT failed and what the user can do about it. "Something went wrong" is useless.
|
||||||
|
|
||||||
|
## When unsure
|
||||||
|
|
||||||
|
Ship the boring version. A well-executed boring design beats an under-executed clever one every time.
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
---
|
||||||
|
description: Methodology for atomic commits, rebase surgery, and clean git history. Grants shell access for running git commands.
|
||||||
|
enabled_tools: execute_command
|
||||||
|
---
|
||||||
|
You are operating on a git repository. Apply these conventions strictly. Use the `execute_command` tool to run git commands.
|
||||||
|
|
||||||
|
## Atomic commits
|
||||||
|
|
||||||
|
Each commit represents one logical change. If the commit message needs the word "and," the change is too large; split it. Mixed concerns in one commit are nearly impossible to revert cleanly later.
|
||||||
|
|
||||||
|
## Commit messages
|
||||||
|
|
||||||
|
- Subject line: imperative mood, ≤50 characters, no trailing period.
|
||||||
|
- Blank line.
|
||||||
|
- Body: explain WHY, not WHAT. The diff shows what changed.
|
||||||
|
- Reference issues by URL or canonical ID, not by free-form description.
|
||||||
|
|
||||||
|
## Rebase, don't merge
|
||||||
|
|
||||||
|
- `git rebase -i origin/main` before opening a PR.
|
||||||
|
- Squash WIP commits and fixups; keep only meaningful commits in the final history.
|
||||||
|
- Never rebase a branch others may have based work on. If unsure, ask.
|
||||||
|
|
||||||
|
## Conflict resolution
|
||||||
|
|
||||||
|
- Read both sides carefully before resolving. Don't reflexively take "ours" or "theirs."
|
||||||
|
- After resolving, run tests before continuing the rebase.
|
||||||
|
- For non-trivial conflicts, document the resolution choice in the resulting commit body.
|
||||||
|
|
||||||
|
## Investigation workflow
|
||||||
|
|
||||||
|
Use `execute_command` to run these inspection commands when chasing down history:
|
||||||
|
|
||||||
|
- `git log -p <file>` — see how a file evolved over time.
|
||||||
|
- `git log -S '<string>'` (pickaxe) — find when a string was added or removed.
|
||||||
|
- `git log --all --grep '<pattern>'` — search commit messages.
|
||||||
|
- `git blame -L <start>,<end> <file>` — current authorship for a line range.
|
||||||
|
- `git diff <ref1>..<ref2> -- <path>` — narrow diffs to specific paths.
|
||||||
|
- `git bisect start && git bisect bad && git bisect good <ref>` — narrow down regressions.
|
||||||
|
|
||||||
|
## Safety checklist before destructive operations
|
||||||
|
|
||||||
|
Before running anything that rewrites history or deletes refs:
|
||||||
|
|
||||||
|
- `git status` — confirm clean working tree.
|
||||||
|
- `git branch --show-current` — confirm which branch you're on.
|
||||||
|
- `git log -3 --oneline` — confirm what's about to be moved.
|
||||||
|
|
||||||
|
## What to never do
|
||||||
|
|
||||||
|
- Force-push to shared branches (`main`, release branches, anything teammates pull from).
|
||||||
|
- `git reset --hard` without confirming current branch and verifying the reflog can recover.
|
||||||
|
- `git push --no-verify` to skip hooks — fix the underlying issue instead.
|
||||||
|
- Commit secrets, even temporarily. Once pushed, treat as compromised; rotate.
|
||||||
|
|
||||||
|
## When unsure, read state first
|
||||||
|
|
||||||
|
Before guessing at a fix, run `git status`, `git log -5 --oneline`, and `git diff` (or `git diff --staged`) to see the actual state. Don't operate on assumptions.
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# Agent-specific configuration
|
# Agent-specific configuration
|
||||||
# Location `<loki-config-dir>/agents/<agent-name>/config.yaml`
|
# Location `<coyote-config-dir>/agents/<agent-name>/config.yaml`
|
||||||
#
|
#
|
||||||
# Available Environment Variables:
|
# Available Environment Variables:
|
||||||
# - <agent-name>_MODEL
|
# - <agent-name>_MODEL
|
||||||
@@ -21,14 +21,14 @@ version: 1 # Version of the agent
|
|||||||
# The auto-continue system provides built-in task tracking for improved reliability.
|
# The auto-continue system provides built-in task tracking for improved reliability.
|
||||||
# When enabled, the model can create todo lists and the system will automatically
|
# When enabled, the model can create todo lists and the system will automatically
|
||||||
# prompt it to continue when incomplete tasks remain.
|
# prompt it to continue when incomplete tasks remain.
|
||||||
# See the [Todo System documentation](https://github.com/Dark-Alex-17/loki/wiki/TODO-System) for more information
|
# See the [Todo System documentation](https://github.com/Dark-Alex-17/coyote/wiki/TODO-System) for more information
|
||||||
auto_continue: false # Enable automatic continuation when incomplete todos remain
|
auto_continue: false # Enable automatic continuation when incomplete todos remain
|
||||||
max_auto_continues: 10 # Maximum number of automatic continuations before stopping
|
max_auto_continues: 10 # Maximum number of automatic continuations before stopping
|
||||||
inject_todo_instructions: true # Inject the default todo tool usage instructions into the agent's system prompt
|
inject_todo_instructions: true # Inject the default todo tool usage instructions into the agent's system prompt
|
||||||
continuation_prompt: null # Custom prompt used when auto-continuing (optional; uses default if null)
|
continuation_prompt: null # Custom prompt used when auto-continuing (optional; uses default if null)
|
||||||
# Sub-Agent Spawning System
|
# Sub-Agent Spawning System
|
||||||
# Enable this agent to spawn and manage child agents in parallel.
|
# Enable this agent to spawn and manage child agents in parallel.
|
||||||
# See https://github.com/Dark-Alex-17/loki/wiki/Agents for detailed documentation.
|
# See https://github.com/Dark-Alex-17/coyote/wiki/Agents for detailed documentation.
|
||||||
can_spawn_agents: false # Enable the agent to spawn child agents
|
can_spawn_agents: false # Enable the agent to spawn child agents
|
||||||
max_concurrent_agents: 4 # Maximum number of agents that can run simultaneously
|
max_concurrent_agents: 4 # Maximum number of agents that can run simultaneously
|
||||||
max_agent_depth: 3 # Maximum nesting depth for sub-agents (prevents runaway spawning)
|
max_agent_depth: 3 # Maximum nesting depth for sub-agents (prevents runaway spawning)
|
||||||
@@ -37,11 +37,16 @@ summarization_model: null # Model to use for summarizing sub-agent output
|
|||||||
summarization_threshold: 4000 # Character threshold above which sub-agent output is summarized before returning to parent
|
summarization_threshold: 4000 # Character threshold above which sub-agent output is summarized before returning to parent
|
||||||
escalation_timeout: 300 # Seconds a sub-agent waits for a user interaction response before timing out (default: 5 minutes)
|
escalation_timeout: 300 # Seconds a sub-agent waits for a user interaction response before timing out (default: 5 minutes)
|
||||||
mcp_servers: # Optional list of MCP servers that the agent utilizes
|
mcp_servers: # Optional list of MCP servers that the agent utilizes
|
||||||
- github # Corresponds to the name of an MCP server in the `<loki-config-dir>/functions/mcp.json` file
|
- github # Corresponds to the name of an MCP server in the `<coyote-config-dir>/functions/mcp.json` file
|
||||||
global_tools: # Optional list of additional global tools to enable for the agent; i.e. not tools specific to the agent
|
global_tools: # Optional list of additional global tools to enable for the agent; i.e. not tools specific to the agent
|
||||||
- web_search
|
- web_search
|
||||||
- fs
|
- fs
|
||||||
- python
|
- python
|
||||||
|
skills_enabled: true # Master switch for skills in this agent (default: inherit from global)
|
||||||
|
enabled_skills: # Optional list of skills available when this agent runs.
|
||||||
|
# Must be a subset of global `visible_skills`. Omit to inherit the global default.
|
||||||
|
- git-master
|
||||||
|
- ai-slop-remover
|
||||||
dynamic_instructions: false # Whether to use dynamic instructions for the agent; if false, static instructions are used
|
dynamic_instructions: false # Whether to use dynamic instructions for the agent; if false, static instructions are used
|
||||||
instructions: | # Static instructions for the agent; ignored if dynamic instructions are used
|
instructions: | # Static instructions for the agent; ignored if dynamic instructions are used
|
||||||
You are a AI agent designed to demonstrate agent capabilities.
|
You are a AI agent designed to demonstrate agent capabilities.
|
||||||
@@ -80,10 +85,10 @@ conversation_starters: # Optional conversation starters for the agent
|
|||||||
- What is the best way to exercise?
|
- What is the best way to exercise?
|
||||||
- How do I manage my time effectively?
|
- How do I manage my time effectively?
|
||||||
documents: # Optional documents to load for the agent
|
documents: # Optional documents to load for the agent
|
||||||
- git:/some/repo # Explicitly tell Loki to use the 'git' document loader using an absolute path
|
- git:/some/repo # Explicitly tell Coyote to use the 'git' document loader using an absolute path
|
||||||
- pdf:some-pdf-file.pdf # Explicitly tell Loki to use the 'pdf' document loader using a relative path
|
- pdf:some-pdf-file.pdf # Explicitly tell Coyote to use the 'pdf' document loader using a relative path
|
||||||
- https://some-website.com/some-page
|
- https://some-website.com/some-page
|
||||||
- some-file.pdf # File with relative path to the <loki-config-dir>/agents/<agent-name> directory; i.e. file in the same directory as this config file
|
- some-file.pdf # File with relative path to the <coyote-config-dir>/agents/<agent-name> directory; i.e. file in the same directory as this config file
|
||||||
- ~/some-file.txt # File in the user's home directory
|
- ~/some-file.txt # File in the user's home directory
|
||||||
- /absolute/path/to/some-file.md # File with absolute path
|
- /absolute/path/to/some-file.md # File with absolute path
|
||||||
- /absolute/path/**/NAME.txt # Find all NAME.txt files in the specified directory and all its subdirectories
|
- /absolute/path/**/NAME.txt # Find all NAME.txt files in the specified directory and all its subdirectories
|
||||||
|
|||||||
+62
-47
@@ -18,31 +18,31 @@ agent_session: null # Set a session to use when starting an agent (
|
|||||||
|
|
||||||
# ---- Appearance ----
|
# ---- Appearance ----
|
||||||
highlight: true # Controls syntax highlighting
|
highlight: true # Controls syntax highlighting
|
||||||
light_theme: false # Activates a light color theme when true. env: LOKI_LIGHT_THEME
|
light_theme: false # Activates a light color theme when true. env: COYOTE_LIGHT_THEME
|
||||||
|
|
||||||
# ---- Miscellaneous ----
|
# ---- Miscellaneous ----
|
||||||
user_agent: null # Set User-Agent HTTP header, use `auto` for loki/<current-version>
|
user_agent: null # Set User-Agent HTTP header, use `auto` for coyote/<current-version>
|
||||||
save_shell_history: true # Whether to save shell execution command to the history file
|
save_shell_history: true # Whether to save shell execution command to the history file
|
||||||
sync_models_url: > # URL to sync model changes from
|
sync_models_url: > # URL to sync model changes from
|
||||||
https://raw.githubusercontent.com/Dark-Alex-17/loki/refs/heads/main/models.yaml
|
https://raw.githubusercontent.com/Dark-Alex-17/coyote/refs/heads/main/models.yaml
|
||||||
|
|
||||||
# ---- REPL Prompt ----
|
# ---- REPL Prompt ----
|
||||||
# Custom REPL left/right prompts; see the [REPL Prompt Documentation](https://github.com/Dark-Alex-17/loki/wiki/REPL-Prompt) for more information
|
# Custom REPL left/right prompts; see the [REPL Prompt Documentation](https://github.com/Dark-Alex-17/coyote/wiki/REPL-Prompt) for more information
|
||||||
left_prompt:
|
left_prompt:
|
||||||
'{color.red}{model}){color.green}{?session {?agent {agent}>}{session}{?role /}}{!session {?agent {agent}>}}{role}{?rag @{rag}}{color.cyan}{?session )}{!session >}{color.reset} '
|
'{color.red}{model}){color.green}{?session {?agent {agent}>}{session}{?role /}}{!session {?agent {agent}>}}{role}{?rag @{rag}}{color.cyan}{?session )}{!session >}{color.reset} '
|
||||||
right_prompt:
|
right_prompt:
|
||||||
'{color.purple}{?session {?consume_tokens {consume_tokens}({consume_percent}%)}{!consume_tokens {consume_tokens}}}{color.reset}'
|
'{color.purple}{?session {?consume_tokens {consume_tokens}({consume_percent}%)}{!consume_tokens {consume_tokens}}}{color.reset}'
|
||||||
|
|
||||||
# ---- Vault ----
|
# ---- Vault ----
|
||||||
# See the [Vault documentation](https://github.com/Dark-Alex-17/loki/wiki/Vault) for more information on the Loki vault
|
# See the [Vault documentation](https://github.com/Dark-Alex-17/coyote/wiki/Vault) for more information on the Coyote vault
|
||||||
vault_password_file: null # Path to a file containing the password for the Loki vault (cannot be a secret template)
|
vault_password_file: null # Path to a file containing the password for the Coyote vault (cannot be a secret template)
|
||||||
|
|
||||||
# ---- Function Calling ----
|
# ---- Function Calling ----
|
||||||
# See the [Tools documentation](https://github.com/Dark-Alex-17/loki/wiki/Tools) for more details
|
# See the [Tools documentation](https://github.com/Dark-Alex-17/coyote/wiki/Tools) for more details
|
||||||
function_calling: true # Enables or disables function calling (Globally).
|
function_calling_support: true # Enables or disables function calling (Globally).
|
||||||
mapping_tools: # Alias for a tool or toolset
|
mapping_tools: # Alias for a tool or toolset
|
||||||
fs: 'fs_cat,fs_ls,fs_mkdir,fs_rm,fs_write,fs_read,fs_glob,fs_grep'
|
fs: 'fs_cat,fs_ls,fs_mkdir,fs_rm,fs_write,fs_read,fs_glob,fs_grep'
|
||||||
enabled_tools: null # Which tools to enable by default. (e.g. 'fs,web_search_loki')
|
enabled_tools: null # Which tools to enable by default. (e.g. 'fs,web_search_coyote')
|
||||||
visible_tools: # Which tools are visible to be compiled (and are thus able to be defined in 'enabled_tools')
|
visible_tools: # Which tools are visible to be compiled (and are thus able to be defined in 'enabled_tools')
|
||||||
# - demo_py.py
|
# - demo_py.py
|
||||||
# - demo_sh.sh
|
# - demo_sh.sh
|
||||||
@@ -69,29 +69,44 @@ visible_tools: # Which tools are visible to be compiled (and a
|
|||||||
# - search_wolframalpha.sh
|
# - search_wolframalpha.sh
|
||||||
# - send_mail.sh
|
# - send_mail.sh
|
||||||
# - send_twilio.sh
|
# - send_twilio.sh
|
||||||
# - web_search_loki.sh
|
# - web_search_coyote.sh
|
||||||
# - web_search_perplexity.sh
|
# - web_search_perplexity.sh
|
||||||
# - web_search_tavily.sh
|
# - web_search_tavily.sh
|
||||||
|
|
||||||
# ---- MCP Servers ----
|
# ---- MCP Servers ----
|
||||||
# See the [MCP Servers documentation](https://github.com/Dark-Alex-17/loki/wiki/MCP-Servers) for more details
|
# See the [MCP Servers documentation](https://github.com/Dark-Alex-17/coyote/wiki/MCP-Servers) for more details
|
||||||
mcp_server_support: true # Enables or disables MCP servers (globally).
|
mcp_server_support: true # Enables or disables MCP servers (globally).
|
||||||
mapping_mcp_servers: # Alias for an MCP server or set of servers
|
mapping_mcp_servers: # Alias for an MCP server or set of servers
|
||||||
git: github,gitmcp
|
git: github,gitmcp
|
||||||
enabled_mcp_servers: null # Which MCP servers to enable by default (e.g. 'github,slack,ddg-search')
|
enabled_mcp_servers: null # Which MCP servers to enable by default (e.g. 'github,slack,ddg-search')
|
||||||
|
|
||||||
|
# ---- Skills ----
|
||||||
|
# Skills are modular knowledge or capability packs the LLM can load and unload mid-conversation.
|
||||||
|
# See the [Skills documentation](https://github.com/Dark-Alex-17/coyote/wiki/Skills) for more details.
|
||||||
|
skills_enabled: true # Master switch. Set to false to hide all skill management tools from the model.
|
||||||
|
visible_skills: # The universe of skills allowed to be enabled in any context. Omit (null) for "all installed".
|
||||||
|
- ai-slop-remover
|
||||||
|
- code-review
|
||||||
|
- frontend-ui-ux
|
||||||
|
- git-master
|
||||||
|
enabled_skills: null # Which skills are available by default (no role/agent/session active). null = all visible.
|
||||||
|
# Example: only expose two skills in the bare REPL.
|
||||||
|
# enabled_skills:
|
||||||
|
# - git-master
|
||||||
|
# - ai-slop-remover
|
||||||
|
|
||||||
# ---- Auto-Continue (Todo System) ----
|
# ---- Auto-Continue (Todo System) ----
|
||||||
# The auto-continue system provides built-in task tracking for improved reliability.
|
# The auto-continue system provides built-in task tracking for improved reliability.
|
||||||
# When enabled, the model can create todo lists and the system will automatically
|
# When enabled, the model can create todo lists and the system will automatically
|
||||||
# prompt it to continue when incomplete tasks remain.
|
# prompt it to continue when incomplete tasks remain.
|
||||||
# See the [Todo System documentation](https://github.com/Dark-Alex-17/loki/wiki/TODO-System) for more information
|
# See the [Todo System documentation](https://github.com/Dark-Alex-17/coyote/wiki/TODO-System) for more information
|
||||||
auto_continue: false # Enable automatic continuation when incomplete todos remain (default: false)
|
auto_continue: false # Enable automatic continuation when incomplete todos remain (default: false)
|
||||||
max_auto_continues: 10 # Maximum number of automatic continuations before stopping (default: 10)
|
max_auto_continues: 10 # Maximum number of automatic continuations before stopping (default: 10)
|
||||||
inject_todo_instructions: true # Inject default todo usage instructions into the system prompt (default: true)
|
inject_todo_instructions: true # Inject default todo usage instructions into the system prompt (default: true)
|
||||||
continuation_prompt: null # Custom prompt used when auto-continuing. If null, uses built-in default
|
continuation_prompt: null # Custom prompt used when auto-continuing. If null, uses built-in default
|
||||||
|
|
||||||
# ---- Session ----
|
# ---- Session ----
|
||||||
# See the [Session documentation](https://github.com/Dark-Alex-17/loki/wiki/Sessions) for more information
|
# See the [Session documentation](https://github.com/Dark-Alex-17/coyote/wiki/Sessions) for more information
|
||||||
save_session: null # Controls the persistence of the session. If true, auto save; if false, don't auto-save save; if null, ask the user what to do
|
save_session: null # Controls the persistence of the session. If true, auto save; if false, don't auto-save save; if null, ask the user what to do
|
||||||
compression_threshold: 4000 # Compress the session when the token count reaches or exceeds this threshold
|
compression_threshold: 4000 # Compress the session when the token count reaches or exceeds this threshold
|
||||||
summarization_prompt: > # The text prompt used for creating a concise summary of session message
|
summarization_prompt: > # The text prompt used for creating a concise summary of session message
|
||||||
@@ -100,9 +115,9 @@ summary_context_prompt: > # The text prompt used for including the summar
|
|||||||
'This is a summary of the chat history as a recap: '
|
'This is a summary of the chat history as a recap: '
|
||||||
|
|
||||||
# ---- RAG ----
|
# ---- RAG ----
|
||||||
# See the [RAG Docs](https://github.com/Dark-Alex-17/loki/wiki/RAG) for more details.
|
# See the [RAG Docs](https://github.com/Dark-Alex-17/coyote/wiki/RAG) for more details.
|
||||||
rag_embedding_model: null # Specifies the embedding model used for context retrieval
|
rag_embedding_model: null # Specifies the embedding model used for context retrieval
|
||||||
rag_reranker_model: null # Specifies the reranker model used for sorting retrieved documents; Loki uses Reciprocal Rank Fusion by default
|
rag_reranker_model: null # Specifies the reranker model used for sorting retrieved documents; Coyote uses Reciprocal Rank Fusion by default
|
||||||
rag_top_k: 5 # Specifies the number of documents to retrieve for answering queries
|
rag_top_k: 5 # Specifies the number of documents to retrieve for answering queries
|
||||||
rag_chunk_size: null # Defines the size of chunks for document processing in characters
|
rag_chunk_size: null # Defines the size of chunks for document processing in characters
|
||||||
rag_chunk_overlap: null # Defines the overlap between chunks
|
rag_chunk_overlap: null # Defines the overlap between chunks
|
||||||
@@ -141,12 +156,12 @@ document_loaders:
|
|||||||
docx: 'pandoc --to plain $1' # Use pandoc to convert a .docx file to text
|
docx: 'pandoc --to plain $1' # Use pandoc to convert a .docx file to text
|
||||||
# (see https://pandoc.org for details on how to install pandoc)
|
# (see https://pandoc.org for details on how to install pandoc)
|
||||||
jina: 'curl -fsSL https://r.jina.ai/$1 -H "Authorization: Bearer {{JINA_API_KEY}}' # Use Jina to translate a website into text;
|
jina: 'curl -fsSL https://r.jina.ai/$1 -H "Authorization: Bearer {{JINA_API_KEY}}' # Use Jina to translate a website into text;
|
||||||
# Requires a Jina API key to be added to the Loki vault
|
# Requires a Jina API key to be added to the Coyote vault
|
||||||
git: > # Use yek to load a git repository into the knowledgebase (https://github.com/bodo-run/yek)
|
git: > # Use yek to load a git repository into the knowledgebase (https://github.com/bodo-run/yek)
|
||||||
sh -c "yek $1 --json | jq 'map({ path: .filename, contents: .content })'"
|
sh -c "yek $1 --json | jq 'map({ path: .filename, contents: .content })'"
|
||||||
|
|
||||||
# ---- Clients ----
|
# ---- Clients ----
|
||||||
# See the [Clients documentation](https://github.com/Dark-Alex-17/loki/wiki/Clients) for more details
|
# See the [Clients documentation](https://github.com/Dark-Alex-17/coyote/wiki/Clients) for more details
|
||||||
clients:
|
clients:
|
||||||
# All clients have the following configuration:
|
# All clients have the following configuration:
|
||||||
# - type: xxxx
|
# - type: xxxx
|
||||||
@@ -177,14 +192,14 @@ clients:
|
|||||||
# See https://platform.openai.com/docs/quickstart
|
# See https://platform.openai.com/docs/quickstart
|
||||||
- type: openai
|
- type: openai
|
||||||
api_base: https://api.openai.com/v1 # Optional
|
api_base: https://api.openai.com/v1 # Optional
|
||||||
api_key: '{{OPENAI_API_KEY}}' # You can either hard-code or inject secrets from the Loki vault
|
api_key: '{{OPENAI_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
|
||||||
organization_id: org-xxx # Optional
|
organization_id: org-xxx # Optional
|
||||||
|
|
||||||
# For any platform compatible with OpenAI's API
|
# For any platform compatible with OpenAI's API
|
||||||
- type: openai-compatible
|
- type: openai-compatible
|
||||||
name: ollama
|
name: ollama
|
||||||
api_base: http://localhost:11434/v1
|
api_base: http://localhost:11434/v1
|
||||||
api_key: '{{OLLAMA_API_KEY}}' # Optional; You can either hard-code or inject secrets from the Loki vault
|
api_key: '{{OLLAMA_API_KEY}}' # Optional; You can either hard-code or inject secrets from the Coyote vault
|
||||||
models:
|
models:
|
||||||
- name: deepseek-r1
|
- name: deepseek-r1
|
||||||
max_input_tokens: 131072
|
max_input_tokens: 131072
|
||||||
@@ -202,9 +217,9 @@ clients:
|
|||||||
# See https://ai.google.dev/docs
|
# See https://ai.google.dev/docs
|
||||||
- type: gemini
|
- type: gemini
|
||||||
api_base: https://generativelanguage.googleapis.com/v1beta
|
api_base: https://generativelanguage.googleapis.com/v1beta
|
||||||
api_key: '{{GEMINI_API_KEY}}' # You can either hard-code or inject secrets from the Loki vault
|
api_key: '{{GEMINI_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
|
||||||
auth: null # When set to 'oauth', Loki will use OAuth instead of an API key
|
auth: null # When set to 'oauth', Coyote will use OAuth instead of an API key
|
||||||
# Authenticate with `loki --authenticate` or `.authenticate` in the REPL
|
# Authenticate with `coyote --authenticate` or `.authenticate` in the REPL
|
||||||
patch:
|
patch:
|
||||||
chat_completions:
|
chat_completions:
|
||||||
'.*':
|
'.*':
|
||||||
@@ -222,49 +237,49 @@ clients:
|
|||||||
# See https://docs.anthropic.com/claude/reference/getting-started-with-the-api
|
# See https://docs.anthropic.com/claude/reference/getting-started-with-the-api
|
||||||
- type: claude
|
- type: claude
|
||||||
api_base: https://api.anthropic.com/v1 # Optional
|
api_base: https://api.anthropic.com/v1 # Optional
|
||||||
api_key: '{{ANTHROPIC_API_KEY}}' # You can either hard-code or inject secrets from the Loki vault
|
api_key: '{{ANTHROPIC_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
|
||||||
auth: null # When set to 'oauth', Loki will use OAuth instead of an API key
|
auth: null # When set to 'oauth', Coyote will use OAuth instead of an API key
|
||||||
# Authenticate with `loki --authenticate` or `.authenticate` in the REPL
|
# Authenticate with `coyote --authenticate` or `.authenticate` in the REPL
|
||||||
|
|
||||||
# See https://docs.mistral.ai/
|
# See https://docs.mistral.ai/
|
||||||
- type: openai-compatible
|
- type: openai-compatible
|
||||||
name: mistral
|
name: mistral
|
||||||
api_base: https://api.mistral.ai/v1
|
api_base: https://api.mistral.ai/v1
|
||||||
api_key: '{{MISTRAL_API_KEY}}' # You can either hard-code or inject secrets from the Loki vault
|
api_key: '{{MISTRAL_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
|
||||||
|
|
||||||
# See https://docs.x.ai/docs
|
# See https://docs.x.ai/docs
|
||||||
- type: openai-compatible
|
- type: openai-compatible
|
||||||
name: xai
|
name: xai
|
||||||
api_base: https://api.x.ai/v1
|
api_base: https://api.x.ai/v1
|
||||||
api_key: '{{XAI_API_KEY}}' # You can either hard-code or inject secrets from the Loki vault
|
api_key: '{{XAI_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
|
||||||
|
|
||||||
# See https://docs.ai21.com/docs/overview
|
# See https://docs.ai21.com/docs/overview
|
||||||
- type: openai-compatible
|
- type: openai-compatible
|
||||||
name: ai12
|
name: ai12
|
||||||
api_base: https://api.ai21.com/studio/v1
|
api_base: https://api.ai21.com/studio/v1
|
||||||
api_key: '{{AI21_API_KEY}}' # You can either hard-code or inject secrets from the Loki vault
|
api_key: '{{AI21_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
|
||||||
|
|
||||||
# See https://docs.cohere.com/docs/the-cohere-platform
|
# See https://docs.cohere.com/docs/the-cohere-platform
|
||||||
- type: cohere
|
- type: cohere
|
||||||
api_base: https://api.cohere.ai/v2 # Optional
|
api_base: https://api.cohere.ai/v2 # Optional
|
||||||
api_key: '{{COHERE_API_KEY}}' # You can either hard-code or inject secrets from the Loki vault
|
api_key: '{{COHERE_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
|
||||||
|
|
||||||
# See https://docs.perplexity.ai/getting-started/overview
|
# See https://docs.perplexity.ai/getting-started/overview
|
||||||
- type: openai-compatible
|
- type: openai-compatible
|
||||||
name: perplexity
|
name: perplexity
|
||||||
api_base: https://api.perplexity.ai
|
api_base: https://api.perplexity.ai
|
||||||
api_key: '{{PERPLEXITY_API_KEY}}' # You can either hard-code or inject secrets from the Loki vault
|
api_key: '{{PERPLEXITY_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
|
||||||
|
|
||||||
# See https://console.groq.com/docs/quickstart
|
# See https://console.groq.com/docs/quickstart
|
||||||
- type: openai-compatible
|
- type: openai-compatible
|
||||||
name: groq
|
name: groq
|
||||||
api_base: https://api.groq.com/openai/v1
|
api_base: https://api.groq.com/openai/v1
|
||||||
api_key: '{{GROQ_API_KEY}}' # You can either hard-code or inject secrets from the Loki vault
|
api_key: '{{GROQ_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
|
||||||
|
|
||||||
# See https://learn.microsoft.com/en-us/azure/ai-services/openai/chatgpt-quickstart
|
# See https://learn.microsoft.com/en-us/azure/ai-services/openai/chatgpt-quickstart
|
||||||
- type: azure-openai
|
- type: azure-openai
|
||||||
api_base: https://{RESOURCE}.openai.azure.com
|
api_base: https://{RESOURCE}.openai.azure.com
|
||||||
api_key: '{{AZURE_OPENAI_API_KEY}}' # You can either hard-code or inject secrets from the Loki vault
|
api_key: '{{AZURE_OPENAI_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
|
||||||
models:
|
models:
|
||||||
- name: gpt-4o # Model deployment name
|
- name: gpt-4o # Model deployment name
|
||||||
max_input_tokens: 128000
|
max_input_tokens: 128000
|
||||||
@@ -295,8 +310,8 @@ clients:
|
|||||||
|
|
||||||
# See https://docs.aws.amazon.com/bedrock/latest/userguide/
|
# See https://docs.aws.amazon.com/bedrock/latest/userguide/
|
||||||
- type: bedrock
|
- type: bedrock
|
||||||
access_key_id: '{{AWS_ACCESS_KEY_ID}}' # You can either hard-code or inject secrets from the Loki vault
|
access_key_id: '{{AWS_ACCESS_KEY_ID}}' # You can either hard-code or inject secrets from the Coyote vault
|
||||||
secret_access_key: '{{AWS_SECRET_ACCESS_KEY}}' # You can either hard-code or inject secrets from the Loki vault
|
secret_access_key: '{{AWS_SECRET_ACCESS_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
|
||||||
region: xxx
|
region: xxx
|
||||||
session_token: xxx # Optional, only needed for temporary credentials
|
session_token: xxx # Optional, only needed for temporary credentials
|
||||||
|
|
||||||
@@ -304,67 +319,67 @@ clients:
|
|||||||
- type: openai-compatible
|
- type: openai-compatible
|
||||||
name: cloudflare
|
name: cloudflare
|
||||||
api_base: https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/ai/v1
|
api_base: https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/ai/v1
|
||||||
api_key: '{{CLOUDFLARE_API_KEY}}' # You can either hard-code or inject secrets from the Loki vault
|
api_key: '{{CLOUDFLARE_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
|
||||||
|
|
||||||
# See https://cloud.baidu.com/doc/WENXINWORKSHOP/index.html
|
# See https://cloud.baidu.com/doc/WENXINWORKSHOP/index.html
|
||||||
- type: openai-compatible
|
- type: openai-compatible
|
||||||
name: ernie
|
name: ernie
|
||||||
api_base: https://qianfan.baidubce.com/v2
|
api_base: https://qianfan.baidubce.com/v2
|
||||||
api_key: '{{BAIDU_API_KEY}}' # You can either hard-code or inject secrets from the Loki vault
|
api_key: '{{BAIDU_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
|
||||||
|
|
||||||
# See https://dashscope.aliyun.com/
|
# See https://dashscope.aliyun.com/
|
||||||
- type: openai-compatible
|
- type: openai-compatible
|
||||||
name: qianwen
|
name: qianwen
|
||||||
api_base: https://dashscope.aliyuncs.com/compatible-mode/v1
|
api_base: https://dashscope.aliyuncs.com/compatible-mode/v1
|
||||||
api_key: '{{ALIYUN_API_KEY}}' # You can either hard-code or inject secrets from the Loki vault
|
api_key: '{{ALIYUN_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
|
||||||
|
|
||||||
# See https://cloud.tencent.com/product/hunyuan
|
# See https://cloud.tencent.com/product/hunyuan
|
||||||
- type: openai-compatible
|
- type: openai-compatible
|
||||||
name: hunyuan
|
name: hunyuan
|
||||||
api_base: https://api.hunyuan.cloud.tencent.com/v1
|
api_base: https://api.hunyuan.cloud.tencent.com/v1
|
||||||
api_key: '{{TENCENT_API_KEY}}' # You can either hard-code or inject secrets from the Loki vault
|
api_key: '{{TENCENT_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
|
||||||
|
|
||||||
# See https://platform.moonshot.cn/docs/intro
|
# See https://platform.moonshot.cn/docs/intro
|
||||||
- type: openai-compatible
|
- type: openai-compatible
|
||||||
name: moonshot
|
name: moonshot
|
||||||
api_base: https://api.moonshot.cn/v1
|
api_base: https://api.moonshot.cn/v1
|
||||||
api_key: '{{MOONSHOT_API_KEY}}' # You can either hard-code or inject secrets from the Loki vault
|
api_key: '{{MOONSHOT_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
|
||||||
|
|
||||||
# See https://platform.deepseek.com/api-docs/
|
# See https://platform.deepseek.com/api-docs/
|
||||||
- type: openai-compatible
|
- type: openai-compatible
|
||||||
name: deepseek
|
name: deepseek
|
||||||
api_base: https://api.deepseek.com
|
api_base: https://api.deepseek.com
|
||||||
api_key: '{{DEEPSEEK_API_KEY}}' # You can either hard-code or inject secrets from the Loki vault
|
api_key: '{{DEEPSEEK_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
|
||||||
|
|
||||||
# See https://open.bigmodel.cn/dev/howuse/introduction
|
# See https://open.bigmodel.cn/dev/howuse/introduction
|
||||||
- type: openai-compatible
|
- type: openai-compatible
|
||||||
name: zhipuai
|
name: zhipuai
|
||||||
api_base: https://open.bigmodel.cn/api/paas/v4
|
api_base: https://open.bigmodel.cn/api/paas/v4
|
||||||
api_key: '{{ZHIPUAI_API_KEY}}' # You can either hard-code or inject secrets from the Loki vault
|
api_key: '{{ZHIPUAI_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
|
||||||
|
|
||||||
# See https://platform.minimaxi.com/document/Fast%20access
|
# See https://platform.minimaxi.com/document/Fast%20access
|
||||||
- type: openai-compatible
|
- type: openai-compatible
|
||||||
name: minimax
|
name: minimax
|
||||||
api_base: https://api.minimax.chat/v1
|
api_base: https://api.minimax.chat/v1
|
||||||
api_key: '{{MINIMAX_API_KEY}}' # You can either hard-code or inject secrets from the Loki vault
|
api_key: '{{MINIMAX_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
|
||||||
|
|
||||||
# See https://openrouter.ai/docs#quick-start
|
# See https://openrouter.ai/docs#quick-start
|
||||||
- type: openai-compatible
|
- type: openai-compatible
|
||||||
name: openrouter
|
name: openrouter
|
||||||
api_base: https://openrouter.ai/api/v1
|
api_base: https://openrouter.ai/api/v1
|
||||||
api_key: '{{OPENROUTER_API_KEY}}' # You can either hard-code or inject secrets from the Loki vault
|
api_key: '{{OPENROUTER_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
|
||||||
|
|
||||||
# See https://github.com/marketplace/models
|
# See https://github.com/marketplace/models
|
||||||
- type: openai-compatible
|
- type: openai-compatible
|
||||||
name: github
|
name: github
|
||||||
api_base: https://models.inference.ai.azure.com
|
api_base: https://models.inference.ai.azure.com
|
||||||
api_key: '{{GITHUB_API_KEY}}' # You can either hard-code or inject secrets from the Loki vault
|
api_key: '{{GITHUB_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
|
||||||
|
|
||||||
# See https://deepinfra.com/docs
|
# See https://deepinfra.com/docs
|
||||||
- type: openai-compatible
|
- type: openai-compatible
|
||||||
name: deepinfra
|
name: deepinfra
|
||||||
api_base: https://api.deepinfra.com/v1/openai
|
api_base: https://api.deepinfra.com/v1/openai
|
||||||
api_key: '{{DEEPINFRA_API_KEY}}' # You can either hard-code or inject secrets from the Loki vault
|
api_key: '{{DEEPINFRA_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
|
||||||
|
|
||||||
|
|
||||||
# ----- RAG dedicated -----
|
# ----- RAG dedicated -----
|
||||||
@@ -373,10 +388,10 @@ clients:
|
|||||||
- type: openai-compatible
|
- type: openai-compatible
|
||||||
name: jina
|
name: jina
|
||||||
api_base: https://api.jina.ai/v1
|
api_base: https://api.jina.ai/v1
|
||||||
api_key: '{{JINA_API_KEY}}' # You can either hard-code or inject secrets from the Loki vault
|
api_key: '{{JINA_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
|
||||||
|
|
||||||
# See https://docs.voyageai.com/docs/introduction
|
# See https://docs.voyageai.com/docs/introduction
|
||||||
- type: openai-compatible
|
- type: openai-compatible
|
||||||
name: voyageai
|
name: voyageai
|
||||||
api_base: https://api.voyageai.com/v1
|
api_base: https://api.voyageai.com/v1
|
||||||
api_key: '{{VOYAGEAI_API_KEY}}' # You can either hard-code or inject secrets from the Loki vault
|
api_key: '{{VOYAGEAI_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
|
||||||
|
|||||||
@@ -10,13 +10,16 @@ temperature: 0.2 # The temperature to use for this role whe
|
|||||||
top_p: 0 # The top_p to use for this role when querying the model
|
top_p: 0 # The top_p to use for this role when querying the model
|
||||||
enabled_tools: fs_ls,fs_cat # A comma-separated list of tools to enable for this role
|
enabled_tools: fs_ls,fs_cat # A comma-separated list of tools to enable for this role
|
||||||
enabled_mcp_servers: github,gitmcp # A comma-separated list of MCP servers to enable for this role
|
enabled_mcp_servers: github,gitmcp # A comma-separated list of MCP servers to enable for this role
|
||||||
|
skills_enabled: true # Master switch for skills in this role (default: inherit from global)
|
||||||
|
enabled_skills: git-master,ai-slop-remover # Comma-separated list of skills available when this role is active.
|
||||||
|
# Must be a subset of global `visible_skills`. Omit to inherit the global default.
|
||||||
prompt: null # A custom prompt to use for this role that will immediately query
|
prompt: null # A custom prompt to use for this role that will immediately query
|
||||||
# the model for output instead of using the instructions below
|
# the model for output instead of using the instructions below
|
||||||
# Auto-Continue (Todo System)
|
# Auto-Continue (Todo System)
|
||||||
# The auto-continue system provides built-in task tracking for improved reliability.
|
# The auto-continue system provides built-in task tracking for improved reliability.
|
||||||
# When enabled, the model can create todo lists and the system will automatically
|
# When enabled, the model can create todo lists and the system will automatically
|
||||||
# prompt it to continue when incomplete tasks remain.
|
# prompt it to continue when incomplete tasks remain.
|
||||||
# See the [Todo System documentation](https://github.com/Dark-Alex-17/loki/wiki/TODO-System) for more information
|
# See the [Todo System documentation](https://github.com/Dark-Alex-17/coyote/wiki/TODO-System) for more information
|
||||||
auto_continue: false # Enable automatic continuation when incomplete todos remain (default: false)
|
auto_continue: false # Enable automatic continuation when incomplete todos remain (default: false)
|
||||||
max_auto_continues: 10 # Maximum number of automatic continuations before stopping (default: 10)
|
max_auto_continues: 10 # Maximum number of automatic continuations before stopping (default: 10)
|
||||||
inject_todo_instructions: true # Inject default todo tool usage instructions into the system prompt (default: true)
|
inject_todo_instructions: true # Inject default todo tool usage instructions into the system prompt (default: true)
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Documentation: https://docs.brew.sh/Formula-Cookbook
|
||||||
|
# https://rubydoc.brew.sh/Formula
|
||||||
|
class Coyote < Formula
|
||||||
|
desc "All-in-one, batteries included LLM CLI tool"
|
||||||
|
homepage "https://github.com/Dark-Alex-17/coyote"
|
||||||
|
if OS.mac? and Hardware::CPU.arm?
|
||||||
|
url "https://github.com/Dark-Alex-17/coyote/releases/download/v$version/coyote-aarch64-apple-darwin.tar.gz"
|
||||||
|
sha256 "$hash_mac_arm"
|
||||||
|
elsif OS.mac? and Hardware::CPU.intel?
|
||||||
|
url "https://github.com/Dark-Alex-17/coyote/releases/download/v$version/coyote-x86_64-apple-darwin.tar.gz"
|
||||||
|
sha256 "$hash_mac"
|
||||||
|
else
|
||||||
|
url "https://github.com/Dark-Alex-17/coyote/releases/download/v$version/coyote-x86_64-unknown-linux-musl.tar.gz"
|
||||||
|
sha256 "$hash_linux"
|
||||||
|
end
|
||||||
|
version "$version"
|
||||||
|
license "MIT"
|
||||||
|
|
||||||
|
def install
|
||||||
|
bin.install "coyote"
|
||||||
|
ohai "You're done! Get started with \"coyote --help\""
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# Documentation: https://docs.brew.sh/Formula-Cookbook
|
|
||||||
# https://rubydoc.brew.sh/Formula
|
|
||||||
class Loki < Formula
|
|
||||||
desc "All-in-one, batteries included LLM CLI tool"
|
|
||||||
homepage "https://github.com/Dark-Alex-17/loki"
|
|
||||||
if OS.mac? and Hardware::CPU.arm?
|
|
||||||
url "https://github.com/Dark-Alex-17/loki/releases/download/v$version/loki-aarch64-apple-darwin.tar.gz"
|
|
||||||
sha256 "$hash_mac_arm"
|
|
||||||
elsif OS.mac? and Hardware::CPU.intel?
|
|
||||||
url "https://github.com/Dark-Alex-17/loki/releases/download/v$version/loki-x86_64-apple-darwin.tar.gz"
|
|
||||||
sha256 "$hash_mac"
|
|
||||||
else
|
|
||||||
url "https://github.com/Dark-Alex-17/loki/releases/download/v$version/loki-x86_64-unknown-linux-musl.tar.gz"
|
|
||||||
sha256 "$hash_linux"
|
|
||||||
end
|
|
||||||
version "$version"
|
|
||||||
license "MIT"
|
|
||||||
|
|
||||||
def install
|
|
||||||
bin.install "loki"
|
|
||||||
ohai "You're done! Get started with \"loki --help\""
|
|
||||||
end
|
|
||||||
end
|
|
||||||
+14
-14
@@ -1,5 +1,5 @@
|
|||||||
# Graph-based agent definition (full-featured reference)
|
# Graph-based agent definition (full-featured reference)
|
||||||
# Location: <loki-config-dir>/agents/<agent-name>/graph.yaml
|
# Location: <coyote-config-dir>/agents/<agent-name>/graph.yaml
|
||||||
#
|
#
|
||||||
# A graph agent is defined by this file alone. An agent directory contains
|
# A graph agent is defined by this file alone. An agent directory contains
|
||||||
# either a config.yaml (a normal LLM-loop agent) or a graph.yaml (a graph
|
# either a config.yaml (a normal LLM-loop agent) or a graph.yaml (a graph
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
# runnable deep-research graph agent, see assets/agents/deep-research/.
|
# runnable deep-research graph agent, see assets/agents/deep-research/.
|
||||||
#
|
#
|
||||||
# Full documentation:
|
# Full documentation:
|
||||||
# https://github.com/Dark-Alex-17/loki/wiki/Graph-Agents
|
# https://github.com/Dark-Alex-17/coyote/wiki/Graph-Agents
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Identity
|
# Identity
|
||||||
@@ -35,7 +35,7 @@ temperature: 0.0 # Default sampling temperature for `llm` node
|
|||||||
top_p: null # Default sampling top-p for `llm` nodes
|
top_p: null # Default sampling top-p for `llm` nodes
|
||||||
|
|
||||||
global_tools: # Tool universe an `llm` node's `tools:` whitelist draws from
|
global_tools: # Tool universe an `llm` node's `tools:` whitelist draws from
|
||||||
- web_search_loki.sh
|
- web_search_coyote.sh
|
||||||
- fetch_url_via_curl.sh
|
- fetch_url_via_curl.sh
|
||||||
|
|
||||||
mcp_servers: # MCP servers an `llm` node may reference via `mcp:<server>`
|
mcp_servers: # MCP servers an `llm` node may reference via `mcp:<server>`
|
||||||
@@ -52,7 +52,7 @@ conversation_starters: # Suggested prompts surfaced in the UI
|
|||||||
# (see initial_state below).
|
# (see initial_state below).
|
||||||
# - Script nodes via the env var `LLM_AGENT_VAR_<UPPER_NAME>`.
|
# - Script nodes via the env var `LLM_AGENT_VAR_<UPPER_NAME>`.
|
||||||
# Values may be overridden at runtime with
|
# Values may be overridden at runtime with
|
||||||
# `loki -a <agent> --agent-variable <name> <value> "..."`.
|
# `coyote -a <agent> --agent-variable <name> <value> "..."`.
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
variables:
|
variables:
|
||||||
- name: project_dir
|
- name: project_dir
|
||||||
@@ -103,7 +103,7 @@ reducers:
|
|||||||
# Values placed into graph state before any node runs; reference anywhere via
|
# Values placed into graph state before any node runs; reference anywhere via
|
||||||
# {{key}}.
|
# {{key}}.
|
||||||
#
|
#
|
||||||
# Note: `initial_prompt` is seeded automatically by Loki with the
|
# Note: `initial_prompt` is seeded automatically by Coyote with the
|
||||||
# caller's prompt. So there's no need to set it here.
|
# caller's prompt. So there's no need to set it here.
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
initial_state:
|
initial_state:
|
||||||
@@ -123,7 +123,7 @@ start: triage # ID of the first node to run (must exist in `nodes
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Nodes
|
# Nodes
|
||||||
# Each node is keyed by its id. The `id:` inside a node must match its key
|
# Each node is keyed by its id. The `id:` inside a node must match its key
|
||||||
# (it may also be omitted and thus Loki fills it in from the key).
|
# (it may also be omitted and thus Coyote fills it in from the key).
|
||||||
#
|
#
|
||||||
# Node types: agent | script | approval | input | llm | rag | map | end
|
# Node types: agent | script | approval | input | llm | rag | map | end
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -202,7 +202,7 @@ nodes:
|
|||||||
instructions: "You are a web researcher. Cite every claim."
|
instructions: "You are a web researcher. Cite every claim."
|
||||||
prompt: "Web research: {{topic}}. Return findings and sources."
|
prompt: "Web research: {{topic}}. Return findings and sources."
|
||||||
tools:
|
tools:
|
||||||
- web_search_loki
|
- web_search_coyote
|
||||||
- mcp:ddg-search
|
- mcp:ddg-search
|
||||||
output_schema:
|
output_schema:
|
||||||
type: object
|
type: object
|
||||||
@@ -226,13 +226,13 @@ nodes:
|
|||||||
# The script also receives these env vars (parity with bash tools called
|
# The script also receives these env vars (parity with bash tools called
|
||||||
# from normal agents):
|
# from normal agents):
|
||||||
# GRAPH_STATE / GRAPH_STATE_FILE state payload (one of the two is set)
|
# GRAPH_STATE / GRAPH_STATE_FILE state payload (one of the two is set)
|
||||||
# LLM_ROOT_DIR loki config dir
|
# LLM_ROOT_DIR coyote config dir
|
||||||
# LLM_PROMPT_UTILS_FILE path to .shared/prompt-utils.sh
|
# LLM_PROMPT_UTILS_FILE path to .shared/prompt-utils.sh
|
||||||
# LLM_AGENT_DATA_DIR this agent's data directory
|
# LLM_AGENT_DATA_DIR this agent's data directory
|
||||||
# LLM_AGENT_VAR_<NAME> one per declared `variables:` entry
|
# LLM_AGENT_VAR_<NAME> one per declared `variables:` entry
|
||||||
# PATH with loki's functions bin dir prepended
|
# PATH with coyote's functions bin dir prepended
|
||||||
# CLICOLOR_FORCE / FORCE_COLOR so child tools emit ANSI colors
|
# CLICOLOR_FORCE / FORCE_COLOR so child tools emit ANSI colors
|
||||||
# The script's working directory is loki's invocation CWD (not the agent
|
# The script's working directory is coyote's invocation CWD (not the agent
|
||||||
# directory), matching the behavior of bash tools.
|
# directory), matching the behavior of bash tools.
|
||||||
#
|
#
|
||||||
# This node fires once: after both `retrieve` and `web_search` finish.
|
# This node fires once: after both `retrieve` and `web_search` finish.
|
||||||
@@ -256,13 +256,13 @@ nodes:
|
|||||||
# targets.
|
# targets.
|
||||||
|
|
||||||
# --- agent node ---------------------------------------------------------
|
# --- agent node ---------------------------------------------------------
|
||||||
# Spawns a full Loki sub-agent and waits for it. The child uses its own
|
# Spawns a full Coyote sub-agent and waits for it. The child uses its own
|
||||||
# tool stack. Agent nodes have no `tools:` field. No schema hint is
|
# tool stack. Agent nodes have no `tools:` field. No schema hint is
|
||||||
# injected even when `output_schema` is set (unlike llm nodes).
|
# injected even when `output_schema` is set (unlike llm nodes).
|
||||||
deep_dive:
|
deep_dive:
|
||||||
id: deep_dive
|
id: deep_dive
|
||||||
type: agent
|
type: agent
|
||||||
agent: deep-research # Name of an existing Loki agent to spawn
|
agent: deep-research # Name of an existing Coyote agent to spawn
|
||||||
prompt: | # User message sent to the child (templated)
|
prompt: | # User message sent to the child (templated)
|
||||||
Research {{topic}} in depth. Existing context:
|
Research {{topic}} in depth. Existing context:
|
||||||
{{context}}
|
{{context}}
|
||||||
@@ -325,7 +325,7 @@ nodes:
|
|||||||
instructions: "Research one subject deeply for a {{audience}} audience."
|
instructions: "Research one subject deeply for a {{audience}} audience."
|
||||||
prompt: "Research {{subject}}: pull the key facts and one citation."
|
prompt: "Research {{subject}}: pull the key facts and one citation."
|
||||||
tools:
|
tools:
|
||||||
- web_search_loki
|
- web_search_coyote
|
||||||
# No `next:`, `state_updates:`, or `output_schema:` here. Map branches
|
# No `next:`, `state_updates:`, or `output_schema:` here. Map branches
|
||||||
# have a strict contract (see `subjects_map.branch` comment).
|
# have a strict contract (see `subjects_map.branch` comment).
|
||||||
|
|
||||||
@@ -348,7 +348,7 @@ nodes:
|
|||||||
instructions: "You write concise research summaries for a {{audience}} audience."
|
instructions: "You write concise research summaries for a {{audience}} audience."
|
||||||
prompt: "Summarize the topic {{topic}}, using your tools as needed."
|
prompt: "Summarize the topic {{topic}}, using your tools as needed."
|
||||||
tools: # Narrow whitelist: exactly these entries, nothing else
|
tools: # Narrow whitelist: exactly these entries, nothing else
|
||||||
- web_search_loki # an exact global-tool / custom-tool name
|
- web_search_coyote # an exact global-tool / custom-tool name
|
||||||
- mcp:ddg-search # `mcp:<server>` includes that server's functions
|
- mcp:ddg-search # `mcp:<server>` includes that server's functions
|
||||||
model: claude:claude-haiku-4-5 # Optional per-node model override
|
model: claude:claude-haiku-4-5 # Optional per-node model override
|
||||||
temperature: 0.3 # Optional per-node sampling override
|
temperature: 0.3 # Optional per-node sampling override
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ fmt:
|
|||||||
cargo fmt --all
|
cargo fmt --all
|
||||||
|
|
||||||
# Build the project for the current system architecture
|
# Build the project for the current system architecture
|
||||||
# (Gets stored at ./target/[debug|release]/loki)
|
# (Gets stored at ./target/[debug|release]/coyote)
|
||||||
[group: 'build']
|
[group: 'build']
|
||||||
[arg('build_type', pattern="debug|release")]
|
[arg('build_type', pattern="debug|release")]
|
||||||
build build_type='debug':
|
build build_type='debug':
|
||||||
|
|||||||
+88
-24
@@ -202,6 +202,24 @@
|
|||||||
# - https://ai.google.dev/api/rest/v1beta/models/streamGenerateContent
|
# - https://ai.google.dev/api/rest/v1beta/models/streamGenerateContent
|
||||||
- provider: gemini
|
- provider: gemini
|
||||||
models:
|
models:
|
||||||
|
- name: gemini-3.5-flash
|
||||||
|
max_input_tokens: 1048576
|
||||||
|
max_output_tokens: 65536
|
||||||
|
input_price: 0.2
|
||||||
|
output_price: 1.5
|
||||||
|
supports_function_calling: true
|
||||||
|
- name: gemini-3-flash-preview
|
||||||
|
max_input_tokens: 1048576
|
||||||
|
max_output_tokens: 65536
|
||||||
|
input_price: 0.2
|
||||||
|
output_price: 1.5
|
||||||
|
supports_function_calling: true
|
||||||
|
- name: gemini-3.1-flash-lite
|
||||||
|
max_input_tokens: 1048576
|
||||||
|
max_output_tokens: 65536
|
||||||
|
input_price: 0.2
|
||||||
|
output_price: 1.5
|
||||||
|
supports_function_calling: true
|
||||||
- name: gemini-3.1-pro-preview
|
- name: gemini-3.1-pro-preview
|
||||||
max_input_tokens: 1048576
|
max_input_tokens: 1048576
|
||||||
max_output_tokens: 65535
|
max_output_tokens: 65535
|
||||||
@@ -238,20 +256,6 @@
|
|||||||
max_input_tokens: 1048576
|
max_input_tokens: 1048576
|
||||||
supports_vision: true
|
supports_vision: true
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: gemini-2.0-flash
|
|
||||||
max_input_tokens: 1048576
|
|
||||||
max_output_tokens: 8192
|
|
||||||
input_price: 0
|
|
||||||
output_price: 0
|
|
||||||
supports_vision: true
|
|
||||||
supports_function_calling: true
|
|
||||||
- name: gemini-2.0-flash-lite
|
|
||||||
max_input_tokens: 1048576
|
|
||||||
max_output_tokens: 8192
|
|
||||||
input_price: 0
|
|
||||||
output_price: 0
|
|
||||||
supports_vision: true
|
|
||||||
supports_function_calling: true
|
|
||||||
- name: gemma-3-27b-it
|
- name: gemma-3-27b-it
|
||||||
max_input_tokens: 131072
|
max_input_tokens: 131072
|
||||||
max_output_tokens: 8192
|
max_output_tokens: 8192
|
||||||
@@ -269,6 +273,20 @@
|
|||||||
# - https://docs.anthropic.com/en/api/messages
|
# - https://docs.anthropic.com/en/api/messages
|
||||||
- provider: claude
|
- provider: claude
|
||||||
models:
|
models:
|
||||||
|
- name: claude-opus-4-8
|
||||||
|
max_input_tokens: 1000000
|
||||||
|
max_output_tokens: 128000
|
||||||
|
input_price: 5
|
||||||
|
output_price: 25
|
||||||
|
supports_vision: true
|
||||||
|
supports_function_calling: true
|
||||||
|
- name: claude-opus-4-7
|
||||||
|
max_input_tokens: 1000000
|
||||||
|
max_output_tokens: 128000
|
||||||
|
input_price: 5
|
||||||
|
output_price: 25
|
||||||
|
supports_vision: true
|
||||||
|
supports_function_calling: true
|
||||||
- name: claude-opus-4-6
|
- name: claude-opus-4-6
|
||||||
max_input_tokens: 200000
|
max_input_tokens: 200000
|
||||||
max_output_tokens: 8192
|
max_output_tokens: 8192
|
||||||
@@ -737,6 +755,24 @@
|
|||||||
# - https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/gemini
|
# - https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/gemini
|
||||||
- provider: vertexai
|
- provider: vertexai
|
||||||
models:
|
models:
|
||||||
|
- name: gemini-3.5-flash
|
||||||
|
max_input_tokens: 1048576
|
||||||
|
max_output_tokens: 65536
|
||||||
|
input_price: 0.2
|
||||||
|
output_price: 1.5
|
||||||
|
supports_function_calling: true
|
||||||
|
- name: gemini-3-flash-preview
|
||||||
|
max_input_tokens: 1048576
|
||||||
|
max_output_tokens: 65536
|
||||||
|
input_price: 0.2
|
||||||
|
output_price: 1.5
|
||||||
|
supports_function_calling: true
|
||||||
|
- name: gemini-3.1-flash-lite
|
||||||
|
max_input_tokens: 1048576
|
||||||
|
max_output_tokens: 65536
|
||||||
|
input_price: 0.2
|
||||||
|
output_price: 1.5
|
||||||
|
supports_function_calling: true
|
||||||
- name: gemini-3.1-pro-preview
|
- name: gemini-3.1-pro-preview
|
||||||
max_input_tokens: 1048576
|
max_input_tokens: 1048576
|
||||||
max_output_tokens: 65536
|
max_output_tokens: 65536
|
||||||
@@ -773,18 +809,18 @@
|
|||||||
max_input_tokens: 1048576
|
max_input_tokens: 1048576
|
||||||
supports_vision: true
|
supports_vision: true
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: gemini-2.0-flash-001
|
- name: claude-opus-4-8
|
||||||
max_input_tokens: 1048576
|
max_input_tokens: 1000000
|
||||||
max_output_tokens: 8192
|
max_output_tokens: 128000
|
||||||
input_price: 0.15
|
input_price: 5
|
||||||
output_price: 0.6
|
output_price: 25
|
||||||
supports_vision: true
|
supports_vision: true
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: gemini-2.0-flash-lite-001
|
- name: claude-opus-4-7
|
||||||
max_input_tokens: 1048576
|
max_input_tokens: 1000000
|
||||||
max_output_tokens: 8192
|
max_output_tokens: 128000
|
||||||
input_price: 0.075
|
input_price: 5
|
||||||
output_price: 0.3
|
output_price: 25
|
||||||
supports_vision: true
|
supports_vision: true
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: claude-opus-4-6
|
- name: claude-opus-4-6
|
||||||
@@ -942,6 +978,20 @@
|
|||||||
# - https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference-call.html
|
# - https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference-call.html
|
||||||
- provider: bedrock
|
- provider: bedrock
|
||||||
models:
|
models:
|
||||||
|
- name: us.anthropic.claude-opus-4-8
|
||||||
|
max_input_tokens: 1000000
|
||||||
|
max_output_tokens: 128000
|
||||||
|
input_price: 5
|
||||||
|
output_price: 25
|
||||||
|
supports_vision: true
|
||||||
|
supports_function_calling: true
|
||||||
|
- name: us.anthropic.claude-opus-4-7
|
||||||
|
max_input_tokens: 1000000
|
||||||
|
max_output_tokens: 128000
|
||||||
|
input_price: 5
|
||||||
|
output_price: 25
|
||||||
|
supports_vision: true
|
||||||
|
supports_function_calling: true
|
||||||
- name: us.anthropic.claude-opus-4-6-v1
|
- name: us.anthropic.claude-opus-4-6-v1
|
||||||
max_input_tokens: 200000
|
max_input_tokens: 200000
|
||||||
max_output_tokens: 8192
|
max_output_tokens: 8192
|
||||||
@@ -1568,6 +1618,20 @@
|
|||||||
max_input_tokens: 131072
|
max_input_tokens: 131072
|
||||||
input_price: 0.1
|
input_price: 0.1
|
||||||
output_price: 0.2
|
output_price: 0.2
|
||||||
|
- name: anthropic/claude-opus-4-8
|
||||||
|
max_input_tokens: 1000000
|
||||||
|
max_output_tokens: 128000
|
||||||
|
input_price: 5
|
||||||
|
output_price: 25
|
||||||
|
supports_vision: true
|
||||||
|
supports_function_calling: true
|
||||||
|
- name: anthropic/claude-opus-4-7
|
||||||
|
max_input_tokens: 1000000
|
||||||
|
max_output_tokens: 128000
|
||||||
|
input_price: 5
|
||||||
|
output_price: 25
|
||||||
|
supports_vision: true
|
||||||
|
supports_function_calling: true
|
||||||
- name: anthropic/claude-opus-4.6
|
- name: anthropic/claude-opus-4.6
|
||||||
max_input_tokens: 200000
|
max_input_tokens: 200000
|
||||||
max_output_tokens: 8192
|
max_output_tokens: 8192
|
||||||
|
|||||||
Generated
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "loki",
|
"name": "coyote",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {}
|
"packages": {}
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
<#
|
<#
|
||||||
loki installer (Windows/PowerShell 5+ and PowerShell 7)
|
coyote installer (Windows/PowerShell 5+ and PowerShell 7)
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
powershell -NoProfile -ExecutionPolicy Bypass -Command "iwr -useb https://raw.githubusercontent.com/Dark-Alex-17/loki/main/scripts/install_loki.ps1 | iex"
|
powershell -NoProfile -ExecutionPolicy Bypass -Command "iwr -useb https://raw.githubusercontent.com/Dark-Alex-17/coyote/main/scripts/install_coyote.ps1 | iex"
|
||||||
pwsh -c "irm https://raw.githubusercontent.com/Dark-Alex-17/loki/main/scripts/install_loki.ps1 | iex -Version vX.Y.Z"
|
pwsh -c "irm https://raw.githubusercontent.com/Dark-Alex-17/coyote/main/scripts/install_coyote.ps1 | iex -Version vX.Y.Z"
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
-Version <tag> (default: latest)
|
-Version <tag> (default: latest)
|
||||||
-BinDir <path> (default: %LOCALAPPDATA%\loki\bin on Windows; ~/.local/bin on *nix PowerShell)
|
-BinDir <path> (default: %LOCALAPPDATA%\coyote\bin on Windows; ~/.local/bin on *nix PowerShell)
|
||||||
#>
|
#>
|
||||||
|
|
||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
[string]$Version = $env:LOKI_VERSION,
|
[string]$Version = $env:COYOTE_VERSION,
|
||||||
[string]$BinDir = $env:BIN_DIR
|
[string]$BinDir = $env:BIN_DIR
|
||||||
)
|
)
|
||||||
|
|
||||||
$Repo = 'Dark-Alex-17/loki'
|
$Repo = 'Dark-Alex-17/coyote'
|
||||||
|
|
||||||
function Write-Info($msg) { Write-Host "[loki-install] $msg" }
|
function Write-Info($msg) { Write-Host "[coyote-install] $msg" }
|
||||||
function Fail($msg) { Write-Error $msg; exit 1 }
|
function Fail($msg) { Write-Error $msg; exit 1 }
|
||||||
|
|
||||||
Add-Type -AssemblyName System.Runtime
|
Add-Type -AssemblyName System.Runtime
|
||||||
@@ -38,7 +38,7 @@ switch ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (-not $BinDir) {
|
if (-not $BinDir) {
|
||||||
if ($isWin) { $BinDir = Join-Path $env:LOCALAPPDATA 'loki\bin' }
|
if ($isWin) { $BinDir = Join-Path $env:LOCALAPPDATA 'coyote\bin' }
|
||||||
else { $home = $env:HOME; if (-not $home) { $home = (Get-Item -Path ~).FullName }; $BinDir = Join-Path $home '.local/bin' }
|
else { $home = $env:HOME; if (-not $home) { $home = (Get-Item -Path ~).FullName }; $BinDir = Join-Path $home '.local/bin' }
|
||||||
}
|
}
|
||||||
New-Item -ItemType Directory -Force -Path $BinDir | Out-Null
|
New-Item -ItemType Directory -Force -Path $BinDir | Out-Null
|
||||||
@@ -49,23 +49,23 @@ $apiBase = "https://api.github.com/repos/$Repo/releases"
|
|||||||
$relUrl = if ($Version) { "$apiBase/tags/$Version" } else { "$apiBase/latest" }
|
$relUrl = if ($Version) { "$apiBase/tags/$Version" } else { "$apiBase/latest" }
|
||||||
Write-Info "Fetching release: $relUrl"
|
Write-Info "Fetching release: $relUrl"
|
||||||
try {
|
try {
|
||||||
$release = Invoke-RestMethod -UseBasicParsing -Headers @{ 'User-Agent' = 'loki-installer' } -Uri $relUrl -Method GET
|
$release = Invoke-RestMethod -UseBasicParsing -Headers @{ 'User-Agent' = 'coyote-installer' } -Uri $relUrl -Method GET
|
||||||
} catch { Fail "Failed to fetch release metadata. $_" }
|
} catch { Fail "Failed to fetch release metadata. $_" }
|
||||||
if (-not $release.assets) { Fail "No assets found in the release." }
|
if (-not $release.assets) { Fail "No assets found in the release." }
|
||||||
|
|
||||||
$candidates = @()
|
$candidates = @()
|
||||||
if ($os -eq 'windows') {
|
if ($os -eq 'windows') {
|
||||||
if ($arch -eq 'x86_64') { $candidates += 'loki-x86_64-pc-windows-msvc.zip' }
|
if ($arch -eq 'x86_64') { $candidates += 'coyote-x86_64-pc-windows-msvc.zip' }
|
||||||
else { $candidates += 'loki-aarch64-pc-windows-msvc.zip' }
|
else { $candidates += 'coyote-aarch64-pc-windows-msvc.zip' }
|
||||||
} elseif ($os -eq 'darwin') {
|
} elseif ($os -eq 'darwin') {
|
||||||
if ($arch -eq 'x86_64') { $candidates += 'loki-x86_64-apple-darwin.tar.gz' }
|
if ($arch -eq 'x86_64') { $candidates += 'coyote-x86_64-apple-darwin.tar.gz' }
|
||||||
else { $candidates += 'loki-aarch64-apple-darwin.tar.gz' }
|
else { $candidates += 'coyote-aarch64-apple-darwin.tar.gz' }
|
||||||
} elseif ($os -eq 'linux') {
|
} elseif ($os -eq 'linux') {
|
||||||
if ($arch -eq 'x86_64') {
|
if ($arch -eq 'x86_64') {
|
||||||
$candidates += 'loki-x86_64-unknown-linux-gnu.tar.gz'
|
$candidates += 'coyote-x86_64-unknown-linux-gnu.tar.gz'
|
||||||
$candidates += 'loki-x86_64-unknown-linux-musl.tar.gz'
|
$candidates += 'coyote-x86_64-unknown-linux-musl.tar.gz'
|
||||||
} else {
|
} else {
|
||||||
$candidates += 'loki-aarch64-unknown-linux-musl.tar.gz'
|
$candidates += 'coyote-aarch64-unknown-linux-musl.tar.gz'
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Fail "Unsupported OS for this installer: $os"
|
Fail "Unsupported OS for this installer: $os"
|
||||||
@@ -84,9 +84,9 @@ if (-not $asset) {
|
|||||||
Write-Info "Selected asset: $($asset.name)"
|
Write-Info "Selected asset: $($asset.name)"
|
||||||
Write-Info "Download URL: $($asset.browser_download_url)"
|
Write-Info "Download URL: $($asset.browser_download_url)"
|
||||||
|
|
||||||
$tmp = New-Item -ItemType Directory -Force -Path ([IO.Path]::Combine([IO.Path]::GetTempPath(), "loki-$(Get-Random)"))
|
$tmp = New-Item -ItemType Directory -Force -Path ([IO.Path]::Combine([IO.Path]::GetTempPath(), "coyote-$(Get-Random)"))
|
||||||
$archive = Join-Path $tmp.FullName 'asset'
|
$archive = Join-Path $tmp.FullName 'asset'
|
||||||
try { Invoke-WebRequest -UseBasicParsing -Headers @{ 'User-Agent' = 'loki-installer' } -Uri $asset.browser_download_url -OutFile $archive } catch { Fail "Failed to download asset. $_" }
|
try { Invoke-WebRequest -UseBasicParsing -Headers @{ 'User-Agent' = 'coyote-installer' } -Uri $asset.browser_download_url -OutFile $archive } catch { Fail "Failed to download asset. $_" }
|
||||||
|
|
||||||
$extractDir = Join-Path $tmp.FullName 'extract'; New-Item -ItemType Directory -Force -Path $extractDir | Out-Null
|
$extractDir = Join-Path $tmp.FullName 'extract'; New-Item -ItemType Directory -Force -Path $extractDir | Out-Null
|
||||||
|
|
||||||
@@ -107,14 +107,14 @@ if ($asset.name -match '\.zip$') {
|
|||||||
|
|
||||||
$bin = $null
|
$bin = $null
|
||||||
Get-ChildItem -Recurse -File $extractDir | ForEach-Object {
|
Get-ChildItem -Recurse -File $extractDir | ForEach-Object {
|
||||||
if ($isWin) { if ($_.Name -ieq 'loki.exe') { $bin = $_.FullName } }
|
if ($isWin) { if ($_.Name -ieq 'coyote.exe') { $bin = $_.FullName } }
|
||||||
else { if ($_.Name -ieq 'loki') { $bin = $_.FullName } }
|
else { if ($_.Name -ieq 'coyote') { $bin = $_.FullName } }
|
||||||
}
|
}
|
||||||
if (-not $bin) { Fail "Could not find loki binary inside the archive." }
|
if (-not $bin) { Fail "Could not find coyote binary inside the archive." }
|
||||||
|
|
||||||
if (-not $isWin) { try { & chmod +x -- $bin } catch {} }
|
if (-not $isWin) { try { & chmod +x -- $bin } catch {} }
|
||||||
|
|
||||||
$exec = if ($isWin) { 'loki.exe'} else { 'loki' }
|
$exec = if ($isWin) { 'coyote.exe'} else { 'coyote' }
|
||||||
$dest = Join-Path $BinDir $exec
|
$dest = Join-Path $BinDir $exec
|
||||||
Copy-Item -Force $bin $dest
|
Copy-Item -Force $bin $dest
|
||||||
Write-Info "Installed: $dest"
|
Write-Info "Installed: $dest"
|
||||||
@@ -135,5 +135,5 @@ if ($isWin) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Info "Done. Try: loki --help"
|
Write-Info "Done. Try: coyote --help"
|
||||||
|
|
||||||
@@ -1,23 +1,23 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# loki installer (Linux/macOS)
|
# coyote installer (Linux/macOS)
|
||||||
#
|
#
|
||||||
# Usage examples:
|
# Usage examples:
|
||||||
# curl -fsSL https://raw.githubusercontent.com/Dark-Alex-17/loki/main/scripts/install_loki.sh | bash
|
# curl -fsSL https://raw.githubusercontent.com/Dark-Alex-17/coyote/main/scripts/install_coyote.sh | bash
|
||||||
# curl -fsSL https://raw.githubusercontent.com/Dark-Alex-17/loki/main/scripts/install_loki.sh | bash -s -- --version vX.Y.Z
|
# curl -fsSL https://raw.githubusercontent.com/Dark-Alex-17/coyote/main/scripts/install_coyote.sh | bash -s -- --version vX.Y.Z
|
||||||
# BIN_DIR="$HOME/.local/bin" bash scripts/install_loki.sh
|
# BIN_DIR="$HOME/.local/bin" bash scripts/install_coyote.sh
|
||||||
#
|
#
|
||||||
# Flags / Env:
|
# Flags / Env:
|
||||||
# --version <tag> Release tag (default: latest). Or set LOKI_VERSION.
|
# --version <tag> Release tag (default: latest). Or set COYOTE_VERSION.
|
||||||
# --bin-dir <dir> Install directory (default: /usr/local/bin or ~/.local/bin). Or set BIN_DIR.
|
# --bin-dir <dir> Install directory (default: /usr/local/bin or ~/.local/bin). Or set BIN_DIR.
|
||||||
|
|
||||||
REPO="Dark-Alex-17/loki"
|
REPO="Dark-Alex-17/coyote"
|
||||||
VERSION="${LOKI_VERSION:-}"
|
VERSION="${COYOTE_VERSION:-}"
|
||||||
BIN_DIR="${BIN_DIR:-}"
|
BIN_DIR="${BIN_DIR:-}"
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
echo "loki installer (Linux/macOS)"
|
echo "coyote installer (Linux/macOS)"
|
||||||
echo
|
echo
|
||||||
echo "Options:"
|
echo "Options:"
|
||||||
echo " --version <tag> Release tag (default: latest)"
|
echo " --version <tag> Release tag (default: latest)"
|
||||||
@@ -44,7 +44,7 @@ fi
|
|||||||
mkdir -p "${BIN_DIR}"
|
mkdir -p "${BIN_DIR}"
|
||||||
|
|
||||||
log() {
|
log() {
|
||||||
echo "[loki-install] $*"
|
echo "[coyote-install] $*"
|
||||||
}
|
}
|
||||||
|
|
||||||
need_cmd() {
|
need_cmd() {
|
||||||
@@ -92,9 +92,9 @@ fi
|
|||||||
|
|
||||||
http_get() {
|
http_get() {
|
||||||
if [[ "$DL" == "curl" ]]; then
|
if [[ "$DL" == "curl" ]]; then
|
||||||
curl -fsSL -H 'User-Agent: loki-installer' "$1"
|
curl -fsSL -H 'User-Agent: coyote-installer' "$1"
|
||||||
else
|
else
|
||||||
wget -qO- --header='User-Agent: loki-installer' "$1"
|
wget -qO- --header='User-Agent: coyote-installer' "$1"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,9 +111,9 @@ fi
|
|||||||
ASSET_CANDIDATES=()
|
ASSET_CANDIDATES=()
|
||||||
if [[ "$OS" == "darwin" ]]; then
|
if [[ "$OS" == "darwin" ]]; then
|
||||||
if [[ "$ARCH" == "x86_64" ]]; then
|
if [[ "$ARCH" == "x86_64" ]]; then
|
||||||
ASSET_CANDIDATES+=("loki-x86_64-apple-darwin.tar.gz")
|
ASSET_CANDIDATES+=("coyote-x86_64-apple-darwin.tar.gz")
|
||||||
else
|
else
|
||||||
ASSET_CANDIDATES+=("loki-aarch64-apple-darwin.tar.gz")
|
ASSET_CANDIDATES+=("coyote-aarch64-apple-darwin.tar.gz")
|
||||||
fi
|
fi
|
||||||
elif [[ "$OS" == "linux" ]]; then
|
elif [[ "$OS" == "linux" ]]; then
|
||||||
if [[ "$ARCH" == "x86_64" ]]; then
|
if [[ "$ARCH" == "x86_64" ]]; then
|
||||||
@@ -122,12 +122,12 @@ elif [[ "$OS" == "linux" ]]; then
|
|||||||
if ldd --version 2>&1 | grep -qi glibc; then LIBC="gnu"; fi
|
if ldd --version 2>&1 | grep -qi glibc; then LIBC="gnu"; fi
|
||||||
|
|
||||||
if [[ "$LIBC" == "gnu" ]]; then
|
if [[ "$LIBC" == "gnu" ]]; then
|
||||||
ASSET_CANDIDATES+=("loki-x86_64-unknown-linux-gnu.tar.gz")
|
ASSET_CANDIDATES+=("coyote-x86_64-unknown-linux-gnu.tar.gz")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
ASSET_CANDIDATES+=("loki-x86_64-unknown-linux-musl.tar.gz")
|
ASSET_CANDIDATES+=("coyote-x86_64-unknown-linux-musl.tar.gz")
|
||||||
else
|
else
|
||||||
ASSET_CANDIDATES+=("loki-aarch64-unknown-linux-musl.tar.gz")
|
ASSET_CANDIDATES+=("coyote-aarch64-unknown-linux-musl.tar.gz")
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo "Error: unsupported OS for this installer: $OS" >&2; exit 1
|
echo "Error: unsupported OS for this installer: $OS" >&2; exit 1
|
||||||
@@ -170,9 +170,9 @@ log "Download URL: $ASSET_URL"
|
|||||||
|
|
||||||
ARCHIVE="$TMPDIR/asset"
|
ARCHIVE="$TMPDIR/asset"
|
||||||
if [[ "$DL" == "curl" ]]; then
|
if [[ "$DL" == "curl" ]]; then
|
||||||
curl -fL -H 'User-Agent: loki-installer' "$ASSET_URL" -o "$ARCHIVE"
|
curl -fL -H 'User-Agent: coyote-installer' "$ASSET_URL" -o "$ARCHIVE"
|
||||||
else
|
else
|
||||||
wget -q --header='User-Agent: loki-installer' "$ASSET_URL" -O "$ARCHIVE"
|
wget -q --header='User-Agent: coyote-installer' "$ASSET_URL" -O "$ARCHIVE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
WORK="$TMPDIR/work"; mkdir -p "$WORK"
|
WORK="$TMPDIR/work"; mkdir -p "$WORK"
|
||||||
@@ -192,21 +192,21 @@ fi
|
|||||||
BIN_PATH=""
|
BIN_PATH=""
|
||||||
while IFS= read -r -d '' f; do
|
while IFS= read -r -d '' f; do
|
||||||
base=$(basename "$f")
|
base=$(basename "$f")
|
||||||
if [[ "$base" == "loki" ]]; then
|
if [[ "$base" == "coyote" ]]; then
|
||||||
BIN_PATH="$f"
|
BIN_PATH="$f"
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
done < <(find "$EXTRACTED_DIR" -type f -print0)
|
done < <(find "$EXTRACTED_DIR" -type f -print0)
|
||||||
|
|
||||||
if [[ -z "$BIN_PATH" ]]; then
|
if [[ -z "$BIN_PATH" ]]; then
|
||||||
echo "Error: could not find 'loki' binary in the archive" >&2
|
echo "Error: could not find 'coyote' binary in the archive" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
chmod +x "$BIN_PATH"
|
chmod +x "$BIN_PATH"
|
||||||
install -m 0755 "$BIN_PATH" "${BIN_DIR}/loki"
|
install -m 0755 "$BIN_PATH" "${BIN_DIR}/coyote"
|
||||||
|
|
||||||
log "Installed: ${BIN_DIR}/loki"
|
log "Installed: ${BIN_DIR}/coyote"
|
||||||
|
|
||||||
case ":$PATH:" in
|
case ":$PATH:" in
|
||||||
*":${BIN_DIR}:"*) ;;
|
*":${BIN_DIR}:"*) ;;
|
||||||
@@ -216,5 +216,5 @@ case ":$PATH:" in
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
log "Done. Try: loki --help"
|
log "Done. Try: coyote --help"
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
_loki_bash() {
|
_coyote_bash() {
|
||||||
if [[ -n "$READLINE_LINE" ]]; then
|
if [[ -n "$READLINE_LINE" ]]; then
|
||||||
READLINE_LINE=$(loki -e "$READLINE_LINE")
|
READLINE_LINE=$(coyote -e "$READLINE_LINE")
|
||||||
READLINE_POINT=${#READLINE_LINE}
|
READLINE_POINT=${#READLINE_LINE}
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
bind -x '"\ee": _loki_bash'
|
bind -x '"\ee": _coyote_bash'
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
fn _loki_elvish {
|
fn _coyote_elvish {
|
||||||
var line = (edit:current-command)
|
var line = (edit:current-command)
|
||||||
var new-line = (loki -e $line)
|
var new-line = (coyote -e $line)
|
||||||
edit:replace-input $new-line
|
edit:replace-input $new-line
|
||||||
}
|
}
|
||||||
|
|
||||||
edit:insert:binding[Alt-e] = $_loki_elvish
|
edit:insert:binding[Alt-e] = $_coyote_elvish
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
function _loki_fish
|
function _coyote_fish
|
||||||
set -l _old (commandline)
|
set -l _old (commandline)
|
||||||
if test -n $_old
|
if test -n $_old
|
||||||
echo -n "⌛"
|
echo -n "⌛"
|
||||||
commandline -f repaint
|
commandline -f repaint
|
||||||
commandline (loki -e $_old)
|
commandline (coyote -e $_old)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
bind \ee _loki_fish
|
bind \ee _coyote_fish
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
def _loki_nushell [] {
|
def _coyote_nushell [] {
|
||||||
let _prev = (commandline)
|
let _prev = (commandline)
|
||||||
if ($_prev != "") {
|
if ($_prev != "") {
|
||||||
print '⌛'
|
print '⌛'
|
||||||
commandline edit -r (loki -e $_prev)
|
commandline edit -r (coyote -e $_prev)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$env.config.keybindings = ($env.config.keybindings | append {
|
$env.config.keybindings = ($env.config.keybindings | append {
|
||||||
name: loki_integration
|
name: coyote_integration
|
||||||
modifier: alt
|
modifier: alt
|
||||||
keycode: char_e
|
keycode: char_e
|
||||||
mode: [emacs, vi_insert]
|
mode: [emacs, vi_insert]
|
||||||
event:[
|
event:[
|
||||||
{
|
{
|
||||||
send: executehostcommand,
|
send: executehostcommand,
|
||||||
cmd: "_loki_nushell"
|
cmd: "_coyote_nushell"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ Set-PSReadLineKeyHandler -Chord "alt+e" -ScriptBlock {
|
|||||||
[Microsoft.PowerShell.PSConsoleReadline]::GetBufferState([ref]$_old, [ref]$null)
|
[Microsoft.PowerShell.PSConsoleReadline]::GetBufferState([ref]$_old, [ref]$null)
|
||||||
if ($_old) {
|
if ($_old) {
|
||||||
[Microsoft.PowerShell.PSConsoleReadLine]::Insert('⌛')
|
[Microsoft.PowerShell.PSConsoleReadLine]::Insert('⌛')
|
||||||
$_new = (loki -e $_old)
|
$_new = (coyote -e $_old)
|
||||||
[Microsoft.PowerShell.PSConsoleReadLine]::DeleteLine()
|
[Microsoft.PowerShell.PSConsoleReadLine]::DeleteLine()
|
||||||
[Microsoft.PowerShell.PSConsoleReadline]::Insert($_new)
|
[Microsoft.PowerShell.PSConsoleReadline]::Insert($_new)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
_loki_zsh() {
|
_coyote_zsh() {
|
||||||
if [[ -n "$BUFFER" ]]; then
|
if [[ -n "$BUFFER" ]]; then
|
||||||
local _old=$BUFFER
|
local _old=$BUFFER
|
||||||
BUFFER+="⌛"
|
BUFFER+="⌛"
|
||||||
zle -I && zle redisplay
|
zle -I && zle redisplay
|
||||||
BUFFER=$(loki -e "$_old")
|
BUFFER=$(coyote -e "$_old")
|
||||||
zle end-of-line
|
zle end-of-line
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
zle -N _loki_zsh
|
zle -N _coyote_zsh
|
||||||
bindkey '\ee' _loki_zsh
|
bindkey '\ee' _coyote_zsh
|
||||||
@@ -9,7 +9,7 @@ use std::env;
|
|||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
const LOKI_CLI_NAME: &str = "loki";
|
const COYOTE_CLI_NAME: &str = "coyote";
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, clap::ValueEnum)]
|
#[derive(Clone, Copy, Debug, clap::ValueEnum)]
|
||||||
pub enum ShellCompletion {
|
pub enum ShellCompletion {
|
||||||
@@ -24,12 +24,14 @@ pub enum ShellCompletion {
|
|||||||
impl ShellCompletion {
|
impl ShellCompletion {
|
||||||
pub fn generate_completions(self, cmd: &mut clap::Command) {
|
pub fn generate_completions(self, cmd: &mut clap::Command) {
|
||||||
match self {
|
match self {
|
||||||
Self::Bash => generate(Shell::Bash, cmd, LOKI_CLI_NAME, &mut io::stdout()),
|
Self::Bash => generate(Shell::Bash, cmd, COYOTE_CLI_NAME, &mut io::stdout()),
|
||||||
Self::Elvish => generate(Shell::Elvish, cmd, LOKI_CLI_NAME, &mut io::stdout()),
|
Self::Elvish => generate(Shell::Elvish, cmd, COYOTE_CLI_NAME, &mut io::stdout()),
|
||||||
Self::Fish => generate(Shell::Fish, cmd, LOKI_CLI_NAME, &mut io::stdout()),
|
Self::Fish => generate(Shell::Fish, cmd, COYOTE_CLI_NAME, &mut io::stdout()),
|
||||||
Self::PowerShell => generate(Shell::PowerShell, cmd, LOKI_CLI_NAME, &mut io::stdout()),
|
Self::PowerShell => {
|
||||||
Self::Zsh => generate(Shell::Zsh, cmd, LOKI_CLI_NAME, &mut io::stdout()),
|
generate(Shell::PowerShell, cmd, COYOTE_CLI_NAME, &mut io::stdout())
|
||||||
Self::Nushell => generate(Nushell, cmd, LOKI_CLI_NAME, &mut io::stdout()),
|
}
|
||||||
|
Self::Zsh => generate(Shell::Zsh, cmd, COYOTE_CLI_NAME, &mut io::stdout()),
|
||||||
|
Self::Nushell => generate(Nushell, cmd, COYOTE_CLI_NAME, &mut io::stdout()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-10
@@ -15,7 +15,7 @@ use std::io::{Read, stdin};
|
|||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(author, version, about, long_about = None)]
|
#[command(author, version, about, long_about = None)]
|
||||||
#[command(
|
#[command(
|
||||||
name = "loki",
|
name = "coyote",
|
||||||
author = crate_authors!(),
|
author = crate_authors!(),
|
||||||
version = crate_version!(),
|
version = crate_version!(),
|
||||||
about = crate_description!(),
|
about = crate_description!(),
|
||||||
@@ -116,6 +116,14 @@ pub struct Cli {
|
|||||||
/// List all macros
|
/// List all macros
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub list_macros: bool,
|
pub list_macros: bool,
|
||||||
|
/// List all installed skills
|
||||||
|
#[arg(long)]
|
||||||
|
pub list_skills: bool,
|
||||||
|
/// Pre-load an existing skill into the session (repeatable). If a single
|
||||||
|
/// `--skill <NAME>` is given and the skill doesn't exist, opens $EDITOR
|
||||||
|
/// with a scaffold to create it.
|
||||||
|
#[arg(long, value_name = "NAME")]
|
||||||
|
pub skill: Vec<String>,
|
||||||
/// Input text
|
/// Input text
|
||||||
#[arg(trailing_var_arg = true)]
|
#[arg(trailing_var_arg = true)]
|
||||||
text: Vec<String>,
|
text: Vec<String>,
|
||||||
@@ -125,19 +133,19 @@ pub struct Cli {
|
|||||||
/// Disable colored log output
|
/// Disable colored log output
|
||||||
#[arg(long, requires = "tail_logs")]
|
#[arg(long, requires = "tail_logs")]
|
||||||
pub disable_log_colors: bool,
|
pub disable_log_colors: bool,
|
||||||
/// Add a secret to the Loki vault
|
/// Add a secret to the Coyote vault
|
||||||
#[arg(long, value_name = "SECRET_NAME", exclusive = true)]
|
#[arg(long, value_name = "SECRET_NAME", exclusive = true)]
|
||||||
pub add_secret: Option<String>,
|
pub add_secret: Option<String>,
|
||||||
/// Decrypt a secret from the Loki vault and print the plaintext
|
/// Decrypt a secret from the Coyote vault and print the plaintext
|
||||||
#[arg(long, value_name = "SECRET_NAME", exclusive = true, add = ArgValueCompleter::new(secrets_completer))]
|
#[arg(long, value_name = "SECRET_NAME", exclusive = true, add = ArgValueCompleter::new(secrets_completer))]
|
||||||
pub get_secret: Option<String>,
|
pub get_secret: Option<String>,
|
||||||
/// Update an existing secret in the Loki vault
|
/// Update an existing secret in the Coyote vault
|
||||||
#[arg(long, value_name = "SECRET_NAME", exclusive = true, add = ArgValueCompleter::new(secrets_completer))]
|
#[arg(long, value_name = "SECRET_NAME", exclusive = true, add = ArgValueCompleter::new(secrets_completer))]
|
||||||
pub update_secret: Option<String>,
|
pub update_secret: Option<String>,
|
||||||
/// Delete a secret from the Loki vault
|
/// Delete a secret from the Coyote vault
|
||||||
#[arg(long, value_name = "SECRET_NAME", exclusive = true, add = ArgValueCompleter::new(secrets_completer))]
|
#[arg(long, value_name = "SECRET_NAME", exclusive = true, add = ArgValueCompleter::new(secrets_completer))]
|
||||||
pub delete_secret: Option<String>,
|
pub delete_secret: Option<String>,
|
||||||
/// List all secrets stored in the Loki vault
|
/// List all secrets stored in the Coyote vault
|
||||||
#[arg(long, exclusive = true)]
|
#[arg(long, exclusive = true)]
|
||||||
pub list_secrets: bool,
|
pub list_secrets: bool,
|
||||||
/// Authenticate with an LLM provider using OAuth (e.g., --authenticate client_name)
|
/// Authenticate with an LLM provider using OAuth (e.g., --authenticate client_name)
|
||||||
@@ -146,10 +154,10 @@ pub struct Cli {
|
|||||||
/// Generate static shell completion scripts
|
/// Generate static shell completion scripts
|
||||||
#[arg(long, value_name = "SHELL", value_enum)]
|
#[arg(long, value_name = "SHELL", value_enum)]
|
||||||
pub completions: Option<ShellCompletion>,
|
pub completions: Option<ShellCompletion>,
|
||||||
/// Update Loki to the latest release, or to a specific version
|
/// Update Coyote to the latest release, or to a specific version
|
||||||
#[arg(long, value_name = "VERSION")]
|
#[arg(long, value_name = "VERSION")]
|
||||||
pub update: Option<Option<String>>,
|
pub update: Option<Option<String>>,
|
||||||
/// With --update, update even if Loki was installed via a package manager
|
/// With --update, update even if Coyote was installed via a package manager
|
||||||
#[arg(long, requires = "update")]
|
#[arg(long, requires = "update")]
|
||||||
pub force: bool,
|
pub force: bool,
|
||||||
}
|
}
|
||||||
@@ -202,7 +210,7 @@ mod tests {
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
|
||||||
fn parse(args: &[&str]) -> Cli {
|
fn parse(args: &[&str]) -> Cli {
|
||||||
let mut full_args = vec!["loki"];
|
let mut full_args = vec!["coyote"];
|
||||||
full_args.extend_from_slice(args);
|
full_args.extend_from_slice(args);
|
||||||
Cli::try_parse_from(full_args).unwrap()
|
Cli::try_parse_from(full_args).unwrap()
|
||||||
}
|
}
|
||||||
@@ -298,6 +306,21 @@ mod tests {
|
|||||||
assert!(parse(&["--list-agents"]).list_agents);
|
assert!(parse(&["--list-agents"]).list_agents);
|
||||||
assert!(parse(&["--list-rags"]).list_rags);
|
assert!(parse(&["--list-rags"]).list_rags);
|
||||||
assert!(parse(&["--list-macros"]).list_macros);
|
assert!(parse(&["--list-macros"]).list_macros);
|
||||||
|
assert!(parse(&["--list-skills"]).list_skills);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_skill_flag_takes_name() {
|
||||||
|
assert_eq!(parse(&["--skill", "git-master"]).skill, vec!["git-master"]);
|
||||||
|
assert!(parse(&[]).skill.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_multiple_skill_flags_preserves_order() {
|
||||||
|
assert_eq!(
|
||||||
|
parse(&["--skill", "alpha", "--skill", "beta", "--skill", "gamma"]).skill,
|
||||||
|
vec!["alpha", "beta", "gamma"]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -436,6 +459,6 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_force_without_update_fails() {
|
fn parse_force_without_update_fails() {
|
||||||
assert!(Cli::try_parse_from(["loki", "--force"]).is_err());
|
assert!(Cli::try_parse_from(["coyote", "--force"]).is_err());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-29
@@ -85,7 +85,7 @@ async fn prepare_chat_completions(
|
|||||||
let ready = oauth::prepare_oauth_access_token(client, &provider, self_.name()).await?;
|
let ready = oauth::prepare_oauth_access_token(client, &provider, self_.name()).await?;
|
||||||
if !ready {
|
if !ready {
|
||||||
bail!(
|
bail!(
|
||||||
"OAuth configured but no tokens found for '{}'. Run: 'loki --authenticate {}' or '.authenticate' in the REPL",
|
"OAuth configured but no tokens found for '{}'. Run: 'coyote --authenticate {}' or '.authenticate' in the REPL",
|
||||||
self_.name(),
|
self_.name(),
|
||||||
self_.name()
|
self_.name()
|
||||||
);
|
);
|
||||||
@@ -100,7 +100,7 @@ async fn prepare_chat_completions(
|
|||||||
request_data.header("x-api-key", api_key);
|
request_data.header("x-api-key", api_key);
|
||||||
} else {
|
} else {
|
||||||
bail!(
|
bail!(
|
||||||
"No authentication configured for '{}'. Set `api_key` or use `auth: oauth` with `loki --authenticate {}`.",
|
"No authentication configured for '{}'. Set `api_key` or use `auth: oauth` with `coyote --authenticate {}`.",
|
||||||
self_.name(),
|
self_.name(),
|
||||||
self_.name()
|
self_.name()
|
||||||
);
|
);
|
||||||
@@ -114,41 +114,35 @@ async fn prepare_chat_completions(
|
|||||||
///
|
///
|
||||||
/// This behavior was discovered 2026-03-17.
|
/// This behavior was discovered 2026-03-17.
|
||||||
///
|
///
|
||||||
/// So this function injects the Claude Code system prompt into the request
|
/// The prefix must be in its **own** top-level system block. Concatenating it
|
||||||
/// body to make it a valid request.
|
/// with role / session content into a single block causes Anthropic to reject
|
||||||
|
/// the request with `rate_limit_error`. Any pre-existing system content is
|
||||||
|
/// preserved as additional blocks after the prefix.
|
||||||
fn inject_oauth_system_prompt(body: &mut Value) {
|
fn inject_oauth_system_prompt(body: &mut Value) {
|
||||||
let existing_text = match body.get("system") {
|
let existing_blocks: Vec<Value> = match body.get("system") {
|
||||||
Some(Value::String(s)) => {
|
Some(Value::String(s)) => {
|
||||||
if s.starts_with(CLAUDE_CODE_PREFIX) {
|
if s.is_empty() {
|
||||||
return;
|
Vec::new()
|
||||||
|
} else {
|
||||||
|
vec![json!({ "type": "text", "text": s })]
|
||||||
}
|
}
|
||||||
(!s.is_empty()).then(|| s.clone())
|
|
||||||
}
|
}
|
||||||
Some(Value::Array(blocks)) => {
|
Some(Value::Array(blocks)) => blocks.clone(),
|
||||||
let already_injected = blocks.iter().any(|b| {
|
_ => Vec::new(),
|
||||||
b.get("text")
|
};
|
||||||
.and_then(|t| t.as_str())
|
|
||||||
.map(|t| t.starts_with(CLAUDE_CODE_PREFIX))
|
let already_injected = existing_blocks
|
||||||
.unwrap_or(false)
|
.first()
|
||||||
});
|
.and_then(|b| b.get("text").and_then(|t| t.as_str()))
|
||||||
|
.map(|t| t == CLAUDE_CODE_PREFIX)
|
||||||
|
.unwrap_or(false);
|
||||||
if already_injected {
|
if already_injected {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let joined: Vec<String> = blocks
|
|
||||||
.iter()
|
|
||||||
.filter_map(|b| b.get("text").and_then(|t| t.as_str()).map(String::from))
|
|
||||||
.collect();
|
|
||||||
(!joined.is_empty()).then(|| joined.join("\n\n"))
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let merged = match existing_text {
|
let mut system = vec![json!({ "type": "text", "text": CLAUDE_CODE_PREFIX })];
|
||||||
Some(rest) => format!("{}\n\n{}", CLAUDE_CODE_PREFIX, rest),
|
system.extend(existing_blocks);
|
||||||
None => CLAUDE_CODE_PREFIX.to_string(),
|
body["system"] = Value::Array(system);
|
||||||
};
|
|
||||||
|
|
||||||
body["system"] = json!([{ "type": "text", "text": merged }]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn claude_chat_completions(
|
pub async fn claude_chat_completions(
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ async fn prepare_chat_completions(
|
|||||||
let ready = oauth::prepare_oauth_access_token(client, &provider, self_.name()).await?;
|
let ready = oauth::prepare_oauth_access_token(client, &provider, self_.name()).await?;
|
||||||
if !ready {
|
if !ready {
|
||||||
bail!(
|
bail!(
|
||||||
"OAuth configured but no tokens found for '{}'. Run: 'loki --authenticate {}' or '.authenticate' in the REPL",
|
"OAuth configured but no tokens found for '{}'. Run: 'coyote --authenticate {}' or '.authenticate' in the REPL",
|
||||||
self_.name(),
|
self_.name(),
|
||||||
self_.name()
|
self_.name()
|
||||||
);
|
);
|
||||||
@@ -122,7 +122,7 @@ async fn prepare_chat_completions(
|
|||||||
request_data.header("x-goog-api-key", api_key);
|
request_data.header("x-goog-api-key", api_key);
|
||||||
} else {
|
} else {
|
||||||
bail!(
|
bail!(
|
||||||
"No authentication configured for '{}'. Set `api_key` or use `auth: oauth` with `loki --authenticate {}`.",
|
"No authentication configured for '{}'. Set `api_key` or use `auth: oauth` with `coyote --authenticate {}`.",
|
||||||
self_.name(),
|
self_.name(),
|
||||||
self_.name()
|
self_.name()
|
||||||
);
|
);
|
||||||
@@ -181,7 +181,7 @@ async fn prepare_embeddings(
|
|||||||
let ready = oauth::prepare_oauth_access_token(client, &provider, self_.name()).await?;
|
let ready = oauth::prepare_oauth_access_token(client, &provider, self_.name()).await?;
|
||||||
if !ready {
|
if !ready {
|
||||||
bail!(
|
bail!(
|
||||||
"OAuth configured but no tokens found for '{}'. Run: 'loki --authenticate {}' or '.authenticate' in the REPL",
|
"OAuth configured but no tokens found for '{}'. Run: 'coyote --authenticate {}' or '.authenticate' in the REPL",
|
||||||
self_.name(),
|
self_.name(),
|
||||||
self_.name()
|
self_.name()
|
||||||
);
|
);
|
||||||
|
|||||||
+20
-1
@@ -207,6 +207,13 @@ impl Agent {
|
|||||||
functions.append_teammate_functions();
|
functions.append_teammate_functions();
|
||||||
functions.append_user_interaction_functions();
|
functions.append_user_interaction_functions();
|
||||||
|
|
||||||
|
if app.function_calling_support
|
||||||
|
&& app.skills_enabled
|
||||||
|
&& !matches!(agent_config.skills_enabled, Some(false))
|
||||||
|
{
|
||||||
|
functions.append_skill_functions();
|
||||||
|
}
|
||||||
|
|
||||||
agent_config.replace_tools_placeholder(&functions);
|
agent_config.replace_tools_placeholder(&functions);
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
@@ -337,6 +344,14 @@ impl Agent {
|
|||||||
&self.config.mcp_servers
|
&self.config.mcp_servers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn skills_enabled(&self) -> Option<bool> {
|
||||||
|
self.config.skills_enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enabled_skills(&self) -> Option<&[String]> {
|
||||||
|
self.config.enabled_skills.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn conversation_starters(&self) -> Vec<String> {
|
pub fn conversation_starters(&self) -> Vec<String> {
|
||||||
self.config
|
self.config
|
||||||
.conversation_starters
|
.conversation_starters
|
||||||
@@ -526,7 +541,7 @@ impl RoleLike for Agent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn enabled_tools(&self) -> Option<String> {
|
fn enabled_tools(&self) -> Option<String> {
|
||||||
self.config.global_tools.clone().join(",").into()
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn enabled_mcp_servers(&self) -> Option<String> {
|
fn enabled_mcp_servers(&self) -> Option<String> {
|
||||||
@@ -615,6 +630,10 @@ pub struct AgentConfig {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub global_tools: Vec<String>,
|
pub global_tools: Vec<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub skills_enabled: Option<bool>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub enabled_skills: Option<Vec<String>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub continuation_prompt: Option<String>,
|
pub continuation_prompt: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub instructions: String,
|
pub instructions: String,
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ pub struct AppConfig {
|
|||||||
pub enabled_tools: Option<String>,
|
pub enabled_tools: Option<String>,
|
||||||
pub visible_tools: Option<Vec<String>>,
|
pub visible_tools: Option<Vec<String>>,
|
||||||
|
|
||||||
|
pub skills_enabled: bool,
|
||||||
|
pub enabled_skills: Option<String>,
|
||||||
|
pub visible_skills: Option<Vec<String>>,
|
||||||
|
|
||||||
pub mcp_server_support: bool,
|
pub mcp_server_support: bool,
|
||||||
pub mapping_mcp_servers: IndexMap<String, String>,
|
pub mapping_mcp_servers: IndexMap<String, String>,
|
||||||
pub enabled_mcp_servers: Option<String>,
|
pub enabled_mcp_servers: Option<String>,
|
||||||
@@ -96,6 +100,10 @@ impl Default for AppConfig {
|
|||||||
enabled_tools: None,
|
enabled_tools: None,
|
||||||
visible_tools: None,
|
visible_tools: None,
|
||||||
|
|
||||||
|
skills_enabled: true,
|
||||||
|
enabled_skills: None,
|
||||||
|
visible_skills: None,
|
||||||
|
|
||||||
mcp_server_support: true,
|
mcp_server_support: true,
|
||||||
mapping_mcp_servers: Default::default(),
|
mapping_mcp_servers: Default::default(),
|
||||||
enabled_mcp_servers: None,
|
enabled_mcp_servers: None,
|
||||||
@@ -158,6 +166,10 @@ impl AppConfig {
|
|||||||
enabled_tools: config.enabled_tools,
|
enabled_tools: config.enabled_tools,
|
||||||
visible_tools: config.visible_tools,
|
visible_tools: config.visible_tools,
|
||||||
|
|
||||||
|
skills_enabled: config.skills_enabled,
|
||||||
|
enabled_skills: config.enabled_skills,
|
||||||
|
visible_skills: config.visible_skills,
|
||||||
|
|
||||||
mcp_server_support: config.mcp_server_support,
|
mcp_server_support: config.mcp_server_support,
|
||||||
mapping_mcp_servers: config.mapping_mcp_servers,
|
mapping_mcp_servers: config.mapping_mcp_servers,
|
||||||
enabled_mcp_servers: config.enabled_mcp_servers,
|
enabled_mcp_servers: config.enabled_mcp_servers,
|
||||||
@@ -379,6 +391,14 @@ impl AppConfig {
|
|||||||
self.enabled_tools = v;
|
self.enabled_tools = v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(Some(v)) = super::read_env_bool(&get_env_name("skills_enabled")) {
|
||||||
|
self.skills_enabled = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(v) = super::read_env_value::<String>(&get_env_name("enabled_skills")) {
|
||||||
|
self.enabled_skills = v;
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(Some(v)) = super::read_env_bool(&get_env_name("mcp_server_support")) {
|
if let Some(Some(v)) = super::read_env_bool(&get_env_name("mcp_server_support")) {
|
||||||
self.mcp_server_support = v;
|
self.mcp_server_support = v;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -879,7 +879,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn from_files_loads_single_text_file() {
|
fn from_files_loads_single_text_file() {
|
||||||
let dir = env::temp_dir().join(format!(
|
let dir = env::temp_dir().join(format!(
|
||||||
"loki-input-test-{}",
|
"coyote-input-test-{}",
|
||||||
SystemTime::now()
|
SystemTime::now()
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -906,7 +906,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn from_files_loads_multiple_files() {
|
fn from_files_loads_multiple_files() {
|
||||||
let dir = env::temp_dir().join(format!(
|
let dir = env::temp_dir().join(format!(
|
||||||
"loki-input-test-multi-{}",
|
"coyote-input-test-multi-{}",
|
||||||
SystemTime::now()
|
SystemTime::now()
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ pub fn install_remote(git_url: &str, filter: Option<InstallFilter>, force: bool)
|
|||||||
if layout.is_empty() {
|
if layout.is_empty() {
|
||||||
println!(
|
println!(
|
||||||
"No recognized assets found in {git_url}. Expected one or more of: \
|
"No recognized assets found in {git_url}. Expected one or more of: \
|
||||||
agents/, roles/, macros/, functions/tools/, functions/mcp.json"
|
agents/, roles/, skills/, macros/, functions/tools/, functions/mcp.json"
|
||||||
);
|
);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -136,7 +136,7 @@ impl Drop for TempRepoDir {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn clone_to_temp(url: &str, reference: Option<&str>) -> Result<TempRepoDir> {
|
fn clone_to_temp(url: &str, reference: Option<&str>) -> Result<TempRepoDir> {
|
||||||
let dest = utils::temp_file("loki-remote-install-", "");
|
let dest = utils::temp_file("coyote-remote-install-", "");
|
||||||
let dest_arg: OsString = dest.as_os_str().into();
|
let dest_arg: OsString = dest.as_os_str().into();
|
||||||
|
|
||||||
let is_sha = reference
|
let is_sha = reference
|
||||||
@@ -193,6 +193,7 @@ fn run_git(args: Vec<OsString>) -> Result<()> {
|
|||||||
struct RemoteLayout {
|
struct RemoteLayout {
|
||||||
agents: Option<PathBuf>,
|
agents: Option<PathBuf>,
|
||||||
roles: Option<PathBuf>,
|
roles: Option<PathBuf>,
|
||||||
|
skills: Option<PathBuf>,
|
||||||
macros: Option<PathBuf>,
|
macros: Option<PathBuf>,
|
||||||
functions_tools: Option<PathBuf>,
|
functions_tools: Option<PathBuf>,
|
||||||
mcp_json: Option<PathBuf>,
|
mcp_json: Option<PathBuf>,
|
||||||
@@ -202,6 +203,7 @@ impl RemoteLayout {
|
|||||||
fn is_empty(&self) -> bool {
|
fn is_empty(&self) -> bool {
|
||||||
self.agents.is_none()
|
self.agents.is_none()
|
||||||
&& self.roles.is_none()
|
&& self.roles.is_none()
|
||||||
|
&& self.skills.is_none()
|
||||||
&& self.macros.is_none()
|
&& self.macros.is_none()
|
||||||
&& self.functions_tools.is_none()
|
&& self.functions_tools.is_none()
|
||||||
&& self.mcp_json.is_none()
|
&& self.mcp_json.is_none()
|
||||||
@@ -215,20 +217,29 @@ fn scan_remote_layout(root: &Path) -> Result<RemoteLayout> {
|
|||||||
if agents.is_dir() {
|
if agents.is_dir() {
|
||||||
layout.agents = Some(agents);
|
layout.agents = Some(agents);
|
||||||
}
|
}
|
||||||
|
|
||||||
let roles = root.join("roles");
|
let roles = root.join("roles");
|
||||||
if roles.is_dir() {
|
if roles.is_dir() {
|
||||||
layout.roles = Some(roles);
|
layout.roles = Some(roles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let skills = root.join("skills");
|
||||||
|
if skills.is_dir() {
|
||||||
|
layout.skills = Some(skills);
|
||||||
|
}
|
||||||
|
|
||||||
let macros = root.join("macros");
|
let macros = root.join("macros");
|
||||||
if macros.is_dir() {
|
if macros.is_dir() {
|
||||||
layout.macros = Some(macros);
|
layout.macros = Some(macros);
|
||||||
}
|
}
|
||||||
|
|
||||||
let functions = root.join("functions");
|
let functions = root.join("functions");
|
||||||
if functions.is_dir() {
|
if functions.is_dir() {
|
||||||
let tools = functions.join("tools");
|
let tools = functions.join("tools");
|
||||||
if tools.is_dir() {
|
if tools.is_dir() {
|
||||||
layout.functions_tools = Some(tools);
|
layout.functions_tools = Some(tools);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mcp = functions.join("mcp.json");
|
let mcp = functions.join("mcp.json");
|
||||||
if mcp.is_file() {
|
if mcp.is_file() {
|
||||||
layout.mcp_json = Some(mcp);
|
layout.mcp_json = Some(mcp);
|
||||||
@@ -251,6 +262,10 @@ fn apply_filter(mut layout: RemoteLayout, filter: Option<InstallFilter>) -> Remo
|
|||||||
roles: layout.roles.take(),
|
roles: layout.roles.take(),
|
||||||
..RemoteLayout::default()
|
..RemoteLayout::default()
|
||||||
},
|
},
|
||||||
|
InstallFilter::Skills => RemoteLayout {
|
||||||
|
skills: layout.skills.take(),
|
||||||
|
..RemoteLayout::default()
|
||||||
|
},
|
||||||
InstallFilter::Macros => RemoteLayout {
|
InstallFilter::Macros => RemoteLayout {
|
||||||
macros: layout.macros.take(),
|
macros: layout.macros.take(),
|
||||||
..RemoteLayout::default()
|
..RemoteLayout::default()
|
||||||
@@ -308,6 +323,7 @@ fn walk_files_inner(dir: &Path, out: &mut Vec<PathBuf>) -> Result<()> {
|
|||||||
enum TopCategory {
|
enum TopCategory {
|
||||||
Agents,
|
Agents,
|
||||||
Roles,
|
Roles,
|
||||||
|
Skills,
|
||||||
Macros,
|
Macros,
|
||||||
FunctionsTools,
|
FunctionsTools,
|
||||||
}
|
}
|
||||||
@@ -317,6 +333,7 @@ impl TopCategory {
|
|||||||
match self {
|
match self {
|
||||||
TopCategory::Agents => "agents",
|
TopCategory::Agents => "agents",
|
||||||
TopCategory::Roles => "roles",
|
TopCategory::Roles => "roles",
|
||||||
|
TopCategory::Skills => "skills",
|
||||||
TopCategory::Macros => "macros",
|
TopCategory::Macros => "macros",
|
||||||
TopCategory::FunctionsTools => "functions/tools",
|
TopCategory::FunctionsTools => "functions/tools",
|
||||||
}
|
}
|
||||||
@@ -356,6 +373,16 @@ fn plan_changes(layout: &RemoteLayout) -> Result<InstallPlan> {
|
|||||||
if let Some(src_dir) = &layout.roles {
|
if let Some(src_dir) = &layout.roles {
|
||||||
plan_dir_into(src_dir, &paths::roles_dir(), TopCategory::Roles, &mut files)?;
|
plan_dir_into(src_dir, &paths::roles_dir(), TopCategory::Roles, &mut files)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(src_dir) = &layout.skills {
|
||||||
|
plan_dir_into(
|
||||||
|
src_dir,
|
||||||
|
&paths::skills_dir(),
|
||||||
|
TopCategory::Skills,
|
||||||
|
&mut files,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(src_dir) = &layout.macros {
|
if let Some(src_dir) = &layout.macros {
|
||||||
plan_dir_into(
|
plan_dir_into(
|
||||||
src_dir,
|
src_dir,
|
||||||
@@ -457,6 +484,7 @@ fn print_plan_summary(plan: &InstallPlan) {
|
|||||||
for cat in [
|
for cat in [
|
||||||
TopCategory::Agents,
|
TopCategory::Agents,
|
||||||
TopCategory::Roles,
|
TopCategory::Roles,
|
||||||
|
TopCategory::Skills,
|
||||||
TopCategory::Macros,
|
TopCategory::Macros,
|
||||||
TopCategory::FunctionsTools,
|
TopCategory::FunctionsTools,
|
||||||
] {
|
] {
|
||||||
@@ -875,7 +903,7 @@ fn print_secret_summary(added: &[String], deferred: &[String]) {
|
|||||||
if !deferred.is_empty() {
|
if !deferred.is_empty() {
|
||||||
println!(
|
println!(
|
||||||
"\nThe following secrets are still required by your MCP servers. \
|
"\nThe following secrets are still required by your MCP servers. \
|
||||||
Add them with `loki --add-secret <NAME>` or `.vault add <NAME>` in the REPL:"
|
Add them with `coyote --add-secret <NAME>` or `.vault add <NAME>` in the REPL:"
|
||||||
);
|
);
|
||||||
for name in deferred {
|
for name in deferred {
|
||||||
println!(" {{{{ {name} }}}}");
|
println!(" {{{{ {name} }}}}");
|
||||||
@@ -982,6 +1010,7 @@ mod tests {
|
|||||||
let l = RemoteLayout {
|
let l = RemoteLayout {
|
||||||
agents: Some(PathBuf::from("a")),
|
agents: Some(PathBuf::from("a")),
|
||||||
roles: Some(PathBuf::from("r")),
|
roles: Some(PathBuf::from("r")),
|
||||||
|
skills: Some(PathBuf::from("s")),
|
||||||
macros: Some(PathBuf::from("m")),
|
macros: Some(PathBuf::from("m")),
|
||||||
functions_tools: Some(PathBuf::from("f")),
|
functions_tools: Some(PathBuf::from("f")),
|
||||||
mcp_json: Some(PathBuf::from("j")),
|
mcp_json: Some(PathBuf::from("j")),
|
||||||
@@ -989,8 +1018,8 @@ mod tests {
|
|||||||
|
|
||||||
let out = apply_filter(l, None);
|
let out = apply_filter(l, None);
|
||||||
|
|
||||||
assert!(out.agents.is_some() && out.roles.is_some() && out.macros.is_some());
|
assert!(out.agents.is_some() && out.roles.is_some() && out.skills.is_some());
|
||||||
assert!(out.functions_tools.is_some() && out.mcp_json.is_some());
|
assert!(out.macros.is_some() && out.functions_tools.is_some() && out.mcp_json.is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -998,6 +1027,7 @@ mod tests {
|
|||||||
let l = RemoteLayout {
|
let l = RemoteLayout {
|
||||||
agents: Some(PathBuf::from("a")),
|
agents: Some(PathBuf::from("a")),
|
||||||
roles: None,
|
roles: None,
|
||||||
|
skills: Some(PathBuf::from("s")),
|
||||||
macros: None,
|
macros: None,
|
||||||
functions_tools: Some(PathBuf::from("f")),
|
functions_tools: Some(PathBuf::from("f")),
|
||||||
mcp_json: Some(PathBuf::from("j")),
|
mcp_json: Some(PathBuf::from("j")),
|
||||||
@@ -1006,6 +1036,7 @@ mod tests {
|
|||||||
let out = apply_filter(l, Some(InstallFilter::Functions));
|
let out = apply_filter(l, Some(InstallFilter::Functions));
|
||||||
|
|
||||||
assert!(out.agents.is_none());
|
assert!(out.agents.is_none());
|
||||||
|
assert!(out.skills.is_none());
|
||||||
assert_eq!(out.functions_tools, Some(PathBuf::from("f")));
|
assert_eq!(out.functions_tools, Some(PathBuf::from("f")));
|
||||||
assert!(out.mcp_json.is_none());
|
assert!(out.mcp_json.is_none());
|
||||||
}
|
}
|
||||||
@@ -1015,6 +1046,7 @@ mod tests {
|
|||||||
let l = RemoteLayout {
|
let l = RemoteLayout {
|
||||||
agents: Some(PathBuf::from("a")),
|
agents: Some(PathBuf::from("a")),
|
||||||
roles: None,
|
roles: None,
|
||||||
|
skills: Some(PathBuf::from("s")),
|
||||||
macros: None,
|
macros: None,
|
||||||
functions_tools: Some(PathBuf::from("f")),
|
functions_tools: Some(PathBuf::from("f")),
|
||||||
mcp_json: Some(PathBuf::from("j")),
|
mcp_json: Some(PathBuf::from("j")),
|
||||||
@@ -1022,7 +1054,7 @@ mod tests {
|
|||||||
|
|
||||||
let out = apply_filter(l, Some(InstallFilter::McpConfig));
|
let out = apply_filter(l, Some(InstallFilter::McpConfig));
|
||||||
|
|
||||||
assert!(out.agents.is_none() && out.functions_tools.is_none());
|
assert!(out.agents.is_none() && out.skills.is_none() && out.functions_tools.is_none());
|
||||||
assert_eq!(out.mcp_json, Some(PathBuf::from("j")));
|
assert_eq!(out.mcp_json, Some(PathBuf::from("j")));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1031,6 +1063,7 @@ mod tests {
|
|||||||
let l = RemoteLayout {
|
let l = RemoteLayout {
|
||||||
agents: Some(PathBuf::from("a")),
|
agents: Some(PathBuf::from("a")),
|
||||||
roles: Some(PathBuf::from("r")),
|
roles: Some(PathBuf::from("r")),
|
||||||
|
skills: Some(PathBuf::from("s")),
|
||||||
macros: Some(PathBuf::from("m")),
|
macros: Some(PathBuf::from("m")),
|
||||||
functions_tools: Some(PathBuf::from("f")),
|
functions_tools: Some(PathBuf::from("f")),
|
||||||
mcp_json: Some(PathBuf::from("j")),
|
mcp_json: Some(PathBuf::from("j")),
|
||||||
@@ -1039,7 +1072,25 @@ mod tests {
|
|||||||
let out = apply_filter(l, Some(InstallFilter::Roles));
|
let out = apply_filter(l, Some(InstallFilter::Roles));
|
||||||
|
|
||||||
assert_eq!(out.roles, Some(PathBuf::from("r")));
|
assert_eq!(out.roles, Some(PathBuf::from("r")));
|
||||||
assert!(out.agents.is_none() && out.macros.is_none());
|
assert!(out.agents.is_none() && out.skills.is_none() && out.macros.is_none());
|
||||||
|
assert!(out.functions_tools.is_none() && out.mcp_json.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn apply_filter_skills_keeps_only_skills() {
|
||||||
|
let l = RemoteLayout {
|
||||||
|
agents: Some(PathBuf::from("a")),
|
||||||
|
roles: Some(PathBuf::from("r")),
|
||||||
|
skills: Some(PathBuf::from("s")),
|
||||||
|
macros: Some(PathBuf::from("m")),
|
||||||
|
functions_tools: Some(PathBuf::from("f")),
|
||||||
|
mcp_json: Some(PathBuf::from("j")),
|
||||||
|
};
|
||||||
|
|
||||||
|
let out = apply_filter(l, Some(InstallFilter::Skills));
|
||||||
|
|
||||||
|
assert_eq!(out.skills, Some(PathBuf::from("s")));
|
||||||
|
assert!(out.agents.is_none() && out.roles.is_none() && out.macros.is_none());
|
||||||
assert!(out.functions_tools.is_none() && out.mcp_json.is_none());
|
assert!(out.functions_tools.is_none() && out.mcp_json.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1084,8 +1135,10 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn scan_remote_layout_finds_known_subdirs() {
|
fn scan_remote_layout_finds_known_subdirs() {
|
||||||
let root = fresh_temp_dir("scan-test-");
|
let root = fresh_temp_dir("scan-test-");
|
||||||
|
|
||||||
fs::create_dir_all(root.join("agents/sample")).unwrap();
|
fs::create_dir_all(root.join("agents/sample")).unwrap();
|
||||||
fs::create_dir_all(root.join("roles")).unwrap();
|
fs::create_dir_all(root.join("roles")).unwrap();
|
||||||
|
fs::create_dir_all(root.join("skills")).unwrap();
|
||||||
fs::create_dir_all(root.join("macros")).unwrap();
|
fs::create_dir_all(root.join("macros")).unwrap();
|
||||||
fs::create_dir_all(root.join("functions/tools")).unwrap();
|
fs::create_dir_all(root.join("functions/tools")).unwrap();
|
||||||
touch(&root.join("functions/mcp.json"));
|
touch(&root.join("functions/mcp.json"));
|
||||||
@@ -1094,12 +1147,30 @@ mod tests {
|
|||||||
let layout = scan_remote_layout(&root).unwrap();
|
let layout = scan_remote_layout(&root).unwrap();
|
||||||
assert!(layout.agents.is_some());
|
assert!(layout.agents.is_some());
|
||||||
assert!(layout.roles.is_some());
|
assert!(layout.roles.is_some());
|
||||||
|
assert!(layout.skills.is_some());
|
||||||
assert!(layout.macros.is_some());
|
assert!(layout.macros.is_some());
|
||||||
assert!(layout.functions_tools.is_some());
|
assert!(layout.functions_tools.is_some());
|
||||||
assert!(layout.mcp_json.is_some());
|
assert!(layout.mcp_json.is_some());
|
||||||
let _ = fs::remove_dir_all(&root);
|
let _ = fs::remove_dir_all(&root);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scan_remote_layout_finds_skills_only() {
|
||||||
|
let root = fresh_temp_dir("scan-skills-only-");
|
||||||
|
fs::create_dir_all(root.join("skills/git-master")).unwrap();
|
||||||
|
touch(&root.join("skills/git-master/SKILL.md"));
|
||||||
|
|
||||||
|
let layout = scan_remote_layout(&root).unwrap();
|
||||||
|
|
||||||
|
assert!(layout.skills.is_some());
|
||||||
|
assert!(layout.agents.is_none());
|
||||||
|
assert!(layout.roles.is_none());
|
||||||
|
assert!(layout.macros.is_none());
|
||||||
|
assert!(layout.functions_tools.is_none());
|
||||||
|
assert!(layout.mcp_json.is_none());
|
||||||
|
let _ = fs::remove_dir_all(&root);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scan_remote_layout_ignores_unrelated_files() {
|
fn scan_remote_layout_ignores_unrelated_files() {
|
||||||
let root = fresh_temp_dir("scan-unrelated-");
|
let root = fresh_temp_dir("scan-unrelated-");
|
||||||
@@ -1223,6 +1294,12 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn merge_non_tty_conflict_aborts_without_force() {
|
fn merge_non_tty_conflict_aborts_without_force() {
|
||||||
|
if *IS_STDOUT_TERMINAL {
|
||||||
|
eprintln!(
|
||||||
|
"Skipping merge_non_tty_conflict_aborts_without_force: requires non-TTY stdout"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
let dir = fresh_temp_dir("merge-non-tty-");
|
let dir = fresh_temp_dir("merge-non-tty-");
|
||||||
let remote = dir.join("remote.json");
|
let remote = dir.join("remote.json");
|
||||||
let target = dir.join("target.json");
|
let target = dir.join("target.json");
|
||||||
@@ -1265,12 +1342,12 @@ mod tests {
|
|||||||
let target = dir.join("target.json");
|
let target = dir.join("target.json");
|
||||||
write_mcp(
|
write_mcp(
|
||||||
&remote,
|
&remote,
|
||||||
r#"{"mcpServers": {"x": {"type":"stdio","command":"echo","env":{"K":"{{LOKI_TEST_MERGE_SECRET}}"}}}}"#,
|
r#"{"mcpServers": {"x": {"type":"stdio","command":"echo","env":{"K":"{{COYOTE_TEST_MERGE_SECRET}}"}}}}"#,
|
||||||
);
|
);
|
||||||
|
|
||||||
let report = merge_mcp_json(None, &remote, &target, false).unwrap();
|
let report = merge_mcp_json(None, &remote, &target, false).unwrap();
|
||||||
|
|
||||||
assert_eq!(report.missing_secrets, vec!["LOKI_TEST_MERGE_SECRET"]);
|
assert_eq!(report.missing_secrets, vec!["COYOTE_TEST_MERGE_SECRET"]);
|
||||||
let _ = fs::remove_dir_all(&dir);
|
let _ = fs::remove_dir_all(&dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1299,9 +1376,15 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn handle_missing_secrets_defers_all_in_non_tty() {
|
fn handle_missing_secrets_defers_all_in_non_tty() {
|
||||||
|
if *IS_STDOUT_TERMINAL {
|
||||||
|
eprintln!(
|
||||||
|
"Skipping handle_missing_secrets_defers_all_in_non_tty: requires non-TTY stdout"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
let missing = vec![
|
let missing = vec![
|
||||||
"LOKI_TEST_STEP4_A".to_string(),
|
"COYOTE_TEST_STEP4_A".to_string(),
|
||||||
"LOKI_TEST_STEP4_B".to_string(),
|
"COYOTE_TEST_STEP4_B".to_string(),
|
||||||
];
|
];
|
||||||
|
|
||||||
assert!(handle_missing_secrets(&missing).is_ok());
|
assert!(handle_missing_secrets(&missing).is_ok());
|
||||||
|
|||||||
+37
-5
@@ -11,6 +11,9 @@ mod rag_cache;
|
|||||||
mod request_context;
|
mod request_context;
|
||||||
mod role;
|
mod role;
|
||||||
mod session;
|
mod session;
|
||||||
|
mod skill;
|
||||||
|
mod skill_policy;
|
||||||
|
mod skill_registry;
|
||||||
pub(crate) mod todo;
|
pub(crate) mod todo;
|
||||||
mod tool_scope;
|
mod tool_scope;
|
||||||
mod update;
|
mod update;
|
||||||
@@ -30,6 +33,12 @@ pub use self::role::{
|
|||||||
CODE_ROLE, CREATE_TITLE_ROLE, EXPLAIN_SHELL_ROLE, Role, RoleLike, SHELL_ROLE,
|
CODE_ROLE, CREATE_TITLE_ROLE, EXPLAIN_SHELL_ROLE, Role, RoleLike, SHELL_ROLE,
|
||||||
};
|
};
|
||||||
use self::session::Session;
|
use self::session::Session;
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
pub use self::skill::Skill;
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
pub use self::skill_policy::SkillPolicy;
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
pub use self::skill_registry::SkillRegistry;
|
||||||
pub use self::update::run_self_update;
|
pub use self::update::run_self_update;
|
||||||
use crate::client::{
|
use crate::client::{
|
||||||
ClientConfig, MessageContentToolCalls, Model, ModelType, OPENAI_COMPATIBLE_PROVIDERS,
|
ClientConfig, MessageContentToolCalls, Model, ModelType, OPENAI_COMPATIBLE_PROVIDERS,
|
||||||
@@ -74,6 +83,7 @@ const LIGHT_THEME: &[u8] = include_bytes!("../../assets/monokai-extended-light.t
|
|||||||
const CONFIG_FILE_NAME: &str = "config.yaml";
|
const CONFIG_FILE_NAME: &str = "config.yaml";
|
||||||
const AGENT_GRAPH_FILE_NAME: &str = "graph.yaml";
|
const AGENT_GRAPH_FILE_NAME: &str = "graph.yaml";
|
||||||
const ROLES_DIR_NAME: &str = "roles";
|
const ROLES_DIR_NAME: &str = "roles";
|
||||||
|
const SKILLS_DIR_NAME: &str = "skills";
|
||||||
const MACROS_DIR_NAME: &str = "macros";
|
const MACROS_DIR_NAME: &str = "macros";
|
||||||
const ENV_FILE_NAME: &str = ".env";
|
const ENV_FILE_NAME: &str = ".env";
|
||||||
const MESSAGES_FILE_NAME: &str = "messages.md";
|
const MESSAGES_FILE_NAME: &str = "messages.md";
|
||||||
@@ -104,13 +114,13 @@ const DEFAULT_VISIBLE_TOOLS: [&str; 18] = [
|
|||||||
"get_current_weather.sh",
|
"get_current_weather.sh",
|
||||||
"search_wikipedia.sh",
|
"search_wikipedia.sh",
|
||||||
"search_arxiv.sh",
|
"search_arxiv.sh",
|
||||||
"web_search_loki.sh",
|
"web_search_coyote.sh",
|
||||||
];
|
];
|
||||||
|
|
||||||
const CLIENTS_FIELD: &str = "clients";
|
const CLIENTS_FIELD: &str = "clients";
|
||||||
|
|
||||||
const SYNC_MODELS_URL: &str =
|
const SYNC_MODELS_URL: &str =
|
||||||
"https://raw.githubusercontent.com/Dark-Alex-17/loki/refs/heads/main/models.yaml";
|
"https://raw.githubusercontent.com/Dark-Alex-17/coyote/refs/heads/main/models.yaml";
|
||||||
|
|
||||||
const SUMMARIZATION_PROMPT: &str =
|
const SUMMARIZATION_PROMPT: &str =
|
||||||
"Summarize the discussion briefly in 200 words or less to use as a prompt for future context.";
|
"Summarize the discussion briefly in 200 words or less to use as a prompt for future context.";
|
||||||
@@ -144,6 +154,10 @@ pub struct Config {
|
|||||||
pub enabled_tools: Option<String>,
|
pub enabled_tools: Option<String>,
|
||||||
pub visible_tools: Option<Vec<String>>,
|
pub visible_tools: Option<Vec<String>>,
|
||||||
|
|
||||||
|
pub skills_enabled: bool,
|
||||||
|
pub enabled_skills: Option<String>,
|
||||||
|
pub visible_skills: Option<Vec<String>>,
|
||||||
|
|
||||||
pub mcp_server_support: bool,
|
pub mcp_server_support: bool,
|
||||||
pub mapping_mcp_servers: IndexMap<String, String>,
|
pub mapping_mcp_servers: IndexMap<String, String>,
|
||||||
pub enabled_mcp_servers: Option<String>,
|
pub enabled_mcp_servers: Option<String>,
|
||||||
@@ -205,6 +219,10 @@ impl Default for Config {
|
|||||||
enabled_tools: None,
|
enabled_tools: None,
|
||||||
visible_tools: None,
|
visible_tools: None,
|
||||||
|
|
||||||
|
skills_enabled: true,
|
||||||
|
enabled_skills: None,
|
||||||
|
visible_skills: None,
|
||||||
|
|
||||||
mcp_server_support: true,
|
mcp_server_support: true,
|
||||||
mapping_mcp_servers: Default::default(),
|
mapping_mcp_servers: Default::default(),
|
||||||
enabled_mcp_servers: None,
|
enabled_mcp_servers: None,
|
||||||
@@ -250,6 +268,7 @@ pub fn install_builtins() -> Result<()> {
|
|||||||
Functions::install_builtin_global_tools(false)?;
|
Functions::install_builtin_global_tools(false)?;
|
||||||
Agent::install_builtin_agents(false)?;
|
Agent::install_builtin_agents(false)?;
|
||||||
Macro::install_macros(false)?;
|
Macro::install_macros(false)?;
|
||||||
|
Skill::install_builtin_skills(false)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,18 +277,20 @@ pub enum AssetCategory {
|
|||||||
Agents,
|
Agents,
|
||||||
Macros,
|
Macros,
|
||||||
Functions,
|
Functions,
|
||||||
|
Skills,
|
||||||
#[value(name = "mcp_config")]
|
#[value(name = "mcp_config")]
|
||||||
McpConfig,
|
McpConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AssetCategory {
|
impl AssetCategory {
|
||||||
pub const NAMES: [&'static str; 4] = ["agents", "macros", "functions", "mcp_config"];
|
pub const NAMES: [&'static str; 5] = ["agents", "macros", "functions", "skills", "mcp_config"];
|
||||||
|
|
||||||
pub fn parse(name: &str) -> Option<Self> {
|
pub fn parse(name: &str) -> Option<Self> {
|
||||||
match name {
|
match name {
|
||||||
"agents" => Some(Self::Agents),
|
"agents" => Some(Self::Agents),
|
||||||
"macros" => Some(Self::Macros),
|
"macros" => Some(Self::Macros),
|
||||||
"functions" => Some(Self::Functions),
|
"functions" => Some(Self::Functions),
|
||||||
|
"skills" => Some(Self::Skills),
|
||||||
"mcp_config" => Some(Self::McpConfig),
|
"mcp_config" => Some(Self::McpConfig),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
@@ -280,6 +301,7 @@ impl AssetCategory {
|
|||||||
pub enum InstallFilter {
|
pub enum InstallFilter {
|
||||||
Agents,
|
Agents,
|
||||||
Roles,
|
Roles,
|
||||||
|
Skills,
|
||||||
Macros,
|
Macros,
|
||||||
Functions,
|
Functions,
|
||||||
#[value(name = "mcp_config")]
|
#[value(name = "mcp_config")]
|
||||||
@@ -287,12 +309,20 @@ pub enum InstallFilter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl InstallFilter {
|
impl InstallFilter {
|
||||||
pub const NAMES: [&'static str; 5] = ["agents", "roles", "macros", "functions", "mcp_config"];
|
pub const NAMES: [&'static str; 6] = [
|
||||||
|
"agents",
|
||||||
|
"roles",
|
||||||
|
"skills",
|
||||||
|
"macros",
|
||||||
|
"functions",
|
||||||
|
"mcp_config",
|
||||||
|
];
|
||||||
|
|
||||||
pub fn parse(name: &str) -> Option<Self> {
|
pub fn parse(name: &str) -> Option<Self> {
|
||||||
match name {
|
match name {
|
||||||
"agents" => Some(Self::Agents),
|
"agents" => Some(Self::Agents),
|
||||||
"roles" => Some(Self::Roles),
|
"roles" => Some(Self::Roles),
|
||||||
|
"skills" => Some(Self::Skills),
|
||||||
"macros" => Some(Self::Macros),
|
"macros" => Some(Self::Macros),
|
||||||
"functions" => Some(Self::Functions),
|
"functions" => Some(Self::Functions),
|
||||||
"mcp_config" => Some(Self::McpConfig),
|
"mcp_config" => Some(Self::McpConfig),
|
||||||
@@ -306,6 +336,7 @@ pub fn install_assets(category: AssetCategory) -> Result<()> {
|
|||||||
AssetCategory::Agents => ("agents", paths::agents_data_dir()),
|
AssetCategory::Agents => ("agents", paths::agents_data_dir()),
|
||||||
AssetCategory::Macros => ("macros", paths::macros_dir()),
|
AssetCategory::Macros => ("macros", paths::macros_dir()),
|
||||||
AssetCategory::Functions => ("functions", paths::functions_dir()),
|
AssetCategory::Functions => ("functions", paths::functions_dir()),
|
||||||
|
AssetCategory::Skills => ("skills", paths::skills_dir()),
|
||||||
AssetCategory::McpConfig => ("MCP config", paths::mcp_config_file()),
|
AssetCategory::McpConfig => ("MCP config", paths::mcp_config_file()),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -318,6 +349,7 @@ pub fn install_assets(category: AssetCategory) -> Result<()> {
|
|||||||
AssetCategory::Agents => Agent::install_builtin_agents(true)?,
|
AssetCategory::Agents => Agent::install_builtin_agents(true)?,
|
||||||
AssetCategory::Macros => Macro::install_macros(true)?,
|
AssetCategory::Macros => Macro::install_macros(true)?,
|
||||||
AssetCategory::Functions => Functions::install_builtin_global_tools(true)?,
|
AssetCategory::Functions => Functions::install_builtin_global_tools(true)?,
|
||||||
|
AssetCategory::Skills => Skill::install_builtin_skills(true)?,
|
||||||
AssetCategory::McpConfig => Functions::install_mcp_config()?,
|
AssetCategory::McpConfig => Functions::install_mcp_config()?,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -625,7 +657,7 @@ pub async fn create_config_file(config_path: &Path) -> Result<()> {
|
|||||||
|
|
||||||
let config_data = serde_yaml::to_string(&config).with_context(|| "Failed to create config")?;
|
let config_data = serde_yaml::to_string(&config).with_context(|| "Failed to create config")?;
|
||||||
let config_data = format!(
|
let config_data = format!(
|
||||||
"# see https://github.com/Dark-Alex-17/loki/blob/main/config.example.yaml\n\n{config_data}"
|
"# see https://github.com/Dark-Alex-17/coyote/blob/main/config.example.yaml\n\n{config_data}"
|
||||||
);
|
);
|
||||||
|
|
||||||
ensure_parent_exists(config_path)?;
|
ensure_parent_exists(config_path)?;
|
||||||
|
|||||||
+38
-1
@@ -3,7 +3,7 @@ use super::{
|
|||||||
AGENT_GRAPH_FILE_NAME, AGENTS_DIR_NAME, BASH_PROMPT_UTILS_FILE_NAME, CONFIG_FILE_NAME,
|
AGENT_GRAPH_FILE_NAME, AGENTS_DIR_NAME, BASH_PROMPT_UTILS_FILE_NAME, CONFIG_FILE_NAME,
|
||||||
ENV_FILE_NAME, FUNCTIONS_BIN_DIR_NAME, FUNCTIONS_DIR_NAME, GLOBAL_TOOLS_DIR_NAME,
|
ENV_FILE_NAME, FUNCTIONS_BIN_DIR_NAME, FUNCTIONS_DIR_NAME, GLOBAL_TOOLS_DIR_NAME,
|
||||||
GLOBAL_TOOLS_UTILS_DIR_NAME, MACROS_DIR_NAME, MCP_FILE_NAME, ModelsOverride, RAGS_DIR_NAME,
|
GLOBAL_TOOLS_UTILS_DIR_NAME, MACROS_DIR_NAME, MCP_FILE_NAME, ModelsOverride, RAGS_DIR_NAME,
|
||||||
ROLES_DIR_NAME,
|
ROLES_DIR_NAME, SKILLS_DIR_NAME,
|
||||||
};
|
};
|
||||||
use crate::client::ProviderModels;
|
use crate::client::ProviderModels;
|
||||||
use crate::utils::{get_env_name, list_file_names, normalize_env_name};
|
use crate::utils::{get_env_name, list_file_names, normalize_env_name};
|
||||||
@@ -65,6 +65,21 @@ pub fn role_file(name: &str) -> PathBuf {
|
|||||||
roles_dir().join(format!("{name}.md"))
|
roles_dir().join(format!("{name}.md"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn skills_dir() -> PathBuf {
|
||||||
|
match env::var(get_env_name("skills_dir")) {
|
||||||
|
Ok(value) => PathBuf::from(value),
|
||||||
|
Err(_) => local_path(SKILLS_DIR_NAME),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn skill_dir(name: &str) -> PathBuf {
|
||||||
|
skills_dir().join(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn skill_file(name: &str) -> PathBuf {
|
||||||
|
skill_dir(name).join("SKILL.md")
|
||||||
|
}
|
||||||
|
|
||||||
pub fn macros_dir() -> PathBuf {
|
pub fn macros_dir() -> PathBuf {
|
||||||
match env::var(get_env_name("macros_dir")) {
|
match env::var(get_env_name("macros_dir")) {
|
||||||
Ok(value) => PathBuf::from(value),
|
Ok(value) => PathBuf::from(value),
|
||||||
@@ -234,6 +249,28 @@ pub fn has_macro(name: &str) -> bool {
|
|||||||
names.contains(&name.to_string())
|
names.contains(&name.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn list_skills() -> Vec<String> {
|
||||||
|
let mut names = Vec::new();
|
||||||
|
if let Ok(rd) = read_dir(skills_dir()) {
|
||||||
|
for entry in rd.flatten() {
|
||||||
|
if let Ok(file_type) = entry.file_type()
|
||||||
|
&& file_type.is_dir()
|
||||||
|
&& let Some(name) = entry.file_name().to_str()
|
||||||
|
&& entry.path().join("SKILL.md").is_file()
|
||||||
|
{
|
||||||
|
names.push(name.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
names.sort_unstable();
|
||||||
|
names
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_skill(name: &str) -> bool {
|
||||||
|
skill_file(name).is_file()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn local_models_override() -> Result<Vec<ProviderModels>> {
|
pub fn local_models_override() -> Result<Vec<ProviderModels>> {
|
||||||
let model_override_path = models_override_file();
|
let model_override_path = models_override_file();
|
||||||
let err = || {
|
let err = || {
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
use super::rag_cache::{RagCache, RagKey};
|
use super::rag_cache::{RagCache, RagKey};
|
||||||
use super::session::Session;
|
use super::session::Session;
|
||||||
|
use super::skill::{SKILL_SCAFFOLD, Skill};
|
||||||
|
use super::skill_policy::SkillPolicy;
|
||||||
|
use super::skill_registry::SkillRegistry;
|
||||||
use super::todo::TodoList;
|
use super::todo::TodoList;
|
||||||
use super::tool_scope::{McpRuntime, ToolScope};
|
use super::tool_scope::{McpRuntime, ToolScope};
|
||||||
use super::{
|
use super::{
|
||||||
@@ -12,7 +15,7 @@ use super::{MessageContentToolCalls, prompts};
|
|||||||
use crate::client::{Model, ModelType, list_models};
|
use crate::client::{Model, ModelType, list_models};
|
||||||
use crate::function::{
|
use crate::function::{
|
||||||
FunctionDeclaration, Functions, ToolCallTracker, ToolResult,
|
FunctionDeclaration, Functions, ToolCallTracker, ToolResult,
|
||||||
user_interaction::USER_FUNCTION_PREFIX,
|
skill::SKILL_FUNCTION_PREFIX, user_interaction::USER_FUNCTION_PREFIX,
|
||||||
};
|
};
|
||||||
use crate::mcp::{
|
use crate::mcp::{
|
||||||
MCP_DESCRIBE_META_FUNCTION_NAME_PREFIX, MCP_INVOKE_META_FUNCTION_NAME_PREFIX,
|
MCP_DESCRIBE_META_FUNCTION_NAME_PREFIX, MCP_INVOKE_META_FUNCTION_NAME_PREFIX,
|
||||||
@@ -34,7 +37,7 @@ use indexmap::IndexMap;
|
|||||||
use indoc::formatdoc;
|
use indoc::formatdoc;
|
||||||
use inquire::{Confirm, MultiSelect, Text, list_option::ListOption, validator::Validation};
|
use inquire::{Confirm, MultiSelect, Text, list_option::ListOption, validator::Validation};
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{BTreeSet, HashMap, HashSet};
|
||||||
use std::fs::{File, OpenOptions, read_dir, read_to_string, remove_dir_all, remove_file};
|
use std::fs::{File, OpenOptions, read_dir, read_to_string, remove_dir_all, remove_file};
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
@@ -82,6 +85,7 @@ pub struct RequestContext {
|
|||||||
pub current_depth: usize,
|
pub current_depth: usize,
|
||||||
pub auto_continue_count: usize,
|
pub auto_continue_count: usize,
|
||||||
pub todo_list: TodoList,
|
pub todo_list: TodoList,
|
||||||
|
pub skill_registry: SkillRegistry,
|
||||||
pub last_continuation_response: Option<String>,
|
pub last_continuation_response: Option<String>,
|
||||||
|
|
||||||
pub render_mode: RenderMode,
|
pub render_mode: RenderMode,
|
||||||
@@ -110,6 +114,7 @@ impl RequestContext {
|
|||||||
current_depth: 0,
|
current_depth: 0,
|
||||||
auto_continue_count: 0,
|
auto_continue_count: 0,
|
||||||
todo_list: TodoList::default(),
|
todo_list: TodoList::default(),
|
||||||
|
skill_registry: SkillRegistry::default(),
|
||||||
last_continuation_response: None,
|
last_continuation_response: None,
|
||||||
render_mode: RenderMode::default(),
|
render_mode: RenderMode::default(),
|
||||||
}
|
}
|
||||||
@@ -127,6 +132,13 @@ impl RequestContext {
|
|||||||
functions.append_user_interaction_functions();
|
functions.append_user_interaction_functions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if app.config.function_calling_support {
|
||||||
|
let policy = SkillPolicy::effective(&app.config, None, None, None)?;
|
||||||
|
if policy.skills_enabled {
|
||||||
|
functions.append_skill_functions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut mcp_runtime = McpRuntime::default();
|
let mut mcp_runtime = McpRuntime::default();
|
||||||
if let Some(registry) = &app.mcp_registry {
|
if let Some(registry) = &app.mcp_registry {
|
||||||
mcp_runtime.sync_from_registry(registry);
|
mcp_runtime.sync_from_registry(registry);
|
||||||
@@ -157,6 +169,7 @@ impl RequestContext {
|
|||||||
current_depth: 0,
|
current_depth: 0,
|
||||||
auto_continue_count: 0,
|
auto_continue_count: 0,
|
||||||
todo_list: TodoList::default(),
|
todo_list: TodoList::default(),
|
||||||
|
skill_registry: SkillRegistry::default(),
|
||||||
last_continuation_response: None,
|
last_continuation_response: None,
|
||||||
render_mode: RenderMode::default(),
|
render_mode: RenderMode::default(),
|
||||||
})
|
})
|
||||||
@@ -198,6 +211,7 @@ impl RequestContext {
|
|||||||
current_depth: self.current_depth,
|
current_depth: self.current_depth,
|
||||||
auto_continue_count: 0,
|
auto_continue_count: 0,
|
||||||
todo_list: self.todo_list.clone(),
|
todo_list: self.todo_list.clone(),
|
||||||
|
skill_registry: self.skill_registry.clone(),
|
||||||
last_continuation_response: None,
|
last_continuation_response: None,
|
||||||
render_mode: self.render_mode,
|
render_mode: self.render_mode,
|
||||||
}
|
}
|
||||||
@@ -237,6 +251,7 @@ impl RequestContext {
|
|||||||
current_depth,
|
current_depth,
|
||||||
auto_continue_count: 0,
|
auto_continue_count: 0,
|
||||||
todo_list: TodoList::default(),
|
todo_list: TodoList::default(),
|
||||||
|
skill_registry: SkillRegistry::default(),
|
||||||
last_continuation_response: None,
|
last_continuation_response: None,
|
||||||
render_mode: parent.render_mode,
|
render_mode: parent.render_mode,
|
||||||
}
|
}
|
||||||
@@ -611,7 +626,7 @@ impl RequestContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
role
|
self.skill_registry.effective_role(&role)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn auto_continue_config(&self) -> AutoContinueConfig {
|
pub fn auto_continue_config(&self) -> AutoContinueConfig {
|
||||||
@@ -814,6 +829,7 @@ impl RequestContext {
|
|||||||
if !app.dry_run {
|
if !app.dry_run {
|
||||||
self.save_message(app, input, output)?;
|
self.save_message(app, input, output)?;
|
||||||
}
|
}
|
||||||
|
self.skill_registry.sweep_auto_unload();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -882,6 +898,7 @@ impl RequestContext {
|
|||||||
("env_file", display_path(&paths::env_file())),
|
("env_file", display_path(&paths::env_file())),
|
||||||
("agents_dir", display_path(&paths::agents_data_dir())),
|
("agents_dir", display_path(&paths::agents_data_dir())),
|
||||||
("roles_dir", display_path(&paths::roles_dir())),
|
("roles_dir", display_path(&paths::roles_dir())),
|
||||||
|
("skills_dir", display_path(&paths::skills_dir())),
|
||||||
("sessions_dir", display_path(&self.sessions_dir())),
|
("sessions_dir", display_path(&self.sessions_dir())),
|
||||||
("rags_dir", display_path(&paths::rags_dir())),
|
("rags_dir", display_path(&paths::rags_dir())),
|
||||||
("macros_dir", display_path(&paths::macros_dir())),
|
("macros_dir", display_path(&paths::macros_dir())),
|
||||||
@@ -1128,7 +1145,9 @@ impl RequestContext {
|
|||||||
.declarations()
|
.declarations()
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|v| {
|
.filter(|v| {
|
||||||
v.name.starts_with(USER_FUNCTION_PREFIX) && !existing.contains(&v.name)
|
(v.name.starts_with(USER_FUNCTION_PREFIX)
|
||||||
|
|| v.name.starts_with(SKILL_FUNCTION_PREFIX))
|
||||||
|
&& !existing.contains(&v.name)
|
||||||
})
|
})
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect();
|
.collect();
|
||||||
@@ -1498,7 +1517,7 @@ impl RequestContext {
|
|||||||
if !target_path.exists() {
|
if !target_path.exists() {
|
||||||
fs::write(
|
fs::write(
|
||||||
&target_path,
|
&target_path,
|
||||||
"# see https://github.com/Dark-Alex-17/loki/blob/main/config.agent.example.yaml\n",
|
"# see https://github.com/Dark-Alex-17/coyote/blob/main/config.agent.example.yaml\n",
|
||||||
)
|
)
|
||||||
.with_context(|| format!("Failed to write to '{}'", target_path.display()))?;
|
.with_context(|| format!("Failed to write to '{}'", target_path.display()))?;
|
||||||
}
|
}
|
||||||
@@ -1537,6 +1556,7 @@ impl RequestContext {
|
|||||||
"session" => (self.sessions_dir(), Some(".yaml")),
|
"session" => (self.sessions_dir(), Some(".yaml")),
|
||||||
"rag" => (paths::rags_dir(), Some(".yaml")),
|
"rag" => (paths::rags_dir(), Some(".yaml")),
|
||||||
"macro" => (paths::macros_dir(), Some(".yaml")),
|
"macro" => (paths::macros_dir(), Some(".yaml")),
|
||||||
|
"skill" => (paths::skills_dir(), None),
|
||||||
"agent-data" => (paths::agents_data_dir(), None),
|
"agent-data" => (paths::agents_data_dir(), None),
|
||||||
_ => bail!("Unknown kind '{kind}'"),
|
_ => bail!("Unknown kind '{kind}'"),
|
||||||
};
|
};
|
||||||
@@ -1862,6 +1882,13 @@ impl RequestContext {
|
|||||||
super::map_completion_values(values)
|
super::map_completion_values(values)
|
||||||
}
|
}
|
||||||
".macro" => super::map_completion_values(paths::list_macros()),
|
".macro" => super::map_completion_values(paths::list_macros()),
|
||||||
|
".skill" => {
|
||||||
|
super::map_completion_values(vec![
|
||||||
|
"loaded".to_string(),
|
||||||
|
"load".to_string(),
|
||||||
|
"unload".to_string(),
|
||||||
|
])
|
||||||
|
}
|
||||||
".starter" => match &self.agent {
|
".starter" => match &self.agent {
|
||||||
Some(agent) => agent
|
Some(agent) => agent
|
||||||
.conversation_starters()
|
.conversation_starters()
|
||||||
@@ -1904,6 +1931,7 @@ impl RequestContext {
|
|||||||
"session",
|
"session",
|
||||||
"rag",
|
"rag",
|
||||||
"macro",
|
"macro",
|
||||||
|
"skill",
|
||||||
"agent-data",
|
"agent-data",
|
||||||
]),
|
]),
|
||||||
".vault" => {
|
".vault" => {
|
||||||
@@ -1916,6 +1944,12 @@ impl RequestContext {
|
|||||||
}
|
}
|
||||||
_ => vec![],
|
_ => vec![],
|
||||||
};
|
};
|
||||||
|
} else if (cmd == ".edit" && args.first() == Some(&"skill") && args.len() == 2)
|
||||||
|
|| (cmd == ".skill" && args.first() == Some(&"load") && args.len() == 2)
|
||||||
|
{
|
||||||
|
values = super::map_completion_values(paths::list_skills());
|
||||||
|
} else if cmd == ".skill" && args.first() == Some(&"unload") && args.len() == 2 {
|
||||||
|
values = super::map_completion_values(self.skill_registry.loaded_names());
|
||||||
} else if cmd == ".install" && args.first() == Some(&"remote") && args.len() >= 2 {
|
} else if cmd == ".install" && args.first() == Some(&"remote") && args.len() >= 2 {
|
||||||
let prev = args.get(args.len() - 2).copied().unwrap_or("");
|
let prev = args.get(args.len() - 2).copied().unwrap_or("");
|
||||||
if prev == "--filter" {
|
if prev == "--filter" {
|
||||||
@@ -2061,6 +2095,35 @@ impl RequestContext {
|
|||||||
enabled_mcp_servers: Option<String>,
|
enabled_mcp_servers: Option<String>,
|
||||||
abort_signal: AbortSignal,
|
abort_signal: AbortSignal,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
let policy = SkillPolicy::effective(
|
||||||
|
app,
|
||||||
|
self.role.as_ref(),
|
||||||
|
self.agent.as_ref(),
|
||||||
|
self.session.as_ref(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let enabled_mcp_servers = if policy.skills_enabled && app.mcp_server_support {
|
||||||
|
let skill_mcps = self.skill_registry.loaded_mcp_servers();
|
||||||
|
match (enabled_mcp_servers.as_deref(), skill_mcps.is_empty()) {
|
||||||
|
(Some("all"), _) | (_, true) => enabled_mcp_servers,
|
||||||
|
(base, false) => {
|
||||||
|
let mut merged: BTreeSet<String> = skill_mcps;
|
||||||
|
if let Some(s) = base {
|
||||||
|
for token in s.split(',') {
|
||||||
|
let t = token.trim();
|
||||||
|
if !t.is_empty() {
|
||||||
|
merged.insert(t.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(merged.into_iter().collect::<Vec<_>>().join(","))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
enabled_mcp_servers
|
||||||
|
};
|
||||||
|
|
||||||
let mut mcp_runtime = McpRuntime::new();
|
let mut mcp_runtime = McpRuntime::new();
|
||||||
|
|
||||||
if app.mcp_server_support
|
if app.mcp_server_support
|
||||||
@@ -2128,6 +2191,9 @@ impl RequestContext {
|
|||||||
if !mcp_runtime.is_empty() {
|
if !mcp_runtime.is_empty() {
|
||||||
functions.append_mcp_meta_functions(mcp_runtime.server_names());
|
functions.append_mcp_meta_functions(mcp_runtime.server_names());
|
||||||
}
|
}
|
||||||
|
if app.function_calling_support && policy.skills_enabled {
|
||||||
|
functions.append_skill_functions();
|
||||||
|
}
|
||||||
|
|
||||||
let tool_tracker = self.tool_scope.tool_tracker.clone();
|
let tool_tracker = self.tool_scope.tool_tracker.clone();
|
||||||
self.tool_scope = ToolScope {
|
self.tool_scope = ToolScope {
|
||||||
@@ -2138,6 +2204,30 @@ impl RequestContext {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn refresh_tool_scope(&mut self, abort_signal: AbortSignal) -> Result<()> {
|
||||||
|
let app = (*self.app.config).clone();
|
||||||
|
let base_mcps = if app.mcp_server_support {
|
||||||
|
if let Some(session) = &self.session {
|
||||||
|
session.enabled_mcp_servers()
|
||||||
|
} else if let Some(agent) = &self.agent {
|
||||||
|
let names = agent.mcp_server_names();
|
||||||
|
if names.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(names.join(","))
|
||||||
|
}
|
||||||
|
} else if let Some(role) = &self.role {
|
||||||
|
role.enabled_mcp_servers()
|
||||||
|
} else {
|
||||||
|
app.enabled_mcp_servers.clone()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
self.rebuild_tool_scope(&app, base_mcps, abort_signal).await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn use_role(
|
pub async fn use_role(
|
||||||
&mut self,
|
&mut self,
|
||||||
app: &AppConfig,
|
app: &AppConfig,
|
||||||
@@ -2410,6 +2500,101 @@ impl RequestContext {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn upsert_skill(&self, app: &AppConfig, name: &str) -> Result<()> {
|
||||||
|
let path = paths::skill_file(name);
|
||||||
|
ensure_parent_exists(&path)?;
|
||||||
|
let is_new = !path.exists();
|
||||||
|
if is_new {
|
||||||
|
fs::write(&path, SKILL_SCAFFOLD)
|
||||||
|
.with_context(|| format!("Failed to scaffold skill at {}", path.display()))?;
|
||||||
|
}
|
||||||
|
let editor = app.editor()?;
|
||||||
|
edit_file(&editor, &path)?;
|
||||||
|
if is_new {
|
||||||
|
println!("✓ Created skill at '{}'.", path.display());
|
||||||
|
} else {
|
||||||
|
println!("✓ Saved skill at '{}'.", path.display());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn load_skill_repl(&mut self, name: &str, abort_signal: AbortSignal) -> Result<()> {
|
||||||
|
if !paths::has_skill(name) {
|
||||||
|
bail!(
|
||||||
|
"Skill '{name}' is not installed (expected at {})",
|
||||||
|
paths::skill_file(name).display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let policy = SkillPolicy::effective(
|
||||||
|
&self.app.config,
|
||||||
|
self.role.as_ref(),
|
||||||
|
self.agent.as_ref(),
|
||||||
|
self.session.as_ref(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
if !policy.skills_enabled {
|
||||||
|
bail!("Skills are disabled in this context");
|
||||||
|
}
|
||||||
|
|
||||||
|
if !policy.allows(name) {
|
||||||
|
bail!("Skill '{name}' is not enabled in this context");
|
||||||
|
}
|
||||||
|
|
||||||
|
let skill = Skill::load(name)?;
|
||||||
|
let fn_on = self.app.config.function_calling_support;
|
||||||
|
let mcp_on = self.app.config.mcp_server_support;
|
||||||
|
let needs_tools = skill
|
||||||
|
.enabled_tools()
|
||||||
|
.map(|s| !s.trim().is_empty())
|
||||||
|
.unwrap_or(false);
|
||||||
|
let needs_mcps = skill
|
||||||
|
.enabled_mcp_servers()
|
||||||
|
.map(|s| !s.trim().is_empty())
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if needs_tools && !fn_on {
|
||||||
|
bail!("Skill '{name}' requires function calling, which is disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
if needs_mcps && !mcp_on {
|
||||||
|
bail!("Skill '{name}' requires MCP servers, which are disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
self.skill_registry.insert(skill)?;
|
||||||
|
if let Err(e) = self.refresh_tool_scope(abort_signal).await {
|
||||||
|
let _ = self.skill_registry.unload(name);
|
||||||
|
bail!("Loaded skill '{name}' but failed to refresh tool scope: {e}");
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("✓ Loaded skill '{name}'.");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn unload_skill_repl(&mut self, name: &str, abort_signal: AbortSignal) -> Result<()> {
|
||||||
|
self.skill_registry.unload(name)?;
|
||||||
|
|
||||||
|
if let Err(e) = self.refresh_tool_scope(abort_signal).await {
|
||||||
|
eprintln!("Warning: unloaded skill '{name}' but tool scope refresh failed: {e}");
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("✓ Unloaded skill '{name}'.");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_loaded_skills(&self) {
|
||||||
|
let names = self.skill_registry.loaded_names();
|
||||||
|
|
||||||
|
if names.is_empty() {
|
||||||
|
println!("No skills loaded.");
|
||||||
|
} else {
|
||||||
|
println!("Loaded skills:");
|
||||||
|
for name in names {
|
||||||
|
println!(" • {name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn apply_prelude(
|
pub async fn apply_prelude(
|
||||||
&mut self,
|
&mut self,
|
||||||
app: &AppConfig,
|
app: &AppConfig,
|
||||||
@@ -2706,7 +2891,7 @@ mod tests {
|
|||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.as_nanos();
|
.as_nanos();
|
||||||
let path = env::temp_dir().join(format!("loki-request-context-tests-{unique}"));
|
let path = env::temp_dir().join(format!("coyote-request-context-tests-{unique}"));
|
||||||
create_dir_all(&path).unwrap();
|
create_dir_all(&path).unwrap();
|
||||||
unsafe {
|
unsafe {
|
||||||
env::set_var(&key, &path);
|
env::set_var(&key, &path);
|
||||||
@@ -3333,6 +3518,58 @@ mod tests {
|
|||||||
assert!(lm.continuous);
|
assert!(lm.continuous);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn after_chat_completion_sweeps_auto_unload_skills_at_turn_end() {
|
||||||
|
let mut ctx = create_test_ctx();
|
||||||
|
ctx.app = Arc::new(AppState {
|
||||||
|
config: Arc::new(AppConfig {
|
||||||
|
dry_run: true,
|
||||||
|
..(*ctx.app.config).clone()
|
||||||
|
}),
|
||||||
|
..(*ctx.app).clone()
|
||||||
|
});
|
||||||
|
|
||||||
|
let ephemeral = Skill::new("ephemeral", "---\nauto_unload: true\n---\nbody");
|
||||||
|
let persistent = Skill::new("persistent", "---\nauto_unload: false\n---\nbody");
|
||||||
|
ctx.skill_registry.insert(ephemeral).unwrap();
|
||||||
|
ctx.skill_registry.insert(persistent).unwrap();
|
||||||
|
|
||||||
|
let input = Input::from_str(&ctx, "hello", None);
|
||||||
|
let app = Arc::clone(&ctx.app.config);
|
||||||
|
ctx.after_chat_completion(app.as_ref(), &input, "response", &[])
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(!ctx.skill_registry.is_loaded("ephemeral"));
|
||||||
|
assert!(ctx.skill_registry.is_loaded("persistent"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn after_chat_completion_preserves_auto_unload_during_tool_loop() {
|
||||||
|
let mut ctx = create_test_ctx();
|
||||||
|
ctx.app = Arc::new(AppState {
|
||||||
|
config: Arc::new(AppConfig {
|
||||||
|
dry_run: true,
|
||||||
|
..(*ctx.app.config).clone()
|
||||||
|
}),
|
||||||
|
..(*ctx.app).clone()
|
||||||
|
});
|
||||||
|
|
||||||
|
let ephemeral = Skill::new("ephemeral", "---\nauto_unload: true\n---\nbody");
|
||||||
|
ctx.skill_registry.insert(ephemeral).unwrap();
|
||||||
|
|
||||||
|
let input = Input::from_str(&ctx, "hello", None);
|
||||||
|
let app = Arc::clone(&ctx.app.config);
|
||||||
|
let tool_result =
|
||||||
|
ToolResult::new(crate::function::ToolCall::default(), serde_json::json!({}));
|
||||||
|
ctx.after_chat_completion(app.as_ref(), &input, "", &[tool_result])
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
ctx.skill_registry.is_loaded("ephemeral"),
|
||||||
|
"auto_unload skills must persist through tool-using rounds"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn role_like_mut_returns_none_when_empty() {
|
fn role_like_mut_returns_none_when_empty() {
|
||||||
let mut ctx = create_test_ctx();
|
let mut ctx = create_test_ctx();
|
||||||
@@ -3837,6 +4074,43 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn install_builtin_skills_force_overwrites_only_with_force() {
|
||||||
|
let _guard = TestConfigDirGuard::new();
|
||||||
|
|
||||||
|
Skill::install_builtin_skills(false).unwrap();
|
||||||
|
let file = paths::skill_file("git-master");
|
||||||
|
assert!(file.exists(), "git-master skill should be installed");
|
||||||
|
|
||||||
|
write(&file, "SENTINEL").unwrap();
|
||||||
|
Skill::install_builtin_skills(false).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
read_to_string(&file).unwrap(),
|
||||||
|
"SENTINEL",
|
||||||
|
"non-force install must not overwrite an existing skill"
|
||||||
|
);
|
||||||
|
|
||||||
|
Skill::install_builtin_skills(true).unwrap();
|
||||||
|
assert_ne!(
|
||||||
|
read_to_string(&file).unwrap(),
|
||||||
|
"SENTINEL",
|
||||||
|
"force install must overwrite the existing skill"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn install_builtin_skills_installs_all_bundled() {
|
||||||
|
let _guard = TestConfigDirGuard::new();
|
||||||
|
|
||||||
|
Skill::install_builtin_skills(false).unwrap();
|
||||||
|
assert!(paths::skill_file("git-master").exists());
|
||||||
|
assert!(paths::skill_file("ai-slop-remover").exists());
|
||||||
|
assert!(paths::skill_file("code-review").exists());
|
||||||
|
assert!(paths::skill_file("frontend-ui-ux").exists());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[serial]
|
#[serial]
|
||||||
fn install_functions_force_preserves_user_mcp_json() {
|
fn install_functions_force_preserves_user_mcp_json() {
|
||||||
|
|||||||
@@ -56,6 +56,10 @@ pub struct Role {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
enabled_mcp_servers: Option<String>,
|
enabled_mcp_servers: Option<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
skills_enabled: Option<bool>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
enabled_skills: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
auto_continue: Option<bool>,
|
auto_continue: Option<bool>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
max_auto_continues: Option<usize>,
|
max_auto_continues: Option<usize>,
|
||||||
@@ -98,6 +102,8 @@ impl Role {
|
|||||||
"enabled_mcp_servers" => {
|
"enabled_mcp_servers" => {
|
||||||
role.enabled_mcp_servers = value.as_str().map(|v| v.to_string())
|
role.enabled_mcp_servers = value.as_str().map(|v| v.to_string())
|
||||||
}
|
}
|
||||||
|
"skills_enabled" => role.skills_enabled = value.as_bool(),
|
||||||
|
"enabled_skills" => role.enabled_skills = value.as_str().map(|v| v.to_string()),
|
||||||
"auto_continue" => role.auto_continue = value.as_bool(),
|
"auto_continue" => role.auto_continue = value.as_bool(),
|
||||||
"max_auto_continues" => {
|
"max_auto_continues" => {
|
||||||
role.max_auto_continues = value.as_u64().map(|v| v as usize)
|
role.max_auto_continues = value.as_u64().map(|v| v as usize)
|
||||||
@@ -147,6 +153,12 @@ impl Role {
|
|||||||
if let Some(enabled_mcp_servers) = self.enabled_mcp_servers() {
|
if let Some(enabled_mcp_servers) = self.enabled_mcp_servers() {
|
||||||
metadata.push(format!("enabled_mcp_servers: {enabled_mcp_servers}"));
|
metadata.push(format!("enabled_mcp_servers: {enabled_mcp_servers}"));
|
||||||
}
|
}
|
||||||
|
if let Some(skills_enabled) = self.skills_enabled {
|
||||||
|
metadata.push(format!("skills_enabled: {skills_enabled}"));
|
||||||
|
}
|
||||||
|
if let Some(enabled_skills) = &self.enabled_skills {
|
||||||
|
metadata.push(format!("enabled_skills: {enabled_skills}"));
|
||||||
|
}
|
||||||
if let Some(auto_continue) = self.auto_continue {
|
if let Some(auto_continue) = self.auto_continue {
|
||||||
metadata.push(format!("auto_continue: {auto_continue}"));
|
metadata.push(format!("auto_continue: {auto_continue}"));
|
||||||
}
|
}
|
||||||
@@ -271,6 +283,14 @@ impl Role {
|
|||||||
self.continuation_prompt.as_deref()
|
self.continuation_prompt.as_deref()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn skills_enabled(&self) -> Option<bool> {
|
||||||
|
self.skills_enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enabled_skills(&self) -> Option<&str> {
|
||||||
|
self.enabled_skills.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn append_to_prompt(&mut self, text: &str) {
|
pub fn append_to_prompt(&mut self, text: &str) {
|
||||||
self.prompt.push_str(text);
|
self.prompt.push_str(text);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ pub struct Session {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
enabled_mcp_servers: Option<String>,
|
enabled_mcp_servers: Option<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
skills_enabled: Option<bool>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
enabled_skills: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
save_session: Option<bool>,
|
save_session: Option<bool>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
compression_threshold: Option<usize>,
|
compression_threshold: Option<usize>,
|
||||||
@@ -75,6 +79,14 @@ pub struct Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Session {
|
impl Session {
|
||||||
|
pub fn skills_enabled(&self) -> Option<bool> {
|
||||||
|
self.skills_enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enabled_skills(&self) -> Option<&str> {
|
||||||
|
self.enabled_skills.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn new_from_ctx(ctx: &RequestContext, app: &AppConfig, name: &str) -> Self {
|
pub fn new_from_ctx(ctx: &RequestContext, app: &AppConfig, name: &str) -> Self {
|
||||||
let role = ctx.extract_role(app);
|
let role = ctx.extract_role(app);
|
||||||
let mut session = Self {
|
let mut session = Self {
|
||||||
|
|||||||
@@ -0,0 +1,326 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use fancy_regex::Regex;
|
||||||
|
use log::{debug, info};
|
||||||
|
use rust_embed::Embed;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
|
#[derive(Embed)]
|
||||||
|
#[folder = "assets/skills/"]
|
||||||
|
struct SkillsAsset;
|
||||||
|
|
||||||
|
static RE_METADATA: LazyLock<Regex> =
|
||||||
|
LazyLock::new(|| Regex::new(r"(?s)-{3,}\s*(.*?)\s*-{3,}\s*(.*)").unwrap());
|
||||||
|
|
||||||
|
pub const SKILL_SCAFFOLD: &str = "\
|
||||||
|
---
|
||||||
|
description: One-line description shown to the model when listing skills.
|
||||||
|
enabled_tools:
|
||||||
|
enabled_mcp_servers:
|
||||||
|
auto_unload: false
|
||||||
|
---
|
||||||
|
Replace this body with the knowledge or methodology this skill teaches.
|
||||||
|
";
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
||||||
|
pub struct Skill {
|
||||||
|
name: String,
|
||||||
|
#[serde(default)]
|
||||||
|
description: String,
|
||||||
|
#[serde(default)]
|
||||||
|
body: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
enabled_tools: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
enabled_mcp_servers: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
auto_unload: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Skill {
|
||||||
|
pub fn new(name: &str, content: &str) -> Self {
|
||||||
|
let mut metadata = "";
|
||||||
|
let mut body = content.trim();
|
||||||
|
if let Ok(Some(caps)) = RE_METADATA.captures(content)
|
||||||
|
&& let (Some(metadata_value), Some(body_value)) = (caps.get(1), caps.get(2))
|
||||||
|
{
|
||||||
|
metadata = metadata_value.as_str().trim();
|
||||||
|
body = body_value.as_str().trim();
|
||||||
|
}
|
||||||
|
let mut body = body.to_string();
|
||||||
|
interpolate_variables(&mut body);
|
||||||
|
let mut skill = Self {
|
||||||
|
name: name.to_string(),
|
||||||
|
body,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
if !metadata.is_empty()
|
||||||
|
&& let Ok(value) = serde_yaml::from_str::<Value>(metadata)
|
||||||
|
&& let Some(value) = value.as_object()
|
||||||
|
{
|
||||||
|
for (key, value) in value {
|
||||||
|
match key.as_str() {
|
||||||
|
"description" => {
|
||||||
|
if let Some(v) = value.as_str() {
|
||||||
|
skill.description = v.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"enabled_tools" => {
|
||||||
|
skill.enabled_tools = value.as_str().map(|v| v.to_string());
|
||||||
|
}
|
||||||
|
"enabled_mcp_servers" => {
|
||||||
|
skill.enabled_mcp_servers = value.as_str().map(|v| v.to_string());
|
||||||
|
}
|
||||||
|
"auto_unload" => {
|
||||||
|
skill.auto_unload = value.as_bool();
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
skill
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn install_builtin_skills(force: bool) -> Result<()> {
|
||||||
|
info!(
|
||||||
|
"Installing built-in skills in {}",
|
||||||
|
paths::skills_dir().display()
|
||||||
|
);
|
||||||
|
|
||||||
|
for file in SkillsAsset::iter() {
|
||||||
|
debug!("Processing skill file: {}", file.as_ref());
|
||||||
|
|
||||||
|
let embedded_file = SkillsAsset::get(&file)
|
||||||
|
.ok_or_else(|| anyhow!("Failed to load embedded skill file: {}", file.as_ref()))?;
|
||||||
|
let content = unsafe { std::str::from_utf8_unchecked(&embedded_file.data) };
|
||||||
|
let file_path = paths::skills_dir().join(file.as_ref());
|
||||||
|
|
||||||
|
if file_path.exists() && !force {
|
||||||
|
debug!(
|
||||||
|
"Skill file already exists, skipping: {}",
|
||||||
|
file_path.display()
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_parent_exists(&file_path)?;
|
||||||
|
info!("Creating skill file: {}", file_path.display());
|
||||||
|
let mut skill_file = File::create(&file_path)?;
|
||||||
|
Write::write_all(&mut skill_file, content.as_bytes())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load(name: &str) -> Result<Self> {
|
||||||
|
let path = paths::skill_file(name);
|
||||||
|
let content = read_to_string(&path)
|
||||||
|
.with_context(|| format!("Failed to read skill '{name}' at {}", path.display()))?;
|
||||||
|
Ok(Skill::new(name, &content))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn name(&self) -> &str {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn description(&self) -> &str {
|
||||||
|
&self.description
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn body(&self) -> &str {
|
||||||
|
&self.body
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enabled_tools(&self) -> Option<&str> {
|
||||||
|
self.enabled_tools.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enabled_mcp_servers(&self) -> Option<&str> {
|
||||||
|
self.enabled_mcp_servers.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn auto_unload(&self) -> bool {
|
||||||
|
self.auto_unload.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_compatible(&self, function_calling_enabled: bool, mcp_enabled: bool) -> bool {
|
||||||
|
if self.declares_tools() && !function_calling_enabled {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.declares_mcp_servers() && !mcp_enabled {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn declares_tools(&self) -> bool {
|
||||||
|
self.enabled_tools
|
||||||
|
.as_deref()
|
||||||
|
.map(|s| !s.trim().is_empty())
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn declares_mcp_servers(&self) -> bool {
|
||||||
|
self.enabled_mcp_servers
|
||||||
|
.as_deref()
|
||||||
|
.map(|s| !s.trim().is_empty())
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn skill_new_parses_body() {
|
||||||
|
let skill = Skill::new("test", "You are a git expert");
|
||||||
|
|
||||||
|
assert_eq!(skill.name(), "test");
|
||||||
|
assert_eq!(skill.body(), "You are a git expert");
|
||||||
|
assert_eq!(skill.description(), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn skill_new_parses_full_metadata() {
|
||||||
|
let content = "---\n\
|
||||||
|
description: Atomic commits, rebase surgery\n\
|
||||||
|
enabled_tools: shell,fs\n\
|
||||||
|
enabled_mcp_servers: github\n\
|
||||||
|
auto_unload: true\n\
|
||||||
|
---\n\
|
||||||
|
You are a git expert";
|
||||||
|
|
||||||
|
let skill = Skill::new("git-master", content);
|
||||||
|
|
||||||
|
assert_eq!(skill.name(), "git-master");
|
||||||
|
assert_eq!(skill.description(), "Atomic commits, rebase surgery");
|
||||||
|
assert_eq!(skill.enabled_tools(), Some("shell,fs"));
|
||||||
|
assert_eq!(skill.enabled_mcp_servers(), Some("github"));
|
||||||
|
assert!(skill.auto_unload());
|
||||||
|
assert_eq!(skill.body(), "You are a git expert");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn skill_new_no_metadata_has_defaults() {
|
||||||
|
let skill = Skill::new("test", "Just a body");
|
||||||
|
|
||||||
|
assert_eq!(skill.description(), "");
|
||||||
|
assert_eq!(skill.enabled_tools(), None);
|
||||||
|
assert_eq!(skill.enabled_mcp_servers(), None);
|
||||||
|
assert!(!skill.auto_unload());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn skill_new_metadata_only() {
|
||||||
|
let content = "---\ndescription: Just metadata\n---";
|
||||||
|
|
||||||
|
let skill = Skill::new("test", content);
|
||||||
|
|
||||||
|
assert_eq!(skill.description(), "Just metadata");
|
||||||
|
assert_eq!(skill.body(), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn skill_new_partial_metadata_leaves_others_none() {
|
||||||
|
let content = "---\ndescription: Partial\n---\nthe body";
|
||||||
|
|
||||||
|
let skill = Skill::new("test", content);
|
||||||
|
|
||||||
|
assert_eq!(skill.description(), "Partial");
|
||||||
|
assert_eq!(skill.enabled_tools(), None);
|
||||||
|
assert_eq!(skill.enabled_mcp_servers(), None);
|
||||||
|
assert!(!skill.auto_unload());
|
||||||
|
assert_eq!(skill.body(), "the body");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn skill_new_ignores_unknown_keys() {
|
||||||
|
let content = "---\ndescription: D\nbogus_field: 42\n---\nbody";
|
||||||
|
|
||||||
|
let skill = Skill::new("test", content);
|
||||||
|
|
||||||
|
assert_eq!(skill.description(), "D");
|
||||||
|
assert_eq!(skill.body(), "body");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn skill_new_trims_body_whitespace() {
|
||||||
|
let content = "---\ndescription: D\n---\n\n\n body content \n\n";
|
||||||
|
|
||||||
|
let skill = Skill::new("test", content);
|
||||||
|
|
||||||
|
assert_eq!(skill.body(), "body content");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn skill_default_has_empty_fields() {
|
||||||
|
let skill = Skill::default();
|
||||||
|
|
||||||
|
assert_eq!(skill.name(), "");
|
||||||
|
assert_eq!(skill.body(), "");
|
||||||
|
assert_eq!(skill.description(), "");
|
||||||
|
assert_eq!(skill.enabled_tools(), None);
|
||||||
|
assert_eq!(skill.enabled_mcp_servers(), None);
|
||||||
|
assert!(!skill.auto_unload());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn is_compatible_knowledge_only_passes_all_combinations() {
|
||||||
|
let skill = Skill::new("test", "Just knowledge");
|
||||||
|
|
||||||
|
assert!(skill.is_compatible(false, false));
|
||||||
|
assert!(skill.is_compatible(true, false));
|
||||||
|
assert!(skill.is_compatible(false, true));
|
||||||
|
assert!(skill.is_compatible(true, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn is_compatible_with_tools_requires_function_calling() {
|
||||||
|
let content = "---\nenabled_tools: shell\n---\nbody";
|
||||||
|
|
||||||
|
let skill = Skill::new("test", content);
|
||||||
|
|
||||||
|
assert!(!skill.is_compatible(false, true));
|
||||||
|
assert!(!skill.is_compatible(false, false));
|
||||||
|
assert!(skill.is_compatible(true, true));
|
||||||
|
assert!(skill.is_compatible(true, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn is_compatible_with_mcp_requires_mcp_enabled() {
|
||||||
|
let content = "---\nenabled_mcp_servers: github\n---\nbody";
|
||||||
|
|
||||||
|
let skill = Skill::new("test", content);
|
||||||
|
|
||||||
|
assert!(!skill.is_compatible(true, false));
|
||||||
|
assert!(!skill.is_compatible(false, false));
|
||||||
|
assert!(skill.is_compatible(true, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn is_compatible_requires_both_when_both_declared() {
|
||||||
|
let content = "---\nenabled_tools: shell\nenabled_mcp_servers: github\n---\nbody";
|
||||||
|
|
||||||
|
let skill = Skill::new("test", content);
|
||||||
|
|
||||||
|
assert!(!skill.is_compatible(true, false));
|
||||||
|
assert!(!skill.is_compatible(false, true));
|
||||||
|
assert!(!skill.is_compatible(false, false));
|
||||||
|
assert!(skill.is_compatible(true, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn is_compatible_empty_string_tools_is_knowledge_only() {
|
||||||
|
let content = "---\nenabled_tools: \"\"\n---\nbody";
|
||||||
|
|
||||||
|
let skill = Skill::new("test", content);
|
||||||
|
|
||||||
|
assert!(skill.is_compatible(false, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,312 @@
|
|||||||
|
use super::agent::Agent;
|
||||||
|
use super::app_config::AppConfig;
|
||||||
|
use super::paths;
|
||||||
|
use super::role::Role;
|
||||||
|
use super::session::Session;
|
||||||
|
|
||||||
|
use anyhow::{Result, bail};
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SkillPolicy {
|
||||||
|
pub skills_enabled: bool,
|
||||||
|
pub enabled: HashSet<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SkillPolicy {
|
||||||
|
pub fn effective(
|
||||||
|
global: &AppConfig,
|
||||||
|
role: Option<&Role>,
|
||||||
|
agent: Option<&Agent>,
|
||||||
|
session: Option<&Session>,
|
||||||
|
) -> Result<Self> {
|
||||||
|
Self::effective_with(
|
||||||
|
global,
|
||||||
|
role,
|
||||||
|
agent,
|
||||||
|
session,
|
||||||
|
&paths::has_skill,
|
||||||
|
&paths::list_skills,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn effective_with<F, G>(
|
||||||
|
global: &AppConfig,
|
||||||
|
role: Option<&Role>,
|
||||||
|
agent: Option<&Agent>,
|
||||||
|
session: Option<&Session>,
|
||||||
|
skill_exists: &F,
|
||||||
|
list_installed: &G,
|
||||||
|
) -> Result<Self>
|
||||||
|
where
|
||||||
|
F: Fn(&str) -> bool,
|
||||||
|
G: Fn() -> Vec<String>,
|
||||||
|
{
|
||||||
|
let mut skills_enabled = global.skills_enabled;
|
||||||
|
if let Some(r) = role
|
||||||
|
&& let Some(false) = r.skills_enabled()
|
||||||
|
{
|
||||||
|
skills_enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(a) = agent
|
||||||
|
&& let Some(false) = a.skills_enabled()
|
||||||
|
{
|
||||||
|
skills_enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(s) = session
|
||||||
|
&& let Some(false) = s.skills_enabled()
|
||||||
|
{
|
||||||
|
skills_enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let visible: Option<HashSet<String>> = global
|
||||||
|
.visible_skills
|
||||||
|
.as_ref()
|
||||||
|
.map(|v| v.iter().cloned().collect());
|
||||||
|
|
||||||
|
let enabled_raw: Option<Vec<String>> = session
|
||||||
|
.and_then(|s| parse_csv_opt(s.enabled_skills()))
|
||||||
|
.or_else(|| agent.and_then(|a| a.enabled_skills().map(|v| v.to_vec())))
|
||||||
|
.or_else(|| role.and_then(|r| parse_csv_opt(r.enabled_skills())))
|
||||||
|
.or_else(|| parse_csv_opt(global.enabled_skills.as_deref()));
|
||||||
|
|
||||||
|
let enabled: HashSet<String> = match enabled_raw {
|
||||||
|
Some(explicit) => {
|
||||||
|
let set: HashSet<String> = explicit.into_iter().collect();
|
||||||
|
for name in &set {
|
||||||
|
if !skill_exists(name) {
|
||||||
|
bail!("enabled_skills references skill '{name}' which is not installed");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(vs) = &visible
|
||||||
|
&& !vs.contains(name)
|
||||||
|
{
|
||||||
|
bail!(
|
||||||
|
"enabled_skills references skill '{name}' which is not in visible_skills"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set
|
||||||
|
}
|
||||||
|
None => match &visible {
|
||||||
|
Some(v) => v.clone(),
|
||||||
|
None => list_installed().into_iter().collect(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
skills_enabled,
|
||||||
|
enabled,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn allows(&self, name: &str) -> bool {
|
||||||
|
self.skills_enabled && self.enabled.contains(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_csv_opt(s: Option<&str>) -> Option<Vec<String>> {
|
||||||
|
s.map(|raw| {
|
||||||
|
raw.split(',')
|
||||||
|
.map(|t| t.trim().to_string())
|
||||||
|
.filter(|t| !t.is_empty())
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn always_true(_: &str) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn empty_installed() -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_app_config(
|
||||||
|
skills_enabled: bool,
|
||||||
|
enabled: Option<&str>,
|
||||||
|
visible: Option<&[&str]>,
|
||||||
|
) -> AppConfig {
|
||||||
|
AppConfig {
|
||||||
|
skills_enabled,
|
||||||
|
enabled_skills: enabled.map(|s| s.to_string()),
|
||||||
|
visible_skills: visible.map(|v| v.iter().map(|s| s.to_string()).collect()),
|
||||||
|
..AppConfig::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn defaults_yield_skills_enabled_with_empty_universe() {
|
||||||
|
let global = AppConfig::default();
|
||||||
|
|
||||||
|
let policy =
|
||||||
|
SkillPolicy::effective_with(&global, None, None, None, &always_true, &empty_installed)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(policy.skills_enabled);
|
||||||
|
assert!(policy.enabled.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn falls_back_to_all_installed_when_no_level_sets_enabled_skills() {
|
||||||
|
let global = AppConfig::default();
|
||||||
|
let installed = || vec!["alpha".to_string(), "beta".to_string()];
|
||||||
|
|
||||||
|
let policy =
|
||||||
|
SkillPolicy::effective_with(&global, None, None, None, &always_true, &installed)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(policy.enabled.len(), 2);
|
||||||
|
assert!(policy.enabled.contains("alpha"));
|
||||||
|
assert!(policy.enabled.contains("beta"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn falls_back_to_visible_when_visible_set_but_no_enabled() {
|
||||||
|
let global = make_app_config(true, None, Some(&["alpha", "beta"]));
|
||||||
|
|
||||||
|
let policy =
|
||||||
|
SkillPolicy::effective_with(&global, None, None, None, &always_true, &empty_installed)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(policy.enabled.len(), 2);
|
||||||
|
assert!(policy.enabled.contains("alpha"));
|
||||||
|
assert!(policy.enabled.contains("beta"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn global_enabled_skills_is_effective_when_no_other_levels() {
|
||||||
|
let global = make_app_config(true, Some("alpha,beta"), Some(&["alpha", "beta", "gamma"]));
|
||||||
|
|
||||||
|
let policy =
|
||||||
|
SkillPolicy::effective_with(&global, None, None, None, &always_true, &empty_installed)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(policy.enabled.contains("alpha"));
|
||||||
|
assert!(policy.enabled.contains("beta"));
|
||||||
|
assert!(!policy.enabled.contains("gamma"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn role_overrides_global_enabled_skills() {
|
||||||
|
let global = make_app_config(true, Some("alpha"), Some(&["alpha", "beta"]));
|
||||||
|
let role = Role::new("test", "---\nenabled_skills: beta\n---\nbody");
|
||||||
|
|
||||||
|
let policy = SkillPolicy::effective_with(
|
||||||
|
&global,
|
||||||
|
Some(&role),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
&always_true,
|
||||||
|
&empty_installed,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(policy.enabled.contains("beta"));
|
||||||
|
assert!(!policy.enabled.contains("alpha"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn any_skills_enabled_false_disables_globally() {
|
||||||
|
let global = make_app_config(true, None, None);
|
||||||
|
let role = Role::new("test", "---\nskills_enabled: false\n---\nbody");
|
||||||
|
|
||||||
|
let policy = SkillPolicy::effective_with(
|
||||||
|
&global,
|
||||||
|
Some(&role),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
&always_true,
|
||||||
|
&empty_installed,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(!policy.skills_enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn allows_returns_false_when_skills_disabled() {
|
||||||
|
let global = AppConfig {
|
||||||
|
skills_enabled: false,
|
||||||
|
..AppConfig::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let policy = SkillPolicy::effective_with(&global, None, None, None, &always_true, &|| {
|
||||||
|
vec!["alpha".to_string()]
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(!policy.allows("alpha"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn allows_returns_true_when_skill_in_enabled_set() {
|
||||||
|
let global = make_app_config(true, Some("alpha"), None);
|
||||||
|
|
||||||
|
let policy =
|
||||||
|
SkillPolicy::effective_with(&global, None, None, None, &always_true, &empty_installed)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(policy.allows("alpha"));
|
||||||
|
assert!(!policy.allows("beta"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validation_rejects_uninstalled_skill_reference() {
|
||||||
|
let global = make_app_config(true, Some("ghost"), None);
|
||||||
|
|
||||||
|
let err =
|
||||||
|
SkillPolicy::effective_with(&global, None, None, None, &|_| false, &empty_installed)
|
||||||
|
.unwrap_err();
|
||||||
|
|
||||||
|
assert!(err.to_string().contains("not installed"));
|
||||||
|
assert!(err.to_string().contains("ghost"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validation_rejects_skill_not_in_visible_set() {
|
||||||
|
let global = make_app_config(true, Some("beta"), Some(&["alpha"]));
|
||||||
|
|
||||||
|
let err =
|
||||||
|
SkillPolicy::effective_with(&global, None, None, None, &always_true, &empty_installed)
|
||||||
|
.unwrap_err();
|
||||||
|
|
||||||
|
assert!(err.to_string().contains("not in visible_skills"));
|
||||||
|
assert!(err.to_string().contains("beta"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validation_skipped_when_no_explicit_enabled_skills() {
|
||||||
|
let global = make_app_config(true, None, None);
|
||||||
|
|
||||||
|
let policy =
|
||||||
|
SkillPolicy::effective_with(&global, None, None, None, &|_| false, &empty_installed)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(policy.enabled.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_string_enabled_skills_resolves_to_empty_override() {
|
||||||
|
let global = make_app_config(true, Some("alpha,beta"), Some(&["alpha", "beta"]));
|
||||||
|
let role = Role::new("test", "---\nenabled_skills: \"\"\n---\nbody");
|
||||||
|
|
||||||
|
let policy = SkillPolicy::effective_with(
|
||||||
|
&global,
|
||||||
|
Some(&role),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
&always_true,
|
||||||
|
&empty_installed,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(policy.enabled.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,314 @@
|
|||||||
|
use super::role::{Role, RoleLike};
|
||||||
|
use super::skill::Skill;
|
||||||
|
|
||||||
|
use anyhow::{Result, bail};
|
||||||
|
use indexmap::IndexMap;
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
pub struct SkillRegistry {
|
||||||
|
loaded: IndexMap<String, Skill>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SkillRegistry {
|
||||||
|
pub fn insert(&mut self, skill: Skill) -> Result<()> {
|
||||||
|
let name = skill.name().to_string();
|
||||||
|
|
||||||
|
if self.loaded.contains_key(&name) {
|
||||||
|
bail!("Skill '{name}' is already loaded");
|
||||||
|
}
|
||||||
|
|
||||||
|
self.loaded.insert(name, skill);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unload(&mut self, name: &str) -> Result<()> {
|
||||||
|
if self.loaded.shift_remove(name).is_none() {
|
||||||
|
bail!("Skill '{name}' is not loaded");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn loaded_names(&self) -> Vec<String> {
|
||||||
|
self.loaded.keys().cloned().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn loaded_mcp_servers(&self) -> BTreeSet<String> {
|
||||||
|
let mut out = BTreeSet::new();
|
||||||
|
for skill in self.loaded.values() {
|
||||||
|
if let Some(csv) = skill.enabled_mcp_servers() {
|
||||||
|
for token in csv.split(',') {
|
||||||
|
let t = token.trim();
|
||||||
|
if !t.is_empty() {
|
||||||
|
out.insert(t.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_loaded(&self, name: &str) -> bool {
|
||||||
|
self.loaded.contains_key(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sweep_auto_unload(&mut self) {
|
||||||
|
self.loaded.retain(|_, skill| !skill.auto_unload());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn effective_role(&self, base: &Role) -> Role {
|
||||||
|
if self.loaded.is_empty() {
|
||||||
|
return base.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut effective = base.clone();
|
||||||
|
let skip_body = effective.is_embedded_prompt();
|
||||||
|
|
||||||
|
let base_tools_set = effective.enabled_tools().is_some();
|
||||||
|
let base_mcps_set = effective.enabled_mcp_servers().is_some();
|
||||||
|
|
||||||
|
let mut tools = parse_csv(effective.enabled_tools().as_deref());
|
||||||
|
let mut mcps = parse_csv(effective.enabled_mcp_servers().as_deref());
|
||||||
|
|
||||||
|
for (_, skill) in &self.loaded {
|
||||||
|
tools.extend(parse_csv(skill.enabled_tools()));
|
||||||
|
mcps.extend(parse_csv(skill.enabled_mcp_servers()));
|
||||||
|
if !skip_body && !skill.body().is_empty() {
|
||||||
|
let separator = if effective.is_empty_prompt() {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
"\n\n"
|
||||||
|
};
|
||||||
|
effective.append_to_prompt(separator);
|
||||||
|
effective.append_to_prompt(skill.body());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if base_tools_set || !tools.is_empty() {
|
||||||
|
effective.set_enabled_tools(Some(join_csv(&tools)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if base_mcps_set || !mcps.is_empty() {
|
||||||
|
effective.set_enabled_mcp_servers(Some(join_csv(&mcps)));
|
||||||
|
}
|
||||||
|
|
||||||
|
effective
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_csv(s: Option<&str>) -> BTreeSet<String> {
|
||||||
|
let mut set = BTreeSet::new();
|
||||||
|
if let Some(raw) = s {
|
||||||
|
for token in raw.split(',') {
|
||||||
|
let trimmed = token.trim();
|
||||||
|
if !trimmed.is_empty() {
|
||||||
|
set.insert(trimmed.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set
|
||||||
|
}
|
||||||
|
|
||||||
|
fn join_csv(set: &BTreeSet<String>) -> String {
|
||||||
|
set.iter().cloned().collect::<Vec<_>>().join(",")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
impl SkillRegistry {
|
||||||
|
fn insert_for_test(&mut self, skill: Skill) {
|
||||||
|
self.loaded.insert(skill.name().to_string(), skill);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn make_skill(name: &str, frontmatter: &str, body: &str) -> Skill {
|
||||||
|
let content = if frontmatter.is_empty() {
|
||||||
|
body.to_string()
|
||||||
|
} else {
|
||||||
|
format!("---\n{frontmatter}\n---\n{body}")
|
||||||
|
};
|
||||||
|
Skill::new(name, &content)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_registry_returns_base_clone() {
|
||||||
|
let base = Role::new("test", "You are a helper");
|
||||||
|
let registry = SkillRegistry::default();
|
||||||
|
|
||||||
|
let effective = registry.effective_role(&base);
|
||||||
|
|
||||||
|
assert_eq!(effective.prompt(), base.prompt());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn one_skill_appends_body_after_base_with_separator() {
|
||||||
|
let mut registry = SkillRegistry::default();
|
||||||
|
registry.insert_for_test(make_skill("git-master", "description: D", "Git knowledge"));
|
||||||
|
|
||||||
|
let base = Role::new("test", "You are a helper");
|
||||||
|
let effective = registry.effective_role(&base);
|
||||||
|
|
||||||
|
assert_eq!(effective.prompt(), "You are a helper\n\nGit knowledge");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn two_skills_compose_bodies_in_insertion_order() {
|
||||||
|
let mut registry = SkillRegistry::default();
|
||||||
|
registry.insert_for_test(make_skill("a", "", "Alpha body"));
|
||||||
|
registry.insert_for_test(make_skill("b", "", "Beta body"));
|
||||||
|
|
||||||
|
let base = Role::new("test", "Base");
|
||||||
|
let effective = registry.effective_role(&base);
|
||||||
|
|
||||||
|
assert_eq!(effective.prompt(), "Base\n\nAlpha body\n\nBeta body");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_base_prompt_omits_leading_separator() {
|
||||||
|
let mut registry = SkillRegistry::default();
|
||||||
|
registry.insert_for_test(make_skill("a", "", "Alpha"));
|
||||||
|
registry.insert_for_test(make_skill("b", "", "Beta"));
|
||||||
|
|
||||||
|
let base = Role::new("test", "");
|
||||||
|
let effective = registry.effective_role(&base);
|
||||||
|
|
||||||
|
assert_eq!(effective.prompt(), "Alpha\n\nBeta");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn embedded_prompt_base_skips_body_composition() {
|
||||||
|
let mut registry = SkillRegistry::default();
|
||||||
|
registry.insert_for_test(make_skill(
|
||||||
|
"git-master",
|
||||||
|
"enabled_tools: shell",
|
||||||
|
"should not appear",
|
||||||
|
));
|
||||||
|
|
||||||
|
let base = Role::new("test", "Process: __INPUT__");
|
||||||
|
let effective = registry.effective_role(&base);
|
||||||
|
|
||||||
|
assert_eq!(effective.prompt(), "Process: __INPUT__");
|
||||||
|
let tools = effective.enabled_tools().expect("tools set by skill");
|
||||||
|
assert!(tools.contains("shell"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn skills_with_empty_body_do_not_inject_separator() {
|
||||||
|
let mut registry = SkillRegistry::default();
|
||||||
|
registry.insert_for_test(make_skill("knowledge", "enabled_tools: fs", ""));
|
||||||
|
|
||||||
|
let base = Role::new("test", "Base");
|
||||||
|
let effective = registry.effective_role(&base);
|
||||||
|
|
||||||
|
assert_eq!(effective.prompt(), "Base");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tools_and_mcps_are_unioned_and_deduplicated() {
|
||||||
|
let mut registry = SkillRegistry::default();
|
||||||
|
registry.insert_for_test(make_skill(
|
||||||
|
"a",
|
||||||
|
"enabled_tools: shell,fs\nenabled_mcp_servers: github",
|
||||||
|
"body",
|
||||||
|
));
|
||||||
|
registry.insert_for_test(make_skill(
|
||||||
|
"b",
|
||||||
|
"enabled_tools: fs,git\nenabled_mcp_servers: github,jira",
|
||||||
|
"body",
|
||||||
|
));
|
||||||
|
|
||||||
|
let mut base = Role::new("test", "body");
|
||||||
|
base.set_enabled_tools(Some("web_search".to_string()));
|
||||||
|
|
||||||
|
let effective = registry.effective_role(&base);
|
||||||
|
|
||||||
|
let tools_str = effective.enabled_tools().unwrap();
|
||||||
|
let tools: BTreeSet<&str> = tools_str.split(',').collect();
|
||||||
|
assert_eq!(tools, BTreeSet::from(["fs", "git", "shell", "web_search"]));
|
||||||
|
|
||||||
|
let mcps_str = effective.enabled_mcp_servers().unwrap();
|
||||||
|
let mcps: BTreeSet<&str> = mcps_str.split(',').collect();
|
||||||
|
assert_eq!(mcps, BTreeSet::from(["github", "jira"]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_skill_tool_contributions_preserves_base_none() {
|
||||||
|
let mut registry = SkillRegistry::default();
|
||||||
|
registry.insert_for_test(make_skill("knowledge", "", "Pure knowledge"));
|
||||||
|
|
||||||
|
let base = Role::new("test", "Base");
|
||||||
|
let effective = registry.effective_role(&base);
|
||||||
|
|
||||||
|
assert!(effective.enabled_tools().is_none());
|
||||||
|
assert!(effective.enabled_mcp_servers().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn base_some_empty_tools_is_preserved() {
|
||||||
|
let mut registry = SkillRegistry::default();
|
||||||
|
registry.insert_for_test(make_skill("knowledge", "", "Pure knowledge"));
|
||||||
|
|
||||||
|
let mut base = Role::new("test", "Base");
|
||||||
|
base.set_enabled_tools(Some(String::new()));
|
||||||
|
let effective = registry.effective_role(&base);
|
||||||
|
|
||||||
|
assert_eq!(effective.enabled_tools().as_deref(), Some(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unload_not_loaded_returns_error() {
|
||||||
|
let mut registry = SkillRegistry::default();
|
||||||
|
|
||||||
|
let err = registry.unload("missing").unwrap_err();
|
||||||
|
|
||||||
|
assert!(err.to_string().contains("not loaded"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unload_existing_succeeds_and_removes() {
|
||||||
|
let mut registry = SkillRegistry::default();
|
||||||
|
registry.insert_for_test(make_skill("git-master", "", "body"));
|
||||||
|
assert!(registry.is_loaded("git-master"));
|
||||||
|
|
||||||
|
registry.unload("git-master").unwrap();
|
||||||
|
assert!(!registry.is_loaded("git-master"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn loaded_names_returns_insertion_order() {
|
||||||
|
let mut registry = SkillRegistry::default();
|
||||||
|
|
||||||
|
registry.insert_for_test(make_skill("zulu", "", "body"));
|
||||||
|
registry.insert_for_test(make_skill("alpha", "", "body"));
|
||||||
|
registry.insert_for_test(make_skill("mike", "", "body"));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
registry.loaded_names(),
|
||||||
|
vec!["zulu".to_string(), "alpha".to_string(), "mike".to_string()]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sweep_removes_only_auto_unload_skills() {
|
||||||
|
let mut registry = SkillRegistry::default();
|
||||||
|
registry.insert_for_test(make_skill("ephemeral", "auto_unload: true", "body"));
|
||||||
|
registry.insert_for_test(make_skill("persistent", "", "body"));
|
||||||
|
|
||||||
|
registry.sweep_auto_unload();
|
||||||
|
|
||||||
|
assert!(!registry.is_loaded("ephemeral"));
|
||||||
|
assert!(registry.is_loaded("persistent"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn is_loaded_returns_false_for_unknown() {
|
||||||
|
let registry = SkillRegistry::default();
|
||||||
|
|
||||||
|
assert!(!registry.is_loaded("nothing"));
|
||||||
|
}
|
||||||
|
}
|
||||||
+17
-17
@@ -69,7 +69,7 @@ fn normalize_version(requested: Option<String>) -> Option<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn is_dir_writable(dir: &Path) -> bool {
|
fn is_dir_writable(dir: &Path) -> bool {
|
||||||
let probe = dir.join(format!(".loki-update-write-test-{}", process::id()));
|
let probe = dir.join(format!(".coyote-update-write-test-{}", process::id()));
|
||||||
match OpenOptions::new().write(true).create_new(true).open(&probe) {
|
match OpenOptions::new().write(true).create_new(true).open(&probe) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
let _ = fs::remove_file(&probe);
|
let _ = fs::remove_file(&probe);
|
||||||
@@ -83,24 +83,24 @@ pub fn run_self_update(requested: Option<String>, force: bool) -> Result<()> {
|
|||||||
let target_tag = normalize_version(requested);
|
let target_tag = normalize_version(requested);
|
||||||
|
|
||||||
let exe_path = env::current_exe()
|
let exe_path = env::current_exe()
|
||||||
.context("Could not determine the path of the running loki executable")?;
|
.context("Could not determine the path of the running coyote executable")?;
|
||||||
let resolved = canonicalize(&exe_path).unwrap_or_else(|_| exe_path.clone());
|
let resolved = canonicalize(&exe_path).unwrap_or_else(|_| exe_path.clone());
|
||||||
let source = classify_install_path(&resolved);
|
let source = classify_install_path(&resolved);
|
||||||
|
|
||||||
if source.is_package_managed() {
|
if source.is_package_managed() {
|
||||||
let body = match source {
|
let body = match source {
|
||||||
InstallSource::Homebrew => format!(
|
InstallSource::Homebrew => format!(
|
||||||
"Loki appears to be installed via Homebrew ({}).\n\
|
"Coyote appears to be installed via Homebrew ({}).\n\
|
||||||
Updating in place replaces the binary inside Homebrew's Cellar; `brew` will\n\
|
Updating in place replaces the binary inside Homebrew's Cellar; `brew` will\n\
|
||||||
then report a version that no longer matches the file on disk, and a later\n\
|
then report a version that no longer matches the file on disk, and a later\n\
|
||||||
`brew upgrade`/`brew reinstall` may overwrite it or fail.\n\
|
`brew upgrade`/`brew reinstall` may overwrite it or fail.\n\
|
||||||
The clean way to update is: brew upgrade loki",
|
The clean way to update is: brew upgrade coyote",
|
||||||
exe_path.display()
|
exe_path.display()
|
||||||
),
|
),
|
||||||
InstallSource::Cargo => format!(
|
InstallSource::Cargo => format!(
|
||||||
"Loki appears to be installed via `cargo install` ({}).\n\
|
"Coyote appears to be installed via `cargo install` ({}).\n\
|
||||||
Updating in place leaves Cargo's records out of sync with the binary on disk.\n\
|
Updating in place leaves Cargo's records out of sync with the binary on disk.\n\
|
||||||
The clean way to update is: cargo install --locked loki-ai",
|
The clean way to update is: cargo install --locked coyote-ai",
|
||||||
exe_path.display()
|
exe_path.display()
|
||||||
),
|
),
|
||||||
InstallSource::Manual => unreachable!("Manual installs are not package-managed"),
|
InstallSource::Manual => unreachable!("Manual installs are not package-managed"),
|
||||||
@@ -130,7 +130,7 @@ pub fn run_self_update(requested: Option<String>, force: bool) -> Result<()> {
|
|||||||
{
|
{
|
||||||
bail!(
|
bail!(
|
||||||
"No write permission for '{}'. Re-run with elevated permissions (e.g. sudo), \
|
"No write permission for '{}'. Re-run with elevated permissions (e.g. sudo), \
|
||||||
or update Loki through your package manager.",
|
or update Coyote through your package manager.",
|
||||||
parent.display()
|
parent.display()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -139,8 +139,8 @@ pub fn run_self_update(requested: Option<String>, force: bool) -> Result<()> {
|
|||||||
let mut builder = Update::configure();
|
let mut builder = Update::configure();
|
||||||
builder
|
builder
|
||||||
.repo_owner("Dark-Alex-17")
|
.repo_owner("Dark-Alex-17")
|
||||||
.repo_name("loki")
|
.repo_name("coyote")
|
||||||
.bin_name("loki")
|
.bin_name("coyote")
|
||||||
.current_version(env!("CARGO_PKG_VERSION"))
|
.current_version(env!("CARGO_PKG_VERSION"))
|
||||||
.no_confirm(true)
|
.no_confirm(true)
|
||||||
.show_download_progress(interactive);
|
.show_download_progress(interactive);
|
||||||
@@ -155,10 +155,10 @@ pub fn run_self_update(requested: Option<String>, force: bool) -> Result<()> {
|
|||||||
|
|
||||||
match status {
|
match status {
|
||||||
Status::UpToDate(version) => {
|
Status::UpToDate(version) => {
|
||||||
println!("Loki is already up to date (v{version}).");
|
println!("Coyote is already up to date (v{version}).");
|
||||||
}
|
}
|
||||||
Status::Updated(version) => {
|
Status::Updated(version) => {
|
||||||
println!("Loki updated to v{version}. Restart loki to use the new version.");
|
println!("Coyote updated to v{version}. Restart coyote to use the new version.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -172,7 +172,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn classify_cargo_install() {
|
fn classify_cargo_install() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
classify_install_path(&PathBuf::from("/home/u/.cargo/bin/loki")),
|
classify_install_path(&PathBuf::from("/home/u/.cargo/bin/coyote")),
|
||||||
InstallSource::Cargo
|
InstallSource::Cargo
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -180,7 +180,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn classify_homebrew_opt_prefix() {
|
fn classify_homebrew_opt_prefix() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
classify_install_path(&PathBuf::from("/opt/homebrew/bin/loki")),
|
classify_install_path(&PathBuf::from("/opt/homebrew/bin/coyote")),
|
||||||
InstallSource::Homebrew
|
InstallSource::Homebrew
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -188,7 +188,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn classify_homebrew_cellar() {
|
fn classify_homebrew_cellar() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
classify_install_path(&PathBuf::from("/usr/local/Cellar/loki/0.3.0/bin/loki")),
|
classify_install_path(&PathBuf::from("/usr/local/Cellar/coyote/0.3.0/bin/coyote")),
|
||||||
InstallSource::Homebrew
|
InstallSource::Homebrew
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -196,7 +196,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn classify_homebrew_linuxbrew() {
|
fn classify_homebrew_linuxbrew() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
classify_install_path(&PathBuf::from("/home/linuxbrew/.linuxbrew/bin/loki")),
|
classify_install_path(&PathBuf::from("/home/linuxbrew/.linuxbrew/bin/coyote")),
|
||||||
InstallSource::Homebrew
|
InstallSource::Homebrew
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -204,7 +204,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn classify_manual_usr_local_bin() {
|
fn classify_manual_usr_local_bin() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
classify_install_path(&PathBuf::from("/usr/local/bin/loki")),
|
classify_install_path(&PathBuf::from("/usr/local/bin/coyote")),
|
||||||
InstallSource::Manual
|
InstallSource::Manual
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -212,7 +212,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn classify_manual_local_bin() {
|
fn classify_manual_local_bin() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
classify_install_path(&PathBuf::from("/home/u/.local/bin/loki")),
|
classify_install_path(&PathBuf::from("/home/u/.local/bin/coyote")),
|
||||||
InstallSource::Manual
|
InstallSource::Manual
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+43
-5
@@ -1,9 +1,11 @@
|
|||||||
|
pub(crate) mod skill;
|
||||||
pub(crate) mod supervisor;
|
pub(crate) mod supervisor;
|
||||||
pub(crate) mod todo;
|
pub(crate) mod todo;
|
||||||
pub(crate) mod user_interaction;
|
pub(crate) mod user_interaction;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{Agent, RequestContext},
|
config::{Agent, RequestContext},
|
||||||
|
graph,
|
||||||
utils::*,
|
utils::*,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -20,6 +22,7 @@ use indoc::formatdoc;
|
|||||||
use rust_embed::Embed;
|
use rust_embed::Embed;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
|
use skill::SKILL_FUNCTION_PREFIX;
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
@@ -352,6 +355,11 @@ impl Functions {
|
|||||||
self.declarations.extend(todo::todo_function_declarations());
|
self.declarations.extend(todo::todo_function_declarations());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn append_skill_functions(&mut self) {
|
||||||
|
self.declarations
|
||||||
|
.extend(skill::skill_function_declarations());
|
||||||
|
}
|
||||||
|
|
||||||
pub fn append_supervisor_functions(&mut self) {
|
pub fn append_supervisor_functions(&mut self) {
|
||||||
self.declarations
|
self.declarations
|
||||||
.extend(supervisor::supervisor_function_declarations());
|
.extend(supervisor::supervisor_function_declarations());
|
||||||
@@ -1038,6 +1046,15 @@ impl ToolCall {
|
|||||||
json!({"tool_call_error": error_msg})
|
json!({"tool_call_error": error_msg})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
_ if cmd_name.starts_with(SKILL_FUNCTION_PREFIX) => {
|
||||||
|
skill::handle_skill_tool(ctx, &cmd_name, &json_data)
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
let error_msg = format!("Skill tool failed: {e}");
|
||||||
|
eprintln!("{}", warning_text(&format!("⚠️ {error_msg} ⚠️")));
|
||||||
|
json!({"tool_call_error": error_msg})
|
||||||
|
})
|
||||||
|
}
|
||||||
_ if cmd_name.starts_with(SUPERVISOR_FUNCTION_PREFIX) => {
|
_ if cmd_name.starts_with(SUPERVISOR_FUNCTION_PREFIX) => {
|
||||||
supervisor::handle_supervisor_tool(ctx, &cmd_name, &json_data)
|
supervisor::handle_supervisor_tool(ctx, &cmd_name, &json_data)
|
||||||
.await
|
.await
|
||||||
@@ -1199,6 +1216,9 @@ pub fn run_llm_function(
|
|||||||
if dir.exists() {
|
if dir.exists() {
|
||||||
bin_dirs.push(dir);
|
bin_dirs.push(dir);
|
||||||
}
|
}
|
||||||
|
if graph::agent_has_graph(&agent_name) {
|
||||||
|
envs.insert("AUTO_CONFIRM".into(), "true".into());
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
bin_dirs.push(paths::functions_bin_dir());
|
bin_dirs.push(paths::functions_bin_dir());
|
||||||
}
|
}
|
||||||
@@ -1242,7 +1262,7 @@ pub fn run_llm_function(
|
|||||||
.map_err(|err| anyhow!("Unable to run {command_name}, {err}"))?;
|
.map_err(|err| anyhow!("Unable to run {command_name}, {err}"))?;
|
||||||
|
|
||||||
let stdout = child.stdout.take().expect("Failed to capture stdout");
|
let stdout = child.stdout.take().expect("Failed to capture stdout");
|
||||||
let mut stderr = child.stderr.take().expect("Failed to capture stderr");
|
let stderr = child.stderr.take().expect("Failed to capture stderr");
|
||||||
|
|
||||||
let stdout_thread = std::thread::spawn(move || {
|
let stdout_thread = std::thread::spawn(move || {
|
||||||
let mut buffer = [0; 1024];
|
let mut buffer = [0; 1024];
|
||||||
@@ -1269,8 +1289,29 @@ pub fn run_llm_function(
|
|||||||
});
|
});
|
||||||
|
|
||||||
let stderr_thread = std::thread::spawn(move || {
|
let stderr_thread = std::thread::spawn(move || {
|
||||||
|
let mut buffer = [0; 1024];
|
||||||
|
let mut reader = stderr;
|
||||||
|
let mut err = io::stderr();
|
||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
let _ = stderr.read_to_end(&mut buf);
|
while let Ok(n) = reader.read(&mut buffer) {
|
||||||
|
if n == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let chunk = &buffer[0..n];
|
||||||
|
buf.extend_from_slice(chunk);
|
||||||
|
let mut last_pos = 0;
|
||||||
|
for (i, &byte) in chunk.iter().enumerate() {
|
||||||
|
if byte == b'\n' {
|
||||||
|
let _ = err.write_all(&chunk[last_pos..i]);
|
||||||
|
let _ = err.write_all(b"\r\n");
|
||||||
|
last_pos = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if last_pos < n {
|
||||||
|
let _ = err.write_all(&chunk[last_pos..n]);
|
||||||
|
}
|
||||||
|
let _ = err.flush();
|
||||||
|
}
|
||||||
buf
|
buf
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1283,9 +1324,6 @@ pub fn run_llm_function(
|
|||||||
let exit_code = status.code().unwrap_or_default();
|
let exit_code = status.code().unwrap_or_default();
|
||||||
if exit_code != 0 {
|
if exit_code != 0 {
|
||||||
let stderr = String::from_utf8_lossy(&stderr_bytes).trim().to_string();
|
let stderr = String::from_utf8_lossy(&stderr_bytes).trim().to_string();
|
||||||
if !stderr.is_empty() {
|
|
||||||
eprintln!("{stderr}");
|
|
||||||
}
|
|
||||||
let tool_error_message = format!("Tool call '{command_name}' exited with code {exit_code}");
|
let tool_error_message = format!("Tool call '{command_name}' exited with code {exit_code}");
|
||||||
eprintln!("{}", warning_text(&format!("⚠️ {tool_error_message} ⚠️")));
|
eprintln!("{}", warning_text(&format!("⚠️ {tool_error_message} ⚠️")));
|
||||||
let mut error_json = json!({"tool_call_error": tool_error_message});
|
let mut error_json = json!({"tool_call_error": tool_error_message});
|
||||||
|
|||||||
@@ -0,0 +1,311 @@
|
|||||||
|
use super::{FunctionDeclaration, JsonSchema};
|
||||||
|
use crate::config::{RequestContext, Skill, SkillPolicy, paths};
|
||||||
|
use crate::utils::create_abort_signal;
|
||||||
|
|
||||||
|
use anyhow::{Result, bail};
|
||||||
|
use indexmap::IndexMap;
|
||||||
|
use log::warn;
|
||||||
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
|
pub const SKILL_FUNCTION_PREFIX: &str = "skill__";
|
||||||
|
|
||||||
|
pub fn skill_function_declarations() -> Vec<FunctionDeclaration> {
|
||||||
|
vec![
|
||||||
|
FunctionDeclaration {
|
||||||
|
name: format!("{SKILL_FUNCTION_PREFIX}list"),
|
||||||
|
description:
|
||||||
|
"List skills available in this context. Returns each skill's name, description, \
|
||||||
|
what tools and MCP servers it grants on load, and whether it is currently loaded. \
|
||||||
|
Call this to discover skills before using skill__load."
|
||||||
|
.to_string(),
|
||||||
|
parameters: JsonSchema {
|
||||||
|
type_value: Some("object".to_string()),
|
||||||
|
properties: Some(IndexMap::new()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
agent: false,
|
||||||
|
},
|
||||||
|
FunctionDeclaration {
|
||||||
|
name: format!("{SKILL_FUNCTION_PREFIX}load"),
|
||||||
|
description:
|
||||||
|
"Load a skill module into the current context. The skill's instructions and any \
|
||||||
|
tools or MCP servers it grants become active for subsequent turns. Call \
|
||||||
|
skill__unload when the skill's work is complete to keep the context lean."
|
||||||
|
.to_string(),
|
||||||
|
parameters: JsonSchema {
|
||||||
|
type_value: Some("object".to_string()),
|
||||||
|
properties: Some(IndexMap::from([(
|
||||||
|
"name".to_string(),
|
||||||
|
JsonSchema {
|
||||||
|
type_value: Some("string".to_string()),
|
||||||
|
description: Some("Name of the skill to load.".into()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)])),
|
||||||
|
required: Some(vec!["name".to_string()]),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
agent: false,
|
||||||
|
},
|
||||||
|
FunctionDeclaration {
|
||||||
|
name: format!("{SKILL_FUNCTION_PREFIX}unload"),
|
||||||
|
description:
|
||||||
|
"Unload a previously loaded skill, removing its instructions and granted tools \
|
||||||
|
from the context. Call this when the skill's work is complete."
|
||||||
|
.to_string(),
|
||||||
|
parameters: JsonSchema {
|
||||||
|
type_value: Some("object".to_string()),
|
||||||
|
properties: Some(IndexMap::from([(
|
||||||
|
"name".to_string(),
|
||||||
|
JsonSchema {
|
||||||
|
type_value: Some("string".to_string()),
|
||||||
|
description: Some("Name of the skill to unload.".into()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)])),
|
||||||
|
required: Some(vec!["name".to_string()]),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
agent: false,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_skill_tool(
|
||||||
|
ctx: &mut RequestContext,
|
||||||
|
cmd_name: &str,
|
||||||
|
args: &Value,
|
||||||
|
) -> Result<Value> {
|
||||||
|
let action = cmd_name
|
||||||
|
.strip_prefix(SKILL_FUNCTION_PREFIX)
|
||||||
|
.unwrap_or(cmd_name);
|
||||||
|
|
||||||
|
let policy = SkillPolicy::effective(
|
||||||
|
&ctx.app.config,
|
||||||
|
ctx.role.as_ref(),
|
||||||
|
ctx.agent.as_ref(),
|
||||||
|
ctx.session.as_ref(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
if !policy.skills_enabled {
|
||||||
|
return Ok(json!({
|
||||||
|
"error": "Skills are disabled in this context"
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
match action {
|
||||||
|
"list" => handle_list(ctx, &policy),
|
||||||
|
"load" => handle_load(ctx, args, &policy).await,
|
||||||
|
"unload" => handle_unload(ctx, args).await,
|
||||||
|
_ => bail!("Unknown skill action: {action}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_list(ctx: &RequestContext, policy: &SkillPolicy) -> Result<Value> {
|
||||||
|
let function_calling_on = ctx.app.config.function_calling_support;
|
||||||
|
let mcp_on = ctx.app.config.mcp_server_support;
|
||||||
|
|
||||||
|
let mut entries = Vec::new();
|
||||||
|
for name in paths::list_skills() {
|
||||||
|
if !policy.allows(&name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let skill = match Skill::load(&name) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Failed to load skill '{name}' for listing: {e}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if !skill.is_compatible(function_calling_on, mcp_on) {
|
||||||
|
warn!(
|
||||||
|
"Skill '{name}' filtered from list: declares tools or MCP servers but those features are disabled"
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.push(json!({
|
||||||
|
"name": skill.name(),
|
||||||
|
"description": skill.description(),
|
||||||
|
"grants_tools": csv_to_vec(skill.enabled_tools()),
|
||||||
|
"grants_mcp_servers": csv_to_vec(skill.enabled_mcp_servers()),
|
||||||
|
"loaded": ctx.skill_registry.is_loaded(skill.name()),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(json!({"skills": entries}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_load(
|
||||||
|
ctx: &mut RequestContext,
|
||||||
|
args: &Value,
|
||||||
|
policy: &SkillPolicy,
|
||||||
|
) -> Result<Value> {
|
||||||
|
let name = match args.get("name").and_then(Value::as_str) {
|
||||||
|
Some(n) if !n.is_empty() => n,
|
||||||
|
_ => return Ok(json!({"error": "name is required"})),
|
||||||
|
};
|
||||||
|
|
||||||
|
if !policy.allows(name) {
|
||||||
|
return Ok(json!({
|
||||||
|
"error": format!("Skill '{name}' is not enabled in this context")
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
let skill = match Skill::load(name) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
return Ok(json!({
|
||||||
|
"error": format!("Failed to load skill '{name}': {e}")
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let function_calling_on = ctx.app.config.function_calling_support;
|
||||||
|
let mcp_on = ctx.app.config.mcp_server_support;
|
||||||
|
|
||||||
|
let tools_declared = skill
|
||||||
|
.enabled_tools()
|
||||||
|
.map(|s| !s.trim().is_empty())
|
||||||
|
.unwrap_or(false);
|
||||||
|
let mcps_declared = skill
|
||||||
|
.enabled_mcp_servers()
|
||||||
|
.map(|s| !s.trim().is_empty())
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if tools_declared && !function_calling_on {
|
||||||
|
return Ok(json!({
|
||||||
|
"error": format!(
|
||||||
|
"Skill '{name}' requires function calling, which is disabled in this context"
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
if mcps_declared && !mcp_on {
|
||||||
|
return Ok(json!({
|
||||||
|
"error": format!(
|
||||||
|
"Skill '{name}' requires MCP servers, which are disabled in this context"
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = ctx.skill_registry.insert(skill) {
|
||||||
|
return Ok(json!({"error": e.to_string()}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = ctx.refresh_tool_scope(create_abort_signal()).await {
|
||||||
|
let _ = ctx.skill_registry.unload(name);
|
||||||
|
return Ok(json!({
|
||||||
|
"error": format!("Loaded skill '{name}' but failed to refresh tool scope: {e}")
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(json!({
|
||||||
|
"status": "ok",
|
||||||
|
"loaded": name,
|
||||||
|
"message": format!("Skill '{name}' loaded")
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_unload(ctx: &mut RequestContext, args: &Value) -> Result<Value> {
|
||||||
|
let name = match args.get("name").and_then(Value::as_str) {
|
||||||
|
Some(n) if !n.is_empty() => n,
|
||||||
|
_ => return Ok(json!({"error": "name is required"})),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = ctx.skill_registry.unload(name) {
|
||||||
|
return Ok(json!({"error": e.to_string()}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = ctx.refresh_tool_scope(create_abort_signal()).await {
|
||||||
|
warn!("Unloaded skill '{name}' but failed to refresh tool scope: {e}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(json!({
|
||||||
|
"status": "ok",
|
||||||
|
"unloaded": name
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn csv_to_vec(csv: Option<&str>) -> Vec<String> {
|
||||||
|
csv.map(|raw| {
|
||||||
|
raw.split(',')
|
||||||
|
.map(|t| t.trim().to_string())
|
||||||
|
.filter(|t| !t.is_empty())
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn declarations_have_three_entries() {
|
||||||
|
let decls = skill_function_declarations();
|
||||||
|
assert_eq!(decls.len(), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn declaration_names_use_skill_prefix() {
|
||||||
|
let decls = skill_function_declarations();
|
||||||
|
|
||||||
|
let names: Vec<&str> = decls.iter().map(|d| d.name.as_str()).collect();
|
||||||
|
|
||||||
|
assert!(names.contains(&"skill__list"));
|
||||||
|
assert!(names.contains(&"skill__load"));
|
||||||
|
assert!(names.contains(&"skill__unload"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_and_unload_require_name_parameter() {
|
||||||
|
let decls = skill_function_declarations();
|
||||||
|
for action in ["load", "unload"] {
|
||||||
|
let decl = decls
|
||||||
|
.iter()
|
||||||
|
.find(|d| d.name == format!("skill__{action}"))
|
||||||
|
.expect("missing declaration");
|
||||||
|
|
||||||
|
let required = decl
|
||||||
|
.parameters
|
||||||
|
.required
|
||||||
|
.as_ref()
|
||||||
|
.expect("required field missing");
|
||||||
|
|
||||||
|
assert!(required.contains(&"name".to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn list_has_no_required_parameters() {
|
||||||
|
let decls = skill_function_declarations();
|
||||||
|
let list_decl = decls
|
||||||
|
.iter()
|
||||||
|
.find(|d| d.name == "skill__list")
|
||||||
|
.expect("skill__list missing");
|
||||||
|
|
||||||
|
let required = list_decl
|
||||||
|
.parameters
|
||||||
|
.required
|
||||||
|
.as_ref()
|
||||||
|
.map(|v| v.is_empty())
|
||||||
|
.unwrap_or(true);
|
||||||
|
|
||||||
|
assert!(required, "skill__list should have no required parameters");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn csv_to_vec_empty_input() {
|
||||||
|
assert!(csv_to_vec(None).is_empty());
|
||||||
|
assert!(csv_to_vec(Some("")).is_empty());
|
||||||
|
assert!(csv_to_vec(Some(" ")).is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn csv_to_vec_parses_and_trims() {
|
||||||
|
let v = csv_to_vec(Some("a, b ,c,, d"));
|
||||||
|
|
||||||
|
assert_eq!(v, vec!["a", "b", "c", "d"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -568,13 +568,13 @@ mod tests {
|
|||||||
let entries = vec![
|
let entries = vec![
|
||||||
"read_query".to_string(),
|
"read_query".to_string(),
|
||||||
"mcp:pubmed-search".to_string(),
|
"mcp:pubmed-search".to_string(),
|
||||||
"web_search_loki".to_string(),
|
"web_search_coyote".to_string(),
|
||||||
"mcp:github".to_string(),
|
"mcp:github".to_string(),
|
||||||
];
|
];
|
||||||
|
|
||||||
let (regular, mcp) = categorize_tools(Some(&entries));
|
let (regular, mcp) = categorize_tools(Some(&entries));
|
||||||
|
|
||||||
assert_eq!(regular, vec!["read_query", "web_search_loki"]);
|
assert_eq!(regular, vec!["read_query", "web_search_coyote"]);
|
||||||
assert_eq!(mcp, vec!["pubmed-search", "github"]);
|
assert_eq!(mcp, vec!["pubmed-search", "github"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -423,7 +423,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn load_from_file_reads_disk() {
|
fn load_from_file_reads_disk() {
|
||||||
let dir = env::temp_dir();
|
let dir = env::temp_dir();
|
||||||
let path = dir.join(format!("loki_graph_parser_test_{}.yaml", process::id()));
|
let path = dir.join(format!("coyote_graph_parser_test_{}.yaml", process::id()));
|
||||||
let yaml = formatdoc! {r#"
|
let yaml = formatdoc! {r#"
|
||||||
name: disk_graph
|
name: disk_graph
|
||||||
version: "1.0"
|
version: "1.0"
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ impl ScriptExecutor {
|
|||||||
cmd.stdout(Stdio::piped());
|
cmd.stdout(Stdio::piped());
|
||||||
cmd.stderr(Stdio::piped());
|
cmd.stderr(Stdio::piped());
|
||||||
cmd.envs(&self.extra_envs);
|
cmd.envs(&self.extra_envs);
|
||||||
|
cmd.env("AUTO_CONFIRM", "true");
|
||||||
match &state_repr {
|
match &state_repr {
|
||||||
StateRepresentation::Inline(json) => {
|
StateRepresentation::Inline(json) => {
|
||||||
cmd.env("GRAPH_STATE", json);
|
cmd.env("GRAPH_STATE", json);
|
||||||
|
|||||||
+2
-2
@@ -812,7 +812,7 @@ model: anthropic:claude-sonnet-4-6
|
|||||||
temperature: 0.2
|
temperature: 0.2
|
||||||
top_p: 0.9
|
top_p: 0.9
|
||||||
global_tools:
|
global_tools:
|
||||||
- web_search_loki.sh
|
- web_search_coyote.sh
|
||||||
mcp_servers:
|
mcp_servers:
|
||||||
- pubmed-search
|
- pubmed-search
|
||||||
conversation_starters:
|
conversation_starters:
|
||||||
@@ -827,7 +827,7 @@ nodes:
|
|||||||
assert_eq!(graph.model.as_deref(), Some("anthropic:claude-sonnet-4-6"));
|
assert_eq!(graph.model.as_deref(), Some("anthropic:claude-sonnet-4-6"));
|
||||||
assert_eq!(graph.temperature, Some(0.2));
|
assert_eq!(graph.temperature, Some(0.2));
|
||||||
assert_eq!(graph.top_p, Some(0.9));
|
assert_eq!(graph.top_p, Some(0.9));
|
||||||
assert_eq!(graph.global_tools, vec!["web_search_loki.sh"]);
|
assert_eq!(graph.global_tools, vec!["web_search_coyote.sh"]);
|
||||||
assert_eq!(graph.mcp_servers, vec!["pubmed-search"]);
|
assert_eq!(graph.mcp_servers, vec!["pubmed-search"]);
|
||||||
assert_eq!(graph.conversation_starters, vec!["Look up 2160-0"]);
|
assert_eq!(graph.conversation_starters, vec!["Look up 2160-0"]);
|
||||||
}
|
}
|
||||||
|
|||||||
+23
@@ -74,6 +74,7 @@ async fn main() -> Result<()> {
|
|||||||
|| cli.list_agents
|
|| cli.list_agents
|
||||||
|| cli.list_rags
|
|| cli.list_rags
|
||||||
|| cli.list_macros
|
|| cli.list_macros
|
||||||
|
|| cli.list_skills
|
||||||
|| cli.list_sessions;
|
|| cli.list_sessions;
|
||||||
let vault_flags = cli.add_secret.is_some()
|
let vault_flags = cli.add_secret.is_some()
|
||||||
|| cli.get_secret.is_some()
|
|| cli.get_secret.is_some()
|
||||||
@@ -191,6 +192,24 @@ async fn run(
|
|||||||
println!("{macros}");
|
println!("{macros}");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
if cli.list_skills {
|
||||||
|
let skills = paths::list_skills().join("\n");
|
||||||
|
println!("{skills}");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if cli.skill.len() == 1 && !paths::has_skill(&cli.skill[0]) {
|
||||||
|
let name = &cli.skill[0];
|
||||||
|
let app = Arc::clone(&ctx.app.config);
|
||||||
|
ctx.upsert_skill(app.as_ref(), name)?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if cli.skill.len() > 1 {
|
||||||
|
for name in &cli.skill {
|
||||||
|
if !paths::has_skill(name) {
|
||||||
|
bail!("Skill '{name}' is not installed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if cli.dry_run {
|
if cli.dry_run {
|
||||||
update_app_config(&mut ctx, |app| app.dry_run = true);
|
update_app_config(&mut ctx, |app| app.dry_run = true);
|
||||||
@@ -304,6 +323,10 @@ async fn run(
|
|||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for name in &cli.skill {
|
||||||
|
ctx.load_skill_repl(name, abort_signal.clone()).await?;
|
||||||
|
}
|
||||||
|
|
||||||
match is_repl {
|
match is_repl {
|
||||||
false => {
|
false => {
|
||||||
let mut input = create_input(&ctx, text, &cli.file, abort_signal.clone()).await?;
|
let mut input = create_input(&ctx, text, &cli.file, abort_signal.clone()).await?;
|
||||||
|
|||||||
@@ -369,7 +369,8 @@ mod tests {
|
|||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.expect("time went backwards")
|
.expect("time went backwards")
|
||||||
.as_nanos();
|
.as_nanos();
|
||||||
let path = std::env::temp_dir().join(format!("loki_python_parser_{file_name}_{unique}.py"));
|
let path =
|
||||||
|
std::env::temp_dir().join(format!("coyote_python_parser_{file_name}_{unique}.py"));
|
||||||
fs::write(&path, source).expect("failed to write temp python source");
|
fs::write(&path, source).expect("failed to write temp python source");
|
||||||
let file = File::open(&path).expect("failed to open temp python source");
|
let file = File::open(&path).expect("failed to open temp python source");
|
||||||
let result = generate_python_declarations(file, file_name, Some(parent));
|
let result = generate_python_declarations(file, file_name, Some(parent));
|
||||||
|
|||||||
@@ -425,7 +425,7 @@ mod tests {
|
|||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.expect("time")
|
.expect("time")
|
||||||
.as_nanos();
|
.as_nanos();
|
||||||
let path = std::env::temp_dir().join(format!("loki_ts_parser_{file_name}_{unique}.ts"));
|
let path = std::env::temp_dir().join(format!("coyote_ts_parser_{file_name}_{unique}.ts"));
|
||||||
fs::write(&path, source).expect("write");
|
fs::write(&path, source).expect("write");
|
||||||
let file = File::open(&path).expect("open");
|
let file = File::open(&path).expect("open");
|
||||||
let result = generate_typescript_declarations(file, file_name, Some(parent));
|
let result = generate_typescript_declarations(file, file_name, Some(parent));
|
||||||
|
|||||||
+66
-7
@@ -46,7 +46,7 @@ pub const DEFAULT_CONTINUATION_PROMPT: &str = indoc! {"
|
|||||||
4. Continue with the next pending item now. Call tools immediately."
|
4. Continue with the next pending item now. Call tools immediately."
|
||||||
};
|
};
|
||||||
|
|
||||||
static REPL_COMMANDS: LazyLock<[ReplCommand; 42]> = LazyLock::new(|| {
|
static REPL_COMMANDS: LazyLock<[ReplCommand; 44]> = LazyLock::new(|| {
|
||||||
[
|
[
|
||||||
ReplCommand::new(".help", "Show this help guide", AssertState::pass()),
|
ReplCommand::new(".help", "Show this help guide", AssertState::pass()),
|
||||||
ReplCommand::new(".info", "Show system info", AssertState::pass()),
|
ReplCommand::new(".info", "Show system info", AssertState::pass()),
|
||||||
@@ -191,6 +191,16 @@ static REPL_COMMANDS: LazyLock<[ReplCommand; 42]> = LazyLock::new(|| {
|
|||||||
AssertState::TrueFalse(StateFlags::RAG, StateFlags::AGENT),
|
AssertState::TrueFalse(StateFlags::RAG, StateFlags::AGENT),
|
||||||
),
|
),
|
||||||
ReplCommand::new(".macro", "Execute a macro", AssertState::pass()),
|
ReplCommand::new(".macro", "Execute a macro", AssertState::pass()),
|
||||||
|
ReplCommand::new(
|
||||||
|
".skill",
|
||||||
|
"List, load, unload, or create skills",
|
||||||
|
AssertState::pass(),
|
||||||
|
),
|
||||||
|
ReplCommand::new(
|
||||||
|
".edit skill",
|
||||||
|
"Modify an existing skill by name",
|
||||||
|
AssertState::pass(),
|
||||||
|
),
|
||||||
ReplCommand::new(
|
ReplCommand::new(
|
||||||
".file",
|
".file",
|
||||||
"Include files, directories, URLs or commands",
|
"Include files, directories, URLs or commands",
|
||||||
@@ -215,7 +225,7 @@ static REPL_COMMANDS: LazyLock<[ReplCommand; 42]> = LazyLock::new(|| {
|
|||||||
),
|
),
|
||||||
ReplCommand::new(
|
ReplCommand::new(
|
||||||
".vault",
|
".vault",
|
||||||
"View or modify the Loki vault",
|
"View or modify the Coyote vault",
|
||||||
AssertState::pass(),
|
AssertState::pass(),
|
||||||
),
|
),
|
||||||
ReplCommand::new(
|
ReplCommand::new(
|
||||||
@@ -225,7 +235,7 @@ static REPL_COMMANDS: LazyLock<[ReplCommand; 42]> = LazyLock::new(|| {
|
|||||||
),
|
),
|
||||||
ReplCommand::new(
|
ReplCommand::new(
|
||||||
".update",
|
".update",
|
||||||
"Update Loki to the latest release (or a specified version)",
|
"Update Coyote to the latest release (or a specified version)",
|
||||||
AssertState::pass(),
|
AssertState::pass(),
|
||||||
),
|
),
|
||||||
ReplCommand::new(".exit", "Exit REPL", AssertState::pass()),
|
ReplCommand::new(".exit", "Exit REPL", AssertState::pass()),
|
||||||
@@ -513,6 +523,41 @@ pub async fn run_repl_command(
|
|||||||
.role <name> [text]... # Temporarily switch to the role, send the text, and switch back"#
|
.role <name> [text]... # Temporarily switch to the role, send the text, and switch back"#
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
".skill" => {
|
||||||
|
let trimmed = args.map(str::trim).unwrap_or("");
|
||||||
|
let mut parts = trimmed.splitn(2, char::is_whitespace);
|
||||||
|
let first = parts.next().unwrap_or("");
|
||||||
|
let rest = parts.next().map(str::trim).unwrap_or("");
|
||||||
|
match first {
|
||||||
|
"" => println!(
|
||||||
|
r#"Usage:
|
||||||
|
.skill loaded # List currently-loaded skills
|
||||||
|
.skill load <name> # Load a skill into the current context
|
||||||
|
.skill unload <name> # Unload a loaded skill
|
||||||
|
.skill <name> # Open the skill in $EDITOR; create with a scaffold if missing
|
||||||
|
# (Use `.edit skill <name>` to edit an existing skill without the create-if-missing behavior.)"#
|
||||||
|
),
|
||||||
|
"loaded" => ctx.list_loaded_skills(),
|
||||||
|
"load" => {
|
||||||
|
if rest.is_empty() {
|
||||||
|
println!("Usage: .skill load <name>");
|
||||||
|
} else {
|
||||||
|
ctx.load_skill_repl(rest, abort_signal.clone()).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"unload" => {
|
||||||
|
if rest.is_empty() {
|
||||||
|
println!("Usage: .skill unload <name>");
|
||||||
|
} else {
|
||||||
|
ctx.unload_skill_repl(rest, abort_signal.clone()).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
name => {
|
||||||
|
let app = Arc::clone(&ctx.app.config);
|
||||||
|
ctx.upsert_skill(app.as_ref(), name)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
".session" => {
|
".session" => {
|
||||||
if let Some(name) = graph::active_agent_graph_name(ctx) {
|
if let Some(name) = graph::active_agent_graph_name(ctx) {
|
||||||
bail!(
|
bail!(
|
||||||
@@ -659,9 +704,23 @@ pub async fn run_repl_command(
|
|||||||
Some("mcp-config") => {
|
Some("mcp-config") => {
|
||||||
ctx.edit_mcp_config()?;
|
ctx.edit_mcp_config()?;
|
||||||
}
|
}
|
||||||
|
Some(s) if s == "skill" || s.starts_with("skill ") => {
|
||||||
|
let name = s.strip_prefix("skill").unwrap_or("").trim();
|
||||||
|
if name.is_empty() {
|
||||||
|
println!("Usage: .edit skill <name>");
|
||||||
|
} else if !paths::has_skill(name) {
|
||||||
|
bail!(
|
||||||
|
"Skill '{name}' is not installed (expected at {})",
|
||||||
|
paths::skill_file(name).display()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let app = Arc::clone(&ctx.app.config);
|
||||||
|
ctx.upsert_skill(app.as_ref(), name)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
println!(
|
println!(
|
||||||
r#"Usage: .edit <config|mcp-config|role|session|rag-docs|agent-config>"#
|
r#"Usage: .edit <config|mcp-config|role|session|rag-docs|agent-config|skill <name>>"#
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -779,7 +838,7 @@ pub async fn run_repl_command(
|
|||||||
ctx.delete(args)?;
|
ctx.delete(args)?;
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
println!("Usage: .delete <role|session|rag|macro|agent-data>")
|
println!("Usage: .delete <role|session|rag|macro|skill|agent-data>")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
".copy" => {
|
".copy" => {
|
||||||
@@ -1265,8 +1324,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn repl_commands_has_42_entries() {
|
fn repl_commands_has_44_entries() {
|
||||||
assert_eq!(REPL_COMMANDS.len(), 42);
|
assert_eq!(REPL_COMMANDS.len(), 44);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+1
-1
@@ -366,7 +366,7 @@ mod tests {
|
|||||||
assert!(is_valid_extension(Some(&md_ext), Path::new("Agents.md")));
|
assert!(is_valid_extension(Some(&md_ext), Path::new("Agents.md")));
|
||||||
assert!(is_valid_extension(
|
assert!(is_valid_extension(
|
||||||
Some(&md_ext),
|
Some(&md_ext),
|
||||||
Path::new("/home/atusa/code/loki.wiki/Agents.md")
|
Path::new("/home/atusa/code/coyote.wiki/Agents.md")
|
||||||
));
|
));
|
||||||
assert!(!is_valid_extension(Some(&md_ext), Path::new("notes.txt")));
|
assert!(!is_valid_extension(Some(&md_ext), Path::new("notes.txt")));
|
||||||
assert!(!is_valid_extension(Some(&md_ext), Path::new("README")));
|
assert!(!is_valid_extension(Some(&md_ext), Path::new("README")));
|
||||||
|
|||||||
+2
-2
@@ -28,7 +28,7 @@ pub fn ensure_password_file_initialized(local_provider: &mut LocalProvider) -> R
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Err(anyhow!(
|
Err(anyhow!(
|
||||||
"A password file is required to utilize the Loki vault. Please configure a password file in your config file and try again."
|
"A password file is required to utilize the Coyote vault. Please configure a password file in your config file and try again."
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,7 +95,7 @@ pub fn create_vault_password_file(vault: &mut Vault) -> Result<()> {
|
|||||||
|
|
||||||
if !ans {
|
if !ans {
|
||||||
return Err(anyhow!(
|
return Err(anyhow!(
|
||||||
"A password file is required to utilize the Loki vault. Please configure a password file in your config file and try again."
|
"A password file is required to utilize the Coyote vault. Please configure a password file in your config file and try again."
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user