use crate::arkuse::api::Usage;
use crate::arkuse::{ark, db, render, window};
use serde_json::{json, Value};
const PROTOCOL_VERSION: &str = "2025-06-18";
#[derive(Clone, Copy)]
pub struct Settings<'a> {
pub db_path: &'a str,
pub days: i64,
pub tz_secs: i64,
pub colour: bool,
pub tier: &'a str,
pub period_start: Option<i64>,
pub account_month: Option<(i64, i64)>,
pub account: Option<&'a Usage>,
pub account_error: Option<&'a str>,
}
pub fn run(settings: Settings) {
use std::io::{BufRead, Write};
let stdin = std::io::stdin();
let mut out = std::io::stdout().lock();
for line in stdin.lock().lines() {
let Ok(line) = line else { break };
if line.trim().is_empty() {
continue;
}
let Ok(msg) = serde_json::from_str::<Value>(&line) else {
continue; };
if let Some(response) = handle(&msg, &settings) {
if writeln!(out, "{response}").is_err() {
break; }
out.flush().ok();
}
}
}
pub fn handle(msg: &Value, settings: &Settings) -> Option<Value> {
let method = msg.get("method")?.as_str()?;
let id = msg.get("id")?;
let result = match method {
"initialize" => Ok(json!({
"protocolVersion": msg
.pointer("/params/protocolVersion")
.and_then(|v| v.as_str())
.unwrap_or(PROTOCOL_VERSION),
"capabilities": { "tools": {} },
"serverInfo": { "name": "arkuse", "version": env!("CARGO_PKG_VERSION") },
})),
"ping" => Ok(json!({})),
"tools/list" => Ok(json!({ "tools": tools() })),
"tools/call" => tool_call(msg, settings),
other => Err((-32601, format!("method not found: {other}"))),
};
Some(match result {
Ok(value) => json!({ "jsonrpc": "2.0", "id": id, "result": value }),
Err((code, message)) => json!({
"jsonrpc": "2.0", "id": id,
"error": { "code": code, "message": message },
}),
})
}
fn tool_call(msg: &Value, settings: &Settings) -> Result<Value, (i64, String)> {
let Settings {
db_path,
days,
tz_secs,
colour,
tier,
period_start,
account_month,
account,
account_error,
} = *settings;
let name = msg
.pointer("/params/name")
.and_then(|v| v.as_str())
.unwrap_or_default();
let args = msg
.pointer("/params/arguments")
.cloned()
.unwrap_or(json!({}));
let number =
|key: &str, fallback: i64| args.get(key).and_then(|v| v.as_i64()).unwrap_or(fallback);
let plan_id = args
.get("plan")
.and_then(|v| v.as_str())
.unwrap_or(tier)
.to_string();
let catalog = ark::catalog();
let rows = || db::read_rows(db_path, window::now_ms() - days * 86_400_000);
let text = match name {
"usage" => rows().map(|rows| {
let mut report = window::build(
&rows,
&catalog,
&plan_id,
window::now_ms(),
account_month.map(|(start, _)| start).or(period_start),
account,
);
if account_month.is_some() {
report.period_label = "Agent Plan billing month".into();
report.period_end_ms = account_month.map(|(_, reset)| reset);
}
report.account = account.cloned();
report.account_error = account_error.map(str::to_string);
let _ = colour;
render::render_text(&report, db_path, 15, colour, render::DEFAULT_BAR_WIDTH)
}),
"plans" => Ok(render::plans_text(colour)),
"models" => {
if catalog.plan(&plan_id).is_none() {
return Err((
-32602,
format!("plan must be one of: {}", catalog.plan_ids().join(", ")),
));
}
let gated = args.get("gated").and_then(|v| v.as_bool()).unwrap_or(false);
Ok(render::models_text(&plan_id, gated, colour))
}
"daily" => rows().map(|rows| {
render::bucket_text(
&rows,
&catalog,
window::now_ms(),
number("days", days) * 86_400_000,
86_400_000,
tz_secs,
colour,
crate::report_render::Fmt::Table,
)
}),
"hourly" => rows().map(|rows| {
render::bucket_text(
&rows,
&catalog,
window::now_ms(),
number("hours", 24) * 3_600_000,
3_600_000,
tz_secs,
colour,
crate::report_render::Fmt::Table,
)
}),
"session" => rows().map(|rows| {
render::session_text(
&rows,
&catalog,
number("limit", 20) as usize,
colour,
crate::report_render::Fmt::Table,
)
}),
"model" => {
let wanted = args
.get("id")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string();
rows().map(|rows| {
let report = window::build(
&rows,
&catalog,
&plan_id,
window::now_ms(),
period_start,
account,
);
render::model_text(&report, &wanted, colour)
})
}
_ => {
return Ok(json!({
"content": [{ "type": "text", "text": format!("unknown tool: {name}") }],
"isError": true,
}))
}
};
Ok(match text {
Ok(body) => json!({ "content": [{ "type": "text", "text": body }] }),
Err(error) => json!({
"content": [{ "type": "text", "text": format!("error: {error}") }],
"isError": true,
}),
})
}
fn tools() -> Value {
json!([
{ "name": "usage", "description": "Volcengine ARK Agent Plan usage in AFP: the plan's own 5-hour / weekly / monthly meters, this machine's per-model AFP, and the reconciliation between them.", "inputSchema": { "type": "object", "properties": { "plan": { "type": "string", "description": "ARK tier: small, medium (default), large, or max" } } } },
{ "name": "plans", "description": "The ARK Agent Plan catalogue: each personal tier's price and monthly AFP quota, the window shares, and the enterprise price list.", "inputSchema": { "type": "object", "properties": {} } },
{ "name": "models", "description": "ARK model coefficients: AFP per 10,000 tokens, the yuan rate that implies, the minimum tier, and whether the given tier grants it.", "inputSchema": { "type": "object", "properties": { "plan": { "type": "string", "description": "ARK tier: small, medium (default), large, or max" }, "gated": { "type": "boolean", "description": "Only models the tier grants" } } } },
{ "name": "daily", "description": "AFP, requests and tokens per day.", "inputSchema": { "type": "object", "properties": { "days": { "type": "integer", "description": "days of history (default 120)" } } } },
{ "name": "hourly", "description": "AFP, requests and tokens per hour.", "inputSchema": { "type": "object", "properties": { "hours": { "type": "integer", "description": "hours of history (default 24)" } } } },
{ "name": "session", "description": "AFP per opencode session, newest first.", "inputSchema": { "type": "object", "properties": { "limit": { "type": "integer", "description": "sessions to list (default 20)" } } } },
{ "name": "model", "description": "One model's coefficient, tokens and AFP for the period, against the given tier.", "inputSchema": { "type": "object", "properties": { "id": { "type": "string", "description": "model id, e.g. deepseek-v4.1-flash" }, "plan": { "type": "string", "description": "ARK tier: small, medium (default), large, or max" } }, "required": ["id"] } },
])
}
#[cfg(test)]
mod tests {
use super::*;
fn call(method: &str, params: Value, id: i64) -> Value {
handle(
&json!({ "jsonrpc": "2.0", "id": id, "method": method, "params": params }),
&Settings {
db_path: "/nonexistent/opencode.db",
days: 120,
tz_secs: 0,
colour: false,
tier: ark::DEFAULT_PLAN,
period_start: None,
account_month: None,
account: None,
account_error: None,
},
)
.expect("notification")
}
#[test]
fn initialize_negotiates_the_client_version() {
let r = call("initialize", json!({ "protocolVersion": "2025-03-26" }), 1);
assert_eq!(r["result"]["protocolVersion"], "2025-03-26");
assert_eq!(r["result"]["serverInfo"]["name"], "arkuse");
}
#[test]
fn unknown_method_is_jsonrpc_error() {
let r = call("resources/list", json!({}), 2);
assert_eq!(r["error"]["code"], -32601);
}
#[test]
fn unknown_tool_is_iserror_result_not_rpc_error() {
let r = call("tools/call", json!({ "name": "nope", "arguments": {} }), 3);
assert!(r.get("error").is_none());
assert_eq!(r["result"]["isError"], true);
}
#[test]
fn a_missing_store_reports_an_error_result() {
let r = call("tools/call", json!({ "name": "usage", "arguments": {} }), 4);
assert_eq!(r["result"]["isError"], true, "{r}");
assert!(r["result"]["content"][0]["text"]
.as_str()
.unwrap()
.contains("error"));
}
#[test]
fn tools_list_covers_every_command() {
let r = call("tools/list", json!({}), 5);
let mut names: Vec<&str> = r["result"]["tools"]
.as_array()
.unwrap()
.iter()
.map(|t| t["name"].as_str().unwrap())
.collect();
names.sort_unstable();
assert_eq!(
names,
["daily", "hourly", "model", "models", "plans", "session", "usage"]
);
}
#[test]
fn models_tool_is_the_catalogue_and_needs_no_store() {
let r = call(
"tools/call",
json!({ "name": "models", "arguments": {} }),
6,
);
assert!(r.get("error").is_none(), "{r}");
let text = r["result"]["content"][0]["text"].as_str().unwrap();
assert!(text.contains("deepseek-v4.1-flash"), "{text}");
assert!(text.contains("GRANTED"), "{text}");
}
#[test]
fn models_tool_rejects_an_unknown_plan() {
let r = call(
"tools/call",
json!({ "name": "models", "arguments": { "plan": "nope" } }),
7,
);
assert_eq!(r["error"]["code"], -32602, "{r}");
}
}