@js-eyes/protocol 2.6.2 → 2.8.1

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/index.js CHANGED
@@ -24,13 +24,12 @@ const FORWARDABLE_ACTIONS = [
24
24
  ];
25
25
 
26
26
  const SENSITIVE_TOOL_NAMES = Object.freeze([
27
- 'js_eyes_execute_script',
28
- 'js_eyes_get_cookies',
29
- 'js_eyes_get_cookies_by_domain',
30
- 'js_eyes_inject_css',
31
- 'js_eyes_upload_file',
32
- 'js_eyes_upload_file_to_tab',
33
- 'js_eyes_install_skill',
27
+ 'browser/execute-script',
28
+ 'browser/get-cookies',
29
+ 'browser/get-cookies-by-domain',
30
+ 'browser/inject-css',
31
+ 'browser/upload-file',
32
+ 'skills/plan-install',
34
33
  ]);
35
34
 
36
35
  const LOOPBACK_HOSTS = Object.freeze(['localhost', '127.0.0.1', '::1', '::ffff:127.0.0.1', '0:0:0:0:0:0:0:1']);
@@ -79,13 +78,12 @@ const DEFAULT_SECURITY_CONFIG = Object.freeze({
79
78
  taint: { ...DEFAULT_TAINT_CONFIG },
80
79
  profile: { ...DEFAULT_PROFILE_CONFIG },
81
80
  toolPolicies: {
82
- js_eyes_execute_script: 'confirm',
83
- js_eyes_get_cookies: 'confirm',
84
- js_eyes_get_cookies_by_domain: 'confirm',
85
- js_eyes_inject_css: 'confirm',
86
- js_eyes_upload_file: 'confirm',
87
- js_eyes_upload_file_to_tab: 'confirm',
88
- js_eyes_install_skill: 'confirm',
81
+ 'browser/execute-script': 'confirm',
82
+ 'browser/get-cookies': 'confirm',
83
+ 'browser/get-cookies-by-domain': 'confirm',
84
+ 'browser/inject-css': 'confirm',
85
+ 'browser/upload-file': 'confirm',
86
+ 'skills/plan-install': 'confirm',
89
87
  },
90
88
  sensitiveCookieDomains: [
91
89
  'bank',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@js-eyes/protocol",
3
- "version": "2.6.2",
3
+ "version": "2.8.1",
4
4
  "description": "Shared protocol constants for JS Eyes runtime packages",
5
5
  "main": "index.js",
6
6
  "files": [
package/safe-npm.js CHANGED
@@ -11,9 +11,9 @@
11
11
  // process.env (tokens, OAuth state, etc.) never leak into the npm run;
12
12
  // * postinstall scripts are disabled unless the caller explicitly opts in.
13
13
  //
14
- // The single allowed binary name is `npm`. No wildcards, no PATHEXT, no shell
15
- // meta-characters: Node's child_process with shell=false treats the argv as a
16
- // literal argument vector.
14
+ // The only allowed binary is npm (or the fixed cmd.exe/npm.cmd wrapper Windows
15
+ // requires for command shims). No wildcards or user-controlled argv: every npm
16
+ // argument still comes from immutable constants below.
17
17
 
18
18
  const fs = require('fs');
19
19
  const path = require('path');
@@ -26,6 +26,7 @@ const ALLOWED_SUBCOMMANDS = Object.freeze({
26
26
 
27
27
  const SAFE_ENV_KEYS = Object.freeze([
28
28
  'PATH',
29
+ 'Path',
29
30
  'HOME',
30
31
  'USERPROFILE',
31
32
  'APPDATA',
@@ -82,7 +83,12 @@ function runNpm(subcommand, targetDir, options = {}) {
82
83
  npm_config_ignore_scripts: allowPostinstall ? 'false' : 'true',
83
84
  });
84
85
 
85
- const result = spawnSync('npm', baseArgs, {
86
+ const bin = process.platform === 'win32' ? 'cmd.exe' : 'npm';
87
+ const args = process.platform === 'win32'
88
+ ? ['/d', '/s', '/c', 'npm.cmd', ...baseArgs]
89
+ : baseArgs;
90
+
91
+ const result = spawnSync(bin, args, {
86
92
  cwd: targetDir,
87
93
  stdio: options.stdio || 'pipe',
88
94
  shell: false,
@@ -97,7 +103,8 @@ function safeNpmCi(targetDir, options = {}) {
97
103
  const { result, args } = runNpm('ci', targetDir, options);
98
104
  if (result.status !== 0) {
99
105
  const stderr = result.stderr ? String(result.stderr) : '';
100
- throw new Error(`npm ${args.join(' ')} 失败 (status=${result.status}): ${stderr.slice(0, 500)}`);
106
+ const reason = result.error ? result.error.message : stderr;
107
+ throw new Error(`npm ${args.join(' ')} 失败 (status=${result.status}): ${String(reason || '').slice(0, 500)}`);
101
108
  }
102
109
  return { ran: true, manager: 'npm', args };
103
110
  }
@@ -106,7 +113,8 @@ function safeNpmInstall(targetDir, options = {}) {
106
113
  const { result, args } = runNpm('install', targetDir, options);
107
114
  if (result.status !== 0) {
108
115
  const stderr = result.stderr ? String(result.stderr) : '';
109
- throw new Error(`npm ${args.join(' ')} 失败 (status=${result.status}): ${stderr.slice(0, 500)}`);
116
+ const reason = result.error ? result.error.message : stderr;
117
+ throw new Error(`npm ${args.join(' ')} 失败 (status=${result.status}): ${String(reason || '').slice(0, 500)}`);
110
118
  }
111
119
  return { ran: true, manager: 'npm', args };
112
120
  }
package/skill-registry.js CHANGED
@@ -3,7 +3,12 @@
3
3
  /**
4
4
  * SkillRegistry — js-eyes 技能运行时注册表
5
5
  *
6
- * 通过「工具级 dispatcher 间接层」实现零重启热加载:
6
+ * 通过两种绑定模式实现零重启热加载:
7
+ * - routerMode=true(OpenClaw 单工具模式):只维护 actionBindings,由 `js-eyes`
8
+ * 总线工具按 `skill/<skillId>/<action>` 委派执行,不向宿主注册子技能工具;
9
+ * - routerMode=false(兼容/测试模式):保留工具级 dispatcher 间接层。
10
+ *
11
+ * 兼容 dispatcher 模式:
7
12
  * - 插件启动时 api.registerTool(name, dispatcher) 仅对每个 tool name 注册一次稳定闭包;
8
13
  * - 每个 dispatcher 在调用时从 toolBindings.get(name) 查当前实现并委派;
9
14
  * - 热加载只改 toolBindings 映射,不触碰宿主注册表。
@@ -15,6 +20,8 @@
15
20
  * 让 OpenClaw / LLM 看到正确的入参约束(required / properties 等)。热加载时若 contract
16
21
  * 改了 schema,会按**引用** mutate 已注册的 dispatcher 对象;若某些 OpenClaw 宿主对 tool
17
22
  * 对象做了深拷贝/快照,则 mutate 不会生效,但首次注册的 schema 仍然是正确的。
23
+ * routerMode 下 OpenClaw 只看到 `js-eyes` 的总线 schema,子技能 schema 作为内部 definition
24
+ * 保留给后续 introspection / 文档输出使用。
18
25
  */
19
26
 
20
27
  const fs = require('fs');
@@ -30,6 +37,7 @@ function isSkillEnabled(...args) { return skillsApi.isSkillEnabled(...args); }
30
37
  function loadSkillContract(...args) { return skillsApi.loadSkillContract(...args); }
31
38
  function readSkillIntegrity(...args) { return skillsApi.readSkillIntegrity(...args); }
32
39
  function resolveSkillSources(...args) { return skillsApi.resolveSkillSources(...args); }
40
+ function skillToolActionName(...args) { return skillsApi.skillToolActionName(...args); }
33
41
  function verifySkillIntegrity(...args) { return skillsApi.verifySkillIntegrity(...args); }
34
42
 
35
43
  const { verifyExtraDir: verifyExtraSkillDir } = require('./extra-integrity');
@@ -51,6 +59,17 @@ function makeLogger(candidate) {
51
59
  };
52
60
  }
53
61
 
62
+ function isBundledPrimarySkill(skill) {
63
+ if (!skill || skill.source !== 'primary') return false;
64
+ const sourcePath = skill.sourcePath || '';
65
+ if (!sourcePath || path.basename(sourcePath) !== 'skills') return false;
66
+ const bundleRoot = path.dirname(sourcePath);
67
+ return (
68
+ fs.existsSync(path.join(bundleRoot, 'openclaw-plugin'))
69
+ && fs.existsSync(path.join(bundleRoot, 'skills'))
70
+ );
71
+ }
72
+
54
73
  /**
55
74
  * 计算 skillDir 内"驱动热更"的关键文件的指纹(mtime 组合)。
56
75
  * 任一文件缺失按 0 处理;出错时退化为空字符串(此时 reload 语义保守:会认为"没变")。
@@ -118,6 +137,7 @@ function createSkillRegistry(options = {}) {
118
137
  pluginConfig = {},
119
138
  wrapSensitiveTool = null,
120
139
  builtinToolNames = [],
140
+ routerMode = false,
121
141
  // When true (default when a watcher is active), we set suppressNextReload
122
142
  // after our own setConfigValue writes so the chokidar echo doesn't cause a
123
143
  // duplicate reload. Leave this false in test harnesses that drive reload()
@@ -125,11 +145,11 @@ function createSkillRegistry(options = {}) {
125
145
  suppressSelfWrites = true,
126
146
  } = options;
127
147
 
128
- if (!api || typeof api.registerTool !== 'function') {
148
+ if (!routerMode && (!api || typeof api.registerTool !== 'function')) {
129
149
  throw new Error('createSkillRegistry: api.registerTool is required');
130
150
  }
131
151
 
132
- const logger = makeLogger(options.logger || api.logger);
152
+ const logger = makeLogger(options.logger || (api && api.logger));
133
153
  const configLoader = typeof options.configLoader === 'function'
134
154
  ? options.configLoader
135
155
  : () => ({});
@@ -148,6 +168,8 @@ function createSkillRegistry(options = {}) {
148
168
  const skills = new Map();
149
169
  // toolName -> { skillId, definition, optional }
150
170
  const toolBindings = new Map();
171
+ // action -> { skillId, toolName, definition, optional }
172
+ const actionBindings = new Map();
151
173
  // toolName -> dispatcher object (registered once per name; kept by reference so we can
152
174
  // mutate `description`/`parameters`/`label` on hot-reload to keep OpenClaw-visible schema in sync)
153
175
  const dispatchers = new Map();
@@ -255,6 +277,11 @@ function createSkillRegistry(options = {}) {
255
277
  removed.push(name);
256
278
  }
257
279
  }
280
+ for (const [action, binding] of actionBindings) {
281
+ if (binding.skillId === skillId) {
282
+ actionBindings.delete(action);
283
+ }
284
+ }
258
285
  return removed;
259
286
  }
260
287
 
@@ -336,6 +363,7 @@ function createSkillRegistry(options = {}) {
336
363
  contract,
337
364
  adapter,
338
365
  toolNames: toolDefs.map((t) => t.toolName),
366
+ actionNames: toolDefs.map((t) => skillToolActionName(skill.id, t.toolName)),
339
367
  // runtime.dispose is used by hot-unload to drain WS, clear intervals, etc.
340
368
  dispose: async () => {
341
369
  const runtime = adapter && adapter.runtime;
@@ -354,17 +382,35 @@ function createSkillRegistry(options = {}) {
354
382
 
355
383
  function applyBindings(state) {
356
384
  const failedDispatchers = [];
385
+ const localActions = new Set();
357
386
  for (const entry of state.toolDefs) {
358
- const dispatched = ensureDispatcher(entry.toolName, entry.optional, entry.definition);
359
- if (!dispatched.ok) {
387
+ if (!routerMode) {
388
+ const dispatched = ensureDispatcher(entry.toolName, entry.optional, entry.definition);
389
+ if (!dispatched.ok) {
390
+ failedDispatchers.push(entry.toolName);
391
+ continue;
392
+ }
393
+ }
394
+ const actionName = skillToolActionName(state.id, entry.toolName);
395
+ if (localActions.has(actionName)) {
396
+ logger.warn(
397
+ `[js-eyes] Skill "${state.id}" skipped tool "${entry.toolName}" because action "${actionName}" duplicates another tool after slug normalization`,
398
+ );
360
399
  failedDispatchers.push(entry.toolName);
361
400
  continue;
362
401
  }
402
+ localActions.add(actionName);
363
403
  toolBindings.set(entry.toolName, {
364
404
  skillId: state.id,
365
405
  definition: entry.definition,
366
406
  optional: entry.optional,
367
407
  });
408
+ actionBindings.set(actionName, {
409
+ skillId: state.id,
410
+ toolName: entry.toolName,
411
+ definition: entry.definition,
412
+ optional: entry.optional,
413
+ });
368
414
  }
369
415
  return { failedDispatchers };
370
416
  }
@@ -475,9 +521,15 @@ function createSkillRegistry(options = {}) {
475
521
  return { ok: false };
476
522
  }
477
523
  if (!integrity.hasIntegrity) {
478
- logger.warn(
479
- `[js-eyes] Skill "${skill.id}" has no .integrity.json (legacy install); load allowed but consider reinstalling`,
480
- );
524
+ if (isBundledPrimarySkill(skill)) {
525
+ logger.info(
526
+ `[js-eyes] Skill "${skill.id}" has no .integrity.json because it is loaded from the bundled/source primary skills directory (${skill.sourcePath}); load allowed. Registry-installed primary skills should carry .integrity.json for tamper checks.`,
527
+ );
528
+ } else {
529
+ logger.warn(
530
+ `[js-eyes] Skill "${skill.id}" has no .integrity.json (legacy primary install); load allowed, but reinstall via \`js-eyes skills install ${skill.id}\` to restore tamper-check metadata`,
531
+ );
532
+ }
481
533
  }
482
534
  return { ok: true };
483
535
  }
@@ -646,12 +698,27 @@ function createSkillRegistry(options = {}) {
646
698
  sourcePath: s.sourcePath,
647
699
  skillDir: s.skillDir,
648
700
  tools: s.toolNames.slice(),
701
+ actions: s.actionNames.slice(),
649
702
  })),
650
703
  toolBindings: Array.from(toolBindings.keys()),
704
+ actionBindings: Array.from(actionBindings.keys()),
651
705
  dispatchersRegistered: Array.from(dispatchers.keys()),
652
706
  };
653
707
  }
654
708
 
709
+ async function executeAction(action, toolCallId, params) {
710
+ const binding = actionBindings.get(action);
711
+ if (!binding) {
712
+ return { content: [{ type: 'text', text: DEFAULT_UNAVAILABLE_MESSAGE(action) }] };
713
+ }
714
+ return binding.definition.execute(toolCallId, params);
715
+ }
716
+
717
+ function getActionDefinition(action) {
718
+ const binding = actionBindings.get(action);
719
+ return binding ? binding.definition : null;
720
+ }
721
+
655
722
  return {
656
723
  init,
657
724
  reload,
@@ -659,9 +726,12 @@ function createSkillRegistry(options = {}) {
659
726
  disposeAll,
660
727
  snapshot,
661
728
  getState,
729
+ executeAction,
730
+ getActionDefinition,
662
731
  // Testing / plugin integration helpers
663
732
  _internals: {
664
733
  toolBindings,
734
+ actionBindings,
665
735
  skills,
666
736
  dispatchers,
667
737
  setSuppressNextReload(v) { suppressNextReload = Boolean(v); },
package/skills.js CHANGED
@@ -13,6 +13,20 @@ const SKILL_CONTRACT_FILE = 'skill.contract.js';
13
13
  const INTEGRITY_FILE = '.integrity.json';
14
14
  const INSTALL_MANIFEST_FILE = 'skills-install.json';
15
15
 
16
+ function toolNameToActionSegment(name) {
17
+ return String(name || '')
18
+ .trim()
19
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
20
+ .replace(/[^a-zA-Z0-9]+/g, '-')
21
+ .replace(/^-+|-+$/g, '')
22
+ .toLowerCase();
23
+ }
24
+
25
+ function skillToolActionName(skillId, toolName) {
26
+ const action = toolNameToActionSegment(toolName);
27
+ return `skill/${skillId}/${action || 'run'}`;
28
+ }
29
+
16
30
  function loadSkillContract(skillDir) {
17
31
  const contractPath = path.resolve(skillDir, SKILL_CONTRACT_FILE);
18
32
  if (!fs.existsSync(contractPath)) return null;
@@ -142,8 +156,10 @@ function normalizeSkillMetadata(skillDir) {
142
156
  ? cli.commands.map((command) => command.name)
143
157
  : [];
144
158
 
159
+ const id = contract?.id || pkg.name || path.basename(skillDir);
160
+
145
161
  return {
146
- id: contract?.id || pkg.name || path.basename(skillDir),
162
+ id,
147
163
  name: contract?.name || pkg.name || path.basename(skillDir),
148
164
  version: contract?.version || pkg.version || '1.0.0',
149
165
  description: contract?.description || pkg.description || '',
@@ -151,6 +167,7 @@ function normalizeSkillMetadata(skillDir) {
151
167
  cliEntry: cli.entry ? path.resolve(skillDir, cli.entry) : path.join(skillDir, 'index.js'),
152
168
  commands,
153
169
  tools,
170
+ actions: tools.map((tool) => skillToolActionName(id, tool)),
154
171
  runtime: contract?.runtime || {},
155
172
  contract,
156
173
  };
@@ -728,6 +745,8 @@ Object.assign(module.exports, {
728
745
  resolveSkillsDir,
729
746
  resolveOpenClawPluginEntry,
730
747
  runSkillCli,
748
+ skillToolActionName,
749
+ toolNameToActionSegment,
731
750
  verifySkillIntegrity,
732
751
  writeIntegrityManifest,
733
752
  });