@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.
- package/INSTALL.md +295 -0
- package/LICENSE +21 -0
- package/README.en.md +162 -0
- package/README.md +162 -0
- package/README.zh-CN.md +162 -0
- package/bin/install-skills.mjs +693 -0
- package/evals/evals.json +236 -0
- package/install-targets/claude-code.json +62 -0
- package/install-targets/codex.json +60 -0
- package/install-targets/hermes.json +60 -0
- package/install-targets/opencode.json +63 -0
- package/integrations/codex/README.md +20 -0
- package/integrations/hermes/README.md +19 -0
- package/integrations/opencode/README.md +33 -0
- package/integrations/opencode/commands/topmind-capture.md +15 -0
- package/integrations/opencode/commands/topmind-doctor.md +11 -0
- package/integrations/opencode/opencode.example.json +20 -0
- package/integrations/opencode/plugins/topmind-plugin.ts +17 -0
- package/integrations/opencode/tests/opencode-config-shape.test.mjs +63 -0
- package/package.json +70 -0
- package/scripts/build-pack.mjs +463 -0
- package/shared/capability-degradation.md +82 -0
- package/shared/document-ingest.md +117 -0
- package/shared/host-loading.md +140 -0
- package/shared/long-url-capture.md +60 -0
- package/shared/output-language.md +32 -0
- package/shared/project-model-brief.md +132 -0
- package/shared/trigger-disambiguation.md +58 -0
- package/shared/writeback-receipt.md +73 -0
- package/skills.md +39 -0
- package/topmind/SKILL.md +170 -0
- package/topmind/references/connector-resolution.md +11 -0
- package/topmind/references/multi-intent.md +17 -0
- package/topmind/references/template-categories.md +45 -0
- package/topmind-capture/SKILL.md +148 -0
- package/topmind-ledger/SKILL.md +86 -0
- package/topmind-loop/DESIGN.md +82 -0
- package/topmind-loop/SKILL.md +111 -0
- package/topmind-loop/references/scopes-and-walk.md +56 -0
- package/topmind-loop/references/state-file.md +52 -0
- package/topmind-maintain/SKILL.md +104 -0
- package/topmind-memory/SKILL.md +117 -0
- package/topmind-organize/SKILL.md +144 -0
- package/topmind-pack.json +418 -0
- package/topmind-wechat/DESIGN.md +50 -0
- package/topmind-wechat/SKILL.md +218 -0
- package/topmind-wechat/assets/themes/amber-review.json +35 -0
- package/topmind-wechat/assets/themes/graphite.json +35 -0
- package/topmind-wechat/assets/themes/minimal-ink.json +35 -0
- package/topmind-wechat/assets/themes/newsprint.json +35 -0
- package/topmind-wechat/assets/themes/tech-blue.json +35 -0
- package/topmind-wechat/references/element-spec.md +162 -0
- package/topmind-wechat/references/known-pits.md +86 -0
- package/topmind-wechat/references/theme-map.md +139 -0
- package/topmind-wechat/references/typography-rules.md +62 -0
- package/topmind-wechat/references/wechat-constraints.md +106 -0
- package/topmind-wechat/references/workflow.md +102 -0
- package/topmind-wechat/references/writing-quality.md +129 -0
- package/topmind-wechat/scripts/lint-wechat.py +446 -0
- package/topmind-wechat/scripts/md2wechat.py +1599 -0
- package/topmind-wechat/scripts/new-article.py +183 -0
- package/topmind-wechat/scripts/push-to-topstream.py +226 -0
- package/topmind-wechat/scripts/scan_ai_flavor.py +464 -0
- package/topmind-wechat/scripts/sync-mapping.py +220 -0
- package/topmind-wechat/scripts/sync-status.py +187 -0
- package/topmind-weread/SKILL.md +97 -0
- package/topmind-write/SKILL.md +116 -0
- 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
|
+
- 四者只共享内容约定,不共享进程
|