podwatch 1.0.2
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/LICENSE +28 -0
- package/README.md +92 -0
- package/bin/podwatch.js +10 -0
- package/dist/classifier.d.ts +22 -0
- package/dist/classifier.d.ts.map +1 -0
- package/dist/classifier.js +157 -0
- package/dist/classifier.js.map +1 -0
- package/dist/hooks/cost.d.ts +26 -0
- package/dist/hooks/cost.d.ts.map +1 -0
- package/dist/hooks/cost.js +107 -0
- package/dist/hooks/cost.js.map +1 -0
- package/dist/hooks/lifecycle.d.ts +16 -0
- package/dist/hooks/lifecycle.d.ts.map +1 -0
- package/dist/hooks/lifecycle.js +273 -0
- package/dist/hooks/lifecycle.js.map +1 -0
- package/dist/hooks/security.d.ts +19 -0
- package/dist/hooks/security.d.ts.map +1 -0
- package/dist/hooks/security.js +128 -0
- package/dist/hooks/security.js.map +1 -0
- package/dist/hooks/sessions.d.ts +10 -0
- package/dist/hooks/sessions.d.ts.map +1 -0
- package/dist/hooks/sessions.js +53 -0
- package/dist/hooks/sessions.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +120 -0
- package/dist/index.js.map +1 -0
- package/dist/redact.d.ts +35 -0
- package/dist/redact.d.ts.map +1 -0
- package/dist/redact.js +372 -0
- package/dist/redact.js.map +1 -0
- package/dist/scanner.d.ts +27 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +117 -0
- package/dist/scanner.js.map +1 -0
- package/dist/transmitter.d.ts +58 -0
- package/dist/transmitter.d.ts.map +1 -0
- package/dist/transmitter.js +654 -0
- package/dist/transmitter.js.map +1 -0
- package/dist/types.d.ts +116 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/dist/updater.d.ts +168 -0
- package/dist/updater.d.ts.map +1 -0
- package/dist/updater.js +579 -0
- package/dist/updater.js.map +1 -0
- package/lib/installer.js +599 -0
- package/openclaw.plugin.json +59 -0
- package/package.json +56 -0
- package/skills/podwatch/SKILL.md +112 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Podwatch
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Podwatch
|
|
2
|
+
|
|
3
|
+
Agent security monitoring, cost tracking, and budget enforcement for [OpenClaw](https://openclaw.ai).
|
|
4
|
+
|
|
5
|
+
## Quick Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx podwatch pw_your_api_key_here
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
That's it. The installer will validate your key, install the plugin to `~/.openclaw/extensions/podwatch/`, configure it, and restart the gateway.
|
|
12
|
+
|
|
13
|
+
## What You Get
|
|
14
|
+
|
|
15
|
+
- **Cost tracking** — per-call LLM cost and token monitoring
|
|
16
|
+
- **Budget enforcement** — block tool calls when spend exceeds limits
|
|
17
|
+
- **Security alerts** — detect risky tool usage patterns
|
|
18
|
+
- **Agent pulse** — online status and heartbeat monitoring
|
|
19
|
+
- **Dashboard** — [podwatch.app](https://podwatch.app)
|
|
20
|
+
|
|
21
|
+
## Prerequisites
|
|
22
|
+
|
|
23
|
+
- **Node.js** ≥ 16
|
|
24
|
+
- **OpenClaw** ≥ v2026.2.0
|
|
25
|
+
- **Linux or macOS** (Windows: use WSL)
|
|
26
|
+
- A **Podwatch API key** — get one at [podwatch.app/dashboard](https://podwatch.app/dashboard)
|
|
27
|
+
|
|
28
|
+
## Usage Modes
|
|
29
|
+
|
|
30
|
+
### As an installer (npx)
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Install with your API key
|
|
34
|
+
npx podwatch pw_abc123def456
|
|
35
|
+
|
|
36
|
+
# Show help
|
|
37
|
+
npx podwatch --help
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The installer:
|
|
41
|
+
1. Validates your API key
|
|
42
|
+
2. Checks OpenClaw is installed and compatible
|
|
43
|
+
3. Copies the plugin to `~/.openclaw/extensions/podwatch/`
|
|
44
|
+
4. Configures the plugin in `~/.openclaw/openclaw.json`
|
|
45
|
+
5. Restarts the gateway to activate
|
|
46
|
+
|
|
47
|
+
### As an OpenClaw plugin
|
|
48
|
+
|
|
49
|
+
The plugin is what gets installed to `~/.openclaw/extensions/podwatch/`. It hooks into OpenClaw's event system to capture diagnostics, enforce budgets, and send security alerts to the Podwatch dashboard.
|
|
50
|
+
|
|
51
|
+
Plugin source is in `src/` (TypeScript), compiled to `dist/`.
|
|
52
|
+
|
|
53
|
+
## No `npx`? No problem.
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
curl -sL https://podwatch.app/install.js | node - pw_your_api_key_here
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Development
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npm run build # Compile TypeScript
|
|
63
|
+
npm run dev # Watch mode
|
|
64
|
+
npm run clean # Remove dist/
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Configuration
|
|
68
|
+
|
|
69
|
+
The installer adds this to `~/.openclaw/openclaw.json`:
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"diagnostics": { "enabled": true },
|
|
74
|
+
"plugins": {
|
|
75
|
+
"entries": {
|
|
76
|
+
"podwatch": {
|
|
77
|
+
"enabled": true,
|
|
78
|
+
"config": {
|
|
79
|
+
"apiKey": "pw_your_key",
|
|
80
|
+
"endpoint": "https://podwatch.app/api",
|
|
81
|
+
"enableBudgetEnforcement": true,
|
|
82
|
+
"enableSecurityAlerts": true
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## License
|
|
91
|
+
|
|
92
|
+
BSD-3-Clause
|
package/bin/podwatch.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool classifier — lightweight client-side classification for real-time
|
|
3
|
+
* security decisions (exfiltration detection, persistence attempts).
|
|
4
|
+
*
|
|
5
|
+
* NOTE: Full risk-level classification remains server-side
|
|
6
|
+
* (podwatch-app/src/lib/risk-classifier.ts). This classifier only provides
|
|
7
|
+
* boolean flags needed for the before_tool_call hook to make blocking/alerting
|
|
8
|
+
* decisions in real time.
|
|
9
|
+
*/
|
|
10
|
+
export interface ToolClassification {
|
|
11
|
+
/** Tool reads credential-like files (.env, .key, .pem, ssh keys). */
|
|
12
|
+
accessesCredentials: boolean;
|
|
13
|
+
/** Tool makes outbound network calls (web_fetch, curl, wget, http_request). */
|
|
14
|
+
makesNetworkCall: boolean;
|
|
15
|
+
/** Tool attempts to set up persistence (crontab, systemd, autostart). */
|
|
16
|
+
persistenceAttempt: boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Classify a tool call for real-time security decisions.
|
|
20
|
+
*/
|
|
21
|
+
export declare function classifyTool(toolName: string, params: Record<string, unknown>): ToolClassification;
|
|
22
|
+
//# sourceMappingURL=classifier.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"classifier.d.ts","sourceRoot":"","sources":["../src/classifier.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,MAAM,WAAW,kBAAkB;IACjC,qEAAqE;IACrE,mBAAmB,EAAE,OAAO,CAAC;IAC7B,+EAA+E;IAC/E,gBAAgB,EAAE,OAAO,CAAC;IAC1B,yEAAyE;IACzE,kBAAkB,EAAE,OAAO,CAAC;CAC7B;AAmED;;GAEG;AACH,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,kBAAkB,CAQpB"}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Tool classifier — lightweight client-side classification for real-time
|
|
4
|
+
* security decisions (exfiltration detection, persistence attempts).
|
|
5
|
+
*
|
|
6
|
+
* NOTE: Full risk-level classification remains server-side
|
|
7
|
+
* (podwatch-app/src/lib/risk-classifier.ts). This classifier only provides
|
|
8
|
+
* boolean flags needed for the before_tool_call hook to make blocking/alerting
|
|
9
|
+
* decisions in real time.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.classifyTool = classifyTool;
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Credential file patterns
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
const CREDENTIAL_FILE_PATTERNS = [
|
|
17
|
+
/\.env$/i,
|
|
18
|
+
/\.env\.[a-z]+$/i, // .env.local, .env.production, etc.
|
|
19
|
+
/\.key$/i,
|
|
20
|
+
/\.pem$/i,
|
|
21
|
+
/\.p12$/i,
|
|
22
|
+
/\.pfx$/i,
|
|
23
|
+
/\.jks$/i,
|
|
24
|
+
/\.keystore$/i,
|
|
25
|
+
/\.ssh\//i,
|
|
26
|
+
/id_rsa/i,
|
|
27
|
+
/id_ed25519/i,
|
|
28
|
+
/id_ecdsa/i,
|
|
29
|
+
/id_dsa/i,
|
|
30
|
+
/authorized_keys/i,
|
|
31
|
+
/known_hosts/i,
|
|
32
|
+
/\.gnupg\//i,
|
|
33
|
+
/\.aws\/credentials/i,
|
|
34
|
+
/\.netrc/i,
|
|
35
|
+
];
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Network tools
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
const NETWORK_TOOLS = new Set([
|
|
40
|
+
"web_fetch",
|
|
41
|
+
"curl",
|
|
42
|
+
"wget",
|
|
43
|
+
"http_request",
|
|
44
|
+
"fetch",
|
|
45
|
+
"httpie",
|
|
46
|
+
]);
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Persistence keywords (for bash/exec/spawn commands)
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
const PERSISTENCE_KEYWORDS = [
|
|
51
|
+
"crontab",
|
|
52
|
+
"systemd",
|
|
53
|
+
"systemctl",
|
|
54
|
+
"autostart",
|
|
55
|
+
"launchctl",
|
|
56
|
+
"launchd",
|
|
57
|
+
"at ", // `at` scheduler
|
|
58
|
+
"rc.local",
|
|
59
|
+
".bashrc",
|
|
60
|
+
".bash_profile",
|
|
61
|
+
".profile",
|
|
62
|
+
".zshrc",
|
|
63
|
+
"init.d",
|
|
64
|
+
"cron.d",
|
|
65
|
+
];
|
|
66
|
+
const EXEC_TOOLS = new Set(["bash", "exec", "spawn", "shell", "run"]);
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Public API
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
/**
|
|
71
|
+
* Classify a tool call for real-time security decisions.
|
|
72
|
+
*/
|
|
73
|
+
function classifyTool(toolName, params) {
|
|
74
|
+
const name = toolName.toLowerCase();
|
|
75
|
+
return {
|
|
76
|
+
accessesCredentials: checkAccessesCredentials(name, params),
|
|
77
|
+
makesNetworkCall: checkMakesNetworkCall(name, params),
|
|
78
|
+
persistenceAttempt: checkPersistenceAttempt(name, params),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Helpers
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
/** Extract command string from exec-like tool params. */
|
|
85
|
+
function extractCommand(params) {
|
|
86
|
+
return ((typeof params.command === "string" && params.command) ||
|
|
87
|
+
(typeof params.cmd === "string" && params.cmd) ||
|
|
88
|
+
(typeof params.script === "string" && params.script) ||
|
|
89
|
+
"");
|
|
90
|
+
}
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Internal checks
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
function checkAccessesCredentials(toolName, params) {
|
|
95
|
+
// Direct file-reading tools: check path params
|
|
96
|
+
if (toolName === "read" || toolName === "read_file" || toolName === "cat") {
|
|
97
|
+
const filePath = (typeof params.path === "string" && params.path) ||
|
|
98
|
+
(typeof params.file_path === "string" && params.file_path) ||
|
|
99
|
+
(typeof params.file === "string" && params.file) ||
|
|
100
|
+
"";
|
|
101
|
+
if (!filePath)
|
|
102
|
+
return false;
|
|
103
|
+
return CREDENTIAL_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
|
|
104
|
+
}
|
|
105
|
+
// Exec-like tools: parse command for credential file paths
|
|
106
|
+
// Check each token/word individually since patterns use anchors (e.g. \.env$)
|
|
107
|
+
if (EXEC_TOOLS.has(toolName)) {
|
|
108
|
+
const command = extractCommand(params);
|
|
109
|
+
if (!command)
|
|
110
|
+
return false;
|
|
111
|
+
// Split on whitespace; also strip common prefixes like @ (curl's file ref)
|
|
112
|
+
const tokens = command.split(/\s+/).map((t) => t.replace(/^[@<>]+/, ""));
|
|
113
|
+
return tokens.some((token) => CREDENTIAL_FILE_PATTERNS.some((pattern) => pattern.test(token)));
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
// Network tool names that may appear as commands inside exec
|
|
118
|
+
const EXEC_NETWORK_COMMANDS = [
|
|
119
|
+
"curl",
|
|
120
|
+
"wget",
|
|
121
|
+
"nc",
|
|
122
|
+
"ncat",
|
|
123
|
+
"ssh",
|
|
124
|
+
"scp",
|
|
125
|
+
"rsync",
|
|
126
|
+
"fetch",
|
|
127
|
+
"http", // httpie
|
|
128
|
+
"httpie",
|
|
129
|
+
];
|
|
130
|
+
function checkMakesNetworkCall(toolName, params) {
|
|
131
|
+
// Direct network tools
|
|
132
|
+
if (NETWORK_TOOLS.has(toolName))
|
|
133
|
+
return true;
|
|
134
|
+
// Exec-like tools: parse command for network tool invocations
|
|
135
|
+
if (EXEC_TOOLS.has(toolName)) {
|
|
136
|
+
const command = extractCommand(params);
|
|
137
|
+
if (!command)
|
|
138
|
+
return false;
|
|
139
|
+
const lower = command.toLowerCase();
|
|
140
|
+
return EXEC_NETWORK_COMMANDS.some((cmd) => {
|
|
141
|
+
// Match as a standalone word (start of command, after pipe, after &&, etc.)
|
|
142
|
+
const re = new RegExp(`(?:^|[|;&\\s])${cmd}(?:\\s|$)`);
|
|
143
|
+
return re.test(lower);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
function checkPersistenceAttempt(toolName, params) {
|
|
149
|
+
if (!EXEC_TOOLS.has(toolName))
|
|
150
|
+
return false;
|
|
151
|
+
const command = extractCommand(params);
|
|
152
|
+
if (!command)
|
|
153
|
+
return false;
|
|
154
|
+
const lower = command.toLowerCase();
|
|
155
|
+
return PERSISTENCE_KEYWORDS.some((kw) => lower.includes(kw));
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=classifier.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"classifier.js","sourceRoot":"","sources":["../src/classifier.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cost handler — extracts token/cost data from before_agent_start messages.
|
|
3
|
+
*
|
|
4
|
+
* The plugin-sdk's onDiagnosticEvent is broken (separate module instances).
|
|
5
|
+
* Instead, we use before_agent_start which provides the full session history
|
|
6
|
+
* with usage data on every assistant message:
|
|
7
|
+
* { provider, model, usage: { input, output, cacheRead, cacheWrite, totalTokens, cost: { total } } }
|
|
8
|
+
*
|
|
9
|
+
* Dedup strategy: lastSeenIndex tracks how far we've read into event.messages.
|
|
10
|
+
* Each invocation only processes messages from lastSeenIndex onwards, then
|
|
11
|
+
* advances the pointer. Zero memory growth, O(1) bookkeeping.
|
|
12
|
+
*/
|
|
13
|
+
import type { PodwatchConfig } from "../index.js";
|
|
14
|
+
/**
|
|
15
|
+
* Reset all dedup state (exported for testing).
|
|
16
|
+
*/
|
|
17
|
+
export declare function _resetCostState(): void;
|
|
18
|
+
/**
|
|
19
|
+
* Clear a single session's index (call on session_end to prevent memory leaks).
|
|
20
|
+
*/
|
|
21
|
+
export declare function _clearSessionIndex(sessionKey: string): void;
|
|
22
|
+
/**
|
|
23
|
+
* Register the cost handler via before_agent_start hook.
|
|
24
|
+
*/
|
|
25
|
+
export declare function registerCostHandler(api: any, config: PodwatchConfig, diagnosticsEnabled: boolean): void;
|
|
26
|
+
//# sourceMappingURL=cost.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cost.d.ts","sourceRoot":"","sources":["../../src/hooks/cost.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAMlD;;GAEG;AACH,wBAAgB,eAAe,IAAI,IAAI,CAEtC;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAE3D;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,GAAG,EACR,MAAM,EAAE,cAAc,EACtB,kBAAkB,EAAE,OAAO,GAC1B,IAAI,CA4EN"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Cost handler — extracts token/cost data from before_agent_start messages.
|
|
4
|
+
*
|
|
5
|
+
* The plugin-sdk's onDiagnosticEvent is broken (separate module instances).
|
|
6
|
+
* Instead, we use before_agent_start which provides the full session history
|
|
7
|
+
* with usage data on every assistant message:
|
|
8
|
+
* { provider, model, usage: { input, output, cacheRead, cacheWrite, totalTokens, cost: { total } } }
|
|
9
|
+
*
|
|
10
|
+
* Dedup strategy: lastSeenIndex tracks how far we've read into event.messages.
|
|
11
|
+
* Each invocation only processes messages from lastSeenIndex onwards, then
|
|
12
|
+
* advances the pointer. Zero memory growth, O(1) bookkeeping.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports._resetCostState = _resetCostState;
|
|
16
|
+
exports._clearSessionIndex = _clearSessionIndex;
|
|
17
|
+
exports.registerCostHandler = registerCostHandler;
|
|
18
|
+
const transmitter_js_1 = require("../transmitter.js");
|
|
19
|
+
// Track how far into event.messages we've already processed — per session
|
|
20
|
+
const lastSeenIndexMap = new Map();
|
|
21
|
+
/**
|
|
22
|
+
* Reset all dedup state (exported for testing).
|
|
23
|
+
*/
|
|
24
|
+
function _resetCostState() {
|
|
25
|
+
lastSeenIndexMap.clear();
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Clear a single session's index (call on session_end to prevent memory leaks).
|
|
29
|
+
*/
|
|
30
|
+
function _clearSessionIndex(sessionKey) {
|
|
31
|
+
lastSeenIndexMap.delete(sessionKey);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Register the cost handler via before_agent_start hook.
|
|
35
|
+
*/
|
|
36
|
+
function registerCostHandler(api, config, diagnosticsEnabled) {
|
|
37
|
+
// before_agent_start carries the full message history with usage on each assistant turn
|
|
38
|
+
api.on("before_agent_start", async (event, ctx) => {
|
|
39
|
+
if (!event?.messages || !Array.isArray(event.messages))
|
|
40
|
+
return;
|
|
41
|
+
const sessionKey = ctx?.sessionKey ?? "__default__";
|
|
42
|
+
// Get per-session index (defaults to 0 for new sessions)
|
|
43
|
+
let lastSeenIndex = lastSeenIndexMap.get(sessionKey) ?? 0;
|
|
44
|
+
// Bounds check: if messages were compacted, reset to 0
|
|
45
|
+
if (lastSeenIndex > event.messages.length) {
|
|
46
|
+
lastSeenIndex = 0;
|
|
47
|
+
}
|
|
48
|
+
// Only process new messages since last invocation
|
|
49
|
+
const newMessages = event.messages.slice(lastSeenIndex);
|
|
50
|
+
lastSeenIndexMap.set(sessionKey, event.messages.length);
|
|
51
|
+
if (newMessages.length === 0)
|
|
52
|
+
return;
|
|
53
|
+
// Detect heartbeat-triggered turns by scanning the last user message
|
|
54
|
+
// OpenClaw heartbeat prompts always contain "HEARTBEAT" (e.g. "Read HEARTBEAT.md")
|
|
55
|
+
let isHeartbeat = false;
|
|
56
|
+
for (let i = event.messages.length - 1; i >= 0; i--) {
|
|
57
|
+
const m = event.messages[i];
|
|
58
|
+
if (m?.role === "user") {
|
|
59
|
+
const text = typeof m.content === "string" ? m.content : JSON.stringify(m.content ?? "");
|
|
60
|
+
if (/HEARTBEAT/i.test(text)) {
|
|
61
|
+
isHeartbeat = true;
|
|
62
|
+
}
|
|
63
|
+
break; // only check the last user message
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
for (const msg of newMessages) {
|
|
67
|
+
// Only assistant messages have usage data
|
|
68
|
+
if (msg.role !== "assistant")
|
|
69
|
+
continue;
|
|
70
|
+
if (!msg.usage)
|
|
71
|
+
continue;
|
|
72
|
+
// Skip zero-cost internal messages (delivery-mirror, etc.)
|
|
73
|
+
if (msg.provider === "openclaw" || msg.model === "delivery-mirror")
|
|
74
|
+
continue;
|
|
75
|
+
if (msg.usage.totalTokens === 0 && !msg.usage.input && !msg.usage.output)
|
|
76
|
+
continue;
|
|
77
|
+
const costTotal = msg.usage.cost?.total ?? undefined;
|
|
78
|
+
transmitter_js_1.transmitter.enqueue({
|
|
79
|
+
type: "cost",
|
|
80
|
+
ts: msg.timestamp ?? Date.now(),
|
|
81
|
+
sessionKey: ctx?.sessionKey,
|
|
82
|
+
agentId: ctx?.agentId,
|
|
83
|
+
provider: msg.provider,
|
|
84
|
+
model: msg.model,
|
|
85
|
+
inputTokens: msg.usage.input ?? 0,
|
|
86
|
+
outputTokens: msg.usage.output ?? 0,
|
|
87
|
+
cacheReadTokens: msg.usage.cacheRead ?? 0,
|
|
88
|
+
cacheWriteTokens: msg.usage.cacheWrite ?? 0,
|
|
89
|
+
totalTokens: msg.usage.totalTokens ?? ((msg.usage.input ?? 0) + (msg.usage.output ?? 0)),
|
|
90
|
+
costUsd: costTotal,
|
|
91
|
+
costBreakdown: msg.usage.cost, // full {input, output, cacheRead, cacheWrite, total} object
|
|
92
|
+
durationMs: undefined,
|
|
93
|
+
// Tag heartbeat-triggered cost events so the dashboard can distinguish them
|
|
94
|
+
...(isHeartbeat ? { sessionType: "heartbeat" } : {}),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}, { name: "podwatch-cost" });
|
|
98
|
+
// Clean up session index on session end to prevent memory leaks
|
|
99
|
+
api.on("session_end", async (_event, ctx) => {
|
|
100
|
+
const sessionKey = ctx?.sessionKey;
|
|
101
|
+
if (sessionKey) {
|
|
102
|
+
lastSeenIndexMap.delete(sessionKey);
|
|
103
|
+
}
|
|
104
|
+
}, { name: "podwatch-cost-cleanup" });
|
|
105
|
+
api.logger.info("[podwatch/cost] Cost tracking via before_agent_start message history");
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=cost.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cost.js","sourceRoot":"","sources":["../../src/hooks/cost.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;GAWG;;AAWH,0CAEC;AAKD,gDAEC;AAKD,kDAgFC;AAtGD,sDAAgD;AAEhD,0EAA0E;AAC1E,MAAM,gBAAgB,GAAG,IAAI,GAAG,EAAkB,CAAC;AAEnD;;GAEG;AACH,SAAgB,eAAe;IAC7B,gBAAgB,CAAC,KAAK,EAAE,CAAC;AAC3B,CAAC;AAED;;GAEG;AACH,SAAgB,kBAAkB,CAAC,UAAkB;IACnD,gBAAgB,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;AACtC,CAAC;AAED;;GAEG;AACH,SAAgB,mBAAmB,CACjC,GAAQ,EACR,MAAsB,EACtB,kBAA2B;IAE3B,wFAAwF;IACxF,GAAG,CAAC,EAAE,CAAC,oBAAoB,EAAE,KAAK,EAAE,KAAU,EAAE,GAAQ,EAAE,EAAE;QAC1D,IAAI,CAAC,KAAK,EAAE,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC;YAAE,OAAO;QAE/D,MAAM,UAAU,GAAW,GAAG,EAAE,UAAU,IAAI,aAAa,CAAC;QAE5D,yDAAyD;QACzD,IAAI,aAAa,GAAG,gBAAgB,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;QAE1D,uDAAuD;QACvD,IAAI,aAAa,GAAG,KAAK,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;YAC1C,aAAa,GAAG,CAAC,CAAC;QACpB,CAAC;QAED,kDAAkD;QAClD,MAAM,WAAW,GAAG,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC;QACxD,gBAAgB,CAAC,GAAG,CAAC,UAAU,EAAE,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAExD,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QAErC,qEAAqE;QACrE,mFAAmF;QACnF,IAAI,WAAW,GAAG,KAAK,CAAC;QACxB,KAAK,IAAI,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;YACpD,MAAM,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;YAC5B,IAAI,CAAC,EAAE,IAAI,KAAK,MAAM,EAAE,CAAC;gBACvB,MAAM,IAAI,GAAG,OAAO,CAAC,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC;gBACzF,IAAI,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;oBAC5B,WAAW,GAAG,IAAI,CAAC;gBACrB,CAAC;gBACD,MAAM,CAAC,mCAAmC;YAC5C,CAAC;QACH,CAAC;QAED,KAAK,MAAM,GAAG,IAAI,WAAW,EAAE,CAAC;YAC9B,0CAA0C;YAC1C,IAAI,GAAG,CAAC,IAAI,KAAK,WAAW;gBAAE,SAAS;YACvC,IAAI,CAAC,GAAG,CAAC,KAAK;gBAAE,SAAS;YAEzB,2DAA2D;YAC3D,IAAI,GAAG,CAAC,QAAQ,KAAK,UAAU,IAAI,GAAG,CAAC,KAAK,KAAK,iBAAiB;gBAAE,SAAS;YAC7E,IAAI,GAAG,CAAC,KAAK,CAAC,WAAW,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM;gBAAE,SAAS;YAEnF,MAAM,SAAS,GAAG,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,IAAI,SAAS,CAAC;YAErD,4BAAW,CAAC,OAAO,CAAC;gBAClB,IAAI,EAAE,MAAM;gBACZ,EAAE,EAAE,GAAG,CAAC,SAAS,IAAI,IAAI,CAAC,GAAG,EAAE;gBAC/B,UAAU,EAAE,GAAG,EAAE,UAAU;gBAC3B,OAAO,EAAE,GAAG,EAAE,OAAO;gBACrB,QAAQ,EAAE,GAAG,CAAC,QAAQ;gBACtB,KAAK,EAAE,GAAG,CAAC,KAAK;gBAChB,WAAW,EAAE,GAAG,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC;gBACjC,YAAY,EAAE,GAAG,CAAC,KAAK,CAAC,MAAM,IAAI,CAAC;gBACnC,eAAe,EAAE,GAAG,CAAC,KAAK,CAAC,SAAS,IAAI,CAAC;gBACzC,gBAAgB,EAAE,GAAG,CAAC,KAAK,CAAC,UAAU,IAAI,CAAC;gBAC3C,WAAW,EAAE,GAAG,CAAC,KAAK,CAAC,WAAW,IAAI,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC;gBACxF,OAAO,EAAE,SAAS;gBAClB,aAAa,EAAE,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,4DAA4D;gBAC3F,UAAU,EAAE,SAAS;gBACrB,4EAA4E;gBAC5E,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aACrD,CAAC,CAAC;QACL,CAAC;IACH,CAAC,EAAE,EAAE,IAAI,EAAE,eAAe,EAAE,CAAC,CAAC;IAE9B,gEAAgE;IAChE,GAAG,CAAC,EAAE,CAAC,aAAa,EAAE,KAAK,EAAE,MAAW,EAAE,GAAQ,EAAE,EAAE;QACpD,MAAM,UAAU,GAAW,GAAG,EAAE,UAAU,CAAC;QAC3C,IAAI,UAAU,EAAE,CAAC;YACf,gBAAgB,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;QACtC,CAAC;IACH,CAAC,EAAE,EAAE,IAAI,EAAE,uBAAuB,EAAE,CAAC,CAAC;IAEtC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,sEAAsE,CAAC,CAAC;AAC1F,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lifecycle handlers — gateway_start, gateway_stop, before_compaction.
|
|
3
|
+
*
|
|
4
|
+
* register(): starts pulse interval + initial skill/plugin scan
|
|
5
|
+
* gateway_stop: flushes pending events, stops intervals
|
|
6
|
+
* before_compaction: tracks context window pressure
|
|
7
|
+
*
|
|
8
|
+
* Pulse = Podwatch plugin's lightweight alive-ping (direct HTTP, no LLM cost).
|
|
9
|
+
* This is NOT OpenClaw's agent heartbeat (which triggers LLM turns).
|
|
10
|
+
*/
|
|
11
|
+
import type { PodwatchConfig } from "../index.js";
|
|
12
|
+
/**
|
|
13
|
+
* Register lifecycle hook handlers.
|
|
14
|
+
*/
|
|
15
|
+
export declare function registerLifecycleHandlers(api: any, config: PodwatchConfig): void;
|
|
16
|
+
//# sourceMappingURL=lifecycle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../src/hooks/lifecycle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAwElD;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,cAAc,GAAG,IAAI,CAwHhF"}
|