Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7427865e46 | |||
| ff5f9c76fd | |||
| 641bbfbe2e | |||
| 5516ec4595 | |||
| 762467efd4 | |||
| 3f54bda360 | |||
| 4d1e388a48 | |||
| 10be86587a | |||
| 6a10591413 | |||
| 321c88e05e | |||
| 23562dfa61 | |||
| cb6ebf1d69 | |||
| a006985faf | |||
| 3fce9ec082 | |||
| 03026c70cc |
@@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"enabledMcpjsonServers": ["storkit"],
|
"enabledMcpjsonServers": [
|
||||||
|
"storkit"
|
||||||
|
],
|
||||||
"permissions": {
|
"permissions": {
|
||||||
"allow": [
|
"allow": [
|
||||||
"Bash(./server/target/debug/storkit:*)",
|
"Bash(./server/target/debug/storkit:*)",
|
||||||
@@ -67,7 +69,8 @@
|
|||||||
"Bash(tail *)",
|
"Bash(tail *)",
|
||||||
"Bash(wc *)",
|
"Bash(wc *)",
|
||||||
"Bash(npx vite:*)",
|
"Bash(npx vite:*)",
|
||||||
"Bash(npm run dev:*)"
|
"Bash(npm run dev:*)",
|
||||||
|
"Bash(stat *)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: "OAuth login button in web UI"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Story 446: OAuth login button in web UI
|
||||||
|
|
||||||
|
## User Story
|
||||||
|
|
||||||
|
As a user of the storkit web UI, I want a login button that triggers the Anthropic OAuth flow, so that I can authenticate without manually navigating to /oauth/authorize.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] Web UI shows a login/authenticate button when no OAuth token is active
|
||||||
|
- [ ] Clicking the button navigates to /oauth/authorize which starts the Anthropic OAuth flow
|
||||||
|
- [ ] After successful OAuth callback, the UI updates to show the authenticated state
|
||||||
|
- [ ] If already authenticated, the button is hidden or shows the current auth status
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
- TBD
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
name: "Element tab-completion display name breaks bot command matching"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Bug 447: Element tab-completion display name breaks bot command matching
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
When a user tab-completes a bot mention in Element, the Matrix client inserts the display name (e.g. `timmy ⚡️`) rather than the user ID (`@timmy`). If the display name contains emoji or special characters, the `strip_bot_mention` function in chat::util may fail to match it against the bot name, causing commands like `ambient on` to not be recognized.
|
||||||
|
|
||||||
|
## How to Reproduce
|
||||||
|
|
||||||
|
1. Set bot display_name to include emoji (e.g. `timmy ⚡️`) in bot.toml\n2. In Element, tab-complete the bot name to get `timmy ⚡️`\n3. Send `timmy ⚡️ ambient on`\n4. The bot does not respond — command not matched
|
||||||
|
|
||||||
|
## Actual Result
|
||||||
|
|
||||||
|
Bot ignores the command. The display name with emoji doesn't match during strip_bot_mention, so the command text is not correctly extracted.
|
||||||
|
|
||||||
|
## Expected Result
|
||||||
|
|
||||||
|
Bot should recognize commands regardless of whether the mention was tab-completed with the display name (including emoji) or typed manually as @localpart.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] strip_bot_mention handles display names containing emoji and special characters
|
||||||
|
- [ ] strip_bot_mention handles Element's tab-completion format (display name followed by colon or comma)
|
||||||
|
- [ ] Commands work whether the user types @timmy, timmy, or tab-completes timmy ⚡️
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: "Send OAuth login link via chat when credentials are missing"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Story 448: Send OAuth login link via chat when credentials are missing
|
||||||
|
|
||||||
|
## User Story
|
||||||
|
|
||||||
|
As a storkit user on Matrix or WhatsApp, I want the bot to send me a clickable OAuth authorize link when credentials are missing or expired, so that I can authenticate without terminal access or manually constructing the URL.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] When storkit detects missing or expired credentials during a chat interaction, it sends the user a clickable /oauth/authorize link
|
||||||
|
- [ ] Works on Matrix and WhatsApp transports
|
||||||
|
- [ ] After successful OAuth callback, the user can immediately resume chatting without restarting storkit
|
||||||
|
- [ ] If credentials are already valid, no login link is sent
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
- TBD
|
||||||
Generated
+114
-50
@@ -26,7 +26,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 = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
|
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crypto-common",
|
"crypto-common 0.1.7",
|
||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cipher",
|
"cipher",
|
||||||
"cpufeatures",
|
"cpufeatures 0.2.17",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -265,16 +265,16 @@ checksum = "a1d084b0137aaa901caf9f1e8b21daa6aa24d41cd806e111335541eff9683bd6"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "blake3"
|
name = "blake3"
|
||||||
version = "1.8.3"
|
version = "1.8.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d"
|
checksum = "4d2d5991425dfd0785aed03aedcf0b321d61975c9b5b3689c774a2610ae0b51e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayref",
|
"arrayref",
|
||||||
"arrayvec",
|
"arrayvec",
|
||||||
"cc",
|
"cc",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"constant_time_eq",
|
"constant_time_eq",
|
||||||
"cpufeatures",
|
"cpufeatures 0.3.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -286,6 +286,15 @@ dependencies = [
|
|||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "block-buffer"
|
||||||
|
version = "0.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be"
|
||||||
|
dependencies = [
|
||||||
|
"hybrid-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block-padding"
|
name = "block-padding"
|
||||||
version = "0.3.3"
|
version = "0.3.3"
|
||||||
@@ -391,7 +400,7 @@ checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cipher",
|
"cipher",
|
||||||
"cpufeatures",
|
"cpufeatures 0.2.17",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -427,7 +436,7 @@ version = "0.4.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crypto-common",
|
"crypto-common 0.1.7",
|
||||||
"inout",
|
"inout",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
@@ -492,6 +501,12 @@ version = "0.9.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "const-oid"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "const_panic"
|
name = "const_panic"
|
||||||
version = "0.2.15"
|
version = "0.2.15"
|
||||||
@@ -551,6 +566,15 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cpufeatures"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crc32fast"
|
name = "crc32fast"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@@ -596,6 +620,15 @@ dependencies = [
|
|||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crypto-common"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710"
|
||||||
|
dependencies = [
|
||||||
|
"hybrid-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ctr"
|
name = "ctr"
|
||||||
version = "0.9.2"
|
version = "0.9.2"
|
||||||
@@ -612,9 +645,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
|
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cpufeatures",
|
"cpufeatures 0.2.17",
|
||||||
"curve25519-dalek-derive",
|
"curve25519-dalek-derive",
|
||||||
"digest",
|
"digest 0.10.7",
|
||||||
"fiat-crypto",
|
"fiat-crypto",
|
||||||
"rustc_version",
|
"rustc_version",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -740,7 +773,7 @@ version = "0.7.10"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
|
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"const-oid",
|
"const-oid 0.9.6",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -813,11 +846,22 @@ version = "0.10.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"block-buffer",
|
"block-buffer 0.10.4",
|
||||||
"crypto-common",
|
"crypto-common 0.1.7",
|
||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "digest"
|
||||||
|
version = "0.11.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c"
|
||||||
|
dependencies = [
|
||||||
|
"block-buffer 0.12.0",
|
||||||
|
"const-oid 0.10.2",
|
||||||
|
"crypto-common 0.2.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "displaydoc"
|
name = "displaydoc"
|
||||||
version = "0.2.5"
|
version = "0.2.5"
|
||||||
@@ -862,7 +906,7 @@ dependencies = [
|
|||||||
"ed25519",
|
"ed25519",
|
||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
"serde",
|
"serde",
|
||||||
"sha2",
|
"sha2 0.10.9",
|
||||||
"subtle",
|
"subtle",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
@@ -1371,7 +1415,7 @@ version = "0.12.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"digest",
|
"digest 0.10.7",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1451,6 +1495,15 @@ version = "1.0.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hybrid-array"
|
||||||
|
version = "0.4.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1a79f2aff40c18ab8615ddc5caa9eb5b96314aef18fe5823090f204ad988e813"
|
||||||
|
dependencies = [
|
||||||
|
"typenum",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper"
|
name = "hyper"
|
||||||
version = "1.8.1"
|
version = "1.8.1"
|
||||||
@@ -1862,9 +1915,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.92"
|
version = "0.3.93"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cc4c90f45aa2e6eacbe8645f77fdea542ac97a494bcd117a67df9ff4d611f995"
|
checksum = "797146bb2677299a1eb6b7b50a890f4c361b29ef967addf5b2fa45dae1bb6d7d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
@@ -2158,7 +2211,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_html_form",
|
"serde_html_form",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2 0.10.9",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -2251,7 +2304,7 @@ dependencies = [
|
|||||||
"ruma",
|
"ruma",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2 0.10.9",
|
||||||
"subtle",
|
"subtle",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"time",
|
"time",
|
||||||
@@ -2286,7 +2339,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde-wasm-bindgen",
|
"serde-wasm-bindgen",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2 0.10.9",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
@@ -2340,7 +2393,7 @@ dependencies = [
|
|||||||
"rmp-serde",
|
"rmp-serde",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2 0.10.9",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
@@ -2599,7 +2652,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_path_to_error",
|
"serde_path_to_error",
|
||||||
"sha2",
|
"sha2 0.10.9",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
@@ -2657,7 +2710,7 @@ version = "0.12.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
|
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"digest",
|
"digest 0.10.7",
|
||||||
"hmac",
|
"hmac",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2842,7 +2895,7 @@ version = "0.8.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
|
checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cpufeatures",
|
"cpufeatures 0.2.17",
|
||||||
"opaque-debug",
|
"opaque-debug",
|
||||||
"universal-hash",
|
"universal-hash",
|
||||||
]
|
]
|
||||||
@@ -3504,7 +3557,7 @@ dependencies = [
|
|||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"ruma-common",
|
"ruma-common",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2 0.10.9",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3552,7 +3605,7 @@ version = "8.11.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1"
|
checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"sha2",
|
"sha2 0.10.9",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3876,8 +3929,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cpufeatures",
|
"cpufeatures 0.2.17",
|
||||||
"digest",
|
"digest 0.10.7",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3887,8 +3940,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cpufeatures",
|
"cpufeatures 0.2.17",
|
||||||
"digest",
|
"digest 0.10.7",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha2"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures 0.3.0",
|
||||||
|
"digest 0.11.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4019,7 +4083,7 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "storkit"
|
name = "storkit"
|
||||||
version = "0.8.2"
|
version = "0.8.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-stream",
|
"async-stream",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -4046,7 +4110,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
"sha2",
|
"sha2 0.11.0",
|
||||||
"strip-ansi-escapes",
|
"strip-ansi-escapes",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -4408,7 +4472,7 @@ dependencies = [
|
|||||||
"toml_datetime 1.1.0+spec-1.1.0",
|
"toml_datetime 1.1.0+spec-1.1.0",
|
||||||
"toml_parser",
|
"toml_parser",
|
||||||
"toml_writer",
|
"toml_writer",
|
||||||
"winnow 1.0.0",
|
"winnow 1.0.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4438,7 +4502,7 @@ dependencies = [
|
|||||||
"indexmap",
|
"indexmap",
|
||||||
"toml_datetime 1.1.0+spec-1.1.0",
|
"toml_datetime 1.1.0+spec-1.1.0",
|
||||||
"toml_parser",
|
"toml_parser",
|
||||||
"winnow 1.0.0",
|
"winnow 1.0.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4447,7 +4511,7 @@ version = "1.1.0+spec-1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011"
|
checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"winnow 1.0.0",
|
"winnow 1.0.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4681,7 +4745,7 @@ version = "0.5.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
|
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crypto-common",
|
"crypto-common 0.1.7",
|
||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -4781,7 +4845,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_bytes",
|
"serde_bytes",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2 0.10.9",
|
||||||
"subtle",
|
"subtle",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"x25519-dalek",
|
"x25519-dalek",
|
||||||
@@ -4851,9 +4915,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
version = "0.2.115"
|
version = "0.2.116"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6523d69017b7633e396a89c5efab138161ed5aafcbc8d3e5c5a42ae38f50495a"
|
checksum = "7dc0882f7b5bb01ae8c5215a1230832694481c1a4be062fd410e12ea3da5b631"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@@ -4864,9 +4928,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-futures"
|
name = "wasm-bindgen-futures"
|
||||||
version = "0.4.65"
|
version = "0.4.66"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2d1faf851e778dfa54db7cd438b70758eba9755cb47403f3496edd7c8fc212f0"
|
checksum = "19280959e2844181895ef62f065c63e0ca07ece4771b53d89bfdb967d97cbf05"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
@@ -4874,9 +4938,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro"
|
name = "wasm-bindgen-macro"
|
||||||
version = "0.2.115"
|
version = "0.2.116"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4e3a6c758eb2f701ed3d052ff5737f5bfe6614326ea7f3bbac7156192dc32e67"
|
checksum = "75973d3066e01d035dbedaad2864c398df42f8dd7b1ea057c35b8407c015b537"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"wasm-bindgen-macro-support",
|
"wasm-bindgen-macro-support",
|
||||||
@@ -4884,9 +4948,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro-support"
|
name = "wasm-bindgen-macro-support"
|
||||||
version = "0.2.115"
|
version = "0.2.116"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "921de2737904886b52bcbb237301552d05969a6f9c40d261eb0533c8b055fedf"
|
checksum = "91af5e4be765819e0bcfee7322c14374dc821e35e72fa663a830bbc7dc199eac"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -4897,9 +4961,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-shared"
|
name = "wasm-bindgen-shared"
|
||||||
version = "0.2.115"
|
version = "0.2.116"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a93e946af942b58934c604527337bad9ae33ba1d5c6900bbb41c2c07c2364a93"
|
checksum = "c9bf0406a78f02f336bf1e451799cca198e8acde4ffa278f0fb20487b150a633"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
@@ -4984,9 +5048,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "web-sys"
|
name = "web-sys"
|
||||||
version = "0.3.92"
|
version = "0.3.93"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "84cde8507f4d7cfcb1185b8cb5890c494ffea65edbe1ba82cfd63661c805ed94"
|
checksum = "749466a37ee189057f54748b200186b59a03417a117267baf3fd89cecc9fb837"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
@@ -5465,9 +5529,9 @@ checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winnow"
|
name = "winnow"
|
||||||
version = "1.0.0"
|
version = "1.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8"
|
checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@ rust-embed = "8"
|
|||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
serde_urlencoded = "0.7"
|
serde_urlencoded = "0.7"
|
||||||
sha2 = "0.10"
|
sha2 = "0.11.0"
|
||||||
serde_yaml = "0.9"
|
serde_yaml = "0.9"
|
||||||
strip-ansi-escapes = "0.2"
|
strip-ansi-escapes = "0.2"
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "living-spec-standalone",
|
"name": "living-spec-standalone",
|
||||||
"version": "0.8.2",
|
"version": "0.8.4",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "living-spec-standalone",
|
"name": "living-spec-standalone",
|
||||||
"version": "0.8.2",
|
"version": "0.8.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/react-syntax-highlighter": "^15.5.13",
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "living-spec-standalone",
|
"name": "living-spec-standalone",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.8.2",
|
"version": "0.8.4",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ vi.mock("./api/client", () => {
|
|||||||
setModelPreference: vi.fn(),
|
setModelPreference: vi.fn(),
|
||||||
cancelChat: vi.fn(),
|
cancelChat: vi.fn(),
|
||||||
setAnthropicApiKey: vi.fn(),
|
setAnthropicApiKey: vi.fn(),
|
||||||
|
getOAuthStatus: vi.fn(),
|
||||||
};
|
};
|
||||||
class ChatWebSocket {
|
class ChatWebSocket {
|
||||||
connect() {}
|
connect() {}
|
||||||
@@ -65,6 +66,7 @@ describe("App", () => {
|
|||||||
mockedApi.getAnthropicApiKeyExists.mockResolvedValue(false);
|
mockedApi.getAnthropicApiKeyExists.mockResolvedValue(false);
|
||||||
mockedApi.getAnthropicModels.mockResolvedValue([]);
|
mockedApi.getAnthropicModels.mockResolvedValue([]);
|
||||||
mockedApi.getModelPreference.mockResolvedValue(null);
|
mockedApi.getModelPreference.mockResolvedValue(null);
|
||||||
|
mockedApi.getOAuthStatus.mockResolvedValue({ authenticated: false, expired: false, expires_at: 0, has_refresh_token: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
async function renderApp() {
|
async function renderApp() {
|
||||||
|
|||||||
+28
-1
@@ -1,4 +1,5 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import type { OAuthStatus } from "./api/client";
|
||||||
import { api } from "./api/client";
|
import { api } from "./api/client";
|
||||||
import { Chat } from "./components/Chat";
|
import { Chat } from "./components/Chat";
|
||||||
import { SelectionScreen } from "./components/selection/SelectionScreen";
|
import { SelectionScreen } from "./components/selection/SelectionScreen";
|
||||||
@@ -14,6 +15,27 @@ function App() {
|
|||||||
const [isOpening, setIsOpening] = React.useState(false);
|
const [isOpening, setIsOpening] = React.useState(false);
|
||||||
const [knownProjects, setKnownProjects] = React.useState<string[]>([]);
|
const [knownProjects, setKnownProjects] = React.useState<string[]>([]);
|
||||||
const [homeDir, setHomeDir] = React.useState<string | null>(null);
|
const [homeDir, setHomeDir] = React.useState<string | null>(null);
|
||||||
|
const [oauthStatus, setOauthStatus] = React.useState<OAuthStatus | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
let active = true;
|
||||||
|
function fetchOAuthStatus() {
|
||||||
|
api
|
||||||
|
.getOAuthStatus()
|
||||||
|
.then((s) => {
|
||||||
|
if (active) setOauthStatus(s);
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}
|
||||||
|
fetchOAuthStatus();
|
||||||
|
const intervalId = window.setInterval(fetchOAuthStatus, 5000);
|
||||||
|
return () => {
|
||||||
|
active = false;
|
||||||
|
window.clearInterval(intervalId);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
api
|
api
|
||||||
@@ -182,10 +204,15 @@ function App() {
|
|||||||
onCloseSuggestions={closeSuggestions}
|
onCloseSuggestions={closeSuggestions}
|
||||||
completionError={completionError}
|
completionError={completionError}
|
||||||
currentPartial={currentPartial}
|
currentPartial={currentPartial}
|
||||||
|
oauthStatus={oauthStatus}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="workspace" style={{ height: "100%" }}>
|
<div className="workspace" style={{ height: "100%" }}>
|
||||||
<Chat projectPath={projectPath} onCloseProject={closeProject} />
|
<Chat
|
||||||
|
projectPath={projectPath}
|
||||||
|
onCloseProject={closeProject}
|
||||||
|
oauthStatus={oauthStatus}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -205,6 +205,13 @@ export interface CommandOutput {
|
|||||||
exit_code: number;
|
exit_code: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OAuthStatus {
|
||||||
|
authenticated: boolean;
|
||||||
|
expired: boolean;
|
||||||
|
expires_at: number;
|
||||||
|
has_refresh_token: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
declare const __STORKIT_PORT__: string;
|
declare const __STORKIT_PORT__: string;
|
||||||
|
|
||||||
const DEFAULT_API_BASE = "/api";
|
const DEFAULT_API_BASE = "/api";
|
||||||
@@ -402,6 +409,10 @@ export const api = {
|
|||||||
deleteStory(storyId: string) {
|
deleteStory(storyId: string) {
|
||||||
return callMcpTool("delete_story", { story_id: storyId });
|
return callMcpTool("delete_story", { story_id: storyId });
|
||||||
},
|
},
|
||||||
|
/** Fetch OAuth status from the server. */
|
||||||
|
getOAuthStatus() {
|
||||||
|
return requestJson<OAuthStatus>("/oauth/status", {}, "");
|
||||||
|
},
|
||||||
/** Execute a bot slash command without LLM invocation. Returns markdown response text. */
|
/** Execute a bot slash command without LLM invocation. Returns markdown response text. */
|
||||||
botCommand(command: string, args: string, baseUrl?: string) {
|
botCommand(command: string, args: string, baseUrl?: string) {
|
||||||
return requestJson<{ response: string }>(
|
return requestJson<{ response: string }>(
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type { AgentConfigInfo } from "../api/agents";
|
|||||||
import { agentsApi } from "../api/agents";
|
import { agentsApi } from "../api/agents";
|
||||||
import type {
|
import type {
|
||||||
AnthropicModelInfo,
|
AnthropicModelInfo,
|
||||||
|
OAuthStatus,
|
||||||
PipelineState,
|
PipelineState,
|
||||||
WizardStateData,
|
WizardStateData,
|
||||||
} from "../api/client";
|
} from "../api/client";
|
||||||
@@ -164,9 +165,10 @@ const getContextWindowSize = (
|
|||||||
interface ChatProps {
|
interface ChatProps {
|
||||||
projectPath: string;
|
projectPath: string;
|
||||||
onCloseProject: () => void;
|
onCloseProject: () => void;
|
||||||
|
oauthStatus?: OAuthStatus | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
export function Chat({ projectPath, onCloseProject, oauthStatus = null }: ChatProps) {
|
||||||
const { messages, setMessages, clearMessages } = useChatHistory(projectPath);
|
const { messages, setMessages, clearMessages } = useChatHistory(projectPath);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [model, setModel] = useState("claude-code-pty");
|
const [model, setModel] = useState("claude-code-pty");
|
||||||
@@ -615,12 +617,6 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
|||||||
const sendMessage = async (messageText: string) => {
|
const sendMessage = async (messageText: string) => {
|
||||||
if (!messageText.trim()) return;
|
if (!messageText.trim()) return;
|
||||||
|
|
||||||
// /help — show available slash commands overlay
|
|
||||||
if (/^\/help\s*$/i.test(messageText)) {
|
|
||||||
setShowHelp(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// /reset — clear session and message history without LLM
|
// /reset — clear session and message history without LLM
|
||||||
if (/^\/reset\s*$/i.test(messageText)) {
|
if (/^\/reset\s*$/i.test(messageText)) {
|
||||||
setMessages([]);
|
setMessages([]);
|
||||||
@@ -657,6 +653,14 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
|||||||
"overview",
|
"overview",
|
||||||
"rebuild",
|
"rebuild",
|
||||||
"loc",
|
"loc",
|
||||||
|
"help",
|
||||||
|
"ambient",
|
||||||
|
"htop",
|
||||||
|
"rmtree",
|
||||||
|
"timer",
|
||||||
|
"unblock",
|
||||||
|
"unreleased",
|
||||||
|
"setup",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (knownCommands.has(cmd)) {
|
if (knownCommands.has(cmd)) {
|
||||||
@@ -940,6 +944,7 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
|||||||
enableTools={enableTools}
|
enableTools={enableTools}
|
||||||
onToggleTools={setEnableTools}
|
onToggleTools={setEnableTools}
|
||||||
wsConnected={wsConnected}
|
wsConnected={wsConnected}
|
||||||
|
oauthStatus={oauthStatus}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Two-column content area */}
|
{/* Two-column content area */}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import type { OAuthStatus } from "../api/client";
|
||||||
import { api } from "../api/client";
|
import { api } from "../api/client";
|
||||||
|
|
||||||
const { useState, useEffect } = React;
|
const { useState, useEffect } = React;
|
||||||
@@ -32,6 +33,7 @@ interface ChatHeaderProps {
|
|||||||
enableTools: boolean;
|
enableTools: boolean;
|
||||||
onToggleTools: (enabled: boolean) => void;
|
onToggleTools: (enabled: boolean) => void;
|
||||||
wsConnected: boolean;
|
wsConnected: boolean;
|
||||||
|
oauthStatus?: OAuthStatus | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getContextEmoji = (percentage: number): string => {
|
const getContextEmoji = (percentage: number): string => {
|
||||||
@@ -55,6 +57,7 @@ export function ChatHeader({
|
|||||||
enableTools,
|
enableTools,
|
||||||
onToggleTools,
|
onToggleTools,
|
||||||
wsConnected,
|
wsConnected,
|
||||||
|
oauthStatus = null,
|
||||||
}: ChatHeaderProps) {
|
}: ChatHeaderProps) {
|
||||||
const hasModelOptions = availableModels.length > 0 || claudeModels.length > 0;
|
const hasModelOptions = availableModels.length > 0 || claudeModels.length > 0;
|
||||||
const [showConfirm, setShowConfirm] = useState(false);
|
const [showConfirm, setShowConfirm] = useState(false);
|
||||||
@@ -340,6 +343,59 @@ export function ChatHeader({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: "16px" }}>
|
<div style={{ display: "flex", alignItems: "center", gap: "16px" }}>
|
||||||
|
{oauthStatus !== null &&
|
||||||
|
(!oauthStatus.authenticated || oauthStatus.expired) && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
title="Authenticate with Claude via OAuth"
|
||||||
|
onClick={() => {
|
||||||
|
window.open("/oauth/authorize", "_blank", "noopener,noreferrer");
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
padding: "6px 12px",
|
||||||
|
borderRadius: "99px",
|
||||||
|
border: "none",
|
||||||
|
fontSize: "0.85em",
|
||||||
|
backgroundColor: "#1a3a5c",
|
||||||
|
color: "#7eb8f7",
|
||||||
|
cursor: "pointer",
|
||||||
|
outline: "none",
|
||||||
|
transition: "all 0.2s",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}}
|
||||||
|
onMouseOver={(e) => {
|
||||||
|
e.currentTarget.style.backgroundColor = "#234d7a";
|
||||||
|
e.currentTarget.style.color = "#a8d4ff";
|
||||||
|
}}
|
||||||
|
onMouseOut={(e) => {
|
||||||
|
e.currentTarget.style.backgroundColor = "#1a3a5c";
|
||||||
|
e.currentTarget.style.color = "#7eb8f7";
|
||||||
|
}}
|
||||||
|
onFocus={(e) => {
|
||||||
|
e.currentTarget.style.backgroundColor = "#234d7a";
|
||||||
|
e.currentTarget.style.color = "#a8d4ff";
|
||||||
|
}}
|
||||||
|
onBlur={(e) => {
|
||||||
|
e.currentTarget.style.backgroundColor = "#1a3a5c";
|
||||||
|
e.currentTarget.style.color = "#7eb8f7";
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{oauthStatus.expired ? "Re-authenticate" : "Login with Claude"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{oauthStatus?.authenticated && !oauthStatus.expired && (
|
||||||
|
<span
|
||||||
|
title="Authenticated with Claude via OAuth"
|
||||||
|
style={{
|
||||||
|
fontSize: "0.8em",
|
||||||
|
color: "#4caf50",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
✓ Claude
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontSize: "0.75em",
|
fontSize: "0.75em",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { KeyboardEvent } from "react";
|
import type { KeyboardEvent } from "react";
|
||||||
|
import type { OAuthStatus } from "../../api/client";
|
||||||
import { ProjectPathInput } from "./ProjectPathInput.tsx";
|
import { ProjectPathInput } from "./ProjectPathInput.tsx";
|
||||||
import { RecentProjectsList } from "./RecentProjectsList.tsx";
|
import { RecentProjectsList } from "./RecentProjectsList.tsx";
|
||||||
|
|
||||||
@@ -24,6 +25,7 @@ export interface SelectionScreenProps {
|
|||||||
onCloseSuggestions: () => void;
|
onCloseSuggestions: () => void;
|
||||||
completionError: string | null;
|
completionError: string | null;
|
||||||
currentPartial: string;
|
currentPartial: string;
|
||||||
|
oauthStatus?: OAuthStatus | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SelectionScreen({
|
export function SelectionScreen({
|
||||||
@@ -43,6 +45,7 @@ export function SelectionScreen({
|
|||||||
onCloseSuggestions,
|
onCloseSuggestions,
|
||||||
completionError,
|
completionError,
|
||||||
currentPartial,
|
currentPartial,
|
||||||
|
oauthStatus = null,
|
||||||
}: SelectionScreenProps) {
|
}: SelectionScreenProps) {
|
||||||
const resolvedHomeDir = homeDir
|
const resolvedHomeDir = homeDir
|
||||||
? homeDir.endsWith("/")
|
? homeDir.endsWith("/")
|
||||||
@@ -57,6 +60,37 @@ export function SelectionScreen({
|
|||||||
<h1>Storkit</h1>
|
<h1>Storkit</h1>
|
||||||
<p>Paste or complete a project path to start.</p>
|
<p>Paste or complete a project path to start.</p>
|
||||||
|
|
||||||
|
{oauthStatus !== null && (
|
||||||
|
<div style={{ marginBottom: "1rem" }}>
|
||||||
|
{!oauthStatus.authenticated || oauthStatus.expired ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
window.open("/oauth/authorize", "_blank", "noopener,noreferrer");
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
padding: "8px 16px",
|
||||||
|
borderRadius: "6px",
|
||||||
|
border: "1px solid #1a3a5c",
|
||||||
|
backgroundColor: "#1a3a5c",
|
||||||
|
color: "#7eb8f7",
|
||||||
|
cursor: "pointer",
|
||||||
|
fontSize: "0.9em",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{oauthStatus.expired ? "Re-authenticate with Claude" : "Login with Claude"}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
title="Authenticated with Claude via OAuth"
|
||||||
|
style={{ color: "#4caf50", fontSize: "0.9em" }}
|
||||||
|
>
|
||||||
|
✓ Authenticated with Claude
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{knownProjects.length > 0 && (
|
{knownProjects.length > 0 && (
|
||||||
<RecentProjectsList
|
<RecentProjectsList
|
||||||
projects={knownProjects}
|
projects={knownProjects}
|
||||||
|
|||||||
@@ -50,10 +50,48 @@ export const SLASH_COMMANDS: SlashCommand[] = [
|
|||||||
name: "/overview <number>",
|
name: "/overview <number>",
|
||||||
description: "Show the implementation summary for a merged story.",
|
description: "Show the implementation summary for a merged story.",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "/ambient",
|
||||||
|
description: "Toggle ambient mode: `/ambient on` or `/ambient off`.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "/htop",
|
||||||
|
description:
|
||||||
|
"Show live system and agent process dashboard: `/htop`, `/htop 10m`, `/htop stop`.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "/loc",
|
||||||
|
description:
|
||||||
|
"Show top source files by line count: `/loc` (top 10), `/loc <N>`, or `/loc <filepath>`.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "/rmtree <number>",
|
||||||
|
description:
|
||||||
|
"Delete the worktree for a story without removing it from the pipeline.",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "/rebuild",
|
name: "/rebuild",
|
||||||
description: "Rebuild the server binary and restart.",
|
description: "Rebuild the server binary and restart.",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "/timer",
|
||||||
|
description:
|
||||||
|
"Schedule a deferred agent start: `/timer <story_id> <HH:MM>`, `/timer list`, `/timer cancel <story_id>`.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "/unblock <number>",
|
||||||
|
description:
|
||||||
|
"Reset a blocked story: clears blocked flag and resets retry count.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "/unreleased",
|
||||||
|
description: "Show stories merged to master since the last release tag.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "/setup",
|
||||||
|
description:
|
||||||
|
"Show setup wizard progress; or `/setup confirm` / `/setup skip` / `/setup retry` to drive the wizard.",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "/reset",
|
name: "/reset",
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -30,6 +30,20 @@ export default defineConfig(() => {
|
|||||||
proxy.on("error", (_err) => {});
|
proxy.on("error", (_err) => {});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"/oauth": {
|
||||||
|
target: `http://127.0.0.1:${String(backendPort)}`,
|
||||||
|
timeout: 120000,
|
||||||
|
configure: (proxy) => {
|
||||||
|
proxy.on("error", (_err) => {});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"/callback": {
|
||||||
|
target: `http://127.0.0.1:${String(backendPort)}`,
|
||||||
|
timeout: 120000,
|
||||||
|
configure: (proxy) => {
|
||||||
|
proxy.on("error", (_err) => {});
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
ignored: [
|
ignored: [
|
||||||
|
|||||||
+5
-1
@@ -81,9 +81,12 @@ echo "==> Releasing ${TAG}"
|
|||||||
echo "==> Building macOS (native)..."
|
echo "==> Building macOS (native)..."
|
||||||
cargo build --release
|
cargo build --release
|
||||||
|
|
||||||
echo "==> Building Linux (static musl via cross)..."
|
echo "==> Building Linux amd64 (static musl via cross)..."
|
||||||
cross build --release --target x86_64-unknown-linux-musl
|
cross build --release --target x86_64-unknown-linux-musl
|
||||||
|
|
||||||
|
echo "==> Building Linux arm64 (static musl via cross)..."
|
||||||
|
cross build --release --target aarch64-unknown-linux-musl
|
||||||
|
|
||||||
# ── Package ────────────────────────────────────────────────────
|
# ── Package ────────────────────────────────────────────────────
|
||||||
DIST="target/dist"
|
DIST="target/dist"
|
||||||
rm -rf "$DIST"
|
rm -rf "$DIST"
|
||||||
@@ -91,6 +94,7 @@ mkdir -p "$DIST"
|
|||||||
|
|
||||||
cp "target/release/${BINARY_NAME}" "${DIST}/${BINARY_NAME}-macos-arm64"
|
cp "target/release/${BINARY_NAME}" "${DIST}/${BINARY_NAME}-macos-arm64"
|
||||||
cp "target/x86_64-unknown-linux-musl/release/${BINARY_NAME}" "${DIST}/${BINARY_NAME}-linux-amd64"
|
cp "target/x86_64-unknown-linux-musl/release/${BINARY_NAME}" "${DIST}/${BINARY_NAME}-linux-amd64"
|
||||||
|
cp "target/aarch64-unknown-linux-musl/release/${BINARY_NAME}" "${DIST}/${BINARY_NAME}-linux-arm64"
|
||||||
chmod +x "${DIST}"/*
|
chmod +x "${DIST}"/*
|
||||||
|
|
||||||
echo "==> Binaries:"
|
echo "==> Binaries:"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "storkit"
|
name = "storkit"
|
||||||
version = "0.8.2"
|
version = "0.8.4"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
|
|||||||
@@ -616,7 +616,13 @@ pub(super) async fn handle_message(
|
|||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
slog!("[matrix-bot] LLM error: {e}");
|
slog!("[matrix-bot] LLM error: {e}");
|
||||||
let err_msg = format!("Error processing your request: {e}");
|
let err_msg = if let Some(url) = crate::llm::oauth::extract_login_url_from_error(&e) {
|
||||||
|
format!(
|
||||||
|
"Authentication required. [Click here to log in to Claude]({url})"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
format!("Error processing your request: {e}")
|
||||||
|
};
|
||||||
let _ = msg_tx.send(err_msg.clone());
|
let _ = msg_tx.send(err_msg.clone());
|
||||||
(err_msg, None)
|
(err_msg, None)
|
||||||
}
|
}
|
||||||
@@ -686,6 +692,24 @@ mod tests {
|
|||||||
assert_eq!(prompt, "@bob:example.com: What's up?");
|
assert_eq!(prompt, "@bob:example.com: What's up?");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -- OAuth login link formatting ----------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn oauth_error_produces_login_link() {
|
||||||
|
let err = "OAuth session expired or credentials missing. Please log in: http://localhost:3001/oauth/authorize";
|
||||||
|
let url = crate::llm::oauth::extract_login_url_from_error(err);
|
||||||
|
assert!(url.is_some(), "should extract URL from OAuth error");
|
||||||
|
let msg = format!("Authentication required. [Click here to log in to Claude]({})", url.unwrap());
|
||||||
|
assert!(msg.contains("http://localhost:3001/oauth/authorize"));
|
||||||
|
assert!(msg.contains("[Click here to log in to Claude]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn non_oauth_error_not_formatted_as_link() {
|
||||||
|
let err = "Some unrelated error";
|
||||||
|
assert!(crate::llm::oauth::extract_login_url_from_error(err).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
// -- bot_name / system prompt -------------------------------------------
|
// -- bot_name / system prompt -------------------------------------------
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -383,7 +383,13 @@ async fn handle_llm_message(ctx: &WhatsAppWebhookContext, sender: &str, user_mes
|
|||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
slog!("[whatsapp] LLM error: {e}");
|
slog!("[whatsapp] LLM error: {e}");
|
||||||
let err_msg = format!("Error processing your request: {e}");
|
let err_msg = if let Some(url) = crate::llm::oauth::extract_login_url_from_error(&e) {
|
||||||
|
format!(
|
||||||
|
"Authentication required. Log in to Claude here: {url}"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
format!("Error processing your request: {e}")
|
||||||
|
};
|
||||||
let _ = msg_tx.send(err_msg.clone());
|
let _ = msg_tx.send(err_msg.clone());
|
||||||
(err_msg, None)
|
(err_msg, None)
|
||||||
}
|
}
|
||||||
@@ -491,6 +497,24 @@ mod tests {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── OAuth login link formatting ───────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn whatsapp_oauth_error_produces_plain_text_url() {
|
||||||
|
let err = "OAuth session expired or credentials missing. Please log in: http://localhost:3001/oauth/authorize";
|
||||||
|
let url = crate::llm::oauth::extract_login_url_from_error(err);
|
||||||
|
assert!(url.is_some(), "should extract URL from OAuth error");
|
||||||
|
let msg = format!("Authentication required. Log in to Claude here: {}", url.unwrap());
|
||||||
|
assert!(msg.contains("http://localhost:3001/oauth/authorize"));
|
||||||
|
assert!(!msg.contains('['), "WhatsApp message should not use Markdown link syntax");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn whatsapp_non_oauth_error_not_formatted_as_link() {
|
||||||
|
let err = "Some unrelated error occurred during processing";
|
||||||
|
assert!(crate::llm::oauth::extract_login_url_from_error(err).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
// ── Allowlist tests ───────────────────────────────────────────────────
|
// ── Allowlist tests ───────────────────────────────────────────────────
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
+49
-5
@@ -46,29 +46,44 @@ pub fn strip_prefix_ci<'a>(text: &'a str, prefix: &str) -> Option<&'a str> {
|
|||||||
/// - `@bot_localpart:server.com rest` → `rest`
|
/// - `@bot_localpart:server.com rest` → `rest`
|
||||||
/// - `@bot_localpart rest` → `rest`
|
/// - `@bot_localpart rest` → `rest`
|
||||||
/// - `DisplayName rest` → `rest`
|
/// - `DisplayName rest` → `rest`
|
||||||
|
/// - `DisplayName: rest` → `rest` (Element tab-completion inserts a colon)
|
||||||
|
/// - `DisplayName, rest` → `rest` (Element tab-completion may insert a comma)
|
||||||
|
/// - `DisplayName ⚡️: rest` → `rest` (display name with emoji)
|
||||||
pub fn strip_bot_mention<'a>(message: &'a str, bot_name: &str, bot_user_id: &str) -> &'a str {
|
pub fn strip_bot_mention<'a>(message: &'a str, bot_name: &str, bot_user_id: &str) -> &'a str {
|
||||||
let trimmed = message.trim();
|
let trimmed = message.trim();
|
||||||
|
|
||||||
// Try full Matrix user ID (e.g. "@timmy:homeserver.local")
|
// Try full Matrix user ID (e.g. "@timmy:homeserver.local")
|
||||||
if let Some(rest) = strip_prefix_ci(trimmed, bot_user_id) {
|
if let Some(rest) = strip_prefix_ci(trimmed, bot_user_id) {
|
||||||
return rest;
|
return strip_mention_separator(rest);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try @localpart (e.g. "@timmy")
|
// Try @localpart (e.g. "@timmy")
|
||||||
if let Some(localpart) = bot_user_id.split(':').next()
|
if let Some(localpart) = bot_user_id.split(':').next()
|
||||||
&& let Some(rest) = strip_prefix_ci(trimmed, localpart)
|
&& let Some(rest) = strip_prefix_ci(trimmed, localpart)
|
||||||
{
|
{
|
||||||
return rest;
|
return strip_mention_separator(rest);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try display name (e.g. "Timmy")
|
// Try display name (e.g. "Timmy" or "timmy ⚡️")
|
||||||
if let Some(rest) = strip_prefix_ci(trimmed, bot_name) {
|
if let Some(rest) = strip_prefix_ci(trimmed, bot_name) {
|
||||||
return rest;
|
return strip_mention_separator(rest);
|
||||||
}
|
}
|
||||||
|
|
||||||
trimmed
|
trimmed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Strip an optional Element tab-completion separator (`:` or `,`) and
|
||||||
|
/// surrounding whitespace from the start of text that follows a bot mention.
|
||||||
|
///
|
||||||
|
/// Element's tab-completion inserts `DisplayName: ` (colon + space) after the
|
||||||
|
/// name. Without this strip the leading `:` would be treated as part of the
|
||||||
|
/// command name and no command would match.
|
||||||
|
fn strip_mention_separator(rest: &str) -> &str {
|
||||||
|
let rest = rest.trim_start();
|
||||||
|
let rest = rest.strip_prefix([',', ':']).unwrap_or(rest);
|
||||||
|
rest.trim_start()
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns `true` when `text` ends while inside an open fenced code block.
|
/// Returns `true` when `text` ends while inside an open fenced code block.
|
||||||
///
|
///
|
||||||
/// A fenced code block opens and closes on lines that start with ` ``` `
|
/// A fenced code block opens and closes on lines that start with ` ``` `
|
||||||
@@ -334,7 +349,36 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn strip_mention_comma_after_name() {
|
fn strip_mention_comma_after_name() {
|
||||||
let rest = strip_bot_mention("@timmy, help", "Timmy", "@timmy:homeserver.local");
|
let rest = strip_bot_mention("@timmy, help", "Timmy", "@timmy:homeserver.local");
|
||||||
assert_eq!(rest.trim().trim_start_matches(',').trim(), "help");
|
assert_eq!(rest.trim(), "help");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn strip_mention_colon_separator_element_tab_completion() {
|
||||||
|
// Element tab-completes display names with a trailing ": "
|
||||||
|
let rest = strip_bot_mention(
|
||||||
|
"timmy ⚡️: ambient on",
|
||||||
|
"timmy ⚡️",
|
||||||
|
"@timmy:homeserver.local",
|
||||||
|
);
|
||||||
|
assert_eq!(rest, "ambient on");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn strip_mention_emoji_display_name_no_separator() {
|
||||||
|
// Display name with emoji, no separator
|
||||||
|
let rest = strip_bot_mention(
|
||||||
|
"timmy ⚡️ ambient on",
|
||||||
|
"timmy ⚡️",
|
||||||
|
"@timmy:homeserver.local",
|
||||||
|
);
|
||||||
|
assert_eq!(rest, "ambient on");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn strip_mention_colon_after_localpart() {
|
||||||
|
// Element may also produce "@timmy: help"
|
||||||
|
let rest = strip_bot_mention("@timmy: help", "Timmy", "@timmy:homeserver.local");
|
||||||
|
assert_eq!(rest, "help");
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- drain_complete_paragraphs ------------------------------------------
|
// -- drain_complete_paragraphs ------------------------------------------
|
||||||
|
|||||||
@@ -161,10 +161,43 @@ pub async fn refresh_access_token() -> Result<(), String> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extract the OAuth login URL from an error message produced by the Claude Code provider.
|
||||||
|
///
|
||||||
|
/// The provider returns errors like:
|
||||||
|
/// `"OAuth session expired or credentials missing. Please log in: http://localhost:3001/oauth/authorize"`
|
||||||
|
///
|
||||||
|
/// Returns the URL portion when the error indicates missing or expired credentials,
|
||||||
|
/// `None` otherwise.
|
||||||
|
pub fn extract_login_url_from_error(err: &str) -> Option<&str> {
|
||||||
|
let marker = "Please log in: ";
|
||||||
|
let start = err.find(marker)?;
|
||||||
|
Some(err[start + marker.len()..].trim())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_login_url_from_oauth_error() {
|
||||||
|
let err = "OAuth session expired or credentials missing. Please log in: http://localhost:3001/oauth/authorize";
|
||||||
|
let url = extract_login_url_from_error(err);
|
||||||
|
assert_eq!(url, Some("http://localhost:3001/oauth/authorize"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_login_url_returns_none_for_unrelated_error() {
|
||||||
|
let err = "Some other error occurred";
|
||||||
|
assert!(extract_login_url_from_error(err).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_login_url_with_different_port() {
|
||||||
|
let err = "OAuth session expired or credentials missing. Please log in: http://localhost:3002/oauth/authorize";
|
||||||
|
let url = extract_login_url_from_error(err);
|
||||||
|
assert_eq!(url, Some("http://localhost:3002/oauth/authorize"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_credentials_file() {
|
fn parse_credentials_file() {
|
||||||
let json = r#"{
|
let json = r#"{
|
||||||
|
|||||||
@@ -138,9 +138,11 @@ impl ClaudeCodeProvider {
|
|||||||
on_token("\n*Refreshing authentication token...*\n");
|
on_token("\n*Refreshing authentication token...*\n");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(_e) => {
|
||||||
|
let port = crate::http::resolve_port();
|
||||||
|
let login_url = format!("http://localhost:{port}/oauth/authorize");
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"OAuth session expired. Please run `claude login` to re-authenticate. ({e})"
|
"OAuth session expired or credentials missing. Please log in: {login_url}"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-3
@@ -123,7 +123,32 @@ pub async fn rebuild_and_restart(
|
|||||||
workspace_root.display()
|
workspace_root.display()
|
||||||
);
|
);
|
||||||
|
|
||||||
// 3. Build the server binary, matching the current build profile so the
|
// 3. Rebuild the frontend bundle so rust-embed picks up the latest assets.
|
||||||
|
let frontend_dir = workspace_root.join("frontend");
|
||||||
|
if frontend_dir.join("package.json").exists() {
|
||||||
|
slog!("[rebuild] Building frontend");
|
||||||
|
let fe_output = tokio::task::spawn_blocking({
|
||||||
|
let frontend_dir = frontend_dir.clone();
|
||||||
|
move || {
|
||||||
|
std::process::Command::new("npm")
|
||||||
|
.args(["run", "build"])
|
||||||
|
.current_dir(&frontend_dir)
|
||||||
|
.output()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Frontend build task panicked: {e}"))?
|
||||||
|
.map_err(|e| format!("Failed to run npm run build: {e}"))?;
|
||||||
|
|
||||||
|
if !fe_output.status.success() {
|
||||||
|
let stderr = String::from_utf8_lossy(&fe_output.stderr);
|
||||||
|
slog!("[rebuild] Frontend build failed:\n{stderr}");
|
||||||
|
return Err(format!("Frontend build failed:\n{stderr}"));
|
||||||
|
}
|
||||||
|
slog!("[rebuild] Frontend build succeeded");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Build the server binary, matching the current build profile so the
|
||||||
// re-exec via current_exe() picks up the new binary.
|
// re-exec via current_exe() picks up the new binary.
|
||||||
let build_args: Vec<&str> = if cfg!(debug_assertions) {
|
let build_args: Vec<&str> = if cfg!(debug_assertions) {
|
||||||
vec!["build", "-p", "storkit"]
|
vec!["build", "-p", "storkit"]
|
||||||
@@ -152,14 +177,14 @@ pub async fn rebuild_and_restart(
|
|||||||
|
|
||||||
slog!("[rebuild] Build succeeded, re-execing with new binary");
|
slog!("[rebuild] Build succeeded, re-execing with new binary");
|
||||||
|
|
||||||
// 4. Send shutdown notification before replacing the process so that chat
|
// 5. Send shutdown notification before replacing the process so that chat
|
||||||
// participants know the bot is going offline. Best-effort only — we
|
// participants know the bot is going offline. Best-effort only — we
|
||||||
// do not abort the rebuild if the send fails.
|
// do not abort the rebuild if the send fails.
|
||||||
if let Some(n) = notifier {
|
if let Some(n) = notifier {
|
||||||
n.notify(ShutdownReason::Rebuild).await;
|
n.notify(ShutdownReason::Rebuild).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Re-exec with the new binary.
|
// 6. Re-exec with the new binary.
|
||||||
// Use the cargo output path rather than current_exe() so that rebuilds
|
// Use the cargo output path rather than current_exe() so that rebuilds
|
||||||
// inside Docker work correctly — the running binary may be installed at
|
// inside Docker work correctly — the running binary may be installed at
|
||||||
// /usr/local/bin/storkit (read-only) while cargo writes the new binary
|
// /usr/local/bin/storkit (read-only) while cargo writes the new binary
|
||||||
|
|||||||
Reference in New Issue
Block a user