@yottameta/yotta-logs 0.2.2 → 0.3.1

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/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
1
+ ## v0.3.1 (2026-09-13)
2
+
3
+ - 修复本机专属路径硬编码:移除本机自定义数据目录候选;opencode 只认 `XDG_DATA_HOME` / `OPENCODE_DATA` / 官方默认路径,Codex notes 只认 `$CODEX_HOME/memories` 或 `~/.codex/memories`。
4
+ - 配置路径改为平台无关:`$YOTTA_LOGS_CONFIG` > Windows `%APPDATA%` > Unix `$XDG_CONFIG_HOME` / `~/.config`。
5
+ - 新增便携覆盖回归:`YOTTA_MEMORY_HOME`、`CODEX_HOME`、`YOTTA_LOGS_CONFIG`;并加本机自定义目录反向断言。
6
+
7
+ ## v0.3.0 (2026-09-08)
8
+
9
+ **评测驱动完善**:新增 FAQ,安装器错误处理与测试补齐。
10
+
11
+ - 新增 references/faq.md,覆盖格式、范围、脱敏、只读边界与安装排障。
12
+ - 安装器支持 --help、参数校验、统一退出码与人话错误提示。
13
+
1
14
  # 更新日志
2
15
 
3
16
  ## v0.2.2 (2026-08-29)
@@ -8,7 +21,7 @@
8
21
 
9
22
  ## v0.2.1 (2026-08-27)
10
23
 
11
- 文档中英双版(老张拍板「英文门面 + 中文全档」):
24
+ 文档中英双版:
12
25
 
13
26
  - **README.md 改为英文**:作为 GitHub / npm / ClawHub 首页的英文门面(翻译 + 精简,覆盖定位 / 核心价值 / 命令 / 快速使用 / 安装 / 使用示例 / 开发校验全流程)。
14
27
  - **新增 README.zh-CN.md**:原中文完整主文档整体平移,继续服务中文用户,顶部加语言切换链接。
@@ -18,7 +31,7 @@
18
31
 
19
32
  ## v0.2.0 (2026-08-27)
20
33
 
21
- 多格式通用化:不再只认 JSONL,按「格式族 × 字段别名归一 + 配置兜底」适配一切格式(老张拍板三点:方向认可 / 版本 v0.2.0 / 默认检索范围动作工时定):
34
+ 多格式通用化:不再只认 JSONL,按「格式族 × 字段别名归一 + 配置兜底」适配一切格式:
22
35
 
23
36
  - **五大格式族 reader**:JSONL(支持嵌套子目录与 Codex rollout payload 形态)/ 单文件 JSON(数组、dict-of-lists)/ SQLite(opencode schema 实测 + 通用列映射兜底,只读 mode=ro)/ Markdown(YAML frontmatter 结构化记忆 + 自由笔记)/ 二进制(只降级读标题不崩)。
24
37
  - **统一 Record 模型**:{source, format, kind, session, time, role, text, path, meta};字段别名归一(time / role / text / session / title),秒 / 毫秒自动推断,JSON 字符串解包。
@@ -30,9 +43,9 @@
30
43
 
31
44
  ## v0.1.0 (2026-08-27)
32
45
 
33
- YottaMeta 自有实现首版(历史会话日志检索方向参考开源社区 session-logs 类技能思路,已完全重写,无上游代码):
46
+ YottaMeta 自有实现首版(历史会话日志检索方向参考开源社区 session-logs 类技能思路,已完全重写):
34
47
 
35
48
  - 元史(yotta-logs)—— 零依赖跨智能体会话日志检索引擎(Python 3.8+ 标准库)。
36
49
  - 能力:locate / scan / search / session / stats / tools / version;关键词 / 正则 / 日期 / 会话 ID / 别名 / 角色过滤;默认脱敏(--no-redact 关闭);--json 结构化输出;只读安全;容错 JSONL 解析(含 sessions.json 索引别名)。
37
50
  - 测试:scripts/test_yotta_logs.py 75 项全绿。
