@topmindspace/topmind-skills 4.13.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.
Files changed (68) hide show
  1. package/INSTALL.md +295 -0
  2. package/LICENSE +21 -0
  3. package/README.en.md +162 -0
  4. package/README.md +162 -0
  5. package/README.zh-CN.md +162 -0
  6. package/bin/install-skills.mjs +693 -0
  7. package/evals/evals.json +236 -0
  8. package/install-targets/claude-code.json +62 -0
  9. package/install-targets/codex.json +60 -0
  10. package/install-targets/hermes.json +60 -0
  11. package/install-targets/opencode.json +63 -0
  12. package/integrations/codex/README.md +20 -0
  13. package/integrations/hermes/README.md +19 -0
  14. package/integrations/opencode/README.md +33 -0
  15. package/integrations/opencode/commands/topmind-capture.md +15 -0
  16. package/integrations/opencode/commands/topmind-doctor.md +11 -0
  17. package/integrations/opencode/opencode.example.json +20 -0
  18. package/integrations/opencode/plugins/topmind-plugin.ts +17 -0
  19. package/integrations/opencode/tests/opencode-config-shape.test.mjs +63 -0
  20. package/package.json +70 -0
  21. package/scripts/build-pack.mjs +463 -0
  22. package/shared/capability-degradation.md +82 -0
  23. package/shared/document-ingest.md +117 -0
  24. package/shared/host-loading.md +140 -0
  25. package/shared/long-url-capture.md +60 -0
  26. package/shared/output-language.md +32 -0
  27. package/shared/project-model-brief.md +132 -0
  28. package/shared/trigger-disambiguation.md +58 -0
  29. package/shared/writeback-receipt.md +73 -0
  30. package/skills.md +39 -0
  31. package/topmind/SKILL.md +170 -0
  32. package/topmind/references/connector-resolution.md +11 -0
  33. package/topmind/references/multi-intent.md +17 -0
  34. package/topmind/references/template-categories.md +45 -0
  35. package/topmind-capture/SKILL.md +148 -0
  36. package/topmind-ledger/SKILL.md +86 -0
  37. package/topmind-loop/DESIGN.md +82 -0
  38. package/topmind-loop/SKILL.md +111 -0
  39. package/topmind-loop/references/scopes-and-walk.md +56 -0
  40. package/topmind-loop/references/state-file.md +52 -0
  41. package/topmind-maintain/SKILL.md +104 -0
  42. package/topmind-memory/SKILL.md +117 -0
  43. package/topmind-organize/SKILL.md +144 -0
  44. package/topmind-pack.json +418 -0
  45. package/topmind-wechat/DESIGN.md +50 -0
  46. package/topmind-wechat/SKILL.md +218 -0
  47. package/topmind-wechat/assets/themes/amber-review.json +35 -0
  48. package/topmind-wechat/assets/themes/graphite.json +35 -0
  49. package/topmind-wechat/assets/themes/minimal-ink.json +35 -0
  50. package/topmind-wechat/assets/themes/newsprint.json +35 -0
  51. package/topmind-wechat/assets/themes/tech-blue.json +35 -0
  52. package/topmind-wechat/references/element-spec.md +162 -0
  53. package/topmind-wechat/references/known-pits.md +86 -0
  54. package/topmind-wechat/references/theme-map.md +139 -0
  55. package/topmind-wechat/references/typography-rules.md +62 -0
  56. package/topmind-wechat/references/wechat-constraints.md +106 -0
  57. package/topmind-wechat/references/workflow.md +102 -0
  58. package/topmind-wechat/references/writing-quality.md +129 -0
  59. package/topmind-wechat/scripts/lint-wechat.py +446 -0
  60. package/topmind-wechat/scripts/md2wechat.py +1599 -0
  61. package/topmind-wechat/scripts/new-article.py +183 -0
  62. package/topmind-wechat/scripts/push-to-topstream.py +226 -0
  63. package/topmind-wechat/scripts/scan_ai_flavor.py +464 -0
  64. package/topmind-wechat/scripts/sync-mapping.py +220 -0
  65. package/topmind-wechat/scripts/sync-status.py +187 -0
  66. package/topmind-weread/SKILL.md +97 -0
  67. package/topmind-write/SKILL.md +116 -0
  68. package/topmind-x/SKILL.md +97 -0
