openclaw-plugin-vt-sentinel 0.7.0 → 0.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/dist/index.d.ts CHANGED
@@ -39,11 +39,38 @@ interface PluginApi {
39
39
  registerHook?: (events: string | string[], handler: (event: any) => Promise<any>, opts?: object) => void;
40
40
  onToolResult?: (handler: (event: any) => Promise<any>) => void;
41
41
  }
42
+ /**
43
+ * Read current plugin version from package.json.
44
+ */
45
+ declare function getCurrentVersion(): string;
42
46
  /**
43
47
  * Simple semver comparison: returns true if `latest` is newer than `current`.
44
48
  * Only handles x.y.z format (no pre-release tags).
45
49
  */
46
50
  export declare function isNewerVersion(latest: string, current: string): boolean;
51
+ /**
52
+ * Fetch latest version string from npm registry. Returns null on error.
53
+ * Single source of truth — used by checkForUpdates() and vt_sentinel_update.
54
+ */
55
+ declare function fetchLatestVersion(): Promise<string | null>;
56
+ /**
57
+ * Get the OpenClaw state directory (respects OPENCLAW_STATE_DIR env var).
58
+ */
59
+ declare function getStateDir(): string;
60
+ /**
61
+ * Generate update instructions or preview. Pure function — all inputs are arguments.
62
+ * Returns text for the agent/user.
63
+ */
64
+ declare function generateUpdateCommands(opts: {
65
+ currentVersion: string;
66
+ latestVersion: string;
67
+ confirm: boolean;
68
+ stateDir: string;
69
+ }): string;
47
70
  export declare function isSelfPath(filePath: string): boolean;
48
71
  export default function vtSentinelPlugin(api: PluginApi): void;
72
+ export declare const _generateUpdateCommands: typeof generateUpdateCommands;
73
+ export declare const _fetchLatestVersion: typeof fetchLatestVersion;
74
+ export declare const _getCurrentVersion: typeof getCurrentVersion;
75
+ export declare const _getStateDir: typeof getStateDir;
49
76
  export {};
package/dist/index.js CHANGED
@@ -36,6 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports._getStateDir = exports._getCurrentVersion = exports._fetchLatestVersion = exports._generateUpdateCommands = void 0;
39
40
  exports.isNewerVersion = isNewerVersion;
40
41
  exports.isSelfPath = isSelfPath;
41
42
  exports.default = vtSentinelPlugin;
