mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 02:25:55 +00:00
Merge main (ea772a0): #11351 squashed, which this branch already holds, and #11255 (Agent chat: a compatible Node for discovered adapters)
Main's tree is #11351's head (084695416, merged here earlier) plus #11255
and its regenerated catalogs, so main is recorded as merged and only
those changes are applied.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
This commit is contained in:
@@ -4248,6 +4248,7 @@
|
||||
"ui.agent_chat_no_images": "هذا الوكيل لا يقبل الصور",
|
||||
"ui.agent_chat_no_picture": "لا توجد صورة في الحافظة",
|
||||
"ui.agent_chat_no_tool_call": "لم يستخدم النموذج أدوات SolidCraft، لذا لم يتغيّر التصميم. اختر نموذجًا يدعم استدعاء الأدوات؛ راجع «النماذج المحلية» في دليل محادثة الوكيل.",
|
||||
"ui.agent_chat_node_too_old": "إصدار Node.js {version} قديم جدًا لهذا الوكيل. ثبّت Node.js 20.10 أو أحدث، ثم اضغط على تحديث.",
|
||||
"ui.agent_chat_none_found": "لم يُعثر على وكيل برمجي. ثبّت أحد هذه ثم اضغط تحديث:",
|
||||
"ui.agent_chat_none_found_short": "لم يُعثر على وكيل",
|
||||
"ui.agent_chat_not_installed": "غير مثبت",
|
||||
|
||||
@@ -4231,6 +4231,7 @@
|
||||
"ui.agent_chat_no_images": "Dieser Agent nimmt keine Bilder an",
|
||||
"ui.agent_chat_no_picture": "In der Zwischenablage befindet sich kein Bild",
|
||||
"ui.agent_chat_no_tool_call": "Das Modell hat die Werkzeuge von SolidCraft nicht verwendet, daher wurde das Design nicht geändert. Wählen Sie ein Modell, das Werkzeugaufrufe unterstützt; siehe „Lokale Modelle“ im Leitfaden zum Agenten-Chat.",
|
||||
"ui.agent_chat_node_too_old": "Node.js {version} ist für diesen Agenten zu alt. Installieren Sie Node.js 20.10 oder neuer und klicken Sie dann auf Aktualisieren.",
|
||||
"ui.agent_chat_none_found": "Kein Coding-Agent gefunden. Installieren Sie einen davon und klicken Sie dann auf Aktualisieren:",
|
||||
"ui.agent_chat_none_found_short": "Kein Agent gefunden",
|
||||
"ui.agent_chat_not_installed": "Nicht installiert",
|
||||
|
||||
@@ -4248,6 +4248,7 @@
|
||||
"ui.agent_chat_no_images": "This agent doesn't take pictures",
|
||||
"ui.agent_chat_no_picture": "There is no picture on the clipboard",
|
||||
"ui.agent_chat_no_tool_call": "The model didn't use SolidCraft's tools, so the design wasn't changed. Choose a model that supports tool calling; see Local models in the agent chat guide.",
|
||||
"ui.agent_chat_node_too_old": "Node.js {version} is too old for this agent. Install Node.js 20.10 or newer, then press Refresh.",
|
||||
"ui.agent_chat_none_found": "No coding agent found. Install one of these, then press Refresh:",
|
||||
"ui.agent_chat_none_found_short": "No agent found",
|
||||
"ui.agent_chat_not_installed": "Not installed",
|
||||
|
||||
@@ -4248,6 +4248,7 @@
|
||||
"ui.agent_chat_no_images": "הסוכן הזה לא מקבל תמונות",
|
||||
"ui.agent_chat_no_picture": "אין תמונה בלוח",
|
||||
"ui.agent_chat_no_tool_call": "המודל לא השתמש בכלים של SolidCraft, ולכן התכן לא השתנה. יש לבחור מודל שתומך בקריאה לכלים; ראו „מודלים מקומיים” במדריך צ'אט הסוכן.",
|
||||
"ui.agent_chat_node_too_old": "גרסת Node.js {version} ישנה מדי עבור סוכן זה. יש להתקין Node.js 20.10 ומעלה, ואז ללחוץ על רענון.",
|
||||
"ui.agent_chat_none_found": "לא נמצא סוכן קוד. יש להתקין אחד מאלה ואז ללחוץ על רענון:",
|
||||
"ui.agent_chat_none_found_short": "לא נמצא סוכן",
|
||||
"ui.agent_chat_not_installed": "לא מותקן",
|
||||
|
||||
@@ -4248,6 +4248,7 @@
|
||||
"ui.agent_chat_no_images": "このエージェントは画像を受け付けません",
|
||||
"ui.agent_chat_no_picture": "クリップボードに画像がありません",
|
||||
"ui.agent_chat_no_tool_call": "モデルが SolidCraft のツールを使わなかったため、デザインは変更されませんでした。ツール呼び出しに対応したモデルを選んでください。エージェントチャットのガイドの「ローカルモデル」を参照してください。",
|
||||
"ui.agent_chat_node_too_old": "Node.js {version} はこのエージェントには古すぎます。Node.js 20.10 以降をインストールしてから、更新を押してください。",
|
||||
"ui.agent_chat_none_found": "コーディングエージェントが見つかりません。次のいずれかをインストールしてから「再検索」を押してください:",
|
||||
"ui.agent_chat_none_found_short": "エージェントが見つかりません",
|
||||
"ui.agent_chat_not_installed": "未インストール",
|
||||
|
||||
@@ -3059,6 +3059,7 @@
|
||||
"ui.agent_chat_no_control": "에이전트 채팅 엔드포인트가 시작되지 않아 에이전트가 디자인에 접근할 수 없습니다.",
|
||||
"ui.agent_chat_no_images": "이 에이전트는 이미지를 받지 않습니다",
|
||||
"ui.agent_chat_no_picture": "클립보드에 이미지가 없습니다",
|
||||
"ui.agent_chat_node_too_old": "Node.js {version}은 이 에이전트에 사용하기에는 너무 오래된 버전입니다. Node.js 20.10 이상을 설치한 후 새로 고침을 누르세요.",
|
||||
"ui.agent_chat_none_found": "코딩 에이전트를 찾지 못했습니다. 다음 중 하나를 설치한 뒤 새로 고침을 누르세요:",
|
||||
"ui.agent_chat_none_found_short": "에이전트를 찾지 못함",
|
||||
"ui.agent_chat_not_installed": "설치되지 않음",
|
||||
|
||||
@@ -4248,6 +4248,7 @@
|
||||
"ui.agent_chat_no_images": "AI Agent này không hỗ trợ tiếp nhận hình ảnh",
|
||||
"ui.agent_chat_no_picture": "Không có hình ảnh trong bộ nhớ tạm",
|
||||
"ui.agent_chat_no_tool_call": "Mô hình không sử dụng các công cụ của SolidCraft nên thiết kế không thay đổi. Hãy chọn mô hình hỗ trợ gọi công cụ; xem mục Mô hình cục bộ trong hướng dẫn trò chuyện AI.",
|
||||
"ui.agent_chat_node_too_old": "Node.js {version} quá cũ đối với tác nhân này. Hãy cài đặt Node.js 20.10 trở lên, rồi nhấn Làm mới.",
|
||||
"ui.agent_chat_none_found": "Không tìm thấy tác tử lập trình nào. Hãy cài đặt một trong các tác tử sau, rồi nhấn Làm mới:",
|
||||
"ui.agent_chat_none_found_short": "Không tìm thấy tác tử nào",
|
||||
"ui.agent_chat_not_installed": "Chưa cài đặt",
|
||||
|
||||
@@ -4248,6 +4248,7 @@
|
||||
"ui.agent_chat_no_images": "该智能体不接收图片",
|
||||
"ui.agent_chat_no_picture": "剪贴板上没有图片",
|
||||
"ui.agent_chat_no_tool_call": "模型没有使用 SolidCraft 的工具,因此设计没有更改。请选择支持工具调用的模型;参见智能体聊天指南中的“本地模型”。",
|
||||
"ui.agent_chat_node_too_old": "Node.js {version} 版本过旧,无法运行此代理。请安装 Node.js 20.10 或更新版本,然后点击刷新。",
|
||||
"ui.agent_chat_none_found": "未找到编程智能体。请安装以下之一,然后点击刷新:",
|
||||
"ui.agent_chat_none_found_short": "未找到智能体",
|
||||
"ui.agent_chat_not_installed": "未安装",
|
||||
|
||||
@@ -275,7 +275,9 @@ pub fn runnable_sibling(program: &Path) -> Option<PathBuf> {
|
||||
|
||||
/// Start probes and sessions with the same PATH used for discovery. Desktop launchers often
|
||||
/// omit Node's install folder even when we found an adapter with a `#!/usr/bin/env node` shebang.
|
||||
/// Only the child's environment changes; inherited absolute PATH entries keep precedence.
|
||||
/// Only the child's environment changes. An explicitly selected Node also takes precedence for
|
||||
/// its descendants; otherwise inherited absolute PATH entries keep precedence. No probes here:
|
||||
/// starting a session calls this on the UI thread.
|
||||
pub(super) fn command_for(program: &Path) -> Command {
|
||||
// Older saved Windows commands may name npm's bare Unix script rather than its .cmd shim.
|
||||
let shim = if cfg!(windows) { runnable_sibling(program) } else { None };
|
||||
@@ -287,7 +289,15 @@ pub(super) fn command_for(program: &Path) -> Command {
|
||||
} else {
|
||||
Command::new(program)
|
||||
};
|
||||
if let Ok(path) = std::env::join_paths(search_dirs()) {
|
||||
let mut dirs = search_dirs();
|
||||
if program.is_absolute()
|
||||
&& program.file_name().is_some_and(|n| n == "node" || n == "node.exe")
|
||||
&& let Some(parent) = program.parent()
|
||||
{
|
||||
dirs.retain(|d| d != parent);
|
||||
dirs.insert(0, parent.to_path_buf());
|
||||
}
|
||||
if let Ok(path) = std::env::join_paths(dirs) {
|
||||
cmd.env("PATH", path);
|
||||
}
|
||||
cmd
|
||||
@@ -301,6 +311,8 @@ struct Probe {
|
||||
/// Why it can't start: the system refused to start it, or it failed and printed nothing but
|
||||
/// an error. A probe that runs out of time says nothing either way (a slow machine).
|
||||
problem: Option<String>,
|
||||
/// A failed JSON import-attributes parse on a Node version that cannot support it.
|
||||
old_node: Option<String>,
|
||||
}
|
||||
|
||||
/// How long an agent is given to answer `--version`.
|
||||
@@ -311,7 +323,10 @@ const PROBLEM_CHARS: usize = 300;
|
||||
|
||||
/// Ask `program --version`; it is stopped after `limit`.
|
||||
fn probe(program: &Path, limit: Duration) -> Probe {
|
||||
let mut cmd = command_for(program);
|
||||
probe_command(program, command_for(program), limit)
|
||||
}
|
||||
|
||||
fn probe_command(program: &Path, mut cmd: Command, limit: Duration) -> Probe {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
@@ -323,7 +338,7 @@ fn probe(program: &Path, limit: Duration) -> Probe {
|
||||
// Windows: "not a valid Win32 application" (193), "not compatible with the version of
|
||||
// Windows you're running" (216); elsewhere: not allowed to run it, an unknown format.
|
||||
// Windows leaves the program's place in its message as `%1`.
|
||||
Err(e) => return Probe { version: None, problem: Some(cut(&e.to_string().replace("%1", &program.display().to_string()))) },
|
||||
Err(e) => return Probe { problem: Some(cut(&e.to_string().replace("%1", &program.display().to_string()))), ..Probe::default() },
|
||||
};
|
||||
let start = Instant::now();
|
||||
let status = loop {
|
||||
@@ -339,7 +354,7 @@ fn probe(program: &Path, limit: Duration) -> Probe {
|
||||
};
|
||||
let out = read_capped(child.stdout.take());
|
||||
if let Some(line) = out.lines().map(str::trim).find(|l| !l.is_empty()) {
|
||||
return Probe { version: Some(version_of(line)), problem: None };
|
||||
return Probe { version: Some(version_of(line)), ..Probe::default() };
|
||||
}
|
||||
if status.success() {
|
||||
return Probe::default();
|
||||
@@ -347,7 +362,54 @@ fn probe(program: &Path, limit: Duration) -> Probe {
|
||||
// Through a `.cmd` shim, Windows' own refusal arrives on stderr with a failing exit code.
|
||||
let err = read_capped(child.stderr.take());
|
||||
let reason = err.lines().map(str::trim).find(|l| !l.is_empty()).map(str::to_string).unwrap_or_else(|| status.to_string());
|
||||
Probe { version: None, problem: Some(cut(&reason)) }
|
||||
let old_node = err
|
||||
.contains("SyntaxError: Unexpected token 'with'")
|
||||
.then(|| {
|
||||
err.lines().find_map(|line| {
|
||||
let version = line.trim().strip_prefix("Node.js ")?;
|
||||
(node_import_attributes(version) == Some(false)).then(|| version.to_string())
|
||||
})
|
||||
})
|
||||
.flatten();
|
||||
Probe { version: None, problem: Some(cut(&reason)), old_node }
|
||||
}
|
||||
|
||||
/// Node added import attributes in 18.20, 20.10 and 21.0. Reject unrecognised versions.
|
||||
fn node_import_attributes(version: &str) -> Option<bool> {
|
||||
let mut parts = version.strip_prefix('v')?.split('.');
|
||||
let major = parts.next()?.parse::<u64>().ok()?;
|
||||
let minor = parts.next()?.parse::<u64>().ok()?;
|
||||
let _patch = parts.next()?.parse::<u64>().ok()?;
|
||||
if parts.next().is_some() {
|
||||
return None;
|
||||
}
|
||||
Some(major >= 21 || (major == 20 && minor >= 10) || (major == 18 && minor >= 20))
|
||||
}
|
||||
|
||||
/// Only replace an unambiguous env-node shebang, never a shell shim or a pinned interpreter.
|
||||
fn uses_env_node(program: &Path) -> bool {
|
||||
use std::io::Read;
|
||||
let Ok(file) = std::fs::File::open(program) else { return false };
|
||||
let mut header = String::new();
|
||||
if file.take(256).read_to_string(&mut header).is_err() {
|
||||
return false;
|
||||
}
|
||||
let words: Vec<_> = header.lines().next().unwrap_or_default().split_whitespace().collect();
|
||||
matches!(words.as_slice(), ["#!/usr/bin/env" | "#!/bin/env", "node"] | ["#!/usr/bin/env" | "#!/bin/env", "-S", "node"])
|
||||
}
|
||||
|
||||
/// These checks run only after the adapter fails on an old Node, on the discovery worker.
|
||||
/// All candidate probes share one time budget, so many installed versions cannot stall Refresh.
|
||||
fn compatible_node(dirs: &[PathBuf], limit: Duration) -> Option<PathBuf> {
|
||||
let start = Instant::now();
|
||||
for dir in dirs.iter().take(64) {
|
||||
let Some(node) = find_in("node", std::slice::from_ref(dir)) else { continue };
|
||||
let remaining = limit.checked_sub(start.elapsed()).filter(|d| !d.is_zero())?;
|
||||
if probe(&node, remaining).version.as_deref().and_then(node_import_attributes) == Some(true) {
|
||||
return Some(node);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// What a finished probe wrote on a pipe, up to 64 KiB.
|
||||
@@ -406,7 +468,20 @@ fn detect_in(dirs: &[PathBuf], home: Option<&Path>, limit: Duration) -> Detectio
|
||||
let login = login_of(k, home);
|
||||
let mut argv = vec![path.to_string_lossy().into_owned()];
|
||||
argv.extend(k.args.iter().map(|a| a.to_string()));
|
||||
let Probe { version, problem } = if k.reports_version { probe(&path, limit) } else { Probe::default() };
|
||||
let mut result = if k.reports_version { probe(&path, limit) } else { Probe::default() };
|
||||
if let Some(version) = result.old_node.as_deref() {
|
||||
result.problem = Some(crate::i18n::format("ui.agent_chat_node_too_old", &[("version", version)]));
|
||||
if uses_env_node(&path)
|
||||
&& let Some(node) = compatible_node(dirs, limit)
|
||||
{
|
||||
let mut cmd = command_for(&node);
|
||||
cmd.arg(&path);
|
||||
result = probe_command(&node, cmd, limit);
|
||||
// Keep this interpreter for the live session as well as the version check.
|
||||
argv.insert(0, node.to_string_lossy().into_owned());
|
||||
}
|
||||
}
|
||||
let Probe { version, problem, .. } = result;
|
||||
d.found.push(Found { id: k.id, name: k.name, version, argv, login, problem, install: k.install });
|
||||
}
|
||||
d.took_ms = u64::try_from(t0.elapsed().as_millis()).unwrap_or(u64::MAX);
|
||||
@@ -468,6 +543,111 @@ pub(super) fn tools_capability(value: &serde_json::Value) -> Option<bool> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A desktop PATH can name an old Node before the runtime installed with an npm adapter.
|
||||
/// Both the version probe and the live session must use the recovered interpreter.
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn a_node_adapter_recovers_from_an_incompatible_desktop_runtime() {
|
||||
use super::super::process::{AgentProcess, FromAgent};
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
const CHILD: &str = "SOLIDCRAFT_NODE_COMPAT_TEST";
|
||||
const BROKEN_NODE: &str = "#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then echo v18.19.1; else echo \"SyntaxError: Unexpected token 'with'\" >&2; echo 'Node.js v18.19.1' >&2; exit 1; fi\n";
|
||||
if let Some(root) = std::env::var_os(CHILD) {
|
||||
let root = PathBuf::from(root);
|
||||
let old = root.join("old");
|
||||
let newer = root.join("modern/bin");
|
||||
let dirs = [old.clone(), newer.clone()];
|
||||
let find = || detect_in(&dirs, None, Duration::from_secs(20)).found.into_iter().find(|f| f.id == "claude").unwrap();
|
||||
let inherited_path = std::env::var_os("PATH");
|
||||
let found = find();
|
||||
assert_eq!(found.version.as_deref(), Some("1.2.3"), "{found:?}");
|
||||
assert_eq!(found.problem, None);
|
||||
assert_eq!(found.argv.first(), Some(&newer.join("node").to_string_lossy().into_owned()));
|
||||
assert_eq!(found.argv.get(1), Some(&old.join("claude-agent-acp").to_string_lossy().into_owned()));
|
||||
assert_eq!(std::env::var_os("PATH"), inherited_path, "only the child's PATH may change");
|
||||
|
||||
let mut process = AgentProcess::spawn(&found.argv, &root, &[], || {}).unwrap();
|
||||
let message = serde_json::json!({"id": 1, "method": "initialize"});
|
||||
process.send(&message);
|
||||
let reply = process.from_agent.recv_timeout(Duration::from_secs(20)).unwrap();
|
||||
process.shutdown(true);
|
||||
match reply {
|
||||
FromAgent::Line(line) => assert_eq!(serde_json::from_str::<serde_json::Value>(&line).unwrap(), message),
|
||||
FromAgent::Exited(tail) => panic!("the recovered adapter did not start: {tail:?}"),
|
||||
}
|
||||
|
||||
// An older runtime that already runs the adapter is left alone.
|
||||
std::fs::write(old.join("node"), "#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then echo v18.19.1; else echo adapter 9.9.9; fi\n").unwrap();
|
||||
let working = find();
|
||||
assert_eq!(working.version.as_deref(), Some("9.9.9"));
|
||||
assert_eq!(working.argv.first(), Some(&old.join("claude-agent-acp").to_string_lossy().into_owned()));
|
||||
|
||||
// Without a compatible runtime the reason must name Node and the remedy.
|
||||
std::fs::write(old.join("node"), BROKEN_NODE).unwrap();
|
||||
std::fs::remove_file(newer.join("node")).unwrap();
|
||||
let unavailable = find();
|
||||
assert_eq!(unavailable.version, None);
|
||||
let reason = unavailable.problem.unwrap();
|
||||
assert!(reason.contains("18.19.1") && reason.contains("20.10") && reason.contains("Refresh"), "{reason}");
|
||||
return;
|
||||
}
|
||||
|
||||
let root = std::env::temp_dir().join(format!("sc agent node compat-{}", std::process::id()));
|
||||
let old = root.join("old");
|
||||
let newer = root.join("modern/bin");
|
||||
std::fs::create_dir_all(&old).unwrap();
|
||||
std::fs::create_dir_all(&newer).unwrap();
|
||||
for (path, script) in [
|
||||
(old.join("claude-agent-acp"), "#!/usr/bin/env node\n"),
|
||||
(old.join("node"), BROKEN_NODE),
|
||||
(
|
||||
newer.join("node"),
|
||||
"#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then echo v22.0.0; elif [ \"$2\" = \"--version\" ]; then echo adapter 1.2.3; elif [ \"$(node --version)\" = v22.0.0 ]; then /bin/cat; else exit 1; fi\n",
|
||||
),
|
||||
] {
|
||||
std::fs::write(&path, script).unwrap();
|
||||
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o755)).unwrap();
|
||||
}
|
||||
let output = Command::new(std::env::current_exe().unwrap())
|
||||
.args(["--exact", "agent_chat::discover::tests::a_node_adapter_recovers_from_an_incompatible_desktop_runtime", "--nocapture"])
|
||||
.env(CHILD, &root)
|
||||
.env("PATH", std::env::join_paths([old, PathBuf::from("/usr/bin"), PathBuf::from("/bin")]).unwrap())
|
||||
.env("NPM_CONFIG_PREFIX", root.join("modern"))
|
||||
.output()
|
||||
.unwrap();
|
||||
let _ = std::fs::remove_dir_all(root);
|
||||
assert!(output.status.success(), "{}\n{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn node_import_attributes_require_a_supported_release() {
|
||||
for version in ["v18.20.0", "v18.20.8", "v20.10.0", "v20.19.0", "v21.0.0", "v22.0.0"] {
|
||||
assert_eq!(node_import_attributes(version), Some(true), "{version}");
|
||||
}
|
||||
for version in ["v16.20.2", "v18.19.1", "v19.9.0", "v20.9.0"] {
|
||||
assert_eq!(node_import_attributes(version), Some(false), "{version}");
|
||||
}
|
||||
for version in ["", "node", "v20", "v20.10", "v22.0.0-rc.1", "v22.0.0.1", "v18446744073709551616.0.0"] {
|
||||
assert_eq!(node_import_attributes(version), None, "{version}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn node_recovery_does_not_rewrite_shell_shims_or_pinned_interpreters() {
|
||||
let path = std::env::temp_dir().join(format!("sc-agent-shebang-{}", std::process::id()));
|
||||
for header in ["#!/usr/bin/env node\n", "#!/bin/env node\r\n", "#!/usr/bin/env -S node\n"] {
|
||||
std::fs::write(&path, header).unwrap();
|
||||
assert!(uses_env_node(&path), "{header}");
|
||||
}
|
||||
for header in ["#!/bin/sh\n", "#!/opt/node/bin/node\n", "#!/usr/bin/env -S node --experimental-modules\n", "@echo off\r\n", "node\n"] {
|
||||
std::fs::write(&path, header).unwrap();
|
||||
assert!(!uses_env_node(&path), "{header}");
|
||||
}
|
||||
std::fs::remove_file(&path).unwrap();
|
||||
assert!(!uses_env_node(&path));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ollama_capabilities_distinguish_tools_chat_and_unknown() {
|
||||
use serde_json::json;
|
||||
|
||||
@@ -195,6 +195,13 @@ shows its **version** (from `--version`, given three seconds) and whether a **lo
|
||||
Version checks and agent sessions also use those folders on their `PATH`, so adapters can find
|
||||
runtimes such as Node when SolidCraft is opened from Finder or the Dock. Existing absolute
|
||||
`PATH` entries keep their order; no shell startup files are run.
|
||||
If a detected Node adapter fails with `SyntaxError: Unexpected token 'with'` on an older Node,
|
||||
SolidCraft checks these folders for a Node version that supports import attributes. For an
|
||||
adapter with an `env node` shebang, it retries with that Node and keeps the same interpreter
|
||||
for the chat session, including programs the adapter starts. These extra checks run in the
|
||||
background with a shared three-second limit for finding Node. Working adapters keep their
|
||||
original launch command. If no suitable Node is found, the agent's error names the old version
|
||||
and asks you to update Node and press **Refresh**.
|
||||
SolidCraft only checks whether the agent's own login file (`~/.claude/.credentials.json`,
|
||||
`~/.codex/auth.json`, `~/.gemini/oauth_creds.json`, …) or one of its key variables exists. It never
|
||||
reads the file and never starts the agent to find out. On macOS Claude Code keeps its login in
|
||||
|
||||
Reference in New Issue
Block a user