@kairyou/agent-tools 0.3.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.
@@ -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
+ };
@@ -21,8 +21,8 @@ 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.
25
- const USAGE_RUNTIME = join(AGENT_TOOLS_HOME, "integrations", "usage", "core.mjs");
24
+ // usage integration's query engine (built artifact; see scripts/build.mjs).
25
+ const USAGE_RUNTIME = join(AGENT_TOOLS_HOME, "dist", "usage", "core.mjs");
26
26
  const DEFAULT_SNAPSHOT_TTL_MS = 60_000;
27
27
  const DEFAULT_REFRESH_COOLDOWN_MS = 30_000;
28
28
  const DEFAULT_FAILURE_BACKOFF_MS = 120_000;
@@ -62,7 +62,7 @@ async function readStdin() {
62
62
  function readJsonFile(file) {
63
63
  try {
64
64
  if (!fs.existsSync(file)) return {};
65
- const raw = stripJsonComments(fs.readFileSync(file, "utf8").replace(/^\uFEFF/, ""));
65
+ const raw = stripTrailingCommas(stripJsonComments(fs.readFileSync(file, "utf8").replace(/^\uFEFF/, "")));
66
66
  return raw.trim() ? JSON.parse(raw) : {};
67
67
  } catch {
68
68
  return {};
@@ -103,6 +103,35 @@ function stripJsonComments(input) {
103
103
  return out;
104
104
  }
105
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
+
106
135
  function parseArgs(argv) {
107
136
  const opts = {};
108
137
  for (let i = 0; i < argv.length; i++) {
@@ -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 = {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kairyou/agent-tools",
3
- "version": "0.3.0",
4
- "description": "Reusable Agent Skills, plus 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"
@@ -11,7 +11,7 @@
11
11
  },
12
12
  "files": [
13
13
  "config.default.jsonc",
14
- "dist/vision/",
14
+ "dist/",
15
15
  "integrations/",
16
16
  "scripts/",
17
17
  "skills/"
@@ -25,8 +25,8 @@
25
25
  "zod": "4.4.3"
26
26
  },
27
27
  "scripts": {
28
- "build:vision": "node scripts/build-vision.mjs",
29
- "prepare": "npm run build:vision",
28
+ "build": "node scripts/build.mjs",
29
+ "prepare": "npm run build",
30
30
  "test": "node --test tests/*.test.mjs",
31
31
  "release": "node scripts/release.mjs"
32
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
+ }
@@ -56,23 +56,25 @@ const INSTALL_ROOT =
56
56
  process.env.AGENT_TOOLS_HOME || path.join(os.homedir(), ".agent-tools");
57
57
  const META_KEY = "_agentTools";
58
58
  const META_VERSION = 1;
59
+ // Everything copied into ~/.agent-tools is built output from dist/ (see
60
+ // scripts/build.mjs); integrations/ holds the sources.
59
61
  const SOURCE = {
60
- codexUsageHook: path.join(REPO_ROOT, "integrations", "usage", "codex-hook.mjs"),
61
- usageScript: path.join(REPO_ROOT, "integrations", "usage", "core.mjs"),
62
- usageCli: path.join(REPO_ROOT, "integrations", "usage", "cli.mjs"),
62
+ codexUsageHook: path.join(REPO_ROOT, "dist", "usage", "codex-hook.mjs"),
63
+ usageScript: path.join(REPO_ROOT, "dist", "usage", "core.mjs"),
64
+ usageCli: path.join(REPO_ROOT, "dist", "usage", "cli.mjs"),
63
65
  config: path.join(REPO_ROOT, "config.default.jsonc"),
64
- claudeStatusline: path.join(REPO_ROOT, "integrations", "statusline", "claude-statusline.mjs"),
65
- opencodeUsagePlugin: path.join(REPO_ROOT, "integrations", "usage", "opencode-plugin.mjs"),
66
- opencodeUsageTui: path.join(REPO_ROOT, "integrations", "usage", "opencode-tui.mjs"),
66
+ claudeStatusline: path.join(REPO_ROOT, "dist", "statusline", "claude-statusline.mjs"),
67
+ opencodeUsagePlugin: path.join(REPO_ROOT, "dist", "usage", "opencode-plugin.mjs"),
68
+ opencodeUsageTui: path.join(REPO_ROOT, "dist", "usage", "opencode-tui.mjs"),
67
69
  };
68
70
  const RUNTIME = {
69
- codexUsageHook: path.join(INSTALL_ROOT, "integrations", "usage", "codex-hook.mjs"),
70
- usageScript: path.join(INSTALL_ROOT, "integrations", "usage", "core.mjs"),
71
- usageCli: path.join(INSTALL_ROOT, "integrations", "usage", "cli.mjs"),
71
+ codexUsageHook: path.join(INSTALL_ROOT, "dist", "usage", "codex-hook.mjs"),
72
+ usageScript: path.join(INSTALL_ROOT, "dist", "usage", "core.mjs"),
73
+ usageCli: path.join(INSTALL_ROOT, "dist", "usage", "cli.mjs"),
72
74
  config: path.join(INSTALL_ROOT, "config.jsonc"),
73
- claudeStatusline: path.join(INSTALL_ROOT, "integrations", "statusline", "claude-statusline.mjs"),
74
- opencodeUsagePlugin: path.join(INSTALL_ROOT, "integrations", "usage", "opencode-plugin.mjs"),
75
- opencodeUsageTui: path.join(INSTALL_ROOT, "integrations", "usage", "opencode-tui.mjs"),
75
+ claudeStatusline: path.join(INSTALL_ROOT, "dist", "statusline", "claude-statusline.mjs"),
76
+ opencodeUsagePlugin: path.join(INSTALL_ROOT, "dist", "usage", "opencode-plugin.mjs"),
77
+ opencodeUsageTui: path.join(INSTALL_ROOT, "dist", "usage", "opencode-tui.mjs"),
76
78
  };
77
79
  const ALL_CAPS = ["statusline", "usage", "vision"];
78
80
  const ALL_AGENTS = ["claude", "codex", "opencode"];
@@ -137,7 +139,11 @@ function stripJsonComments(input) {
137
139
  function readJsonc(file) {
138
140
  if (!fs.existsSync(file)) return {};
139
141
  const raw = fs.readFileSync(file, "utf8").replace(/^\uFEFF/, "");
140
- return raw.trim() ? JSON.parse(stripJsonComments(raw)) : {};
142
+ if (!raw.trim()) return {};
143
+ const errors = [];
144
+ const parsed = parseJsonc(raw, errors, { allowTrailingComma: true });
145
+ if (errors.length > 0) throw new Error(`Cannot parse ${file} as JSONC`);
146
+ return parsed ?? {};
141
147
  }
142
148
 
143
149
  function headerComments(text) {
@@ -180,7 +186,7 @@ function isOpenCodeUsageTuiEntry(entry) {
180
186
  const spec = Array.isArray(entry) ? entry[0] : entry;
181
187
  return (
182
188
  typeof spec === "string" &&
183
- /\/integrations\/usage\/opencode-tui\.mjs$/i.test(spec.replace(/\\/g, "/"))
189
+ /\/dist\/usage\/opencode-tui\.mjs$/i.test(spec.replace(/\\/g, "/"))
184
190
  );
185
191
  }
186
192
 
@@ -265,6 +271,11 @@ function installRuntimeAssets(opts) {
265
271
  addFile(SOURCE.config, RUNTIME.config, { mergeJsonc: true });
266
272
  }
267
273
  if (files.length === 0) return;
274
+ for (const [src] of files) {
275
+ if (!fs.existsSync(src)) {
276
+ throw new Error(`Missing built artifact ${src}. Run npm run build.`);
277
+ }
278
+ }
268
279
  console.log(`runtime: ${INSTALL_ROOT}`);
269
280
  for (const [src, dest, options] of files) copyRuntimeFile(src, dest, opts.dryRun, options);
270
281
  }
@@ -514,7 +525,7 @@ function installVisionRuntime(opts) {
514
525
  }
515
526
  for (const name of ["mcp-server.mjs", "cli.mjs"]) {
516
527
  if (!fs.existsSync(path.join(VISION_DIST_DIR, name))) {
517
- throw new Error(`Missing bundled vision runtime ${path.join(VISION_DIST_DIR, name)}. Run npm run build:vision.`);
528
+ throw new Error(`Missing bundled vision runtime ${path.join(VISION_DIST_DIR, name)}. Run npm run build.`);
518
529
  }
519
530
  }
520
531
 
@@ -1,35 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import fs from "node:fs";
4
- import path from "node:path";
5
- import { fileURLToPath } from "node:url";
6
- import { build } from "esbuild";
7
-
8
- const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
9
- const OUT_DIR = path.join(ROOT, "dist", "vision");
10
- const STAGE_DIR = path.join(ROOT, "dist", `.vision-build-${process.pid}-${Date.now()}`);
11
-
12
- try {
13
- await build({
14
- entryPoints: {
15
- "mcp-server": path.join(ROOT, "integrations", "vision", "mcp-server.mjs"),
16
- cli: path.join(ROOT, "integrations", "vision", "lib", "cli.mjs"),
17
- },
18
- outdir: STAGE_DIR,
19
- bundle: true,
20
- platform: "node",
21
- format: "esm",
22
- target: "node22",
23
- mainFields: ["module", "main"],
24
- outExtension: { ".js": ".mjs" },
25
- packages: "bundle",
26
- sourcemap: false,
27
- legalComments: "none",
28
- });
29
-
30
- fs.rmSync(OUT_DIR, { recursive: true, force: true });
31
- fs.renameSync(STAGE_DIR, OUT_DIR);
32
- console.log(`built ${path.relative(ROOT, OUT_DIR)}`);
33
- } finally {
34
- fs.rmSync(STAGE_DIR, { recursive: true, force: true });
35
- }