@@ -78,6 +79,17 @@ function textResponse(text) {
78
79
  // --- Update Check ---
79
80
  const PACKAGE_NAME = 'openclaw-plugin-vt-sentinel';
80
81
  const NPM_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
82
+ /**
83
+ * Read current plugin version from package.json.
84
+ */
85
+ function getCurrentVersion() {
86
+ try {
87
+ return JSON.parse(fs.readFileSync(path.resolve(__dirname, '..', 'package.json'), 'utf-8')).version || '0.0.0';
88
+ }
89
+ catch {
90
+ return '0.0.0';
91
+ }
92
+ }
81
93
  /**
82
94
  * Simple semver comparison: returns true if `latest` is newer than `current`.
83
95
  * Only handles x.y.z format (no pre-release tags).
@@ -94,21 +106,98 @@ function isNewerVersion(latest, current) {
94
106
  return false;
95
107
  }
96
108
  /**
97
- * Check npm registry for a newer version. Fire-and-forget, never throws.
109
+ * Fetch latest version string from npm registry. Returns null on error.
110
+ * Single source of truth — used by checkForUpdates() and vt_sentinel_update.
98
111
  */
99
- async function checkForUpdates(logger) {
112
+ async function fetchLatestVersion() {
100
113
  try {
101
- const pkgPath = path.resolve(__dirname, '..', 'package.json');
102
- const currentVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version;
103
114
  const resp = await axios_1.default.get(NPM_REGISTRY_URL, { timeout: 5000 });
104
- const latestVersion = resp.data?.version;
105
- if (latestVersion && isNewerVersion(latestVersion, currentVersion)) {
115
+ return resp.data?.version || null;
116
+ }
117
+ catch {
118
+ return null;
119
+ }
120
+ }
121
+ /**
122
+ * Get the OpenClaw state directory (respects OPENCLAW_STATE_DIR env var).
123
+ */
124
+ function getStateDir() {
125
+ return process.env.OPENCLAW_STATE_DIR || path.join(os.homedir(), '.openclaw');
126
+ }
127
+ /**
128
+ * Generate update instructions or preview. Pure function — all inputs are arguments.
129
+ * Returns text for the agent/user.
130
+ */
131
+ function generateUpdateCommands(opts) {
132
+ if (!isNewerVersion(opts.latestVersion, opts.currentVersion)) {
133
+ return `VT Sentinel v${opts.currentVersion} is already the latest version.`;
134
+ }
135
+ if (!opts.confirm) {
136
+ const lines = [];
137
+ lines.push(`Update available: v${opts.currentVersion} → v${opts.latestVersion}`);
138
+ lines.push('');
139
+ lines.push('What will happen:');
140
+ lines.push(' - The plugin will be updated to the latest version');
141
+ lines.push(' - Your configuration, audit logs, and VTAI credentials are preserved');
142
+ lines.push(' - The gateway will need to be restarted');
143
+ lines.push('');
144
+ lines.push('Call vt_sentinel_update with confirm: true to get the upgrade commands.');
145
+ return lines.join('\n');
146
+ }
147
+ const stateDir = opts.stateDir;
148
+ const quotedExtDir = `"${path.join(stateDir, 'extensions', PACKAGE_NAME)}"`;
149
+ const configPath = path.join(stateDir, 'openclaw.json');
150
+ const lines = [];
151
+ lines.push(`Upgrade: v${opts.currentVersion} → v${opts.latestVersion}`);
152
+ lines.push('');
153
+ lines.push('Run these commands in a separate terminal (stopping the gateway will end this chat session):');
154
+ lines.push('');
155
+ lines.push(' 1. openclaw gateway stop');
156
+ lines.push(` 2. openclaw plugins update ${PACKAGE_NAME}`);
157
+ lines.push(' 3. openclaw gateway start');
158
+ lines.push('');
159
+ lines.push('Your configuration, audit logs, and credentials are preserved.');
160
+ lines.push('After restart, use vt_sentinel_status to verify the new version.');
161
+ lines.push('');
162
+ lines.push('---');
163
+ lines.push('If step 2 reports "already at X.Y.Z", the install spec may be version-pinned.');
164
+ lines.push('In that case, replace step 2 with:');
165
+ lines.push('');
166
+ lines.push(` 2a. Remove the extension directory:`);
167
+ lines.push(` rm -rf ${quotedExtDir} (Linux/macOS)`);
168
+ lines.push(` rmdir /s /q ${quotedExtDir.replace(/\//g, '\\\\')} (Windows)`);
169
+ lines.push('');
170
+ lines.push(` 2b. Back up and clean the config entry:`);
171
+ // Generate a safe node -e script for config cleanup
172
+ const cleanupScript = `node -e "const fs=require('fs'),p='${configPath.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}';try{const b=fs.readFileSync(p,'utf8');fs.writeFileSync(p+'.bak',b);const c=JSON.parse(b);if(c.plugins){delete(c.plugins.entries||{})['${PACKAGE_NAME}'];delete(c.plugins.installs||{})['${PACKAGE_NAME}'];}fs.writeFileSync(p,JSON.stringify(c,null,2));console.log('Config cleaned (backup: '+p+'.bak)')}catch(e){console.error('Failed: '+e.message);process.exit(1)}"`;
173
+ lines.push(` ${cleanupScript}`);
174
+ lines.push('');
175
+ lines.push(` 2c. Reinstall:`);
176
+ lines.push(` openclaw plugins install ${PACKAGE_NAME}`);
177
+ return lines.join('\n');
178
+ }
179
+ /**
180
+ * Check npm registry for a newer version. Fire-and-forget, never throws.
181
+ */
182
+ async function checkForUpdates(logger, callbacks) {
183
+ try {
184
+ const currentVersion = getCurrentVersion();
185
+ const latestVersion = await fetchLatestVersion();
186
+ if (!latestVersion) {
187
+ callbacks?.onError();
188
+ return;
189
+ }
190
+ if (isNewerVersion(latestVersion, currentVersion)) {
106
191
  logger.info(`[VT-Sentinel] Update available: ${currentVersion} → ${latestVersion}. ` +
107
- `Run: openclaw plugins install ${PACKAGE_NAME}`);
192
+ `Use vt_sentinel_update to check upgrade instructions.`);
193
+ callbacks?.onNewer(latestVersion);
194
+ }
195
+ else {
196
+ callbacks?.onUpToDate();
108
197
  }
109
198
  }
110
199
  catch {
111
- // Best-effort — silently ignore network/parse errors
200
+ callbacks?.onError();
112
201
  }
113
202
  }
114
203
  // --- Self-exclusion: never scan/quarantine our own plugin files ---
@@ -138,6 +227,9 @@ function vtSentinelPlugin(api) {
138
227
  let scanner = null;
139
228
  /** Tracks root directories passed to chokidar. Never use getWatched() for diffs. */
140
229
  const watchRoots = new Set();
230
+ // Update check state (closure-scoped, not module-level)
231
+ let latestKnownVersion = null;
232
+ let updateCheckFailed = false;
141
233
  const getConfig = () => {
142
234
  const entry = api.config?.plugins?.entries?.['openclaw-plugin-vt-sentinel'];
143
235
  return entry?.config ?? null;
@@ -701,15 +793,6 @@ function vtSentinelPlugin(api) {
701
793
  }
702
794
  },
703
795
  });
704
- // --- Helper: get current plugin version ---
705
- const getCurrentVersion = () => {
706
- try {
707
- return JSON.parse(fs.readFileSync(path.resolve(__dirname, '..', 'package.json'), 'utf-8')).version || '0.0.0';
708
- }
709
- catch {
710
- return '0.0.0';
711
- }
712
- };
713
796
  // --- Helper: apply config changes to scanner and watcher ---
714
797
  const applyConfigChange = (diff, newConfig) => {
715
798
  if (diff.scannerNeedsRebuild && scanner) {
@@ -782,6 +865,9 @@ function vtSentinelPlugin(api) {
782
865
  blockedFileCount: blocklist.size,
783
866
  runtimeOverrideCount: Object.keys(configManager.getRuntimeOverrides()).length,
784
867
  presetName: eff.configPreset,
868
+ updateAvailable: latestKnownVersion != null && !updateCheckFailed,
869
+ latestVersion: latestKnownVersion || undefined,
870
+ updateCheckFailed,
785
871
  }));
786
872
  },
