@zicolasjac-ai/remote-readonly-ssh 1.1.5 → 1.1.6
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 +20 -10
- package/bin/rrs.js +31 -8
- package/package.json +1 -1
- package/skills/rrs/SKILL.md +1 -1
- package/src/init.js +4 -4
- package/src/setup.js +97 -22
package/README.md
CHANGED
|
@@ -90,19 +90,29 @@ rrs <tool> [arg] [--host <ip>] [--config <file>] [--json]
|
|
|
90
90
|
|
|
91
91
|
本工具附带一份 `SKILL.md`,教 AI 助手何时用、怎么用 `rrs`。安装后用 `rrs setup skills` 一键分发到各 agent 的 skills 目录:
|
|
92
92
|
|
|
93
|
-
| `--agent` |
|
|
94
|
-
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
98
|
-
| `
|
|
93
|
+
| `--agent` | 写入方式 | 路径 |
|
|
94
|
+
|-----------|----------|------|
|
|
95
|
+
| `agents`(canonical) | **实体文件** | `~/.agents/skills/rrs/SKILL.md` |
|
|
96
|
+
| `opencode` | 软链接 → agents | `~/.config/opencode/skills/rrs/SKILL.md` |
|
|
97
|
+
| `claude` / `claudecode` / `cursor` / `windsurf` / `codex` | 软链接 → agents | `~/.claude/skills/rrs/SKILL.md` |
|
|
98
|
+
| `qoder` | 软链接 → agents | `~/.qoder/skills/rrs/SKILL.md` |
|
|
99
|
+
| `all`(默认) | 实体 + 上述 3 个软链接 | 一行命令装齐 |
|
|
100
|
+
|
|
101
|
+
`rrs setup skills` 默认 `agent=all`(不询问,直接装),输出一行简明提示:
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
rrs skill 已安装: agents (实体), opencode/claude/qoder (软链接)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
> **Windows 注意**:普通用户缺少 `SeCreateSymbolicLinkPrivilege`,软链接创建会被 OS 拒绝;rrs 会自动 fallback 为复制,并提示"Windows 已 fallback 为复制(请开启开发者模式以启用真软链接)"。开启开发者模式后重新跑 `rrs setup skills` 即可使用真软链接。
|
|
99
108
|
|
|
100
109
|
```bash
|
|
101
|
-
rrs setup skills #
|
|
110
|
+
rrs setup skills # 一行命令装齐(实体 + 3 个软链接)
|
|
102
111
|
npx -y @zicolasjac-ai/remote-readonly-ssh@latest setup skills # 全局未装时用 npx
|
|
103
|
-
rrs setup skills --agent opencode #
|
|
104
|
-
rrs setup skills --
|
|
105
|
-
rrs setup
|
|
112
|
+
rrs setup skills --agent opencode # 只装 opencode 链接(agents 实体也自动建)
|
|
113
|
+
rrs setup skills --agent qoder # 只装 qoder 链接
|
|
114
|
+
rrs setup skills --dry-run # 仅预览将要创建的目标
|
|
115
|
+
rrs setup uninstall skills # 全部移除(实体 + 软链接)
|
|
106
116
|
```
|
|
107
117
|
|
|
108
118
|
卸载 npm 包时会自动尝试清理这些 skill 文件(`preuninstall` 钩子,失败不影响卸载)。
|
package/bin/rrs.js
CHANGED
|
@@ -24,8 +24,8 @@ function printUsage() {
|
|
|
24
24
|
["dir <path>", "列目录(路径白名单)"],
|
|
25
25
|
["bin-version <bin>", "查二进制版本(二进制白名单)"],
|
|
26
26
|
["exec-only <cmd>", "通用只读命令(前缀白名单+禁止模式)"],
|
|
27
|
-
["setup skills [--agent <n>]", "把 SKILL.md
|
|
28
|
-
["setup uninstall skills [--agent <n>]", "
|
|
27
|
+
["setup skills [--agent <n>]", "把 SKILL.md 装到 agents(实体) + opencode/claude/qoder(软链接)"],
|
|
28
|
+
["setup uninstall skills [--agent <n>]", "移除上述目录的 skill"],
|
|
29
29
|
];
|
|
30
30
|
console.log("rrs — CLI and Agent Skill for safe read-only SSH inspection\n");
|
|
31
31
|
console.log("用法: rrs <工具> [参数] [--host <ip>] [--config <file>] [--json]");
|
|
@@ -80,7 +80,7 @@ function parseArgs(argv) {
|
|
|
80
80
|
function runSetup(opts) {
|
|
81
81
|
const agent = opts.agent || "all";
|
|
82
82
|
if (!opts.setupTarget || opts.setupTarget !== "skills") {
|
|
83
|
-
console.error("用法: rrs setup skills [--agent all|opencode|claude|cursor|windsurf|codex|
|
|
83
|
+
console.error("用法: rrs setup skills [--agent all|opencode|claude|claudecode|cursor|windsurf|codex|qoder|agents|openhands] [--dry-run]");
|
|
84
84
|
console.error(" rrs setup uninstall skills [--agent ...] [--dry-run]");
|
|
85
85
|
process.exit(1);
|
|
86
86
|
}
|
|
@@ -93,11 +93,34 @@ function runSetup(opts) {
|
|
|
93
93
|
console.error(`[setup] 错误: ${e.message}`);
|
|
94
94
|
process.exit(1);
|
|
95
95
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
|
|
96
|
+
|
|
97
|
+
if (opts.dryRun) {
|
|
98
|
+
console.log(opts.setupCmd === "uninstall" ? "将卸载:" : "将安装:");
|
|
99
|
+
for (const r of results) {
|
|
100
|
+
console.log(` ${r.action.replace("would-", "将")} ${r.dir}`);
|
|
101
|
+
}
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 默认输出:一行简明
|
|
106
|
+
const canonical = results.find((r) => r.agent === "agents");
|
|
107
|
+
const links = results.filter((r) => r.agent !== "agents");
|
|
108
|
+
const linkNames = links.map((r) => r.agent).join("/");
|
|
109
|
+
const anyFallback = links.some((r) => r.fallback);
|
|
110
|
+
const verb = opts.setupCmd === "uninstall" ? "已卸载" : "已安装";
|
|
111
|
+
|
|
112
|
+
if (opts.setupCmd === "install") {
|
|
113
|
+
if (canonical && links.length > 0) {
|
|
114
|
+
console.log(`rrs skill ${verb}: agents (实体), ${linkNames} (软链接)${anyFallback ? " — Windows 已自动 fallback 为复制(请开启开发者模式以启用真软链接)" : ""}`);
|
|
115
|
+
} else if (canonical) {
|
|
116
|
+
console.log(`rrs skill ${verb}: agents (实体)`);
|
|
117
|
+
} else if (links.length > 0) {
|
|
118
|
+
console.log(`rrs skill ${verb}: ${linkNames} (软链接)${anyFallback ? " — Windows 已自动 fallback 为复制" : ""}`);
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
const removed = results.filter((r) => r.action !== "missing").map((r) => r.agent).join("/");
|
|
122
|
+
if (removed) console.log(`rrs skill ${verb}: ${removed}`);
|
|
123
|
+
else console.log(`rrs skill 未找到任何已安装项`);
|
|
101
124
|
}
|
|
102
125
|
}
|
|
103
126
|
|
package/package.json
CHANGED
package/skills/rrs/SKILL.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: rrs
|
|
3
3
|
description: 通过 JumpServer 堡垒机对服务器做白名单强制的只读运维查询(sys-info / proc / ports / dir / read-file / bin-version / exec-only)。技术强制只读、命令前缀白名单 + 禁止模式 + 路径白名单 + 主机白名单多层校验,每次调用(含被拒绝的)都写入本地审计日志。AI Agent 需要做"查看服务器现状"这类操作时调用此 skill;它不会写入任何远程状态。
|
|
4
4
|
license: MIT
|
|
5
|
-
compatibility: opencode, claude-code, cursor, windsurf, openhands, codex
|
|
5
|
+
compatibility: opencode, claude-code, cursor, windsurf, openhands, qoder, codex
|
|
6
6
|
metadata:
|
|
7
7
|
audience: ai-agents
|
|
8
8
|
category: ops
|
package/src/init.js
CHANGED
|
@@ -119,13 +119,13 @@ async function runInteractive({ input = process.stdin, output = process.stdout,
|
|
|
119
119
|
|
|
120
120
|
let installedSkills = false;
|
|
121
121
|
if (installSkills) {
|
|
122
|
-
const ans = (await rl.question("安装 AI skill 到 opencode
|
|
122
|
+
const ans = (await rl.question("安装 AI skill 到 agents/opencode/claude/qoder? [Y/n]: ")).trim().toLowerCase();
|
|
123
123
|
const install = ans || "y";
|
|
124
124
|
if (install !== "n") {
|
|
125
125
|
const results = setupSkill.install(packageRoot, { agent: "all" });
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
126
|
+
const links = results.filter((r) => r.agent !== "agents");
|
|
127
|
+
const fallback = links.some((r) => r.fallback);
|
|
128
|
+
output.write(`rrs skill 已安装: agents (实体), ${links.map((r) => r.agent).join("/")} (软链接)${fallback ? " — Windows 已 fallback 为复制(请开开发者模式启用真软链接)" : ""}\n`);
|
|
129
129
|
installedSkills = true;
|
|
130
130
|
}
|
|
131
131
|
}
|
package/src/setup.js
CHANGED
|
@@ -5,28 +5,34 @@ const path = require("path");
|
|
|
5
5
|
const os = require("os");
|
|
6
6
|
|
|
7
7
|
const SKILL_NAME = "rrs";
|
|
8
|
+
const CANONICAL = "agents"; // 唯一的物理文件位置;其他目标都是软链接
|
|
8
9
|
|
|
9
10
|
const ALIAS_TO_GROUP = {
|
|
10
|
-
opencode:
|
|
11
|
-
claude:
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
opencode: ["opencode"],
|
|
12
|
+
claude: ["claude"],
|
|
13
|
+
claudecode: ["claude"],
|
|
14
|
+
agents: ["agents"],
|
|
15
|
+
openhands: ["agents"],
|
|
16
|
+
qoder: ["qoder"],
|
|
17
|
+
cursor: ["claude"],
|
|
18
|
+
windsurf: ["claude"],
|
|
19
|
+
codex: ["claude"],
|
|
17
20
|
};
|
|
18
21
|
|
|
19
22
|
const RELATIVE_DIRS = {
|
|
20
23
|
opencode: [".config", "opencode", "skills"],
|
|
21
24
|
claude: [".claude", "skills"],
|
|
22
25
|
agents: [".agents", "skills"],
|
|
26
|
+
qoder: [".qoder", "skills"],
|
|
23
27
|
};
|
|
24
28
|
|
|
29
|
+
const LINK_TARGETS = ["opencode", "claude", "qoder"]; // 除了 agents 之外要建软链接的目录
|
|
30
|
+
|
|
25
31
|
function listTargets(agent) {
|
|
26
32
|
const a = (agent || "all").toLowerCase();
|
|
27
|
-
if (a === "all") return [
|
|
33
|
+
if (a === "all") return [CANONICAL, ...LINK_TARGETS];
|
|
28
34
|
const groups = ALIAS_TO_GROUP[a];
|
|
29
|
-
if (!groups) throw new Error(`未知 --agent [${agent}],支持: all / opencode / claude / cursor / windsurf / codex /
|
|
35
|
+
if (!groups) throw new Error(`未知 --agent [${agent}],支持: all / opencode / claude / claudecode / cursor / windsurf / codex / qoder / agents / openhands`);
|
|
30
36
|
return groups;
|
|
31
37
|
}
|
|
32
38
|
|
|
@@ -40,23 +46,80 @@ function skillSrc(packageRoot) {
|
|
|
40
46
|
return path.join(packageRoot, "skills", SKILL_NAME, "SKILL.md");
|
|
41
47
|
}
|
|
42
48
|
|
|
49
|
+
// 实体文件必须先存在;其他目录作为软链接指向它
|
|
50
|
+
function ensureCanonical(packageRoot, home) {
|
|
51
|
+
const dest = targetFile(CANONICAL, home);
|
|
52
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
53
|
+
fs.writeFileSync(dest, fs.readFileSync(skillSrc(packageRoot), "utf8"), "utf8");
|
|
54
|
+
return dest;
|
|
55
|
+
}
|
|
56
|
+
|
|
43
57
|
function install(packageRoot, opts = {}) {
|
|
44
58
|
const agent = opts.agent || "all";
|
|
45
59
|
const dryRun = !!opts.dryRun;
|
|
46
60
|
const home = opts.home || os.homedir();
|
|
61
|
+
// 默认尝试软链接;可在测试中置 false 强制复制(避免 Windows 权限要求)
|
|
62
|
+
const useSymlink = opts.useSymlink !== false;
|
|
47
63
|
const src = skillSrc(packageRoot);
|
|
48
64
|
if (!fs.existsSync(src)) throw new Error(`缺少 skill 资源: ${src}`);
|
|
49
|
-
|
|
65
|
+
|
|
66
|
+
const requested = listTargets(agent);
|
|
67
|
+
const wantsLinks = requested.some((t) => t !== CANONICAL);
|
|
50
68
|
const results = [];
|
|
51
|
-
|
|
69
|
+
|
|
70
|
+
// 物理文件:若请求包含 agents 或任何链接目标,都必须确保 agents 实体存在
|
|
71
|
+
const needCanonical = requested.includes(CANONICAL) || wantsLinks;
|
|
72
|
+
if (needCanonical && !dryRun) {
|
|
73
|
+
ensureCanonical(packageRoot, home);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const canonicalPath = targetFile(CANONICAL, home);
|
|
77
|
+
|
|
78
|
+
for (const t of requested) {
|
|
52
79
|
const dest = targetFile(t, home);
|
|
80
|
+
if (t === CANONICAL) {
|
|
81
|
+
if (dryRun) {
|
|
82
|
+
results.push({ agent: t, dir: dest, action: "would-write", exists: fs.existsSync(dest) });
|
|
83
|
+
} else {
|
|
84
|
+
results.push({ agent: t, dir: dest, action: "written", exists: true });
|
|
85
|
+
}
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
// 软链接:agents 实体 → 该目录的 SKILL.md
|
|
53
89
|
if (dryRun) {
|
|
54
|
-
results.push({
|
|
90
|
+
results.push({
|
|
91
|
+
agent: t,
|
|
92
|
+
dir: dest,
|
|
93
|
+
action: fs.existsSync(dest) ? "would-relink" : "would-link",
|
|
94
|
+
target: canonicalPath,
|
|
95
|
+
});
|
|
55
96
|
continue;
|
|
56
97
|
}
|
|
57
98
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
58
|
-
|
|
59
|
-
|
|
99
|
+
// 清理旧文件/链接
|
|
100
|
+
if (fs.existsSync(dest) || fs.lstatSync(dest, { throwIfNoEntry: false })) {
|
|
101
|
+
try { fs.unlinkSync(dest); } catch (_) {}
|
|
102
|
+
}
|
|
103
|
+
if (useSymlink) {
|
|
104
|
+
try {
|
|
105
|
+
fs.symlinkSync(canonicalPath, dest, "file");
|
|
106
|
+
results.push({ agent: t, dir: dest, action: "linked", exists: true, target: canonicalPath });
|
|
107
|
+
continue;
|
|
108
|
+
} catch (e) {
|
|
109
|
+
// Windows 普通用户缺少 SeCreateSymbolicLinkPrivilege;降级为复制
|
|
110
|
+
if (e.code !== "EPERM" && e.code !== "EACCES" && e.code !== "ENOTSUP") throw e;
|
|
111
|
+
// 继续走复制
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
fs.copyFileSync(canonicalPath, dest);
|
|
115
|
+
results.push({
|
|
116
|
+
agent: t,
|
|
117
|
+
dir: dest,
|
|
118
|
+
action: "copied",
|
|
119
|
+
exists: true,
|
|
120
|
+
target: canonicalPath,
|
|
121
|
+
fallback: true,
|
|
122
|
+
});
|
|
60
123
|
}
|
|
61
124
|
return results;
|
|
62
125
|
}
|
|
@@ -65,24 +128,36 @@ function uninstall(opts = {}) {
|
|
|
65
128
|
const agent = opts.agent || "all";
|
|
66
129
|
const dryRun = !!opts.dryRun;
|
|
67
130
|
const home = opts.home || os.homedir();
|
|
68
|
-
const
|
|
131
|
+
const requested = listTargets(agent);
|
|
69
132
|
const results = [];
|
|
70
|
-
|
|
133
|
+
|
|
134
|
+
for (const t of requested) {
|
|
71
135
|
const dest = targetFile(t, home);
|
|
72
|
-
const
|
|
73
|
-
if (!
|
|
136
|
+
const lstat = fs.lstatSync(dest, { throwIfNoEntry: false });
|
|
137
|
+
if (!lstat) {
|
|
74
138
|
results.push({ agent: t, dir: dest, action: "missing", exists: false });
|
|
75
139
|
continue;
|
|
76
140
|
}
|
|
77
141
|
if (dryRun) {
|
|
78
|
-
results.push({
|
|
142
|
+
results.push({
|
|
143
|
+
agent: t,
|
|
144
|
+
dir: dest,
|
|
145
|
+
action: lstat.isSymbolicLink() ? "would-unlink" : "would-delete",
|
|
146
|
+
exists: true,
|
|
147
|
+
});
|
|
79
148
|
continue;
|
|
80
149
|
}
|
|
81
150
|
fs.unlinkSync(dest);
|
|
82
|
-
|
|
83
|
-
|
|
151
|
+
const parent = path.dirname(dest);
|
|
152
|
+
try { fs.rmdirSync(parent); } catch (_) {}
|
|
153
|
+
results.push({
|
|
154
|
+
agent: t,
|
|
155
|
+
dir: dest,
|
|
156
|
+
action: lstat.isSymbolicLink() ? "unlinked" : "deleted",
|
|
157
|
+
exists: false,
|
|
158
|
+
});
|
|
84
159
|
}
|
|
85
160
|
return results;
|
|
86
161
|
}
|
|
87
162
|
|
|
88
|
-
module.exports = { install, uninstall, listTargets, targetFile, skillSrc, ALIAS_TO_GROUP, SKILL_NAME };
|
|
163
|
+
module.exports = { install, uninstall, listTargets, targetFile, skillSrc, ALIAS_TO_GROUP, RELATIVE_DIRS, SKILL_NAME, CANONICAL, LINK_TARGETS };
|