@kairyou/agent-tools 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +147 -69
  2. package/README.zh-CN.md +143 -67
  3. package/dist/vision/cli.mjs +1972 -0
  4. package/dist/vision/mcp-server.mjs +32858 -0
  5. package/{statusline/claude/statusline.mjs → integrations/statusline/claude-statusline.mjs} +4 -2
  6. package/integrations/usage/cli.mjs +27 -0
  7. package/{hooks/codex/usage-hook.mjs → integrations/usage/codex-hook.mjs} +1 -1
  8. package/{lib/usage.mjs → integrations/usage/core.mjs} +5 -1
  9. package/{plugins/opencode/usage-plugin.mjs → integrations/usage/opencode-plugin.mjs} +1 -1
  10. package/integrations/usage/skills/at-usage/SKILL.md +16 -0
  11. package/integrations/vision/lib/cli.mjs +137 -0
  12. package/integrations/vision/lib/config.mjs +159 -0
  13. package/integrations/vision/lib/errors.mjs +35 -0
  14. package/integrations/vision/lib/image-source.mjs +273 -0
  15. package/integrations/vision/lib/inspect.mjs +108 -0
  16. package/integrations/vision/lib/providers/anthropic-compatible.mjs +59 -0
  17. package/integrations/vision/lib/providers/openai-compatible.mjs +56 -0
  18. package/integrations/vision/lib/providers/shared.mjs +251 -0
  19. package/integrations/vision/lib/rate-limit.mjs +188 -0
  20. package/integrations/vision/lib/redact.mjs +30 -0
  21. package/integrations/vision/mcp-server.mjs +96 -0
  22. package/integrations/vision/skills/at-vision/SKILL.md +66 -0
  23. package/package.json +15 -10
  24. package/scripts/build-vision.mjs +35 -0
  25. package/scripts/capture-codex-tools.mjs +48 -0
  26. package/scripts/install.mjs +511 -29
  27. package/scripts/release.mjs +126 -0
  28. package/skills/integrations/at-zentao/SKILL.md +148 -0
  29. package/skills/workflow/at-commit/SKILL.md +3 -8
  30. package/skills/workflow/at-review/SKILL.md +9 -4
  31. package/skills/workflow/at-simplify/SKILL.md +1 -0
  32. package/hooks/claude/.gitkeep +0 -1
  33. package/hooks/codex/.gitkeep +0 -1
  34. package/hooks/common/.gitkeep +0 -1
  35. package/hooks/opencode/.gitkeep +0 -1
  36. package/statusline/.gitkeep +0 -1
  37. package/statusline/codex/.gitkeep +0 -1
  38. /package/{plugins/opencode/usage-tui.mjs → integrations/usage/opencode-tui.mjs} +0 -0