787
873
  });
@@ -930,6 +1016,48 @@ function vtSentinelPlugin(api) {
930
1016
  return textResponse((0, status_renderer_1.renderHelp)());
931
1017
  },
932
1018
  });
1019
+ // --- Tool: vt_sentinel_update ---
1020
+ api.registerTool({
1021
+ name: 'vt_sentinel_update',
1022
+ description: 'Check for VT Sentinel updates and get upgrade instructions. Call with confirm: true to generate the exact commands to run in a separate terminal.',
1023
+ parameters: {
1024
+ type: 'object',
1025
+ properties: {
1026
+ confirm: {
1027
+ type: 'boolean',
1028
+ description: 'Set true to generate upgrade commands (default: just checks for updates)',
1029
+ },
1030
+ },
1031
+ required: [],
1032
+ },
1033
+ execute: async (_ctx, rawParams) => {
1034
+ const params = rawParams || {};
1035
+ // Strict validation: reject non-boolean confirm
1036
+ if ('confirm' in params && typeof params.confirm !== 'boolean') {
1037
+ return textResponse('Error: confirm must be true or false');
1038
+ }
1039
+ const latestVersion = await fetchLatestVersion();
1040
+ if (!latestVersion) {
1041
+ updateCheckFailed = true;
1042
+ return textResponse('Error: Could not reach npm registry. Check internet connectivity and try again.');
1043
+ }
1044
+ const currentVersion = getCurrentVersion();
1045
+ if (isNewerVersion(latestVersion, currentVersion)) {
1046
+ latestKnownVersion = latestVersion;
1047
+ updateCheckFailed = false;
1048
+ }
1049
+ else {
1050
+ latestKnownVersion = null;
1051
+ updateCheckFailed = false;
1052
+ }
1053
+ return textResponse(generateUpdateCommands({
1054
+ currentVersion,
1055
+ latestVersion,
1056
+ confirm: params.confirm === true,
1057
+ stateDir: getStateDir(),
1058
+ }));
1059
+ },
1060
+ });
933
1061
  // --- Hook: auto-scan tool results ---
