mirror of
https://github.com/RightNow-AI/openfang.git
synced 2026-07-30 06:32:17 +00:00
batch fixes
This commit is contained in:
Generated
+45
-15
@@ -3866,7 +3866,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-api"
|
||||
version = "0.3.0"
|
||||
version = "0.3.2"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -3902,7 +3902,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-channels"
|
||||
version = "0.3.0"
|
||||
version = "0.3.2"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -3933,7 +3933,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-cli"
|
||||
version = "0.3.0"
|
||||
version = "0.3.2"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"clap_complete",
|
||||
@@ -3960,7 +3960,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-desktop"
|
||||
version = "0.3.0"
|
||||
version = "0.3.2"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"open",
|
||||
@@ -3986,7 +3986,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-extensions"
|
||||
version = "0.3.0"
|
||||
version = "0.3.2"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"argon2",
|
||||
@@ -4014,7 +4014,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-hands"
|
||||
version = "0.3.0"
|
||||
version = "0.3.2"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dashmap",
|
||||
@@ -4031,7 +4031,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-kernel"
|
||||
version = "0.3.0"
|
||||
version = "0.3.2"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -4067,7 +4067,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-memory"
|
||||
version = "0.3.0"
|
||||
version = "0.3.2"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -4086,7 +4086,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-migrate"
|
||||
version = "0.3.0"
|
||||
version = "0.3.2"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dirs 6.0.0",
|
||||
@@ -4105,7 +4105,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-runtime"
|
||||
version = "0.3.0"
|
||||
version = "0.3.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -4137,7 +4137,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-skills"
|
||||
version = "0.3.0"
|
||||
version = "0.3.2"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"hex",
|
||||
@@ -4155,11 +4155,12 @@ dependencies = [
|
||||
"tracing",
|
||||
"uuid",
|
||||
"walkdir",
|
||||
"zip 2.4.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openfang-types"
|
||||
version = "0.3.0"
|
||||
version = "0.3.2"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -4178,7 +4179,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-wire"
|
||||
version = "0.3.0"
|
||||
version = "0.3.2"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -6553,7 +6554,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"url",
|
||||
"windows-sys 0.60.2",
|
||||
"zip",
|
||||
"zip 4.6.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8790,7 +8791,7 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.3.0"
|
||||
version = "0.3.2"
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
@@ -8970,6 +8971,23 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "2.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"crc32fast",
|
||||
"crossbeam-utils",
|
||||
"displaydoc",
|
||||
"flate2",
|
||||
"indexmap 2.13.0",
|
||||
"memchr",
|
||||
"thiserror 2.0.18",
|
||||
"zopfli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "4.6.1"
|
||||
@@ -8988,6 +9006,18 @@ version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
|
||||
[[package]]
|
||||
name = "zopfli"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"crc32fast",
|
||||
"log",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd"
|
||||
version = "0.13.3"
|
||||
|
||||
@@ -122,6 +122,9 @@ argon2 = "0.5"
|
||||
# Lightweight regex
|
||||
regex-lite = "0.1"
|
||||
|
||||
# Zip archive extraction
|
||||
zip = { version = "2", default-features = false, features = ["deflate"] }
|
||||
|
||||
# Email (SMTP + IMAP)
|
||||
lettre = { version = "0.11", default-features = false, features = ["builder", "hostname", "smtp-transport", "tokio1", "tokio1-rustls-tls"] }
|
||||
imap = "2"
|
||||
|
||||
@@ -483,6 +483,10 @@ pub async fn status(State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||
"default_provider": state.kernel.config.default_model.provider,
|
||||
"default_model": state.kernel.config.default_model.model,
|
||||
"uptime_seconds": uptime,
|
||||
"api_listen": state.kernel.config.api_listen,
|
||||
"home_dir": state.kernel.config.home_dir.display().to_string(),
|
||||
"log_level": state.kernel.config.log_level,
|
||||
"network_enabled": state.kernel.config.network_enabled,
|
||||
"agents": agents,
|
||||
}))
|
||||
}
|
||||
@@ -2966,6 +2970,46 @@ pub async fn clawhub_skill_detail(
|
||||
}
|
||||
}
|
||||
|
||||
/// GET /api/clawhub/skill/{slug}/code — Fetch the source code (SKILL.md) of a ClawHub skill.
|
||||
pub async fn clawhub_skill_code(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(slug): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
let cache_dir = state.kernel.config.home_dir.join(".cache").join("clawhub");
|
||||
let client = openfang_skills::clawhub::ClawHubClient::new(cache_dir);
|
||||
|
||||
// Try to fetch SKILL.md first, then fallback to package.json
|
||||
let mut code = String::new();
|
||||
let mut filename = String::new();
|
||||
|
||||
if let Ok(content) = client.get_file(&slug, "SKILL.md").await {
|
||||
code = content;
|
||||
filename = "SKILL.md".to_string();
|
||||
} else if let Ok(content) = client.get_file(&slug, "package.json").await {
|
||||
code = content;
|
||||
filename = "package.json".to_string();
|
||||
} else if let Ok(content) = client.get_file(&slug, "skill.toml").await {
|
||||
code = content;
|
||||
filename = "skill.toml".to_string();
|
||||
}
|
||||
|
||||
if code.is_empty() {
|
||||
return (
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(serde_json::json!({"error": "No source code found for this skill"})),
|
||||
);
|
||||
}
|
||||
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({
|
||||
"slug": slug,
|
||||
"filename": filename,
|
||||
"code": code,
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
/// POST /api/clawhub/install — Install a skill from ClawHub.
|
||||
///
|
||||
/// Runs the full security pipeline: SHA256 verification, format detection,
|
||||
@@ -5026,6 +5070,10 @@ pub async fn list_providers(State(state): State<Arc<AppState>>) -> impl IntoResp
|
||||
entry["latency_ms"] = serde_json::json!(probe.latency_ms);
|
||||
if !probe.discovered_models.is_empty() {
|
||||
entry["discovered_models"] = serde_json::json!(probe.discovered_models);
|
||||
// Merge discovered models into the catalog so agents can use them
|
||||
if let Ok(mut catalog) = state.kernel.model_catalog.write() {
|
||||
catalog.merge_discovered_models(&p.id, &probe.discovered_models);
|
||||
}
|
||||
}
|
||||
if let Some(err) = &probe.error {
|
||||
entry["error"] = serde_json::json!(err);
|
||||
@@ -6387,16 +6435,25 @@ pub async fn set_provider_url(
|
||||
let probe =
|
||||
openfang_runtime::provider_health::probe_provider(&name, &base_url).await;
|
||||
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({
|
||||
"status": "saved",
|
||||
"provider": name,
|
||||
"base_url": base_url,
|
||||
"reachable": probe.reachable,
|
||||
"latency_ms": probe.latency_ms,
|
||||
})),
|
||||
)
|
||||
// Merge discovered models into catalog
|
||||
if !probe.discovered_models.is_empty() {
|
||||
if let Ok(mut catalog) = state.kernel.model_catalog.write() {
|
||||
catalog.merge_discovered_models(&name, &probe.discovered_models);
|
||||
}
|
||||
}
|
||||
|
||||
let mut resp = serde_json::json!({
|
||||
"status": "saved",
|
||||
"provider": name,
|
||||
"base_url": base_url,
|
||||
"reachable": probe.reachable,
|
||||
"latency_ms": probe.latency_ms,
|
||||
});
|
||||
if !probe.discovered_models.is_empty() {
|
||||
resp["discovered_models"] = serde_json::json!(probe.discovered_models);
|
||||
}
|
||||
|
||||
(StatusCode::OK, Json(resp))
|
||||
}
|
||||
|
||||
/// Upsert a provider URL in the `[provider_urls]` section of config.toml.
|
||||
|
||||
@@ -321,6 +321,10 @@ pub async fn build_router(
|
||||
"/api/clawhub/skill/{slug}",
|
||||
axum::routing::get(routes::clawhub_skill_detail),
|
||||
)
|
||||
.route(
|
||||
"/api/clawhub/skill/{slug}/code",
|
||||
axum::routing::get(routes::clawhub_skill_code),
|
||||
)
|
||||
.route(
|
||||
"/api/clawhub/install",
|
||||
axum::routing::post(routes::clawhub_install),
|
||||
|
||||
@@ -125,6 +125,8 @@ const WEBCHAT_HTML: &str = concat!(
|
||||
include_str!("../static/js/pages/approvals.js"),
|
||||
"\n",
|
||||
include_str!("../static/js/pages/comms.js"),
|
||||
"\n",
|
||||
include_str!("../static/js/pages/runtime.js"),
|
||||
"\n</script>\n",
|
||||
// Alpine.js MUST be last — it processes x-data and fires alpine:init
|
||||
"<script>\n",
|
||||
|
||||
@@ -156,6 +156,10 @@
|
||||
</div>
|
||||
<template x-if="!collapsed">
|
||||
<div x-transition>
|
||||
<a class="nav-item" :class="{ active: page === 'runtime' }" @click="navigate('runtime')" :aria-current="page === 'runtime' ? 'page' : false">
|
||||
<span class="nav-icon"><svg viewBox="0 0 24 24"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg></span>
|
||||
<span class="nav-label">Runtime</span>
|
||||
</a>
|
||||
<a class="nav-item" :class="{ active: page === 'settings' }" @click="navigate('settings')" :aria-current="page === 'settings' ? 'page' : false">
|
||||
<span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M4 21v-7M4 10V3M12 21v-9M12 8V3M20 21v-5M20 12V3"/><path d="M1 14h6M9 8h6M17 16h6"/></svg></span>
|
||||
<span class="nav-label">Settings</span>
|
||||
@@ -2305,7 +2309,14 @@ args = ["-y", "@modelcontextprotocol/server-filesystem", "/path"]</pre>
|
||||
</template>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-primary btn-block" @click="installFromClawHub(skillDetail.slug)" :disabled="installingSlug === skillDetail.slug || skillDetail.installed || isSkillInstalled(skillDetail.slug)" x-text="skillDetail.installed || isSkillInstalled(skillDetail.slug) ? 'Already Installed' : installingSlug === skillDetail.slug ? 'Installing...' : 'Install from ClawHub'"></button>
|
||||
<button class="btn btn-primary" style="flex:1" @click="installFromClawHub(skillDetail.slug)" :disabled="installingSlug === skillDetail.slug || skillDetail.installed || isSkillInstalled(skillDetail.slug)" x-text="skillDetail.installed || isSkillInstalled(skillDetail.slug) ? 'Already Installed' : installingSlug === skillDetail.slug ? 'Installing...' : 'Install from ClawHub'"></button>
|
||||
<button class="btn btn-ghost" @click="viewSkillCode(skillDetail.slug)" :disabled="skillCodeLoading" x-text="skillCodeLoading ? 'Loading...' : showSkillCode ? 'Hide Code' : 'View Code'"></button>
|
||||
</div>
|
||||
<div x-show="showSkillCode && skillCode" x-transition class="mt-3" style="max-height:300px;overflow:auto;border:1px solid var(--border);border-radius:8px;background:var(--bg-inset)">
|
||||
<div class="flex justify-between items-center" style="padding:6px 12px;border-bottom:1px solid var(--border)">
|
||||
<span class="text-xs text-dim" x-text="skillCodeFilename"></span>
|
||||
</div>
|
||||
<pre style="margin:0;padding:12px;font-size:12px;line-height:1.5;white-space:pre-wrap;word-break:break-all" x-text="skillCode"></pre>
|
||||
</div>
|
||||
<div class="text-xs text-dim mt-2 text-center">Skills are security-scanned before installation. Prompt injection and malware patterns are blocked.</div>
|
||||
</div>
|
||||
@@ -4563,6 +4574,71 @@ args = ["-y", "@modelcontextprotocol/server-filesystem", "/path"]</pre>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ─── Runtime page ─── -->
|
||||
<template x-if="page === 'runtime'">
|
||||
<div x-data="runtimePage">
|
||||
<div class="page-header"><h2>Runtime</h2></div>
|
||||
<div class="page-body" x-init="loadData()">
|
||||
<div x-show="loading" class="loading-state"><div class="spinner"></div><span>Loading runtime info...</span></div>
|
||||
<div x-show="!loading">
|
||||
<div class="grid grid-cols-4" style="gap:16px;margin-bottom:24px">
|
||||
<div class="card stat-card">
|
||||
<div class="stat-label">Uptime</div>
|
||||
<div class="stat-value" x-text="uptime"></div>
|
||||
</div>
|
||||
<div class="card stat-card">
|
||||
<div class="stat-label">Agents</div>
|
||||
<div class="stat-value" x-text="agentCount"></div>
|
||||
</div>
|
||||
<div class="card stat-card">
|
||||
<div class="stat-label">Version</div>
|
||||
<div class="stat-value" x-text="version"></div>
|
||||
</div>
|
||||
<div class="card stat-card">
|
||||
<div class="stat-label">Default Model</div>
|
||||
<div class="stat-value" style="font-size:13px" x-text="defaultModel"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom:16px">
|
||||
<div class="card-header">System</div>
|
||||
<table class="table" style="margin-top:8px">
|
||||
<tbody>
|
||||
<tr><td style="width:180px;font-weight:500">Platform</td><td x-text="platform"></td></tr>
|
||||
<tr><td style="font-weight:500">Architecture</td><td x-text="arch"></td></tr>
|
||||
<tr><td style="font-weight:500">API Listen</td><td x-text="apiListen"></td></tr>
|
||||
<tr><td style="font-weight:500">Home Directory</td><td x-text="homeDir"></td></tr>
|
||||
<tr><td style="font-weight:500">Log Level</td><td x-text="logLevel"></td></tr>
|
||||
<tr><td style="font-weight:500">Network</td><td x-text="networkEnabled ? 'Enabled' : 'Disabled'"></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom:16px">
|
||||
<div class="card-header">Providers</div>
|
||||
<table class="table" style="margin-top:8px">
|
||||
<thead><tr><th>Provider</th><th>Status</th><th>Models</th><th>Latency</th></tr></thead>
|
||||
<tbody>
|
||||
<template x-for="p in providers" :key="p.id">
|
||||
<tr>
|
||||
<td x-text="p.display_name || p.id"></td>
|
||||
<td><span class="badge" :class="p.reachable ? 'badge-success' : (p.auth_status === 'Configured' ? 'badge-success' : 'badge-dim')" x-text="p.reachable ? 'Online' : (p.auth_status === 'Configured' ? 'Ready' : 'Not configured')"></span></td>
|
||||
<td x-text="p.model_count"></td>
|
||||
<td x-text="p.latency_ms ? p.latency_ms + 'ms' : '-'"></td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-ghost btn-sm" @click="loadData()">Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -218,7 +218,7 @@ function app() {
|
||||
});
|
||||
|
||||
// Hash routing
|
||||
var validPages = ['overview','agents','sessions','approvals','comms','workflows','scheduler','channels','skills','hands','analytics','logs','settings','wizard'];
|
||||
var validPages = ['overview','agents','sessions','approvals','comms','workflows','scheduler','channels','skills','hands','analytics','logs','runtime','settings','wizard'];
|
||||
var pageRedirects = {
|
||||
'chat': 'agents',
|
||||
'templates': 'agents',
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
// Runtime page — system overview and provider status
|
||||
document.addEventListener('alpine:init', function() {
|
||||
Alpine.data('runtimePage', function() {
|
||||
return {
|
||||
loading: true,
|
||||
uptime: '-',
|
||||
agentCount: 0,
|
||||
version: '-',
|
||||
defaultModel: '-',
|
||||
platform: '-',
|
||||
arch: '-',
|
||||
apiListen: '-',
|
||||
homeDir: '-',
|
||||
logLevel: '-',
|
||||
networkEnabled: false,
|
||||
providers: [],
|
||||
|
||||
async loadData() {
|
||||
this.loading = true;
|
||||
try {
|
||||
var results = await Promise.all([
|
||||
OpenFangAPI.get('/api/status'),
|
||||
OpenFangAPI.get('/api/version'),
|
||||
OpenFangAPI.get('/api/providers'),
|
||||
OpenFangAPI.get('/api/agents')
|
||||
]);
|
||||
var status = results[0];
|
||||
var ver = results[1];
|
||||
var prov = results[2];
|
||||
var agents = results[3];
|
||||
|
||||
this.version = ver.version || '-';
|
||||
this.platform = ver.platform || '-';
|
||||
this.arch = ver.arch || '-';
|
||||
this.agentCount = Array.isArray(agents) ? agents.length : 0;
|
||||
this.defaultModel = status.default_model || '-';
|
||||
this.apiListen = status.api_listen || status.listen || '-';
|
||||
this.homeDir = status.home_dir || '-';
|
||||
this.logLevel = status.log_level || '-';
|
||||
this.networkEnabled = !!status.network_enabled;
|
||||
|
||||
// Compute uptime from uptime_seconds
|
||||
var diff = status.uptime_seconds || 0;
|
||||
if (diff < 60) this.uptime = diff + 's';
|
||||
else if (diff < 3600) this.uptime = Math.floor(diff / 60) + 'm ' + (diff % 60) + 's';
|
||||
else if (diff < 86400) this.uptime = Math.floor(diff / 3600) + 'h ' + Math.floor((diff % 3600) / 60) + 'm';
|
||||
else this.uptime = Math.floor(diff / 86400) + 'd ' + Math.floor((diff % 86400) / 3600) + 'h';
|
||||
|
||||
this.providers = (prov.providers || []).filter(function(p) {
|
||||
return p.auth_status === 'Configured' || p.reachable || p.is_local;
|
||||
});
|
||||
} catch(e) {
|
||||
console.error('Runtime load error:', e);
|
||||
}
|
||||
this.loading = false;
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
@@ -25,6 +25,10 @@ function skillsPage() {
|
||||
// Skill detail modal
|
||||
skillDetail: null,
|
||||
detailLoading: false,
|
||||
showSkillCode: false,
|
||||
skillCode: '',
|
||||
skillCodeFilename: '',
|
||||
skillCodeLoading: false,
|
||||
|
||||
// MCP servers
|
||||
mcpServers: [],
|
||||
@@ -205,6 +209,26 @@ function skillsPage() {
|
||||
closeDetail() {
|
||||
this.skillDetail = null;
|
||||
this.installResult = null;
|
||||
this.showSkillCode = false;
|
||||
this.skillCode = '';
|
||||
this.skillCodeFilename = '';
|
||||
},
|
||||
|
||||
async viewSkillCode(slug) {
|
||||
if (this.showSkillCode) {
|
||||
this.showSkillCode = false;
|
||||
return;
|
||||
}
|
||||
this.skillCodeLoading = true;
|
||||
try {
|
||||
var data = await OpenFangAPI.get('/api/clawhub/skill/' + encodeURIComponent(slug) + '/code');
|
||||
this.skillCode = data.code || '';
|
||||
this.skillCodeFilename = data.filename || 'source';
|
||||
this.showSkillCode = true;
|
||||
} catch(e) {
|
||||
OpenFangToast.error('Could not load skill source code');
|
||||
}
|
||||
this.skillCodeLoading = false;
|
||||
},
|
||||
|
||||
// Install from ClawHub
|
||||
|
||||
@@ -20,6 +20,7 @@ reqwest = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
hex = { workspace = true }
|
||||
serde_yaml = { workspace = true }
|
||||
zip = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
||||
|
||||
@@ -17,7 +17,7 @@ use crate::SkillError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::path::{Path, PathBuf};
|
||||
use tracing::info;
|
||||
use tracing::{info, warn};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// API response types (matching actual ClawHub v1 API — verified Feb 2026)
|
||||
@@ -459,27 +459,49 @@ impl ClawHubClient {
|
||||
let skill_dir = target_dir.join(slug);
|
||||
std::fs::create_dir_all(&skill_dir)?;
|
||||
|
||||
// Extract: ClawHub delivers as zip bundles. For now, detect content type
|
||||
// and write accordingly. A full zip extraction would use the `zip` crate.
|
||||
// Detect content type and extract accordingly
|
||||
let content_str = String::from_utf8_lossy(&bytes);
|
||||
let is_skillmd = content_str.trim_start().starts_with("---");
|
||||
|
||||
if is_skillmd {
|
||||
std::fs::write(skill_dir.join("SKILL.md"), &*bytes)?;
|
||||
} else {
|
||||
// Try to detect if it's a zip by magic bytes (PK\x03\x04)
|
||||
if bytes.len() >= 4 && bytes[0] == 0x50 && bytes[1] == 0x4b {
|
||||
// It's a zip — write as raw zip for future extraction
|
||||
std::fs::write(skill_dir.join("skill.zip"), &*bytes)?;
|
||||
// Try to fetch the SKILL.md file directly as fallback
|
||||
if let Ok(skillmd_content) = self.get_file(slug, "SKILL.md").await {
|
||||
std::fs::write(skill_dir.join("SKILL.md"), &skillmd_content)?;
|
||||
} else {
|
||||
std::fs::write(skill_dir.join("package.json"), &*bytes)?;
|
||||
} else if bytes.len() >= 4 && bytes[0] == 0x50 && bytes[1] == 0x4b {
|
||||
// Zip archive — extract all files
|
||||
let cursor = std::io::Cursor::new(&*bytes);
|
||||
match zip::ZipArchive::new(cursor) {
|
||||
Ok(mut archive) => {
|
||||
for i in 0..archive.len() {
|
||||
let mut file = match archive.by_index(i) {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
warn!(index = i, error = %e, "Skipping zip entry");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let Some(enclosed_name) = file.enclosed_name() else {
|
||||
warn!("Skipping zip entry with unsafe path");
|
||||
continue;
|
||||
};
|
||||
let out_path = skill_dir.join(enclosed_name);
|
||||
if file.is_dir() {
|
||||
std::fs::create_dir_all(&out_path)?;
|
||||
} else {
|
||||
if let Some(parent) = out_path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
let mut out_file = std::fs::File::create(&out_path)?;
|
||||
std::io::copy(&mut file, &mut out_file)?;
|
||||
}
|
||||
}
|
||||
info!(slug, entries = archive.len(), "Extracted skill zip");
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(slug, error = %e, "Failed to read zip, saving raw");
|
||||
std::fs::write(skill_dir.join("skill.zip"), &*bytes)?;
|
||||
}
|
||||
} else {
|
||||
std::fs::write(skill_dir.join("package.json"), &*bytes)?;
|
||||
}
|
||||
} else {
|
||||
std::fs::write(skill_dir.join("package.json"), &*bytes)?;
|
||||
}
|
||||
|
||||
// Step 2-3: Detect format and convert
|
||||
|
||||
Reference in New Issue
Block a user