agent-skill-installer 0.1.5 → 0.1.7

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/bin/cli.js CHANGED
@@ -3,6 +3,7 @@
3
3
  const fs = require("node:fs/promises");
4
4
  const path = require("node:path");
5
5
  const os = require("node:os");
6
+ const readline = require("node:readline/promises");
6
7
 
7
8
  const REPO_ROOT = path.resolve(__dirname, "..");
8
9
  const SKILLS_SOURCE_DIR = path.join(REPO_ROOT, "skills");
@@ -21,7 +22,7 @@ Commands:
21
22
 
22
23
  Options:
23
24
  --dest <path> Custom destination directory.
24
- --force Overwrite destination if a skill already exists.
25
+ --force Overwrite destination if a skill already exists (no prompt).
25
26
  -h, --help Show this help.
26
27
  `);
27
28
  }
@@ -112,6 +113,33 @@ async function listSkills() {
112
113
  }
113
114
  }
114
115
 
116
+ async function confirmOverwrite(name, targetPath) {
117
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
118
+ throw new Error(
119
+ `Destination already exists for "${name}": ${targetPath} (non-interactive mode, use --force to overwrite)`
120
+ );
121
+ }
122
+
123
+ const rl = readline.createInterface({
124
+ input: process.stdin,
125
+ output: process.stdout
126
+ });
127
+
128
+ try {
129
+ const answer = (
130
+ await rl.question(
131
+ `Skill "${name}" already exists at ${targetPath}. Overwrite it? [y/N] `
132
+ )
133
+ )
134
+ .trim()
135
+ .toLowerCase();
136
+
137
+ return answer === "y" || answer === "yes";
138
+ } finally {
139
+ rl.close();
140
+ }
141
+ }
142
+
115
143
  async function installSkills(rawArgs) {
116
144
  const parsed = parseInstallArgs(rawArgs);
117
145
  if (parsed.help) {
@@ -137,13 +165,16 @@ async function installSkills(rawArgs) {
137
165
  const to = path.join(parsed.dest, name);
138
166
  const exists = await dirExists(to);
139
167
 
140
- if (exists && !parsed.force) {
141
- throw new Error(
142
- `Destination already exists for "${name}": ${to} (use --force to overwrite)`
143
- );
168
+ if (exists && parsed.force) {
169
+ await fs.rm(to, { recursive: true, force: true });
144
170
  }
145
171
 
146
- if (exists && parsed.force) {
172
+ if (exists && !parsed.force) {
173
+ const shouldOverwrite = await confirmOverwrite(name, to);
174
+ if (!shouldOverwrite) {
175
+ console.log(`Skipped "${name}" (kept existing skill).`);
176
+ continue;
177
+ }
147
178
  await fs.rm(to, { recursive: true, force: true });
148
179
  }
149
180
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-skill-installer",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Install one or more Codex skills from this package via npx.",
5
5
  "bin": {
6
6
  "agent-skill-installer": "bin/cli.js"
@@ -7,45 +7,56 @@ Produce a daily report from local Codex sessions.
7
7
 
8
8
  ## Workflow
9
9
 
10
- 1. Resolve target date.
11
- - If user says `today`, use an absolute date (e.g. `2026-03-02`).
10
+ 1. Resolve target date to absolute date `YYYY-MM-DD`.
11
+ - If user says `today`, convert explicitly (for example `2026-03-02`).
12
+ - If user says relative dates (`yesterday`, `tomorrow`), convert to concrete dates first.
12
13
 
13
- 2. Read session files for that date.
14
+ 2. Extract `assistant final_answer` entries with bundled script.
14
15
 
15
16
  ```bash