934
1062
  const handleToolResult = async (event) => {
935
1063
  const s = await ensureScanner();
@@ -946,10 +1074,8 @@ function vtSentinelPlugin(api) {
946
1074
  };
947
1075
  if (!stateStore.isFirstRunShown(scope)) {
948
1076
  try {
949
- const pkgPath = path.resolve(__dirname, '..', 'package.json');
950
- const version = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version || '0.0.0';
951
1077
  const onboardingText = (0, status_renderer_1.renderOnboarding)({
952
- version,
1078
+ version: getCurrentVersion(),
953
1079
  apiMode: process.env.VIRUSTOTAL_API_KEY === 'vtai-active' ? 'vtai' : 'user_key',
954
1080
  watchDirs: [...watchRoots],
955
1081
  effectiveConfig: configManager.getEffective(),
@@ -957,6 +1083,7 @@ function vtSentinelPlugin(api) {
957
1083
  'vt_scan_file', 'vt_check_hash', 'vt_upload_consent',
958
1084
  'vt_sentinel_status', 'vt_sentinel_configure',
959
1085
  'vt_sentinel_reset_policy', 'vt_sentinel_help',
1086
+ 'vt_sentinel_update',
960
1087
  ],
961
1088
  });
962
1089
  const injected = injectOnboarding(event, onboardingText);
@@ -1171,9 +1298,13 @@ function vtSentinelPlugin(api) {
1171
1298
  vtSentinelPlugin._computeAutoWatchDirs = computeAutoWatchDirs;
1172
1299
  vtSentinelPlugin._handleWatcherFile = handleWatcherFile;
1173
1300
  vtSentinelPlugin._enrichFromContext = enrichFromContext;
1174
- api.logger.info('[VT-Sentinel] Plugin loaded — 7 tools + active protection hooks registered (VTAI auto-registration enabled)');
1301
+ api.logger.info('[VT-Sentinel] Plugin loaded — 8 tools + active protection hooks registered (VTAI auto-registration enabled)');
1175
1302
  // Non-blocking update check (fire-and-forget)
1176
- checkForUpdates(api.logger);
1303
+ checkForUpdates(api.logger, {
1304
+ onNewer: (v) => { latestKnownVersion = v; updateCheckFailed = false; },
1305
+ onUpToDate: () => { latestKnownVersion = null; updateCheckFailed = false; },
1306
+ onError: () => { updateCheckFailed = true; },
1307
+ });
1177
1308
  }
1178
1309
  // --- Hook helpers ---
1179
1310
  function extractResultText(event) {
@@ -1228,3 +1359,9 @@ function injectWarning(event, result) {
1228
1359
  event.toolResult._vtSentinelWarning = warning;
1229
1360
  }
1230
1361
  }
1362
+ // --- Test exports ---
1363
+ // Exported for unit testing only. Not part of the public API.
1364
+ exports._generateUpdateCommands = generateUpdateCommands;
1365
+ exports._fetchLatestVersion = fetchLatestVersion;
1366
+ exports._getCurrentVersion = getCurrentVersion;
1367
+ exports._getStateDir = getStateDir;
@@ -14,6 +14,9 @@ export declare function renderStatus(opts: {
14
14
  blockedFileCount: number;
15
15
  runtimeOverrideCount: number;
16
16
  presetName: string;
17
+ updateAvailable?: boolean;
18
+ latestVersion?: string;
19
+ updateCheckFailed?: boolean;
17
20
  }): string;
18
21
  export declare function renderPolicyMatrix(config: FullConfig): string;
19
22
  export declare function renderHelp(): string;
@@ -38,6 +38,12 @@ function renderStatus(opts) {
38
38
  const lines = [];
39
39
  const cfg = opts.effectiveConfig;
40
40
  lines.push(`VT Sentinel v${opts.version} — Status`);
41
+ if (opts.updateAvailable && opts.latestVersion) {
42
+ lines.push(` Update available: v${opts.version} → v${opts.latestVersion} — use vt_sentinel_update for upgrade instructions`);
43
+ }
44
+ else if (opts.updateCheckFailed) {
45
+ lines.push(' Update check: last check failed (network error). Use vt_sentinel_update to retry.');
46
+ }
41
47
  lines.push('');
42
48
  // Config
43
49
  lines.push('Effective Configuration:');
@@ -138,6 +144,9 @@ function renderHelp() {
138
144
  lines.push(' vt_sentinel_help {}');
139
145
  lines.push(' Show this guide');
140
146
  lines.push('');
147
+ lines.push(' vt_sentinel_update { confirm: true }');
148
+ lines.push(' Check for updates and get upgrade instructions');
149
+ lines.push('');
141
150
  lines.push('PRESETS:');
142
151
  lines.push(' balanced (default)');
143
152
  lines.push(' Ask before uploading sensitive files. Quarantine malicious. Log all scans.');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-plugin-vt-sentinel",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "description": "VirusTotal Sentinel for OpenClaw - Malware detection and AI-powered code analysis",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -140,6 +140,14 @@ Shows usage examples, privacy explanation, and available presets.
140
140
  vt_sentinel_help {}
141
141
  ```
142
142
 
143
+ ### `vt_sentinel_update` — Check for Updates
144
+ Checks npm for a newer version and generates upgrade instructions.
145
+
146
+ ```
147
+ vt_sentinel_update {}
148
+ vt_sentinel_update { "confirm": true }
149
+ ```
150
+
143
151
  ## Active Protection
144
152
 
145
153
  VT Sentinel automatically protects the system in real-time: