mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-08-09 21:38:43 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
784a3d3a32 | ||
|
|
077bfa87b8 | ||
|
|
733f2e5929 | ||
|
|
0f994f89ac | ||
|
|
24f76398f9 | ||
|
|
14475915df | ||
|
|
a50e04f565 | ||
|
|
32fbd66912 | ||
|
|
3503a9da8e | ||
|
|
cef6abf5d0 | ||
|
|
3f098f95fe |
@@ -35,7 +35,7 @@ jobs:
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
node-version: "24"
|
||||
|
||||
- name: Install source generators
|
||||
run: |
|
||||
|
||||
@@ -16,6 +16,7 @@ use tokio::net::{TcpListener, TcpStream};
|
||||
const OAUTH_CLIENT_ID: &str = "a1fe44800c2d7e803cad1b4bf07a291c";
|
||||
const KEYRING_USER: &str = "yaak";
|
||||
const AUTH_TIMEOUT: Duration = Duration::from_secs(300);
|
||||
const CALLBACK_READ_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
const MAX_REQUEST_BYTES: usize = 16 * 1024;
|
||||
|
||||
type CommandResult<T = ()> = std::result::Result<T, String>;
|
||||
@@ -209,35 +210,71 @@ async fn receive_oauth_code(
|
||||
expected_state: &str,
|
||||
app_base_url: &str,
|
||||
) -> CommandResult<String> {
|
||||
// Browsers speculatively open extra connections that may never carry a
|
||||
// request. Handle each connection concurrently so an idle socket can't
|
||||
// block the one carrying the real callback.
|
||||
let (tx, mut rx) = tokio::sync::mpsc::channel::<CommandResult<String>>(1);
|
||||
|
||||
loop {
|
||||
let (mut stream, _) = listener
|
||||
.accept()
|
||||
.await
|
||||
.map_err(|e| format!("OAuth callback server accept error: {e}"))?;
|
||||
|
||||
match parse_callback_request(&mut stream).await {
|
||||
Ok((state, code)) => {
|
||||
if state != expected_state {
|
||||
let _ = write_bad_request(&mut stream, "Invalid OAuth state").await;
|
||||
continue;
|
||||
}
|
||||
|
||||
let success_redirect = format!("{app_base_url}/login/oauth/success");
|
||||
write_redirect(&mut stream, &success_redirect)
|
||||
.await
|
||||
.map_err(|e| format!("Failed responding to OAuth callback: {e}"))?;
|
||||
return Ok(code);
|
||||
tokio::select! {
|
||||
accepted = listener.accept() => {
|
||||
let (stream, _) = accepted
|
||||
.map_err(|e| format!("OAuth callback server accept error: {e}"))?;
|
||||
tokio::spawn(handle_callback_connection(
|
||||
stream,
|
||||
expected_state.to_string(),
|
||||
app_base_url.to_string(),
|
||||
tx.clone(),
|
||||
));
|
||||
}
|
||||
Err(error) => {
|
||||
let _ = write_bad_request(&mut stream, &error).await;
|
||||
if error.starts_with("OAuth provider returned error:") {
|
||||
return Err(error);
|
||||
result = rx.recv() => {
|
||||
if let Some(result) = result {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_callback_connection(
|
||||
mut stream: TcpStream,
|
||||
expected_state: String,
|
||||
app_base_url: String,
|
||||
tx: tokio::sync::mpsc::Sender<CommandResult<String>>,
|
||||
) {
|
||||
let parsed = match tokio::time::timeout(
|
||||
CALLBACK_READ_TIMEOUT,
|
||||
parse_callback_request(&mut stream),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(parsed) => parsed,
|
||||
Err(_) => return, // Idle speculative connection; drop it
|
||||
};
|
||||
|
||||
match parsed {
|
||||
Ok((state, code)) => {
|
||||
if state != expected_state {
|
||||
let _ = write_bad_request(&mut stream, "Invalid OAuth state").await;
|
||||
return;
|
||||
}
|
||||
|
||||
let success_redirect = format!("{app_base_url}/login/oauth/success");
|
||||
let result = match write_redirect(&mut stream, &success_redirect).await {
|
||||
Ok(()) => Ok(code),
|
||||
Err(e) => Err(format!("Failed responding to OAuth callback: {e}")),
|
||||
};
|
||||
let _ = tx.send(result).await;
|
||||
}
|
||||
Err(error) => {
|
||||
let _ = write_bad_request(&mut stream, &error).await;
|
||||
if error.starts_with("OAuth provider returned error:") {
|
||||
let _ = tx.send(Err(error)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn parse_callback_request(stream: &mut TcpStream) -> CommandResult<(String, String)> {
|
||||
let target = read_http_target(stream).await?;
|
||||
if !target.starts_with("/oauth/callback") {
|
||||
@@ -488,6 +525,37 @@ mod tests {
|
||||
assert!(err.contains("User denied"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn receive_oauth_code_ignores_idle_speculative_connections() {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
|
||||
let addr = listener.local_addr().expect("local addr");
|
||||
|
||||
let server = tokio::spawn(async move {
|
||||
receive_oauth_code(listener, "expected-state", "http://localhost:9444").await
|
||||
});
|
||||
|
||||
// Browsers preconnect sockets that never carry a request; these must
|
||||
// not block the connection carrying the real callback.
|
||||
let _idle1 = TcpStream::connect(addr).await.expect("connect idle 1");
|
||||
let _idle2 = TcpStream::connect(addr).await.expect("connect idle 2");
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
|
||||
let mut client = TcpStream::connect(addr).await.expect("connect");
|
||||
client
|
||||
.write_all(
|
||||
b"GET /oauth/callback?code=abc123&state=expected-state HTTP/1.1\r\nHost: localhost\r\n\r\n",
|
||||
)
|
||||
.await
|
||||
.expect("write");
|
||||
|
||||
let code = tokio::time::timeout(std::time::Duration::from_secs(2), server)
|
||||
.await
|
||||
.expect("idle connections must not block the real callback")
|
||||
.expect("join")
|
||||
.expect("should return code");
|
||||
assert_eq!(code, "abc123");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn receive_oauth_code_fails_fast_on_provider_error() {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
|
||||
|
||||
@@ -1098,7 +1098,7 @@ impl PluginManager {
|
||||
&InternalEventPayload::ImportRequest(ImportRequest {
|
||||
content: content.to_string(),
|
||||
}),
|
||||
Duration::from_secs(5),
|
||||
Duration::from_secs(60),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
Generated
+79
-37
@@ -83,12 +83,12 @@
|
||||
"@tauri-apps/cli": "npm:@tauri-apps/cli-cef@3.0.0-alpha.6",
|
||||
"@types/babel__core": "^7.20.5",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"@yaakapp/cli": "latest",
|
||||
"@yaakapp/cli": "*",
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"dotenv-cli": "^11.0.0",
|
||||
"nodejs-file-downloader": "^4.13.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"postcss": "^8.5.16",
|
||||
"postcss": "^8.5.25",
|
||||
"tailwindcss": "^4.3.2",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "npm:@voidzero-dev/vite-plus-core@^0.2.1",
|
||||
@@ -6331,20 +6331,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
|
||||
"integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz",
|
||||
"integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "^3.1.2",
|
||||
"content-type": "^1.0.5",
|
||||
"content-type": "^2.0.0",
|
||||
"debug": "^4.4.3",
|
||||
"http-errors": "^2.0.0",
|
||||
"iconv-lite": "^0.7.0",
|
||||
"http-errors": "^2.0.1",
|
||||
"iconv-lite": "^0.7.2",
|
||||
"on-finished": "^2.4.1",
|
||||
"qs": "^6.14.1",
|
||||
"raw-body": "^3.0.1",
|
||||
"type-is": "^2.0.1"
|
||||
"qs": "^6.15.2",
|
||||
"raw-body": "^3.0.2",
|
||||
"type-is": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@@ -6354,6 +6354,19 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser/node_modules/content-type": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
|
||||
"integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
@@ -8386,9 +8399,9 @@
|
||||
"integrity": "sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw=="
|
||||
},
|
||||
"node_modules/fast-uri": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz",
|
||||
"integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -9249,9 +9262,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/hono": {
|
||||
"version": "4.12.25",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.25.tgz",
|
||||
"integrity": "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==",
|
||||
"version": "4.12.27",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.27.tgz",
|
||||
"integrity": "sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
@@ -12107,9 +12120,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.15",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
|
||||
"integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",
|
||||
"version": "3.3.16",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
|
||||
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -13483,9 +13496,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.16",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
|
||||
"integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==",
|
||||
"version": "8.5.25",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz",
|
||||
"integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -13503,7 +13516,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.12",
|
||||
"nanoid": "^3.3.16",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
@@ -14616,9 +14629,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/seroval": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.4.2.tgz",
|
||||
"integrity": "sha512-N3HEHRCZYn3cQbsC4B5ldj9j+tHdf4JZoYPlcI4rRYu0Xy4qN8MQf1Z08EibzB0WpgRG5BGK08FTrmM66eSzKQ==",
|
||||
"version": "1.5.6",
|
||||
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.6.tgz",
|
||||
"integrity": "sha512-rVQVWjjSvlINzaQPZH5JFqsqEsIWdTxY3iJZCnTL/5gQbXIRooVZKI60tVCkOVfzcRPejboxO2t0P89dg5mQaA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
@@ -14739,9 +14752,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/shell-quote": {
|
||||
"version": "1.8.4",
|
||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz",
|
||||
"integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==",
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz",
|
||||
"integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -15916,17 +15929,34 @@
|
||||
}
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
|
||||
"integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz",
|
||||
"integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"content-type": "^1.0.5",
|
||||
"content-type": "^2.0.0",
|
||||
"media-typer": "^1.1.0",
|
||||
"mime-types": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
"node": ">= 18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/type-is/node_modules/content-type": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
|
||||
"integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/type-is/node_modules/mime-db": {
|
||||
@@ -17266,9 +17296,9 @@
|
||||
"version": "0.2.1",
|
||||
"dependencies": {
|
||||
"@hono/mcp": "^0.2.3",
|
||||
"@hono/node-server": "^1.19.13",
|
||||
"@hono/node-server": "^2.0.10",
|
||||
"@modelcontextprotocol/sdk": "^1.26.0",
|
||||
"hono": "^4.12.25",
|
||||
"hono": "^4.12.27",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -17276,6 +17306,18 @@
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
},
|
||||
"plugins-external/mcp-server/node_modules/@hono/node-server": {
|
||||
"version": "2.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.0.10.tgz",
|
||||
"integrity": "sha512-ZcnNVhKTmyDJeg0UlnZjvM73JBsTAuhrH/J4fjwGOw59PwOW51r4J+p6CsKZWXdKSme4MFqU62CZMOsdDrU4CA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"hono": "^4"
|
||||
}
|
||||
},
|
||||
"plugins/action-copy-curl": {
|
||||
"name": "@yaak/action-copy-curl",
|
||||
"version": "0.1.0"
|
||||
|
||||
+1
-1
@@ -121,7 +121,7 @@
|
||||
"dotenv-cli": "^11.0.0",
|
||||
"nodejs-file-downloader": "^4.13.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"postcss": "^8.5.16",
|
||||
"postcss": "^8.5.25",
|
||||
"tailwindcss": "^4.3.2",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "npm:@voidzero-dev/vite-plus-core@^0.2.1",
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from "./context";
|
||||
import type { TreeNode } from "./common";
|
||||
import { getNodeKey } from "./common";
|
||||
import { isImeCompositionEvent } from "./keyboard";
|
||||
import type { TreeProps } from "./Tree";
|
||||
import { TreeIndentGuide } from "./TreeIndentGuide";
|
||||
|
||||
@@ -170,6 +171,8 @@ function TreeItem_<T extends { id: string }>({
|
||||
const handleEditKeyDown = useCallback(
|
||||
async (e: ReactKeyboardEvent<HTMLInputElement>) => {
|
||||
e.stopPropagation(); // Don't trigger other tree keys (like arrows)
|
||||
if (isImeCompositionEvent(e.nativeEvent)) return;
|
||||
|
||||
switch (e.key) {
|
||||
case "Enter":
|
||||
if (editing) {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { isImeCompositionEvent } from "./keyboard";
|
||||
|
||||
describe("isImeCompositionEvent", () => {
|
||||
test("detects an active standards-based composition", () => {
|
||||
expect(isImeCompositionEvent({ isComposing: true, keyCode: 13 })).toBe(true);
|
||||
});
|
||||
|
||||
test("detects the Safari/WebKit key code fallback", () => {
|
||||
expect(isImeCompositionEvent({ isComposing: false, keyCode: 229 })).toBe(true);
|
||||
});
|
||||
|
||||
test("does not classify an ordinary Enter keydown as composition", () => {
|
||||
expect(isImeCompositionEvent({ isComposing: false, keyCode: 13 })).toBe(false);
|
||||
});
|
||||
|
||||
test("does not classify an ordinary Escape keydown as composition", () => {
|
||||
expect(isImeCompositionEvent({ isComposing: false, keyCode: 27 })).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
export type ImeKeyboardEvent = Pick<KeyboardEvent, "isComposing" | "keyCode">;
|
||||
|
||||
export function isImeCompositionEvent(event: ImeKeyboardEvent): boolean {
|
||||
// Safari can clear `isComposing` on the keydown that finishes composition.
|
||||
// `229` is retained as the compatibility signal that an IME is processing it.
|
||||
return event.isComposing || event.keyCode === 229;
|
||||
}
|
||||
@@ -15,9 +15,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@hono/mcp": "^0.2.3",
|
||||
"@hono/node-server": "^1.19.13",
|
||||
"@hono/node-server": "^2.0.10",
|
||||
"@modelcontextprotocol/sdk": "^1.26.0",
|
||||
"hono": "^4.12.25",
|
||||
"hono": "^4.12.27",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
Reference in New Issue
Block a user