agentwheel 0.4.1 → 0.5.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 (3) hide show
  1. package/README.md +25 -2
  2. package/dist/index.js +122 -29
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -33,9 +33,9 @@ No lock-in. No central gatekeeper. Your packages live in plain git repos, your c
33
33
 
34
34
  ---
35
35
 
36
- > **Status: early (v0.4).** The lifecycle core is real and tested — local/git/skillkit/vercel
36
+ > **Status: early (v0.5).** The lifecycle core is real and tested — local/git/skillkit/vercel
37
37
  > sources, optional registry discovery, plan/sync/update/drift/uninstall, overlays, eject/remember,
38
- > profiles, runtime auto-detection, fleet targeting, rich JSON merge, and pluggable adapters.
38
+ > profiles, runtime auto-detection, fleet targeting, asset-includes, rich JSON merge, and pluggable adapters.
39
39
  > Expect sharp edges.
40
40
 
41
41
  ## What it does
@@ -147,6 +147,28 @@ A package is a git repo (or folder) with a JSON manifest and a canonical layout:
147
147
  }
148
148
  ```
149
149
 
150
+ Packages can compose shared files into each directory artifact at staging time. This keeps one
151
+ canonical copy in the package repo while installing self-contained skills:
152
+
153
+ ```jsonc
154
+ {
155
+ "type": "skills",
156
+ "path": "skills",
157
+ "assets": [
158
+ {
159
+ "from": "packages/tmux-bridge/bin",
160
+ "into": "bin",
161
+ "include": ["*.sh"],
162
+ "mode": "preserve"
163
+ }
164
+ ]
165
+ }
166
+ ```
167
+
168
+ `mode: "preserve"` keeps executable bits on copied scripts. The composed files are included in
169
+ the skill directory hash, so idempotency and drift detection work as if the assets had always
170
+ belonged to the skill.
171
+
150
172
  Publish by pushing to any git host. A registry exists only for short names and discovery — it's
151
173
  optional, and `agentwheel add <url|path>` always works without it.
152
174
 
@@ -221,6 +243,7 @@ clear conversion format.
221
243
  - [x] **v0.2** — git source driver; `update` (pinned & tracking); overlays/additive/override/eject; `init`; hermes + copilot adapters; commands/mcp/hooks artifacts; OpenClaw semantic plugin planning.
222
244
  - [x] **v0.3** — skillkit/vercel source drivers; optional registry & federation; programmatic adapters behind `--allow-adapter-code`; rich JSON merge for mcp/hooks/settings; profiles.
223
245
  - [x] **v0.4** — runtime auto-detection; no `--target-root` needed for normal use; fleet config with named agents; global + project config merge; `--agent` and `--all`.
246
+ - [x] **v0.5** — asset-includes compose shared files into skills at install time; executable bits preserved; hashes include composed assets.
224
247
 
225
248
  ## Design docs
226
249
 
package/dist/index.js CHANGED
@@ -34,6 +34,12 @@ var artifactTypeSchema = z.enum([
34
34
  "plugins"
35
35
  ]);
36
36
  var fileKindSchema = z.enum(["file", "dir"]);
37
+ var packageAssetSchema = z.object({
38
+ from: z.string().min(1),
39
+ into: z.string().min(1),
40
+ include: z.array(z.string().min(1)).optional(),
41
+ mode: z.enum(["preserve", "copy"]).default("preserve")
42
+ });
37
43
  var artifactSchema = z.object({
38
44
  type: artifactTypeSchema,
39
45
  name: z.string().min(1),
@@ -43,7 +49,8 @@ var artifactSchema = z.object({
43
49
  kind: fileKindSchema,
44
50
  hash: z.string().min(16),
45
51
  packageName: z.string().min(1).optional(),
46
- channel: z.enum(["managed", "overlay", "addition", "override", "ejected"]).default("managed")
52
+ channel: z.enum(["managed", "overlay", "addition", "override", "ejected"]).default("managed"),
53
+ assets: z.array(packageAssetSchema).optional()
47
54
  });
48
55
 
49
56
  // src/model/adapter.ts
@@ -804,7 +811,8 @@ import { parse as parse2, printParseErrorCode as printParseErrorCode2 } from "js
804
811
  import { z as z4 } from "zod";
805
812
  var packageProvideSchema = z4.object({
806
813
  type: artifactTypeSchema,
807
- path: z4.string().min(1)
814
+ path: z4.string().min(1),
815
+ assets: z4.array(packageAssetSchema).optional()
808
816
  });
809
817
  var packageManifestSchema = z4.object({
810
818
  schemaVersion: z4.literal(1),
@@ -973,7 +981,7 @@ async function listFromManifest(root, packageName) {
973
981
  const stats = await stat2(full);
974
982
  if (provide.type === "instructions") {
975
983
  if (stats.isFile()) {
976
- artifacts.push(await artifactForFile(provide.type, basename(full), full, provide.path, packageName));
984
+ artifacts.push(await artifactForFile(provide.type, basename(full), full, provide.path, packageName, provide.assets));
977
985
  }
978
986
  continue;
979
987
  }
@@ -981,16 +989,16 @@ async function listFromManifest(root, packageName) {
981
989
  for (const entry of await sortedDirEntries(full)) {
982
990
  const child = join6(full, entry.name);
983
991
  if (provide.type === "skills" && entry.isDirectory()) {
984
- artifacts.push(await artifactForDir(provide.type, entry.name, child, join6(provide.path, entry.name), packageName));
992
+ artifacts.push(await artifactForDir(provide.type, entry.name, child, join6(provide.path, entry.name), packageName, provide.assets));
985
993
  } else if (provide.type === "plugins" && entry.isDirectory()) {
986
- artifacts.push(await artifactForDir(provide.type, entry.name, child, join6(provide.path, entry.name), packageName));
994
+ artifacts.push(await artifactForDir(provide.type, entry.name, child, join6(provide.path, entry.name), packageName, provide.assets));
987
995
  } else if (entry.isFile()) {
988
996
  const name = provide.type === "rules" && entry.name.endsWith(".md") ? entry.name : entry.name;
989
- artifacts.push(await artifactForFile(provide.type, name, child, join6(provide.path, entry.name), packageName));
997
+ artifacts.push(await artifactForFile(provide.type, name, child, join6(provide.path, entry.name), packageName, provide.assets));
990
998
  }
991
999
  }
992
1000
  } else if (stats.isFile()) {
993
- artifacts.push(await artifactForFile(provide.type, basename(full), full, provide.path, packageName));
1001
+ artifacts.push(await artifactForFile(provide.type, basename(full), full, provide.path, packageName, provide.assets));
994
1002
  }
995
1003
  }
996
1004
  return artifacts;
@@ -1007,7 +1015,7 @@ async function listGenericArtifacts(type, dir, relativeRoot, packageName) {
1007
1015
  }
1008
1016
  return artifacts;
1009
1017
  }
1010
- async function artifactForFile(type, name, sourcePath, relativePath, packageName) {
1018
+ async function artifactForFile(type, name, sourcePath, relativePath, packageName, assets) {
1011
1019
  return {
1012
1020
  type,
1013
1021
  name,
@@ -1016,10 +1024,11 @@ async function artifactForFile(type, name, sourcePath, relativePath, packageName
1016
1024
  kind: "file",
1017
1025
  hash: await hashPath(sourcePath),
1018
1026
  packageName,
1019
- channel: "managed"
1027
+ channel: "managed",
1028
+ assets
1020
1029
  };
1021
1030
  }
1022
- async function artifactForDir(type, name, sourcePath, relativePath, packageName) {
1031
+ async function artifactForDir(type, name, sourcePath, relativePath, packageName, assets) {
1023
1032
  return {
1024
1033
  type,
1025
1034
  name,
@@ -1028,7 +1037,8 @@ async function artifactForDir(type, name, sourcePath, relativePath, packageName)
1028
1037
  kind: "dir",
1029
1038
  hash: await hashPath(sourcePath),
1030
1039
  packageName,
1031
- channel: "managed"
1040
+ channel: "managed",
1041
+ assets
1032
1042
  };
1033
1043
  }
1034
1044
 
@@ -1463,8 +1473,8 @@ function getSourceDriver(name = "local") {
1463
1473
  }
1464
1474
 
1465
1475
  // src/staging/staging.ts
1466
- import { cp as cp3, mkdir as mkdir5, mkdtemp } from "fs/promises";
1467
- import { dirname as dirname5, join as join12 } from "path";
1476
+ import { chmod, cp as cp3, mkdir as mkdir5, mkdtemp, readdir as readdir4, stat as stat6 } from "fs/promises";
1477
+ import { basename as basename7, dirname as dirname5, join as join12, relative as relative2, resolve as resolve8, sep } from "path";
1468
1478
  import { tmpdir as tmpdir2 } from "os";
1469
1479
 
1470
1480
  // src/staging/customize.ts
@@ -1583,6 +1593,7 @@ async function stageSource(driver, source, options = {}) {
1583
1593
  const stagedPath = join12(root, artifact.relativePath);
1584
1594
  await mkdir5(dirname5(stagedPath), { recursive: true });
1585
1595
  await cp3(artifact.sourcePath, stagedPath, { recursive: artifact.kind === "dir", dereference: true });
1596
+ await composeAssets(artifact, resolved.resolvedPath, stagedPath);
1586
1597
  stagedArtifacts.push({
1587
1598
  ...artifact,
1588
1599
  stagedPath,
@@ -1623,11 +1634,93 @@ async function stageSource(driver, source, options = {}) {
1623
1634
  }
1624
1635
  };
1625
1636
  }
1637
+ async function composeAssets(artifact, packageRoot, stagedPath) {
1638
+ if (!artifact.assets?.length) return;
1639
+ if (artifact.kind !== "dir") {
1640
+ throw new Error(`Asset includes require a directory artifact: ${artifact.type}/${artifact.name}`);
1641
+ }
1642
+ for (const asset of artifact.assets) {
1643
+ const source = resolvePackagePath(packageRoot, asset.from);
1644
+ const dest = join12(stagedPath, asset.into);
1645
+ await copyAsset(asset, source, dest);
1646
+ }
1647
+ }
1648
+ async function copyAsset(asset, source, dest) {
1649
+ const sourceStats = await stat6(source);
1650
+ if (sourceStats.isFile()) {
1651
+ if (matchesAny(basename7(source), asset.include)) {
1652
+ await mkdir5(dest, { recursive: true });
1653
+ await copyAssetFile(source, join12(dest, basename7(source)), asset);
1654
+ }
1655
+ return;
1656
+ }
1657
+ if (!sourceStats.isDirectory()) {
1658
+ throw new Error(`Asset include source is not a file or directory: ${source}`);
1659
+ }
1660
+ if (!asset.include?.length) {
1661
+ await mkdir5(dirname5(dest), { recursive: true });
1662
+ await cp3(source, dest, { recursive: true, dereference: true });
1663
+ if (asset.mode === "copy") await normalizeCopiedModes(dest);
1664
+ return;
1665
+ }
1666
+ for (const file of await listFiles(source)) {
1667
+ const rel = relative2(source, file).replaceAll("\\", "/");
1668
+ if (!matchesAny(rel, asset.include) && !matchesAny(basename7(file), asset.include)) continue;
1669
+ await copyAssetFile(file, join12(dest, rel), asset);
1670
+ }
1671
+ }
1672
+ async function copyAssetFile(source, dest, asset) {
1673
+ await mkdir5(dirname5(dest), { recursive: true });
1674
+ await cp3(source, dest, { dereference: true });
1675
+ if (asset.mode === "copy") await chmod(dest, 420);
1676
+ }
1677
+ function resolvePackagePath(packageRoot, path) {
1678
+ const resolved = resolve8(packageRoot, path);
1679
+ const root = resolve8(packageRoot);
1680
+ if (resolved !== root && !resolved.startsWith(`${root}${sep}`)) {
1681
+ throw new Error(`Asset include escapes package root: ${path}`);
1682
+ }
1683
+ return resolved;
1684
+ }
1685
+ async function listFiles(root) {
1686
+ const out = [];
1687
+ async function walk2(dir) {
1688
+ for (const entry of (await readdir4(dir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
1689
+ const full = join12(dir, entry.name);
1690
+ if (entry.isDirectory()) {
1691
+ await walk2(full);
1692
+ } else if (entry.isFile()) {
1693
+ out.push(full);
1694
+ }
1695
+ }
1696
+ }
1697
+ await walk2(root);
1698
+ return out;
1699
+ }
1700
+ async function normalizeCopiedModes(path) {
1701
+ const stats = await stat6(path);
1702
+ if (stats.isFile()) {
1703
+ await chmod(path, 420);
1704
+ return;
1705
+ }
1706
+ if (!stats.isDirectory()) return;
1707
+ for (const entry of await readdir4(path, { withFileTypes: true })) {
1708
+ await normalizeCopiedModes(join12(path, entry.name));
1709
+ }
1710
+ }
1711
+ function matchesAny(path, patterns) {
1712
+ if (!patterns?.length) return true;
1713
+ return patterns.some((pattern) => matchesGlob(path, pattern));
1714
+ }
1715
+ function matchesGlob(path, pattern) {
1716
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
1717
+ return new RegExp(`^${escaped}$`).test(path);
1718
+ }
1626
1719
 
1627
1720
  // src/model/workspace.ts
1628
1721
  import { readFile as readFile8 } from "fs/promises";
1629
1722
  import { homedir as homedir3 } from "os";
1630
- import { dirname as dirname6, join as join13, resolve as resolve8 } from "path";
1723
+ import { dirname as dirname6, join as join13, resolve as resolve9 } from "path";
1631
1724
  import { z as z5 } from "zod";
1632
1725
  var workspacePackageSchema = z5.object({
1633
1726
  name: z5.string().min(1),
@@ -1687,11 +1780,11 @@ function globalWorkspaceConfigPath(globalRoot = homedir3()) {
1687
1780
  return join13(globalRoot, ".agentwheel", "config.json");
1688
1781
  }
1689
1782
  async function findWorkspaceRoot(start = process.cwd()) {
1690
- let current = resolve8(start);
1783
+ let current = resolve9(start);
1691
1784
  while (true) {
1692
1785
  if (await pathExists(workspaceConfigPath(current))) return current;
1693
1786
  const parent = dirname6(current);
1694
- if (parent === current) return resolve8(start);
1787
+ if (parent === current) return resolve9(start);
1695
1788
  current = parent;
1696
1789
  }
1697
1790
  }
@@ -1715,9 +1808,9 @@ function mergeWorkspaceConfig(global, project) {
1715
1808
  });
1716
1809
  }
1717
1810
  function resolveConfigPath(path, baseRoot) {
1718
- if (path.startsWith("~/")) return resolve8(homedir3(), path.slice(2));
1811
+ if (path.startsWith("~/")) return resolve9(homedir3(), path.slice(2));
1719
1812
  if (path === "~") return homedir3();
1720
- return path.startsWith("/") ? resolve8(path) : resolve8(baseRoot, path);
1813
+ return path.startsWith("/") ? resolve9(path) : resolve9(baseRoot, path);
1721
1814
  }
1722
1815
  function emptyWorkspaceConfig() {
1723
1816
  return { schemaVersion: 1, packages: [], registry: {}, profiles: {}, agents: {} };
@@ -1781,9 +1874,9 @@ import { rm as rm7 } from "fs/promises";
1781
1874
  import { join as join16 } from "path";
1782
1875
 
1783
1876
  // src/registry/client.ts
1784
- import { readFile as readFile9, rm as rm6, stat as stat6 } from "fs/promises";
1877
+ import { readFile as readFile9, rm as rm6, stat as stat7 } from "fs/promises";
1785
1878
  import { homedir as homedir4 } from "os";
1786
- import { dirname as dirname8, join as join15, resolve as resolve9 } from "path";
1879
+ import { dirname as dirname8, join as join15, resolve as resolve10 } from "path";
1787
1880
  import { fileURLToPath } from "url";
1788
1881
 
1789
1882
  // src/model/registry.ts
@@ -1886,8 +1979,8 @@ var RegistryClient = class {
1886
1979
  }
1887
1980
  const filePath = source.startsWith("file:") ? fileURLToPath(source) : source;
1888
1981
  if (await pathExists(filePath)) {
1889
- const fullPath = resolve9(filePath);
1890
- const stats = await stat6(fullPath);
1982
+ const fullPath = resolve10(filePath);
1983
+ const stats = await stat7(fullPath);
1891
1984
  return readFile9(stats.isDirectory() ? join15(fullPath, "index.json") : fullPath, "utf8");
1892
1985
  }
1893
1986
  const resolved = await this.git.fetch(await this.git.resolve(source, { cacheRoot: join15(dirname8(this.cachePath), "registry-repos") }));
@@ -2021,7 +2114,7 @@ function shouldUpdatePackage(pkg, lock) {
2021
2114
  }
2022
2115
 
2023
2116
  // src/runtime/target.ts
2024
- import { basename as basename7, dirname as dirname9, join as join18, resolve as resolve10 } from "path";
2117
+ import { basename as basename8, dirname as dirname9, join as join18, resolve as resolve11 } from "path";
2025
2118
  var runtimeMarkers = [
2026
2119
  { adapter: "openclaw", dirs: [".openclaw", ".clawdbot", ".moltbot"] },
2027
2120
  { adapter: "claude", dirs: [".claude"] },
@@ -2030,9 +2123,9 @@ var runtimeMarkers = [
2030
2123
  { adapter: "copilot", dirs: [".github"] }
2031
2124
  ];
2032
2125
  async function resolveRuntimeTarget(request = {}) {
2033
- const cwd = resolve10(request.cwd ?? process.cwd());
2126
+ const cwd = resolve11(request.cwd ?? process.cwd());
2034
2127
  if (request.targetRoot) {
2035
- const targetRoot = resolve10(request.targetRoot);
2128
+ const targetRoot = resolve11(request.targetRoot);
2036
2129
  return {
2037
2130
  adapter: request.adapter ?? "openclaw",
2038
2131
  targetRoot,
@@ -2059,7 +2152,7 @@ async function resolveRuntimeTarget(request = {}) {
2059
2152
  async function resolveAllRuntimeTargets(request = {}) {
2060
2153
  if (request.targetRoot) return [await resolveRuntimeTarget(request)];
2061
2154
  if (request.agent) return [await resolveRuntimeTarget(request)];
2062
- const cwd = resolve10(request.cwd ?? process.cwd());
2155
+ const cwd = resolve11(request.cwd ?? process.cwd());
2063
2156
  const workspaceRoot = await findWorkspaceRoot(cwd);
2064
2157
  const config = await readMergedWorkspaceConfig(workspaceRoot, { globalRoot: request.globalRoot });
2065
2158
  const targets = Object.entries(config.agents).map(([name]) => targetFromAgent(name, config, workspaceRoot));
@@ -2069,12 +2162,12 @@ async function resolveAllRuntimeTargets(request = {}) {
2069
2162
  return targets;
2070
2163
  }
2071
2164
  async function detectRuntimeTarget(cwd = process.cwd(), adapterFilter) {
2072
- const root = resolve10(cwd);
2165
+ const root = resolve11(cwd);
2073
2166
  const matches = [];
2074
2167
  for (const marker of runtimeMarkers) {
2075
2168
  if (adapterFilter && marker.adapter !== adapterFilter) continue;
2076
2169
  for (const dir of marker.dirs) {
2077
- if (basename7(root) === dir) {
2170
+ if (basename8(root) === dir) {
2078
2171
  matches.push({ adapter: marker.adapter, targetRoot: dirname9(root) });
2079
2172
  } else if (await pathExists(join18(root, dir))) {
2080
2173
  matches.push({ adapter: marker.adapter, targetRoot: root });
@@ -2110,7 +2203,7 @@ function dedupeTargets(matches) {
2110
2203
 
2111
2204
  // src/cli/index.ts
2112
2205
  var program = new Command();
2113
- program.name("agentwheel").description("Multi-runtime agent artifact orchestrator").version("0.4.1");
2206
+ program.name("agentwheel").description("Multi-runtime agent artifact orchestrator").version("0.5.0");
2114
2207
  program.command("init").argument("[kind]", "workspace or package", "workspace").option("--target-root <path>", "workspace root", process.cwd()).action(async (kind, options) => {
2115
2208
  const root = normalizeTargetRoot(options.targetRoot);
2116
2209
  if (kind === "package") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentwheel",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Weave skills, rules, and instructions across every AI agent.",
5
5
  "type": "module",
6
6
  "license": "MIT",