@kairyou/agent-tools 0.2.0 → 0.4.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 (40) hide show
  1. package/README.md +112 -115
  2. package/README.zh-CN.md +108 -115
  3. package/dist/statusline/claude-statusline.mjs +355 -0
  4. package/dist/usage/cli.mjs +25 -0
  5. package/dist/usage/codex-hook.mjs +144 -0
  6. package/dist/usage/core.mjs +1054 -0
  7. package/dist/usage/opencode-plugin.mjs +80 -0
  8. package/dist/usage/opencode-tui.mjs +46 -0
  9. package/dist/vision/cli.mjs +13 -13
  10. package/dist/vision/mcp-server.mjs +12 -12
  11. package/{statusline/claude/statusline.mjs → integrations/statusline/claude-statusline.mjs} +33 -2
  12. package/integrations/usage/cli.mjs +27 -0
  13. package/{hooks/codex/usage-hook.mjs → integrations/usage/codex-hook.mjs} +1 -1
  14. package/{lib/usage.mjs → integrations/usage/core.mjs} +34 -1
  15. package/{plugins/opencode/usage-plugin.mjs → integrations/usage/opencode-plugin.mjs} +1 -1
  16. package/integrations/usage/skills/at-usage/SKILL.md +16 -0
  17. package/{plugins → integrations}/vision/mcp-server.mjs +4 -4
  18. package/package.json +8 -12
  19. package/scripts/build.mjs +65 -0
  20. package/scripts/install.mjs +135 -90
  21. package/scripts/release.mjs +75 -5
  22. package/hooks/claude/.gitkeep +0 -1
  23. package/hooks/codex/.gitkeep +0 -1
  24. package/hooks/common/.gitkeep +0 -1
  25. package/hooks/opencode/.gitkeep +0 -1
  26. package/scripts/build-vision.mjs +0 -35
  27. package/statusline/.gitkeep +0 -1
  28. package/statusline/codex/.gitkeep +0 -1
  29. /package/{plugins/opencode/usage-tui.mjs → integrations/usage/opencode-tui.mjs} +0 -0
  30. /package/{lib/vision → integrations/vision/lib}/cli.mjs +0 -0
  31. /package/{lib/vision → integrations/vision/lib}/config.mjs +0 -0
  32. /package/{lib/vision → integrations/vision/lib}/errors.mjs +0 -0
  33. /package/{lib/vision → integrations/vision/lib}/image-source.mjs +0 -0
  34. /package/{lib/vision → integrations/vision/lib}/inspect.mjs +0 -0
  35. /package/{lib/vision → integrations/vision/lib}/providers/anthropic-compatible.mjs +0 -0
  36. /package/{lib/vision → integrations/vision/lib}/providers/openai-compatible.mjs +0 -0
  37. /package/{lib/vision → integrations/vision/lib}/providers/shared.mjs +0 -0
  38. /package/{lib/vision → integrations/vision/lib}/rate-limit.mjs +0 -0
  39. /package/{lib/vision → integrations/vision/lib}/redact.mjs +0 -0
  40. /package/{plugins → integrations}/vision/skills/at-vision/SKILL.md +0 -0
