@better_openclaw/betterclaw 3.0.2 → 3.0.4

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.2",
3
+ "version": "3.0.4",
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/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
@@ -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
  }
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
  };
@@ -1,12 +1,39 @@
1
1
  import type { ContextManager } from "../context.js";
2
2
  import { loadTriageProfile } from "../learner.js";
3
3
 
4
+ const STALE_THRESHOLD_S = 600; // 10 minutes
5
+
6
+ /** Format seconds into human-readable age string */
7
+ function formatAge(seconds: number): string {
8
+ if (seconds < 60) return `${Math.round(seconds)}s ago`;
9
+ if (seconds < 3600) return `${Math.round(seconds / 60)}m ago`;
10
+ if (seconds < 86400) return `${Math.round(seconds / 3600)}h ago`;
11
+ return `${Math.round(seconds / 86400)}d ago`;
12
+ }
13
+
14
+ /**
15
+ * On premium, stale device data (>10 min) is replaced with a pointer
16
+ * to the fresh node command. The agent can't shortcut to stale values.
17
+ */
18
+ function deviceFieldOrPointer(
19
+ data: Record<string, unknown> | null,
20
+ ageSeconds: number | null,
21
+ freshCommand: string,
22
+ isPremium: boolean,
23
+ ): Record<string, unknown> | null {
24
+ if (!data) return null;
25
+ if (isPremium && ageSeconds != null && ageSeconds > STALE_THRESHOLD_S) {
26
+ return { stale: true, ageHuman: formatAge(ageSeconds), freshCommand };
27
+ }
28
+ return { ...data, dataAgeSeconds: ageSeconds };
29
+ }
30
+
4
31
  export function createGetContextTool(ctx: ContextManager, stateDir?: string) {
5
32
  return {
6
33
  name: "get_context",
7
34
  label: "Get Device Context",
8
35
  description:
9
- "Get BetterClaw context — patterns, trends, activity zone, event history, and cached device snapshots with staleness indicators. On premium, node commands return fresher data for current readings. On free, this includes the latest device snapshot.",
36
+ "Get BetterClaw context — patterns, trends, activity zone, and event history. On premium, stale device readings (>10 min) are hidden use node commands (location.get, device.battery, health.*) for current data. On free, this includes the full device snapshot.",
10
37
  parameters: {},
11
38
  async execute(_id: string, _params: Record<string, unknown>) {
12
39
  const state = ctx.get();
@@ -20,22 +47,31 @@ export function createGetContextTool(ctx: ContextManager, stateDir?: string) {
20
47
  tierHint: {
21
48
  tier: runtime.tier,
22
49
  note: isPremium
23
- ? "Node commands available for fresh readings (location.get, device.battery, health.*)"
50
+ ? "Node commands available for fresh readings (location.get, device.battery, health.*). Stale device data is hidden — call the node command instead."
24
51
  : "This is the only data source on free tier — check dataAgeSeconds for freshness",
25
52
  },
26
53
  smartMode: runtime.smartMode,
27
54
  };
28
55
 
29
56
  result.device = {
30
- battery: state.device.battery
31
- ? { ...state.device.battery, updatedAt: ctx.getTimestamp("battery"), dataAgeSeconds: dataAge.battery }
32
- : null,
33
- location: state.device.location
34
- ? { ...state.device.location, updatedAt: ctx.getTimestamp("location"), dataAgeSeconds: dataAge.location }
35
- : null,
36
- health: state.device.health
37
- ? { ...state.device.health, updatedAt: ctx.getTimestamp("health"), dataAgeSeconds: dataAge.health }
38
- : null,
57
+ battery: deviceFieldOrPointer(
58
+ state.device.battery as unknown as Record<string, unknown>,
59
+ dataAge.battery,
60
+ "device.battery",
61
+ isPremium,
62
+ ),
63
+ location: deviceFieldOrPointer(
64
+ state.device.location as unknown as Record<string, unknown>,
65
+ dataAge.location,
66
+ "location.get",
67
+ isPremium,
68
+ ),
69
+ health: deviceFieldOrPointer(
70
+ state.device.health as unknown as Record<string, unknown>,
71
+ dataAge.health,
72
+ "health.summary",
73
+ isPremium,
74
+ ),
39
75
  };
40
76
 
41
77
  result.activity = { ...state.activity, updatedAt: ctx.getTimestamp("activity") };
@@ -53,6 +89,7 @@ export function createGetContextTool(ctx: ContextManager, stateDir?: string) {
53
89
 
54
90
  return {
55
91
  content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
92
+ details: undefined,
56
93
  };
57
94
  },
58
95
  };
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
  }