Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ pnpm clean # rm -rf dist bin
| `login --key <key>` | Persist API Key |
| `logout [--purge]` | Clear credentials / remove config dir |
| `tools` | List MCP tools + schemas |
| `call <toolName> [--kv ...]` | Invoke a tool (`key=value` scalars, `key:=value` JSON) |
| `call <toolName> [--args <json>]` | Invoke a tool (arguments as a JSON object) |
| `config [--set-url ...] [--set-auth-header ...]` | View / modify configuration |

Global options: `--json`, `--url <url>`, `--auth-header <header>`, `--version`
Expand Down Expand Up @@ -102,10 +102,11 @@ Env vars > Config file > Defaults

`--url`, `--auth-header`, `--json` are per-invocation overrides, don't write to file.

### --kv Syntax (httpie-style)
### --args Syntax

- `key=value` — scalar, auto-infer string/number/boolean/null
- `key:=value` — JSON, `JSON.parse` failure is an explicit error (no silent fallback)
- `--args '<json>'` — pass all arguments as a single JSON object
- No `--args` needed for parameterless tools
- Invalid JSON or non-object values fail explicitly with a formatted error

### Dual-Channel Distribution

Expand Down
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ wf login --key <YOUR_API_KEY>
wf tools

# 3. Call a tool
wf call <toolName> --kv page=1 pageSize=10
wf call <toolName> --args '{"page":1,"pageSize":10}' --json
```

## Commands
Expand All @@ -89,15 +89,17 @@ wf call <toolName> --kv page=1 pageSize=10
### wf call arguments

```bash
# Call a tool with scalar arguments (auto-inferred: string, number, boolean, null)
wf call <toolName> --kv key1=value1 key2=value2 key3=null
# Pass all arguments as a JSON object
wf call <toolName> --args '{"page":1,"pageSize":10,"filter":{"tag":"tool"}}' --json

# JSON value via key:=value (httpie-style, for tools that accept JSON fields)
wf call <toolName> --kv metadata:='{"timeout":5000}' tags:='["a","b"]'
# Parameterless tools — no --args needed
wf call <toolName> --json

# key:=value parse error fails explicitly (no silent fallback to string)
wf call <toolName> --kv bad:='{not json}'
# Error: --kv key:=value expects valid JSON, got: "{not json}"
# Invalid JSON or non-object values fail explicitly
wf call <toolName> --args 'not json' --json
# Error: --args expects valid JSON, got: "not json"
wf call <toolName> --args '[1,2,3]' --json
# Error: --args expects a JSON object, got: "[1,2,3]"
```

## Configuration
Expand Down
18 changes: 10 additions & 8 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ wf login --key <YOUR_API_KEY>
wf tools

# 3. 调用工具
wf call <toolName> --kv page=1 pageSize=10
wf call <toolName> --args '{"page":1,"pageSize":10}' --json
```

## 命令
Expand All @@ -89,15 +89,17 @@ wf call <toolName> --kv page=1 pageSize=10
### wf call 传参

```bash
# 标量 key=value(自动推断:string、number、boolean、null)
wf call <toolName> --kv key1=value1 key2=value2 key3=null
# 所有参数作为 JSON 对象传入
wf call <toolName> --args '{"page":1,"pageSize":10,"filter":{"tag":"tool"}}' --json

# JSON 值用 key:=value 语法(httpie 风格,工具接受 JSON 字段时使用)
wf call <toolName> --kv metadata:='{"timeout":5000}' tags:='["a","b"]'
# 无参数工具——不需要 --args
wf call <toolName> --json

# key:=value 解析失败会直接报错,不会静默降级为字符串
wf call <toolName> --kv bad:='{not json}'
# Error: --kv key:=value expects valid JSON, got: "{not json}"
# 无效 JSON 或非对象类型会直接报错
wf call <toolName> --args 'not json' --json
# Error: --args expects valid JSON, got: "not json"
wf call <toolName> --args '[1,2,3]' --json
# Error: --args expects a JSON object, got: "[1,2,3]"
```

