@volter/supercode-interchange 0.1.22 → 0.1.23

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 (129) hide show
  1. package/LICENSE-diffy-MIT +23 -0
  2. package/README.md +422 -2
  3. package/channel-records.mjs +71 -0
  4. package/channel-session-counts.mjs +81 -0
  5. package/channels-read.mjs +90 -0
  6. package/corpus-audit-records.mjs +192 -0
  7. package/corpus-audit-render.mjs +47 -0
  8. package/corpus-audit-schema.mjs +89 -0
  9. package/corpus-audit.d.ts +16 -0
  10. package/corpus-audit.mjs +132 -0
  11. package/file-io.mjs +21 -2
  12. package/harness-settings.mjs +19 -0
  13. package/hermes-fire-links.mjs +135 -0
  14. package/hermes-fires.mjs +106 -0
  15. package/hermes-read-home.d.ts +3 -0
  16. package/hermes-read-home.mjs +235 -0
  17. package/hermes-read-store.mjs +49 -0
  18. package/hermes-read-validation.mjs +170 -0
  19. package/hermes-read-workflow.mjs +164 -0
  20. package/hermes-read-yaml.mjs +68 -0
  21. package/hermes-store.mjs +1 -1
  22. package/homes.mjs +2 -2
  23. package/index.d.ts +14 -1
  24. package/index.mjs +1 -1
  25. package/inspection-builtin-tools.json +261 -0
  26. package/inspection-config-fields.json +765 -0
  27. package/inspection-schemas.d.ts +4 -0
  28. package/inspection-schemas.mjs +117 -0
  29. package/job-notepad-read.mjs +97 -0
  30. package/jobs-control.mjs +250 -0
  31. package/jobs-read.mjs +198 -0
  32. package/jsonl.mjs +4 -3
  33. package/memory-show.mjs +187 -0
  34. package/metadata.mjs +45 -3
  35. package/native-json.mjs +46 -3
  36. package/ontology-contracts.d.ts +5 -0
  37. package/ontology-contracts.mjs +29 -0
  38. package/ontology-operations.json +198 -0
  39. package/ontology-schemas.json +7946 -0
  40. package/openclaw-jobs-read.mjs +117 -0
  41. package/openclaw-profile-home.mjs +142 -0
  42. package/orchestrator-read-records.mjs +112 -0
  43. package/orchestrator-store.mjs +21 -9
  44. package/package.json +59 -4
  45. package/parity-ledger.json +6984 -0
  46. package/parity-orchestration.json +5538 -0
  47. package/parity-report.d.ts +7 -0
  48. package/parity-report.mjs +46 -0
  49. package/preset-profiles.mjs +27 -0
  50. package/profile-command.mjs +32 -0
  51. package/profile-orchestrator-door.mjs +104 -0
  52. package/profile-paths.mjs +25 -0
  53. package/profiles-control.mjs +98 -0
  54. package/profiles-read.mjs +119 -0
  55. package/reduction-attribution.mjs +54 -0
  56. package/reduction-diff.mjs +165 -0
  57. package/reduction-log.mjs +177 -0
  58. package/reduction-view.mjs +145 -0
  59. package/retained-projection.mjs +9 -0
  60. package/routes-read.mjs +103 -0
  61. package/runs-read.mjs +224 -0
  62. package/session-activity.d.ts +23 -0
  63. package/session-activity.mjs +124 -0
  64. package/session-append-scan.mjs +27 -17
  65. package/session-branch-handoff.mjs +115 -0
  66. package/session-claude-graph.mjs +29 -7
  67. package/session-claude-index.mjs +732 -114
  68. package/session-claude-runtime.d.ts +28 -0
  69. package/session-claude-runtime.mjs +171 -6
  70. package/session-claude-scan-store.mjs +164 -0
  71. package/session-claude.mjs +9 -0
  72. package/session-codex-activity.d.ts +6 -0
  73. package/session-codex-activity.mjs +120 -0
  74. package/session-codex-window.mjs +24 -4
  75. package/session-config-scan.mjs +141 -0
  76. package/session-convert.d.ts +33 -0
  77. package/session-convert.mjs +179 -0
  78. package/session-emission-version.json +1 -0
  79. package/session-file.mjs +11 -1
  80. package/session-follower.mjs +17 -5
  81. package/session-fresh-id.mjs +15 -0
  82. package/session-hermes-import.mjs +61 -0
  83. package/session-hermes.mjs +1 -1
  84. package/session-import.mjs +37 -0
  85. package/session-inspect.d.ts +18 -0
  86. package/session-inspect.mjs +122 -0
  87. package/session-machine-tree.d.ts +33 -0
  88. package/session-machine-tree.mjs +274 -0
  89. package/session-materialize-files.mjs +78 -0
  90. package/session-materialize-opencode.mjs +137 -0
  91. package/session-materialize.mjs +186 -0
  92. package/session-message.mjs +85 -0
  93. package/session-model.d.ts +9 -0
  94. package/session-model.mjs +1 -0
  95. package/session-opencode-store.mjs +39 -3
  96. package/session-path.mjs +6 -3
  97. package/session-pi-content.mjs +7 -2
  98. package/session-read.d.ts +134 -2
  99. package/session-read.mjs +86 -9
  100. package/session-recorded-config.mjs +3 -2
  101. package/session-records.mjs +12 -6
  102. package/session-residue-store.mjs +90 -0
  103. package/session-residue.mjs +150 -0
  104. package/session-resume-instructions.mjs +1 -1
  105. package/session-saved-read.mjs +105 -0
  106. package/session-transfer.mjs +141 -0
  107. package/session-views.mjs +1 -0
  108. package/session-write-claude.mjs +135 -0
  109. package/session-write-codex.mjs +146 -0
  110. package/session-write-gemini.mjs +77 -0
  111. package/session-write-goose.mjs +99 -0
  112. package/session-write-grok.mjs +70 -0
  113. package/session-write-native.mjs +41 -0
  114. package/session-write-opencode.mjs +197 -0
  115. package/session-write-pi.mjs +130 -0
  116. package/session-write-residue.mjs +19 -0
  117. package/session-write-utils.mjs +77 -0
  118. package/session-write.d.ts +10 -0
  119. package/session-write.mjs +37 -0
  120. package/skill-toml.mjs +80 -0
  121. package/skills-control.mjs +193 -0
  122. package/skills-list.mjs +237 -0
  123. package/source-files.mjs +12 -2
  124. package/sqlite-reader.mjs +134 -18
  125. package/support-registry.json +402 -0
  126. package/support-report.mjs +69 -0
  127. package/trigger-records.mjs +77 -0
  128. package/triggers-read.mjs +67 -0
  129. package/workflow-read.mjs +165 -0
