@kairyou/agent-tools 0.7.2 → 0.7.4

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 CHANGED
@@ -75,8 +75,8 @@ npx -y @kairyou/agent-tools@latest <capability> -a <agent...>
75
75
 
76
76
  `--dry-run` previews, `--uninstall` unwires the integration from the agent, and
77
77
  re-running the install command updates. The installer only touches config
78
- entries it wrote itself; shared files under `~/.agent-tools` are kept on
79
- uninstall.
78
+ entries it wrote itself, and `config.jsonc` updates only add missing default
79
+ keys without touching your edits or comments.
80
80
 
81
81
  | Capability | Claude Code | Codex | OpenCode |
82
82
  | --- | --- | --- | --- |
@@ -90,10 +90,10 @@ uninstall.
90
90
  npx -y @kairyou/agent-tools@latest statusline -a claude
91
91
  ```
92
92
 
93
- The installer writes `statusLine` to `~/.claude/settings.json`. The default
94
- output is:
93
+ The installer writes `statusLine` to `~/.claude/settings.json`. Example output:
95
94
 
96
95
  ```text
96
+ # Pick and order the fields via statusline.fields in ~/.agent-tools/config.jsonc:
97
97
  ⎇ main | Opus 4.8 | 5h 7% ⟳2h54m | w 41% ⟳3d1h
98
98
 
99
99
  # With a compatible API relay, its quota info is shown instead, e.g.:
@@ -104,10 +104,6 @@ Here `5h` and `w` are Claude's rolling usage windows and `⟳` is the reset
104
104
  countdown; see [Provider usage](#provider-usage) below for relay quota
105
105
  compatibility and configuration.
106
106
 
107
- To choose what appears, edit `statusline.fields` in
108
- `~/.agent-tools/config.jsonc`. Installer updates only add missing default keys
109
- and never touch your edits or comments.
110
-
111
107
  ### Provider usage
112
108
 
113
109
  For API relay / gateway setups: shows the relay's balance / quota inside the
package/README.zh-CN.md CHANGED
@@ -72,7 +72,8 @@ npx -y @kairyou/agent-tools@latest <capability> -a <agent...>
72
72
  ```
73
73
 
74
74
  `--dry-run` 预览, `--uninstall` 解除对应集成, 重新执行安装命令即为更新.
75
- 安装器只改动自己写入的配置项; 卸载会保留 `~/.agent-tools` 下的共享文件.
75
+ 安装器只改动自己写入的配置项, 更新 `config.jsonc` 时只补充缺失的默认键,
76
+ 不会动你的修改和注释.
76
77
 
77
78
  | Capability | Claude Code | Codex | OpenCode |
78
79
  | --- | --- | --- | --- |
@@ -86,9 +87,10 @@ npx -y @kairyou/agent-tools@latest <capability> -a <agent...>
86
87
  npx -y @kairyou/agent-tools@latest statusline -a claude
87
88
  ```
88
89
 
89
- 安装器会把 `statusLine` 写入 `~/.claude/settings.json`. 默认显示:
90
+ 安装器会把 `statusLine` 写入 `~/.claude/settings.json`. 输出示例:
90
91
 
91
92
  ```text
93
+ # 显示项与顺序由 ~/.agent-tools/config.jsonc 的 statusline.fields 控制:
92
94
  ⎇ main | Opus 4.8 | 5h 7% ⟳2h54m | w 41% ⟳3d1h
93
95
 
94
96
  # 使用兼容的 API 中转时, 显示中转的额度信息, 比如:
@@ -98,9 +100,6 @@ npx -y @kairyou/agent-tools@latest statusline -a claude
98
100
  其中 `5h` / `w` 是 Claude 的滚动用量窗口, `⟳` 后面是重置倒计时;
99
101
  中转额度信息的兼容性与配置见下方 [Provider usage](#provider-usage).
100
102
 
101
- 如需控制显示项, 修改 `~/.agent-tools/config.jsonc` 里的
102
- `statusline.fields`. 安装器更新时只会补充缺失的默认键, 不会动你的修改和注释.
103
-
104
103
  ### Provider usage
105
104
 
106
105
  面向使用 API 中转的场景: 在 agent 内直接显示中转网关的余额/额度, 按量付费或
File without changes
File without changes
File without changes
File without changes
File without changes
package/package.json CHANGED
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "name": "@kairyou/agent-tools",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
4
4
  "description": "Reusable Agent Skills and installable integrations (statusline, provider usage, vision) for Codex, Claude Code, and opencode.",
5
5
  "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/kairyou/agent-tools.git"
9
+ },
6
10
  "engines": {
7
11
  "node": ">=22"
8
12
  },
@@ -45,6 +45,7 @@
45
45
  // -h, --help Show this help.
46
46
 
47
47
  import { spawnSync } from "node:child_process";
48
+ import { createHash } from "node:crypto";
48
49
  import fs from "node:fs";
49
50
  import os from "node:os";
50
51
  import path from "node:path";
@@ -93,6 +94,7 @@ const USAGE_SKILL_NAME = "at-usage";
93
94
  const USAGE_SKILL_SRC = path.join(REPO_ROOT, "integrations", "usage", "skills", USAGE_SKILL_NAME);
94
95
  const VISION_BUNDLED_MCP_SERVER = path.join(REPO_ROOT, "dist", "vision", "mcp-server.mjs");
95
96
  const VISION_BUNDLED_CLI = path.join(REPO_ROOT, "dist", "vision", "cli.mjs");
97
+ const INSTALL_STATE_PATH = path.join(INSTALL_ROOT, "install-state.json");
96
98
 
97
99
  function fwd(p) {
98
100
  return p.replace(/\\/g, "/");
@@ -445,13 +447,12 @@ const VISION_RATE_LIMIT_STATE = path.join(INSTALL_ROOT, "cache", "vision-rate-li
445
447
  const VISION_DIST_DIR = path.join(REPO_ROOT, "dist", "vision");
446
448
  const VISION_RUNTIME_SERVER = path.join(VISION_RUNTIME_DIR, "mcp-server.mjs");
447
449
  const VISION_RUNTIME_CLI = path.join(VISION_RUNTIME_DIR, "cli.mjs");
448
- const SKILL_MARKER = ".agent-tools-managed.json";
449
- const VISION_SKILL_MARKER_DATA = Object.freeze({
450
+ const VISION_SKILL_IDENTITY = Object.freeze({
450
451
  owner: "@kairyou/agent-tools",
451
452
  capability: "vision",
452
453
  artifact: "skill",
453
454
  });
454
- const USAGE_SKILL_MARKER_DATA = Object.freeze({
455
+ const USAGE_SKILL_IDENTITY = Object.freeze({
455
456
  owner: "@kairyou/agent-tools",
456
457
  capability: "usage",
457
458
  artifact: "skill",
@@ -558,26 +559,110 @@ function installVisionRuntime(opts) {
558
559
  }
559
560
  }
560
561
 
561
- function isManagedSkillDir(dest, markerData) {
562
- const marker = path.join(dest, SKILL_MARKER);
563
- if (!fs.existsSync(marker)) return false;
562
+ function skillManifestKey(dest) {
563
+ const resolved = fwd(path.resolve(dest));
564
+ return process.platform === "win32" ? resolved.toLowerCase() : resolved;
565
+ }
566
+
567
+ function skillHash(skillFile) {
568
+ if (!fs.existsSync(skillFile)) return "";
569
+ return createHash("sha256").update(fs.readFileSync(skillFile)).digest("hex");
570
+ }
571
+
572
+ function matchesIdentity(value, identity) {
573
+ return Object.entries(identity).every(([key, expected]) => value?.[key] === expected);
574
+ }
575
+
576
+ function readInstallState() {
577
+ const value = readJson(INSTALL_STATE_PATH);
578
+ if (Object.keys(value).length === 0) return { version: 1, artifacts: {} };
579
+ if (
580
+ value.version !== 1 ||
581
+ !value.artifacts ||
582
+ typeof value.artifacts !== "object" ||
583
+ Array.isArray(value.artifacts)
584
+ ) {
585
+ throw new Error(`Cannot parse ${INSTALL_STATE_PATH}: unsupported install state schema`);
586
+ }
587
+ return value;
588
+ }
589
+
590
+ function writeInstallState(state) {
591
+ if (Object.keys(state.artifacts).length === 0) {
592
+ fs.rmSync(INSTALL_STATE_PATH, { force: true });
593
+ return;
594
+ }
595
+ const text = JSON.stringify(state, null, 2) + "\n";
596
+ const suffix = `${process.pid}-${Date.now()}`;
597
+ const stage = `${INSTALL_STATE_PATH}.stage-${suffix}`;
598
+ const backup = `${INSTALL_STATE_PATH}.backup-${suffix}`;
599
+ let movedCurrent = false;
600
+ fs.mkdirSync(path.dirname(INSTALL_STATE_PATH), { recursive: true });
564
601
  try {
565
- const value = JSON.parse(fs.readFileSync(marker, "utf8"));
566
- return Object.entries(markerData).every(([key, expected]) => value?.[key] === expected);
567
- } catch {
568
- return false;
602
+ fs.writeFileSync(stage, text);
603
+ if (fs.existsSync(INSTALL_STATE_PATH)) {
604
+ fs.renameSync(INSTALL_STATE_PATH, backup);
605
+ movedCurrent = true;
606
+ }
607
+ fs.renameSync(stage, INSTALL_STATE_PATH);
608
+ fs.rmSync(backup, { force: true });
609
+ } catch (error) {
610
+ fs.rmSync(stage, { force: true });
611
+ if (movedCurrent && !fs.existsSync(INSTALL_STATE_PATH) && fs.existsSync(backup)) {
612
+ fs.renameSync(backup, INSTALL_STATE_PATH);
613
+ }
614
+ throw error;
615
+ } finally {
616
+ fs.rmSync(stage, { force: true });
617
+ if (fs.existsSync(INSTALL_STATE_PATH)) fs.rmSync(backup, { force: true });
569
618
  }
619
+ console.log(` wrote ${INSTALL_STATE_PATH}`);
620
+ }
621
+
622
+ function managedSkillStatus(dest, identity) {
623
+ const entry = readInstallState().artifacts[skillManifestKey(dest)];
624
+ if (entry) {
625
+ if (!matchesIdentity(entry, identity)) return "other";
626
+ const currentHash = skillHash(path.join(dest, "SKILL.md"));
627
+ return currentHash && currentHash === entry.sha256 ? "managed" : "modified";
628
+ }
629
+ return "unmanaged";
630
+ }
631
+
632
+ function recordManagedSkill(dest, identity) {
633
+ const state = readInstallState();
634
+ state.artifacts[skillManifestKey(dest)] = {
635
+ path: fwd(path.resolve(dest)),
636
+ ...identity,
637
+ sha256: skillHash(path.join(dest, "SKILL.md")),
638
+ };
639
+ writeInstallState(state);
640
+ }
641
+
642
+ function forgetManagedSkill(dest) {
643
+ const state = readInstallState();
644
+ if (!state.artifacts[skillManifestKey(dest)]) return;
645
+ delete state.artifacts[skillManifestKey(dest)];
646
+ writeInstallState(state);
570
647
  }
571
648
 
572
649
  function installManagedSkillDir(
573
650
  skillsRoot,
574
- { name, source, markerData, replacements, remove, dryRun }
651
+ { name, source, identity, replacements, remove, dryRun }
575
652
  ) {
576
653
  const dest = path.join(skillsRoot, name);
577
- const isManaged = () => isManagedSkillDir(dest, markerData);
654
+ const status = fs.existsSync(dest) ? managedSkillStatus(dest, identity) : "missing";
578
655
  if (remove) {
579
- if (!fs.existsSync(dest)) return;
580
- if (!isManaged()) {
656
+ if (!fs.existsSync(dest)) {
657
+ if (!dryRun) forgetManagedSkill(dest);
658
+ return;
659
+ }
660
+ if (status === "modified") {
661
+ if (!dryRun) forgetManagedSkill(dest);
662
+ console.log(` kept modified skill and released ownership ${dest}`);
663
+ return;
664
+ }
665
+ if (status !== "managed") {
581
666
  console.log(` kept unmanaged ${dest}`);
582
667
  return;
583
668
  }
@@ -586,10 +671,18 @@ function installManagedSkillDir(
586
671
  return;
587
672
  }
588
673
  fs.rmSync(dest, { recursive: true, force: true });
674
+ forgetManagedSkill(dest);
589
675
  console.log(` removed ${dest}`);
590
676
  return;
591
677
  }
592
- if (fs.existsSync(dest) && !isManaged()) {
678
+ if (status === "modified") {
679
+ console.error(
680
+ ` Refusing to overwrite modified managed skill directory ${dest}. ` +
681
+ `Move or restore it, then re-run the install.`
682
+ );
683
+ process.exit(1);
684
+ }
685
+ if (fs.existsSync(dest) && status !== "managed") {
593
686
  console.error(
594
687
  ` Refusing to overwrite existing unowned skill directory ${dest}. ` +
595
688
  `Move or remove it, then re-run the install.`
@@ -612,10 +705,7 @@ function installManagedSkillDir(
612
705
  skill = skill.replaceAll(token, value);
613
706
  }
614
707
  fs.writeFileSync(skillFile, skill);
615
- fs.writeFileSync(
616
- path.join(dest, SKILL_MARKER),
617
- JSON.stringify(markerData, null, 2) + "\n"
618
- );
708
+ recordManagedSkill(dest, identity);
619
709
  console.log(` wrote ${dest}`);
620
710
  }
621
711
 
@@ -623,7 +713,7 @@ function installVisionSkillDir(skillsRoot, { remove, dryRun }) {
623
713
  installManagedSkillDir(skillsRoot, {
624
714
  name: VISION_SKILL_NAME,
625
715
  source: VISION_SKILL_SRC,
626
- markerData: VISION_SKILL_MARKER_DATA,
716
+ identity: VISION_SKILL_IDENTITY,
627
717
  replacements: { "{{VISION_CLI_PATH}}": fwd(VISION_RUNTIME_CLI) },
628
718
  remove,
629
719
  dryRun,
@@ -634,7 +724,7 @@ function installUsageSkillDir(skillsRoot, agent, { remove, dryRun }) {
634
724
  installManagedSkillDir(skillsRoot, {
635
725
  name: USAGE_SKILL_NAME,
636
726
  source: USAGE_SKILL_SRC,
637
- markerData: USAGE_SKILL_MARKER_DATA,
727
+ identity: USAGE_SKILL_IDENTITY,
638
728
  replacements: {
639
729
  "{{USAGE_CLI_PATH}}": fwd(RUNTIME.usageCli),
640
730
  "{{USAGE_AGENT}}": agent,
@@ -3,52 +3,20 @@
3
3
  //
4
4
  // Usage:
5
5
  // npm run release [-- minor|major] # default: patch
6
- // npm run release -- --dry-run # npm test + preview tarball, no bump/publish
6
+ // npm run release -- --dry-run # npm test + preview tarball, no bump/push
7
7
  //
8
- // Steps: clean-tree check -> npm auth check -> npm test -> npm version -> npm publish -> git push
8
+ // Local steps: clean-tree check -> npm test -> npm version -> git push --follow-tags.
9
+ // Publishing runs on GitHub Actions via npm trusted publishing (OIDC, no
10
+ // tokens, no npm login): .github/workflows/release.yml publishes when the
11
+ // version tag lands.
9
12
  //
10
13
  // Release when scripts/ integrations/ config.default.jsonc or
11
14
  // package.json deps change — npx users only get these from the published package.
12
15
  // (skills/ ships from GitHub via `npx skills add`; no release needed.)
13
16
 
14
17
  import { spawnSync } from "node:child_process";
15
- import { readFileSync } from "node:fs";
16
18
 
17
19
  const BUMPS = ["patch", "minor", "major"];
18
- const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
19
- const PACKAGE_NAME = packageJson.name;
20
-
21
- if (typeof PACKAGE_NAME !== "string" || PACKAGE_NAME.trim() === "") {
22
- console.error("package.json must define a non-empty name before releasing.");
23
- process.exit(2);
24
- }
25
-
26
- function readNpmConfig(key) {
27
- const npmExecPath = process.env.npm_execpath;
28
- const result = npmExecPath
29
- ? spawnSync(process.execPath, [npmExecPath, "config", "get", key], { encoding: "utf8" })
30
- : spawnSync("npm", ["config", "get", key], { encoding: "utf8", shell: true });
31
-
32
- if (result.status !== 0) {
33
- const detail = result.stderr?.trim() || result.error?.message || "npm config exited unexpectedly";
34
- console.error(`Unable to read npm config ${key}:\n${detail}`);
35
- process.exit(result.status ?? 2);
36
- }
37
-
38
- const value = result.stdout?.trim();
39
- return value && value !== "undefined" && value !== "null" ? value : undefined;
40
- }
41
-
42
- const packageScope = PACKAGE_NAME.match(/^(@[^/]+)\//)?.[1];
43
- const REGISTRY = (packageScope && readNpmConfig(`${packageScope}:registry`)) || readNpmConfig("registry");
44
-
45
- try {
46
- const registryUrl = new URL(REGISTRY);
47
- if (!["http:", "https:"].includes(registryUrl.protocol)) throw new Error("unsupported protocol");
48
- } catch {
49
- console.error("npm registry config must be a valid HTTP(S) URL before releasing.");
50
- process.exit(2);
51
- }
52
20
 
53
21
  let bump = "patch";
54
22
  let dryRun = false;
@@ -61,66 +29,33 @@ for (const arg of process.argv.slice(2)) {
61
29
  }
62
30
  }
63
31
 
64
- function run(command, opts = {}) {
65
- const { onFailure, ...spawnOptions } = opts;
32
+ function run(command) {
66
33
  console.log(`\n> ${command}`);
67
- const result = spawnSync(command, { stdio: "inherit", shell: true, ...spawnOptions });
34
+ const result = spawnSync(command, { stdio: "inherit", shell: true });
68
35
  if (result.status !== 0) {
69
36
  console.error(`\nAborted: \`${command}\` exited with ${result.status}.`);