38
- - 版权:YottaMeta 纯自有 MIT + NOTICE 品牌声明;README 一行上游致谢。
51
+ - 版权:YottaMeta 纯自有 MIT + NOTICE 品牌声明。
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 YottaMeta
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1
+ MIT License
2
+
3
+ Copyright (c) 2026 YottaMeta
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  SOFTWARE.
package/README.md CHANGED
@@ -162,12 +162,19 @@ bash install.sh --list # list agents -> default directories
162
162
 
163
163
  ## Changelog
164
164
 
165
+ - v0.3.1 (2026-09-13): Removed machine-specific hardcoded paths; discovery and config now follow environment variables and platform defaults, with regression coverage.
166
+ - v0.3.0 (2026-09-08): Evaluation-driven refinement — added FAQ, hardened installer error handling and exit codes, and added installer tests.
167
+
165
168
  - v0.2.2 (2026-08-29): Install docs alignment — unified four install methods (npx -y @yottameta/yotta-logs --agent/--dir, git clone, GitHub Download ZIP, install.sh --agent/--dir/--list), removed the legacy GitHub-clone installer and global-install (-g) recommendations; bilingual README install section synced to 发布规范 §3.3.1. No functional change.
166
169
 
167
170
  - v0.2.1 (2026-08-27): Bilingual documentation — English README as the GitHub / npm / ClawHub homepage, full Chinese doc moved to README.zh-CN.md, English npm description.
168
171
  - v0.2.0 (2026-08-27): Multi-format generalization — JSONL / single-file JSON / SQLite (opencode etc.) / Markdown (memory + free notes) / binary; unified Record + field-alias normalization + config fallback; discover; new --source / --kind / --format filters and default search scope (session + structured memory on, free notes / binary logs off). See CHANGELOG.md.
169
172
  - v0.1.0 (2026-08-27): Initial release — zero-dependency JSONL session log search engine (locate / scan / search / session / stats / tools / version + default redaction + sessions.json alias + read-only).
170
173
 
174
+ ## References
175
+
176
+ - references/faq.md
177
+
171
178
  ## License
172
179
 
173
180
  MIT © YottaMeta — see [LICENSE](./LICENSE).
package/README.zh-CN.md CHANGED
@@ -162,11 +162,18 @@ bash install.sh --list # 列出智能体 -> 默认目录
162
162
 
163
163
  ## 更新日志
164
164
 
165
+ - v0.3.1(2026-09-13):移除本机专属硬编码路径;discovery 与配置统一走环境变量和平台默认位置,并补回归覆盖。
166
+ - v0.3.0(2026-09-08):评测驱动完善——新增常见问题,安装器错误处理与退出码加固,并补齐安装器测试。
167
+
165
168
  - v0.2.2(2026-08-29):安装方式统一为四方式(对齐发布规范 §3.3.1)——方式一 npx -y @yottameta/yotta-logs --agent / --dir(推荐,走 npm 源);方式二 git clone;方式三 GitHub Download ZIP;方式四 bash install.sh --agent/--dir/--list。移除旧式 GitHub 克隆安装器与全局安装(-g)推荐;中英 README 安装节同步。无功能变更。
166
169
 
167
170
  - v0.2.0(2026-08-27):多格式通用化——JSONL / 单文件 JSON / SQLite(opencode 等)/ Markdown(记忆 + 自由笔记)/ 二进制五大格式族,统一 Record + 字段别名归一 + 配置兜底,discover 全源登记,新增 --source / --kind / --format 过滤与默认检索范围(会话 + 结构化记忆开、自由笔记 / 二进制日志关)。详见 CHANGELOG.md。
168
171
  - v0.1.0(2026-08-27):首版——零依赖 JSONL 会话日志检索引擎(locate / scan / search / session / stats / tools / version + 默认脱敏 + sessions.json 别名 + 只读)。
169
172
 
173
+ ## 参考文档
174
+
175
+ - references/faq.md
176
+
170
177
  ## 许可证
171
178
 
172
179
  MIT © YottaMeta —— 详见 [LICENSE](./LICENSE)。
