vibeusage 0.2.20 → 0.2.21
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibeusage",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.21",
|
|
4
4
|
"description": "Codex CLI token usage tracker (macOS-first, notify-driven).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"type": "commonjs",
|
|
10
10
|
"scripts": {
|
|
11
11
|
"test": "node --test test/*.test.js",
|
|
12
|
+
"ci:local": "npm test && npm run validate:copy && npm run validate:ui-hardcode && npm run validate:guardrails && node --test test/architecture-guardrails.test.js && npm run build:insforge:check && npm --prefix dashboard run build",
|
|
12
13
|
"smoke": "node scripts/smoke/insforge-smoke.cjs",
|
|
13
14
|
"build:insforge": "node scripts/build-insforge-functions.cjs",
|
|
14
15
|
"build:insforge:check": "node scripts/build-insforge-functions.cjs --check",
|
|
@@ -24,6 +25,7 @@
|
|
|
24
25
|
"architecture:canvas:focus": "node scripts/ops/architecture-canvas.cjs --focus",
|
|
25
26
|
"architecture:canvas:list-modules": "node scripts/ops/architecture-canvas.cjs --list-modules",
|
|
26
27
|
"validate:guardrails": "node scripts/validate-architecture-guardrails.cjs",
|
|
28
|
+
"validate:retros": "node scripts/validate-retros.cjs",
|
|
27
29
|
"validate:insforge2-db": "node scripts/ops/insforge2-db-validate.cjs",
|
|
28
30
|
"graph:scip": "node scripts/graph/generate-scip.cjs",
|
|
29
31
|
"graph:auto-index": "node scripts/graph/auto-index.cjs"
|
package/src/commands/init.js
CHANGED
|
@@ -22,7 +22,11 @@ const {
|
|
|
22
22
|
isGeminiHookConfigured
|
|
23
23
|
} = require('../lib/gemini-config');
|
|
24
24
|
const { resolveOpencodeConfigDir, upsertOpencodePlugin, isOpencodePluginInstalled } = require('../lib/opencode-config');
|
|
25
|
-
const {
|
|
25
|
+
const { removeOpenclawHookConfig, probeOpenclawHookState } = require('../lib/openclaw-hook');
|
|
26
|
+
const {
|
|
27
|
+
installOpenclawSessionPlugin,
|
|
28
|
+
probeOpenclawSessionPluginState
|
|
29
|
+
} = require('../lib/openclaw-session-plugin');
|
|
26
30
|
const { beginBrowserAuth, openInBrowser } = require('../lib/browser-auth');
|
|
27
31
|
const {
|
|
28
32
|
issueDeviceTokenWithPassword,
|
|
@@ -408,32 +412,47 @@ async function applyIntegrationSetup({ home, trackerDir, notifyPath, notifyOrigi
|
|
|
408
412
|
summary.push({ label: 'Opencode Plugin', status: opencodeResult?.changed ? 'installed' : 'set', detail: 'Plugin installed' });
|
|
409
413
|
}
|
|
410
414
|
|
|
411
|
-
const openclawBefore = await
|
|
412
|
-
const openclawInstall = await
|
|
415
|
+
const openclawBefore = await probeOpenclawSessionPluginState({ home, trackerDir, env: process.env });
|
|
416
|
+
const openclawInstall = await installOpenclawSessionPlugin({
|
|
417
|
+
home,
|
|
418
|
+
trackerDir,
|
|
419
|
+
packageName: 'vibeusage',
|
|
420
|
+
env: process.env
|
|
421
|
+
});
|
|
413
422
|
if (openclawInstall?.skippedReason === 'openclaw-cli-missing') {
|
|
414
|
-
summary.push({ label: 'OpenClaw
|
|
415
|
-
} else if (openclawInstall?.skippedReason === 'openclaw-
|
|
423
|
+
summary.push({ label: 'OpenClaw Session Plugin', status: 'skipped', detail: 'OpenClaw CLI not found' });
|
|
424
|
+
} else if (openclawInstall?.skippedReason === 'openclaw-plugins-install-failed') {
|
|
416
425
|
summary.push({
|
|
417
|
-
label: 'OpenClaw
|
|
426
|
+
label: 'OpenClaw Session Plugin',
|
|
418
427
|
status: 'skipped',
|
|
419
428
|
detail: `Install failed${openclawInstall.error ? `: ${openclawInstall.error}` : ''}`
|
|
420
429
|
});
|
|
421
430
|
} else if (openclawInstall?.skippedReason === 'openclaw-config-unreadable') {
|
|
422
431
|
summary.push({
|
|
423
|
-
label: 'OpenClaw
|
|
432
|
+
label: 'OpenClaw Session Plugin',
|
|
424
433
|
status: 'skipped',
|
|
425
434
|
detail: openclawInstall.error ? `OpenClaw config unreadable: ${openclawInstall.error}` : 'OpenClaw config unreadable'
|
|
426
435
|
});
|
|
427
436
|
} else if (openclawInstall?.configured) {
|
|
428
437
|
summary.push({
|
|
429
|
-
label: 'OpenClaw
|
|
438
|
+
label: 'OpenClaw Session Plugin',
|
|
430
439
|
status: openclawBefore?.configured ? 'set' : 'installed',
|
|
431
440
|
detail: openclawBefore?.configured
|
|
432
|
-
? '
|
|
433
|
-
: '
|
|
441
|
+
? 'Session plugin already linked'
|
|
442
|
+
: 'Session plugin linked (restart OpenClaw gateway to activate)'
|
|
434
443
|
});
|
|
435
444
|
} else {
|
|
436
|
-
summary.push({ label: 'OpenClaw
|
|
445
|
+
summary.push({ label: 'OpenClaw Session Plugin', status: 'skipped', detail: 'OpenClaw session plugin unavailable' });
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const legacyHookState = await probeOpenclawHookState({ home, trackerDir, env: process.env });
|
|
449
|
+
if (legacyHookState?.configured || legacyHookState?.linked || legacyHookState?.enabled) {
|
|
450
|
+
await removeOpenclawHookConfig({ home, trackerDir, env: process.env });
|
|
451
|
+
summary.push({
|
|
452
|
+
label: 'OpenClaw Hook (legacy)',
|
|
453
|
+
status: 'updated',
|
|
454
|
+
detail: 'Removed legacy command hook (migrated to session plugin)'
|
|
455
|
+
});
|
|
437
456
|
}
|
|
438
457
|
|
|
439
458
|
const codeProbe = await probeFile(context.codeConfigPath);
|
|
@@ -515,22 +534,31 @@ async function previewIntegrations({ context }) {
|
|
|
515
534
|
detail: opencodeDetail
|
|
516
535
|
});
|
|
517
536
|
|
|
518
|
-
const openclawState = await
|
|
537
|
+
const openclawState = await probeOpenclawSessionPluginState({ home, trackerDir: context.trackerDir, env: process.env });
|
|
519
538
|
if (openclawState?.skippedReason === 'openclaw-config-missing') {
|
|
520
|
-
summary.push({ label: 'OpenClaw
|
|
539
|
+
summary.push({ label: 'OpenClaw Session Plugin', status: 'skipped', detail: 'OpenClaw config not found' });
|
|
521
540
|
} else if (openclawState?.skippedReason === 'openclaw-config-unreadable') {
|
|
522
541
|
summary.push({
|
|
523
|
-
label: 'OpenClaw
|
|
542
|
+
label: 'OpenClaw Session Plugin',
|
|
524
543
|
status: 'skipped',
|
|
525
544
|
detail: openclawState.error ? `OpenClaw config unreadable: ${openclawState.error}` : 'OpenClaw config unreadable'
|
|
526
545
|
});
|
|
527
546
|
} else {
|
|
528
547
|
summary.push({
|
|
529
|
-
label: 'OpenClaw
|
|
548
|
+
label: 'OpenClaw Session Plugin',
|
|
530
549
|
status: openclawState?.configured ? 'set' : 'installed',
|
|
531
550
|
detail: openclawState?.configured
|
|
532
|
-
? '
|
|
533
|
-
: 'Will link
|
|
551
|
+
? 'Session plugin already linked'
|
|
552
|
+
: 'Will link session plugin (restart OpenClaw gateway to activate)'
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const legacyHookState = await probeOpenclawHookState({ home, trackerDir: context.trackerDir, env: process.env });
|
|
557
|
+
if (legacyHookState?.configured || legacyHookState?.linked || legacyHookState?.enabled) {
|
|
558
|
+
summary.push({
|
|
559
|
+
label: 'OpenClaw Hook (legacy)',
|
|
560
|
+
status: 'updated',
|
|
561
|
+
detail: 'Will remove legacy command hook during migration'
|
|
534
562
|
});
|
|
535
563
|
}
|
|
536
564
|
|
|
@@ -787,6 +815,12 @@ async function installLocalTrackerApp({ appDir }) {
|
|
|
787
815
|
const binFrom = path.join(packageRoot, 'bin', 'tracker.js');
|
|
788
816
|
const nodeModulesFrom = path.join(packageRoot, 'node_modules');
|
|
789
817
|
|
|
818
|
+
// When running from the installed local runtime (or when appDir is symlinked to this package),
|
|
819
|
+
// source and destination resolve to the same place. Do not delete appDir in that case.
|
|
820
|
+
if (await pathsPointToSameLocation(packageRoot, appDir)) {
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
|
|
790
824
|
const srcTo = path.join(appDir, 'src');
|
|
791
825
|
const binToDir = path.join(appDir, 'bin');
|
|
792
826
|
const binTo = path.join(binToDir, 'tracker.js');
|
|
@@ -801,6 +835,21 @@ async function installLocalTrackerApp({ appDir }) {
|
|
|
801
835
|
await copyRuntimeDependencies({ from: nodeModulesFrom, to: nodeModulesTo });
|
|
802
836
|
}
|
|
803
837
|
|
|
838
|
+
async function pathsPointToSameLocation(a, b) {
|
|
839
|
+
const aReal = await safeRealpath(a);
|
|
840
|
+
const bReal = await safeRealpath(b);
|
|
841
|
+
if (aReal && bReal) return aReal === bReal;
|
|
842
|
+
return path.resolve(a) === path.resolve(b);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
async function safeRealpath(p) {
|
|
846
|
+
try {
|
|
847
|
+
return await fs.realpath(p);
|
|
848
|
+
} catch (_err) {
|
|
849
|
+
return null;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
804
853
|
function spawnInitSync({ trackerBinPath, packageName }) {
|
|
805
854
|
const fallbackPkg = packageName || 'vibeusage';
|
|
806
855
|
const argv = ['sync', '--drain'];
|
package/src/commands/status.js
CHANGED
|
@@ -16,6 +16,7 @@ const { collectLocalSubscriptions } = require('../lib/subscriptions');
|
|
|
16
16
|
const { normalizeState: normalizeUploadState } = require('../lib/upload-throttle');
|
|
17
17
|
const { collectTrackerDiagnostics } = require('../lib/diagnostics');
|
|
18
18
|
const { probeOpenclawHookState } = require('../lib/openclaw-hook');
|
|
19
|
+
const { probeOpenclawSessionPluginState } = require('../lib/openclaw-session-plugin');
|
|
19
20
|
const { resolveTrackerPaths } = require('../lib/tracker-paths');
|
|
20
21
|
|
|
21
22
|
async function cmdStatus(argv = []) {
|
|
@@ -75,6 +76,7 @@ async function cmdStatus(argv = []) {
|
|
|
75
76
|
hookCommand: geminiHookCommand
|
|
76
77
|
});
|
|
77
78
|
const opencodePluginConfigured = await isOpencodePluginInstalled({ configDir: opencodeConfigDir });
|
|
79
|
+
const openclawSessionPluginState = await probeOpenclawSessionPluginState({ home, trackerDir, env: process.env });
|
|
78
80
|
const openclawHookState = await probeOpenclawHookState({ home, trackerDir, env: process.env });
|
|
79
81
|
|
|
80
82
|
const lastUpload = uploadThrottle.lastSuccessMs
|
|
@@ -125,7 +127,8 @@ async function cmdStatus(argv = []) {
|
|
|
125
127
|
`- Claude hooks: ${claudeHookConfigured ? 'set' : 'unset'}`,
|
|
126
128
|
`- Gemini hooks: ${geminiHookConfigured ? 'set' : 'unset'}`,
|
|
127
129
|
`- Opencode plugin: ${opencodePluginConfigured ? 'set' : 'unset'}`,
|
|
128
|
-
`- OpenClaw
|
|
130
|
+
`- OpenClaw session plugin: ${openclawSessionPluginState?.configured ? 'set' : 'unset'}`,
|
|
131
|
+
`- OpenClaw hook (legacy): ${openclawHookState?.configured ? 'set' : 'unset'}`,
|
|
129
132
|
...subscriptionLines,
|
|
130
133
|
''
|
|
131
134
|
]
|
|
@@ -12,6 +12,7 @@ const {
|
|
|
12
12
|
} = require('../lib/gemini-config');
|
|
13
13
|
const { resolveOpencodeConfigDir, removeOpencodePlugin } = require('../lib/opencode-config');
|
|
14
14
|
const { removeOpenclawHookConfig } = require('../lib/openclaw-hook');
|
|
15
|
+
const { removeOpenclawSessionPluginConfig } = require('../lib/openclaw-session-plugin');
|
|
15
16
|
const { resolveTrackerPaths } = require('../lib/tracker-paths');
|
|
16
17
|
|
|
17
18
|
async function cmdUninstall(argv) {
|
|
@@ -62,6 +63,7 @@ async function cmdUninstall(argv) {
|
|
|
62
63
|
const opencodeRemove = opencodeConfigExists
|
|
63
64
|
? await removeOpencodePlugin({ configDir: opencodeConfigDir })
|
|
64
65
|
: { removed: false, skippedReason: 'config-missing' };
|
|
66
|
+
const openclawSessionPluginRemove = await removeOpenclawSessionPluginConfig({ home, trackerDir, env: process.env });
|
|
65
67
|
const openclawHookRemove = await removeOpenclawHookConfig({ home, trackerDir, env: process.env });
|
|
66
68
|
|
|
67
69
|
// Remove installed notify handler.
|
|
@@ -114,11 +116,16 @@ async function cmdUninstall(argv) {
|
|
|
114
116
|
? '- Opencode plugin: skipped (unexpected content)'
|
|
115
117
|
: '- Opencode plugin: skipped'
|
|
116
118
|
: `- Opencode plugin: skipped (${opencodeConfigDir} not found)`,
|
|
119
|
+
openclawSessionPluginRemove?.removed
|
|
120
|
+
? `- OpenClaw session plugin removed: ${openclawSessionPluginRemove.openclawConfigPath}`
|
|
121
|
+
: openclawSessionPluginRemove?.skippedReason === 'openclaw-config-missing'
|
|
122
|
+
? '- OpenClaw session plugin: skipped (openclaw config not found)'
|
|
123
|
+
: '- OpenClaw session plugin: no change',
|
|
117
124
|
openclawHookRemove?.removed
|
|
118
|
-
? `- OpenClaw hook removed: ${openclawHookRemove.openclawConfigPath}`
|
|
125
|
+
? `- OpenClaw hook (legacy) removed: ${openclawHookRemove.openclawConfigPath}`
|
|
119
126
|
: openclawHookRemove?.skippedReason === 'openclaw-config-missing'
|
|
120
|
-
? '- OpenClaw hook: skipped (openclaw config not found)'
|
|
121
|
-
: '- OpenClaw hook: no change',
|
|
127
|
+
? '- OpenClaw hook (legacy): skipped (openclaw config not found)'
|
|
128
|
+
: '- OpenClaw hook (legacy): no change',
|
|
122
129
|
opts.purge ? `- Purged: ${path.join(home, '.vibeusage')}` : '- Purge: skipped (use --purge)',
|
|
123
130
|
''
|
|
124
131
|
].join('\n')
|
package/src/lib/diagnostics.js
CHANGED
|
@@ -14,6 +14,7 @@ const {
|
|
|
14
14
|
const { resolveOpencodeConfigDir, isOpencodePluginInstalled } = require('./opencode-config');
|
|
15
15
|
const { normalizeState: normalizeUploadState } = require('./upload-throttle');
|
|
16
16
|
const { probeOpenclawHookState } = require('./openclaw-hook');
|
|
17
|
+
const { probeOpenclawSessionPluginState } = require('./openclaw-session-plugin');
|
|
17
18
|
const { resolveTrackerPaths } = require('./tracker-paths');
|
|
18
19
|
|
|
19
20
|
async function collectTrackerDiagnostics({
|
|
@@ -69,6 +70,7 @@ async function collectTrackerDiagnostics({
|
|
|
69
70
|
hookCommand: geminiHookCommand
|
|
70
71
|
});
|
|
71
72
|
const opencodePluginConfigured = await isOpencodePluginInstalled({ configDir: opencodeConfigDir });
|
|
73
|
+
const openclawSessionPluginState = await probeOpenclawSessionPluginState({ home, trackerDir, env: process.env });
|
|
72
74
|
const openclawHookState = await probeOpenclawHookState({ home, trackerDir, env: process.env });
|
|
73
75
|
|
|
74
76
|
const lastSuccessAt = uploadThrottle.lastSuccessMs ? new Date(uploadThrottle.lastSuccessMs).toISOString() : null;
|
|
@@ -120,6 +122,9 @@ async function collectTrackerDiagnostics({
|
|
|
120
122
|
claude_hook_configured: claudeHookConfigured,
|
|
121
123
|
gemini_hook_configured: geminiHookConfigured,
|
|
122
124
|
opencode_plugin_configured: opencodePluginConfigured,
|
|
125
|
+
openclaw_session_plugin_configured: Boolean(openclawSessionPluginState?.configured),
|
|
126
|
+
openclaw_session_plugin_linked: Boolean(openclawSessionPluginState?.linked),
|
|
127
|
+
openclaw_session_plugin_enabled: Boolean(openclawSessionPluginState?.enabled),
|
|
123
128
|
openclaw_hook_configured: Boolean(openclawHookState?.configured),
|
|
124
129
|
openclaw_hook_linked: Boolean(openclawHookState?.linked),
|
|
125
130
|
openclaw_hook_enabled: Boolean(openclawHookState?.enabled)
|
package/src/lib/openclaw-hook.js
CHANGED
|
@@ -337,7 +337,8 @@ function buildHookHandler({ trackerDir, packageName = 'vibeusage', openclawHome
|
|
|
337
337
|
`\n` +
|
|
338
338
|
`function resolveSessionEntry(event) {\n` +
|
|
339
339
|
` const ctx = (event && event.context && typeof event.context === 'object') ? event.context : {};\n` +
|
|
340
|
-
` if (event
|
|
340
|
+
` if (!event || event.type !== 'command') return null;\n` +
|
|
341
|
+
` if (event.action === 'stop') return (ctx.sessionEntry && typeof ctx.sessionEntry === 'object') ? ctx.sessionEntry : null;\n` +
|
|
341
342
|
` if (ctx.previousSessionEntry && typeof ctx.previousSessionEntry === 'object') return ctx.previousSessionEntry;\n` +
|
|
342
343
|
` if (ctx.sessionEntry && typeof ctx.sessionEntry === 'object') return ctx.sessionEntry;\n` +
|
|
343
344
|
` return null;\n` +
|
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
const os = require('node:os');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const fs = require('node:fs/promises');
|
|
4
|
+
const fssync = require('node:fs');
|
|
5
|
+
const cp = require('node:child_process');
|
|
6
|
+
|
|
7
|
+
const OPENCLAW_SESSION_PLUGIN_ID = 'openclaw-session-sync';
|
|
8
|
+
const OPENCLAW_SESSION_PLUGIN_DIRNAME = 'openclaw-plugin';
|
|
9
|
+
|
|
10
|
+
function resolveOpenclawSessionPluginPaths({ home = os.homedir(), trackerDir, env = process.env } = {}) {
|
|
11
|
+
if (!trackerDir) throw new Error('trackerDir is required');
|
|
12
|
+
|
|
13
|
+
const openclawConfigPath =
|
|
14
|
+
normalizeString(env.OPENCLAW_CONFIG_PATH) || path.join(home, '.openclaw', 'openclaw.json');
|
|
15
|
+
|
|
16
|
+
const openclawHome =
|
|
17
|
+
normalizeString(env.VIBEUSAGE_OPENCLAW_HOME) ||
|
|
18
|
+
normalizeString(env.OPENCLAW_STATE_DIR) ||
|
|
19
|
+
path.join(home, '.openclaw');
|
|
20
|
+
|
|
21
|
+
const pluginDir = path.join(trackerDir, OPENCLAW_SESSION_PLUGIN_DIRNAME);
|
|
22
|
+
const pluginEntryDir = path.join(pluginDir, OPENCLAW_SESSION_PLUGIN_ID);
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
pluginId: OPENCLAW_SESSION_PLUGIN_ID,
|
|
26
|
+
pluginDir,
|
|
27
|
+
pluginEntryDir,
|
|
28
|
+
openclawConfigPath,
|
|
29
|
+
openclawHome
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function installOpenclawSessionPlugin({
|
|
34
|
+
home = os.homedir(),
|
|
35
|
+
trackerDir,
|
|
36
|
+
packageName = 'vibeusage',
|
|
37
|
+
env = process.env
|
|
38
|
+
} = {}) {
|
|
39
|
+
const paths = resolveOpenclawSessionPluginPaths({ home, trackerDir, env });
|
|
40
|
+
|
|
41
|
+
await ensureOpenclawSessionPluginFiles({
|
|
42
|
+
pluginDir: paths.pluginDir,
|
|
43
|
+
trackerDir,
|
|
44
|
+
packageName,
|
|
45
|
+
openclawHome: paths.openclawHome
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const installResult = runOpenclawCli(['plugins', 'install', '--link', paths.pluginEntryDir], env);
|
|
49
|
+
if (installResult.skippedReason) {
|
|
50
|
+
return { configured: false, ...paths, ...installResult };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const enableResult = runOpenclawCli(['plugins', 'enable', paths.pluginId], env);
|
|
54
|
+
if (enableResult.skippedReason) {
|
|
55
|
+
return {
|
|
56
|
+
configured: false,
|
|
57
|
+
...paths,
|
|
58
|
+
skippedReason: enableResult.skippedReason,
|
|
59
|
+
error: enableResult.error,
|
|
60
|
+
stdout: `${installResult.stdout || ''}\n${enableResult.stdout || ''}`.trim(),
|
|
61
|
+
stderr: `${installResult.stderr || ''}\n${enableResult.stderr || ''}`.trim(),
|
|
62
|
+
code: enableResult.code
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const state = await probeOpenclawSessionPluginState({ home, trackerDir, env });
|
|
67
|
+
return {
|
|
68
|
+
configured: state.configured,
|
|
69
|
+
changed:
|
|
70
|
+
/Linked plugin path:/i.test(installResult.stdout || '') ||
|
|
71
|
+
/Enabled plugin/i.test(enableResult.stdout || '') ||
|
|
72
|
+
/already enabled/i.test(enableResult.stdout || ''),
|
|
73
|
+
...paths,
|
|
74
|
+
stdout: `${installResult.stdout || ''}\n${enableResult.stdout || ''}`.trim(),
|
|
75
|
+
stderr: `${installResult.stderr || ''}\n${enableResult.stderr || ''}`.trim(),
|
|
76
|
+
code: enableResult.code
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function ensureOpenclawSessionPluginFiles({ pluginDir, trackerDir, packageName = 'vibeusage', openclawHome } = {}) {
|
|
81
|
+
if (!pluginDir || !trackerDir) throw new Error('pluginDir and trackerDir are required');
|
|
82
|
+
|
|
83
|
+
const pluginEntryDir = path.join(pluginDir, OPENCLAW_SESSION_PLUGIN_ID);
|
|
84
|
+
await fs.mkdir(pluginEntryDir, { recursive: true });
|
|
85
|
+
|
|
86
|
+
const packageJsonPath = path.join(pluginEntryDir, 'package.json');
|
|
87
|
+
const pluginMetaPath = path.join(pluginEntryDir, 'openclaw.plugin.json');
|
|
88
|
+
const indexPath = path.join(pluginEntryDir, 'index.js');
|
|
89
|
+
|
|
90
|
+
await fs.writeFile(packageJsonPath, buildSessionPluginPackageJson(), 'utf8');
|
|
91
|
+
await fs.writeFile(pluginMetaPath, buildSessionPluginMeta(), 'utf8');
|
|
92
|
+
await fs.writeFile(
|
|
93
|
+
indexPath,
|
|
94
|
+
buildSessionPluginIndex({
|
|
95
|
+
trackerDir,
|
|
96
|
+
packageName,
|
|
97
|
+
openclawHome: openclawHome || path.join(os.homedir(), '.openclaw')
|
|
98
|
+
}),
|
|
99
|
+
'utf8'
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function probeOpenclawSessionPluginState({ home = os.homedir(), trackerDir, env = process.env } = {}) {
|
|
104
|
+
const paths = resolveOpenclawSessionPluginPaths({ home, trackerDir, env });
|
|
105
|
+
const { openclawConfigPath, pluginEntryDir, pluginId } = paths;
|
|
106
|
+
|
|
107
|
+
const pluginFilesReady =
|
|
108
|
+
fssync.existsSync(path.join(pluginEntryDir, 'package.json')) &&
|
|
109
|
+
fssync.existsSync(path.join(pluginEntryDir, 'index.js'));
|
|
110
|
+
|
|
111
|
+
let cfg = null;
|
|
112
|
+
try {
|
|
113
|
+
const raw = await fs.readFile(openclawConfigPath, 'utf8');
|
|
114
|
+
cfg = JSON.parse(raw);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
if (err?.code === 'ENOENT' || err?.code === 'ENOTDIR') {
|
|
117
|
+
return {
|
|
118
|
+
configured: false,
|
|
119
|
+
enabled: false,
|
|
120
|
+
linked: false,
|
|
121
|
+
installed: false,
|
|
122
|
+
pluginFilesReady,
|
|
123
|
+
skippedReason: 'openclaw-config-missing',
|
|
124
|
+
...paths
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
configured: false,
|
|
129
|
+
enabled: false,
|
|
130
|
+
linked: false,
|
|
131
|
+
installed: false,
|
|
132
|
+
pluginFilesReady,
|
|
133
|
+
skippedReason: 'openclaw-config-unreadable',
|
|
134
|
+
error: err?.message || String(err),
|
|
135
|
+
...paths
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const pluginEntry = cfg?.plugins?.entries?.[pluginId];
|
|
140
|
+
const enabled = pluginEntry ? pluginEntry.enabled !== false : false;
|
|
141
|
+
|
|
142
|
+
const loadPaths = Array.isArray(cfg?.plugins?.load?.paths) ? cfg.plugins.load.paths : [];
|
|
143
|
+
const normalizedPluginEntryDir = path.resolve(pluginEntryDir);
|
|
144
|
+
const linked = loadPaths.some((entry) => path.resolve(String(entry || '')) === normalizedPluginEntryDir);
|
|
145
|
+
|
|
146
|
+
const installs = cfg?.plugins?.installs && typeof cfg.plugins.installs === 'object' ? cfg.plugins.installs : {};
|
|
147
|
+
const installEntry = installs[pluginId];
|
|
148
|
+
const installed = Boolean(installEntry);
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
configured: enabled && linked && pluginFilesReady,
|
|
152
|
+
enabled,
|
|
153
|
+
linked,
|
|
154
|
+
installed,
|
|
155
|
+
pluginFilesReady,
|
|
156
|
+
...paths
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function removeOpenclawSessionPluginConfig({ home = os.homedir(), trackerDir, env = process.env } = {}) {
|
|
161
|
+
const paths = resolveOpenclawSessionPluginPaths({ home, trackerDir, env });
|
|
162
|
+
const { openclawConfigPath, pluginEntryDir, pluginId } = paths;
|
|
163
|
+
|
|
164
|
+
let cfg;
|
|
165
|
+
try {
|
|
166
|
+
cfg = JSON.parse(await fs.readFile(openclawConfigPath, 'utf8'));
|
|
167
|
+
} catch (err) {
|
|
168
|
+
if (err?.code === 'ENOENT' || err?.code === 'ENOTDIR') {
|
|
169
|
+
return { removed: false, skippedReason: 'openclaw-config-missing', ...paths };
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
removed: false,
|
|
173
|
+
skippedReason: 'openclaw-config-unreadable',
|
|
174
|
+
error: err?.message || String(err),
|
|
175
|
+
...paths
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let changed = false;
|
|
180
|
+
const plugins = cfg?.plugins;
|
|
181
|
+
|
|
182
|
+
if (plugins?.entries && Object.prototype.hasOwnProperty.call(plugins.entries, pluginId)) {
|
|
183
|
+
delete plugins.entries[pluginId];
|
|
184
|
+
changed = true;
|
|
185
|
+
if (Object.keys(plugins.entries).length === 0) delete plugins.entries;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (plugins?.load && Array.isArray(plugins.load.paths)) {
|
|
189
|
+
const target = path.resolve(pluginEntryDir);
|
|
190
|
+
const after = plugins.load.paths.filter((entry) => path.resolve(String(entry || '')) !== target);
|
|
191
|
+
if (after.length !== plugins.load.paths.length) {
|
|
192
|
+
plugins.load.paths = after;
|
|
193
|
+
changed = true;
|
|
194
|
+
if (after.length === 0) delete plugins.load.paths;
|
|
195
|
+
if (Object.keys(plugins.load).length === 0) delete plugins.load;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (plugins?.installs && typeof plugins.installs === 'object') {
|
|
200
|
+
const installs = plugins.installs;
|
|
201
|
+
if (Object.prototype.hasOwnProperty.call(installs, pluginId)) {
|
|
202
|
+
delete installs[pluginId];
|
|
203
|
+
changed = true;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const target = path.resolve(pluginEntryDir);
|
|
207
|
+
for (const [id, entry] of Object.entries(installs)) {
|
|
208
|
+
const sourcePath = normalizeString(entry?.sourcePath);
|
|
209
|
+
const installPath = normalizeString(entry?.installPath);
|
|
210
|
+
if (
|
|
211
|
+
(sourcePath && path.resolve(sourcePath) === target) ||
|
|
212
|
+
(installPath && path.resolve(installPath) === target)
|
|
213
|
+
) {
|
|
214
|
+
delete installs[id];
|
|
215
|
+
changed = true;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (Object.keys(installs).length === 0) delete plugins.installs;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (plugins && Object.keys(plugins).length === 0) {
|
|
223
|
+
delete cfg.plugins;
|
|
224
|
+
changed = true;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (changed) {
|
|
228
|
+
await fs.writeFile(openclawConfigPath, `${JSON.stringify(cfg, null, 2)}\n`, 'utf8');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const hadFiles = await fs
|
|
232
|
+
.stat(pluginEntryDir)
|
|
233
|
+
.then((st) => st.isDirectory())
|
|
234
|
+
.catch(() => false);
|
|
235
|
+
await fs.rm(pluginEntryDir, { recursive: true, force: true }).catch(() => {});
|
|
236
|
+
|
|
237
|
+
return { removed: changed || hadFiles, ...paths };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function runOpenclawCli(args, env = process.env) {
|
|
241
|
+
let res;
|
|
242
|
+
try {
|
|
243
|
+
res = cp.spawnSync('openclaw', args, {
|
|
244
|
+
env,
|
|
245
|
+
encoding: 'utf8',
|
|
246
|
+
timeout: 30_000
|
|
247
|
+
});
|
|
248
|
+
} catch (err) {
|
|
249
|
+
return {
|
|
250
|
+
code: 1,
|
|
251
|
+
skippedReason: err?.code === 'ENOENT' ? 'openclaw-cli-missing' : 'openclaw-cli-error',
|
|
252
|
+
error: err?.message || String(err),
|
|
253
|
+
stdout: '',
|
|
254
|
+
stderr: ''
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (res.error?.code === 'ENOENT') {
|
|
259
|
+
return {
|
|
260
|
+
code: 1,
|
|
261
|
+
skippedReason: 'openclaw-cli-missing',
|
|
262
|
+
error: res.error.message,
|
|
263
|
+
stdout: res.stdout || '',
|
|
264
|
+
stderr: res.stderr || ''
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if ((res.status || 0) !== 0) {
|
|
269
|
+
return {
|
|
270
|
+
code: Number(res.status || 1),
|
|
271
|
+
skippedReason: 'openclaw-plugins-install-failed',
|
|
272
|
+
error: (res.stderr || res.stdout || '').trim() || 'openclaw plugins install failed',
|
|
273
|
+
stdout: res.stdout || '',
|
|
274
|
+
stderr: res.stderr || ''
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
code: 0,
|
|
280
|
+
stdout: res.stdout || '',
|
|
281
|
+
stderr: res.stderr || ''
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function buildSessionPluginPackageJson() {
|
|
286
|
+
return `${JSON.stringify(
|
|
287
|
+
{
|
|
288
|
+
name: '@vibeusage/openclaw-session-sync',
|
|
289
|
+
version: '0.0.0',
|
|
290
|
+
private: true,
|
|
291
|
+
type: 'module',
|
|
292
|
+
openclaw: {
|
|
293
|
+
extensions: ['./index.js']
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
null,
|
|
297
|
+
2
|
|
298
|
+
)}\n`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function buildSessionPluginMeta() {
|
|
302
|
+
return `${JSON.stringify(
|
|
303
|
+
{
|
|
304
|
+
id: OPENCLAW_SESSION_PLUGIN_ID,
|
|
305
|
+
name: 'VibeUsage OpenClaw Session Sync',
|
|
306
|
+
description: 'Trigger vibeusage sync on OpenClaw agent/session lifecycle events.',
|
|
307
|
+
configSchema: {
|
|
308
|
+
type: 'object',
|
|
309
|
+
additionalProperties: false,
|
|
310
|
+
properties: {}
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
null,
|
|
314
|
+
2
|
|
315
|
+
)}\n`;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function buildSessionPluginIndex({ trackerDir, packageName = 'vibeusage', openclawHome }) {
|
|
319
|
+
const trackerBinPath = path.join(trackerDir, 'app', 'bin', 'tracker.js');
|
|
320
|
+
const fallbackPkg = packageName || 'vibeusage';
|
|
321
|
+
const safeOpenclawHome = openclawHome || path.join(os.homedir(), '.openclaw');
|
|
322
|
+
|
|
323
|
+
return `import fs from 'node:fs';\n` +
|
|
324
|
+
`import path from 'node:path';\n` +
|
|
325
|
+
`import cp from 'node:child_process';\n` +
|
|
326
|
+
`\n` +
|
|
327
|
+
`const trackerDir = ${JSON.stringify(trackerDir)};\n` +
|
|
328
|
+
`const trackerBinPath = ${JSON.stringify(trackerBinPath)};\n` +
|
|
329
|
+
`const fallbackPkg = ${JSON.stringify(fallbackPkg)};\n` +
|
|
330
|
+
`const openclawHome = ${JSON.stringify(safeOpenclawHome)};\n` +
|
|
331
|
+
`const depsMarkerPath = path.join(trackerDir, 'app', 'node_modules', '@insforge', 'sdk', 'package.json');\n` +
|
|
332
|
+
`const triggerStatePath = path.join(trackerDir, 'openclaw.session-sync.trigger-state.json');\n` +
|
|
333
|
+
`const SESSION_TRIGGER_THROTTLE_MS = 15_000;\n` +
|
|
334
|
+
`\n` +
|
|
335
|
+
`export default function register(api) {\n` +
|
|
336
|
+
` api.on('agent_end', async (_event, ctx) => {\n` +
|
|
337
|
+
` try {\n` +
|
|
338
|
+
` const sessionKey = normalize(ctx && ctx.sessionKey);\n` +
|
|
339
|
+
` if (!sessionKey) return;\n` +
|
|
340
|
+
`\n` +
|
|
341
|
+
` const agentId = normalize(ctx && ctx.agentId) || parseAgentId(sessionKey);\n` +
|
|
342
|
+
` if (!agentId) return;\n` +
|
|
343
|
+
`\n` +
|
|
344
|
+
` const sessionInfo = resolveSessionInfo(agentId, sessionKey);\n` +
|
|
345
|
+
` const sessionId = normalize(sessionInfo && sessionInfo.sessionId);\n` +
|
|
346
|
+
` if (!sessionId) return;\n` +
|
|
347
|
+
`\n` +
|
|
348
|
+
` if (!allowTrigger('agent_end', agentId, sessionId)) return;\n` +
|
|
349
|
+
`\n` +
|
|
350
|
+
` spawnSync({\n` +
|
|
351
|
+
` args: ['sync', '--auto', '--from-openclaw'],\n` +
|
|
352
|
+
` env: buildSessionEnv({\n` +
|
|
353
|
+
` agentId,\n` +
|
|
354
|
+
` sessionId,\n` +
|
|
355
|
+
` sessionKey,\n` +
|
|
356
|
+
` sessionEntry: sessionInfo && sessionInfo.entry\n` +
|
|
357
|
+
` })\n` +
|
|
358
|
+
` });\n` +
|
|
359
|
+
` } catch (_) {}\n` +
|
|
360
|
+
` });\n` +
|
|
361
|
+
`\n` +
|
|
362
|
+
` api.on('gateway_start', async () => {\n` +
|
|
363
|
+
` try {\n` +
|
|
364
|
+
` if (!allowTrigger('gateway_start', 'gateway', 'startup')) return;\n` +
|
|
365
|
+
` spawnSync({ args: ['sync', '--auto'] });\n` +
|
|
366
|
+
` } catch (_) {}\n` +
|
|
367
|
+
` });\n` +
|
|
368
|
+
`\n` +
|
|
369
|
+
` api.on('gateway_stop', async () => {\n` +
|
|
370
|
+
` try {\n` +
|
|
371
|
+
` if (!allowTrigger('gateway_stop', 'gateway', 'stop')) return;\n` +
|
|
372
|
+
` spawnSync({ args: ['sync', '--auto'] });\n` +
|
|
373
|
+
` } catch (_) {}\n` +
|
|
374
|
+
` });\n` +
|
|
375
|
+
`}\n` +
|
|
376
|
+
`\n` +
|
|
377
|
+
`function spawnSync({ args, env = {} }) {\n` +
|
|
378
|
+
` const hasLocalRuntime = fs.existsSync(trackerBinPath);\n` +
|
|
379
|
+
` const hasLocalDeps = fs.existsSync(depsMarkerPath);\n` +
|
|
380
|
+
` const argv = Array.isArray(args) && args.length > 0 ? args : ['sync', '--auto'];\n` +
|
|
381
|
+
` const cmd = hasLocalRuntime && hasLocalDeps\n` +
|
|
382
|
+
` ? [process.execPath, trackerBinPath, ...argv]\n` +
|
|
383
|
+
` : ['npx', '--yes', fallbackPkg, ...argv];\n` +
|
|
384
|
+
` const child = cp.spawn(cmd[0], cmd.slice(1), {\n` +
|
|
385
|
+
` detached: true,\n` +
|
|
386
|
+
` stdio: 'ignore',\n` +
|
|
387
|
+
` env: { ...process.env, ...env }\n` +
|
|
388
|
+
` });\n` +
|
|
389
|
+
` child.unref();\n` +
|
|
390
|
+
`}\n` +
|
|
391
|
+
`\n` +
|
|
392
|
+
`function buildSessionEnv({ agentId, sessionId, sessionKey, sessionEntry }) {\n` +
|
|
393
|
+
` const out = {\n` +
|
|
394
|
+
` VIBEUSAGE_OPENCLAW_AGENT_ID: agentId,\n` +
|
|
395
|
+
` VIBEUSAGE_OPENCLAW_PREV_SESSION_ID: sessionId,\n` +
|
|
396
|
+
` VIBEUSAGE_OPENCLAW_HOME: openclawHome\n` +
|
|
397
|
+
` };\n` +
|
|
398
|
+
` const key = normalize(sessionKey);\n` +
|
|
399
|
+
` if (key) out.VIBEUSAGE_OPENCLAW_SESSION_KEY = key;\n` +
|
|
400
|
+
` const prevTotalTokens = toNonNegativeInt(sessionEntry && sessionEntry.totalTokens);\n` +
|
|
401
|
+
` const prevInputTokens = toNonNegativeInt(sessionEntry && sessionEntry.inputTokens);\n` +
|
|
402
|
+
` const prevOutputTokens = toNonNegativeInt(sessionEntry && sessionEntry.outputTokens);\n` +
|
|
403
|
+
` const prevModel = normalize(sessionEntry && sessionEntry.model);\n` +
|
|
404
|
+
` const prevUpdatedAt = toIso(sessionEntry && sessionEntry.updatedAt);\n` +
|
|
405
|
+
` if (prevTotalTokens != null) out.VIBEUSAGE_OPENCLAW_PREV_TOTAL_TOKENS = String(prevTotalTokens);\n` +
|
|
406
|
+
` if (prevInputTokens != null) out.VIBEUSAGE_OPENCLAW_PREV_INPUT_TOKENS = String(prevInputTokens);\n` +
|
|
407
|
+
` if (prevOutputTokens != null) out.VIBEUSAGE_OPENCLAW_PREV_OUTPUT_TOKENS = String(prevOutputTokens);\n` +
|
|
408
|
+
` if (prevModel) out.VIBEUSAGE_OPENCLAW_PREV_MODEL = prevModel;\n` +
|
|
409
|
+
` if (prevUpdatedAt) out.VIBEUSAGE_OPENCLAW_PREV_UPDATED_AT = prevUpdatedAt;\n` +
|
|
410
|
+
` return out;\n` +
|
|
411
|
+
`}\n` +
|
|
412
|
+
`\n` +
|
|
413
|
+
`function resolveSessionInfo(agentId, sessionKey) {\n` +
|
|
414
|
+
` const key = normalize(sessionKey);\n` +
|
|
415
|
+
` if (!key) return null;\n` +
|
|
416
|
+
` const sessionsPath = path.join(openclawHome, 'agents', agentId, 'sessions', 'sessions.json');\n` +
|
|
417
|
+
` try {\n` +
|
|
418
|
+
` const raw = fs.readFileSync(sessionsPath, 'utf8');\n` +
|
|
419
|
+
` const parsed = JSON.parse(raw);\n` +
|
|
420
|
+
` if (!parsed || typeof parsed !== 'object') return null;\n` +
|
|
421
|
+
` const entry = parsed[key];\n` +
|
|
422
|
+
` if (!entry || typeof entry !== 'object') return null;\n` +
|
|
423
|
+
` return {\n` +
|
|
424
|
+
` sessionKey: key,\n` +
|
|
425
|
+
` sessionId: normalize(entry.sessionId),\n` +
|
|
426
|
+
` entry\n` +
|
|
427
|
+
` };\n` +
|
|
428
|
+
` } catch (_) {}\n` +
|
|
429
|
+
` return null;\n` +
|
|
430
|
+
`}\n` +
|
|
431
|
+
`\n` +
|
|
432
|
+
`function parseAgentId(sessionKey) {\n` +
|
|
433
|
+
` const s = normalize(sessionKey);\n` +
|
|
434
|
+
` if (!s || !s.startsWith('agent:')) return null;\n` +
|
|
435
|
+
` const parts = s.split(':');\n` +
|
|
436
|
+
` return parts.length >= 2 ? normalize(parts[1]) : null;\n` +
|
|
437
|
+
`}\n` +
|
|
438
|
+
`\n` +
|
|
439
|
+
`function allowTrigger(kind, scope, target) {\n` +
|
|
440
|
+
` const key = [kind, scope || 'na', target || 'na'].join(':');\n` +
|
|
441
|
+
` const now = Date.now();\n` +
|
|
442
|
+
` let state = {};\n` +
|
|
443
|
+
` try {\n` +
|
|
444
|
+
` state = JSON.parse(fs.readFileSync(triggerStatePath, 'utf8'));\n` +
|
|
445
|
+
` if (!state || typeof state !== 'object') state = {};\n` +
|
|
446
|
+
` } catch (_) {}\n` +
|
|
447
|
+
` const last = Number(state[key] || 0);\n` +
|
|
448
|
+
` if (Number.isFinite(last) && now - last < SESSION_TRIGGER_THROTTLE_MS) return false;\n` +
|
|
449
|
+
` state[key] = now;\n` +
|
|
450
|
+
` try {\n` +
|
|
451
|
+
` fs.mkdirSync(path.dirname(triggerStatePath), { recursive: true });\n` +
|
|
452
|
+
` fs.writeFileSync(triggerStatePath, JSON.stringify(state), 'utf8');\n` +
|
|
453
|
+
` } catch (_) {}\n` +
|
|
454
|
+
` return true;\n` +
|
|
455
|
+
`}\n` +
|
|
456
|
+
`\n` +
|
|
457
|
+
`function normalize(v) {\n` +
|
|
458
|
+
` if (typeof v !== 'string') return null;\n` +
|
|
459
|
+
` const s = v.trim();\n` +
|
|
460
|
+
` return s.length > 0 ? s : null;\n` +
|
|
461
|
+
`}\n` +
|
|
462
|
+
`\n` +
|
|
463
|
+
`function toNonNegativeInt(v) {\n` +
|
|
464
|
+
` const n = Number(v);\n` +
|
|
465
|
+
` if (!Number.isFinite(n) || n < 0) return null;\n` +
|
|
466
|
+
` return Math.floor(n);\n` +
|
|
467
|
+
`}\n` +
|
|
468
|
+
`\n` +
|
|
469
|
+
`function toIso(v) {\n` +
|
|
470
|
+
` if (typeof v === 'string') {\n` +
|
|
471
|
+
` const s = normalize(v);\n` +
|
|
472
|
+
` if (s && !Number.isNaN(Date.parse(s))) return s;\n` +
|
|
473
|
+
` }\n` +
|
|
474
|
+
` const n = Number(v);\n` +
|
|
475
|
+
` if (!Number.isFinite(n) || n <= 0) return null;\n` +
|
|
476
|
+
` const ms = n < 1e12 ? Math.floor(n * 1000) : Math.floor(n);\n` +
|
|
477
|
+
` const d = new Date(ms);\n` +
|
|
478
|
+
` return Number.isNaN(d.getTime()) ? null : d.toISOString();\n` +
|
|
479
|
+
`}\n`;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function normalizeString(value) {
|
|
483
|
+
if (typeof value !== 'string') return null;
|
|
484
|
+
const trimmed = value.trim();
|
|
485
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
module.exports = {
|
|
489
|
+
OPENCLAW_SESSION_PLUGIN_ID,
|
|
490
|
+
OPENCLAW_SESSION_PLUGIN_DIRNAME,
|
|
491
|
+
resolveOpenclawSessionPluginPaths,
|
|
492
|
+
ensureOpenclawSessionPluginFiles,
|
|
493
|
+
installOpenclawSessionPlugin,
|
|
494
|
+
probeOpenclawSessionPluginState,
|
|
495
|
+
removeOpenclawSessionPluginConfig
|
|
496
|
+
};
|