@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.
- package/README.md +112 -115
- package/README.zh-CN.md +108 -115
- package/dist/statusline/claude-statusline.mjs +355 -0
- package/dist/usage/cli.mjs +25 -0
- package/dist/usage/codex-hook.mjs +144 -0
- package/dist/usage/core.mjs +1054 -0
- package/dist/usage/opencode-plugin.mjs +80 -0
- package/dist/usage/opencode-tui.mjs +46 -0
- package/dist/vision/cli.mjs +13 -13
- package/dist/vision/mcp-server.mjs +12 -12
- package/{statusline/claude/statusline.mjs → integrations/statusline/claude-statusline.mjs} +33 -2
- package/integrations/usage/cli.mjs +27 -0
- package/{hooks/codex/usage-hook.mjs → integrations/usage/codex-hook.mjs} +1 -1
- package/{lib/usage.mjs → integrations/usage/core.mjs} +34 -1
- package/{plugins/opencode/usage-plugin.mjs → integrations/usage/opencode-plugin.mjs} +1 -1
- package/integrations/usage/skills/at-usage/SKILL.md +16 -0
- package/{plugins → integrations}/vision/mcp-server.mjs +4 -4
- package/package.json +8 -12
- package/scripts/build.mjs +65 -0
- package/scripts/install.mjs +135 -90
- package/scripts/release.mjs +75 -5
- package/hooks/claude/.gitkeep +0 -1
- package/hooks/codex/.gitkeep +0 -1
- package/hooks/common/.gitkeep +0 -1
- package/hooks/opencode/.gitkeep +0 -1
- package/scripts/build-vision.mjs +0 -35
- package/statusline/.gitkeep +0 -1
- package/statusline/codex/.gitkeep +0 -1
- /package/{plugins/opencode/usage-tui.mjs → integrations/usage/opencode-tui.mjs} +0 -0
- /package/{lib/vision → integrations/vision/lib}/cli.mjs +0 -0
- /package/{lib/vision → integrations/vision/lib}/config.mjs +0 -0
- /package/{lib/vision → integrations/vision/lib}/errors.mjs +0 -0
- /package/{lib/vision → integrations/vision/lib}/image-source.mjs +0 -0
- /package/{lib/vision → integrations/vision/lib}/inspect.mjs +0 -0
- /package/{lib/vision → integrations/vision/lib}/providers/anthropic-compatible.mjs +0 -0
- /package/{lib/vision → integrations/vision/lib}/providers/openai-compatible.mjs +0 -0
- /package/{lib/vision → integrations/vision/lib}/providers/shared.mjs +0 -0
- /package/{lib/vision → integrations/vision/lib}/rate-limit.mjs +0 -0
- /package/{lib/vision → integrations/vision/lib}/redact.mjs +0 -0
- /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
|
+
};
|
package/dist/vision/cli.mjs
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
//
|
|
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
|
-
//
|
|
6
|
+
// integrations/vision/lib/inspect.mjs
|
|
7
7
|
import crypto3 from "node:crypto";
|
|
8
8
|
import path4 from "node:path";
|
|
9
9
|
|
|
10
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
30955
|
+
// integrations/vision/lib/inspect.mjs
|
|
30956
30956
|
import crypto3 from "node:crypto";
|
|
30957
30957
|
import path4 from "node:path";
|
|
30958
30958
|
|
|
30959
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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(
|
|
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",
|
|
@@ -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
|
|
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
|
|
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 "
|
|
10
|
-
import { isVisionError } from "
|
|
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.
|
|
4
|
-
"description": "Skills and
|
|
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/
|
|
16
|
-
"
|
|
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
|
|
33
|
-
"prepare": "npm run build
|
|
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
|
+
}
|