package/SKILL.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: yotta-logs
3
- version: 0.2.2
3
+ version: 0.3.1
4
4
  description: 元史 —— 跨智能体的历史会话 / 记忆日志检索技能:零依赖检索 / 分析 JSONL、JSON、SQLite、Markdown 多格式会话与记忆文件,回溯旧对话与父会话上下文,为跨会话追溯提供原始日志依据。触发:用户问起先前聊过的内容 / 父会话 / 历史上下文、要查以前说过的结论、跨会话回溯某次讨论、需要从会话日志或记忆文件定位某段决策时。边界:仅读取本机自己的会话日志 / 记忆文件;不修改、不删除;只查本地不联网上传。
5
5
  license: MIT
6
6
  ---
@@ -84,6 +84,7 @@ python3 scripts/yotta_logs.py tools --dir /path/to/logs --format sqlite
84
84
  - references/format.md — 统一 Record 模型与各格式族读取规则
85
85
  - references/cli.md — CLI 子命令 / 参数 / 退出码 / JSON schema / 配置详解
86
86
  - references/security.md — 安全边界 / 脱敏规则 / 与元忆的差异化
87
+ - references/faq.md — 常见问题与安装排障
87
88
 
88
89
  ## 责任声明
89
90
 
package/bin/install.js CHANGED
@@ -1,163 +1,245 @@
1
- #!/usr/bin/env node
2
- /**
3
- * yotta-logs 跨平台安装器(YottaSkills)
4
- * 用法:
5
- * npx -y @yottameta/yotta-logs --agent <name> # 按智能体默认用户级目录安装(推荐)
6
- * npx -y @yottameta/yotta-logs --dir PATH # 装到指定目录(用户改了目录的智能体)
7
- * npx -y @yottameta/yotta-logs -g # 安装到全部已知智能体用户级目录
8
- * npx -y @yottameta/yotta-logs # 安装到检测到的项目级目录
9
- * npx -y @yottameta/yotta-logs --list # 列出智能体 -> 默认目录
10
- */
11
- 'use strict';
12
- const fs = require('fs');
13
- const path = require('path');
14
- const os = require('os');
15
-
16
- const SKILL_NAME = 'yotta-logs';
17
- const PKG_ROOT = path.join(__dirname, '..');
18
-
19
- // 智能体 -> 用户级默认技能目录(dirs 按优先级排列;--agent 装到第一个)
20
- // 依据官方文档:.agents/skills 并非通用目录,被 OpenCode / Cursor / Cline / Amp /
21
- // Kimi / Gemini CLI / GitHub Copilot 等读取;Claude Code 与 Codex 默认不读 .agents。
22
- const AGENT_DIRS = {
23
- claude: { label: 'Claude Code', dirs: ['.claude/skills'] },
24
- cursor: { label: 'Cursor', dirs: ['.cursor/skills', '.agents/skills'] },
25
- codex: { label: 'Codex', dirs: ['.codex/skills'] }, // 特判:$CODEX_HOME/skills
26
- gemini: { label: 'Gemini CLI', dirs: ['.gemini/skills', '.agents/skills'] },
27
- goose: { label: 'Goose', dirs: ['.config/goose/skills', '.agents/skills'] },
28
- amp: { label: 'Amp', dirs: ['.config/agents/skills', '.agents/skills'] },
29
- opencode: { label: 'OpenCode', dirs: ['.config/opencode/skills'] }, // 特判:$XDG_CONFIG_HOME
30
- windsurf: { label: 'Windsurf', dirs: ['.codeium/windsurf/skills'] },
31
- workbuddy: { label: 'WorkBuddy', dirs: ['.workbuddy/skills'] },
32
- kiro: { label: 'Kiro', dirs: ['.kiro/skills'] },
33
- trae: { label: 'Trae Code CLI', dirs: ['.traecli/skills'] },
34
- 'trae-cn': { label: 'Trae IDE(国内)', dirs: ['.trae-cn/skills'] },
35
- qwen: { label: 'Qwen Code', dirs: ['.qwen/skills'] },
36
- comate: { label: 'Comate 文心快码', dirs: ['.comate/skills'] },
37
- codebuddy: { label: 'CodeBuddy Code', dirs: ['.codebuddy/skills'] },
38
- kimi: { label: 'Kimi Code CLI', dirs: ['.kimi/skills'] },
39
- agents: { label: '通用 AGENTS.md', dirs: ['.agents/skills'] },
40
- };
41
-
42
- // Codex 用户级目录特判:优先 $CODEX_HOME/skills,否则 ~/.codex/skills
43
- function codexUserDir() {
44
- const base = process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
45
- return path.join(base, 'skills');
46
- }
47
-
48
- // OpenCode 用户级目录特判:优先 $XDG_CONFIG_HOME/opencode/skills,否则 ~/.config/opencode/skills
49
- function opencodeUserDir() {
50
- const base = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
51
- return path.join(base, 'opencode', 'skills');
52
- }
53
-
54
- function resolveUserDir(rel) {
55
- if (rel === '.codex/skills') return codexUserDir();
56
- if (rel === '.config/opencode/skills') return opencodeUserDir();
57
- return path.join(os.homedir(), rel);
58
- }
59
-
60
- function installTo(dest) {
61
- const target = path.join(dest, SKILL_NAME);
62
- fs.mkdirSync(target, { recursive: true });
63
- copyDir(PKG_ROOT, target, new Set(['package.json', 'bin', 'node_modules', '.git']));
64
- console.log('installed -> ' + target);
65
- }
66
-
67
- function copyDir(src, dst, skip) {
68
- for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
69
- if (skip.has(entry.name)) continue;
70
- const s = path.join(src, entry.name);
71
- const d = path.join(dst, entry.name);
72
- if (entry.isDirectory()) {
73
- fs.mkdirSync(d, { recursive: true });
74
- copyDir(s, d, skip);
75
- } else if (entry.isFile()) {
76
- fs.copyFileSync(s, d);
77
- }
78
- }
79
- }
80
-
81
- function displayDir(rel) {
82
- if (process.platform === 'win32') return '%USERPROFILE%\\' + rel.replace(/\//g, '\\');
83
- return '~/' + rel;
84
- }
85
-
86
- function main() {
87
- const args = process.argv.slice(2);
88
- const isGlobal = args.includes('-g') || args.includes('--global');
89
- const list = args.includes('--list') || args.includes('-l');
90
- let explicitDir = null;
91
- const di = args.indexOf('--dir');
92
- if (di !== -1 && args[di + 1]) explicitDir = args[di + 1];
93
- let agent = null;
94
- const ai = args.indexOf('--agent');
95
- if (ai !== -1 && args[ai + 1]) agent = String(args[ai + 1]).toLowerCase();
96
-
97
- if (list) {
98
- console.log('智能体 -> 默认技能目录(--agent <name> 装到第一个,用户级):');
99
- for (const [key, v] of Object.entries(AGENT_DIRS)) {
100
- const resolved = v.dirs.map(displayDir);
101
- console.log(' ' + key.padEnd(10) + v.label.padEnd(18) + resolved.join('、'));
102
- }
103
- console.log('\n说明:Windows 用 %USERPROFILE%,Linux/macOS 用 ~;仅收录有官方默认目录的智能体。');
104
- console.log('改了目录的请用 --dir <路径>,不要依赖默认位置;若设置了 CODEX_HOME / XDG_CONFIG_HOME,安装自动以该变量为准。');
105
- return;
106
- }
107
-
108
- if (explicitDir) { installTo(explicitDir); return; }
109
-
110
- if (agent) {
111
- const info = AGENT_DIRS[agent];
112
- if (!info) {
113
- console.log('未收录智能体: ' + agent + '。请用 --dir <路径> 指定技能目录。');
114
- console.log('可用: ' + Object.keys(AGENT_DIRS).join(', '));
115
- return;
116
- }
117
- installTo(resolveUserDir(info.dirs[0]));
118
- console.log('完成。');
119
- return;
120
- }
121
-
122
- if (isGlobal) {
123
- const seen = new Set();
124
- for (const v of Object.values(AGENT_DIRS)) {
125
- for (const d of v.dirs) {
126
- if (seen.has(d)) continue;
127
- seen.add(d);
128
- installTo(resolveUserDir(d));
129
- }
130
- }
131
- console.log('完成。');
132
- return;
133
- }
134
-
135
- const PROJECT_DIRS = [
136
- '.claude/skills',
137
- '.cursor/skills',
138
- '.codex/skills',
139
- '.config/goose/skills',
140
- '.config/agents/skills',
141
- '.opencode/skills',
142
- '.codeium/windsurf/skills',
143
- '.workbuddy/skills',
144
- '.kiro/skills',
145
- '.traecli/skills',
146
- '.gemini/skills',
147
- '.trae-cn/skills',
148
- '.qwen/skills',
149
- '.comate/skills',
150
- '.codebuddy/skills',
151
- '.kimi/skills',
152
- '.agents/skills',
153
- ];
154
- let installedAny = false;
155
- for (const d of PROJECT_DIRS) {
156
- if (fs.existsSync(d)) { installTo(d); installedAny = true; }
157
- }
158
- if (!installedAny) {
159
- console.log('未检测到项目级智能体目录。可手动复制,或用 --agent <name> / -g 装到用户级。');
160
- }
161
- }
162
-
163
- main();
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @yottameta/yotta-logs cross-platform installer.
4
+ *
5
+ * Usage:
6
+ * npx -y @yottameta/yotta-logs --agent <name>
7
+ * npx -y @yottameta/yotta-logs --dir <path>
8
+ * npx -y @yottameta/yotta-logs -g
9
+ * npx -y @yottameta/yotta-logs
10
+ * npx -y @yottameta/yotta-logs --list
11
+ */
12
+ 'use strict';
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const os = require('os');
17
+
18
+ const SKILL_NAME = 'yotta-logs';
19
+ const PKG_ROOT = path.join(__dirname, '..');
20
+ const COPY_SKIP = new Set(['package.json', 'bin', 'node_modules', '.git', '.github', 'test']);
21
+
22
+ const AGENT_DIRS = {
23
+ claude: { label: 'Claude Code', dirs: ['.claude/skills'] },
24
+ cursor: { label: 'Cursor', dirs: ['.cursor/skills', '.agents/skills'] },
25
+ codex: { label: 'Codex', dirs: ['.codex/skills'] },
26
+ gemini: { label: 'Gemini CLI', dirs: ['.gemini/skills', '.agents/skills'] },
27
+ goose: { label: 'Goose', dirs: ['.config/goose/skills', '.agents/skills'] },
28
+ amp: { label: 'Amp', dirs: ['.config/agents/skills', '.agents/skills'] },
29
+ opencode: { label: 'OpenCode', dirs: ['.config/opencode/skills'] },
30
+ windsurf: { label: 'Windsurf', dirs: ['.codeium/windsurf/skills'] },
31
+ workbuddy: { label: 'WorkBuddy', dirs: ['.workbuddy/skills'] },
32
+ kiro: { label: 'Kiro', dirs: ['.kiro/skills'] },
33
+ trae: { label: 'Trae Code CLI', dirs: ['.traecli/skills'] },
34
+ 'trae-cn': { label: 'Trae IDE', dirs: ['.trae-cn/skills'] },
35
+ qwen: { label: 'Qwen Code', dirs: ['.qwen/skills'] },
36
+ comate: { label: 'Comate', dirs: ['.comate/skills'] },
37
+ codebuddy: { label: 'CodeBuddy Code', dirs: ['.codebuddy/skills'] },
38
+ kimi: { label: 'Kimi Code CLI', dirs: ['.kimi/skills'] },
39
+ agents: { label: 'Generic AGENTS.md', dirs: ['.agents/skills'] },
40
+ };
41
+
42
+ class UsageError extends Error {}
43
+ class TargetError extends Error {}
44
+ class InstallError extends Error {}
45
+
46
+ function usage() {
47
+ console.log('yotta-logs installer');
48
+ console.log('');
49
+ console.log('Usage:');
50
+ console.log(' npx -y @yottameta/yotta-logs --agent <name> Install to an agent default directory');
51
+ console.log(' npx -y @yottameta/yotta-logs --dir <path> Install to a custom directory');
52
+ console.log(' npx -y @yottameta/yotta-logs -g Install to all known agent directories');
53
+ console.log(' npx -y @yottameta/yotta-logs Auto-detect project-level directories');
54
+ console.log(' npx -y @yottameta/yotta-logs --list List supported agent directories');
55
+ console.log('');
56
+ console.log('Options:');
57
+ console.log(' --agent <name> Agent key, see --list');
58
+ console.log(' --dir <path> Custom skills directory');
59
+ console.log(' -g, --global Install to all known user-level directories');
60
+ console.log(' --list, -l List supported agents');
61
+ console.log(' -h, --help Show this help');
62
+ }
63
+
64
+ function parseArgs(argv) {
65
+ const opts = { help: false, list: false, global: false, dir: null, agent: null };
66
+ for (let i = 0; i < argv.length; i++) {
67
+ const arg = argv[i];
68
+ if (arg === '--help' || arg === '-h') opts.help = true;
69
+ else if (arg === '--list' || arg === '-l') opts.list = true;
70
+ else if (arg === '--global' || arg === '-g') opts.global = true;
71
+ else if (arg === '--dir') {
72
+ const value = argv[++i];
73
+ if (!value) throw new UsageError('--dir requires a non-empty path');
74
+ opts.dir = value;
75
+ } else if (arg === '--agent') {
76
+ const value = argv[++i];
77
+ if (!value) throw new UsageError('--agent requires a non-empty name');
78
+ opts.agent = value.toLowerCase();
79
+ } else {
80
+ throw new UsageError('Unknown argument: ' + arg);
81
+ }
82
+ }
83
+ if (!opts.help) {
84
+ const selected = [opts.dir, opts.agent, opts.global].filter(Boolean).length;
85
+ if (selected > 1) throw new UsageError('Use only one of --dir, --agent, or -g');
86
+ }
87
+ return opts;
88
+ }
89
+
90
+ function codexUserDir() {
91
+ const base = process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
92
+ return path.join(base, 'skills');
93
+ }
94
+
95
+ function opencodeUserDir() {
96
+ const base = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
97
+ return path.join(base, 'opencode', 'skills');
98
+ }
99
+
100
+ function resolveUserDir(rel) {
101
+ if (rel === '.codex/skills') return codexUserDir();
102
+ if (rel === '.config/opencode/skills') return opencodeUserDir();
103
+ return path.join(os.homedir(), rel);
104
+ }
105
+
106
+ function displayDir(rel) {
107
+ if (process.platform === 'win32') return '%USERPROFILE%\\' + rel.replace(/\//g, '\\');
108
+ return '~/' + rel;
109
+ }
110
+
111
+ function printList() {
112
+ console.log('Agent -> default skill directory:');
113
+ for (const [key, value] of Object.entries(AGENT_DIRS)) {
114
+ console.log(' ' + key.padEnd(12) + value.label.padEnd(20) + value.dirs.map(displayDir).join(', '));
115
+ }
116
+ console.log('');
117
+ console.log('Use --dir <path> for agents not listed. CODEX_HOME and XDG_CONFIG_HOME are respected.');
118
+ }
119
+
120
+ function assertSafeTarget(target) {
121
+ const rel = path.relative(PKG_ROOT, target);
122
+ if (rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel))) {
123
+ throw new UsageError('Target directory must be outside the skill source directory');
124
+ }
125
+ }
126
+
127
+ function copyDir(src, dst, skip) {
128
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
129
+ if (skip.has(entry.name)) continue;
130
+ const from = path.join(src, entry.name);
131
+ const to = path.join(dst, entry.name);
132
+ try {
133
+ if (entry.isDirectory()) {
134
+ fs.mkdirSync(to, { recursive: true });
135
+ copyDir(from, to, skip);
136
+ } else if (entry.isFile()) {
137
+ fs.copyFileSync(from, to);
138
+ }
139
+ } catch (err) {
140
+ throw new InstallError('Failed to copy ' + from + ' -> ' + to + ': ' + err.message);
141
+ }
142
+ }
143
+ }
144
+
145
+ function installTo(dest) {
146
+ if (!dest || typeof dest !== 'string') throw new UsageError('Destination directory is required');
147
+ const target = path.resolve(dest, SKILL_NAME);
148
+ assertSafeTarget(target);
149
+ try {
150
+ fs.mkdirSync(target, { recursive: true });
151
+ copyDir(PKG_ROOT, target, COPY_SKIP);
152
+ if (!fs.existsSync(path.join(target, 'SKILL.md'))) {
153
+ throw new InstallError('Installed directory is missing SKILL.md');
154
+ }
155
+ } catch (err) {
156
+ if (err instanceof UsageError || err instanceof InstallError) throw err;
157
+ throw new InstallError('Cannot install to ' + target + ': ' + err.message);
158
+ }
159
+ console.log('installed -> ' + target);
160
+ return target;
161
+ }
162
+
163
+ function projectDirs() {
164
+ return [
165
+ '.claude/skills',
166
+ '.cursor/skills',
167
+ '.codex/skills',
168
+ '.config/goose/skills',
169
+ '.config/agents/skills',
170
+ '.opencode/skills',
171
+ '.codeium/windsurf/skills',
172
+ '.workbuddy/skills',
173
+ '.kiro/skills',
174
+ '.traecli/skills',
175
+ '.gemini/skills',
176
+ '.trae-cn/skills',
177
+ '.qwen/skills',
178
+ '.comate/skills',
179
+ '.codebuddy/skills',
180
+ '.kimi/skills',
181
+ '.agents/skills',
182
+ ].filter((dir) => fs.existsSync(dir));
183
+ }
184
+
185
+ function run() {
186
+ const opts = parseArgs(process.argv.slice(2));
187
+ if (opts.help) { usage(); return; }
188
+ if (opts.list) { printList(); return; }
189
+
190
+ if (opts.dir) { installTo(opts.dir); return; }
191
+
192
+ if (opts.agent) {
193
+ const info = AGENT_DIRS[opts.agent];
194
+ if (!info) {
195
+ throw new UsageError('Unknown agent: ' + opts.agent + '. Available: ' + Object.keys(AGENT_DIRS).join(', ') + '. Use --dir for a custom directory.');
196
+ }
197
+ installTo(resolveUserDir(info.dirs[0]));
198
+ return;
199
+ }
200
+
201
+ if (opts.global) {
202
+ const seen = new Set();
203
+ let count = 0;
204
+ for (const value of Object.values(AGENT_DIRS)) {
205
+ for (const dir of value.dirs) {
206
+ if (seen.has(dir)) continue;
207
+ seen.add(dir);
208
+ installTo(resolveUserDir(dir));
209
+ count++;
210
+ }
211
+ }
212
+ console.log('Installed to ' + count + ' directories.');
213
+ return;
214
+ }
215
+
216
+ const dirs = projectDirs();
217
+ if (!dirs.length) {
218
+ throw new TargetError('No project-level agent directory detected. Use --agent <name> or --dir <path>.');
219
+ }
220
+ for (const dir of dirs) installTo(dir);
221
+ }
222
+
223
+ function main() {
224
+ try {
225
+ run();
226
+ } catch (err) {
227
+ if (err instanceof UsageError) {
228
+ console.error('Usage error: ' + err.message);
229
+ usage();
230
+ process.exitCode = 2;
231
+ } else if (err instanceof TargetError) {
232
+ console.error('Target error: ' + err.message);
233
+ process.exitCode = 4;
234
+ } else if (err instanceof InstallError) {
235
+ console.error('Install failed: ' + err.message);
236
+ console.error('Fix: check directory permissions and free space, then retry. Use --dir to choose another directory.');
237
+ process.exitCode = 1;
238
+ } else {
239
+ console.error('Unexpected error: ' + (err && err.message ? err.message : String(err)));
240
+ process.exitCode = 1;
241
+ }
242
+ }
243
+ }
244
+
245
+ main();