70
- onFailure?.();
71
37
  process.exit(result.status ?? 1);
72
38
  }
73
39
  }
74
40
 
75
- function printCommands(message, commands) {
76
- console.error(`\n${message}\n`);
77
- for (const command of commands) console.error(` ${command}`);
78
- }
79
-
80
41
  const status = spawnSync("git status --porcelain", { shell: true, encoding: "utf8" });
81
42
  if (!dryRun && status.stdout.trim() !== "") {
82
43
  console.error("Working tree is not clean — commit or stash first:\n" + status.stdout);
83
44
  process.exit(1);
84
45
  }
85
46
 
86
- if (!dryRun) {
87
- // Fail before npm version creates a commit and tag when the saved token is stale.
88
- run(`npm whoami --registry=${REGISTRY}`, {
89
- onFailure() {
90
- printCommands("npm authentication is missing or expired. Log in, verify the account, then retry:", [
91
- `npm login --registry=${REGISTRY}`,
92
- `npm whoami --registry=${REGISTRY}`,
93
- `npm run release -- ${bump}`,
94
- ]);
95
- },
96
- });
97
- }
98
-
99
47
  run("npm test");
100
48
 
101
49
  if (dryRun) {
102
50
  // Shows exactly which files would ship; verify nothing is missing/extra.
103
51
  run("npm pack --dry-run");
104
- console.log("\nDry run complete. No version bump, nothing published.");
52
+ console.log("\nDry run complete. No version bump, nothing pushed.");
105
53
  process.exit(0);
106
54
  }
107
55
 
108
56
  run(`npm version ${bump}`);
109
- run("npm publish", {
110
- onFailure() {
111
- printCommands(
112
- "npm publish failed after the version commit and tag were created. Do not run the release command again. Fix authentication/permissions, then resume:",
113
- [
114
- `npm login --registry=${REGISTRY}`,
115
- `npm whoami --registry=${REGISTRY}`,
116
- `npm owner ls ${PACKAGE_NAME}`,
117
- "npm publish",
118
- `npm view ${PACKAGE_NAME} version`,
119
- "git push --follow-tags",
120
- ],
121
- );
122
- },
123
- });
124
57
  run("git push --follow-tags");
125
58
 
126
- console.log("\nRelease complete.");
59
+ console.log(
60
+ "\nTag pushed. GitHub Actions publishes it: https://github.com/kairyou/agent-tools/actions"
61
+ );