@better_openclaw/betterclaw 3.0.1 → 3.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better_openclaw/betterclaw",
3
- "version": "3.0.1",
3
+ "version": "3.0.3",
4
4
  "description": "Intelligent event filtering, context tracking, and proactive triggers for BetterClaw",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {
@@ -9,12 +9,17 @@
9
9
  },
10
10
  "type": "module",
11
11
  "openclaw": {
12
- "extensions": ["./src/index.ts"]
12
+ "extensions": [
13
+ "./src/index.ts"
14
+ ]
13
15
  },
14
16
  "dependencies": {
15
17
  "@sinclair/typebox": "^0.34.0"
16
18
  },
17
19
  "devDependencies": {
20
+ "@types/node": "^25.5.2",
21
+ "openclaw": "^2026.4.5",
22
+ "typescript": "^6.0.2",
18
23
  "vitest": "^3.0.0"
19
24
  }
20
25
  }
package/src/cli.ts CHANGED
@@ -24,7 +24,14 @@ export const BETTERCLAW_COMMANDS = [
24
24
  "system.notify",
25
25
  ].sort();
26
26
 
27
+ export const BETTERCLAW_TOOLS = ["check_tier", "get_context"];
28
+
27
29
  export function mergeAllowCommands(existing: string[], toAdd: string[]): string[] {
28
30
  const set = new Set([...existing, ...toAdd]);
29
31
  return [...set].sort();
30
32
  }