package/skill-toml.mjs ADDED
@@ -0,0 +1,80 @@
1
+ // Complete TOML 1.0 grammar, with the native Value admission boundaries. The parser also reads TOML 1.1; the forms only
2
+ // 1.1 admits (inline tables over several lines or with a trailing comma, times without seconds, \x and \e escapes)
3
+ // are refused here, as the native toml 0.8 reader refuses them.
4
+ // The skills overlay reads only string/bool fields; dates remain date values.
5
+ import { parse } from 'smol-toml';
6
+
7
+ const scalar = value => { if (/[\uD800-\uDFFF]/u.test(value)) throw new Error('invalid TOML Unicode scalar'); };
8
+ function nativeValues(value) {
9
+ if (typeof value === 'bigint' && (value < -0x8000000000000000n || value > 0x7fffffffffffffffn)) throw new Error('TOML integer is outside signed 64-bit range');
10
+ if (typeof value === 'string') scalar(value);
11
+ else if (value != null && typeof value === 'object' && !(value instanceof Date)) {
12
+ for (const [key, member] of Object.entries(value)) { scalar(key); nativeValues(member); }
13
+ }
14
+ }
15
+ const dateToken = /(?:\d{4}-\d{2}-\d{2}(?:[Tt ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})?)?|\d{2}:\d{2}:\d{2}(?:\.\d+)?)(?=$|[\s#,\]}])/y;
16
+ const time11 = /(?:\d{4}-\d{2}-\d{2}[Tt ])?\d{2}:\d{2}(?!:\d)/y;
17
+ function nativeDates(source) {
18
+ let out = '', copied = 0;
19
+ // Open brackets outside strings and comments: a line break or comment directly inside an inline table, or a comma
20
+ // closing one, is TOML 1.1 only (an array inside it may still span lines, as TOML 1.0 allows).
21
+ const open = [];
22
+ for (let at = 0; at < source.length;) {
23
+ const char = source[at];
24
+ if (open.at(-1) === '{' && (char === '\n' || char === '#')) throw new Error('TOML 1.0 inline tables are one line');
25
+ if (char === '#') { const next = source.indexOf('\n', at); at = next < 0 ? source.length : next + 1; continue; }
26
+ if (char === '{' || char === '[') { open.push(char); at++; continue; }
27
+ if (char === '}' || char === ']') {
28
+ if (char === '}') {
29
+ let before = at - 1; while (before >= 0 && /\s/.test(source[before])) before--;
30
+ if (source[before] === ',') throw new Error('TOML 1.0 inline tables take no trailing comma');
31
+ }
32
+ open.pop(); at++; continue;
33
+ }
34
+ if (char === '"' || char === "'") {
35
+ const fence = source.startsWith(char.repeat(3), at) ? char.repeat(3) : char; at += fence.length;
36
+ for (; at < source.length; at++) {
37
+ if (char === '"' && source[at] === '\\') {
38
+ if (source[at + 1] === 'x' || source[at + 1] === 'e') throw new Error('TOML 1.0 has no \\x or \\e escape');
39
+ at++; continue;
40
+ }
41
+ if (!source.startsWith(fence, at)) continue;
42
+ at += fence.length;
43
+ if (fence.length === 3) for (let extra = 0; extra < 2 && source[at] === char; extra++) at++;
44
+ break;
45
+ }
46
+ continue;
47
+ }
48
+ if (/\d/.test(char) && (!at || /[=,\[{\t \r\n]/.test(source[at - 1]))) {
49
+ time11.lastIndex = at; if (time11.test(source)) throw new Error('TOML 1.0 times carry seconds');
50
+ dateToken.lastIndex = at; const match = dateToken.exec(source);
51
+ if (match) {
52
+ const token = match[0], hasDate = token[4] === '-', timeAt = hasDate ? token.length > 10 ? 11 : null : 0;
53
+ if (hasDate) {
54
+ const year = Number(token.slice(0, 4)), month = Number(token.slice(5, 7)), day = Number(token.slice(8, 10));
55
+ const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
56
+ const last = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1];
57
+ if (!last || day < 1 || day > last) throw new Error('invalid TOML date');
58
+ }
59
+ if (timeAt != null) {
60
+ const hour = Number(token.slice(timeAt, timeAt + 2)), minute = Number(token.slice(timeAt + 3, timeAt + 5)), second = Number(token.slice(timeAt + 6, timeAt + 8));
61
+ const offset = /[+-](\d{2}):(\d{2})$/.exec(token);
62
+ if (hour > 23 || minute > 59 || second > 60 || offset && (Number(offset[1]) > 23 || Number(offset[2]) > 59)) throw new Error('invalid TOML time');
63
+ // Rust accepts second 60. The parser uses Date, which cannot encode
64
+ // it. Mask that component only; no date is consulted by this overlay.
65
+ if (second === 60) {
66
+ const secondAt = at + timeAt + 6;
67
+ out += source.slice(copied, secondAt) + '59'; copied = secondAt + 2;
68
+ }
69
+ }
70
+ at += token.length; continue;
71
+ }
72
+ }
73
+ at++;
74
+ }
75
+ return out + source.slice(copied);
76
+ }
77
+ export function parseSkillToml(source) {
78
+ const doc = parse(nativeDates(source.startsWith('\ufeff') ? source.slice(1) : source), { integersAsBigInt: true });
79
+ nativeValues(doc); return doc;
80
+ }
@@ -0,0 +1,193 @@
1
+ // Controlled skill doors. Directory packages belong to the core four loaders;
2
+ // Hermes/OpenClaw own their CLI mutations. A successful command is followed by
3
+ // the same installed-skill reader, never inferred from its exit status alone.
4
+ import { statSync, lstatSync, realpathSync, readdirSync, mkdirSync, copyFileSync, chmodSync, rmSync } from 'node:fs';
5
+ import { spawn } from 'node:child_process';
6
+ import { isAbsolute } from 'node:path';
7
+ import { SKILL_HARNESSES, skillHomes, skillRoots, writableSkillRoots, declaredSkillName, skillPathParent, skillPathName, skillPathKey, listInstalledSkills } from './skills-list.mjs';
8
+ import { childPath } from './session-spend-files.mjs';
9
+ import { nativeTrim } from './text.mjs';
10
+ import { supportRegistryReport } from './support-report.mjs';
11
+
12
+ const invalid = message => { throw Object.assign(new Error(message), { rpcCode: -32602 }); };
13
+ const unsupported = message => { throw Object.assign(new Error(message), { rpcCode: -32020 }); };
14
+ const failed = message => { throw Object.assign(new Error(message), { rpcCode: -32000 }); };
15
+ const display = path => Buffer.isBuffer(path) ? path.toString('utf8') : path;
16
+ const same = (left, right) => skillPathKey(left) === skillPathKey(right);
17
+ const file = path => { try { return statSync(path).isFile(); } catch { return false; } };
18
+ const directory = path => { try { return statSync(path).isDirectory(); } catch { return false; } };
19
+ const exists = path => { try { statSync(path); return true; } catch { return false; } };
20
+ const actual = (path, cwd) => !path.length || isAbsolute(display(path)) || process.platform === 'win32' && /^[A-Za-z]:/.test(display(path)) ? path : childPath(cwd, path);
21
+ export const skillShellQuote = value => value && /^[A-Za-z0-9\-_./:@=+,]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
22
+ function struct(value, fields, name) {
23
+ if (Array.isArray(value)) {
24
+ if (value.length > fields.length) invalid(`${name} has too many fields`);
25
+ return Object.fromEntries(value.map((entry, at) => [fields[at], entry]));
26
+ }
27
+ if (value == null || typeof value !== 'object') invalid(`${name} must be a struct`);
28
+ return value;
29
+ }
30
+ function mutationParams(params, context) {
31
+ const value = struct(params, ['harness', 'name', 'source', 'scope', 'cwd', 'homes'], 'skill mutation');
32
+ if (Object.hasOwn(value, 'harness') && typeof value.harness !== 'string') invalid('harness must be a string');
33
+ for (const key of ['name', 'source', 'scope', 'cwd']) if (value[key] != null && typeof value[key] !== 'string') invalid(`${key} must be a string`);
34
+ if (value.scope != null && !['managed', 'user', 'project', 'plugin', 'bundled'].includes(value.scope)) invalid(`unknown skill scope: ${value.scope}`);
35
+ // This object crosses the same string-valued request boundary as supplied
36
+ // homes when the mutation re-reads the installed inventory.
37
+ const homes = Object.fromEntries(Object.entries(skillHomes(context.env)).map(([key, path]) => [key, display(path)]));
38
+ if (Object.hasOwn(value, 'homes')) {
39
+ const supplied = struct(value.homes, Object.keys(homes), 'homes');
40
+ for (const key of Object.keys(homes)) if (Object.hasOwn(supplied, key)) {
41
+ if (typeof supplied[key] !== 'string') invalid(`homes.${key} must be a string`);
42
+ homes[key] = supplied[key];
43
+ }
44
+ }
45
+ return { ...value, harness: value.harness ?? '', homes, scope: value.scope ?? 'user', cwd: value.cwd ?? context.cwd };
46
+ }
47
+ const rows = (mutation, context) => listInstalledSkills({ harness: mutation.harness, cwd: mutation.cwd, homes: mutation.homes }, context);
48
+ const names = (mutation, context) => new Set(rows(mutation, context).map(row => row.name));
49
+ const findName = (mutation, name, context) => rows(mutation, context).find(row => row.name === name);
50
+ function requireSource(mutation) {
51
+ const source = nativeTrim(mutation.source ?? '');
52
+ if (!source) invalid('`skills.install` needs a `source`: a local skill directory, or the identifier the harness\'s own install verb accepts');
53
+ return source;
54
+ }
55
+ function requireName(mutation) {
56
+ const name = nativeTrim(mutation.name ?? '');
57
+ if (!name) invalid('`skills.remove` needs the skill `name` to remove');
58
+ return name;
59
+ }
60
+ function validateName(value) {
61
+ const name = nativeTrim(value);
62
+ if (!name || name.startsWith('.') || /[\/\\\0]/.test(name) || process.platform === 'win32' && /^[A-Za-z]:/.test(name)) {
63
+ invalid(`\`${value}\` is not a skill name: a skill is one directory inside the harness's own root, so a name may not be empty, hidden, or contain a path separator`);
64
+ }
65
+ return name;
66
+ }
67
+ function canonical(path, cwd) {
68
+ try { return realpathSync.native(actual(path, cwd), { encoding: 'buffer' }); } catch { /* resolve the existing prefix */ }
69
+ const parent = skillPathParent(Buffer.from(path)), leaf = skillPathName(path);
70
+ if (parent == null || leaf == null) return Buffer.from(path);
71
+ return childPath(canonical(parent, cwd), leaf);
72
+ }
73
+ function contained(path, roots, context) {
74
+ const parent = skillPathParent(canonical(path, context.cwd));
75
+ if (parent != null && roots.some(root => same(parent, canonical(root, context.cwd)))) return;
76
+ invalid(`${display(path)} is outside the skills roots supercode recognizes (${roots.map(display).join(', ')}); every install and removal stays inside the harness's own root`);
77
+ }
78
+ function copyPackage(source, destination, context) {
79
+ try { mkdirSync(actual(destination, context.cwd), { recursive: true }); }
80
+ catch (error) { failed(`${display(destination)} could not be created: ${error.message}`); }
81
+ let entries;
82
+ try { entries = readdirSync(actual(source, context.cwd), { encoding: 'buffer' }); }
83
+ catch (error) { failed(`${display(source)} could not be read: ${error.message}`); }
84
+ for (const name of entries) {
85
+ const from = childPath(source, name), to = childPath(destination, name); let kind;
86
+ try { kind = lstatSync(actual(from, context.cwd)); }
87
+ catch (error) { failed(`${display(from)} could not be read: ${error.message}`); }
88
+ if (kind.isSymbolicLink()) invalid(`${display(from)} is a symlink; supercode copies a skill package's own files only, so a link that could point outside it is refused`);
89
+ if (kind.isDirectory()) copyPackage(from, to, context);
90
+ else if (kind.isFile()) {
91
+ try { copyFileSync(actual(from, context.cwd), actual(to, context.cwd)); chmodSync(actual(to, context.cwd), kind.mode & 0o7777); }
92
+ catch (error) { failed(`${display(from)} could not be copied to ${display(to)}: ${error.message}`); }
93
+ } else invalid(`${display(from)} is neither a file nor a directory; a skill package holds only its own files`);
94
+ }
95
+ }
96
+ function refusePresent(mutation, name, ran, context) {
97
+ const row = findName(mutation, name, context);
98
+ if (row) failed(`\`${ran}\` reported success but \`${name}\` is still installed at ${row.location}`);
99
+ }
100
+ function directoryMutation(verb, mutation, context) {
101
+ const writable = writableSkillRoots(mutation.harness, mutation.scope, mutation.homes, mutation.cwd);
102
+ if (!writable.length) unsupported(`\`${mutation.harness}\` has no ${mutation.scope} skills root Volter Harness may write; its inventory names none`);
103
+ if (verb === 'install') {
104
+ const source = requireSource(mutation);
105
+ if (!directory(actual(source, context.cwd))) invalid(`\`${source}\` is not a directory: \`${mutation.harness}\`'s skills door is its loader's own root, so the source must be the skill PACKAGE — a directory holding SKILL.md`);
106
+ const declared = declaredSkillName(source, context.cwd);
107
+ if (declared == null) invalid(`\`${source}\` holds no SKILL.md, so it is not a skill package the harness's loader would read`);
108
+ const name = validateName(nativeTrim(mutation.name ?? '') || declared), root = writable[0], destination = childPath(root, name);
109
+ if (exists(actual(destination, context.cwd))) invalid(`\`${name}\` is already installed at ${display(destination)}; remove it first`);
110
+ try { mkdirSync(actual(root, context.cwd), { recursive: true }); }
111
+ catch (error) { failed(`${mutation.harness}'s ${mutation.scope} skills root ${display(root)} could not be created: ${error.message}`); }
112
+ contained(destination, [root], context);
113
+ const ran = `cp -R ${skillShellQuote(source)} ${skillShellQuote(display(destination))}`;
114
+ try { copyPackage(source, destination, context); }
115
+ catch (error) { try { rmSync(actual(destination, context.cwd), { recursive: true }); } catch { /* retain the primary copy refusal */ } throw error; }
116
+ const skill = rows(mutation, context).find(row => same(row.location, destination));
117
+ if (!skill) failed(`\`${ran}\` succeeded but ${mutation.harness}'s loader does not report a skill at ${display(destination)}`);
118
+ return { harness: mutation.harness, verb, ran, name: skill.name, skill };
119
+ }
120
+ const name = validateName(requireName(mutation)), matches = rows(mutation, context).filter(row => row.name === name && row.scope === mutation.scope);
121
+ if (!matches.length) invalid(`\`${mutation.harness}\` has no ${mutation.scope} skill \`${name}\``);
122
+ if (matches.length !== 1) invalid(`\`${mutation.harness}\` reports ${matches.length} skills named \`${name}\` in its ${mutation.scope} roots (${matches.map(row => row.location).join(', ')}); supercode refuses to guess which one to delete`);
123
+ const row = matches[0], recognized = [...writable, ...skillRoots(mutation.harness, mutation.homes, mutation.cwd, context.cwd).filter(([scope]) => scope === mutation.scope).map(([, root]) => root)];
124
+ contained(row.location, recognized, context);
125
+ if (!file(actual(childPath(row.location, 'SKILL.md'), context.cwd))) invalid(`${row.location} holds no SKILL.md; supercode removes skill PACKAGES, never a directory it cannot identify as one`);
126
+ const ran = `rm -r ${skillShellQuote(row.location)}`;
127
+ try { rmSync(actual(row.location, context.cwd), { recursive: true }); }
128
+ catch (error) { failed(`\`${ran}\` failed: ${error.message}`); }
129
+ refusePresent(mutation, name, ran, context);
130
+ return { harness: mutation.harness, verb, ran, name, removed: true };
131
+ }
132
+ function harnessProgram(harness, context) {
133
+ const overridden = nativeTrim(context.env[harness === 'hermes' ? 'SUPERCODE_HERMES_BIN' : 'SUPERCODE_OPENCLAW_BIN'] ?? '');
134
+ if (overridden) return overridden;
135
+ const program = supportRegistryReport(context).harnesses.find(row => row.id === harness)?.runtime?.default_launch?.program;
136
+ if (!program) failed(`the registry has no launch for \`${harness}\`, so its CLI cannot be located`);
137
+ return program.replace(/-acp$/, '');
138
+ }
139
+ function commandFor(verb, mutation, context) {
140
+ if (mutation.harness === 'hermes' && mutation.scope !== 'user') unsupported('hermes keeps skills in one root per HERMES_HOME (`<HERMES_HOME>/skills`, and a profile IS a HERMES_HOME); it has no project-scoped skills root, so supercode refuses rather than inventing one');
141
+ if (mutation.harness === 'openclaw' && verb === 'remove') unsupported('openclaw 2026.7.1-2 publishes no `skills remove` verb (`openclaw skills` has search|install|update|verify|curator|workshop|list|info|check). supercode refuses rather than deleting files out of the harness\'s managed directory behind its back');
142
+ const program = harnessProgram(mutation.harness, context), args = ['skills'], env = { ...context.env }, name = nativeTrim(mutation.name ?? '');
143
+ if (mutation.harness === 'hermes') {
144
+ env.HERMES_HOME = display(mutation.homes.hermes);
145
+ if (verb === 'install') {
146
+ const source = requireSource(mutation);
147
+ if (directory(actual(source, context.cwd))) unsupported(`\`hermes skills install\` takes a registry identifier (\`owner/repo/skills/x\`) or a direct HTTP(S) URL to a SKILL.md — its pinned help enumerates no local-directory form, so \`${source}\` cannot be handed to it. Serve the package's SKILL.md over HTTP, or install it into a harness whose door is the directory`);
148
+ args.push('install', '--yes'); if (name) args.push('--name', name); args.push(source);
149
+ } else args.push('uninstall', requireName(mutation), '--yes');
150
+ } else {
151
+ env.OPENCLAW_STATE_DIR = display(mutation.homes.openclaw); env.OPENCLAW_CONFIG_PATH = display(childPath(mutation.homes.openclaw, 'openclaw.json'));
152
+ args.push('install', requireSource(mutation)); if (mutation.scope === 'user') args.push('--global'); if (name) args.push('--as', name);
153
+ }
154
+ return { program, args, env, ran: [program, ...args].map(skillShellQuote).join(' ') };
155
+ }
156
+ async function runCommand(command, context) {
157
+ if (context.signal?.aborted) failed('skill client closed before the harness command started');
158
+ const output = await new Promise((resolve, reject) => {
159
+ let child; const stdout = [], stderr = []; let force;
160
+ try { child = spawn(command.program, command.args, { cwd: context.cwd, env: command.env, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true }); }
161
+ catch (error) { reject(error); return; }
162
+ const abort = () => { child.kill('SIGTERM'); force = setTimeout(() => child.kill('SIGKILL'), 500); };
163
+ child.stdout.on('data', data => stdout.push(data)); child.stderr.on('data', data => stderr.push(data));
164
+ child.once('error', reject);
165
+ child.once('close', (code, signal) => { clearTimeout(force); context.signal?.removeEventListener('abort', abort); resolve({ code, signal, stdout: Buffer.concat(stdout).toString('utf8'), stderr: Buffer.concat(stderr).toString('utf8') }); });
166
+ context.signal?.addEventListener('abort', abort, { once: true }); if (context.signal?.aborted) abort();
167
+ }).catch(error => failed(`\`${command.ran}\` could not be executed: ${error.message}`));
168
+ if (output.code !== 0) {
169
+ const detail = nativeTrim(output.stderr) || nativeTrim(output.stdout) || 'the harness printed nothing';
170
+ failed(`\`${command.ran}\` failed (${output.signal ? `signal: ${output.signal}` : process.platform === 'win32' ? `exit code: ${output.code}` : `exit status: ${output.code}`}): ${detail}`);
171
+ }
172
+ }
173
+ async function cliMutation(verb, mutation, context) {
174
+ const command = commandFor(verb, mutation, context), before = verb === 'install' ? names(mutation, context) : null, removing = verb === 'remove' ? requireName(mutation) : null;
175
+ await runCommand(command, context);
176
+ if (verb === 'remove') {
177
+ refusePresent(mutation, removing, command.ran, context);
178
+ return { harness: mutation.harness, verb, ran: command.ran, name: removing, removed: true };
179
+ }
180
+ const after = names(mutation, context), fresh = [...after].filter(name => !before.has(name)).sort((a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b))), requested = nativeTrim(mutation.name ?? '');
181
+ const name = fresh.length === 1 ? fresh[0] : requested && after.has(requested) ? requested : null;
182
+ if (name == null) failed(`\`${command.ran}\` exited 0 but ${mutation.harness}'s skills root gained ${fresh.length} skill(s), so the installed skill cannot be identified — pass \`name\` to say which one it should be`);
183
+ const skill = findName(mutation, name, context);
184
+ if (!skill) failed(`\`${command.ran}\` exited 0 but ${mutation.harness}'s skills roots hold no \`${name}\` afterwards`);
185
+ return { harness: mutation.harness, verb, ran: command.ran, name, skill };
186
+ }
187
+ export async function mutateInstalledSkill(verb, params, { env = process.env, cwd = process.cwd(), signal } = {}) {
188
+ const context = { env, cwd, signal }, mutation = mutationParams(params, context);
189
+ if (!SKILL_HARNESSES.includes(mutation.harness)) unsupported(`\`${mutation.harness}\` has no skills root Volter Harness reads; \`skills.${verb}\` is supported for: ${SKILL_HARNESSES.join(', ')}`);
190
+ if (!['user', 'project'].includes(mutation.scope)) invalid(`\`${mutation.scope}\` is a root the harness owns, not one a client may write; use user or project`);
191
+ if (verb !== 'install' && verb !== 'remove') invalid(`unknown skill mutation: ${verb}`);
192
+ return ['hermes', 'openclaw'].includes(mutation.harness) ? cliMutation(verb, mutation, context) : directoryMutation(verb, mutation, context);
193
+ }
@@ -0,0 +1,237 @@
1
+ // Read-only skill packages. Controlled installation shares these exact loader
2
+ // roots and frontmatter rules; runtime skill loading keeps its native owner.
3
+ import { readFileSync, statSync } from 'node:fs';
4
+ import { isAbsolute } from 'node:path';
5
+ import { userInfo } from 'node:os';
6
+ import { parseSkillToml } from './skill-toml.mjs';
7
+ import { childPath, directoryEntries, textName } from './session-spend-files.mjs';
8
+ import { nativeTrim, truncateScalars } from './text.mjs';
9
+
10
+ export const SKILL_HARNESSES = Object.freeze(['claude-code', 'codex', 'opencode', 'pi', 'hermes', 'openclaw']);
11
+ const scopes = ['managed', 'user', 'project', 'plugin', 'bundled'];
12
+ const skipped = new Set(['node_modules', 'target', '.git', 'scripts', 'references']);
13
+ const whitespace = /[\u0009-\u000d\u0020\u0085\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000]+/u;
14
+ const decoder = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
15
+ const invalid = message => { throw Object.assign(new Error(message), { rpcCode: -32602 }); };
16
+ const asciiLower = value => value.replace(/[A-Z]/g, char => char.toLowerCase());
17
+ const join = (...parts) => parts.reduce((root, part) => childPath(root, part), Buffer.alloc(0));
18
+ const selected = (path, cwd) => !path.length || isAbsolute(path.toString()) || process.platform === 'win32' && /^[A-Za-z]:/.test(path.toString()) ? path : childPath(cwd, path);
19
+ const file = (path, cwd) => { try { return statSync(selected(path, cwd)).isFile(); } catch { return false; } };
20
+ const exists = (path, cwd) => { try { statSync(selected(path, cwd)); return true; } catch { return false; } };
21
+ const read = (path, cwd) => { try { return decoder.decode(readFileSync(selected(path, cwd))); } catch { return null; } };
22
+ const unquote = value => nativeTrim(nativeTrim(value).replace(/^["']+|["']+$/g, ''));
23
+
24
+ // PathBuf comparisons normalize separators and interior '.' components, but
25
+ // keep '..' lexical. Filesystem reads retain the selected spelling throughout.
26
+ function components(path) {
27
+ let source = Buffer.from(path), prefix = Buffer.alloc(0), rooted = false;
28
+ if (process.platform === 'win32') {
29
+ const text = source.toString('utf8');
30
+ const match = /^(?:\\\\[?.]\\(?:UNC\\[^\\]+\\[^\\]+|[A-Za-z]:|[^\\]+)|\\\\[^\\/]+[\\/][^\\/]+|[A-Za-z]:)/i.exec(text);
31
+ if (match) {
32
+ prefix = Buffer.from(match[0].replaceAll('/', '\\')); source = source.subarray(prefix.length);
33
+ // Windows PathBuf compares ordinary drive prefixes case-insensitively.
34
+ // Keep the filesystem/location spelling; normalize only the comparison.
35
+ if (prefix.length === 2 && prefix[1] === 58 && prefix[0] >= 97 && prefix[0] <= 122) prefix[0] -= 32;
36
+ }
37
+ }
38
+ const separator = byte => byte === 47 || process.platform === 'win32' && byte === 92;
39
+ rooted = source.length > 0 && separator(source[0]);
40
+ const parts = [], out = prefix.length ? [[0, prefix]] : [];
41
+ if (rooted) out.push([1, Buffer.alloc(0)]);
42
+ let begin = 0;
43
+ for (let at = 0; at <= source.length; at++) if (at === source.length || separator(source[at])) {
44
+ const part = source.subarray(begin, at); begin = at + 1;
45
+ if (part.length) parts.push(part);
46
+ }
47
+ for (let at = 0; at < parts.length; at++) {
48
+ const part = parts[at];
49
+ if (part.equals(Buffer.from('.'))) { if (!at && !rooted) out.push([2, Buffer.alloc(0)]); }
50
+ else out.push([part.equals(Buffer.from('..')) ? 3 : 4, part]);
51
+ }
52
+ return out;
53
+ }
54
+ const pathKey = path => JSON.stringify(components(path).map(([kind, text]) => [kind, text.toString('hex')]));
55
+ function pathCompare(a, b) {
56
+ const left = components(a), right = components(b);
57
+ for (let at = 0; at < Math.min(left.length, right.length); at++) {
58
+ const order = left[at][0] - right[at][0] || Buffer.compare(left[at][1], right[at][1]);
59
+ if (order) return order;
60
+ }
61
+ return left.length - right.length;
62
+ }
63
+ const compareText = (a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b));
64
+ function directory(root, cwd) { try { return statSync(selected(root, cwd)).isDirectory(); } catch { return false; } }
65
+ function directories(root, cwd) {
66
+ return [...directoryEntries(selected(root, cwd))].map(entry => childPath(root, entry.name)).filter(path => directory(path, cwd)).sort(pathCompare);
67
+ }
68
+ function parent(path) {
69
+ const parts = components(path); if (!parts.length || parts.at(-1)[0] <= 1) return null;
70
+ // Remove the last component, retaining the spelling of all preceding ones.
71
+ let text = path.toString('utf8'); const prefix = parts[0][0] === 0 ? parts[0][1].length : 0;
72
+ const slash = process.platform === 'win32' ? /[\\/]/ : /\//;
73
+ const minimum = prefix + (parts.some(([kind]) => kind === 1) ? 1 : 0);
74
+ const trimSeparators = () => { while (text.length > minimum && slash.test(text.at(-1))) text = text.slice(0, -1); };
75
+ trimSeparators();
76
+ for (;;) {
77
+ let start = text.length; while (start > prefix && !slash.test(text[start - 1])) start--;
78
+ if (text.slice(start) !== '.' || start === prefix) { text = text.slice(0, start); trimSeparators(); break; }
79
+ text = text.slice(0, start); trimSeparators();
80
+ }
81
+ return Buffer.from(text);
82
+ }
83
+ function projectRoots(cwd, markers, readerCwd) {
84
+ const roots = [], seen = new Set(); let ancestor = Buffer.from(cwd);
85
+ for (let count = 0; ancestor != null && count < 32; count++, ancestor = parent(ancestor)) {
86
+ for (const marker of markers) {
87
+ const root = join(ancestor, ...marker), key = pathKey(root);
88
+ if (directory(root, readerCwd) && !seen.has(key)) { seen.add(key); roots.push(root); }
89
+ }
90
+ if (exists(join(ancestor, '.git'), readerCwd)) break;
91
+ }
92
+ return roots;
93
+ }
94
+ export function skillHomes(env) {
95
+ let home; try { home = env.HOME || process.platform === 'win32' && env.USERPROFILE || userInfo().homedir || '.'; } catch { home = '.'; }
96
+ return {
97
+ claude_code: env.CLAUDE_CONFIG_DIR ?? join(home, '.claude'), codex: env.CODEX_HOME ?? join(home, '.codex'),
98
+ opencode: env.OPENCODE_CONFIG_DIR ?? join(env.XDG_CONFIG_HOME ?? join(home, '.config'), 'opencode'),
99
+ pi: env.PI_CODING_AGENT_DIR ?? join(home, '.pi', 'agent'), hermes: env.HERMES_HOME ?? join(home, '.hermes'),
100
+ openclaw: env.OPENCLAW_STATE_DIR ?? (env.OPENCLAW_HOME != null ? join(env.OPENCLAW_HOME, '.openclaw') : join(home, '.openclaw')),
101
+ agents: join(home, '.agents'),
102
+ };
103
+ }
104
+ function struct(params, keys, name) {
105
+ if (Array.isArray(params)) { if (params.length > keys.length) invalid(`${name} has too many fields`); return Object.fromEntries(params.map((value, at) => [keys[at], value])); }
106
+ if (params == null || typeof params !== 'object') invalid(`${name} must be a struct`);
107
+ return params;
108
+ }
109
+ function queryParams(params, context) {
110
+ const query = struct(params, ['harness', 'scope', 'cwd', 'homes'], 'skills params');
111
+ for (const key of ['harness', 'scope', 'cwd']) if (query[key] != null && typeof query[key] !== 'string') invalid(`${key} must be a string`);
112
+ if (query.scope != null && !scopes.includes(query.scope)) invalid(`unknown skill scope: ${query.scope}`);
113
+ const homes = skillHomes(context.env);
114
+ if (Object.hasOwn(query, 'homes')) {
115
+ const supplied = struct(query.homes, Object.keys(homes), 'homes');
116
+ for (const key of Object.keys(homes)) if (Object.hasOwn(supplied, key)) {
117
+ if (typeof supplied[key] !== 'string') invalid(`homes.${key} must be a string`);
118
+ homes[key] = supplied[key];
119
+ }
120
+ }
121
+ if (query.harness != null && !SKILL_HARNESSES.includes(query.harness)) throw Object.assign(new Error(`\`${query.harness}\` has no skills root Volter Harness reads`), { rpcCode: -32020 });
122
+ return { ...query, homes, cwd: query.cwd ?? context.cwd };
123
+ }
124
+ export function skillRoots(harness, homes, cwd, readerCwd = cwd) {
125
+ const out = [], add = (scope, path) => out.push([scope, path]), project = markers => projectRoots(cwd, markers, readerCwd).forEach(path => add('project', path));
126
+ if (harness === 'claude-code') {
127
+ add('managed', process.platform === 'darwin' ? '/Library/Application Support/ClaudeCode/skills' : '/etc/claude-code/skills'); add('user', join(homes.claude_code, 'skills'));
128
+ for (const market of directories(join(homes.claude_code, 'plugins', 'cache'), readerCwd)) for (const plugin of directories(market, readerCwd)) for (const version of directories(plugin, readerCwd)) add('plugin', join(version, 'skills'));
129
+ project([['.claude', 'skills']]);
130
+ } else if (harness === 'codex') {
131
+ add('managed', '/etc/codex/skills'); add('bundled', join(homes.codex, 'skills', '.system')); add('user', join(homes.agents, 'skills')); add('user', join(homes.codex, 'skills')); project([['.agents', 'skills']]);
132
+ } else if (harness === 'opencode') {
133
+ add('user', join(homes.opencode, 'skill')); add('user', join(homes.opencode, 'skills')); project([['.opencode', 'skill'], ['.opencode', 'skills']]);
134
+ } else if (harness === 'pi') {
135
+ add('user', join(homes.pi, 'skills')); add('user', join(homes.agents, 'skills')); project([['.pi', 'skills'], ['.agents', 'skills']]);
136
+ } else if (harness === 'hermes') {
137
+ add('user', join(homes.hermes, 'skills')); for (const profile of directories(join(homes.hermes, 'profiles'), readerCwd)) add('user', join(profile, 'skills'));
138
+ } else if (harness === 'openclaw') {
139
+ add('managed', join(homes.openclaw, 'skills')); add('plugin', join(homes.openclaw, 'plugin-skills')); add('user', join(homes.agents, 'skills'));
140
+ add('project', join(homes.openclaw, 'workspace', 'skills')); add('project', join(homes.openclaw, 'workspace', '.agents', 'skills'));
141
+ }
142
+ return out.filter(([, root]) => directory(root, readerCwd));
143
+ }
144
+ function frontmatter(manifest, cwd) {
145
+ const source = read(manifest, cwd), out = new Map(); if (source == null) return out;
146
+ const lines = truncateScalars(source, 8192).split('\n').map(line => line.endsWith('\r') ? line.slice(0, -1) : line);
147
+ if (nativeTrim(lines.shift() ?? '') !== '---') return out;
148
+ let pending;
149
+ for (const line of lines) {
150
+ const trimmed = line.replace(new RegExp(`${whitespace.source}$`, 'u'), ''), item = nativeTrim(trimmed);
151
+ if (item === '---' || item === '...') break;
152
+ if (!trimmed || item.startsWith('#')) continue;
153
+ if (whitespace.test(trimmed[0])) {
154
+ if (pending && item.startsWith('- ')) { const value = unquote(item.slice(2)); if (value) out.set(pending, out.has(pending) && out.get(pending) ? `${out.get(pending)}, ${value}` : value); }
155
+ continue;
156
+ }
157
+ pending = undefined; const colon = trimmed.indexOf(':'); if (colon < 0) continue;
158
+ const key = asciiLower(nativeTrim(trimmed.slice(0, colon))), value = unquote(trimmed.slice(colon + 1));
159
+ if (!key) continue;
160
+ if (!value) pending = key;
161
+ else if (!out.has(key)) out.set(key, value);
162
+ }
163
+ return out;
164
+ }
165
+ const boolean = value => ['true', 'yes', 'on'].includes(asciiLower(unquote(value))) ? true : ['false', 'no', 'off'].includes(asciiLower(unquote(value))) ? false : null;
166
+ function collect(harness, scope, root, depth, out, cwd) {
167
+ if (out.length >= 512) return;
168
+ for (const directory of directories(root, cwd)) {
169
+ if (out.length >= 512) return;
170
+ const name = textName(components(directory).at(-1)?.[1] ?? Buffer.alloc(0));
171
+ if (name == null || name.startsWith('.') || skipped.has(name)) continue;
172
+ const manifest = join(directory, 'SKILL.md');
173
+ if (file(manifest, cwd)) {
174
+ const front = frontmatter(manifest, cwd), row = { name: nativeTrim(front.get('name') ?? '') || name, harness, scope, location: textName(directory), enabled: null };
175
+ if (front.has('description')) row.description = nativeTrim(front.get('description')).split(whitespace).join(' ');
176
+ const version = nativeTrim(front.get('version') ?? ''); if (version) row.version = version;
177
+ if (front.has('enabled')) row.enabled = boolean(front.get('enabled'));
178
+ else if (front.has('disable-model-invocation')) { const disabled = boolean(front.get('disable-model-invocation')); if (disabled != null) row.enabled = !disabled; }
179
+ if (row.location == null) throw new Error('skill location is not valid UTF-8'); out.push(row); continue;
180
+ }
181
+ const before = out.length; if (depth + 1 < 3) collect(harness, scope, directory, depth + 1, out, cwd);
182
+ if (before === out.length) { const location = textName(directory); if (location == null) throw new Error('skill location is not valid UTF-8'); out.push({ name, harness, scope, location, enabled: null }); }
183
+ }
184
+ }
185
+ function codexEnablement(homes, rows, cwd) {
186
+ const source = read(join(homes.codex, 'config.toml'), cwd); if (source == null) return;
187
+ let skills; try { skills = parseSkillToml(source).skills; } catch { return; }
188
+ for (const row of rows) {
189
+ if (row.harness !== 'codex') continue;
190
+ if (row.scope === 'bundled' && typeof skills?.bundled?.enabled === 'boolean') row.enabled = skills.bundled.enabled;
191
+ for (const entry of Array.isArray(skills?.config) ? skills.config : []) {
192
+ if (typeof entry?.enabled !== 'boolean') continue;
193
+ if (typeof entry.name === 'string' && entry.name === row.name || typeof entry.path === 'string' && pathKey(entry.path) === pathKey(row.location)) row.enabled = entry.enabled;
194
+ }
195
+ }
196
+ }
197
+ export function listInstalledSkills(params, { env = process.env, cwd = process.cwd() } = {}) {
198
+ const query = queryParams(params, { env, cwd }), rows = [], seen = new Set();
199
+ for (const harness of SKILL_HARNESSES) {
200
+ if (query.harness != null && query.harness !== harness) continue;
201
+ for (const [scope, root] of skillRoots(harness, query.homes, query.cwd, cwd)) {
202
+ if (query.scope != null && query.scope !== scope) continue;
203
+ const found = []; collect(harness, scope, root, 0, found, cwd);
204
+ for (const row of found) { const key = `${harness}\0${pathKey(row.location)}`; if (!seen.has(key)) { seen.add(key); rows.push(row); } }
205
+ }
206
+ }
207
+ codexEnablement(query.homes, rows, cwd);
208
+ return rows.sort((a, b) => compareText(a.harness, b.harness) || scopes.indexOf(a.scope) - scopes.indexOf(b.scope) || compareText(a.name, b.name) || pathCompare(a.location, b.location));
209
+ }
210
+
211
+ // Internal mutation helpers use the reader's PathBuf comparison and manifest
212
+ // rules. Existing runtime roots remain independent of the writable root table.
213
+ export { parent as skillPathParent, pathKey as skillPathKey };
214
+ export function skillPathName(path) {
215
+ const last = components(Buffer.from(path)).at(-1);
216
+ return last?.[0] === 4 ? last[1] : null;
217
+ }
218
+ export function writableSkillRoots(harness, scope, homes, cwd) {
219
+ const project = markers => markers.map(marker => join(cwd, ...marker));
220
+ if (scope === 'user') {
221
+ if (harness === 'claude-code') return [join(homes.claude_code, 'skills')];
222
+ if (harness === 'codex') return [join(homes.agents, 'skills'), join(homes.codex, 'skills')];
223
+ if (harness === 'opencode') return [join(homes.opencode, 'skill'), join(homes.opencode, 'skills')];
224
+ if (harness === 'pi') return [join(homes.pi, 'skills'), join(homes.agents, 'skills')];
225
+ } else if (scope === 'project') {
226
+ if (harness === 'claude-code') return project([['.claude', 'skills']]);
227
+ if (harness === 'codex') return project([['.agents', 'skills']]);
228
+ if (harness === 'opencode') return project([['.opencode', 'skill'], ['.opencode', 'skills']]);
229
+ if (harness === 'pi') return project([['.pi', 'skills'], ['.agents', 'skills']]);
230
+ }
231
+ return [];
232
+ }
233
+ export function declaredSkillName(path, cwd) {
234
+ const manifest = join(path, 'SKILL.md');
235
+ if (!file(manifest, cwd)) return null;
236
+ return nativeTrim(frontmatter(manifest, cwd).get('name') ?? '') || textName(components(Buffer.from(path)).at(-1)?.[1] ?? Buffer.alloc(0)) || null;
237
+ }
package/source-files.mjs CHANGED
@@ -16,8 +16,18 @@ export async function optionalFile(path, work, metrics) {
16
16
  catch (error) { if (missing(error)) return { path, missing: true, result: null }; throw error; }
17
17
  }
18
18
 
19
- export function readSourceJson(path, metrics) {
20
- return optionalFile(path, async (reader) => parseJsonLine(await reader.read(0, reader.size), { strictUtf8: true }), metrics);
19
+ const sourceJsonReads = new WeakSet();
20
+
21
+ /** Only a result from this process can reuse its parsed native-number facts.
22
+ * Opening and verifying the current file still checks identity and readability. */
23
+ export async function readSourceJson(path, metrics, previous) {
24
+ const source = await optionalFile(path, async (reader) => {
25
+ if (previous?.path === path && sourceJsonReads.has(previous) && sameFingerprint(previous.fingerprint, reader.fingerprint))
26
+ return previous.result;
27
+ return parseJsonLine(await reader.read(0, reader.size), { strictUtf8: true });
28
+ }, metrics);
29
+ sourceJsonReads.add(source);
30
+ return source;
21
31
  }
22
32
 
23
33
  /** A compound descriptor includes every auxiliary identity, including an