@chrysb/alphaclaw 0.3.2 → 0.3.4-beta.0
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/bin/alphaclaw.js +47 -2
- package/lib/cli/git-sync.js +25 -0
- package/lib/plugin/usage-tracker/index.js +308 -0
- package/lib/plugin/usage-tracker/openclaw.plugin.json +8 -0
- package/lib/public/css/explorer.css +1033 -0
- package/lib/public/css/shell.css +50 -4
- package/lib/public/css/theme.css +41 -1
- package/lib/public/icons/folder-line.svg +1 -0
- package/lib/public/icons/hashtag.svg +3 -0
- package/lib/public/icons/home-5-line.svg +1 -0
- package/lib/public/icons/save-fill.svg +3 -0
- package/lib/public/js/app.js +310 -160
- package/lib/public/js/components/action-button.js +12 -1
- package/lib/public/js/components/file-tree.js +497 -0
- package/lib/public/js/components/file-viewer.js +714 -0
- package/lib/public/js/components/icons.js +182 -0
- package/lib/public/js/components/segmented-control.js +33 -0
- package/lib/public/js/components/sidebar-git-panel.js +149 -0
- package/lib/public/js/components/sidebar.js +254 -0
- package/lib/public/js/components/telegram-workspace/index.js +353 -0
- package/lib/public/js/components/telegram-workspace/manage.js +397 -0
- package/lib/public/js/components/telegram-workspace/onboarding.js +616 -0
- package/lib/public/js/components/usage-tab.js +528 -0
- package/lib/public/js/components/watchdog-tab.js +1 -1
- package/lib/public/js/lib/api.js +51 -1
- package/lib/public/js/lib/browse-draft-state.js +109 -0
- package/lib/public/js/lib/file-highlighting.js +6 -0
- package/lib/public/js/lib/file-tree-utils.js +12 -0
- package/lib/public/js/lib/syntax-highlighters/css.js +124 -0
- package/lib/public/js/lib/syntax-highlighters/frontmatter.js +49 -0
- package/lib/public/js/lib/syntax-highlighters/html.js +209 -0
- package/lib/public/js/lib/syntax-highlighters/index.js +28 -0
- package/lib/public/js/lib/syntax-highlighters/javascript.js +134 -0
- package/lib/public/js/lib/syntax-highlighters/json.js +61 -0
- package/lib/public/js/lib/syntax-highlighters/markdown.js +37 -0
- package/lib/public/js/lib/syntax-highlighters/utils.js +13 -0
- package/lib/public/js/lib/telegram-api.js +78 -0
- package/lib/public/js/lib/ui-settings.js +38 -0
- package/lib/public/setup.html +34 -29
- package/lib/server/alphaclaw-version.js +3 -3
- package/lib/server/constants.js +2 -0
- package/lib/server/onboarding/openclaw.js +15 -0
- package/lib/server/onboarding/workspace.js +3 -2
- package/lib/server/routes/auth.js +5 -1
- package/lib/server/routes/browse.js +295 -0
- package/lib/server/routes/telegram.js +185 -60
- package/lib/server/routes/usage.js +133 -0
- package/lib/server/usage-db.js +570 -0
- package/lib/server.js +45 -4
- package/lib/setup/core-prompts/AGENTS.md +0 -101
- package/lib/setup/core-prompts/TOOLS.md +3 -1
- package/lib/setup/skills/control-ui/SKILL.md +12 -20
- package/package.json +1 -1
- package/lib/public/js/components/telegram-workspace.js +0 -1365
package/bin/alphaclaw.js
CHANGED
|
@@ -5,8 +5,20 @@ const fs = require("fs");
|
|
|
5
5
|
const os = require("os");
|
|
6
6
|
const path = require("path");
|
|
7
7
|
const { execSync } = require("child_process");
|
|
8
|
+
const {
|
|
9
|
+
normalizeGitSyncFilePath,
|
|
10
|
+
validateGitSyncFilePath,
|
|
11
|
+
} = require("../lib/cli/git-sync");
|
|
8
12
|
const { buildSecretReplacements } = require("../lib/server/helpers");
|
|
9
13
|
|
|
14
|
+
const kUsageTrackerPluginPath = path.resolve(
|
|
15
|
+
__dirname,
|
|
16
|
+
"..",
|
|
17
|
+
"lib",
|
|
18
|
+
"plugin",
|
|
19
|
+
"usage-tracker",
|
|
20
|
+
);
|
|
21
|
+
|
|
10
22
|
// ---------------------------------------------------------------------------
|
|
11
23
|
// Parse CLI flags
|
|
12
24
|
// ---------------------------------------------------------------------------
|
|
@@ -84,6 +96,7 @@ start options:
|
|
|
84
96
|
|
|
85
97
|
git-sync options:
|
|
86
98
|
--message, -m <text> Commit message
|
|
99
|
+
--file, -f <path> Optional file path in .openclaw to sync only one file
|
|
87
100
|
|
|
88
101
|
telegram topic add options:
|
|
89
102
|
--thread <id> Telegram thread ID
|
|
@@ -93,6 +106,7 @@ telegram topic add options:
|
|
|
93
106
|
|
|
94
107
|
Examples:
|
|
95
108
|
alphaclaw git-sync --message "sync workspace"
|
|
109
|
+
alphaclaw git-sync --message "update config" --file "workspace/app/config.json"
|
|
96
110
|
alphaclaw telegram topic add --thread 12 --name "Testing"
|
|
97
111
|
alphaclaw telegram topic add --thread 12 --name "Testing" --system "Handle QA requests"
|
|
98
112
|
`);
|
|
@@ -234,10 +248,21 @@ const runGitSync = () => {
|
|
|
234
248
|
const commitMessage = String(
|
|
235
249
|
flagValue(commandArgs, "--message", "-m") || "",
|
|
236
250
|
).trim();
|
|
251
|
+
const requestedFilePath = String(
|
|
252
|
+
flagValue(commandArgs, "--file", "-f") || "",
|
|
253
|
+
).trim();
|
|
254
|
+
const normalizedFilePath = normalizeGitSyncFilePath(requestedFilePath);
|
|
237
255
|
if (!commitMessage) {
|
|
238
256
|
console.error("[alphaclaw] Missing --message for git-sync");
|
|
239
257
|
return 1;
|
|
240
258
|
}
|
|
259
|
+
if (normalizedFilePath) {
|
|
260
|
+
const pathValidation = validateGitSyncFilePath(normalizedFilePath);
|
|
261
|
+
if (!pathValidation.ok) {
|
|
262
|
+
console.error(pathValidation.error);
|
|
263
|
+
return 1;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
241
266
|
if (!githubToken) {
|
|
242
267
|
console.error("[alphaclaw] Missing GITHUB_TOKEN for git-sync");
|
|
243
268
|
return 1;
|
|
@@ -312,13 +337,23 @@ const runGitSync = () => {
|
|
|
312
337
|
`[alphaclaw] Remote branch "${branch}" not found, skipping pull`,
|
|
313
338
|
);
|
|
314
339
|
}
|
|
315
|
-
|
|
340
|
+
if (normalizedFilePath) {
|
|
341
|
+
runGit(`add -A -- ${quoteArg(normalizedFilePath)}`);
|
|
342
|
+
} else {
|
|
343
|
+
runGit("add -A");
|
|
344
|
+
}
|
|
316
345
|
try {
|
|
317
346
|
runGit("diff --cached --quiet");
|
|
318
347
|
console.log("[alphaclaw] No changes to commit");
|
|
319
348
|
return 0;
|
|
320
349
|
} catch {}
|
|
321
|
-
|
|
350
|
+
if (normalizedFilePath) {
|
|
351
|
+
runGit(
|
|
352
|
+
`commit -m ${quoteArg(commitMessage)} -- ${quoteArg(normalizedFilePath)}`,
|
|
353
|
+
);
|
|
354
|
+
} else {
|
|
355
|
+
runGit(`commit -m ${quoteArg(commitMessage)}`);
|
|
356
|
+
}
|
|
322
357
|
runGit(`push origin ${quoteArg(branch)}`, { withAuth: true });
|
|
323
358
|
const hash = String(runGit("rev-parse --short HEAD")).trim();
|
|
324
359
|
console.log(`[alphaclaw] Git sync complete (${hash})`);
|
|
@@ -745,6 +780,8 @@ if (fs.existsSync(configPath)) {
|
|
|
745
780
|
const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
746
781
|
if (!cfg.channels) cfg.channels = {};
|
|
747
782
|
if (!cfg.plugins) cfg.plugins = {};
|
|
783
|
+
if (!cfg.plugins.load) cfg.plugins.load = {};
|
|
784
|
+
if (!Array.isArray(cfg.plugins.load.paths)) cfg.plugins.load.paths = [];
|
|
748
785
|
if (!cfg.plugins.entries) cfg.plugins.entries = {};
|
|
749
786
|
let changed = false;
|
|
750
787
|
|
|
@@ -771,6 +808,14 @@ if (fs.existsSync(configPath)) {
|
|
|
771
808
|
console.log("[alphaclaw] Discord added");
|
|
772
809
|
changed = true;
|
|
773
810
|
}
|
|
811
|
+
if (!cfg.plugins.load.paths.includes(kUsageTrackerPluginPath)) {
|
|
812
|
+
cfg.plugins.load.paths.push(kUsageTrackerPluginPath);
|
|
813
|
+
changed = true;
|
|
814
|
+
}
|
|
815
|
+
if (cfg.plugins.entries["usage-tracker"]?.enabled !== true) {
|
|
816
|
+
cfg.plugins.entries["usage-tracker"] = { enabled: true };
|
|
817
|
+
changed = true;
|
|
818
|
+
}
|
|
774
819
|
|
|
775
820
|
if (changed) {
|
|
776
821
|
let content = JSON.stringify(cfg, null, 2);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const normalizeGitSyncFilePath = (requestedFilePath) => {
|
|
2
|
+
const rawPath = String(requestedFilePath || "").trim();
|
|
3
|
+
if (!rawPath) return "";
|
|
4
|
+
return rawPath.replace(/\\/g, "/").replace(/^\.\/+/, "");
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
const validateGitSyncFilePath = (normalizedFilePath) => {
|
|
8
|
+
if (!normalizedFilePath) return { ok: true };
|
|
9
|
+
if (
|
|
10
|
+
normalizedFilePath.startsWith("/") ||
|
|
11
|
+
normalizedFilePath.startsWith("../") ||
|
|
12
|
+
normalizedFilePath.includes("/../")
|
|
13
|
+
) {
|
|
14
|
+
return {
|
|
15
|
+
ok: false,
|
|
16
|
+
error: "[alphaclaw] --file must stay within /data/.openclaw",
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
return { ok: true };
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
module.exports = {
|
|
23
|
+
normalizeGitSyncFilePath,
|
|
24
|
+
validateGitSyncFilePath,
|
|
25
|
+
};
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const os = require("os");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const { DatabaseSync } = require("node:sqlite");
|
|
5
|
+
|
|
6
|
+
const kPluginId = "usage-tracker";
|
|
7
|
+
const kFallbackRootDir = path.join(os.homedir(), ".alphaclaw");
|
|
8
|
+
|
|
9
|
+
const coerceCount = (value) => {
|
|
10
|
+
const parsed = Number.parseInt(String(value ?? 0), 10);
|
|
11
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const resolveRootDir = () =>
|
|
15
|
+
process.env.ALPHACLAW_ROOT_DIR ||
|
|
16
|
+
process.env.OPENCLAW_HOME ||
|
|
17
|
+
process.env.OPENCLAW_ROOT_DIR ||
|
|
18
|
+
kFallbackRootDir;
|
|
19
|
+
|
|
20
|
+
const safeAlterTable = (database, sql) => {
|
|
21
|
+
try {
|
|
22
|
+
database.exec(sql);
|
|
23
|
+
} catch (err) {
|
|
24
|
+
const message = String(err?.message || "").toLowerCase();
|
|
25
|
+
if (!message.includes("duplicate column name")) throw err;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const ensureSchema = (database) => {
|
|
30
|
+
database.exec("PRAGMA journal_mode=WAL;");
|
|
31
|
+
database.exec("PRAGMA synchronous=NORMAL;");
|
|
32
|
+
database.exec("PRAGMA busy_timeout=5000;");
|
|
33
|
+
database.exec(`
|
|
34
|
+
CREATE TABLE IF NOT EXISTS usage_events (
|
|
35
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
36
|
+
timestamp INTEGER NOT NULL,
|
|
37
|
+
session_id TEXT,
|
|
38
|
+
session_key TEXT,
|
|
39
|
+
run_id TEXT,
|
|
40
|
+
provider TEXT NOT NULL,
|
|
41
|
+
model TEXT NOT NULL,
|
|
42
|
+
input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
43
|
+
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
44
|
+
cache_read_tokens INTEGER NOT NULL DEFAULT 0,
|
|
45
|
+
cache_write_tokens INTEGER NOT NULL DEFAULT 0,
|
|
46
|
+
total_tokens INTEGER NOT NULL DEFAULT 0
|
|
47
|
+
);
|
|
48
|
+
`);
|
|
49
|
+
database.exec(`
|
|
50
|
+
CREATE INDEX IF NOT EXISTS idx_usage_events_ts
|
|
51
|
+
ON usage_events(timestamp DESC);
|
|
52
|
+
`);
|
|
53
|
+
database.exec(`
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_usage_events_session
|
|
55
|
+
ON usage_events(session_id);
|
|
56
|
+
`);
|
|
57
|
+
safeAlterTable(
|
|
58
|
+
database,
|
|
59
|
+
"ALTER TABLE usage_events ADD COLUMN session_key TEXT;",
|
|
60
|
+
);
|
|
61
|
+
database.exec(`
|
|
62
|
+
CREATE INDEX IF NOT EXISTS idx_usage_events_session_key
|
|
63
|
+
ON usage_events(session_key);
|
|
64
|
+
`);
|
|
65
|
+
database.exec(`
|
|
66
|
+
CREATE TABLE IF NOT EXISTS usage_daily (
|
|
67
|
+
date TEXT NOT NULL,
|
|
68
|
+
model TEXT NOT NULL,
|
|
69
|
+
provider TEXT,
|
|
70
|
+
input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
71
|
+
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
72
|
+
cache_read_tokens INTEGER NOT NULL DEFAULT 0,
|
|
73
|
+
cache_write_tokens INTEGER NOT NULL DEFAULT 0,
|
|
74
|
+
total_tokens INTEGER NOT NULL DEFAULT 0,
|
|
75
|
+
turn_count INTEGER NOT NULL DEFAULT 0,
|
|
76
|
+
PRIMARY KEY (date, model)
|
|
77
|
+
);
|
|
78
|
+
`);
|
|
79
|
+
database.exec(`
|
|
80
|
+
CREATE TABLE IF NOT EXISTS tool_events (
|
|
81
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
82
|
+
timestamp INTEGER NOT NULL,
|
|
83
|
+
session_id TEXT,
|
|
84
|
+
session_key TEXT,
|
|
85
|
+
tool_name TEXT NOT NULL,
|
|
86
|
+
success INTEGER NOT NULL DEFAULT 1,
|
|
87
|
+
duration_ms INTEGER
|
|
88
|
+
);
|
|
89
|
+
`);
|
|
90
|
+
database.exec(`
|
|
91
|
+
CREATE INDEX IF NOT EXISTS idx_tool_events_session
|
|
92
|
+
ON tool_events(session_id);
|
|
93
|
+
`);
|
|
94
|
+
safeAlterTable(
|
|
95
|
+
database,
|
|
96
|
+
"ALTER TABLE tool_events ADD COLUMN session_key TEXT;",
|
|
97
|
+
);
|
|
98
|
+
database.exec(`
|
|
99
|
+
CREATE INDEX IF NOT EXISTS idx_tool_events_session_key
|
|
100
|
+
ON tool_events(session_key);
|
|
101
|
+
`);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const createPlugin = () => {
|
|
105
|
+
let database = null;
|
|
106
|
+
let dbPath = "";
|
|
107
|
+
let insertUsageEventStmt = null;
|
|
108
|
+
let upsertUsageDailyStmt = null;
|
|
109
|
+
let insertToolEventStmt = null;
|
|
110
|
+
|
|
111
|
+
const getDatabase = () => {
|
|
112
|
+
if (database) return database;
|
|
113
|
+
const rootDir = resolveRootDir();
|
|
114
|
+
const dbDir = path.join(rootDir, "db");
|
|
115
|
+
fs.mkdirSync(dbDir, { recursive: true });
|
|
116
|
+
dbPath = path.join(dbDir, "usage.db");
|
|
117
|
+
database = new DatabaseSync(dbPath);
|
|
118
|
+
ensureSchema(database);
|
|
119
|
+
insertUsageEventStmt = database.prepare(`
|
|
120
|
+
INSERT INTO usage_events (
|
|
121
|
+
timestamp,
|
|
122
|
+
session_id,
|
|
123
|
+
session_key,
|
|
124
|
+
run_id,
|
|
125
|
+
provider,
|
|
126
|
+
model,
|
|
127
|
+
input_tokens,
|
|
128
|
+
output_tokens,
|
|
129
|
+
cache_read_tokens,
|
|
130
|
+
cache_write_tokens,
|
|
131
|
+
total_tokens
|
|
132
|
+
) VALUES (
|
|
133
|
+
$timestamp,
|
|
134
|
+
$session_id,
|
|
135
|
+
$session_key,
|
|
136
|
+
$run_id,
|
|
137
|
+
$provider,
|
|
138
|
+
$model,
|
|
139
|
+
$input_tokens,
|
|
140
|
+
$output_tokens,
|
|
141
|
+
$cache_read_tokens,
|
|
142
|
+
$cache_write_tokens,
|
|
143
|
+
$total_tokens
|
|
144
|
+
)
|
|
145
|
+
`);
|
|
146
|
+
upsertUsageDailyStmt = database.prepare(`
|
|
147
|
+
INSERT INTO usage_daily (
|
|
148
|
+
date,
|
|
149
|
+
model,
|
|
150
|
+
provider,
|
|
151
|
+
input_tokens,
|
|
152
|
+
output_tokens,
|
|
153
|
+
cache_read_tokens,
|
|
154
|
+
cache_write_tokens,
|
|
155
|
+
total_tokens,
|
|
156
|
+
turn_count
|
|
157
|
+
) VALUES (
|
|
158
|
+
$date,
|
|
159
|
+
$model,
|
|
160
|
+
$provider,
|
|
161
|
+
$input_tokens,
|
|
162
|
+
$output_tokens,
|
|
163
|
+
$cache_read_tokens,
|
|
164
|
+
$cache_write_tokens,
|
|
165
|
+
$total_tokens,
|
|
166
|
+
1
|
|
167
|
+
)
|
|
168
|
+
ON CONFLICT(date, model) DO UPDATE SET
|
|
169
|
+
provider = COALESCE(excluded.provider, usage_daily.provider),
|
|
170
|
+
input_tokens = usage_daily.input_tokens + excluded.input_tokens,
|
|
171
|
+
output_tokens = usage_daily.output_tokens + excluded.output_tokens,
|
|
172
|
+
cache_read_tokens = usage_daily.cache_read_tokens + excluded.cache_read_tokens,
|
|
173
|
+
cache_write_tokens = usage_daily.cache_write_tokens + excluded.cache_write_tokens,
|
|
174
|
+
total_tokens = usage_daily.total_tokens + excluded.total_tokens,
|
|
175
|
+
turn_count = usage_daily.turn_count + 1
|
|
176
|
+
`);
|
|
177
|
+
insertToolEventStmt = database.prepare(`
|
|
178
|
+
INSERT INTO tool_events (
|
|
179
|
+
timestamp,
|
|
180
|
+
session_id,
|
|
181
|
+
session_key,
|
|
182
|
+
tool_name,
|
|
183
|
+
success,
|
|
184
|
+
duration_ms
|
|
185
|
+
) VALUES (
|
|
186
|
+
$timestamp,
|
|
187
|
+
$session_id,
|
|
188
|
+
$session_key,
|
|
189
|
+
$tool_name,
|
|
190
|
+
$success,
|
|
191
|
+
$duration_ms
|
|
192
|
+
)
|
|
193
|
+
`);
|
|
194
|
+
return database;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const writeUsageEvent = (event, ctx, logger) => {
|
|
198
|
+
const usage = event?.usage ?? {};
|
|
199
|
+
const timestamp = Date.now();
|
|
200
|
+
const date = new Date(timestamp).toISOString().slice(0, 10);
|
|
201
|
+
const inputTokens = coerceCount(usage.input);
|
|
202
|
+
const outputTokens = coerceCount(usage.output);
|
|
203
|
+
const cacheReadTokens = coerceCount(usage.cacheRead);
|
|
204
|
+
const cacheWriteTokens = coerceCount(usage.cacheWrite);
|
|
205
|
+
const fallbackTotal = inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens;
|
|
206
|
+
const totalTokens = coerceCount(usage.total) || fallbackTotal;
|
|
207
|
+
if (totalTokens <= 0) return;
|
|
208
|
+
getDatabase();
|
|
209
|
+
insertUsageEventStmt.run({
|
|
210
|
+
$timestamp: timestamp,
|
|
211
|
+
$session_id: String(event?.sessionId || ctx?.sessionId || ""),
|
|
212
|
+
$session_key: String(ctx?.sessionKey || ""),
|
|
213
|
+
$run_id: String(event?.runId || ""),
|
|
214
|
+
$provider: String(event?.provider || "unknown"),
|
|
215
|
+
$model: String(event?.model || "unknown"),
|
|
216
|
+
$input_tokens: inputTokens,
|
|
217
|
+
$output_tokens: outputTokens,
|
|
218
|
+
$cache_read_tokens: cacheReadTokens,
|
|
219
|
+
$cache_write_tokens: cacheWriteTokens,
|
|
220
|
+
$total_tokens: totalTokens,
|
|
221
|
+
});
|
|
222
|
+
upsertUsageDailyStmt.run({
|
|
223
|
+
$date: date,
|
|
224
|
+
$model: String(event?.model || "unknown"),
|
|
225
|
+
$provider: String(event?.provider || "unknown"),
|
|
226
|
+
$input_tokens: inputTokens,
|
|
227
|
+
$output_tokens: outputTokens,
|
|
228
|
+
$cache_read_tokens: cacheReadTokens,
|
|
229
|
+
$cache_write_tokens: cacheWriteTokens,
|
|
230
|
+
$total_tokens: totalTokens,
|
|
231
|
+
});
|
|
232
|
+
if (logger?.debug) {
|
|
233
|
+
logger.debug(
|
|
234
|
+
`[${kPluginId}] usage event recorded model=${String(event?.model || "unknown")} total=${totalTokens}`,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const deriveToolSuccess = (event) => {
|
|
240
|
+
const message = event?.message;
|
|
241
|
+
if (!message || typeof message !== "object") {
|
|
242
|
+
return event?.error ? 0 : 1;
|
|
243
|
+
}
|
|
244
|
+
if (message?.isError === true) return 0;
|
|
245
|
+
if (message?.ok === false) return 0;
|
|
246
|
+
if (typeof message?.error === "string" && message.error.trim()) return 0;
|
|
247
|
+
return 1;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const writeToolEvent = (event, ctx) => {
|
|
251
|
+
const toolName = String(event?.toolName || "").trim();
|
|
252
|
+
if (!toolName) return;
|
|
253
|
+
const sessionKey = String(ctx?.sessionKey || "").trim();
|
|
254
|
+
const sessionId = String(ctx?.sessionId || "").trim();
|
|
255
|
+
if (!sessionKey && !sessionId) return;
|
|
256
|
+
getDatabase();
|
|
257
|
+
insertToolEventStmt.run({
|
|
258
|
+
$timestamp: Date.now(),
|
|
259
|
+
$session_id: sessionId,
|
|
260
|
+
$session_key: sessionKey,
|
|
261
|
+
$tool_name: toolName,
|
|
262
|
+
$success: deriveToolSuccess(event),
|
|
263
|
+
$duration_ms: coerceCount(event?.durationMs) || null,
|
|
264
|
+
});
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
id: kPluginId,
|
|
269
|
+
name: "AlphaClaw Usage Tracker",
|
|
270
|
+
description: "Captures LLM and tool usage into SQLite for Usage UI",
|
|
271
|
+
register: (api) => {
|
|
272
|
+
const logger = api?.logger;
|
|
273
|
+
try {
|
|
274
|
+
getDatabase();
|
|
275
|
+
logger?.info?.(`[${kPluginId}] initialized db=${dbPath}`);
|
|
276
|
+
} catch (err) {
|
|
277
|
+
logger?.error?.(`[${kPluginId}] failed to initialize database: ${err?.message || err}`);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
api.on("llm_output", (event, ctx) => {
|
|
281
|
+
try {
|
|
282
|
+
writeUsageEvent(event, ctx, logger);
|
|
283
|
+
} catch (err) {
|
|
284
|
+
logger?.error?.(`[${kPluginId}] llm_output write error: ${err?.message || err}`);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
api.on("tool_result_persist", (event, ctx) => {
|
|
288
|
+
try {
|
|
289
|
+
writeToolEvent(
|
|
290
|
+
{
|
|
291
|
+
...event,
|
|
292
|
+
toolName: String(event?.toolName || ctx?.toolName || ""),
|
|
293
|
+
durationMs: event?.durationMs,
|
|
294
|
+
},
|
|
295
|
+
ctx,
|
|
296
|
+
);
|
|
297
|
+
} catch (err) {
|
|
298
|
+
logger?.error?.(`[${kPluginId}] tool_result_persist write error: ${err?.message || err}`);
|
|
299
|
+
}
|
|
300
|
+
return {};
|
|
301
|
+
});
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const plugin = createPlugin();
|
|
307
|
+
module.exports = plugin;
|
|
308
|
+
module.exports.default = plugin;
|