vibeusage 0.2.19 → 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 +3 -1
- package/src/commands/init.js +66 -17
- package/src/commands/status.js +4 -1
- package/src/commands/sync.js +125 -1
- package/src/commands/uninstall.js +10 -3
- package/src/lib/diagnostics.js +5 -0
- package/src/lib/openclaw-hook.js +43 -4
- package/src/lib/openclaw-session-plugin.js +496 -0
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
|
]
|
package/src/commands/sync.js
CHANGED
|
@@ -123,6 +123,17 @@ async function cmdSync(argv) {
|
|
|
123
123
|
});
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
const openclawFallback = await applyOpenclawTotalsFallback({
|
|
127
|
+
trackerDir,
|
|
128
|
+
signal: openclawSignal,
|
|
129
|
+
cursors,
|
|
130
|
+
queuePath,
|
|
131
|
+
projectQueuePath
|
|
132
|
+
});
|
|
133
|
+
openclawResult.filesProcessed += openclawFallback.filesProcessed;
|
|
134
|
+
openclawResult.eventsAggregated += openclawFallback.eventsAggregated;
|
|
135
|
+
openclawResult.bucketsQueued += openclawFallback.bucketsQueued;
|
|
136
|
+
|
|
126
137
|
const claudeFiles = await listClaudeProjectFiles(claudeProjectsDir);
|
|
127
138
|
let claudeResult = { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
128
139
|
if (claudeFiles.length > 0) {
|
|
@@ -432,10 +443,123 @@ function resolveOpenclawSignal({ home, env } = {}) {
|
|
|
432
443
|
const openclawHome = normalizeString(env.VIBEUSAGE_OPENCLAW_HOME) || path.join(home || os.homedir(), '.openclaw');
|
|
433
444
|
const sessionFile = path.join(openclawHome, 'agents', agentId, 'sessions', `${sessionId}.jsonl`);
|
|
434
445
|
|
|
435
|
-
|
|
446
|
+
const prevTotals = {
|
|
447
|
+
totalTokens: normalizeNonNegativeInt(env.VIBEUSAGE_OPENCLAW_PREV_TOTAL_TOKENS),
|
|
448
|
+
inputTokens: normalizeNonNegativeInt(env.VIBEUSAGE_OPENCLAW_PREV_INPUT_TOKENS),
|
|
449
|
+
outputTokens: normalizeNonNegativeInt(env.VIBEUSAGE_OPENCLAW_PREV_OUTPUT_TOKENS),
|
|
450
|
+
model: normalizeString(env.VIBEUSAGE_OPENCLAW_PREV_MODEL),
|
|
451
|
+
updatedAt: normalizeIsoOrEpoch(env.VIBEUSAGE_OPENCLAW_PREV_UPDATED_AT)
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
return {
|
|
455
|
+
agentId,
|
|
456
|
+
sessionId,
|
|
457
|
+
sessionKey: normalizeString(env.VIBEUSAGE_OPENCLAW_SESSION_KEY),
|
|
458
|
+
openclawHome,
|
|
459
|
+
sessionFile,
|
|
460
|
+
prevTotals
|
|
461
|
+
};
|
|
436
462
|
}
|
|
437
463
|
|
|
438
464
|
|
|
465
|
+
async function applyOpenclawTotalsFallback({ trackerDir, signal, cursors, queuePath, projectQueuePath }) {
|
|
466
|
+
const totalTokens = Number(signal?.prevTotals?.totalTokens || 0);
|
|
467
|
+
if (!trackerDir || !signal || totalTokens <= 0) {
|
|
468
|
+
return { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const sessionKey = `${signal.agentId}:${signal.sessionId}`;
|
|
472
|
+
const statePath = path.join(trackerDir, 'openclaw.fallback.state.json');
|
|
473
|
+
const fallbackFilePath = path.join(trackerDir, 'openclaw.fallback.jsonl');
|
|
474
|
+
const state = (await readJson(statePath)) || { version: 1, sessions: {} };
|
|
475
|
+
const sessions = state.sessions && typeof state.sessions === 'object' ? state.sessions : {};
|
|
476
|
+
const prev = sessions[sessionKey] && typeof sessions[sessionKey] === 'object' ? sessions[sessionKey] : null;
|
|
477
|
+
|
|
478
|
+
const current = {
|
|
479
|
+
totalTokens: normalizeNonNegativeInt(signal?.prevTotals?.totalTokens) || 0,
|
|
480
|
+
inputTokens: normalizeNonNegativeInt(signal?.prevTotals?.inputTokens) || 0,
|
|
481
|
+
outputTokens: normalizeNonNegativeInt(signal?.prevTotals?.outputTokens) || 0,
|
|
482
|
+
model: normalizeString(signal?.prevTotals?.model) || 'unknown',
|
|
483
|
+
updatedAt: normalizeIsoOrEpoch(signal?.prevTotals?.updatedAt) || new Date().toISOString(),
|
|
484
|
+
seenAt: new Date().toISOString()
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
let deltaTotal = current.totalTokens;
|
|
488
|
+
let deltaInput = current.inputTokens;
|
|
489
|
+
let deltaOutput = current.outputTokens;
|
|
490
|
+
if (prev) {
|
|
491
|
+
deltaTotal = Math.max(0, current.totalTokens - (normalizeNonNegativeInt(prev.totalTokens) || 0));
|
|
492
|
+
deltaInput = Math.max(0, current.inputTokens - (normalizeNonNegativeInt(prev.inputTokens) || 0));
|
|
493
|
+
deltaOutput = Math.max(0, current.outputTokens - (normalizeNonNegativeInt(prev.outputTokens) || 0));
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (deltaTotal > 0 && deltaInput + deltaOutput === 0) {
|
|
497
|
+
deltaInput = deltaTotal;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
sessions[sessionKey] = current;
|
|
501
|
+
state.version = 1;
|
|
502
|
+
state.sessions = sessions;
|
|
503
|
+
|
|
504
|
+
if (deltaTotal <= 0) {
|
|
505
|
+
await writeJson(statePath, state);
|
|
506
|
+
return { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
await ensureDir(path.dirname(fallbackFilePath));
|
|
510
|
+
const syntheticMessage = {
|
|
511
|
+
type: 'message',
|
|
512
|
+
timestamp: current.updatedAt,
|
|
513
|
+
message: {
|
|
514
|
+
role: 'assistant',
|
|
515
|
+
model: current.model,
|
|
516
|
+
usage: {
|
|
517
|
+
input: deltaInput,
|
|
518
|
+
output: deltaOutput,
|
|
519
|
+
cacheRead: 0,
|
|
520
|
+
cacheWrite: 0,
|
|
521
|
+
totalTokens: deltaTotal
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
await fs.appendFile(fallbackFilePath, `${JSON.stringify(syntheticMessage)}\n`, 'utf8');
|
|
526
|
+
await writeJson(statePath, state);
|
|
527
|
+
|
|
528
|
+
return parseOpenclawIncremental({
|
|
529
|
+
sessionFiles: [{ path: fallbackFilePath, source: 'openclaw' }],
|
|
530
|
+
cursors,
|
|
531
|
+
queuePath,
|
|
532
|
+
projectQueuePath,
|
|
533
|
+
source: 'openclaw'
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function normalizeNonNegativeInt(value) {
|
|
538
|
+
const n = Number(value);
|
|
539
|
+
if (!Number.isFinite(n) || n < 0) return null;
|
|
540
|
+
return Math.floor(n);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function normalizeIsoOrEpoch(value) {
|
|
544
|
+
if (typeof value === 'string') {
|
|
545
|
+
const trimmed = value.trim();
|
|
546
|
+
if (trimmed.length > 0 && !Number.isNaN(Date.parse(trimmed))) return trimmed;
|
|
547
|
+
const numeric = Number(trimmed);
|
|
548
|
+
if (Number.isFinite(numeric) && numeric > 0) {
|
|
549
|
+
const ms = numeric < 1e12 ? Math.floor(numeric * 1000) : Math.floor(numeric);
|
|
550
|
+
const iso = new Date(ms).toISOString();
|
|
551
|
+
if (!Number.isNaN(Date.parse(iso))) return iso;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const n = Number(value);
|
|
556
|
+
if (!Number.isFinite(n) || n <= 0) return null;
|
|
557
|
+
const ms = n < 1e12 ? Math.floor(n * 1000) : Math.floor(n);
|
|
558
|
+
const dt = new Date(ms);
|
|
559
|
+
if (Number.isNaN(dt.getTime())) return null;
|
|
560
|
+
return dt.toISOString();
|
|
561
|
+
}
|
|
562
|
+
|
|
439
563
|
async function safeStatSize(p) {
|
|
440
564
|
try {
|
|
441
565
|
const st = await fs.stat(p);
|
|
@@ -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
|
@@ -252,12 +252,12 @@ function buildHookMarkdown() {
|
|
|
252
252
|
name: ${OPENCLAW_HOOK_NAME}
|
|
253
253
|
description: "Trigger vibeusage sync when OpenClaw sessions roll over"
|
|
254
254
|
metadata:
|
|
255
|
-
{ "openclaw": { "emoji": "📈", "events": ["command:new", "command:stop"], "requires": { "bins": ["node"] } } }
|
|
255
|
+
{ "openclaw": { "emoji": "📈", "events": ["command:new", "command:reset", "command:stop"], "requires": { "bins": ["node"] } } }
|
|
256
256
|
---
|
|
257
257
|
|
|
258
258
|
# VibeUsage OpenClaw Sync Hook
|
|
259
259
|
|
|
260
|
-
Triggers non-blocking 'vibeusage sync --auto --from-openclaw' runs when OpenClaw command events indicate
|
|
260
|
+
Triggers non-blocking 'vibeusage sync --auto --from-openclaw' runs when OpenClaw command events indicate session rollover/reset/stop.
|
|
261
261
|
`;
|
|
262
262
|
}
|
|
263
263
|
|
|
@@ -281,13 +281,14 @@ function buildHookHandler({ trackerDir, packageName = 'vibeusage', openclawHome
|
|
|
281
281
|
`module.exports = async function handler(event) {\n` +
|
|
282
282
|
` try {\n` +
|
|
283
283
|
` if (!event || event.type !== 'command') return;\n` +
|
|
284
|
-
` if (event.action !== 'new' && event.action !== 'stop') return;\n` +
|
|
284
|
+
` if (event.action !== 'new' && event.action !== 'reset' && event.action !== 'stop') return;\n` +
|
|
285
285
|
`\n` +
|
|
286
286
|
` const sessionKey = normalize(event.sessionKey);\n` +
|
|
287
287
|
` const agentId = parseAgentId(sessionKey);\n` +
|
|
288
288
|
` if (!agentId) return;\n` +
|
|
289
289
|
`\n` +
|
|
290
|
-
` const
|
|
290
|
+
` const sessionEntry = resolveSessionEntry(event);\n` +
|
|
291
|
+
` const sessionId = normalize(sessionEntry && sessionEntry.sessionId) || resolveSessionId(event);\n` +
|
|
291
292
|
` if (!sessionId) return;\n` +
|
|
292
293
|
`\n` +
|
|
293
294
|
` const now = Date.now();\n` +
|
|
@@ -302,9 +303,20 @@ function buildHookHandler({ trackerDir, packageName = 'vibeusage', openclawHome
|
|
|
302
303
|
` const env = {\n` +
|
|
303
304
|
` ...process.env,\n` +
|
|
304
305
|
` VIBEUSAGE_OPENCLAW_AGENT_ID: agentId,\n` +
|
|
306
|
+
` VIBEUSAGE_OPENCLAW_SESSION_KEY: sessionKey,\n` +
|
|
305
307
|
` VIBEUSAGE_OPENCLAW_PREV_SESSION_ID: sessionId,\n` +
|
|
306
308
|
` VIBEUSAGE_OPENCLAW_HOME: openclawHome\n` +
|
|
307
309
|
` };\n` +
|
|
310
|
+
` const prevTotalTokens = toNonNegativeInt(sessionEntry && sessionEntry.totalTokens);\n` +
|
|
311
|
+
` const prevInputTokens = toNonNegativeInt(sessionEntry && sessionEntry.inputTokens);\n` +
|
|
312
|
+
` const prevOutputTokens = toNonNegativeInt(sessionEntry && sessionEntry.outputTokens);\n` +
|
|
313
|
+
` const prevModel = normalize(sessionEntry && sessionEntry.model);\n` +
|
|
314
|
+
` const prevUpdatedAt = toIso(sessionEntry && sessionEntry.updatedAt);\n` +
|
|
315
|
+
` if (prevTotalTokens != null) env.VIBEUSAGE_OPENCLAW_PREV_TOTAL_TOKENS = String(prevTotalTokens);\n` +
|
|
316
|
+
` if (prevInputTokens != null) env.VIBEUSAGE_OPENCLAW_PREV_INPUT_TOKENS = String(prevInputTokens);\n` +
|
|
317
|
+
` if (prevOutputTokens != null) env.VIBEUSAGE_OPENCLAW_PREV_OUTPUT_TOKENS = String(prevOutputTokens);\n` +
|
|
318
|
+
` if (prevModel) env.VIBEUSAGE_OPENCLAW_PREV_MODEL = prevModel;\n` +
|
|
319
|
+
` if (prevUpdatedAt) env.VIBEUSAGE_OPENCLAW_PREV_UPDATED_AT = prevUpdatedAt;\n` +
|
|
308
320
|
`\n` +
|
|
309
321
|
` const hasLocalRuntime = fs.existsSync(trackerBinPath);\n` +
|
|
310
322
|
` const hasLocalDeps = fs.existsSync(depsMarkerPath);\n` +
|
|
@@ -323,6 +335,33 @@ function buildHookHandler({ trackerDir, packageName = 'vibeusage', openclawHome
|
|
|
323
335
|
` return s.length > 0 ? s : null;\n` +
|
|
324
336
|
`}\n` +
|
|
325
337
|
`\n` +
|
|
338
|
+
`function resolveSessionEntry(event) {\n` +
|
|
339
|
+
` const ctx = (event && event.context && typeof event.context === 'object') ? event.context : {};\n` +
|
|
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` +
|
|
342
|
+
` if (ctx.previousSessionEntry && typeof ctx.previousSessionEntry === 'object') return ctx.previousSessionEntry;\n` +
|
|
343
|
+
` if (ctx.sessionEntry && typeof ctx.sessionEntry === 'object') return ctx.sessionEntry;\n` +
|
|
344
|
+
` return null;\n` +
|
|
345
|
+
`}\n` +
|
|
346
|
+
`\n` +
|
|
347
|
+
`function toNonNegativeInt(v) {\n` +
|
|
348
|
+
` const n = Number(v);\n` +
|
|
349
|
+
` if (!Number.isFinite(n) || n < 0) return null;\n` +
|
|
350
|
+
` return Math.floor(n);\n` +
|
|
351
|
+
`}\n` +
|
|
352
|
+
`\n` +
|
|
353
|
+
`function toIso(v) {\n` +
|
|
354
|
+
` if (typeof v === 'string') {\n` +
|
|
355
|
+
` const s = normalize(v);\n` +
|
|
356
|
+
` if (s && !Number.isNaN(Date.parse(s))) return s;\n` +
|
|
357
|
+
` }\n` +
|
|
358
|
+
` const n = Number(v);\n` +
|
|
359
|
+
` if (!Number.isFinite(n) || n <= 0) return null;\n` +
|
|
360
|
+
` const ms = n < 1e12 ? Math.floor(n * 1000) : Math.floor(n);\n` +
|
|
361
|
+
` const d = new Date(ms);\n` +
|
|
362
|
+
` return Number.isNaN(d.getTime()) ? null : d.toISOString();\n` +
|
|
363
|
+
`}\n` +
|
|
364
|
+
`\n` +
|
|
326
365
|
`function parseAgentId(sessionKey) {\n` +
|
|
327
366
|
` const s = normalize(sessionKey);\n` +
|
|
328
367
|
` if (!s || !s.startsWith('agent:')) 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
|
+
};
|