@@ -0,0 +1,80 @@
1
+ // integrations/usage/opencode-plugin.mjs
2
+ import { queryProviderUsage } from "./core.mjs";
3
+ var DEFAULT_REFRESH_MS = 6e4;
4
+ function toastMessage(text) {
5
+ return text.replace(/^API \| /, "");
6
+ }
7
+ function firstString(value, keys) {
8
+ for (const key of keys) {
9
+ if (typeof value?.[key] === "string" && value[key].trim()) return value[key].trim();
10
+ }
11
+ return "";
12
+ }
13
+ function providerContext(input) {
14
+ const info = input?.provider?.info || {};
15
+ const options = input?.provider?.options || {};
16
+ const providerName = String(input?.model?.providerID || info.id || "opencode");
17
+ const label = String(info.name || providerName);
18
+ return {
19
+ providerName,
20
+ provider: { name: label },
21
+ label,
22
+ baseUrl: process.env.PROVIDER_USAGE_BASE_URL || firstString(options, ["baseURL", "baseUrl", "base_url"]),
23
+ key: process.env.PROVIDER_USAGE_API_KEY || process.env.SUB2API_API_KEY || firstString(options, ["apiKey", "api_key"])
24
+ };
25
+ }
26
+ function refreshInterval(options) {
27
+ const configured = Number(options?.refreshMs || process.env.PROVIDER_USAGE_REFRESH_MS);
28
+ return Number.isFinite(configured) && configured >= 0 ? configured : DEFAULT_REFRESH_MS;
29
+ }
30
+ var AgentToolsUsage = async ({ client }, options = {}) => {
31
+ const contexts = /* @__PURE__ */ new Map();
32
+ const refreshedAt = /* @__PURE__ */ new Map();
33
+ const inFlight = /* @__PURE__ */ new Map();
34
+ const refreshMs = refreshInterval(options);
35
+ async function showUsage(text) {
36
+ if (!text) return;
37
+ try {
38
+ await client.tui.showToast({
39
+ body: {
40
+ title: "Provider usage",
41
+ message: toastMessage(text),
42
+ variant: "info",
43
+ duration: 8e3
44
+ }
45
+ });
46
+ } catch {
47
+ }
48
+ }
49
+ async function refreshSession(sessionID) {
50
+ const context = contexts.get(sessionID);
51
+ if (!context?.baseUrl || !context?.key) return;
52
+ const cacheKey = context.baseUrl.replace(/\/+$/, "");
53
+ const now = Date.now();
54
+ if (now - (refreshedAt.get(cacheKey) || 0) < refreshMs) return;
55
+ if (inFlight.has(cacheKey)) return await inFlight.get(cacheKey);
56
+ refreshedAt.set(cacheKey, now);
57
+ const task = queryProviderUsage(context, {
58
+ agent: "opencode",
59
+ rememberSnapshot: true
60
+ }).then((result) => showUsage(result?.text || "")).catch(() => {
61
+ }).finally(() => inFlight.delete(cacheKey));
62
+ inFlight.set(cacheKey, task);
63
+ return await task;
64
+ }
65
+ return {
66
+ "chat.params": async (input) => {
67
+ contexts.set(input.sessionID, providerContext(input));
68
+ },
69
+ event: async ({ event }) => {
70
+ if (event.type === "session.idle") {
71
+ void refreshSession(event.properties.sessionID);
72
+ } else if (event.type === "session.deleted") {
73
+ contexts.delete(event.properties.sessionID);
74
+ }
75
+ }
76
+ };
77
+ };
78
+ export {
79
+ AgentToolsUsage
80
+ };
@@ -0,0 +1,46 @@
1
+ // integrations/usage/opencode-tui.mjs
2
+ import { readFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ var AGENT_TOOLS_HOME = process.env.AGENT_TOOLS_HOME || join(homedir(), ".agent-tools");
6
+ var SNAPSHOT_PATH = join(AGENT_TOOLS_HOME, "cache", "usage-snapshot.json");
7
+ function toastMessage(text) {
8
+ return text.replace(/^API \| /, "");
9
+ }
10
+ function latestUsage() {
11
+ try {
12
+ const data = JSON.parse(readFileSync(SNAPSHOT_PATH, "utf8"));
13
+ return Object.values(data?.items || {}).filter((item) => typeof item?.text === "string" && item.text).sort((a, b) => String(b.updatedAt || "").localeCompare(String(a.updatedAt || "")))[0]?.text || "";
14
+ } catch {
15
+ return "";
16
+ }
17
+ }
18
+ var tui = async (api) => {
19
+ api.keymap.registerLayer({
20
+ commands: [
21
+ {
22
+ name: "agent-tools.usage",
23
+ title: "Provider usage",
24
+ category: "Agent Tools",
25
+ namespace: "palette",
26
+ slashName: "at-usage",
27
+ run() {
28
+ const message = latestUsage();
29
+ api.ui.toast({
30
+ title: "Provider usage",
31
+ message: message ? toastMessage(message) : "Provider usage is not available yet",
32
+ variant: message ? "info" : "warning",
33
+ duration: 8e3
34
+ });
35
+ }
36
+ }
37
+ ]
38
+ });
39
+ };
40
+ var opencode_tui_default = {
41
+ id: "agent-tools-usage",
42
+ tui
43
+ };
44
+ export {
45
+ opencode_tui_default as default
46
+ };
@@ -1,13 +1,13 @@
1
- // lib/vision/cli.mjs
1
+ // integrations/vision/lib/cli.mjs
2
2
  import fs4 from "node:fs";
3
3
  import path5 from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
 
6
- // lib/vision/inspect.mjs
6
+ // integrations/vision/lib/inspect.mjs
7
7
  import crypto3 from "node:crypto";
8
8
  import path4 from "node:path";
9
9
 
10
- // lib/vision/config.mjs
10
+ // integrations/vision/lib/config.mjs
11
11
  import fs from "node:fs";
12
12
  import os from "node:os";
13
13
  import path from "node:path";
@@ -871,7 +871,7 @@ var ParseErrorCode;
871
871
  ParseErrorCode2[ParseErrorCode2["InvalidCharacter"] = 16] = "InvalidCharacter";
872
872
  })(ParseErrorCode || (ParseErrorCode = {}));