## 配置
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@weavefox/cli",
"version": "0.0.3",
"version": "0.0.4",
"description": "WeaveFox CLI - Call server-side open capabilities via MCP protocol, for developers and AI agents",
"license": "MIT",
"repository": {
Expand Down
8 changes: 4 additions & 4 deletions skills/weavefox/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,11 @@ Returns all server-side tools with their names, descriptions, and parameter sche
## Call a Tool

```bash
# Scalar arguments (auto-inferred: string, number, boolean, null)
wf call <toolName> --kv key1=value1 key2=value2 --json
# Pass all arguments as a JSON object
wf call <toolName> --args '{"key":"value","num":42,"nested":{"obj":true}}' --json

# JSON arguments (httpie-style key:=value)
wf call <toolName> --kv key:='{"nested":"object"}' --json
# Parameterless tools — no --args needed
wf call <toolName> --json
```

## Parse Output
Expand Down
2 changes: 1 addition & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { join } from 'node:path';

const DEFAULT_MCP_URL = 'https://www.weavefox.cn/mcp';
const DEFAULT_AUTH_HEADER = 'Authorization';
const CONFIG_DIR = join(homedir(), '.weavefox');
export const CONFIG_DIR = join(homedir(), '.weavefox');
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
const CONFIG_FILE_MODE = 0o600;

Expand Down
122 changes: 51 additions & 71 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* wf login --key <key> Persist API Key
* wf logout [--purge] Clear credentials (optionally remove dir)
* wf tools List MCP tools exposed by the server
* wf call <toolName> [--kv ...] Invoke a tool (-kv key=value | key:=value)
* wf call <toolName> [--args ...] Invoke a tool (--args '<json>')
* wf config [--set-url <url>] Show / override MCP server URL
*
* Global options:
Expand All @@ -30,6 +30,11 @@ import {
WeaveFoxCliError,
} from './mcp-client.js';
import { outputToolResult, outputToolList } from './format.js';
import { startUpdateCheck } from './update-check.js';

// Start the update check early so it runs in parallel with the command.
// The promise is cached for 1 hour — most invocations resolve from disk instantly.
const updatePromise = startUpdateCheck(pkg.version);

export const cli = cac('wf')
.version(pkg.version)
Expand All @@ -41,7 +46,7 @@ export const cli = cac('wf')
cli
.command('login', 'Save your WeaveFox API Key locally')
.option('--key <key>', 'Your WeaveFox API Key')
.action((options) => {
.action(async (options) => {
if (!options.key) {
console.error(
pc.red('Error: ') + 'Please provide a key: ' + pc.cyan('wf login --key <YOUR_KEY>'),
Expand All @@ -50,12 +55,13 @@ cli
}
setConfig({ apiKey: options.key });
console.log(pc.green('✓') + ' API Key saved to ' + pc.dim(getConfigPath()));
await printUpdateNotice(options.json);
});

cli
.command('logout', 'Clear saved credentials')
.option('--purge', 'Remove the entire config directory (use before uninstalling)')
.action((options) => {
.action(async (options) => {
if (options.purge) {
purgeConfig();
console.log(pc.green('✓') + ' Config directory removed: ' + pc.dim(getConfigPath().replace(/config\.json$/, '')));
Expand All @@ -64,6 +70,7 @@ cli
clearConfig();
console.log(pc.green('✓') + ' Credentials cleared.');
}
await printUpdateNotice(options.json);
});

cli
Expand All @@ -78,14 +85,32 @@ cli

cli
.command('call <toolName>', 'Call a specific MCP tool by name')
.option('--kv <key=value>', 'Pass arguments as key=value (scalars) or key:=value (JSON)', {
type: [String],
})
.option('--args <json>', 'Pass all arguments as a JSON object')
.action(async (toolName, options) => {
const { json, url, authHeader } = options;
const args = parseToolArgs(options.kv);
const { json, url, authHeader, args: jsonArgs } = options;

await withClient(json, url, authHeader, async (client) => {
let args: Record<string, unknown> = {};

if (jsonArgs) {
let parsed: unknown;
try {
parsed = JSON.parse(jsonArgs);
} catch {
throw new WeaveFoxCliError(
'invalid_args',
`--args expects valid JSON, got: "${jsonArgs}"`,
);
}
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
throw new WeaveFoxCliError(
'invalid_args',
`--args expects a JSON object, got: "${jsonArgs}"`,
);
}
args = parsed as Record<string, unknown>;
}

const result = await callTool(client, toolName, args);
outputToolResult(result, json);
});
Expand All @@ -95,7 +120,7 @@ cli
.command('config', 'View or modify CLI configuration')
.option('--set-url <url>', 'Set the MCP Server URL')
.option('--set-auth-header <header>', 'Set the auth header name (default: Authorization)')
.action((options) => {
.action(async (options) => {
if (options.setUrl) {
setConfig({ mcpUrl: options.setUrl });
console.log(pc.green('✓') + ' MCP Server URL updated.');
Expand All @@ -113,6 +138,7 @@ cli
console.log(` ${pc.cyan('Auth header')} ${config.authHeader}${config.authHeader === 'Authorization' ? pc.dim(' (Bearer)') : ''}`);
console.log(` ${pc.cyan('Logged in')} ${hasApiKey() ? pc.green('Yes') : pc.red('No')}`);
console.log();
await printUpdateNotice(options.json);
});

/**
Expand Down Expand Up @@ -152,76 +178,30 @@ async function withClient(
if (client) {
await closeMcpClient(client);
}
await printUpdateNotice(jsonMode);
}
}

/**
* Parses --kv pairs into a arguments object.
*
* Two syntaxes (httpie-inspired):
* key=value Scalar; parsed via parseKvValue (auto-infers string/number/
* boolean/null).
* key:=value Explicit JSON; passes the value through JSON.parse. A parse
* failure is thrown, NOT silently downgraded to a string —
* the caller would otherwise get a bogus string that looks
* like JSON but isn't.
*
* Multiple pairs are allowed on one command line; later pairs override
* earlier ones for the same key.
*/
function parseToolArgs(kvPairs: string[] | undefined): Record<string, unknown> {
const args: Record<string, unknown> = {};
if (!kvPairs || kvPairs.length === 0) return args;

for (const pair of kvPairs) {
const jsonSep = pair.indexOf(':=');
const eqSep = pair.indexOf('=');

if (jsonSep !== -1 && (eqSep === -1 || jsonSep < eqSep)) {
const key = pair.slice(0, jsonSep).trim();
const raw = pair.slice(jsonSep + 2).trim();
try {
args[key] = JSON.parse(raw);
} catch {
throw new WeaveFoxCliError(
'invalid_kv',
`--kv key:=value expects valid JSON, got: "${raw}"`,
);
}
} else if (eqSep !== -1) {
const key = pair.slice(0, eqSep).trim();
const value = pair.slice(eqSep + 1).trim();
args[key] = parseKvValue(value);
} else {
throw new WeaveFoxCliError(
'invalid_kv',
`--kv expects key=value or key:=value, got: "${pair}"`,
);
}
}

return args;
}

/**
* Auto-infer scalar primitives for --kv key=value.
* Use --kv key:=value for any JSON-typed value (objects, arrays, edge cases).
*/
function parseKvValue(value: string): unknown {
const lower = value.toLowerCase();
if (lower === 'true') return true;
if (lower === 'false') return false;
if (lower === 'null') return null;
if (/^-?\d+$/.test(value)) return Number.parseInt(value, 10);
if (/^-?\d+\.\d+$/.test(value)) return Number.parseFloat(value);
return value;
}

/** show first 4 + last 4; mask everything in between. */
function maskApiKey(key: string): string {
if (!key) return pc.dim('(not set)');
if (key.length <= 8) return '*'.repeat(key.length);
return key.slice(0, 4) + '****' + key.slice(-4);
}

/**
* Prints a one-line update notice if a newer version is available.
* Skipped in `--json` mode to avoid polluting machine-readable output.
* The check was started (in parallel) before cli.parse, so by the time
* the command finishes the result is usually already resolved.
*/
async function printUpdateNotice(jsonMode: boolean): Promise<void> {
if (jsonMode) return;
const latest = await updatePromise;
if (latest) {
console.log(pc.yellow(`\nUpdate available: ${pkg.version} → ${latest}`));
console.log(pc.dim(`Run: npm i -g @weavefox/cli`));
}
}

cli.parse();
Loading