33
+
34
+ export function mergeAlsoAllow(existing: string[], toAdd: string[]): string[] {
35
+ const set = new Set([...existing, ...toAdd]);
36
+ return [...set];
37
+ }
package/src/context.ts CHANGED
@@ -9,7 +9,7 @@ export class ContextManager {
9
9
  private contextPath: string;
10
10
  private patternsPath: string;
11
11
  private context: DeviceContext;
12
- private runtimeState: RuntimeState = { tier: "free", smartMode: false };
12
+ private runtimeState: RuntimeState = { tier: null, smartMode: false };
13
13
  private timestamps: Record<string, number> = {};
14
14
  private deviceConfig: DeviceConfig = {};
15
15
 
@@ -44,6 +44,11 @@ export class ContextManager {
44
44
  const parsed = JSON.parse(raw);
45
45
  this.timestamps = parsed._timestamps ?? {};
46
46
  delete parsed._timestamps;
47
+ const savedTier = parsed._tier;
48
+ delete parsed._tier;
49
+ if (savedTier === "free" || savedTier === "premium") {
50
+ this.runtimeState = { ...this.runtimeState, tier: savedTier };
51
+ }
47
52
  this.context = parsed as DeviceContext;
48
53
  } catch {
49
54
  this.context = ContextManager.empty();
@@ -78,7 +83,7 @@ export class ContextManager {
78
83
 
79
84
  async save(): Promise<void> {
80
85
  await fs.mkdir(path.dirname(this.contextPath), { recursive: true });
81
- const data = { ...this.context, _timestamps: this.timestamps };
86
+ const data = { ...this.context, _timestamps: this.timestamps, _tier: this.runtimeState.tier };
82
87
  await fs.writeFile(this.contextPath, JSON.stringify(data, null, 2) + "\n", "utf8");
83
88
  const configPath = path.join(path.dirname(this.contextPath), "device-config.json");
84
89
  await fs.writeFile(configPath, JSON.stringify(this.deviceConfig, null, 2) + "\n", "utf8");
package/src/index.ts CHANGED
@@ -7,7 +7,7 @@ import { RulesEngine } from "./filter.js";
7
7
  import { PatternEngine } from "./patterns.js";
8
8
  import { processEvent } from "./pipeline.js";
9
9
  import type { PipelineDeps } from "./pipeline.js";
10
- import { BETTERCLAW_COMMANDS, mergeAllowCommands } from "./cli.js";
10
+ import { BETTERCLAW_COMMANDS, BETTERCLAW_TOOLS, mergeAllowCommands, mergeAlsoAllow } from "./cli.js";
11
11
  import { storeJwt } from "./jwt.js";
12
12
  import { loadTriageProfile, runLearner } from "./learner.js";
13
13
  import { ReactionTracker } from "./reactions.js";
@@ -70,7 +70,6 @@ export default {
70
70
 
71
71
  // Calibration state
72
72
  let calibrationStartedAt: number | null = null;
73
- let pingReceived = false;
74
73
 
75
74
  const calibrationFile = path.join(stateDir, "calibration.json");
76
75
  (async () => {
@@ -142,9 +141,9 @@ export default {
142
141
 
143
142
  // Ping health check
144
143
  api.registerGatewayMethod("betterclaw.ping", async ({ params, respond, context }) => {
145
- const validTiers: Array<"free" | "premium" | "premium+"> = ["free", "premium", "premium+"];
144
+ const validTiers: Array<"free" | "premium"> = ["free", "premium"];
146
145
  const rawTier = (params as Record<string, unknown>)?.tier as string;
147
- const tier = validTiers.includes(rawTier as any) ? (rawTier as "free" | "premium" | "premium+") : "free";
146
+ const tier = validTiers.includes(rawTier as any) ? (rawTier as "free" | "premium") : "free";
148
147
  const smartMode = (params as Record<string, unknown>)?.smartMode === true;
149
148
 
150
149
  const jwt = (params as Record<string, unknown>)?.jwt as string | undefined;
@@ -159,10 +158,8 @@ export default {
159
158
 
160
159
  ctxManager.setRuntimeState({ tier, smartMode });
161
160
 
162
- pingReceived = true;
163
-
164
161
  // Initialize calibration on first premium ping
165
- if ((tier === "premium" || tier === "premium+") && calibrationStartedAt === null) {
162
+ if (tier === "premium" && calibrationStartedAt === null) {
166
163
  const existingProfile = await loadTriageProfile(stateDir);
167
164
  if (existingProfile?.computedAt) {
168
165
  calibrationStartedAt = existingProfile.computedAt - config.calibrationDays * 86400;
@@ -365,7 +362,6 @@ export default {
365
362
  // Agent tools
366
363
  api.registerTool(
367
364
  createCheckTierTool(ctxManager, () => ({
368
- pingReceived,
369
365
  calibrating: isCalibrating(),
370
366
  calibrationEndsAt: calibrationStartedAt
371
367
  ? calibrationStartedAt + config.calibrationDays * 86400
@@ -454,7 +450,7 @@ export default {
454
450
  if (ctxManager.getRuntimeState().smartMode) {
455
451
  // Scan reactions first (feeds into learner)
456
452
  try {
457
- await scanPendingReactions({ reactions: reactionTracker, api, config, stateDir });
453
+ await scanPendingReactions({ reactions: reactionTracker, api });
458
454
  } catch (err) {
459
455
  api.logger.error(`betterclaw: reaction scan failed: ${err}`);
460
456
  }
@@ -490,37 +486,54 @@ export default {
490
486
 
491
487
  cmd
492
488
  .command("setup")
493
- .description("Configure gateway allowedCommands for BetterClaw")
489
+ .description("Configure gateway allowedCommands and agent tools for BetterClaw")
494
490
  .option("--dry-run", "Preview changes without writing")
495
491
  .action(async (opts: { dryRun?: boolean }) => {
496
492
  try {
497
493
  const currentConfig = await api.runtime.config.loadConfig();
498
- const existing: string[] =
499
- (currentConfig as any)?.gateway?.nodes?.allowCommands ?? [];
500
- const merged = mergeAllowCommands(existing, BETTERCLAW_COMMANDS);
501
- const added = merged.length - existing.length;
494
+ const configObj = { ...currentConfig } as any;
495
+
496
+ // 1. Merge node allowedCommands
497
+ const existingCmds: string[] = configObj?.gateway?.nodes?.allowCommands ?? [];
498
+ const mergedCmds = mergeAllowCommands(existingCmds, BETTERCLAW_COMMANDS);
499
+ const addedCmds = mergedCmds.length - existingCmds.length;
500
+
501
+ // 2. Merge tools.alsoAllow for plugin tools
502
+ configObj.tools = configObj.tools ?? {};
503
+ const existingAllow: string[] = configObj.tools.alsoAllow ?? [];
504
+ const mergedAllow = mergeAlsoAllow(existingAllow, BETTERCLAW_TOOLS);
505
+ const addedTools = mergedAllow.length - existingAllow.length;
502
506
 
503
507
  if (opts.dryRun) {
504
- console.log(`[dry-run] Would set ${merged.length} allowedCommands (${added} new)`);
505
- if (added > 0) {
506
- const newCmds = merged.filter((c) => !existing.includes(c));
507
- console.log(`New commands: ${newCmds.join(", ")}`);
508
+ if (addedCmds > 0) {
509
+ const newCmds = mergedCmds.filter((c) => !existingCmds.includes(c));
510
+ console.log(`[dry-run] Would add ${addedCmds} node commands: ${newCmds.join(", ")}`);
511
+ }
512
+ if (addedTools > 0) {
513
+ const newTools = mergedAllow.filter((t) => !existingAllow.includes(t));
514
+ console.log(`[dry-run] Would add ${addedTools} agent tools to alsoAllow: ${newTools.join(", ")}`);
515
+ }
516
+ if (addedCmds === 0 && addedTools === 0) {
517
+ console.log("[dry-run] Everything already configured.");
508
518
  }
509
519
  return;
510
520
  }
511
521
 
512
- if (added === 0) {
513
- console.log(`All ${BETTERCLAW_COMMANDS.length} BetterClaw commands already configured.`);
522
+ if (addedCmds === 0 && addedTools === 0) {
523
+ console.log("All BetterClaw commands and tools already configured.");
514
524
  return;
515
525
  }
516
526
 
517
- const configObj = { ...currentConfig } as any;
518
527
  configObj.gateway = configObj.gateway ?? {};
519
528
  configObj.gateway.nodes = configObj.gateway.nodes ?? {};
520
- configObj.gateway.nodes.allowCommands = merged;
529
+ configObj.gateway.nodes.allowCommands = mergedCmds;
530
+ configObj.tools.alsoAllow = mergedAllow;
521
531
  await api.runtime.config.writeConfigFile(configObj);
522
532
 
523
- console.log(`Added ${added} new commands (${merged.length} total). Restart gateway to apply.`);
533
+ const parts: string[] = [];
534
+ if (addedCmds > 0) parts.push(`${addedCmds} node commands`);
535
+ if (addedTools > 0) parts.push(`${addedTools} agent tools (${BETTERCLAW_TOOLS.join(", ")})`);
536
+ console.log(`Added ${parts.join(" + ")}. Restart gateway to apply.`);
524
537
  } catch (err) {
525
538
  console.error(`Failed to update config: ${err}`);
526
539
  process.exit(1);
package/src/jwt.ts CHANGED
@@ -54,7 +54,7 @@ export async function verifyJwt(
54
54
  const valid = await crypto.subtle.verify(
55
55
  { name: "ECDSA", hash: "SHA-256" },
56
56
  publicKey,
57
- signature,
57
+ signature as Uint8Array<ArrayBuffer>,
58
58
  signingInput
59
59
  );
60
60
  if (!valid) return null;
package/src/learner.ts CHANGED
@@ -165,7 +165,7 @@ export async function runLearner(deps: RunLearnerDeps): Promise<void> {
165
165
  });
166
166
 
167
167
  // 11. Parse last assistant message — handle both string and content-block formats
168
- const lastAssistant = messages.filter((m: any) => m.role === "assistant").pop();
168
+ const lastAssistant = (messages as any[]).filter((m) => m.role === "assistant").pop();
169
169
  if (lastAssistant) {
170
170
  const content = typeof lastAssistant.content === "string"
171
171
  ? lastAssistant.content
@@ -17,9 +17,6 @@ export interface ClassificationResult {
17
17
  export interface ScanDeps {
18
18
  api: OpenClawPluginApi;
19
19
  reactions: ReactionTracker;
20
- classificationModel: string;
21
- classificationApiBase?: string;
22
- getApiKey: () => Promise<string | undefined>;
23
20
  }
24
21
 
25
22
  // --- Helpers ---
@@ -151,7 +148,7 @@ export async function scanPendingReactions(deps: ScanDeps): Promise<void> {
151
148
  sessionKey: "main",
152
149
  limit: 200,
153
150
  });
154
- messages = fetched;
151
+ messages = fetched as typeof messages;
155
152
  } catch (err) {
156
153
  api.logger.error(
157
154
  `reaction-scanner: failed to fetch session messages: ${err instanceof Error ? err.message : String(err)}`,
@@ -198,7 +195,7 @@ export async function scanPendingReactions(deps: ScanDeps): Promise<void> {
198
195
  limit: 5,
199
196
  });
200
197
 
201
- const lastAssistant = classifyMessages.filter((m: any) => m.role === "assistant").pop();
198
+ const lastAssistant = (classifyMessages as any[]).filter((m) => m.role === "assistant").pop();
202
199
  if (lastAssistant) {
203
200
  const content = extractText(lastAssistant.content);
204
201
  if (content) {
@@ -2,7 +2,6 @@ import { Type } from "@sinclair/typebox";
2
2
  import type { ContextManager } from "../context.js";
3
3
 
4
4
  export interface CheckTierState {
5
- pingReceived: boolean;
6
5
  calibrating: boolean;
7
6
  calibrationEndsAt?: number;
8
7
  }
@@ -16,8 +15,9 @@ export function createCheckTierTool(ctx: ContextManager, getState: () => CheckTi
16
15
  parameters: Type.Object({}),
17
16
  async execute(_id: string, _params: Record<string, unknown>) {
18
17
  const state = getState();
18
+ const runtime = ctx.getRuntimeState();
19
19
 
20
- if (!state.pingReceived) {
20
+ if (runtime.tier === null) {
21
21
  return {
22
22
  content: [{
23
23
  type: "text" as const,
@@ -28,13 +28,12 @@ export function createCheckTierTool(ctx: ContextManager, getState: () => CheckTi
28
28
  cacheInstruction: "Re-check in about a minute.",
29
29
  }, null, 2),
30
30
  }],
31
+ details: undefined,
31
32
  };
32
33
  }
33
-
34
- const runtime = ctx.getRuntimeState();
35
34
  const cacheUntil = Math.floor(Date.now() / 1000) + 86400;
36
35
 
37
- const isPremium = runtime.tier === "premium" || runtime.tier === "premium+";
36
+ const isPremium = runtime.tier === "premium";
38
37
 
39
38
  const dataPath = isPremium
40
39
  ? "Use node commands for current device readings: location.get, device.battery, health.steps, health.heartrate, health.hrv, health.sleep, health.distance, health.restinghr, health.workouts, health.summary, geofence.list. Use get_context for patterns, trends, history, and broad situational awareness — its device snapshot may not be perfectly recent but is useful for the big picture."
@@ -57,6 +56,7 @@ export function createCheckTierTool(ctx: ContextManager, getState: () => CheckTi
57
56
 
58
57
  return {
59
58
  content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
59
+ details: undefined,
60
60
  };
61
61
  },
62
62
  };
@@ -14,7 +14,7 @@ export function createGetContextTool(ctx: ContextManager, stateDir?: string) {
14
14
  const patterns = await ctx.readPatterns();
15
15
  const dataAge = ctx.getDataAge();
16
16
 
17
- const isPremium = runtime.tier === "premium" || runtime.tier === "premium+";
17
+ const isPremium = runtime.tier === "premium";
18
18
 
19
19
  const result: Record<string, unknown> = {
20
20
  tierHint: {
@@ -53,6 +53,7 @@ export function createGetContextTool(ctx: ContextManager, stateDir?: string) {
53
53
 
54
54
  return {
55
55
  content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
56
+ details: undefined,
56
57
  };
57
58
  },
58
59
  };
package/src/types.ts CHANGED
@@ -157,8 +157,7 @@ export interface DeviceConfig {
157
157
  proactiveEnabled?: boolean;
158
158
  }
159
159
 
160
- // Runtime state (not persisted)
161
160
  export interface RuntimeState {
162
- tier: "free" | "premium" | "premium+";
161
+ tier: "free" | "premium" | null;
163
162
  smartMode: boolean;
164
163
  }