@@ -0,0 +1,63 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { promises as fs } from "node:fs";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+ const integrationRoot = path.resolve(__dirname, "..");
9
+ const repoRoot = path.resolve(integrationRoot, "..", "..");
10
+
11
+ test("OpenCode example config keeps a thin adapter shape", async () => {
12
+ const raw = await fs.readFile(path.join(integrationRoot, "opencode.example.json"), "utf8");
13
+ const config = JSON.parse(raw);
14
+
15
+ assert.ok(Array.isArray(config.skills.paths));
16
+ assert.ok(config.skills.paths.includes("${topmind_ENGINE_ROOT}/skills"));
17
+ assert.equal(config.mcp.topmind.type, "local");
18
+ assert.deepEqual(config.mcp.topmind.command, [
19
+ "node",
20
+ "${topmind_ENGINE_ROOT}/utr/server/topmind-mcp.mjs",
21
+ ]);
22
+ assert.deepEqual(config.plugin, [
23
+ "${topmind_ENGINE_ROOT}/integrations/opencode/plugins/topmind-plugin.ts",
24
+ ]);
25
+ assert.doesNotMatch(raw, /\/Users\/|\/home\/|~\//u);
26
+ });
27
+
28
+ test("OpenCode command templates exist for capture and doctor", async () => {
29
+ const capture = await fs.readFile(path.join(integrationRoot, "commands", "topmind-capture.md"), "utf8");
30
+ const doctor = await fs.readFile(path.join(integrationRoot, "commands", "topmind-doctor.md"), "utf8");
31
+
32
+ assert.match(capture, /capture -> auto route/u);
33
+ assert.match(capture, /workspace-write\.capture-note/u);
34
+ assert.match(doctor, /topmind-cli\.mjs doctor --json --mcp/u);
35
+ });
36
+
37
+ test("OpenCode plugin skeleton exports a topmind plugin", async () => {
38
+ const source = await fs.readFile(path.join(repoRoot, "integrations", "opencode", "plugins", "topmind-plugin.ts"), "utf8");
39
+
40
+ assert.match(source, /export default async function topmindPlugin/u);
41
+ assert.match(source, /name: "topmind"/u);
42
+ });
43
+
44
+ test("OpenCode adapter follows the portable skill-pack contract", async () => {
45
+ const [pack, target, readme, pluginSource] = await Promise.all([
46
+ fs.readFile(path.join(repoRoot, "topmind-pack.json"), "utf8").then(JSON.parse),
47
+ fs.readFile(path.join(repoRoot, "install-targets", "opencode.json"), "utf8").then(JSON.parse),
48
+ fs.readFile(path.join(integrationRoot, "README.md"), "utf8"),
49
+ fs.readFile(path.join(repoRoot, "integrations", "opencode", "plugins", "topmind-plugin.ts"), "utf8"),
50
+ ]);
51
+
52
+ assert.equal(pack.portable_contract.requires_desktop, false);
53
+ assert.equal(target.content_truth, pack.portable_contract.content_truth);
54
+ assert.ok(target.capabilities.includes("skills"));
55
+ assert.ok(target.capabilities.includes("mcp"));
56
+ assert.ok(target.capabilities.includes("plugin"));
57
+ assert.match(readme, /content truth/u);
58
+ assert.match(readme, /Expose only `topmind`|only daily.*`topmind`/iu);
59
+
60
+ assert.match(pluginSource, /contentTruth:\s*"topmind-workspace\/categories-and-topics"/u);
61
+ assert.match(pluginSource, /writesContent:\s*false/u);
62
+ assert.doesNotMatch(readme + "\n" + pluginSource, /should fork OpenCode|write topmind content directly:\s*true|writesContent:\s*true/u);
63
+ });
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@topmindspace/topmind-skills",
3
+ "version": "4.13.0",
4
+ "description": "topmind Agent Skills pack — portable markdown skills for capture, organize, write, memory, maintain, loop.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "TopMindSpace",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/topmindspace/topmind-skills.git"
11
+ },
12
+ "homepage": "https://github.com/topmindspace/topmind-skills#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/topmindspace/topmind-skills/issues"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public",
18
+ "registry": "https://registry.npmjs.org/"
19
+ },
20
+ "files": [
21
+ "bin/",
22
+ "scripts/",
23
+ "shared/",
24
+ "install-targets/",
25
+ "integrations/",
26
+ "evals/",
27
+ "topmind/",
28
+ "topmind-capture/",
29
+ "topmind-ledger/",
30
+ "topmind-loop/",
31
+ "topmind-maintain/",
32
+ "topmind-memory/",
33
+ "topmind-organize/",
34
+ "topmind-wechat/",
35
+ "topmind-weread/",
36
+ "topmind-write/",
37
+ "topmind-x/",
38
+ "topmind-pack.json",
39
+ "skills.md",
40
+ "INSTALL.md",
41
+ "README.md",
42
+ "README.zh-CN.md",
43
+ "README.en.md",
44
+ "LICENSE"
45
+ ],
46
+ "engines": {
47
+ "node": ">=20.11"
48
+ },
49
+ "bin": {
50
+ "topmind-skills": "bin/install-skills.mjs"
51
+ },
52
+ "scripts": {
53
+ "test": "node --test tests/*.test.mjs integrations/opencode/tests/*.test.mjs",
54
+ "pack": "node scripts/build-pack.mjs",
55
+ "prepublishOnly": "npm test",
56
+ "versions": "node -e \"console.log('skills', require('./topmind-pack.json').version)\"",
57
+ "add": "node bin/install-skills.mjs add",
58
+ "update": "node bin/install-skills.mjs update",
59
+ "list": "node bin/install-skills.mjs list"
60
+ },
61
+ "keywords": [
62
+ "agent-skills",
63
+ "skill-pack",
64
+ "topmind",
65
+ "personal-knowledge",
66
+ "claude-code",
67
+ "codex",
68
+ "opencode"
69
+ ]
70
+ }
@@ -0,0 +1,463 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * topmind Skill Pack — Build & Pack (v4.3+)
4
+ *
5
+ * Reads `skills/topmind-pack.json` (single source of truth) and assembles
6
+ * a portable distributable bundle under `dist/`.
7
+ *
8
+ * Outputs (per build) — surface prefix "skills" so users never confuse with Desktop:
9
+ * - dist/topmind-skills-<version>.tar.gz
10
+ * - dist/topmind-skills-<version>.zip
11
+ * - dist/topmind-skills-<version>-manifest.json
12
+ * - dist/topmind-skills-<version>.SHA256SUMS
13
+ *
14
+ * Bundle layout (top-level prefix is the bundle root):
15
+ *
16
+ * topmind-skills-<version>/
17
+ * ├── SKILL.md (router entry — Agent Skills root convention)
18
+ * ├── skills.md · LICENSE · README.md · topmind-pack.json
19
+ * ├── topmind/SKILL.md + references/ (router — daily entry, full copy)
20
+ * ├── topmind-capture|organize|…/SKILL.md (actions + optional connectors)
21
+ * ├── topmind-loop/DESIGN.md + references/ (when present)
22
+ * ├── install-targets/*.json
23
+ * ├── evals/evals.json
24
+ * └── shared/*.md (degradation, receipts, disambiguation, …)
25
+ *
26
+ * Why root SKILL.md? Many hosts unzip a pack and look for `SKILL.md` at the
27
+ * archive root (Agent Skills single-skill convention). topmind is multi-skill;
28
+ * root SKILL.md is the **router** (same as topmind/SKILL.md) so zip-as-one-skill
29
+ * loaders work. Full multi-skill install still uses subdirs + shared/.
30
+ *
31
+ * Excluded (dev-only): tests/
32
+ *
33
+ * Cross-platform: tar.gz uses `tar -czf` (GNU tar + bsdtar). zip tries `zip`
34
+ * first, then falls back to `tar -a -cf` (bsdtar on Windows 10+/macOS) so the
35
+ * build works on macOS / Linux / Windows without extra toolchain.
36
+ */
37
+
38
+ import { promises as fs } from "node:fs";
39
+ import path from "node:path";
40
+ import { fileURLToPath } from "node:url";
41
+ import { createHash } from "node:crypto";
42
+ import { spawn } from "node:child_process";
43
+
44
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
45
+ const repoRoot = path.resolve(__dirname, "..");
46
+ // After the topmind-skills split the pack root IS the repo root.
47
+ const skillsRoot = repoRoot;
48
+ const distRoot = path.join(repoRoot, "dist");
49
+
50
+ // ---- helpers --------------------------------------------------------------
51
+
52
+ function log(msg) {
53
+ process.stdout.write(`[build-pack] ${msg}\n`);
54
+ }
55
+
56
+ async function ensureDir(p) {
57
+ await fs.mkdir(p, { recursive: true });
58
+ }
59
+
60
+ function sha256File(buffer) {
61
+ return createHash("sha256").update(buffer).digest("hex");
62
+ }
63
+
64
+ function run(cmd, args, cwd) {
65
+ return new Promise((resolve, reject) => {
66
+ const child = spawn(cmd, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
67
+ let stderr = "";
68
+ child.stderr.on("data", (d) => (stderr += d.toString("utf8")));
69
+ child.on("error", reject);
70
+ child.on("exit", (code) => {
71
+ if (code === 0) resolve();
72
+ else reject(new Error(`${cmd} ${args.join(" ")} exited ${code}\n${stderr}`));
73
+ });
74
+ });
75
+ }
76
+
77
+ /**
78
+ * Like `run` but resolves to {ok} instead of rejecting. Used to probe
79
+ * whether an external command exists before falling back to alternatives
80
+ * (cross-platform: `zip` is absent on stock Windows, but `tar -a` works there).
81
+ */
82
+ function tryRun(cmd, args, cwd) {
83
+ return new Promise((resolve) => {
84
+ const child = spawn(cmd, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
85
+ let stderr = "";
86
+ child.stderr.on("data", (d) => (stderr += d.toString("utf8")));
87
+ child.on("error", () => resolve({ ok: false, error: `ENOENT: ${cmd}` }));
88
+ child.on("exit", (code) =>
89
+ resolve({ ok: code === 0, error: code === 0 ? null : `${cmd} exited ${code}\n${stderr}` }),
90
+ );
91
+ });
92
+ }
93
+
94
+ // ---- pack.json loader -----------------------------------------------------
95
+
96
+ async function loadPack() {
97
+ const raw = await fs.readFile(path.join(skillsRoot, "topmind-pack.json"), "utf8");
98
+ return JSON.parse(raw);
99
+ }
100
+
101
+ // ---- file enumeration -----------------------------------------------------
102
+
103
+ /**
104
+ * Walk `skills/` and return the relative paths to include in the bundle.
105
+ *
106
+ * Include:
107
+ * - skills/topmind-pack.json
108
+ * - skills/README.md
109
+ * - skills/LICENSE
110
+ * - skills/<skill>/SKILL.md
111
+ * - skills/<skill>/DESIGN.md (only if present — topmind-loop has one)
112
+ * - skills/<skill>/references/ tree (all .md; progressive disclosure resources)
113
+ * - skills/install-targets/*.json
114
+ * - skills/evals/evals.json
115
+ * - skills/shared/*.md
116
+ *
117
+ * Exclude:
118
+ * - skills/tests/**
119
+ * - any dotfile
120
+ */
121
+ async function collectBundleFiles() {
122
+ const include = new Set();
123
+
124
+ include.add("topmind-pack.json");
125
+ include.add("README.md");
126
+ include.add("LICENSE");
127
+ include.add("INSTALL.md");
128
+ // Package landing page (canonical lowercase — many agent platforms look for this)
129
+ include.add("skills.md");
130
+
131
+ for (const entry of await fs.readdir(skillsRoot, { withFileTypes: true })) {
132
+ if (!entry.isDirectory()) continue;
133
+ if (entry.name.startsWith(".")) continue;
134
+ if (["tests", "dist"].includes(entry.name)) continue;
135
+ if (["install-targets", "evals", "shared"].includes(entry.name)) continue;
136
+
137
+ const skillMd = path.join(skillsRoot, entry.name, "SKILL.md");
138
+ if (await fileExists(skillMd)) include.add(path.join(entry.name, "SKILL.md"));
139
+ const designMd = path.join(skillsRoot, entry.name, "DESIGN.md");
140
+ if (await fileExists(designMd)) include.add(path.join(entry.name, "DESIGN.md"));
141
+ // Progressive disclosure: skill-local references/
142
+ const refsDir = path.join(skillsRoot, entry.name, "references");
143
+ for (const rel of await listMarkdownFiles(refsDir, path.join(entry.name, "references"))) {
144
+ include.add(rel);
145
+ }
146
+ }
147
+
148
+ for (const f of await fs.readdir(path.join(skillsRoot, "install-targets"))) {
149
+ if (f.endsWith(".json")) include.add(path.join("install-targets", f));
150
+ }
151
+
152
+ include.add(path.join("evals", "evals.json"));
153
+
154
+ for (const f of await fs.readdir(path.join(skillsRoot, "shared"))) {
155
+ if (f.endsWith(".md")) include.add(path.join("shared", f));
156
+ }
157
+
158
+ return [...include].sort();
159
+ }
160
+
161
+ /** Recursively list *.md under dir as paths relative to skillsRoot. */
162
+ async function listMarkdownFiles(absDir, relPrefix) {
163
+ const out = [];
164
+ let entries;
165
+ try {
166
+ entries = await fs.readdir(absDir, { withFileTypes: true });
167
+ } catch {
168
+ return out;
169
+ }
170
+ for (const e of entries) {
171
+ if (e.name.startsWith(".")) continue;
172
+ const abs = path.join(absDir, e.name);
173
+ const rel = path.join(relPrefix, e.name);
174
+ if (e.isDirectory()) {
175
+ out.push(...(await listMarkdownFiles(abs, rel)));
176
+ } else if (e.isFile() && e.name.endsWith(".md")) {
177
+ out.push(rel);
178
+ }
179
+ }
180
+ return out;
181
+ }
182
+
183
+ async function fileExists(p) {
184
+ try {
185
+ const stat = await fs.stat(p);
186
+ return stat.isFile();
187
+ } catch {
188
+ return false;
189
+ }
190
+ }
191
+
192
+ // ---- staging --------------------------------------------------------------
193
+
194
+ /**
195
+ * Copy the selected files into a staging directory with a top-level prefix
196
+ * (`topmind-<version>/`). The system tar/zip will archive this directory
197
+ * verbatim to get the bundle layout.
198
+ *
199
+ * mtime is normalized to epoch (1970-01-01) so the resulting tar archive
200
+ * is byte-stable across rebuilds. zip includes file mtimes in its own
201
+ * structure, so we set them on the staged files via utimes() too.
202
+ */
203
+ async function stage(bundleRoot) {
204
+ await ensureDir(bundleRoot);
205
+ const files = await collectBundleFiles();
206
+ const epoch = new Date(0);
207
+ for (const rel of files) {
208
+ const src = path.join(skillsRoot, rel);
209
+ const dest = path.join(bundleRoot, rel);
210
+ await ensureDir(path.dirname(dest));
211
+ await fs.copyFile(src, dest);
212
+ await fs.utimes(dest, epoch, epoch);
213
+ }
214
+
215
+ // Emit an uppercase `SKILLS.md` mirror at the bundle root, but only on
216
+ // case-sensitive filesystems. On case-insensitive macOS / Windows the
217
+ // lowercase `skills.md` and uppercase `SKILLS.md` are the same file —
218
+ // creating both is impossible.
219
+ try {
220
+ const lower = path.join(bundleRoot, "skills.md");
221
+ const upper = path.join(bundleRoot, "SKILLS.md");
222
+ const lowerContent = await fs.readFile(lower, "utf8");
223
+ // Probe: write a unique sentinel to `SKILLS.md`. If the FS is
224
+ // case-insensitive, this overwrites `skills.md` too. We detect that by
225
+ // reading `skills.md` and seeing whether it now contains the sentinel.
226
+ const sentinel = `__probe_${Date.now()}__`;
227
+ await fs.writeFile(upper, sentinel);
228
+ const reread = await fs.readFile(lower, "utf8");
229
+ if (reread.trim() === sentinel) {
230
+ // Case-insensitive: cannot have both. Restore the canonical file.
231
+ await fs.writeFile(lower, lowerContent);
232
+ await fs.utimes(lower, epoch, epoch);
233
+ } else {
234
+ // Case-sensitive: write the real content as SKILLS.md.
235
+ await fs.writeFile(upper, lowerContent);
236
+ await fs.utimes(upper, epoch, epoch);
237
+ }
238
+ } catch {
239
+ // Probe failed — bundle has lowercase only, which is the canonical form.
240
+ }
241
+
242
+ // Root SKILL.md = router (Agent Skills zip-root convention).
243
+ // Many hosts fail with "SKILL.md not found" when loading multi-skill zips
244
+ // that only nest topmind/SKILL.md. Stage router at pack root as the entry.
245
+ const routerSrc = path.join(skillsRoot, "topmind", "SKILL.md");
246
+ const rootSkill = path.join(bundleRoot, "SKILL.md");
247
+ if (await fileExists(routerSrc)) {
248
+ let body = await fs.readFile(routerSrc, "utf8");
249
+ // Rewrite relative shared links: from topmind/ they are ../shared/;
250
+ // from pack root they must be shared/. Skill-local references/ → topmind/references/.
251
+ body = body.replace(/\(\.\.\/shared\//g, "(shared/");
252
+ body = body.replace(/degradation:\s*\.\.\/shared\//g, "degradation: shared/");
253
+ body = body.replace(/\((?:\.\.\/)?references\//g, "(topmind/references/");
254
+ body = body.replace(/\]\((?:\.\.\/)?references\//g, "](topmind/references/");
255
+ // Prefix a one-line note so hosts know this is a multi-skill pack entry.
256
+ if (!body.includes("<!-- topmind-pack-root-entry -->")) {
257
+ const note =
258
+ "<!-- topmind-pack-root-entry -->\n" +
259
+ "> **Pack root entry**: this `SKILL.md` is the topmind **router** " +
260
+ "(same as `topmind/SKILL.md`). Multi-skill install: load sibling " +
261
+ "`topmind-*/` dirs + `shared/`. See `skills.md` + `INSTALL.md`.\n\n";
262
+ // Insert after frontmatter if present
263
+ if (body.startsWith("---")) {
264
+ const end = body.indexOf("\n---", 3);
265
+ if (end !== -1) {
266
+ const after = end + 4; // past \n---
267
+ body = body.slice(0, after) + "\n" + note + body.slice(after).replace(/^\n*/, "");
268
+ } else {
269
+ body = note + body;
270
+ }
271
+ } else {
272
+ body = note + body;
273
+ }
274
+ }
275
+ await fs.writeFile(rootSkill, body);
276
+ await fs.utimes(rootSkill, epoch, epoch);
277
+ // Staged-only: not in skills/ source tree; archive includes it via bundleRoot walk.
278
+ // Manifest file list stays source-based (collectBundleFiles); SKILL.md is pack-entry glue.
279
+ }
280
+
281
+ return files;
282
+ }
283
+
284
+ // ---- archive builders -----------------------------------------------------
285
+
286
+ async function writeTarGz(tarPath, bundleRoot) {
287
+ // BSD tar on macOS, GNU tar on Linux: both support `tar -czf <out> -C <parent> <name>`.
288
+ // Use the parent dir as cwd so the archive contains exactly the top-level dir.
289
+ // Files have been mtime-normalized to epoch in stage(), so the archive is
290
+ // byte-stable across rebuilds.
291
+ const parent = path.dirname(bundleRoot);
292
+ const name = path.basename(bundleRoot);
293
+ await run("tar", ["-czf", tarPath, "-C", parent, name], parent);
294
+ }
295
+
296
+ async function writeZip(zipPath, bundleRoot) {
297
+ // Cross-platform zip generation.
298
+ // 1. `zip -r -q -X` — works on macOS / Linux (where `zip` is commonly installed)
299
+ // 2. `tar -a -cf` — bsdtar auto-format fallback (ships with Windows 10+ and macOS;
300
+ // the `-a` flag infers zip from the `.zip` extension)
301
+ // GNU tar on Linux does not create zip archives, so strategy 1 is tried first there.
302
+ const parent = path.dirname(bundleRoot);
303
+ const name = path.basename(bundleRoot);
304
+
305
+ const primary = await tryRun("zip", ["-r", "-q", "-X", zipPath, name], parent);
306
+ if (primary.ok) return;
307
+
308
+ const fallback = await tryRun("tar", ["-a", "-cf", zipPath, "-C", parent, name], parent);
309
+ if (fallback.ok) return;
310
+
311
+ throw new Error(
312
+ `zip generation failed on this platform.\n` +
313
+ ` tried: zip -r -q -X → ${primary.error}\n` +
314
+ ` tried: tar -a -cf → ${fallback.error}\n` +
315
+ `Install \`zip\` or ensure bsdtar (Windows 10+/macOS) is available on PATH.`,
316
+ );
317
+ }
318
+
319
+ // ---- manifest & checksums -------------------------------------------------
320
+
321
+ async function writeManifestAndSums(pack, tarPath, zipPath, files) {
322
+ const fileEntries = [];
323
+ for (const rel of files) {
324
+ const data = await fs.readFile(path.join(skillsRoot, rel));
325
+ fileEntries.push({
326
+ path: rel,
327
+ size: data.length,
328
+ sha256: sha256File(data),
329
+ });
330
+ }
331
+
332
+ const tarStat = await fs.stat(tarPath);
333
+ const zipStat = await fs.stat(zipPath);
334
+ const tarSha = sha256File(await fs.readFile(tarPath));
335
+ const zipSha = sha256File(await fs.readFile(zipPath));
336
+
337
+ const manifest = {
338
+ schema_version: 1,
339
+ name: pack.name,
340
+ label: pack.label,
341
+ version: pack.version,
342
+ updated: pack.updated,
343
+ daily_entry: pack.daily_entry,
344
+ metadata: pack.metadata,
345
+ skills: pack.skills.map((s) => ({ id: s.id, role: s.role, human_facing: s.human_facing })),
346
+ entry_files: pack.entry_files,
347
+ install_targets: pack.install_targets,
348
+ archive: {
349
+ tar_gz: {
350
+ path: path.basename(tarPath),
351
+ size: tarStat.size,
352
+ sha256: tarSha,
353
+ },
354
+ zip: {
355
+ path: path.basename(zipPath),
356
+ size: zipStat.size,
357
+ sha256: zipSha,
358
+ },
359
+ },
360
+ files: fileEntries,
361
+ total_size: fileEntries.reduce((acc, f) => acc + f.size, 0),
362
+ built_at: new Date().toISOString(),
363
+ built_by: "scripts/build-pack.mjs",
364
+ };
365
+
366
+ const manifestPath = path.join(distRoot, `topmind-skills-${pack.version}-manifest.json`);
367
+ await fs.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
368
+
369
+ const sumsPath = path.join(distRoot, `topmind-skills-${pack.version}.SHA256SUMS`);
370
+ const sumsText =
371
+ `${tarSha} ${path.basename(tarPath)}\n` +
372
+ `${zipSha} ${path.basename(zipPath)}\n`;
373
+ await fs.writeFile(sumsPath, sumsText);
374
+
375
+ return { manifestPath, sumsPath, manifest };
376
+ }
377
+
378
+ // ---- stale artifact cleanup -----------------------------------------------
379
+
380
+ /**
381
+ * Remove skills-pack artifacts that belong to a different version (or the
382
+ * legacy `topmind-<ver>.*` name without the "skills" surface prefix).
383
+ * Never touch extension (`topmind-clip-extension-*`) or other surfaces.
384
+ */
385
+ async function cleanStaleArtifacts(currentVersion) {
386
+ const skillsPrefix = "topmind-skills-";
387
+ const currentPrefix = `${skillsPrefix}${currentVersion}`;
388
+ // Legacy ambiguous name (pre surface-prefix rename)
389
+ const legacyRe = /^topmind-\d+\.\d+\.\d+/u;
390
+ let cleaned = 0;
391
+
392
+ try {
393
+ const entries = await fs.readdir(distRoot);
394
+ for (const entry of entries) {
395
+ const isCurrentSkills = entry.startsWith(currentPrefix);
396
+ const isOtherSkills =
397
+ entry.startsWith(skillsPrefix) && !isCurrentSkills;
398
+ const isLegacyAmbiguous =
399
+ legacyRe.test(entry) && !entry.startsWith("topmind-clip-extension-");
400
+ if (!isOtherSkills && !isLegacyAmbiguous) continue;
401
+ if (isCurrentSkills) continue;
402
+ await fs.rm(path.join(distRoot, entry), { force: true, recursive: true });
403
+ cleaned += 1;
404
+ }
405
+ } catch {
406
+ // dist/ might not exist yet — harmless
407
+ }
408
+
409
+ if (cleaned > 0) {
410
+ log(`cleaned ${cleaned} stale skills artifact(s) from previous versions`);
411
+ }
412
+ }
413
+
414
+ // ---- main -----------------------------------------------------------------
415
+
416
+ async function main() {
417
+ const pack = await loadPack();
418
+ const version = pack.version;
419
+ // Surface-prefixed name: never "topmind-<ver>" alone (looks like the whole product)
420
+ const bundleTop = `topmind-skills-${version}`;
421
+
422
+ await ensureDir(distRoot);
423
+ const stagingDir = path.join(distRoot, bundleTop);
424
+ // Idempotent rebuild — clean current version artifacts
425
+ await fs.rm(stagingDir, { recursive: true, force: true });
426
+ for (const ext of [".tar.gz", ".zip", "-manifest.json", ".SHA256SUMS"]) {
427
+ await fs.rm(path.join(distRoot, `${bundleTop}${ext}`), { force: true });
428
+ }
429
+
430
+ // Clean stale artifacts from previous versions to prevent accumulation
431
+ await cleanStaleArtifacts(version);
432
+
433
+ log(`version: ${version}`);
434
+ log(`staging files into ${bundleTop}/ ...`);
435
+ const files = await stage(stagingDir);
436
+
437
+ const tarPath = path.join(distRoot, `${bundleTop}.tar.gz`);
438
+ const zipPath = path.join(distRoot, `${bundleTop}.zip`);
439
+
440
+ log(`writing tar.gz ...`);
441
+ await writeTarGz(tarPath, stagingDir);
442
+ log(`writing zip ...`);
443
+ await writeZip(zipPath, stagingDir);
444
+
445
+ log(`writing manifest + SHA256SUMS ...`);
446
+ const { manifestPath, sumsPath, manifest } = await writeManifestAndSums(pack, tarPath, zipPath, files);
447
+
448
+ // Clean up staging
449
+ await fs.rm(stagingDir, { recursive: true, force: true });
450
+
451
+ const tarStat = await fs.stat(tarPath);
452
+ const zipStat = await fs.stat(zipPath);
453
+ log(`done.`);
454
+ log(` ${path.basename(tarPath)} (${tarStat.size} bytes)`);
455
+ log(` ${path.basename(zipPath)} (${zipStat.size} bytes)`);
456
+ log(` ${path.basename(manifestPath)} (${manifest.files.length} files, ${manifest.total_size} bytes uncompressed)`);
457
+ log(` ${path.basename(sumsPath)}`);
458
+ }
459
+
460
+ main().catch((err) => {
461
+ process.stderr.write(`[build-pack] error: ${err.stack || err.message || err}\n`);
462
+ process.exit(1);
463
+ });
@@ -0,0 +1,82 @@
1
+ # topmind Skills — Capability Degradation
2
+
3
+ > **共享降级表(唯一真源)**。各 SKILL.md frontmatter `degradation` 指向本文件。
4
+ > Skill 是纯 Markdown 指令集,**不依赖特定运行时**。主路径 = Host 文件工具 + `PROJECT-MODEL`;UTR 为可选加速。边界见 `PRODUCT-BOUNDARIES.md`。
5
+ > Host 如何三级加载 / 安装 `shared/`:见 [`host-loading.md`](./host-loading.md)。
6
+
7
+ ## 三级能力(主 → 次)
8
+
9
+ ```text
10
+ Level 1(主): Host 文件工具
11
+ → 直接读写工作区 Markdown / 目录
12
+ → 遵守 PROJECT-MODEL 路径与 frontmatter
13
+ → 写入返回路径回执
14
+
15
+ Level 2(可选): UTR CLI / MCP
16
+ → workspace-read.* / workspace-write.* 等确定性命令
17
+ → 适合批量、脚本、强校验
18
+
19
+ Level 3(最低): 仅对话
20
+ → 给出路径与步骤;不静默假装已写入
21
+ ```
22
+
23
+ **不要**把「先装 UTR」写成用户前提。
24
+
25
+ ## Level 1 — Host 文件工具
26
+
27
+ | 意图 | 操作要点 |
28
+ |------|----------|
29
+ | 看类别 | 扫描 `{NN}[- ]{Name}/` + 读 `topmind.yaml` v4(含 categories.extensions / overrides);跳过 hidden |
30
+ | 看专题 | 列出类别下专题目录 + 根层单篇 `.md` |
31
+ | 检视专题 | 读 `topic.md`(若有)+ 专题根 `.md` |
32
+ | 捕获 | 写带 frontmatter 的 `.md` 到专题根 / 大类根 / role:buffer |
33
+ | 交付 | 写到 role:delivery(常为 `88-交付/`) |
34
+ | 追加记忆 | 默认 `memory/profile.md`(主)或 `memory/periodic/{YYYY}/`(周期反思);**开专题**写内容大类 `{YYYY-主题}/`;仅用户明说才写 `memory/topics/{slug}.md` |
35
+ | 健康检查 | 遍历结构、报告缺失 / 垃圾;自定义类合法 |
36
+ | 归档 / 恢复 | 移动并写入 role:system(常为 `99-归档/`) |
37
+
38
+ 写回:`auto | confirm`(见 `TOOLS.md`)。
39
+
40
+ ## Level 2 — UTR(可选)
41
+
42
+ | 需要 | 命令 |
43
+ |------|------|
44
+ | 类别 / 专题 | `list-categories`(WorkspaceModel Descriptor)· `list-topics` · `inspect-topic` · `list-topic-files` · `list-inbox` |
45
+ | 捕获 / 创建 | `create-topic` · `capture-note` · `save-output` |
46
+ | 记忆 | `memory.append-profile` · `memory.append-topic` · `memory.promote` · `memory.digest` |
47
+ | Inbox 路由 | `plan-inbox-routing`(primary;整理 inbox 前规划) |
48
+ | 检查 / 维护 | `doctor-workspace` · `archive-topic` · `archive-stream-year` · `restore-safety-receipt` · `contract.reseed` |
49
+
50
+ 扩展面见 `TOOLS.md` §Current Command Surface。输入用独立字段 `category` + `topic`。
51
+
52
+ ## Level 3 — 对话 only
53
+
54
+ - 给出明确路径:`{workspace}/{类别}/{专题}/xxx.md`
55
+ - 描述可手动执行的步骤
56
+ - 可提示安装 UTR 获得脚本化能力(**非强制**)
57
+
58
+ ## 回执格式
59
+
60
+ ```text
61
+ 已收进/已更新:大类 → 专题(或单篇 / Inbox)
62
+ 位置:relative/path.md
63
+ 操作:create | update | delete | archive | restore
64
+ 保存模式:auto | confirm
65
+ 判断:信心与理由(如有)
66
+ 下一步:继续写 / 整理(活动窗口)/ 更新我的情况 / 建议开专题(内容大类)/ 手动移动
67
+ ```
68
+
69
+ 完整 write evidence 见 `TOOLS.md`。
70
+
71
+ ## Save Settings
72
+
73
+ ```yaml
74
+ writeback_mode: auto | confirm
75
+ ```
76
+
77
+ ## 与 Desktop
78
+
79
+ - Desktop **不是** Skills 运行时,也不是 UTR 的 GUI
80
+ - Desktop 内捕获 / 编辑 / AI 走 **WorkspaceService**
81
+ - 同一套 Skills 文案在任意 agent host 独立生效
82
+ - 四者只共享内容约定,不共享进程