16
- ls -1 ~/.codex/sessions/2026/03/02/*.jsonl
17
+ python3 skills/codex-session-daily-report/scripts/extract_final_answers.py --date 2026-03-02
17
18
  ```
18
19
 
19
- 3. Extract completed assistant outputs only (`final_answer`).
20
+ 3. If user asks for a custom session root, pass `--root`.
20
21
 
21
22
  ```bash
22
- for f in ~/.codex/sessions/2026/03/02/*.jsonl; do
23
- jq -r 'select(.type=="response_item" and .payload.type=="message" and .payload.role=="assistant" and .payload.phase=="final_answer")
24
- | .timestamp + "\t" + ((.payload.content // [])
25
- | map(select(.type=="output_text") | .text)
26
- | join(" ") | gsub("[\n\r\t]+";" ") | gsub(" +";" "))' "$f"
27
- done
23
+ python3 skills/codex-session-daily-report/scripts/extract_final_answers.py \
24
+ --date 2026-03-02 \
25
+ --root ~/.codex/sessions
28
26
  ```
29
27
 
30
- 4. Summarize by主题 and result.
31
- - Merge similar tasks into one topic line.
32
- - Keep key evidence: file paths, tests, and commit hashes if present.
33
- - Keep neutral language and avoid guessing.
28
+ 4. Summarize only completed outcomes from extracted lines.
29
+ - Merge similar tasks into one topic.
30
+ - Keep concrete evidence when present: file paths, test commands/results, commit hashes.
31
+ - Keep neutral wording and avoid guessing missing facts.
34
32
 
35
- 5. Output daily report only (not logs).
33
+ 5. Output report only, not raw logs, unless user explicitly asks for raw lines.
36
34
 
37
35
  ## Output Format
38
36
 
39
37
  Use this structure:
40
38
 
41
39
  1. 日期:`YYYY-MM-DD`
42
- 2. 今日业务事项:按主题列 2-6 条
43
- 3. 关键产出:代码改动/测试结论/提交哈希
44
- 4. 风险与待办:未完成项、潜在误判点
40
+ 2. 统计快照:`会话文件 N`、`final_answer N`、`覆盖时段 HH:mm-HH:mm`
41
+ 3. 今日业务事项:按主题列 2-6 条,每条包含 `动作`、`结果`、`证据`
42
+ 4. 关键产出:按类型分组输出
43
+ - 代码改动:关键文件路径
44
+ - 验证结果:命令与结论
45
+ - 提交记录:提交哈希与提交说明
46
+ 5. 风险与待办:每条包含 `风险/阻塞`、`影响`、`下一步`
47
+ 6. 数据缺口说明:若无会话文件、无 final_answer 或证据不足,明确说明影响范围
45
48
 
46
49
  ## Constraints
47
50
 
48
- - Prefer `assistant final_answer` over commentary.
51
+ - Use `assistant final_answer` only; do not use `commentary` as completion evidence.
49
52
  - Do not output raw JSONL unless user explicitly asks.
50
53
  - Keep report concise and factual.
51
- - If no completed entries are found, state that clearly.
54
+ - If no session files or no `final_answer` are found, state that clearly in the report.
55
+ - Prefer explicit evidence fields over narrative paragraphs.
56
+ - If evidence is missing for a topic, mark it as `证据不足` instead of guessing.
57
+
58
+ ## Resource
59
+
60
+ - `skills/codex-session-daily-report/scripts/extract_final_answers.py`
61
+ - Input: `--date YYYY-MM-DD`, optional `--root`.
62
+ - Output: `timestamp<TAB>session_file<TAB>text`.
@@ -1,4 +1,4 @@
1
1
  interface:
2
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."
3
+ short_description: "Summarize Codex final answers into a daily report"
4
+ default_prompt: "Use $codex-session-daily-report to resolve a concrete target date, extract assistant final_answer entries from ~/.codex/sessions, and output a concise factual daily report."
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env python3
2
+ """Extract assistant final answers from Codex session JSONL files."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import glob
8
+ import json
9
+ import os
10
+ import re
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ WHITESPACE_RE = re.compile(r"\s+")
15
+
16
+
17
+ def normalize_text(text: str) -> str:
18
+ return WHITESPACE_RE.sub(" ", text).strip()
19
+
20
+
21
+ def parse_date(value: str) -> tuple[str, str, str]:
22
+ parts = value.split("-")
23
+ if len(parts) != 3 or not all(part.isdigit() for part in parts):
24
+ raise ValueError("日期格式必须为 YYYY-MM-DD")
25
+
26
+ year, month, day = parts
27
+ if len(year) != 4 or len(month) != 2 or len(day) != 2:
28
+ raise ValueError("日期格式必须为 YYYY-MM-DD")
29
+
30
+ return year, month, day
31
+
32
+
33
+ def collect_session_files(root: str, date_value: str) -> list[str]:
34
+ year, month, day = parse_date(date_value)
35
+ pattern = os.path.join(root, year, month, day, "*.jsonl")
36
+ return sorted(glob.glob(pattern))
37
+
38
+
39
+ def extract_records(file_path: str) -> list[str]:
40
+ records: list[str] = []
41
+ with Path(file_path).open("r", encoding="utf-8") as handle:
42
+ for line in handle:
43
+ if not line.strip():
44
+ continue
45
+
46
+ try:
47
+ item = json.loads(line)
48
+ except json.JSONDecodeError:
49
+ continue
50
+
51
+ if item.get("type") != "response_item":
52
+ continue
53
+
54
+ payload = item.get("payload") or {}
55
+ if payload.get("type") != "message":
56
+ continue
57
+ if payload.get("role") != "assistant":
58
+ continue
59
+ if payload.get("phase") != "final_answer":
60
+ continue
61
+
62
+ content = payload.get("content") or []
63
+ text_parts = [
64
+ part.get("text", "")
65
+ for part in content
66
+ if isinstance(part, dict) and part.get("type") == "output_text"
67
+ ]
68
+ text = normalize_text(" ".join(text_parts))
69
+ if not text:
70
+ continue
71
+
72
+ timestamp = item.get("timestamp", "")
73
+ session_file = os.path.basename(file_path)
74
+ records.append(f"{timestamp}\t{session_file}\t{text}")
75
+
76
+ return records
77
+
78
+
79
+ def main() -> int:
80
+ parser = argparse.ArgumentParser(
81
+ description="提取指定日期的 assistant final_answer 内容",
82
+ )
83
+ parser.add_argument(
84
+ "--date",
85
+ required=True,
86
+ help="目标日期,格式 YYYY-MM-DD",
87
+ )
88
+ parser.add_argument(
89
+ "--root",
90
+ default="~/.codex/sessions",
91
+ help="sessions 根目录,默认 ~/.codex/sessions",
92
+ )
93
+ args = parser.parse_args()
94
+
95
+ root = os.path.expanduser(args.root)
96
+ files = collect_session_files(root, args.date)
97
+
98
+ if not files:
99
+ print(f"未找到会话文件: {args.date}", file=sys.stderr)
100
+ return 1
101
+
102
+ wrote = False
103
+ for file_path in files:
104
+ for record in extract_records(file_path):
105
+ print(record)
106
+ wrote = True
107
+
108
+ if not wrote:
109
+ print(f"未找到 final_answer: {args.date}", file=sys.stderr)
110
+ return 2
111
+
112
+ return 0
113
+
114
+
115
+ if __name__ == "__main__":
116
+ sys.exit(main())