agent-skill-installer 0.1.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 ADDED
@@ -0,0 +1,47 @@
1
+ # codex-workbench
2
+
3
+ 用于迭代维护个人 Codex 能力资产的工作仓库。
4
+
5
+ ## 目录结构
6
+
7
+ ```text
8
+ .
9
+ ├── skills/ # 各个 skill 的主目录(一项能力一个子目录)
10
+ ├── scripts/ # 跨 skill 的通用脚本
11
+ ├── templates/ # 可复用模板(提交信息、报告、提示词等)
12
+ └── docs/ # 设计说明与维护文档
13
+ ```
14
+
15
+ ## 当前 Skills
16
+
17
+ - `codex-session-daily-report`
18
+ - `git-commit-helper`
19
+
20
+ ## npm CLI(npx 安装 skills)
21
+
22
+ 本仓库已提供可执行 CLI,包名/命令名为 `agent-skill-installer`。
23
+
24
+ ```bash
25
+ # 查看可安装 skills
26
+ npx agent-skill-installer list
27
+
28
+ # 安装一个 skill(默认安装到 ~/.codex/skills)
29
+ npx agent-skill-installer install git-commit-helper
30
+
31
+ # 一次安装多个 skills
32
+ npx agent-skill-installer install git-commit-helper codex-session-daily-report
33
+
34
+ # 安装到自定义目录
35
+ npx agent-skill-installer install git-commit-helper --dest ~/.codex/skills
36
+
37
+ # 覆盖已存在 skill
38
+ npx agent-skill-installer install git-commit-helper --force
39
+ ```
40
+
41
+ 发布到 npm 后,其他人即可直接使用上述 `npx` 命令。
42
+
43
+ ## 约定
44
+
45
+ 1. 单一 skill 放在 `skills/<skill-name>/`。
46
+ 2. 每个 skill 至少包含 `SKILL.md`。
47
+ 3. 优先小步提交,每次只做一个明确意图的变更。
package/bin/cli.js ADDED
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("node:fs/promises");
4
+ const path = require("node:path");
5
+ const os = require("node:os");
6
+
7
+ const REPO_ROOT = path.resolve(__dirname, "..");
8
+ const SKILLS_SOURCE_DIR = path.join(REPO_ROOT, "skills");
9
+ const DEFAULT_DEST = path.join(os.homedir(), ".codex", "skills");
10
+
11
+ function printHelp() {
12
+ console.log(`agent-skill-installer
13
+
14
+ Usage:
15
+ npx agent-skill-installer list
16
+ npx agent-skill-installer install <skill...> [--dest <path>] [--force]
17
+
18
+ Commands:
19
+ list List installable skills in this package.
20
+ install <skill...> Install one or more skills to ~/.codex/skills by default.
21
+
22
+ Options:
23
+ --dest <path> Custom destination directory.
24
+ --force Overwrite destination if a skill already exists.
25
+ -h, --help Show this help.
26
+ `);
27
+ }
28
+
29
+ async function dirExists(targetPath) {
30
+ try {
31
+ const stat = await fs.stat(targetPath);
32
+ return stat.isDirectory();
33
+ } catch {
34
+ return false;
35
+ }
36
+ }
37
+
38
+ async function getAvailableSkills() {
39
+ const entries = await fs.readdir(SKILLS_SOURCE_DIR, { withFileTypes: true });
40
+ const skills = [];
41
+
42
+ for (const entry of entries) {
43
+ if (!entry.isDirectory()) {
44
+ continue;
45
+ }
46
+
47
+ const skillDir = path.join(SKILLS_SOURCE_DIR, entry.name);
48
+ const skillFile = path.join(skillDir, "SKILL.md");
49
+
50
+ try {
51
+ await fs.access(skillFile);
52
+ skills.push(entry.name);
53
+ } catch {
54
+ // Skip folders that are not valid skills.
55
+ }
56
+ }
57
+
58
+ return skills.sort();
59
+ }
60
+
61
+ function parseInstallArgs(args) {
62
+ const parsed = {
63
+ names: [],
64
+ dest: DEFAULT_DEST,
65
+ force: false
66
+ };
67
+
68
+ for (let i = 0; i < args.length; i += 1) {
69
+ const arg = args[i];
70
+
71
+ if (arg === "--dest") {
72
+ const value = args[i + 1];
73
+ if (!value || value.startsWith("-")) {
74
+ throw new Error("Missing value for --dest");
75
+ }
76
+ parsed.dest = path.resolve(value);
77
+ i += 1;
78
+ continue;
79
+ }
80
+
81
+ if (arg === "--force") {
82
+ parsed.force = true;
83
+ continue;
84
+ }
85
+
86
+ if (arg === "-h" || arg === "--help") {
87
+ parsed.help = true;
88
+ continue;
89
+ }
90
+
91
+ if (arg.startsWith("-")) {
92
+ throw new Error(`Unknown option: ${arg}`);
93
+ }
94
+
95
+ parsed.names.push(arg);
96
+ }
97
+
98
+ return parsed;
99
+ }
100
+
101
+ async function listSkills() {
102
+ const skills = await getAvailableSkills();
103
+
104
+ if (skills.length === 0) {
105
+ console.log("No installable skills found in this package.");
106
+ return;
107
+ }
108
+
109
+ console.log("Installable skills:");
110
+ for (const skill of skills) {
111
+ console.log(`- ${skill}`);
112
+ }
113
+ }
114
+
115
+ async function installSkills(rawArgs) {
116
+ const parsed = parseInstallArgs(rawArgs);
117
+ if (parsed.help) {
118
+ printHelp();
119
+ return;
120
+ }
121
+
122
+ if (parsed.names.length === 0) {
123
+ throw new Error("Please provide at least one skill name to install.");
124
+ }
125
+
126
+ const available = new Set(await getAvailableSkills());
127
+ for (const name of parsed.names) {
128
+ if (!available.has(name)) {
129
+ throw new Error(`Skill "${name}" not found. Run "list" to view available skills.`);
130
+ }
131
+ }
132
+
133
+ await fs.mkdir(parsed.dest, { recursive: true });
134
+
135
+ for (const name of parsed.names) {
136
+ const from = path.join(SKILLS_SOURCE_DIR, name);
137
+ const to = path.join(parsed.dest, name);
138
+ const exists = await dirExists(to);
139
+
140
+ if (exists && !parsed.force) {
141
+ throw new Error(
142
+ `Destination already exists for "${name}": ${to} (use --force to overwrite)`
143
+ );
144
+ }
145
+
146
+ if (exists && parsed.force) {
147
+ await fs.rm(to, { recursive: true, force: true });
148
+ }
149
+
150
+ await fs.cp(from, to, { recursive: true });
151
+ console.log(`Installed "${name}" -> ${to}`);
152
+ }
153
+
154
+ console.log("Done. Restart Codex to pick up new skills.");
155
+ }
156
+
157
+ async function main() {
158
+ const args = process.argv.slice(2);
159
+ const command = args[0];
160
+
161
+ if (!command || command === "-h" || command === "--help") {
162
+ printHelp();
163
+ return;
164
+ }
165
+
166
+ if (command === "list") {
167
+ await listSkills();
168
+ return;
169
+ }
170
+
171
+ if (command === "install") {
172
+ await installSkills(args.slice(1));
173
+ return;
174
+ }
175
+
176
+ throw new Error(`Unknown command: ${command}`);
177
+ }
178
+
179
+ main().catch((error) => {
180
+ console.error(`Error: ${error.message}`);
181
+ process.exitCode = 1;
182
+ });
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "agent-skill-installer",
3
+ "version": "0.1.0",
4
+ "description": "Install one or more Codex skills from this package via npx.",
5
+ "bin": {
6
+ "agent-skill-installer": "bin/cli.js"
7
+ },
8
+ "files": [
9
+ "bin",
10
+ "skills",
11
+ "README.md"
12
+ ],
13
+ "scripts": {
14
+ "check": "node bin/cli.js list"
15
+ },
16
+ "keywords": [
17
+ "codex",
18
+ "skills",
19
+ "cli"
20
+ ],
21
+ "license": "MIT",
22
+ "engines": {
23
+ "node": ">=18.0.0"
24
+ }
25
+ }
@@ -0,0 +1,53 @@
1
+ ---
2
+ name: codex-session-daily-report
3
+ description: Read Codex session files for a target date, summarize completed work from assistant final answers, and produce a concise daily report. Use when the user asks to summarize today's Codex activity, generate a daily report from `~/.codex/sessions`, or review completed outcomes across sessions.
4
+ ---
5
+
6
+ # Codex Session Daily Report
7
+
8
+ Produce a daily report from local Codex sessions.
9
+
10
+ ## Workflow
11
+
12
+ 1. Resolve target date.
13
+ - If user says `today`, use an absolute date (e.g. `2026-03-02`).
14
+
15
+ 2. Read session files for that date.
16
+
17
+ ```bash
18
+ ls -1 ~/.codex/sessions/2026/03/02/*.jsonl
19
+ ```
20
+
21
+ 3. Extract completed assistant outputs only (`final_answer`).
22
+
23
+ ```bash
24
+ for f in ~/.codex/sessions/2026/03/02/*.jsonl; do
25
+ jq -r 'select(.type=="response_item" and .payload.type=="message" and .payload.role=="assistant" and .payload.phase=="final_answer")
26
+ | .timestamp + "\t" + ((.payload.content // [])
27
+ | map(select(.type=="output_text") | .text)
28
+ | join(" ") | gsub("[\n\r\t]+";" ") | gsub(" +";" "))' "$f"
29
+ done
30
+ ```
31
+
32
+ 4. Summarize by主题 and result.
33
+ - Merge similar tasks into one topic line.
34
+ - Keep key evidence: file paths, tests, and commit hashes if present.
35
+ - Keep neutral language and avoid guessing.
36
+
37
+ 5. Output daily report only (not logs).
38
+
39
+ ## Output Format
40
+
41
+ Use this structure:
42
+
43
+ 1. 日期:`YYYY-MM-DD`
44
+ 2. 今日业务事项:按主题列 2-6 条
45
+ 3. 关键产出:代码改动/测试结论/提交哈希
46
+ 4. 风险与待办:未完成项、潜在误判点
47
+
48
+ ## Constraints
49
+
50
+ - Prefer `assistant final_answer` over commentary.
51
+ - Do not output raw JSONL unless user explicitly asks.
52
+ - Keep report concise and factual.
53
+ - If no completed entries are found, state that clearly.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Codex Session Daily Report"
3
+ short_description: "Generate daily reports from Codex session history."
4
+ default_prompt: "Use $codex-session-daily-report to read today's Codex sessions and output a concise daily report."
@@ -0,0 +1,50 @@
1
+ ---
2
+ name: git-commit-helper
3
+ description: "严格生成与校验中文 Git 提交信息,固定格式为 type(scope): summary,type 仅允许 feat/fix/refactor/perf/style/test/docs/chore,summary 必须用祈使句、不超过 50 字符、不得以句号结尾且不得包含 and/&/multiple changes。使用该技能于编写提交说明、检查提交文本是否合规,或执行使用 codex (codex-ice@gmail.com) 身份的提交时。"
4
+ ---
5
+
6
+ # Git Commit Helper
7
+
8
+ ## 概览
9
+
10
+ 按固定规范生成中文提交信息,并在提交前进行格式校验。
11
+ 统一使用作者身份:`codex <codex-ice@gmail.com>`。
12
+
13
+ ## 工作流
14
+
15
+ 1. 先查看暂存变更,确认本次提交只表达一个意图:`git diff --cached --name-status`。
16
+ 2. 选择 `type`,必须是以下之一:`feat` `fix` `refactor` `perf` `style` `test` `docs` `chore`。
17
+ 3. 写首行:`<type>(<scope>): <summary>`。
18
+ 4. 首行后可选正文;若 `type=perf`,必须写正文说明优化原因。
19
+ 5. 使用脚本校验:`python3 scripts/validate_commit_message.py --file <msg-file>`。
20
+ 6. 通过校验后提交,并固定作者身份:
21
+ `git -c user.name='codex' -c user.email='codex-ice@gmail.com' commit -F <msg-file>`。
22
+
23
+ ## 强制规则
24
+
25
+ 1. 格式必须严格为:`<type>(<scope>): <summary>`。
26
+ 2. `type` 只能从这 8 个值中选择:`feat` `fix` `refactor` `perf` `style` `test` `docs` `chore`。
27
+ 3. `summary` 必须满足:
28
+ - 使用祈使句。
29
+ - 不超过 50 字符。
30
+ - 不要句号(`。` 或 `.`)。
31
+ - 不要出现 `and`、`&`、`multiple changes`。
32
+ - 使用中文表达。
33
+ 4. 提交文本(`summary` 与可选正文)使用中文。
34
+ 5. `perf` 类型必须在正文说明优化原因。
35
+
36
+ ## 示例
37
+
38
+ 正确:
39
+ - `fix(album): 避免滚动时重复渲染`
40
+ - `docs(api): 补充鉴权参数说明`
41
+
42
+ 错误:
43
+ - `update code`
44
+ - `fix bug and refactor logic`
45
+ - `optimize`
46
+
47
+ ## 脚本资源
48
+
49
+ - `scripts/validate_commit_message.py`
50
+ 校验提交文本是否符合本技能的强制规则。
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Git Commit Helper"
3
+ short_description: "Generate Chinese Git commit messages with strict checks"
4
+ default_prompt: "Use $git-commit-helper to draft a Chinese commit message and validate it before committing as codex <codex-ice@gmail.com>."
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env python3
2
+ """Validate commit message for git-commit-helper skill rules."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import re
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ ALLOWED_TYPES = ("feat", "fix", "refactor", "perf", "style", "test", "docs", "chore")
12
+ HEADER_RE = re.compile(
13
+ r"^(?P<type>feat|fix|refactor|perf|style|test|docs|chore)"
14
+ r"\((?P<scope>[a-z0-9][a-z0-9-]*)\): (?P<summary>.+)$"
15
+ )
16
+ CJK_RE = re.compile(r"[\u4e00-\u9fff]")
17
+ FORBIDDEN_SUMMARY_PATTERNS = (
18
+ re.compile(r"\band\b", re.IGNORECASE),
19
+ re.compile(r"&"),
20
+ re.compile(r"multiple changes", re.IGNORECASE),
21
+ )
22
+ FORBIDDEN_ENDINGS = (".", "。")
23
+ MAX_SUMMARY_LEN = 50
24
+
25
+
26
+ def load_message(args: argparse.Namespace) -> str:
27
+ if args.message and args.file:
28
+ raise ValueError("--message 与 --file 只能二选一")
29
+ if not args.message and not args.file:
30
+ raise ValueError("必须提供 --message 或 --file")
31
+
32
+ if args.message:
33
+ return args.message
34
+
35
+ content = Path(args.file).read_text(encoding="utf-8")
36
+ return content.rstrip("\n")
37
+
38
+
39
+ def validate_commit_message(message: str) -> list[str]:
40
+ errors: list[str] = []
41
+ lines = message.splitlines()
42
+
43
+ if not lines:
44
+ return ["提交信息不能为空"]
45
+
46
+ header = lines[0].strip()
47
+ if not header:
48
+ return ["提交首行不能为空"]
49
+
50
+ match = HEADER_RE.match(header)
51
+ commit_type = None
52
+ summary = ""
53
+ if not match:
54
+ errors.append("首行格式必须为 <type>(<scope>): <summary>")
55
+ else:
56
+ commit_type = match.group("type")
57
+ summary = match.group("summary").strip()
58
+
59
+ if len(lines) > 1 and lines[1] != "":
60
+ errors.append("若包含正文,第 2 行必须为空行")
61
+
62
+ body = "\n".join(lines[2:]).strip() if len(lines) > 2 else ""
63
+
64
+ if commit_type and commit_type not in ALLOWED_TYPES:
65
+ errors.append("type 必须是 feat/fix/refactor/perf/style/test/docs/chore 之一")
66
+
67
+ if summary:
68
+ if len(summary) > MAX_SUMMARY_LEN:
69
+ errors.append("summary 长度不能超过 50 字符")
70
+ if summary.endswith(FORBIDDEN_ENDINGS):
71
+ errors.append("summary 结尾不能使用句号")
72
+ if not CJK_RE.search(summary):
73
+ errors.append("summary 必须使用中文")
74
+
75
+ for pattern in FORBIDDEN_SUMMARY_PATTERNS:
76
+ if pattern.search(summary):
77
+ errors.append("summary 不能包含 and / & / multiple changes")
78
+ break
79
+
80
+ if body and not CJK_RE.search(body):
81
+ errors.append("正文需使用中文")
82
+
83
+ if commit_type == "perf" and not body:
84
+ errors.append("perf 类型必须提供正文说明优化原因")
85
+
86
+ return errors
87
+
88
+
89
+ def main() -> int:
90
+ parser = argparse.ArgumentParser(
91
+ description="校验提交信息是否符合 git-commit-helper 规则",
92
+ )
93
+ parser.add_argument("--message", help="直接传入提交信息文本")
94
+ parser.add_argument("--file", help="从文件读取提交信息")
95
+ args = parser.parse_args()
96
+
97
+ try:
98
+ message = load_message(args)
99
+ except Exception as exc: # noqa: BLE001
100
+ print(f"参数错误: {exc}")
101
+ return 2
102
+
103
+ errors = validate_commit_message(message)
104
+ if errors:
105
+ print("校验失败:")
106
+ for idx, err in enumerate(errors, start=1):
107
+ print(f"{idx}. {err}")
108
+ return 1
109
+
110
+ print("校验通过")
111
+ return 0
112
+
113
+
114
+ if __name__ == "__main__":
115
+ sys.exit(main())