@@ -20,7 +20,9 @@ const AGENT_TOOLS_HOME = process.env.AGENT_TOOLS_HOME || join(SCRIPT_DIR, "..",
20
20
  const DEFAULT_CONFIG_FILE = join(AGENT_TOOLS_HOME, "config.jsonc");
21
21
  const SNAPSHOT_FILE = join(AGENT_TOOLS_HOME, "cache", "usage-snapshot.json");
22
22
  const REFRESH_STATE_FILE = join(AGENT_TOOLS_HOME, "cache", "usage-refresh-state.json");
23
- const USAGE_RUNTIME = join(AGENT_TOOLS_HOME, "lib", "usage.mjs");
23
+ // Cross-integration dependency: statusline renders provider usage via the
24
+ // usage integration's query engine.
25
+ const USAGE_RUNTIME = join(AGENT_TOOLS_HOME, "integrations", "usage", "core.mjs");
24
26
  const DEFAULT_SNAPSHOT_TTL_MS = 60_000;
25
27
  const DEFAULT_REFRESH_COOLDOWN_MS = 30_000;
26
28
  const DEFAULT_FAILURE_BACKOFF_MS = 120_000;
@@ -132,7 +134,7 @@ function normalizeField(field) {
132
134
  }
133
135
 
134
136
  function mergeConfig(cli) {
135
- const rootConfig = readJsonFile(process.env.AGENT_TOOLS_CONFIG || DEFAULT_CONFIG_FILE);
137
+ const rootConfig = readJsonFile(DEFAULT_CONFIG_FILE);
136
138
  const fileConfig = rootConfig.statusline || {};
137
139
  const envFields = process.env.AGENT_TOOLS_STATUSLINE_FIELDS;
138
140
  const envSeparator = process.env.AGENT_TOOLS_STATUSLINE_SEPARATOR;
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ // Local CLI used by the managed at-usage skills.
3
+
4
+ import { queryAgentProviderUsage } from "./core.mjs";
5
+
6
+ function parseAgent(argv) {
7
+ for (let index = 0; index < argv.length; index += 1) {
8
+ const arg = argv[index];
9
+ if (arg === "--agent" && argv[index + 1]) return argv[index + 1];
10
+ if (arg.startsWith("--agent=")) return arg.slice("--agent=".length);
11
+ }
12
+ return null;
13
+ }
14
+
15
+ const agent = parseAgent(process.argv.slice(2));
16
+ if (agent !== "claude" && agent !== "codex") {
17
+ process.stderr.write(agent ? `Unsupported agent: ${agent}\n` : "Missing --agent <claude|codex>\n");
18
+ process.exitCode = 2;
19
+ } else {
20
+ try {
21
+ const result = await queryAgentProviderUsage(agent);
22
+ if (result?.text) process.stdout.write(`${result.text}\n`);
23
+ } catch {
24
+ // Usage is informational. Leave stdout empty so the skill can report the
25
+ // provider as unavailable without exposing endpoint or credential details.
26
+ }
27
+ }
@@ -11,7 +11,7 @@ import { fileURLToPath } from "node:url";
11
11
 
12
12
  const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
13
13
  const AGENT_TOOLS_HOME = process.env.AGENT_TOOLS_HOME || path.resolve(SCRIPT_DIR, "..", "..");
14
- const USAGE_SCRIPT = path.join(AGENT_TOOLS_HOME, "lib", "usage.mjs");
14
+ const USAGE_SCRIPT = path.join(SCRIPT_DIR, "core.mjs");
15
15
  const LOG_PATH = path.join(AGENT_TOOLS_HOME, "logs", "usage-hook.log");
16
16
  const TIMEOUT_MS = Number(process.env.AGENT_TOOLS_USAGE_HOOK_TIMEOUT_MS || 4500);
17
17
  const MAX_LOG_BYTES = Number(process.env.AGENT_TOOLS_USAGE_HOOK_LOG_BYTES || 256 * 1024);
@@ -14,7 +14,7 @@ const CODEX_HOME = process.env.CODEX_HOME || join(homedir(), ".codex");
14
14
  const AGENT_TOOLS_HOME = process.env.AGENT_TOOLS_HOME || join(homedir(), ".agent-tools");
15
15
  const AUTH_PATH = join(CODEX_HOME, "auth.json");
16
16
  const CODEX_CONFIG_PATH = join(CODEX_HOME, "config.toml");
17
- const AGENT_CONFIG_PATH = process.env.AGENT_TOOLS_CONFIG || join(AGENT_TOOLS_HOME, "config.jsonc");
17
+ const AGENT_CONFIG_PATH = join(AGENT_TOOLS_HOME, "config.jsonc");
18
18
  const DEBUG_PATH = join(AGENT_TOOLS_HOME, "logs", "usage-debug.log");
19
19
  const ROUTE_CACHE_PATH = join(AGENT_TOOLS_HOME, "cache", "usage-routes.json");
20
20
  const SNAPSHOT_PATH = join(AGENT_TOOLS_HOME, "cache", "usage-snapshot.json");
@@ -1171,6 +1171,10 @@ export async function queryProviderUsage(input, options = {}) {
1171
1171
  }
1172
1172
 
1173
1173
  async function refresh(agent = "codex") {
1174
+ return await queryAgentProviderUsage(agent);
1175
+ }
1176
+
1177
+ export async function queryAgentProviderUsage(agent = "codex") {
1174
1178
  return await queryUsageContext(await usageContext(agent), {
1175
1179
  agent,
1176
1180
  rememberSnapshot: agent === "claude",
@@ -1,4 +1,4 @@
1
- import { queryProviderUsage } from "../../lib/usage.mjs";
1
+ import { queryProviderUsage } from "./core.mjs";
2
2
 
3
3
  const DEFAULT_REFRESH_MS = 60_000;
4
4
 
@@ -0,0 +1,16 @@
1
+ ---
2
+ name: at-usage
3
+ description: Query and display the current API provider balance and recent usage.
4
+ ---
5
+
6
+ Run this installed local command once using the shell or command-execution tool:
7
+
8
+ ```text
9
+ node "{{USAGE_CLI_PATH}}" --agent {{USAGE_AGENT}}
10
+ ```
11
+
12
+ Return stdout verbatim as the complete response. Do not explain, reformat, or
13
+ wrap it in Markdown. If stdout is empty, say exactly `Provider usage is unavailable.`
14
+
15
+ Never run `npx`, `npm`, `pnpm`, `bun`, install a package, or substitute another
16
+ usage script. Use only the installed command above.
@@ -0,0 +1,137 @@
1
+ // `agent-tools inspect-image` and the installed agent fallback entry for the
2
+ // vision runtime. MCP remains preferred; this direct entry covers hosts or
3
+ // model gateways that cannot invoke MCP namespace tools.
4
+ //
5
+ // Usage:
6
+ // agent-tools inspect-image <path|url> --question "<text>" [--question "..."]
7
+ // agent-tools inspect-image --request-file <request.json> [--json]
8
+ //
9
+ // Options:
10
+ // -q, --question <text> Question about the image (repeatable with a target).
11
+ // --request-file <path> Read MCP-shaped { image_source, questions } JSON.
12
+ // --json Print the raw JSON result only.
13
+ // -h, --help Show this help.
14
+
15
+ import fs from "node:fs";
16
+ import path from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+ import { createVisionService } from "./inspect.mjs";
19
+ import { isVisionError } from "./errors.mjs";
20
+
21
+ function printHelp() {
22
+ const lines = [];
23
+ for (const line of fs.readFileSync(fileURLToPath(import.meta.url), "utf8").split("\n")) {
24
+ if (line.startsWith("//")) lines.push(line.replace(/^\/\/ ?/, ""));
25
+ else if (lines.length) break;
26
+ }
27
+ console.log(lines.join("\n"));
28
+ }
29
+
30
+ function parseArgs(argv) {
31
+ const opts = { target: null, questions: [], requestFile: null, json: false, help: false };
32
+ for (let i = 0; i < argv.length; i++) {
33
+ const a = argv[i];
34
+ switch (a) {
35
+ case "-q":
36
+ case "--question": {
37
+ const value = argv[++i];
38
+ if (!value) {
39
+ console.error(`Missing value for ${a}`);
40
+ process.exit(2);
41
+ }
42
+ opts.questions.push(value);
43
+ break;
44
+ }
45
+ case "--json": opts.json = true; break;
46
+ case "--request-file": {
47
+ const value = argv[++i];
48
+ if (!value) {
49
+ console.error("Missing value for --request-file");
50
+ process.exit(2);
51
+ }
52
+ opts.requestFile = value;
53
+ break;
54
+ }
55
+ case "-h":
56
+ case "--help": opts.help = true; break;
57
+ default:
58
+ if (a.startsWith("-")) {
59
+ console.error(`Unknown option: ${a}`);
60
+ process.exit(2);
61
+ }
62
+ if (opts.target) {
63
+ console.error(`Unexpected extra argument: ${a} (one image per call)`);
64
+ process.exit(2);
65
+ }
66
+ opts.target = a;
67
+ }
68
+ }
69
+ return opts;
70
+ }
71
+
72
+ export async function runInspectImageCli(argv) {
73
+ const opts = parseArgs(argv);
74
+ if (opts.help || (!opts.target && !opts.requestFile && opts.questions.length === 0)) {
75
+ printHelp();
76
+ return opts.help ? 0 : 2;
77
+ }
78
+ if (opts.requestFile && (opts.target || opts.questions.length > 0)) {
79
+ console.error("--request-file cannot be combined with <path|url> or --question.");
80
+ return 2;
81
+ }
82
+
83
+ try {
84
+ let imageSource;
85
+ let questions;
86
+ if (opts.requestFile) {
87
+ const requestPath = path.resolve(opts.requestFile);
88
+ let request;
89
+ try {
90
+ request = JSON.parse(fs.readFileSync(requestPath, "utf8"));
91
+ } catch (err) {
92
+ console.error(`Cannot read --request-file ${requestPath}: ${err.message}`);
93
+ return 2;
94
+ }
95
+ imageSource = request?.image_source;
96
+ questions = request?.questions;
97
+ } else {
98
+ if (!opts.target) {
99
+ console.error("Missing <path|url> argument.");
100
+ return 2;
101
+ }
102
+ if (opts.questions.length === 0) {
103
+ console.error('Missing --question. Example: --question "What error code is shown?"');
104
+ return 2;
105
+ }
106
+ const type = /^https?:\/\//i.test(opts.target) ? "url" : "file";
107
+ imageSource = { type, value: opts.target };
108
+ questions = opts.questions.map((text, i) => ({ id: `q${i + 1}`, text }));
109
+ }
110
+
111
+ const service = createVisionService();
112
+ const result = await service.inspect({
113
+ image_source: imageSource,
114
+ questions,
115
+ });
116
+ if (opts.json) {
117
+ console.log(JSON.stringify(result, null, 2));
118
+ return 0;
119
+ }
120
+ console.log(`request_id: ${result.request_id}`);
121
+ for (const answer of result.answers) {
122
+ const q = questions.find((x) => x.id === answer.question_id);
123
+ console.log(`\n${answer.question_id}: ${q ? q.text : ""}`);
124
+ console.log(` answer: ${answer.answer === null ? "(none)" : answer.answer}`);
125
+ if (answer.uncertainty) console.log(` uncertainty: ${answer.uncertainty}`);
126
+ }
127
+ return 0;
128
+ } catch (err) {
129
+ const code = isVisionError(err) ? err.code : "internal_error";
130
+ console.error(`[${code}] ${err.message}`);
131
+ return 1;
132
+ }
133
+ }
134
+
135
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
136
+ process.exitCode = await runInspectImageCli(process.argv.slice(2));
137
+ }
@@ -0,0 +1,159 @@
1
+ // Vision configuration loader. The single source of truth is
2
+ // ~/.agent-tools/config.jsonc (override root with AGENT_TOOLS_HOME). No
3
+ // implicit environment fallback: `apiKey` is either a literal string or an
4
+ // explicit `{ "env": "VARIABLE_NAME" }` secret reference declared in the file.
5
+
6
+ import fs from "node:fs";
7
+ import os from "node:os";
8
+ import path from "node:path";
9
+ import { parse as parseJsonc } from "jsonc-parser";
10
+ import { ERROR_CODES, VisionError } from "./errors.mjs";
11
+
12
+ export const PROVIDERS = Object.freeze(["openai-compatible", "anthropic-compatible"]);
13
+
14
+ export const CONFIG_DEFAULTS = Object.freeze({
15
+ timeoutMs: 30000,
16
+ maxImageBytes: 20 * 1024 * 1024,
17
+ maxConcurrentRequests: 2,
18
+ maxRequestsPerMinute: 30,
19
+ // Sent to both providers (Anthropic requires max_tokens; OpenAI gateway
20
+ // defaults are unpredictable). Lower it if your model caps output smaller.
21
+ maxOutputTokens: 8192,
22
+ });
23
+
24
+ const CONFIG_TEMPLATE = `{
25
+ "vision": {
26
+ "provider": "openai-compatible", // or "anthropic-compatible"
27
+ "baseUrl": "https://gateway.example.com/v1",
28
+ "model": "internal-vlm",
29
+ "apiKey": { "env": "OPENAI_API_KEY" } // or a literal string
30
+ }
31
+ }`;
32
+
33
+ export function agentToolsHome(env = process.env) {
34
+ return env.AGENT_TOOLS_HOME || path.join(os.homedir(), ".agent-tools");
35
+ }
36
+
37
+ export function configPath(env = process.env) {
38
+ return path.join(agentToolsHome(env), "config.jsonc");
39
+ }
40
+
41
+ function configError(message) {
42
+ return new VisionError(
43
+ ERROR_CODES.CONFIG,
44
+ `${message}\nAdd a "vision" block to ${configPath()} , for example:\n${CONFIG_TEMPLATE}`
45
+ );
46
+ }
47
+
48
+ // Resolve `apiKey` per the secret reference rules. Returns the secret string,
49
+ // or null when the gateway does not require a key (field omitted).
50
+ export function resolveSecret(apiKey, env = process.env) {
51
+ if (apiKey === undefined || apiKey === null) return null;
52
+ if (typeof apiKey === "string") {
53
+ if (apiKey.trim() === "") {
54
+ throw new VisionError(ERROR_CODES.CONFIG, "vision.apiKey is an empty string; remove it or set a value.");
55
+ }
56
+ return apiKey;
57
+ }
58
+ if (typeof apiKey === "object" && typeof apiKey.env === "string" && apiKey.env.trim() !== "") {
59
+ const name = apiKey.env;
60
+ const value = env[name];
61
+ if (value === undefined || value === "") {
62
+ // Name the variable, never its (missing) value; no silent fallback.
63
+ throw new VisionError(
64
+ ERROR_CODES.CONFIG,
65
+ `vision.apiKey references environment variable "${name}", which is not set or empty.`
66
+ );
67
+ }
68
+ return value;
69
+ }
70
+ throw new VisionError(
71
+ ERROR_CODES.CONFIG,
72
+ 'vision.apiKey must be a string or { "env": "VARIABLE_NAME" }.'
73
+ );
74
+ }
75
+
76
+ function positiveInt(raw, name, fallback, { allowZero = false } = {}) {
77
+ if (raw === undefined || raw === null) return fallback;
78
+ if (!Number.isInteger(raw) || raw < 0 || (!allowZero && raw === 0)) {
79
+ throw new VisionError(
80
+ ERROR_CODES.CONFIG,
81
+ `vision.${name} must be a positive integer${allowZero ? " (0 disables it)" : ""}.`
82
+ );
83
+ }
84
+ return raw;
85
+ }
86
+
87
+ function normalizeBaseUrl(raw) {
88
+ if (typeof raw !== "string") throw configError("vision.baseUrl must be an http(s) URL.");
89
+ let parsed;
90
+ try {
91
+ parsed = new URL(raw);
92
+ } catch {
93
+ throw configError("vision.baseUrl must be an http(s) URL.");
94
+ }
95
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
96
+ throw configError("vision.baseUrl must be an http(s) URL.");
97
+ }
98
+ if (parsed.username || parsed.password) {
99
+ throw configError("vision.baseUrl must not contain credentials.");
100
+ }
101
+ if (parsed.search || parsed.hash) {
102
+ throw configError("vision.baseUrl must not contain a query string or fragment.");
103
+ }
104
+ return parsed.toString().replace(/\/+$/, "");
105
+ }
106
+
107
+ // Load and validate the vision config. `file`/`env` are injectable for tests.
108
+ export function loadVisionConfig({ file, env = process.env } = {}) {
109
+ const target = file || configPath(env);
110
+ if (!fs.existsSync(target)) {
111
+ throw configError(`Config file not found: ${target}`);
112
+ }
113
+ let raw;
114
+ try {
115
+ raw = fs.readFileSync(target, "utf8").replace(/^/, "");
116
+ } catch (err) {
117
+ throw configError(`Cannot read ${target}: ${err.message}`);
118
+ }
119
+ const errors = [];
120
+ const parsed = parseJsonc(raw, errors, { allowTrailingComma: true });
121
+ if (errors.length > 0 || !parsed || typeof parsed !== "object") {
122
+ throw configError(`Cannot parse ${target} as JSONC.`);
123
+ }
124
+ const vision = parsed.vision;
125
+ if (!vision || typeof vision !== "object") {
126
+ throw configError(`Missing "vision" section in ${target}.`);
127
+ }
128
+ if (!PROVIDERS.includes(vision.provider)) {
129
+ throw configError(
130
+ `vision.provider must be one of: ${PROVIDERS.join(", ")} (got ${JSON.stringify(vision.provider ?? null)}).`
131
+ );
132
+ }
133
+ const baseUrl = normalizeBaseUrl(vision.baseUrl);
134
+ if (typeof vision.model !== "string" || vision.model.trim() === "") {
135
+ throw configError("vision.model must be a non-empty string.");
136
+ }
137
+
138
+ return {
139
+ provider: vision.provider,
140
+ baseUrl,
141
+ model: vision.model,
142
+ apiKey: resolveSecret(vision.apiKey, env),
143
+ timeoutMs: positiveInt(vision.timeoutMs, "timeoutMs", CONFIG_DEFAULTS.timeoutMs),
144
+ maxImageBytes: positiveInt(vision.maxImageBytes, "maxImageBytes", CONFIG_DEFAULTS.maxImageBytes),
145
+ maxConcurrentRequests: positiveInt(
146
+ vision.maxConcurrentRequests,
147
+ "maxConcurrentRequests",
148
+ CONFIG_DEFAULTS.maxConcurrentRequests
149
+ ),
150
+ // 0 disables the rolling-window limit; concurrency has no disable switch.
151
+ maxRequestsPerMinute: positiveInt(
152
+ vision.maxRequestsPerMinute,
153
+ "maxRequestsPerMinute",
154
+ CONFIG_DEFAULTS.maxRequestsPerMinute,
155
+ { allowZero: true }
156
+ ),
157
+ maxOutputTokens: positiveInt(vision.maxOutputTokens, "maxOutputTokens", CONFIG_DEFAULTS.maxOutputTokens),
158
+ };
159
+ }
@@ -0,0 +1,35 @@
1
+ // Normalized error type for the vision runtime. Every failure surfaced to the
2
+ // MCP tool, the diagnostic CLI, or tests carries a stable `code` so callers can
3
+ // branch on category without parsing prose.
4
+
5
+ export const ERROR_CODES = Object.freeze({
6
+ CONFIG: "config_error",
7
+ INPUT: "input_error",
8
+ FETCH: "fetch_error",
9
+ RATE_LIMIT: "rate_limit_error",
10
+ PROVIDER_AUTH: "provider_auth_error",
11
+ PROVIDER_HTTP: "provider_http_error",
12
+ PROVIDER_TIMEOUT: "provider_timeout_error",
13
+ PROVIDER_RESPONSE: "provider_response_error",
14
+ });
15
+
16
+ export class VisionError extends Error {
17
+ constructor(code, message, { cause, detail } = {}) {
18
+ super(message, cause ? { cause } : undefined);
19
+ this.name = "VisionError";
20
+ this.code = code;
21
+ if (detail !== undefined) this.detail = detail;
22
+ }
23
+ }
24
+
25
+ export function isVisionError(err) {
26
+ return err instanceof VisionError;
27
+ }
28
+
29
+ // Wrap unknown failures so callers always see a VisionError. Existing
30
+ // VisionErrors pass through untouched.
31
+ export function toVisionError(err, fallbackCode = ERROR_CODES.PROVIDER_HTTP) {
32
+ if (isVisionError(err)) return err;
33
+ const message = err && typeof err.message === "string" ? err.message : String(err);
34
+ return new VisionError(fallbackCode, message, { cause: err });
35
+ }