Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 488b798275 | |||
| 0df19967ca | |||
| 6e04015676 | |||
| acaf9477a1 | |||
| 46a89d481a | |||
| c51428414e | |||
| 50405800c6 | |||
| 4aca056bc9 | |||
| 5e725340b4 | |||
| 3fa2064e3e | |||
| 16f9722851 | |||
| 5f0680c6c1 | |||
| 57e0197d75 | |||
| dc4bac3a85 | |||
| f16545ec36 | |||
| d132ed8e64 | |||
| 2a633d604a | |||
| 6a44c0b8ee | |||
| 3f97e34f21 | |||
| 49a8a23d75 | |||
| 1358a32476 | |||
| 9b79160c95 | |||
| 0cbe99677f | |||
| 46b1609528 | |||
| 2b0b08ceda | |||
| 19cc684433 | |||
| fecb157291 | |||
| ac84e7240e | |||
| d5d82bdb00 | |||
| f10edd6718 | |||
| 3f6cd55833 | |||
| a9e8bc4d87 | |||
| 063e0fa76e | |||
| 9e7bd33822 | |||
| 7427865e46 | |||
| ff5f9c76fd | |||
| 641bbfbe2e | |||
| 5516ec4595 | |||
| 762467efd4 | |||
| 3f54bda360 | |||
| 4d1e388a48 | |||
| 10be86587a | |||
| 6a10591413 | |||
| 321c88e05e | |||
| 23562dfa61 | |||
| cb6ebf1d69 |
@@ -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 *)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
name: "Zombie process accumulation from unrereaped child processes"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Bug 452: Zombie process accumulation from unrereaped child processes
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Storkit accumulates zombie processes over time from unrereaped child and grandchild processes. Observed 101 zombies in Docker container, 27 on macOS host. Breakdown: 51 esbuild, 36 echo, 5 claude, 5 sh, 2 bash, 1 cargo.
|
||||||
|
|
||||||
|
Root cause: storkit does not reap orphaned grandchild processes. The zombies are mostly grandchildren (`esbuild`, `echo`, `sh`, `cargo`) spawned by `npm run build`, `cargo test`, etc. during worktree setup and gate checks. This happens both natively (observed 27 zombies on macOS host) and in Docker containers. When the intermediate parent exits, these grandchildren get reparented to storkit (or PID 1 in Docker) and become zombies because nobody calls `waitpid` for them.
|
||||||
|
|
||||||
|
**Already fixed:**
|
||||||
|
- `docker-compose.yml` now has `init: true` which uses tini as PID 1 in Docker — this handles zombie reaping inside containers
|
||||||
|
- `llm/providers/claude_code.rs` now has `child.wait()` after `child.kill()` in all code paths, and the reader thread is joined before returning
|
||||||
|
- `agents/pty.rs` reader thread is now joined before returning
|
||||||
|
|
||||||
|
**Remaining:** Storkit running natively (e.g. on macOS) still accumulates zombie grandchildren because there is no tini. The fix is to add a background reaper thread that periodically calls `waitpid(-1, WNOHANG)` in a loop to clean up any orphaned children. This should be spawned early in `main()` on Unix platforms. Example:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[cfg(unix)]
|
||||||
|
std::thread::spawn(|| {
|
||||||
|
loop {
|
||||||
|
unsafe { while libc::waitpid(-1, std::ptr::null_mut(), libc::WNOHANG) > 0 {} }
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs(5));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## How to Reproduce
|
||||||
|
|
||||||
|
Run several agent sessions. Check with `ps -eo stat,comm | grep Z | awk '{print $2}' | sort | uniq -c | sort -rn`.
|
||||||
|
|
||||||
|
## Actual Result
|
||||||
|
|
||||||
|
Zombie processes accumulate continuously. Never reaped.
|
||||||
|
|
||||||
|
## Expected Result
|
||||||
|
|
||||||
|
No zombie accumulation during normal operation.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [x] `child.wait()` is called after `child.kill()` in all code paths in `claude_code.rs`
|
||||||
|
- [x] Reader threads are joined in both `pty.rs` and `claude_code.rs`
|
||||||
|
- [x] `init: true` added to docker-compose.yml for Docker deployments
|
||||||
|
- [ ] Background reaper thread added for native (non-Docker) deployments
|
||||||
|
- [ ] Verified with `ps aux | grep '<defunct>'` after running multiple agent sessions natively on macOS
|
||||||
+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
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
name: "OAuth callback URL ignores --port CLI flag"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Bug 449: OAuth callback URL ignores --port CLI flag
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
OAuthState is initialized with `resolve_port()` (reads STORKIT_PORT env var, defaults to 3001) instead of the actual port the server is listening on. When the server is started with `--port 4000`, the OAuth callback URL is still generated as `http://localhost:3001/callback`, so the Anthropic redirect lands on the wrong server and the state parameter lookup fails with "Unknown or expired state parameter".
|
||||||
|
|
||||||
|
## How to Reproduce
|
||||||
|
|
||||||
|
Start storkit with `--port 4000` (without setting STORKIT_PORT env var). Click the OAuth login button in the web UI. Authenticate with Anthropic. The callback redirect goes to localhost:3001 instead of localhost:4000.
|
||||||
|
|
||||||
|
## Actual Result
|
||||||
|
|
||||||
|
Callback hits port 3001 (or wrong port). If a different storkit is running there, it returns "Invalid State". If nothing is running there, the page fails to load.
|
||||||
|
|
||||||
|
## Expected Result
|
||||||
|
|
||||||
|
Callback URL should use the actual server port (from --port CLI flag), so the redirect returns to the correct server instance.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] build_routes receives the actual listening port and passes it to OAuthState::new
|
||||||
|
- [ ] OAuth callback URL matches the port the server is actually listening on
|
||||||
|
- [ ] Works with --port flag, STORKIT_PORT env var, and default port
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
name: "Web UI silently swallows chat errors including OAuth login link"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Bug 450: Web UI silently swallows chat errors including OAuth login link
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
When the WebSocket chat returns an error (e.g. OAuth authentication failed with a login URL), the `onError` handler in `Chat.tsx` only logs to `console.error` and resets loading state. The error message is never displayed to the user. This means the OAuth login link from story #448 works on Matrix/WhatsApp but is invisible in the web UI.
|
||||||
|
|
||||||
|
## How to Reproduce
|
||||||
|
|
||||||
|
Use the web UI with missing or expired OAuth credentials. Send any chat message. The server detects auth failure, attempts token refresh, fails, and returns an error containing a login URL over WebSocket.
|
||||||
|
|
||||||
|
## Actual Result
|
||||||
|
|
||||||
|
Nothing visible happens. The error is logged to browser console only. The user sees no feedback.
|
||||||
|
|
||||||
|
## Expected Result
|
||||||
|
|
||||||
|
The error message (including the clickable OAuth login link) should be displayed in the chat as an assistant message so the user can act on it.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] WebSocket error messages are displayed in the chat UI as assistant messages
|
||||||
|
- [ ] OAuth login URL in the error is rendered as a clickable link
|
||||||
|
- [ ] Consistent with how Matrix and WhatsApp transports display the same error
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
name: "Chat.test.tsx /help test expects removed overlay behavior"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Bug 451: Chat.test.tsx /help test expects removed overlay behavior
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
The test `AC: /help shows help overlay` in `Chat.test.tsx:1645` expects `/help` to show the `help-overlay` testid element. However, the `/help` intercept was removed from Chat.tsx and `help` was added to `knownCommands`, so `/help` now goes through `api.botCommand()` like other commands. The test needs to be updated to expect a `botCommand("help", ...)` call instead of the overlay. This is blocking gates on stories 449 and 450.
|
||||||
|
|
||||||
|
## How to Reproduce
|
||||||
|
|
||||||
|
Run `cd frontend && npm test` — the test `AC: /help shows help overlay` fails.
|
||||||
|
|
||||||
|
## Actual Result
|
||||||
|
|
||||||
|
Test fails: `findByTestId("help-overlay")` times out because the overlay is never rendered. `/help` is dispatched to the backend via `botCommand` instead.
|
||||||
|
|
||||||
|
## Expected Result
|
||||||
|
|
||||||
|
Test should pass by expecting `/help` to call `api.botCommand("help", ...)` and display the response in chat, consistent with the current code behavior.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] The /help test in Chat.test.tsx is updated to expect botCommand dispatch
|
||||||
|
- [ ] All frontend tests pass
|
||||||
|
- [ ] HelpOverlay component and showHelp state can be removed from Chat.tsx if no longer used
|
||||||
+53
@@ -0,0 +1,53 @@
|
|||||||
|
---
|
||||||
|
name: "Agent PTY crashes with fatal runtime error on restart after gate failure"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Bug 453: Agent PTY crashes with fatal runtime error on restart after gate failure
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
When an agent completes coding and the acceptance gates fail (e.g. a test failure), the pipeline restarts the agent on the same worktree. The restarted Claude Code PTY process crashes immediately with `fatal runtime error: assertion failed: output.write(&bytes).is_ok(), aborting`. The process exits in the same second it spawns (Session: None), burns through all 3 retries, and blocks the story.
|
||||||
|
|
||||||
|
Key observations:
|
||||||
|
- The crash is **deterministic, not intermittent**: the first PTY spawn in a worktree always works; the second spawn (restart) always crashes
|
||||||
|
- Running `claude -p "hello"` manually in the same worktree works fine (no crash) — the issue is specific to spawning via portable-pty
|
||||||
|
- The worktree is clean (all changes committed) — the agent has nothing to do but fix the gate failure
|
||||||
|
- The crash is inside the Claude Code binary, not storkit code
|
||||||
|
- Observed on every story that needed a restart: 329, 400, 420, 438, 446, 449, 450
|
||||||
|
- Stories that passed gates on the first run were never affected — they never triggered a second spawn
|
||||||
|
|
||||||
|
Likely cause: the reader thread spawned by `std::thread::spawn` in `pty.rs` (line 248-255) is never joined. After `run_agent_pty_streaming` returns, the pipeline immediately calls `start_agent` for the retry, but the old reader thread may still be running and holding a cloned PTY reader fd. The new PTY allocation could collide with the still-open fd from the previous session.
|
||||||
|
|
||||||
|
The root cause is unknown. It is NOT caused by zombie process accumulation (that is a separate issue in #452).
|
||||||
|
|
||||||
|
**Timeline:** The crash first appeared on 2026-03-21. Agent logs go back to 2026-02-23 with no instances before that date. Stories that hit it: 329 (Mar 21), 400 (Mar 26), 420 (Mar 28), 438 (Mar 28), 446 (Mar 30), 449 (Mar 31), 450 (Mar 31).
|
||||||
|
|
||||||
|
**Suspect commits around 2026-03-21:**
|
||||||
|
- `4344081b` — storkit: merge 343_refactor_abstract_agent_runtime_to_support_non_claude_code_backends (refactored agent runtime layer)
|
||||||
|
- `c4e45b28` — The great storkit name conversion
|
||||||
|
- Story 359 — Docker security hardening (`cap_drop: ALL`, added back only `SETUID`/`SETGID`) — could affect PTY allocation
|
||||||
|
- Story 329 — Docker/OrbStack evaluation spike (first crash was on this story's mergemaster)
|
||||||
|
|
||||||
|
**Ruled out:** Docker capability restrictions (cap_drop: ALL) — tested by temporarily removing all cap_drop/security_opt; crash still occurs.
|
||||||
|
|
||||||
|
**Evidence of stale PTY fd:** After all agents stopped, storkit (PID 7) was still holding an open fd to `/dev/pts/ptmx` (fd 46). This is a leaked PTY master fd from a previous agent session. The reader thread spawned by `std::thread::spawn` in `pty.rs` is never joined, so the cloned reader fd stays open in the storkit process after the agent exits.
|
||||||
|
|
||||||
|
Remaining areas to investigate: the unjoined reader thread leaking PTY fds, and whether the leaked fd from the first session interferes with the second PTY allocation.
|
||||||
|
|
||||||
|
## How to Reproduce
|
||||||
|
|
||||||
|
1. Have a story in current stage with committed code in its worktree. 2. Introduce a test failure that causes gates to fail. 3. The pipeline restarts the agent on the same worktree. 4. The Claude Code process crashes immediately on spawn.
|
||||||
|
|
||||||
|
## Actual Result
|
||||||
|
|
||||||
|
`fatal runtime error: assertion failed: output.write(&bytes).is_ok(), aborting` — process exits instantly (same second as spawn), Session: None. Burns through retries and blocks the story.
|
||||||
|
|
||||||
|
## Expected Result
|
||||||
|
|
||||||
|
The restarted agent should start successfully, receive the gate failure context, and be able to fix the issue.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] Agent restart after gate failure successfully spawns a Claude Code PTY session
|
||||||
|
- [ ] No fatal runtime error on PTY restart in a worktree with prior committed work
|
||||||
|
- [ ] If Claude Code fails to start, the error is handled gracefully without burning retries
|
||||||
Generated
+73
-79
@@ -1497,18 +1497,18 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hybrid-array"
|
name = "hybrid-array"
|
||||||
version = "0.4.9"
|
version = "0.4.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1a79f2aff40c18ab8615ddc5caa9eb5b96314aef18fe5823090f204ad988e813"
|
checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper"
|
name = "hyper"
|
||||||
version = "1.8.1"
|
version = "1.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
|
checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atomic-waker",
|
"atomic-waker",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -1521,7 +1521,6 @@ dependencies = [
|
|||||||
"httpdate",
|
"httpdate",
|
||||||
"itoa",
|
"itoa",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"pin-utils",
|
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"tokio",
|
"tokio",
|
||||||
"want",
|
"want",
|
||||||
@@ -1595,12 +1594,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_collections"
|
name = "icu_collections"
|
||||||
version = "2.1.1"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43"
|
checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"displaydoc",
|
"displaydoc",
|
||||||
"potential_utf",
|
"potential_utf",
|
||||||
|
"utf8_iter",
|
||||||
"yoke",
|
"yoke",
|
||||||
"zerofrom",
|
"zerofrom",
|
||||||
"zerovec",
|
"zerovec",
|
||||||
@@ -1608,9 +1608,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_locale_core"
|
name = "icu_locale_core"
|
||||||
version = "2.1.1"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6"
|
checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"displaydoc",
|
"displaydoc",
|
||||||
"litemap",
|
"litemap",
|
||||||
@@ -1621,9 +1621,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_normalizer"
|
name = "icu_normalizer"
|
||||||
version = "2.1.1"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599"
|
checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"icu_collections",
|
"icu_collections",
|
||||||
"icu_normalizer_data",
|
"icu_normalizer_data",
|
||||||
@@ -1635,15 +1635,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_normalizer_data"
|
name = "icu_normalizer_data"
|
||||||
version = "2.1.1"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a"
|
checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_properties"
|
name = "icu_properties"
|
||||||
version = "2.1.2"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec"
|
checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"icu_collections",
|
"icu_collections",
|
||||||
"icu_locale_core",
|
"icu_locale_core",
|
||||||
@@ -1655,15 +1655,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_properties_data"
|
name = "icu_properties_data"
|
||||||
version = "2.1.2"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af"
|
checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_provider"
|
name = "icu_provider"
|
||||||
version = "2.1.1"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614"
|
checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"displaydoc",
|
"displaydoc",
|
||||||
"icu_locale_core",
|
"icu_locale_core",
|
||||||
@@ -1915,9 +1915,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.92"
|
version = "0.3.94"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cc4c90f45aa2e6eacbe8645f77fdea542ac97a494bcd117a67df9ff4d611f995"
|
checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
@@ -2003,9 +2003,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.183"
|
version = "0.2.184"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
|
checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libredox"
|
name = "libredox"
|
||||||
@@ -2038,9 +2038,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "litemap"
|
name = "litemap"
|
||||||
version = "0.8.1"
|
version = "0.8.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
|
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
@@ -2764,12 +2764,6 @@ version = "0.2.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pin-utils"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pkcs8"
|
name = "pkcs8"
|
||||||
version = "0.10.2"
|
version = "0.10.2"
|
||||||
@@ -2923,9 +2917,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "potential_utf"
|
name = "potential_utf"
|
||||||
version = "0.1.4"
|
version = "0.1.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77"
|
checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zerovec",
|
"zerovec",
|
||||||
]
|
]
|
||||||
@@ -3879,9 +3873,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_spanned"
|
name = "serde_spanned"
|
||||||
version = "1.1.0"
|
version = "1.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "876ac351060d4f882bb1032b6369eb0aef79ad9df1ea8bc404874d8cc3d0cd98"
|
checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
@@ -4083,7 +4077,7 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "storkit"
|
name = "storkit"
|
||||||
version = "0.8.3"
|
version = "0.8.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-stream",
|
"async-stream",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -4115,7 +4109,7 @@ dependencies = [
|
|||||||
"tempfile",
|
"tempfile",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-tungstenite 0.29.0",
|
"tokio-tungstenite 0.29.0",
|
||||||
"toml 1.1.0+spec-1.1.0",
|
"toml 1.1.2+spec-1.1.0",
|
||||||
"uuid",
|
"uuid",
|
||||||
"wait-timeout",
|
"wait-timeout",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
@@ -4336,9 +4330,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinystr"
|
name = "tinystr"
|
||||||
version = "0.8.2"
|
version = "0.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
|
checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"displaydoc",
|
"displaydoc",
|
||||||
"zerovec",
|
"zerovec",
|
||||||
@@ -4462,14 +4456,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "1.1.0+spec-1.1.0"
|
version = "1.1.2+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 = "f8195ca05e4eb728f4ba94f3e3291661320af739c4e43779cbdfae82ab239fcc"
|
checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"serde_core",
|
"serde_core",
|
||||||
"serde_spanned",
|
"serde_spanned",
|
||||||
"toml_datetime 1.1.0+spec-1.1.0",
|
"toml_datetime 1.1.1+spec-1.1.0",
|
||||||
"toml_parser",
|
"toml_parser",
|
||||||
"toml_writer",
|
"toml_writer",
|
||||||
"winnow 1.0.1",
|
"winnow 1.0.1",
|
||||||
@@ -4486,39 +4480,39 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_datetime"
|
name = "toml_datetime"
|
||||||
version = "1.1.0+spec-1.1.0"
|
version = "1.1.1+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 = "97251a7c317e03ad83774a8752a7e81fb6067740609f75ea2b585b569a59198f"
|
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_edit"
|
name = "toml_edit"
|
||||||
version = "0.25.8+spec-1.1.0"
|
version = "0.25.10+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 = "16bff38f1d86c47f9ff0647e6838d7bb362522bdf44006c7068c2b1e606f1f3c"
|
checksum = "a82418ca169e235e6c399a84e395ab6debeb3bc90edc959bf0f48647c6a32d1b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"toml_datetime 1.1.0+spec-1.1.0",
|
"toml_datetime 1.1.1+spec-1.1.0",
|
||||||
"toml_parser",
|
"toml_parser",
|
||||||
"winnow 1.0.1",
|
"winnow 1.0.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_parser"
|
name = "toml_parser"
|
||||||
version = "1.1.0+spec-1.1.0"
|
version = "1.1.2+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 = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"winnow 1.0.1",
|
"winnow 1.0.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_writer"
|
name = "toml_writer"
|
||||||
version = "1.1.0+spec-1.1.0"
|
version = "1.1.1+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 = "d282ade6016312faf3e41e57ebbba0c073e4056dab1232ab1cb624199648f8ed"
|
checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower"
|
name = "tower"
|
||||||
@@ -4674,9 +4668,9 @@ checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typewit"
|
name = "typewit"
|
||||||
version = "1.14.2"
|
version = "1.15.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8c1ae7cc0fdb8b842d65d127cb981574b0d2b249b74d1c7a2986863dc134f71"
|
checksum = "06fee3a8df48c50c55ad646a4e03b00a370da6fe1850ebf467a8d0165dfcafae"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"typewit_proc_macros",
|
"typewit_proc_macros",
|
||||||
]
|
]
|
||||||
@@ -4915,9 +4909,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
version = "0.2.115"
|
version = "0.2.117"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6523d69017b7633e396a89c5efab138161ed5aafcbc8d3e5c5a42ae38f50495a"
|
checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@@ -4928,9 +4922,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-futures"
|
name = "wasm-bindgen-futures"
|
||||||
version = "0.4.65"
|
version = "0.4.67"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2d1faf851e778dfa54db7cd438b70758eba9755cb47403f3496edd7c8fc212f0"
|
checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
@@ -4938,9 +4932,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro"
|
name = "wasm-bindgen-macro"
|
||||||
version = "0.2.115"
|
version = "0.2.117"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4e3a6c758eb2f701ed3d052ff5737f5bfe6614326ea7f3bbac7156192dc32e67"
|
checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"wasm-bindgen-macro-support",
|
"wasm-bindgen-macro-support",
|
||||||
@@ -4948,9 +4942,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro-support"
|
name = "wasm-bindgen-macro-support"
|
||||||
version = "0.2.115"
|
version = "0.2.117"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "921de2737904886b52bcbb237301552d05969a6f9c40d261eb0533c8b055fedf"
|
checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -4961,9 +4955,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-shared"
|
name = "wasm-bindgen-shared"
|
||||||
version = "0.2.115"
|
version = "0.2.117"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a93e946af942b58934c604527337bad9ae33ba1d5c6900bbb41c2c07c2364a93"
|
checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
@@ -5048,9 +5042,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "web-sys"
|
name = "web-sys"
|
||||||
version = "0.3.92"
|
version = "0.3.94"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "84cde8507f4d7cfcb1185b8cb5890c494ffea65edbe1ba82cfd63661c805ed94"
|
checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
@@ -5659,9 +5653,9 @@ checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yoke"
|
name = "yoke"
|
||||||
version = "0.8.1"
|
version = "0.8.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954"
|
checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"stable_deref_trait",
|
"stable_deref_trait",
|
||||||
"yoke-derive",
|
"yoke-derive",
|
||||||
@@ -5670,9 +5664,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yoke-derive"
|
name = "yoke-derive"
|
||||||
version = "0.8.1"
|
version = "0.8.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
|
checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -5702,18 +5696,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerofrom"
|
name = "zerofrom"
|
||||||
version = "0.1.6"
|
version = "0.1.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
|
checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zerofrom-derive",
|
"zerofrom-derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerofrom-derive"
|
name = "zerofrom-derive"
|
||||||
version = "0.1.6"
|
version = "0.1.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
|
checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -5743,9 +5737,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerotrie"
|
name = "zerotrie"
|
||||||
version = "0.2.3"
|
version = "0.2.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851"
|
checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"displaydoc",
|
"displaydoc",
|
||||||
"yoke",
|
"yoke",
|
||||||
@@ -5754,9 +5748,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerovec"
|
name = "zerovec"
|
||||||
version = "0.11.5"
|
version = "0.11.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002"
|
checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"yoke",
|
"yoke",
|
||||||
"zerofrom",
|
"zerofrom",
|
||||||
@@ -5765,9 +5759,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerovec-derive"
|
name = "zerovec-derive"
|
||||||
version = "0.11.2"
|
version = "0.11.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
|
checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|||||||
+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"
|
||||||
|
|||||||
@@ -108,6 +108,11 @@ services:
|
|||||||
retries: 3
|
retries: 3
|
||||||
start_period: 10s
|
start_period: 10s
|
||||||
|
|
||||||
|
# Use tini as PID 1 to reap zombie child processes.
|
||||||
|
# Without this, grandchild processes (esbuild, cargo, etc.) spawned by
|
||||||
|
# npm/cargo during worktree setup and gate checks become zombies.
|
||||||
|
init: true
|
||||||
|
|
||||||
# Restart policy – restart on crash but not on manual stop
|
# Restart policy – restart on crash but not on manual stop
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "living-spec-standalone",
|
"name": "living-spec-standalone",
|
||||||
"version": "0.8.3",
|
"version": "0.8.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "living-spec-standalone",
|
"name": "living-spec-standalone",
|
||||||
"version": "0.8.3",
|
"version": "0.8.5",
|
||||||
"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.3",
|
"version": "0.8.5",
|
||||||
"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,12 @@ 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 }>(
|
||||||
|
|||||||
@@ -1642,7 +1642,8 @@ describe("Slash command handling (Story 374)", () => {
|
|||||||
expect(mockedApi.botCommand).not.toHaveBeenCalled();
|
expect(mockedApi.botCommand).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("AC: /help shows help overlay", async () => {
|
it("AC: /help calls botCommand and displays response", async () => {
|
||||||
|
mockedApi.botCommand.mockResolvedValue({ response: "Available commands: status, help, ..." });
|
||||||
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
|
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
|
||||||
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
||||||
|
|
||||||
@@ -1658,9 +1659,14 @@ describe("Slash command handling (Story 374)", () => {
|
|||||||
fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
|
fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(await screen.findByTestId("help-overlay")).toBeInTheDocument();
|
await waitFor(() => {
|
||||||
|
expect(mockedApi.botCommand).toHaveBeenCalledWith(
|
||||||
|
"help",
|
||||||
|
"",
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
});
|
||||||
expect(lastSendChatArgs).toBeNull();
|
expect(lastSendChatArgs).toBeNull();
|
||||||
expect(mockedApi.botCommand).not.toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("AC: botCommand API error shows error message in chat", async () => {
|
it("AC: botCommand API error shows error message in chat", async () => {
|
||||||
@@ -1685,3 +1691,40 @@ describe("Slash command handling (Story 374)", () => {
|
|||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Bug 450: WebSocket error messages displayed in chat", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
capturedWsHandlers = null;
|
||||||
|
setupMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("AC1: WebSocket error message is shown in chat as an assistant message", async () => {
|
||||||
|
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
|
||||||
|
|
||||||
|
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
capturedWsHandlers?.onError("Something went wrong on the server.");
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await screen.findByText("Something went wrong on the server."),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("AC2: OAuth login URL in WebSocket error is rendered as a clickable link", async () => {
|
||||||
|
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
|
||||||
|
|
||||||
|
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
capturedWsHandlers?.onError(
|
||||||
|
"OAuth login required. Please visit: https://example.com/oauth/login",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const link = await screen.findByRole("link", { name: /https:\/\/example\.com\/oauth\/login/ });
|
||||||
|
expect(link).toBeInTheDocument();
|
||||||
|
expect(link).toHaveAttribute("href", "https://example.com/oauth/login");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -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,14 @@ 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");
|
||||||
@@ -407,6 +413,14 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
|||||||
console.error("WebSocket error:", message);
|
console.error("WebSocket error:", message);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setActivityStatus(null);
|
setActivityStatus(null);
|
||||||
|
const markdownMessage = message.replace(
|
||||||
|
/(https?:\/\/[^\s]+)/g,
|
||||||
|
"[$1]($1)",
|
||||||
|
);
|
||||||
|
setMessages((prev) => [
|
||||||
|
...prev,
|
||||||
|
{ role: "assistant", content: markdownMessage },
|
||||||
|
]);
|
||||||
if (queuedMessagesRef.current.length > 0) {
|
if (queuedMessagesRef.current.length > 0) {
|
||||||
const batch = queuedMessagesRef.current.map((item) => item.text);
|
const batch = queuedMessagesRef.current.map((item) => item.text);
|
||||||
queuedMessagesRef.current = [];
|
queuedMessagesRef.current = [];
|
||||||
@@ -615,12 +629,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 +665,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 +956,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,63 @@ 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,43 @@ 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: [
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "storkit"
|
name = "storkit"
|
||||||
version = "0.8.3"
|
version = "0.8.5"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
|
|||||||
@@ -245,13 +245,16 @@ fn run_agent_pty_blocking(
|
|||||||
// via recv_timeout: if no output arrives within the configured window
|
// via recv_timeout: if no output arrives within the configured window
|
||||||
// the process is killed and the agent is marked Failed.
|
// the process is killed and the agent is marked Failed.
|
||||||
let (line_tx, line_rx) = std::sync::mpsc::channel::<std::io::Result<String>>();
|
let (line_tx, line_rx) = std::sync::mpsc::channel::<std::io::Result<String>>();
|
||||||
std::thread::spawn(move || {
|
let sid_for_reader = story_id.to_string();
|
||||||
|
let aname_for_reader = agent_name.to_string();
|
||||||
|
let reader_handle = std::thread::spawn(move || {
|
||||||
let buf_reader = BufReader::new(reader);
|
let buf_reader = BufReader::new(reader);
|
||||||
for line in buf_reader.lines() {
|
for line in buf_reader.lines() {
|
||||||
if line_tx.send(line).is_err() {
|
if line_tx.send(line).is_err() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
slog!("[agent:{sid_for_reader}:{aname_for_reader}] Reader thread exiting");
|
||||||
});
|
});
|
||||||
|
|
||||||
let timeout_dur = if inactivity_timeout_secs > 0 {
|
let timeout_dur = if inactivity_timeout_secs > 0 {
|
||||||
@@ -424,6 +427,15 @@ fn run_agent_pty_blocking(
|
|||||||
let _ = child.kill();
|
let _ = child.kill();
|
||||||
let _ = child.wait();
|
let _ = child.wait();
|
||||||
|
|
||||||
|
// Wait for the reader thread to finish so it releases the cloned PTY
|
||||||
|
// master fd before we return. Without this, the next PTY spawn for the
|
||||||
|
// same story can collide with a still-open fd from this session (#453).
|
||||||
|
slog!("[agent:{story_id}:{agent_name}] Waiting for reader thread to exit");
|
||||||
|
if let Err(e) = reader_handle.join() {
|
||||||
|
slog!("[agent:{story_id}:{agent_name}] Reader thread panicked: {e:?}");
|
||||||
|
}
|
||||||
|
slog!("[agent:{story_id}:{agent_name}] Reader thread joined");
|
||||||
|
|
||||||
slog!(
|
slog!(
|
||||||
"[agent:{story_id}:{agent_name}] Done. Session: {:?}",
|
"[agent:{story_id}:{agent_name}] Done. Session: {:?}",
|
||||||
session_id
|
session_id
|
||||||
|
|||||||
@@ -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 ------------------------------------------
|
||||||
|
|||||||
+12
-2
@@ -64,12 +64,13 @@ pub fn build_routes(
|
|||||||
ctx: AppContext,
|
ctx: AppContext,
|
||||||
whatsapp_ctx: Option<Arc<WhatsAppWebhookContext>>,
|
whatsapp_ctx: Option<Arc<WhatsAppWebhookContext>>,
|
||||||
slack_ctx: Option<Arc<SlackWebhookContext>>,
|
slack_ctx: Option<Arc<SlackWebhookContext>>,
|
||||||
|
port: u16,
|
||||||
) -> impl poem::Endpoint {
|
) -> impl poem::Endpoint {
|
||||||
let ctx_arc = std::sync::Arc::new(ctx);
|
let ctx_arc = std::sync::Arc::new(ctx);
|
||||||
|
|
||||||
let (api_service, docs_service) = build_openapi_service(ctx_arc.clone());
|
let (api_service, docs_service) = build_openapi_service(ctx_arc.clone());
|
||||||
|
|
||||||
let oauth_state = Arc::new(oauth::OAuthState::new(resolve_port()));
|
let oauth_state = Arc::new(oauth::OAuthState::new(port));
|
||||||
|
|
||||||
let mut route = Route::new()
|
let mut route = Route::new()
|
||||||
.nest("/api", api_service)
|
.nest("/api", api_service)
|
||||||
@@ -236,6 +237,15 @@ mod tests {
|
|||||||
fn build_routes_constructs_without_panic() {
|
fn build_routes_constructs_without_panic() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
let ctx = context::AppContext::new_test(tmp.path().to_path_buf());
|
let ctx = context::AppContext::new_test(tmp.path().to_path_buf());
|
||||||
let _endpoint = build_routes(ctx, None, None);
|
let _endpoint = build_routes(ctx, None, None, 3001);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_routes_accepts_custom_port() {
|
||||||
|
// Verify build_routes compiles and runs with a non-default port,
|
||||||
|
// ensuring the port parameter flows through to OAuthState.
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let ctx = context::AppContext::new_test(tmp.path().to_path_buf());
|
||||||
|
let _endpoint = build_routes(ctx, None, None, 9999);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -423,6 +423,13 @@ mod tests {
|
|||||||
assert_eq!(state.callback_url(), "http://localhost:3001/callback");
|
assert_eq!(state.callback_url(), "http://localhost:3001/callback");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn oauth_state_callback_url_uses_given_port() {
|
||||||
|
// Ensure OAuthState::new uses the port passed to it, not a hardcoded value.
|
||||||
|
let state = OAuthState::new(9876);
|
||||||
|
assert_eq!(state.callback_url(), "http://localhost:9876/callback");
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn html_response_contains_title_and_message() {
|
async fn html_response_contains_title_and_message() {
|
||||||
let resp = html_response(StatusCode::OK, "Test Title", "Test message");
|
let resp = html_response(StatusCode::OK, "Test Title", "Test message");
|
||||||
|
|||||||
@@ -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}"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -254,7 +256,7 @@ fn run_pty_session(
|
|||||||
// Read NDJSON lines from stdout
|
// Read NDJSON lines from stdout
|
||||||
let (line_tx, line_rx) = std::sync::mpsc::channel::<Option<String>>();
|
let (line_tx, line_rx) = std::sync::mpsc::channel::<Option<String>>();
|
||||||
|
|
||||||
std::thread::spawn(move || {
|
let reader_handle = std::thread::spawn(move || {
|
||||||
let buf_reader = BufReader::new(reader);
|
let buf_reader = BufReader::new(reader);
|
||||||
slog!("[pty-debug] Reader thread started");
|
slog!("[pty-debug] Reader thread started");
|
||||||
for line in buf_reader.lines() {
|
for line in buf_reader.lines() {
|
||||||
@@ -282,6 +284,8 @@ fn run_pty_session(
|
|||||||
loop {
|
loop {
|
||||||
if cancelled.load(Ordering::Relaxed) {
|
if cancelled.load(Ordering::Relaxed) {
|
||||||
let _ = child.kill();
|
let _ = child.kill();
|
||||||
|
let _ = child.wait();
|
||||||
|
let _ = reader_handle.join();
|
||||||
return Err("Cancelled".to_string());
|
return Err("Cancelled".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,8 +367,11 @@ fn run_pty_session(
|
|||||||
}
|
}
|
||||||
// If still running after 2s, kill it
|
// If still running after 2s, kill it
|
||||||
let _ = child.kill();
|
let _ = child.kill();
|
||||||
|
let _ = child.wait();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Wait for the reader thread to release the cloned PTY master fd.
|
||||||
|
let _ = reader_handle.join();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -495,7 +495,7 @@ async fn main() -> Result<(), std::io::Error> {
|
|||||||
matrix_shutdown_tx: Some(Arc::clone(&matrix_shutdown_tx)),
|
matrix_shutdown_tx: Some(Arc::clone(&matrix_shutdown_tx)),
|
||||||
};
|
};
|
||||||
|
|
||||||
let app = build_routes(ctx, whatsapp_ctx.clone(), slack_ctx.clone());
|
let app = build_routes(ctx, whatsapp_ctx.clone(), slack_ctx.clone(), port);
|
||||||
|
|
||||||
// Optional Matrix bot: connect to the homeserver and start listening for
|
// Optional Matrix bot: connect to the homeserver and start listening for
|
||||||
// messages if `.storkit/bot.toml` is present and enabled.
|
// messages if `.storkit/bot.toml` is present and enabled.
|
||||||
|
|||||||
+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