873
873
 
874
- // lib/vision/errors.mjs
874
+ // integrations/vision/lib/errors.mjs
875
875
  var ERROR_CODES = Object.freeze({
876
876
  CONFIG: "config_error",
877
877
  INPUT: "input_error",
@@ -899,7 +899,7 @@ function toVisionError(err, fallbackCode = ERROR_CODES.PROVIDER_HTTP) {
899
899
  return new VisionError(fallbackCode, message, { cause: err });
900
900
  }
901
901
 
902
- // lib/vision/config.mjs
902
+ // integrations/vision/lib/config.mjs
903
903
  var PROVIDERS = Object.freeze(["openai-compatible", "anthropic-compatible"]);
904
904
  var CONFIG_DEFAULTS = Object.freeze({
905
905
  timeoutMs: 3e4,
@@ -1037,7 +1037,7 @@ function loadVisionConfig({ file, env = process.env } = {}) {
1037
1037
  };
1038
1038
  }
1039
1039
 
1040
- // lib/vision/image-source.mjs
1040
+ // integrations/vision/lib/image-source.mjs
1041
1041
  import fs2 from "node:fs";
1042
1042
  import os2 from "node:os";
1043
1043
  import path2 from "node:path";
@@ -1285,7 +1285,7 @@ async function loadImageSource(source, { maxImageBytes, timeoutMs, fetchImpl } =
1285
1285
  throw new VisionError(ERROR_CODES.INPUT, `Unsupported image_source.type: ${JSON.stringify(source.type)}`);
1286
1286
  }
1287
1287
 
1288
- // lib/vision/rate-limit.mjs
1288
+ // integrations/vision/lib/rate-limit.mjs
1289
1289
  import crypto from "node:crypto";
1290
1290
  import fs3 from "node:fs";
1291
1291
  import path3 from "node:path";
@@ -1442,7 +1442,7 @@ function createLimiter(config, now = Date.now, { stateFile = null } = {}) {
1442
1442
  return stateFile ? createSharedLimiter(config, now, stateFile) : createProcessLimiter(config, now);
1443
1443
  }
1444
1444
 
1445
- // lib/vision/redact.mjs
1445
+ // integrations/vision/lib/redact.mjs
1446
1446
  var MASK = "***";
1447
1447
  function redactSecrets(text, secrets) {
1448
1448
  if (typeof text !== "string" || text.length === 0) return text;
@@ -1454,7 +1454,7 @@ function redactSecrets(text, secrets) {
1454
1454
  return out;
1455
1455
  }
1456
1456
 
1457
- // lib/vision/providers/shared.mjs
1457
+ // integrations/vision/lib/providers/shared.mjs
1458
1458
  import crypto2 from "node:crypto";
1459
1459
  async function* encodeBase64(readable) {
1460
1460
  let carry = Buffer.alloc(0);
@@ -1667,7 +1667,7 @@ async function postJson({ url, headers, body, timeoutMs, fetchImpl, providerLabe
1667
1667
  }
1668
1668
  }
1669
1669
 
1670
- // lib/vision/providers/anthropic-compatible.mjs
1670
+ // integrations/vision/lib/providers/anthropic-compatible.mjs
1671
1671
  function messagesUrl(baseUrl) {
1672
1672
  return baseUrl.endsWith("/v1") ? `${baseUrl}/messages` : `${baseUrl}/v1/messages`;
1673
1673
  }
@@ -1718,7 +1718,7 @@ async function inspectWithAnthropicCompatible({ config, image, questions, fetchI
1718
1718
  return normalizeAnswers(text, questions);
1719
1719
  }
1720
1720
 
1721
- // lib/vision/providers/openai-compatible.mjs
1721
+ // integrations/vision/lib/providers/openai-compatible.mjs
1722
1722
  function replyText2(json) {
1723
1723
  const message = json?.choices?.[0]?.message;
1724
1724
  if (typeof message?.content === "string") return message.content;
@@ -1765,7 +1765,7 @@ async function inspectWithOpenAICompatible({ config, image, questions, fetchImpl
1765
1765
  return normalizeAnswers(text, questions);
1766
1766
  }
1767
1767
 
1768
- // lib/vision/inspect.mjs
1768
+ // integrations/vision/lib/inspect.mjs
1769
1769
  var PROVIDER_IMPL = {
1770
1770
  "openai-compatible": inspectWithOpenAICompatible,
1771
1771
  "anthropic-compatible": inspectWithAnthropicCompatible
@@ -1849,7 +1849,7 @@ function createVisionService({ config, fetchImpl, now, limiterStateFile } = {})
1849
1849
  return { config: resolved, inspect };
1850
1850
  }
1851
1851
 
1852
- // lib/vision/cli.mjs
1852
+ // integrations/vision/lib/cli.mjs
1853
1853
  function printHelp() {
1854
1854
  const lines = [];
1855
1855
  for (const line of fs4.readFileSync(fileURLToPath(import.meta.url), "utf8").split("\n")) {
@@ -30952,11 +30952,11 @@ var StdioServerTransport = class {
30952
30952
  }
30953
30953
  };
30954
30954
 
30955
- // lib/vision/inspect.mjs
30955
+ // integrations/vision/lib/inspect.mjs
30956
30956
  import crypto3 from "node:crypto";
30957
30957
  import path4 from "node:path";
30958
30958
 
30959
- // lib/vision/config.mjs
30959
+ // integrations/vision/lib/config.mjs
30960
30960
  import fs from "node:fs";
30961
30961
  import os from "node:os";
30962
30962
  import path from "node:path";
@@ -31820,7 +31820,7 @@ var ParseErrorCode;
31820
31820
  ParseErrorCode2[ParseErrorCode2["InvalidCharacter"] = 16] = "InvalidCharacter";
31821
31821
  })(ParseErrorCode || (ParseErrorCode = {}));
31822
31822
 
31823
- // lib/vision/errors.mjs
31823
+ // integrations/vision/lib/errors.mjs
31824
31824
  var ERROR_CODES = Object.freeze({
31825
31825
  CONFIG: "config_error",
31826
31826
  INPUT: "input_error",
@@ -31848,7 +31848,7 @@ function toVisionError(err, fallbackCode = ERROR_CODES.PROVIDER_HTTP) {
31848
31848
  return new VisionError(fallbackCode, message, { cause: err });
31849
31849
  }
31850
31850
 
31851
- // lib/vision/config.mjs
31851
+ // integrations/vision/lib/config.mjs
31852
31852
  var PROVIDERS = Object.freeze(["openai-compatible", "anthropic-compatible"]);
31853
31853
  var CONFIG_DEFAULTS = Object.freeze({
31854
31854
  timeoutMs: 3e4,
@@ -31986,7 +31986,7 @@ function loadVisionConfig({ file: file2, env = process.env } = {}) {
31986
31986
  };
31987
31987
  }
31988
31988
 
31989
- // lib/vision/image-source.mjs
31989
+ // integrations/vision/lib/image-source.mjs
31990
31990
  import fs2 from "node:fs";
31991
31991
  import os2 from "node:os";
31992
31992
  import path2 from "node:path";
@@ -32234,7 +32234,7 @@ async function loadImageSource(source, { maxImageBytes, timeoutMs, fetchImpl } =
32234
32234
  throw new VisionError(ERROR_CODES.INPUT, `Unsupported image_source.type: ${JSON.stringify(source.type)}`);
32235
32235
  }
32236
32236
 
32237
- // lib/vision/rate-limit.mjs
32237
+ // integrations/vision/lib/rate-limit.mjs
32238
32238
  import crypto from "node:crypto";
32239
32239
  import fs3 from "node:fs";
32240
32240
  import path3 from "node:path";
@@ -32391,7 +32391,7 @@ function createLimiter(config2, now = Date.now, { stateFile = null } = {}) {
32391
32391
  return stateFile ? createSharedLimiter(config2, now, stateFile) : createProcessLimiter(config2, now);
32392
32392
  }
32393
32393
 
32394
- // lib/vision/redact.mjs
32394
+ // integrations/vision/lib/redact.mjs
32395
32395
  var MASK = "***";
32396
32396
  function redactSecrets(text, secrets) {
32397
32397
  if (typeof text !== "string" || text.length === 0) return text;
@@ -32403,7 +32403,7 @@ function redactSecrets(text, secrets) {
32403
32403
  return out;
32404
32404
  }
32405
32405
 
32406
- // lib/vision/providers/shared.mjs
32406
+ // integrations/vision/lib/providers/shared.mjs
32407
32407
  import crypto2 from "node:crypto";
32408
32408
  async function* encodeBase64(readable) {
32409
32409
  let carry = Buffer.alloc(0);
@@ -32616,7 +32616,7 @@ async function postJson({ url: url2, headers, body, timeoutMs, fetchImpl, provid
32616
32616
  }
32617
32617
  }
32618
32618
 
32619
- // lib/vision/providers/anthropic-compatible.mjs
32619
+ // integrations/vision/lib/providers/anthropic-compatible.mjs
32620
32620
  function messagesUrl(baseUrl) {
32621
32621
  return baseUrl.endsWith("/v1") ? `${baseUrl}/messages` : `${baseUrl}/v1/messages`;
32622
32622
  }
@@ -32667,7 +32667,7 @@ async function inspectWithAnthropicCompatible({ config: config2, image, question
32667
32667
  return normalizeAnswers(text, questions);
32668
32668
  }
32669
32669
 
32670
- // lib/vision/providers/openai-compatible.mjs
32670
+ // integrations/vision/lib/providers/openai-compatible.mjs
32671
32671
  function replyText2(json2) {
32672
32672
  const message = json2?.choices?.[0]?.message;
32673
32673
  if (typeof message?.content === "string") return message.content;
@@ -32714,7 +32714,7 @@ async function inspectWithOpenAICompatible({ config: config2, image, questions,
32714
32714
  return normalizeAnswers(text, questions);
32715
32715
  }
32716
32716
 
32717
- // lib/vision/inspect.mjs
32717
+ // integrations/vision/lib/inspect.mjs
32718
32718
  var PROVIDER_IMPL = {
32719
32719
  "openai-compatible": inspectWithOpenAICompatible,
32720
32720
  "anthropic-compatible": inspectWithAnthropicCompatible
@@ -32798,7 +32798,7 @@ function createVisionService({ config: config2, fetchImpl, now, limiterStateFile
32798
32798
  return { config: resolved, inspect };
32799
32799
  }
32800
32800
 
32801
- // plugins/vision/mcp-server.mjs
32801
+ // integrations/vision/mcp-server.mjs
32802
32802
  var TOOL_DESCRIPTION = [
32803
32803
  "This is a callable MCP tool, not an MCP resource. Invoke it directly; never use list_mcp_resources or read_mcp_resource, and never treat inspect_image as a resource URI.",
32804
32804
  "Ask a vision model factual questions about one image (local file path or http(s) URL).",
@@ -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 (built artifact; see scripts/build.mjs).
25
+ const USAGE_RUNTIME = join(AGENT_TOOLS_HOME, "dist", "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;
@@ -60,7 +62,7 @@ async function readStdin() {
60
62
  function readJsonFile(file) {
61
63
  try {
62
64
  if (!fs.existsSync(file)) return {};
63
- const raw = stripJsonComments(fs.readFileSync(file, "utf8").replace(/^\uFEFF/, ""));
65
+ const raw = stripTrailingCommas(stripJsonComments(fs.readFileSync(file, "utf8").replace(/^\uFEFF/, "")));
64
66
  return raw.trim() ? JSON.parse(raw) : {};
65
67
  } catch {
66
68
  return {};
@@ -101,6 +103,35 @@ function stripJsonComments(input) {
101
103
  return out;
102
104
  }
103
105
 
106
+ // Matches jsonc-parser's allowTrailingComma so every config.jsonc reader in
107
+ // this package accepts the same syntax. Runs on comment-stripped input.
108
+ function stripTrailingCommas(input) {
109
+ let out = "";
110
+ let inString = false;
111
+ let escaped = false;
112
+ for (let i = 0; i < input.length; i++) {
113
+ const ch = input[i];
114
+ if (inString) {
115
+ out += ch;
116
+ escaped = ch === "\\" ? !escaped : false;
117
+ if (ch === "\"" && !escaped) inString = false;
118
+ continue;
119
+ }
120
+ if (ch === "\"") {
121
+ inString = true;
122
+ out += ch;
123
+ continue;
124
+ }
125
+ if (ch === ",") {
126
+ let j = i + 1;
127
+ while (j < input.length && /\s/.test(input[j])) j++;
128
+ if (input[j] === "}" || input[j] === "]") continue;
129
+ }
130
+ out += ch;
131
+ }
132
+ return out;
133
+ }
134
+
104
135
  function parseArgs(argv) {
105
136
  const opts = {};
106
137
  for (let i = 0; i < argv.length; i++) {
@@ -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);
@@ -236,6 +236,35 @@ function stripJsonComments(input) {
236
236
  return out;
237
237
  }
238
238
 
239
+ // Matches jsonc-parser's allowTrailingComma so every config.jsonc reader in
240
+ // this package accepts the same syntax. Runs on comment-stripped input.
241
+ function stripTrailingCommas(input) {
242
+ let out = "";
243
+ let inString = false;
244
+ let escaped = false;
245
+ for (let i = 0; i < input.length; i += 1) {
246
+ const ch = input[i];
247
+ if (inString) {
248
+ out += ch;
249
+ escaped = ch === "\\" ? !escaped : false;
250
+ if (ch === "\"" && !escaped) inString = false;
251
+ continue;
252
+ }
253
+ if (ch === "\"") {
254
+ inString = true;
255
+ out += ch;
256
+ continue;
257
+ }
258
+ if (ch === ",") {
259
+ let j = i + 1;
260
+ while (j < input.length && /\s/.test(input[j])) j += 1;
261
+ if (input[j] === "}" || input[j] === "]") continue;
262
+ }
263
+ out += ch;
264
+ }
265
+ return out;
266
+ }
267
+
239
268
  let agentConfigCache;
240
269
  async function agentConfig() {
241
270
  if (agentConfigCache) return agentConfigCache;
@@ -245,7 +274,7 @@ async function agentConfig() {
245
274
  agentConfigCache = {};
246
275
  return agentConfigCache;
247
276
  }
248
- const parsed = JSON.parse(stripJsonComments(raw.replace(/^\uFEFF/, "")));
277
+ const parsed = JSON.parse(stripTrailingCommas(stripJsonComments(raw.replace(/^\uFEFF/, ""))));
249
278
  agentConfigCache = parsed.providerUsage || {};
250
279
  } catch {
251
280
  agentConfigCache = {};
@@ -1171,6 +1200,10 @@ export async function queryProviderUsage(input, options = {}) {
1171
1200
  }
1172
1201
 
1173
1202
  async function refresh(agent = "codex") {
1203
+ return await queryAgentProviderUsage(agent);
1204
+ }
1205
+
1206
+ export async function queryAgentProviderUsage(agent = "codex") {
1174
1207
  return await queryUsageContext(await usageContext(agent), {
1175
1208
  agent,
1176
1209
  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.
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
- // Vision MCP stdio server. Thin shell over lib/vision: registers the
2
+ // Vision MCP stdio server. Thin shell over ./lib: registers the
3
3
  // inspect_image tool, translates results/errors, and nothing else. Launched by
4
- // hosts as `agent-tools mcp-vision` (or `node plugins/vision/mcp-server.mjs`).
4
+ // hosts as `agent-tools mcp-vision` (or `node integrations/vision/mcp-server.mjs`).
5
5
 
6
6
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
7
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
8
  import { z } from "zod";
9
- import { createVisionService, QUESTION_LIMITS } from "../../lib/vision/inspect.mjs";
10
- import { isVisionError } from "../../lib/vision/errors.mjs";
9
+ import { createVisionService, QUESTION_LIMITS } from "./lib/inspect.mjs";
10
+ import { isVisionError } from "./lib/errors.mjs";
11
11
 
12
12
  // Stable soft constraints live here: this text follows the tool into every
13
13
  // session, whether or not the at-vision skill is loaded.
package/package.json CHANGED
@@ -1,24 +1,20 @@
1
1
  {
2
2
  "name": "@kairyou/agent-tools",
3
- "version": "0.2.0",
4
- "description": "Skills and runtime integrations for Codex, Claude Code, and opencode.",
3
+ "version": "0.4.0",
4
+ "description": "Reusable Agent Skills and installable integrations (statusline, provider usage, vision) for Codex, Claude Code, and opencode.",
5
5
  "license": "MIT",
6
6
  "engines": {
7
7
  "node": ">=22"
8
8
  },
9
9
  "publishConfig": {
10
- "access": "public",
11
- "registry": "https://registry.npmjs.org/"
10
+ "access": "public"
12
11
  },
13
12
  "files": [
14
13
  "config.default.jsonc",
15
- "dist/vision/",
16
- "hooks/",
17
- "lib/",
18
- "plugins/",
14
+ "dist/",
15
+ "integrations/",
19
16
  "scripts/",
20
- "skills/",
21
- "statusline/"
17
+ "skills/"
22
18
  ],
23
19
  "dependencies": {
24
20
  "jsonc-parser": "3.3.1"
@@ -29,8 +25,8 @@
29
25
  "zod": "4.4.3"
30
26
  },
31
27
  "scripts": {
32
- "build:vision": "node scripts/build-vision.mjs",
33
- "prepare": "npm run build:vision",
28
+ "build": "node scripts/build.mjs",
29
+ "prepare": "npm run build",
34
30
  "test": "node --test tests/*.test.mjs",
35
31
  "release": "node scripts/release.mjs"
36
32
  },
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env node
2
+ // Bundles everything that ships to ~/.agent-tools into dist/<capability>.
3
+ // Installed artifacts are always built output; integrations/ holds the sources.
4
+
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import { build } from "esbuild";
9
+
10
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
11
+ const DIST = path.join(ROOT, "dist");
12
+
13
+ const TARGETS = {
14
+ statusline: {
15
+ entryPoints: {
16
+ "claude-statusline": path.join(ROOT, "integrations", "statusline", "claude-statusline.mjs"),
17
+ },
18
+ },
19
+ usage: {
20
+ entryPoints: {
21
+ core: path.join(ROOT, "integrations", "usage", "core.mjs"),
22
+ cli: path.join(ROOT, "integrations", "usage", "cli.mjs"),
23
+ "codex-hook": path.join(ROOT, "integrations", "usage", "codex-hook.mjs"),
24
+ "opencode-plugin": path.join(ROOT, "integrations", "usage", "opencode-plugin.mjs"),
25
+ "opencode-tui": path.join(ROOT, "integrations", "usage", "opencode-tui.mjs"),
26
+ },
27
+ // core.mjs detects "run as a script" via process.argv[1]; inlining it into
28
+ // the other entries would re-trigger that check inside their bundles, so it
29
+ // stays a sibling file that they import at runtime.
30
+ external: ["./core.mjs"],
31
+ },
32
+ vision: {
33
+ entryPoints: {
34
+ "mcp-server": path.join(ROOT, "integrations", "vision", "mcp-server.mjs"),
35
+ cli: path.join(ROOT, "integrations", "vision", "lib", "cli.mjs"),
36
+ },
37
+ },
38
+ };
39
+
40
+ for (const [name, { entryPoints, external = [] }] of Object.entries(TARGETS)) {
41
+ const outDir = path.join(DIST, name);
42
+ const stage = path.join(DIST, `.${name}-build-${process.pid}-${Date.now()}`);
43
+ try {
44
+ await build({
45
+ entryPoints,
46
+ outdir: stage,
47
+ bundle: true,
48
+ platform: "node",
49
+ format: "esm",
50
+ target: "node22",
51
+ mainFields: ["module", "main"],
52
+ outExtension: { ".js": ".mjs" },
53
+ packages: "bundle",
54
+ external,
55
+ sourcemap: false,
56
+ legalComments: "none",
57
+ });
58
+
59
+ fs.rmSync(outDir, { recursive: true, force: true });
60
+ fs.renameSync(stage, outDir);
61
+ console.log(`built ${path.relative(ROOT, outDir)}`);
62
+ } finally {
63
+ fs.rmSync(stage, { recursive: true, force: true });
64
+ }
65
+ }