@topmindspace/topmind-writing-skills 0.9.0 → 0.9.2
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 +13 -0
- package/README.en.md +18 -11
- package/README.md +19 -10
- package/bin/topmind-writing-skills.js +178 -56
- package/lib/host-pack.cjs +11 -1
- package/package.json +2 -2
- package/runtime/article-renderer.d.mts +21 -0
- package/runtime/article-renderer.mjs +225 -4
- package/runtime/content-sha256.mjs +19 -0
- package/runtime/length-rules.d.mts +24 -0
- package/runtime/length-rules.mjs +262 -0
- package/runtime/length-ruleset.d.mts +13 -0
- package/runtime/length-ruleset.mjs +121 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
## [0.9.2] - 2026-10-31
|
|
2
|
+
|
|
3
|
+
- 宿主包运行时显式提供文章渲染、从源稿渲染、本地文本提取、长度评估和长度规则集评估。文章结果绑定所选稿与产物的 SHA-256,可选绑定创作快照指纹。
|
|
4
|
+
- 长度规则集只接受调用方传入的 JSON,先校验结构和日期再报告。本包不附带、也不启用真实平台规则。Unicode 码点上限不能代替 X 加权长度或编辑器里的最终核对。
|
|
5
|
+
- 八个技能的版本不变。
|
|
6
|
+
|
|
7
|
+
## [0.9.1] - 2026-10-28
|
|
8
|
+
|
|
9
|
+
- 安装器新增 `install all`、`update` 与 `status`:整套首次安装只需一条命令,后续更新只处理已装技能,状态直接显示安装版本与包快照。
|
|
10
|
+
- 默认目标优先识别真实安装与宿主配置;多个候选要求明确 `--to`,避免静默写入错误目录。
|
|
11
|
+
- 安装记录用于识别本地改动;受管副本的改动在自动更新时会阻止覆盖,显式强制更新与旧版未受管安装会保留旧副本。软链接和异名目录不再被替换。
|
|
12
|
+
- 根 README 中英文对齐新的上手、升级与本地安装流程。
|
|
13
|
+
|
|
1
14
|
## [0.9.0] - 2026-10-27
|
|
2
15
|
|
|
3
16
|
- 扩展 8 个技能的创作方向、完整示例和适用条件,保留事实、排版、保真与工具协议,清理统一审美门槛和旧规则冲突。
|
package/README.en.md
CHANGED
|
@@ -14,10 +14,12 @@ English | [中文](./README.md)
|
|
|
14
14
|
</p>
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
npx @topmindspace/topmind-writing-skills install
|
|
18
|
-
npx @topmindspace/topmind-writing-skills
|
|
17
|
+
npx @topmindspace/topmind-writing-skills install all # first-time setup
|
|
18
|
+
npx @topmindspace/topmind-writing-skills@latest update # later, update installed skills
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
Then tell your AI assistant "revise this", "write a quick brief", or "make a cover for this article". To install just one skill, use `install topmind-revise` or another ID from `list`. Desktop and Obsidian already include a reviewed writing snapshot; update it through each app's About & Updates screen rather than these npx commands.
|
|
22
|
+
|
|
21
23
|
Hosts start with the active note and allow explicit source changes. Drafts, a small read-only memory window and the selected skill are loaded in layers; other resources are retrieved as needed. Compaction does not reset completed work or budgets.
|
|
22
24
|
|
|
23
25
|
## Desktop and Obsidian integration
|
|
@@ -28,6 +30,8 @@ Open a Markdown document and choose **Writing & delivery** from the article tool
|
|
|
28
30
|
|
|
29
31
|
Built-in HTML/text typography requires no Python. Preview and copy use the same output; changed sources or images require regeneration. Diagnostics identify missing images, diagram source, extended containers and table paste limitations. AI images, PNG rendering and publishing require their respective tools. Copying does not publish; wording scores do not decide editorial quality.
|
|
30
32
|
|
|
33
|
+
The host pack also renders from source bytes, extracts local text from clipboard HTML, and evaluates a length rule set. The length entry accepts only caller-supplied JSON. This package does not ship or enable production platform rules. Fields, dates and reports are in [the length rule-set contract](docs/length-ruleset-v1.md). A Unicode code-point ceiling is not X weighted length and is not the target editor's final check. JavaScript and Python renderer differences are in [the capability matrix](docs/renderer-capability-matrix.md).
|
|
34
|
+
|
|
31
35
|
About & updates tracks the application, core skills, independent writing pack and integrations separately. Writing resources can update independently; compiled renderer code follows the host release. Managed packs are isolated, local modifications block replacement and failed installs preserve the previous version. Supported Desktop installs can download an update and explicitly restart; manual downloads remain available when signing or the installation channel prevents automatic updates.
|
|
32
36
|
|
|
33
37
|
## More choices, concrete references
|
|
@@ -240,15 +244,19 @@ Every skill drafts only; you publish.
|
|
|
240
244
|
**npm = pinned snapshot; GitHub = latest.**
|
|
241
245
|
|
|
242
246
|
```bash
|
|
247
|
+
npx @topmindspace/topmind-writing-skills install all --to ./.claude/skills
|
|
248
|
+
npx @topmindspace/topmind-writing-skills status --to ./.claude/skills
|
|
249
|
+
npx @topmindspace/topmind-writing-skills@latest update --to ./.claude/skills
|
|
243
250
|
npx @topmindspace/topmind-writing-skills install topmind-briefs --to ./.claude/skills
|
|
244
|
-
npx @topmindspace/topmind-writing-skills
|
|
245
|
-
npx @topmindspace/topmind-writing-skills install topmind-briefs --force # reinstall / downgrade
|
|
251
|
+
npx @topmindspace/topmind-writing-skills install topmind-briefs --force --to ./.claude/skills
|
|
246
252
|
npx @topmindspace/topmind-writing-skills uninstall topmind-briefs --to ./.claude/skills
|
|
247
|
-
npx github:topmindspace/topmind-writing-skills
|
|
253
|
+
npx github:topmindspace/topmind-writing-skills update --to ./.claude/skills
|
|
248
254
|
```
|
|
249
255
|
|
|
250
|
-
-
|
|
251
|
-
-
|
|
256
|
+
- `install all` installs the full set once; `update` changes only already installed skills, and `status` compares the package snapshot with a destination. All accept `--to`.
|
|
257
|
+
- Without `--to`, the CLI selects a single existing TopMind installation, skills directory, or host configuration. If several match, it lists them and asks for `--to`. With no host configured, it uses `./.agents/skills` in the current project. Output always names the destination.
|
|
258
|
+
- Replacements are staged first; failures keep the old copy. Local edits to a managed skill stop automatic updates until `--force` is explicit, which retains the previous copy. Upgrading an older untracked installation also retains a backup. Symlinks and unrelated directories are never replaced.
|
|
259
|
+
- Equal versions are left alone; downgrading a newer installed version needs `--force`. Reopen the AI session after updating so the host discovers new skills.
|
|
252
260
|
- Skill ids are not npm packages; don't `npm install topmind-briefs`.
|
|
253
261
|
|
|
254
262
|
### Upgrade from a local checkout
|
|
@@ -256,12 +264,11 @@ npx github:topmindspace/topmind-writing-skills install topmind-briefs # late
|
|
|
256
264
|
Install local, unpublished changes from the checkout; `@latest` contains only the published npm snapshot. Verify the target first. If tools share skills through symlinks, update the resolved shared directory and preserve those links. For a standalone Codex installation, run from the repository root:
|
|
257
265
|
|
|
258
266
|
```bash
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
done
|
|
267
|
+
node bin/topmind-writing-skills.js status --to "$HOME/.codex/skills"
|
|
268
|
+
node bin/topmind-writing-skills.js update --to "$HOME/.codex/skills"
|
|
262
269
|
```
|
|
263
270
|
|
|
264
|
-
Replace `--to` with the verified shared destination when appropriate
|
|
271
|
+
Use `install all` for the first install. Replace `--to` with the verified shared destination when appropriate. Unpublished changes with the same skill version require `install <id> --force --to <actual-dir>`. The CLI copies each skill's self-contained resources and never writes to a destination's root `shared/` directory.
|
|
265
272
|
|
|
266
273
|
<details>
|
|
267
274
|
<summary>Repository layout and releases</summary>
|
package/README.md
CHANGED
|
@@ -14,10 +14,12 @@
|
|
|
14
14
|
</p>
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
npx @topmindspace/topmind-writing-skills install
|
|
18
|
-
npx @topmindspace/topmind-writing-skills
|
|
17
|
+
npx @topmindspace/topmind-writing-skills install all # 首次安装整套
|
|
18
|
+
npx @topmindspace/topmind-writing-skills@latest update # 以后只更新已装技能
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
装好后直接对 AI 说「改这篇」「写条快讯」「给这篇做封面」。只想装一个,用 `install topmind-revise` 等技能 ID;`list` 查看全部。Desktop 和 Obsidian 已内置经过验证的写作快照,应用内更新从各自“关于与更新”进入,不需要再运行上面的 npx 命令。
|
|
22
|
+
|
|
21
23
|
宿主默认绑定活跃笔记,允许显式换稿。当前稿件、少量全局记忆和所选技能分层加载;其余资源按需读取,压缩不重置已完成操作和预算。
|
|
22
24
|
|
|
23
25
|
## 在 Desktop 与 Obsidian 中使用
|
|
@@ -28,6 +30,8 @@ npx @topmindspace/topmind-writing-skills list # 看全
|
|
|
28
30
|
|
|
29
31
|
内置 HTML/文本排版无需 Python,预览与复制同源;源稿或图片改变后需要重新生成。扩展容器、图表源码、缺图和平台表格粘贴限制会显示诊断。封面生图、PNG 和外部发布依赖实际工具,复制不等于已发布。审校不使用“AI 味分数”作为定稿门槛。
|
|
30
32
|
|
|
33
|
+
宿主包运行时另外提供从源稿生成文章结果、从剪贴板 HTML 提取本地文本,以及长度规则集评估。长度入口只接受调用方传入的 JSON,本包不附带、也不启用真实平台规则。规则字段、日期和报告见 [长度规则集运行契约](docs/length-ruleset-v1.md)。Unicode 码点上限不能代替 X 加权长度或目标编辑器里的最终核对。JavaScript 与 Python 渲染器的差异见 [能力矩阵](docs/renderer-capability-matrix.md)。
|
|
34
|
+
|
|
31
35
|
“关于与更新”分别展示 Desktop、核心技能、独立写作包和集成组件。写作资源可独立更新;编译进宿主的排版代码随宿主升级。受管包按身份隔离,覆盖本地定制会被阻止,失败保留旧版。Desktop 支持标准更新器下载后显式重启;签名、平台或渠道不支持时保留手动下载。
|
|
32
36
|
|
|
33
37
|
## 加选项,让经验帮助创作
|
|
@@ -258,15 +262,19 @@ python3 scripts/de_ai_lint.py 文章.md --genre long # 长文
|
|
|
258
262
|
**npm = 钉版本快照;GitHub = 跟仓库最新。**
|
|
259
263
|
|
|
260
264
|
```bash
|
|
265
|
+
npx @topmindspace/topmind-writing-skills install all --to ./.claude/skills
|
|
266
|
+
npx @topmindspace/topmind-writing-skills status --to ./.claude/skills
|
|
267
|
+
npx @topmindspace/topmind-writing-skills@latest update --to ./.claude/skills
|
|
261
268
|
npx @topmindspace/topmind-writing-skills install topmind-briefs --to ./.claude/skills
|
|
262
|
-
npx @topmindspace/topmind-writing-skills
|
|
263
|
-
npx @topmindspace/topmind-writing-skills install topmind-briefs --force # 同版本重装 / 降级
|
|
269
|
+
npx @topmindspace/topmind-writing-skills install topmind-briefs --force --to ./.claude/skills
|
|
264
270
|
npx @topmindspace/topmind-writing-skills uninstall topmind-briefs --to ./.claude/skills
|
|
265
|
-
npx github:topmindspace/topmind-writing-skills
|
|
271
|
+
npx github:topmindspace/topmind-writing-skills update --to ./.claude/skills
|
|
266
272
|
```
|
|
267
273
|
|
|
268
|
-
-
|
|
269
|
-
-
|
|
274
|
+
- `install all` 首次安装整套;`update` 默认只更新已装的技能,不会擅自加装新技能;`status` 显示当前包快照与安装目录的差异。三者都支持 `--to` 指定实际目录。
|
|
275
|
+
- 不写 `--to` 时先找唯一的 TopMind 安装,再找唯一的技能目录或宿主配置;若同时存在多个候选,会列出目录并要求明确选择。没有宿主配置时使用当前项目的 `./.agents/skills`。结果会打印实际目录。
|
|
276
|
+
- 更新先复制到临时目录,完整后才替换;失败保留旧版。受管安装发现本地改动会停止,需明确 `--force`;强制替换时保留旧副本。升级早期未带安装记录的副本也会自动留备份。软链接或非同名技能目录不会被覆盖。
|
|
277
|
+
- 同版本不重复复制;已装版本比包新时拒绝降级,除非显式 `--force`。升级后重新打开 AI 会话,让宿主重新发现技能。
|
|
270
278
|
- 技能 id 不是独立 npm 包,不要 `npm install topmind-briefs`。
|
|
271
279
|
|
|
272
280
|
### 从本地仓库升级
|
|
@@ -274,11 +282,12 @@ npx github:topmindspace/topmind-writing-skills install topmind-briefs # 跟
|
|
|
274
282
|
本地未发布改动要从当前仓库安装,`@latest` 只包含 npm 已发布版本。先核实目标目录;已有跨工具软链接时更新其共享实际目录,保留链接关系。Codex 独立安装可在仓库根目录运行:
|
|
275
283
|
|
|
276
284
|
```bash
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
done
|
|
285
|
+
node bin/topmind-writing-skills.js status --to "$HOME/.codex/skills"
|
|
286
|
+
node bin/topmind-writing-skills.js update --to "$HOME/.codex/skills"
|
|
280
287
|
```
|
|
281
288
|
|
|
289
|
+
首次安装改用 `install all`;修改过技能内容而未改版本号时,用 `install <id> --force --to <实际目录>` 明确重装。CLI 只复制单个技能目录内的独立资源,不写入目标根目录的 `shared/`。
|
|
290
|
+
|
|
282
291
|
若 Codex 技能链接到其他共享目录,将 `--to` 换为核实后的实际目录;检查每项安装输出。安装了新技能或更新入口后,重开会话让宿主重新发现;同版本未发布内容需要明确加 `--force` 才会重装。
|
|
283
292
|
|
|
284
293
|
<details>
|
|
@@ -17,11 +17,13 @@
|
|
|
17
17
|
|
|
18
18
|
const fs = require('fs');
|
|
19
19
|
const path = require('path');
|
|
20
|
+
const crypto = require('crypto');
|
|
20
21
|
|
|
21
22
|
const ROOT = path.resolve(__dirname, '..');
|
|
22
23
|
// Skills live at repo root (one directory per skill with SKILL.md).
|
|
23
24
|
const INFRA_DIRS = new Set(['bin', 'docs', 'scripts', 'shared', 'evals', 'node_modules', 'dist', 'release-assets']);
|
|
24
25
|
const SKILL_ID_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
26
|
+
const INSTALL_RECORD = '.topmind-install.json';
|
|
25
27
|
|
|
26
28
|
function die(msg, code = 1) {
|
|
27
29
|
console.error(msg);
|
|
@@ -113,7 +115,7 @@ function installerVersion() {
|
|
|
113
115
|
|
|
114
116
|
function resolveDefaultTarget() {
|
|
115
117
|
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
116
|
-
const candidates = [
|
|
118
|
+
const candidates = [...new Set([
|
|
117
119
|
path.join(process.cwd(), '.agents', 'skills'),
|
|
118
120
|
path.join(process.cwd(), '.claude', 'skills'),
|
|
119
121
|
path.join(process.cwd(), '.cursor', 'skills'),
|
|
@@ -123,25 +125,89 @@ function resolveDefaultTarget() {
|
|
|
123
125
|
path.join(home, '.agents', 'skills'),
|
|
124
126
|
path.join(home, '.cursor', 'skills'),
|
|
125
127
|
path.join(home, '.codex', 'skills'),
|
|
126
|
-
];
|
|
127
|
-
|
|
128
|
-
if (
|
|
129
|
-
|
|
130
|
-
|
|
128
|
+
])];
|
|
129
|
+
const choose = (paths) => {
|
|
130
|
+
if (paths.length === 1) return paths[0];
|
|
131
|
+
if (paths.length > 1) die(`More than one skill directory matches:\n ${paths.join('\n ')}\nChoose one with --to <dir>.`);
|
|
132
|
+
return null;
|
|
133
|
+
};
|
|
134
|
+
// Prefer the user's existing TopMind installation, then an existing skills
|
|
135
|
+
// directory, then a single host configuration. A mere cwd is not a signal.
|
|
136
|
+
const ids = listSkillIds();
|
|
137
|
+
const withTopmind = candidates.filter((dir) => ids.some((id) => fs.existsSync(path.join(dir, id, 'SKILL.md'))));
|
|
138
|
+
const withSkills = candidates.filter((dir) => fs.existsSync(dir));
|
|
139
|
+
const withHost = candidates.filter((dir) => fs.existsSync(path.dirname(dir)) && path.dirname(dir) !== process.cwd() && path.dirname(dir) !== home);
|
|
140
|
+
for (const group of [withTopmind, withSkills, withHost]) {
|
|
141
|
+
const selected = choose(group);
|
|
142
|
+
if (selected) return selected;
|
|
131
143
|
}
|
|
144
|
+
// No host exists yet: use the cross-agent project convention explicitly.
|
|
132
145
|
return path.join(process.cwd(), '.agents', 'skills');
|
|
133
146
|
}
|
|
134
147
|
|
|
148
|
+
function readSkillName(skillPath) {
|
|
149
|
+
try {
|
|
150
|
+
const text = fs.readFileSync(path.join(skillPath, 'SKILL.md'), 'utf8');
|
|
151
|
+
const fm = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
152
|
+
return (fm && fm[1].match(/^name:\s*([a-z0-9-]+)\s*$/m) || [])[1] || '';
|
|
153
|
+
} catch { return ''; }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function skipGeneratedEntry(entry) {
|
|
157
|
+
return (entry.isDirectory() && ['node_modules', 'dist', '__pycache__'].includes(entry.name)) ||
|
|
158
|
+
(entry.isFile() && entry.name.endsWith('.pyc'));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function skillDigest(skillPath) {
|
|
162
|
+
const hash = crypto.createHash('sha256');
|
|
163
|
+
const walk = (dir, relative = '') => {
|
|
164
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
165
|
+
if (!relative && entry.name === INSTALL_RECORD) continue;
|
|
166
|
+
if (skipGeneratedEntry(entry)) continue;
|
|
167
|
+
const rel = path.join(relative, entry.name);
|
|
168
|
+
const absolute = path.join(dir, entry.name);
|
|
169
|
+
if (entry.isDirectory()) {
|
|
170
|
+
walk(absolute, rel);
|
|
171
|
+
} else {
|
|
172
|
+
hash.update(rel.replace(/\\/g, '/') + '\0');
|
|
173
|
+
if (entry.isFile()) hash.update(fs.readFileSync(absolute));
|
|
174
|
+
else hash.update(`non-regular:${entry.name}`);
|
|
175
|
+
hash.update('\0');
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
walk(skillPath);
|
|
180
|
+
return hash.digest('hex');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function installedState(skillId, targetRoot) {
|
|
184
|
+
const dest = path.join(targetRoot, skillId);
|
|
185
|
+
let stat;
|
|
186
|
+
try { stat = fs.lstatSync(dest); } catch (error) {
|
|
187
|
+
if (error.code === 'ENOENT' || error.code === 'ENOTDIR') return { kind: 'missing', version: '' };
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
if (!stat.isDirectory() || readSkillName(dest) !== skillId) return { kind: 'conflict', version: '' };
|
|
191
|
+
const version = readSkillVersion(dest);
|
|
192
|
+
const recordPath = path.join(dest, INSTALL_RECORD);
|
|
193
|
+
if (!fs.existsSync(recordPath)) return { kind: 'legacy', version };
|
|
194
|
+
try {
|
|
195
|
+
const record = JSON.parse(fs.readFileSync(recordPath, 'utf8'));
|
|
196
|
+
if (record.id === skillId && record.digest === skillDigest(dest)) return { kind: 'managed', version };
|
|
197
|
+
} catch { /* Treat unreadable records as local changes. */ }
|
|
198
|
+
return { kind: 'modified', version };
|
|
199
|
+
}
|
|
200
|
+
|
|
135
201
|
function copyDir(src, dest) {
|
|
136
202
|
fs.mkdirSync(dest, { recursive: true });
|
|
137
203
|
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
138
204
|
const s = path.join(src, entry.name);
|
|
139
205
|
const d = path.join(dest, entry.name);
|
|
140
206
|
if (entry.isDirectory()) {
|
|
141
|
-
if (entry
|
|
207
|
+
if (skipGeneratedEntry(entry)) continue;
|
|
142
208
|
copyDir(s, d);
|
|
143
209
|
} else if (entry.isFile()) {
|
|
144
|
-
if (entry
|
|
210
|
+
if (skipGeneratedEntry(entry)) continue;
|
|
145
211
|
fs.copyFileSync(s, d);
|
|
146
212
|
} else {
|
|
147
213
|
console.warn(`warning: skipping non-regular file: ${s}`);
|
|
@@ -207,7 +273,7 @@ function compareVersions(a, b) {
|
|
|
207
273
|
return 0;
|
|
208
274
|
}
|
|
209
275
|
|
|
210
|
-
function
|
|
276
|
+
function planInstall(skillId, targetRoot, force) {
|
|
211
277
|
assertSkillId(skillId);
|
|
212
278
|
const src = skillDir(skillId);
|
|
213
279
|
if (!fs.existsSync(path.join(src, 'SKILL.md'))) {
|
|
@@ -215,6 +281,26 @@ function install(skillId, targetRoot, { force = false, defaultTarget = false } =
|
|
|
215
281
|
}
|
|
216
282
|
const dest = path.join(targetRoot, skillId);
|
|
217
283
|
assertDestOutsideSource(src, dest);
|
|
284
|
+
const state = installedState(skillId, targetRoot);
|
|
285
|
+
if (state.kind === 'conflict') die(`Refusing to replace ${dest}: it is not a ${skillId} skill directory.`);
|
|
286
|
+
if (state.kind === 'modified' && !force) {
|
|
287
|
+
die(`Local changes found in ${dest}. Keep them or pass --force to replace; the previous copy will be retained.`);
|
|
288
|
+
}
|
|
289
|
+
const packageVer = readSkillVersion(src) || 'unknown';
|
|
290
|
+
const cmp = compareVersions(packageVer, state.version);
|
|
291
|
+
if (state.kind !== 'missing' && !force && cmp < 0) {
|
|
292
|
+
die(`Installed ${skillId} ${state.version} is newer than packaged ${packageVer}; not downgrading. Pass --force (or -f) to downgrade.`);
|
|
293
|
+
}
|
|
294
|
+
return { src, dest, state, packageVer, cmp };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function install(skillId, targetRoot, { force = false, defaultTarget = false } = {}) {
|
|
298
|
+
const { src, dest, state, cmp } = planInstall(skillId, targetRoot, force);
|
|
299
|
+
if (state.kind !== 'missing' && !force && cmp === 0) {
|
|
300
|
+
console.log(`Already up to date: ${skillId} ${state.version} (use --force to reinstall)`);
|
|
301
|
+
console.log(` at: ${dest}`);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
218
304
|
try {
|
|
219
305
|
fs.mkdirSync(targetRoot, { recursive: true });
|
|
220
306
|
} catch (e) {
|
|
@@ -224,37 +310,28 @@ function install(skillId, targetRoot, { force = false, defaultTarget = false } =
|
|
|
224
310
|
'Choose a --to directory that can be created (not a file, not unwritable).'
|
|
225
311
|
);
|
|
226
312
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
// 已装过:默认升级。打包版本更新 → 直接覆盖(不备份);相同 → 已是最新;已装版本更新 → 需 --force。
|
|
230
|
-
const installedVer = readSkillVersion(dest);
|
|
231
|
-
const packageVer = readSkillVersion(src) || 'unknown';
|
|
232
|
-
const cmp = compareVersions(packageVer, installedVer);
|
|
233
|
-
if (!force && cmp === 0) {
|
|
234
|
-
console.log(`Already up to date: ${skillId} ${installedVer} (use --force to reinstall)`);
|
|
235
|
-
console.log(` at: ${dest}`);
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
if (!force && cmp < 0) {
|
|
239
|
-
die(
|
|
240
|
-
`Installed ${skillId} ${installedVer} is newer than packaged ${packageVer}; not downgrading. ` +
|
|
241
|
-
'Pass --force (or -f) to downgrade.'
|
|
242
|
-
);
|
|
243
|
-
}
|
|
244
|
-
upgradedFrom = cmp > 0 ? installedVer || 'unknown' : null;
|
|
245
|
-
}
|
|
313
|
+
const upgradedFrom = state.kind !== 'missing' && cmp > 0 ? state.version || 'unknown' : null;
|
|
314
|
+
const retainPrevious = state.kind === 'legacy' || state.kind === 'modified';
|
|
246
315
|
const stage = fs.mkdtempSync(path.join(targetRoot, `.${skillId}-stage-`));
|
|
247
316
|
let previous = null;
|
|
248
317
|
try {
|
|
249
318
|
copyDir(src, stage);
|
|
250
|
-
|
|
319
|
+
fs.writeFileSync(path.join(stage, INSTALL_RECORD), JSON.stringify({
|
|
320
|
+
id: skillId,
|
|
321
|
+
version: readSkillVersion(src),
|
|
322
|
+
digest: skillDigest(stage),
|
|
323
|
+
}, null, 2) + '\n');
|
|
324
|
+
if (state.kind !== 'missing') {
|
|
251
325
|
previous = fs.mkdtempSync(path.join(targetRoot, `.${skillId}-previous-`));
|
|
252
326
|
fs.rmdirSync(previous);
|
|
253
327
|
fs.renameSync(dest, previous);
|
|
254
328
|
}
|
|
255
329
|
try { fs.renameSync(stage, dest); }
|
|
256
330
|
catch (error) { if (previous) { fs.renameSync(previous, dest); previous = null; } throw error; }
|
|
257
|
-
if (previous
|
|
331
|
+
if (previous && !retainPrevious) {
|
|
332
|
+
try { fs.rmSync(previous, { recursive: true, force: true }); }
|
|
333
|
+
catch { console.warn(`Previous copy retained at: ${previous}`); }
|
|
334
|
+
}
|
|
258
335
|
} catch (error) {
|
|
259
336
|
fs.rmSync(stage, { recursive: true, force: true });
|
|
260
337
|
die(`Install failed; the previous installed skill was retained. ${error.message}`);
|
|
@@ -266,9 +343,25 @@ function install(skillId, targetRoot, { force = false, defaultTarget = false } =
|
|
|
266
343
|
else console.log(`Installed ${skillId}`);
|
|
267
344
|
console.log(` to: ${dest}${toNote}`);
|
|
268
345
|
console.log(` version: skill ${skillVer} (installer ${installerVersion()})`);
|
|
346
|
+
if (previous && retainPrevious) console.log(` backup: ${previous} (previous untracked or locally changed copy)`);
|
|
269
347
|
console.log(` next: ${nextStep(skillId, dest)}`);
|
|
270
348
|
}
|
|
271
349
|
|
|
350
|
+
function status(targetRoot) {
|
|
351
|
+
console.log(`Skill directory: ${targetRoot}`);
|
|
352
|
+
console.log('Packaged version | Installed version | State | Skill');
|
|
353
|
+
for (const id of listSkillIds()) {
|
|
354
|
+
const packaged = readSkillVersion(skillDir(id)) || 'unknown';
|
|
355
|
+
const state = installedState(id, targetRoot);
|
|
356
|
+
let label = state.kind;
|
|
357
|
+
if (state.kind === 'managed' || state.kind === 'legacy') {
|
|
358
|
+
const cmp = compareVersions(packaged, state.version);
|
|
359
|
+
label = cmp > 0 ? 'update available' : cmp < 0 ? 'installed newer' : state.kind === 'legacy' ? 'untracked' : 'current';
|
|
360
|
+
}
|
|
361
|
+
console.log(`${packaged} | ${state.version || '—'} | ${label} | ${id}`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
272
365
|
function nextStep(skillId, dest) {
|
|
273
366
|
return 'restart your agent so it picks up the skill';
|
|
274
367
|
}
|
|
@@ -319,22 +412,25 @@ function usageText() {
|
|
|
319
412
|
|
|
320
413
|
Usage:
|
|
321
414
|
topmind-writing-skills list|ls
|
|
322
|
-
topmind-writing-skills
|
|
415
|
+
topmind-writing-skills status [--to|-t <dir>]
|
|
416
|
+
topmind-writing-skills install <skill-id|all> [--to|-t <dir>] [--force|-f]
|
|
417
|
+
topmind-writing-skills update [skill-id|all] [--to|-t <dir>] [--force|-f]
|
|
323
418
|
topmind-writing-skills uninstall <skill-id> [--to|-t <dir>]
|
|
324
419
|
topmind-writing-skills help
|
|
325
420
|
|
|
326
421
|
Behavior:
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
422
|
+
status compares the local package with skills installed in <dir>.
|
|
423
|
+
install installs one skill or all eight; newer packaged versions upgrade
|
|
424
|
+
existing copies. Equal versions are left alone. A newer installed
|
|
425
|
+
version requires --force. Locally changed managed copies are kept
|
|
426
|
+
unless --force is explicit. Untracked older copies are backed up
|
|
427
|
+
during upgrade. The replacement is staged before switching.
|
|
428
|
+
update upgrades only installed skills (all by default); does not add new ones.
|
|
334
429
|
uninstall removes <dir>/<skill-id>/ (refuses: not installed, not a skill
|
|
335
430
|
directory, or the package's own skill source).
|
|
336
|
-
No --to:
|
|
337
|
-
|
|
431
|
+
No --to: choose a single existing TopMind installation, skills directory or
|
|
432
|
+
host configuration. Ambiguous matches require --to. With no host configured,
|
|
433
|
+
use ./.agents/skills and print the chosen location.
|
|
338
434
|
|
|
339
435
|
Target probe order (no --to): ./.agents/skills, ./.claude/skills,
|
|
340
436
|
./.cursor/skills, ./.codex/skills, ./.mimocode/skills, then
|
|
@@ -342,6 +438,9 @@ Target probe order (no --to): ./.agents/skills, ./.claude/skills,
|
|
|
342
438
|
|
|
343
439
|
Examples (npm recommended when published):
|
|
344
440
|
npx @topmindspace/topmind-writing-skills list
|
|
441
|
+
npx @topmindspace/topmind-writing-skills status
|
|
442
|
+
npx @topmindspace/topmind-writing-skills install all
|
|
443
|
+
npx @topmindspace/topmind-writing-skills@latest update
|
|
345
444
|
npx @topmindspace/topmind-writing-skills install topmind-briefs
|
|
346
445
|
npx @topmindspace/topmind-writing-skills install topmind-briefs --to ./.agents/skills
|
|
347
446
|
npx @topmindspace/topmind-writing-skills install topmind-briefs --force
|
|
@@ -358,6 +457,22 @@ function usage() {
|
|
|
358
457
|
console.log(usageText());
|
|
359
458
|
}
|
|
360
459
|
|
|
460
|
+
function parseOptions(args, start, { allowForce = false } = {}) {
|
|
461
|
+
let to = null;
|
|
462
|
+
let force = false;
|
|
463
|
+
for (let i = start; i < args.length; i++) {
|
|
464
|
+
if (args[i] === '--to' || args[i] === '-t') {
|
|
465
|
+
to = args[++i];
|
|
466
|
+
if (!to || to.startsWith('-')) die('Option --to requires a directory path.');
|
|
467
|
+
} else if (allowForce && (args[i] === '--force' || args[i] === '-f')) {
|
|
468
|
+
force = true;
|
|
469
|
+
} else {
|
|
470
|
+
die(`Unknown option: ${args[i]}\n\n` + usageText());
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return { targetRoot: to ? path.resolve(to) : resolveDefaultTarget(), force, defaultTarget: !to };
|
|
474
|
+
}
|
|
475
|
+
|
|
361
476
|
function main(argv) {
|
|
362
477
|
const args = argv.slice(2);
|
|
363
478
|
const cmd = args[0];
|
|
@@ -381,25 +496,32 @@ function main(argv) {
|
|
|
381
496
|
return;
|
|
382
497
|
}
|
|
383
498
|
|
|
384
|
-
if (cmd === '
|
|
385
|
-
const
|
|
386
|
-
|
|
499
|
+
if (cmd === 'status') {
|
|
500
|
+
const { targetRoot } = parseOptions(args, 1);
|
|
501
|
+
status(targetRoot);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (cmd === 'install' || cmd === 'update') {
|
|
506
|
+
const requested = cmd === 'update' && (!args[1] || args[1].startsWith('-')) ? 'all' : args[1];
|
|
507
|
+
if (!requested || requested.startsWith('-')) {
|
|
387
508
|
die('Missing skill id.\n\n' + usageText());
|
|
388
509
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
die(`Unknown option: ${args[i]}\n\n` + usageText());
|
|
399
|
-
}
|
|
510
|
+
const optionStart = cmd === 'update' && requested === 'all' && args[1] !== 'all' ? 1 : 2;
|
|
511
|
+
const options = parseOptions(args, optionStart, { allowForce: true });
|
|
512
|
+
const ids = requested === 'all' ? listSkillIds() : [requested];
|
|
513
|
+
const selected = cmd === 'update'
|
|
514
|
+
? ids.filter(id => installedState(id, options.targetRoot).kind !== 'missing')
|
|
515
|
+
: ids;
|
|
516
|
+
if (!selected.length) {
|
|
517
|
+
console.log(`No TopMind writing skills installed in ${options.targetRoot}. Run install all to get started.`);
|
|
518
|
+
return;
|
|
400
519
|
}
|
|
401
|
-
|
|
402
|
-
|
|
520
|
+
// Check every target before the first write; a conflict should not leave
|
|
521
|
+
// an avoidably half-updated set.
|
|
522
|
+
for (const id of selected) planInstall(id, options.targetRoot, options.force);
|
|
523
|
+
for (const id of selected) install(id, options.targetRoot, options);
|
|
524
|
+
if (selected.length > 1) console.log(`${cmd === 'install' ? 'Installed' : 'Updated'} ${selected.length} writing skills in ${options.targetRoot}.`);
|
|
403
525
|
return;
|
|
404
526
|
}
|
|
405
527
|
|
package/lib/host-pack.cjs
CHANGED
|
@@ -28,7 +28,17 @@ function buildHostPack(root) {
|
|
|
28
28
|
if (!/^---\r?\n/u.test(raw) || !/^name:\s*/mu.test(raw)) throw new Error(`Invalid skill: ${id}`);
|
|
29
29
|
return { id, name: id, description: meta.description || id, version: meta.version, packId: 'topmind-writing-skills' };
|
|
30
30
|
});
|
|
31
|
-
const
|
|
31
|
+
const capabilities = [
|
|
32
|
+
{ id: 'article.render', entry: 'runtime/article-renderer.mjs', export: 'prepareArticleResult' },
|
|
33
|
+
{ id: 'article.renderFromSource', entry: 'runtime/article-renderer.mjs', export: 'prepareArticleResultFromSource' },
|
|
34
|
+
{ id: 'article.localTextDocument', entry: 'runtime/article-renderer.mjs', export: 'articleLocalTextDocument' },
|
|
35
|
+
{ id: 'length.evaluate', entry: 'runtime/length-rules.mjs', export: 'evaluateLengthRules' },
|
|
36
|
+
{ id: 'length.ruleset.evaluate', entry: 'runtime/length-ruleset.mjs', export: 'evaluateLengthRuleSet' },
|
|
37
|
+
];
|
|
38
|
+
for (const capability of capabilities) {
|
|
39
|
+
if (!Object.hasOwn(files, capability.entry)) throw new Error(`Missing runtime capability entry: ${capability.entry}`);
|
|
40
|
+
}
|
|
41
|
+
const manifest = { schema_version: 1, name: 'topmind-writing-skills', version: pkg.version, license: pkg.license, repository: pkg.repository, entry_files: ids.map(id => `${id}/SKILL.md`), files, runtime: { api: 1, entry: 'runtime/article-renderer.mjs', capabilities, update: 'host-release' } };
|
|
32
42
|
const digest = hash(JSON.stringify(manifest));
|
|
33
43
|
return { manifest: { ...manifest, digest }, snapshot: { id: manifest.name, version: pkg.version, source: 'https://github.com/topmindspace/topmind-writing-skills', license: pkg.license, digest, skills, resources } };
|
|
34
44
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@topmindspace/topmind-writing-skills",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"description": "TopMindSpace 写作技能合集 — 通过 npm / npx 安装技能(公众号长文、X 长文、干货短文、X 引流短帖、改稿与质检、封面图、长图海报)。只出稿与发布包,不代发。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "TopMindspace",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"check:pack": "node scripts/check_npm_pack.js",
|
|
77
77
|
"validate:spec": "bash -c 'for s in $(node scripts/discover_skills.js); do ${AGENTSKILLS:-agentskills} validate \"$s\" || exit 1; done'",
|
|
78
78
|
"prepack": "node scripts/sync_npm_files.js --check && node scripts/sync_shared.js --check && python3 shared/scripts/knowledge.py validate",
|
|
79
|
-
"install:skills": "node
|
|
79
|
+
"install:skills": "node bin/topmind-writing-skills.js install all",
|
|
80
80
|
"check": "node scripts/run_skill_gates.js check",
|
|
81
81
|
"privacy": "python3 scripts/ci_privacy_scan.py",
|
|
82
82
|
"audit": "node scripts/run_skill_gates.js audit",
|
|
@@ -1,9 +1,30 @@
|
|
|
1
1
|
export type WritingPlatform = 'wechat' | 'x' | 'text';
|
|
2
2
|
export type WritingTheme = 'minimal-ink' | 'tech-blue' | 'newsprint' | 'graphite';
|
|
3
3
|
export const WRITING_THEMES: Record<WritingTheme, { ink: string; accent: string; paper: string; muted: string }>;
|
|
4
|
+
export const ARTICLE_RENDERER_PROVIDER: Readonly<{ id: string; version: string }>;
|
|
4
5
|
export const WRITING_ACTIONS: ReadonlyArray<{ id: string; zh: string; en: string; prompt: string; promptEn: string }>;
|
|
5
6
|
export function stripFrontmatter(value: string): string;
|
|
6
7
|
export interface ArticleDelivery { platform: WritingPlatform; theme: WritingTheme; html: string; body: string; text: string; checklist: string; images: Array<{ source: string; alt: string; status: string }>; diagnostics: Array<{ code: string; source?: string }> }
|
|
7
8
|
export function renderArticle(input: { markdown: string; platform?: WritingPlatform; theme?: WritingTheme; title?: string; images?: Record<string, string> }): ArticleDelivery;
|
|
8
9
|
export function resolveArticleImage(sourcePath: string, reference: string): string;
|
|
9
10
|
export function prepareArticle(input: { sourcePath: string; markdown: string; platform?: WritingPlatform; theme?: WritingTheme; title?: string }, readImage: (relative: string) => Promise<string>): Promise<ArticleDelivery>;
|
|
11
|
+
export interface ArticleResult {
|
|
12
|
+
schemaVersion: 1;
|
|
13
|
+
capabilityId: 'article.render';
|
|
14
|
+
provider: Readonly<{ id: string; version: string }>;
|
|
15
|
+
status: 'ok' | 'warning' | 'error';
|
|
16
|
+
input: { fingerprint: string | null; target?: { platform: WritingPlatform; theme: WritingTheme };
|
|
17
|
+
source?: { relativePath: string; sha256: string };
|
|
18
|
+
imageBytes?: Array<{ relativePath: string; sha256: string }>;
|
|
19
|
+
creationSnapshotFingerprint?: string; localTextTitle?: string | null;
|
|
20
|
+
renderParameters?: { theme: WritingTheme; title: string } };
|
|
21
|
+
artifacts: Array<{ role: 'preview' | 'clipboard-html' | 'source-markdown' | 'image-checklist' | 'local-clipboard-text'; format: string; content: string; sha256: string }>;
|
|
22
|
+
images: ArticleDelivery['images'];
|
|
23
|
+
diagnostics: Array<{ code: string; severity: 'warning' | 'error'; message: string; location?: { source: string } }>;
|
|
24
|
+
}
|
|
25
|
+
export function renderArticleResult(input?: { markdown?: string; platform?: WritingPlatform; theme?: WritingTheme; title?: string; sourcePath?: string; images?: Record<string, string> }): Promise<ArticleResult>;
|
|
26
|
+
export function prepareArticleResult(input: { sourcePath: string; markdown: string; platform?: WritingPlatform; theme?: WritingTheme; title?: string }, readImage: (relative: string) => Promise<string>): Promise<ArticleResult>;
|
|
27
|
+
export function prepareArticleResultFromSource(input: { sourcePath: string; sourceBytes: Uint8Array;
|
|
28
|
+
platform?: WritingPlatform; theme?: WritingTheme; title?: string; creationSnapshotFingerprint?: string },
|
|
29
|
+
readImage: (relative: string) => Promise<string>): Promise<ArticleResult>;
|
|
30
|
+
export function articleLocalTextDocument(result: ArticleResult): { text_kind: 'platform_text'; title?: string; body: string } | null;
|