clawkit-ai 1.0.4 → 1.0.6

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/sync.mjs +134 -10
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawkit-ai",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Transform your OpenClaw agent into a production-ready AI assistant with memory, skills, and a dashboard",
5
5
  "type": "module",
6
6
  "bin": {
package/src/sync.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from 'fs';
2
2
  import { join } from 'path';
3
+ import { execSync } from 'child_process';
3
4
  import { c, log, detectWorkspace } from './utils.mjs';
4
5
 
5
6
  const API_BASE = 'https://clawkit-api.vercel.app';
@@ -20,13 +21,13 @@ const SERVICE_PATTERNS = [
20
21
  { key: 'brave_search', names: ['Brave Search', 'Brave API'], patterns: [/Brave.*(?:Key|API).*?[:`]\s*`?([^\s`]+)/i] },
21
22
  { key: 'telegram', names: ['Telegram'], patterns: [/Telegram.*(?:Token|Bot).*?[:`]\s*`?(\d+:[^\s`]+)/i] },
22
23
  { key: 'discord', names: ['Discord'], patterns: [/Discord.*(?:Token|Bot).*?[:`]\s*`?([^\s`]+)/i] },
23
- { key: 'github', names: ['GitHub'], patterns: [/GitHub.*(?:Token|PAT|User).*?[:`]\s*`?([^\s`]+)/i] },
24
- { key: 'email', names: ['Email', 'IMAP', 'SMTP'], patterns: [/(?:Email|IMAP|SMTP).*?(?:address|account)?.*?[:`]\s*`?([^\s`]*@[^\s`]+)/i, /App Password.*?[:`]\s*`?([a-z ]{16,})/i] },
25
- { key: 'sogni', names: ['Sogni'], patterns: [/SOGNI_USERNAME.*?[="`]\s*`?([^\s`"]+)/i] },
26
- { key: 'upload_post', names: ['Upload-Post', 'upload-post'], patterns: [/upload-post.*?(?:Key|API).*?[:`]\s*`?(ey[^\s`]+)/i, /Authorization.*?Apikey\s+([^\s`]+)/i] },
27
- { key: 'vercel', names: ['Vercel'], patterns: [/Vercel.*?(?:Token).*?[:`]\s*`?(vcp_[^\s`]+)/i] },
28
- { key: 'netlify', names: ['Netlify'], patterns: [/Netlify.*?(?:Site|ID).*?[:`]\s*`?([0-9a-f-]{20,})/i] },
29
- { key: 'maton', names: ['Maton', 'Google Workspace'], patterns: [/MATON_API_KEY.*?[:`]\s*`?([A-Za-z0-9_-]{30,})/i] },
24
+ { key: 'github', names: ['GitHub'], patterns: [/\*\*PAT:\*\*\s*`?(ghp_[^\s`]+)/i, /github.*(?:Token|PAT).*?`?(ghp_[^\s`]+)/i] },
25
+ { key: 'email', names: ['Email', 'IMAP', 'SMTP'], patterns: [/\*\*Email:\*\*\s*`?([^\s`]+@[^\s`]+)/i, /App Password.*?[:`]\s*`?([a-z ]{10,})/i] },
26
+ { key: 'sogni', names: ['Sogni'], patterns: [/SOGNI_USERNAME.*?["'`]\s*([^\s"'`]+)/i, /SOGNI_PASSWORD.*?["'`]\s*([^\s"'`]+)/i] },
27
+ { key: 'upload_post', names: ['Upload-Post', 'upload-post'], patterns: [/\*\*API Key:\*\*\s*`?(ey[^\s`]+)/i, /upload-post.*?Key.*?`?(ey[^\s`]+)/i] },
28
+ { key: 'vercel', names: ['Vercel'], patterns: [/(?:Token|token).*?`?(vcp_[^\s`]+)/i, /--token\s+(vcp_[^\s`]+)/i] },
29
+ { key: 'netlify', names: ['Netlify'], patterns: [/(?:Site ID|site).*?`?([0-9a-f]{8}-[0-9a-f-]{27,})/i] },
30
+ { key: 'maton', names: ['Maton', 'Google Workspace'], patterns: [/MATON_API_KEY.*?`?([A-Za-z0-9_-]{30,})/i] },
30
31
  ];
31
32
 
32
33
  function parseAccountsFromTools(workspace) {
@@ -238,7 +239,75 @@ function gatherState(workspace) {
238
239
  }
239
240
  }
240
241
 
241
- // Recent activity from daily notes
242
+ // --- Cron Jobs (from OpenClaw gateway API) ---
243
+ try {
244
+ // Try the local gateway WebSocket API via HTTP
245
+ const cronOut = execSync('curl -s -m 3 http://127.0.0.1:18789/api/cron/list 2>/dev/null || true', { timeout: 5000, encoding: 'utf8' });
246
+ const cronMatch = cronOut.match(/\{[\s\S]*\}/);
247
+ if (cronMatch) {
248
+ const cronData = JSON.parse(cronMatch[0]);
249
+ const jobs = cronData.jobs || [];
250
+ state.cronJobs = jobs.map(j => ({
251
+ name: j.name || (j.payload?.text || '').slice(0, 40) || 'Unnamed',
252
+ schedule: j.schedule?.kind === 'cron' ? j.schedule.expr :
253
+ j.schedule?.kind === 'every' ? `every ${Math.round((j.schedule.everyMs || 0) / 60000)}m` :
254
+ j.schedule?.kind === 'at' ? `at ${j.schedule.at}` : 'unknown',
255
+ lastRun: j.state?.lastRunAtMs ? new Date(j.state.lastRunAtMs).toISOString() : undefined,
256
+ status: j.state?.lastStatus || (j.enabled ? 'ok' : 'disabled'),
257
+ }));
258
+ }
259
+ } catch {}
260
+
261
+ // Fallback: parse cron info from HEARTBEAT.md
262
+ if (state.cronJobs.length === 0) {
263
+ const hbPath = join(workspace, 'HEARTBEAT.md');
264
+ if (existsSync(hbPath)) {
265
+ const hb = readFileSync(hbPath, 'utf8');
266
+ const tasks = hb.match(/\*\*(?:Task|Check):\*\*\s*(.+)/g) || [];
267
+ for (const t of tasks.slice(0, 8)) {
268
+ state.cronJobs.push({
269
+ name: t.replace(/\*\*(?:Task|Check):\*\*\s*/, '').slice(0, 50),
270
+ schedule: 'heartbeat',
271
+ status: 'ok',
272
+ });
273
+ }
274
+ }
275
+ }
276
+
277
+ // --- Recent Activity (real sources: git log, file changes, daily notes) ---
278
+
279
+ // Git log (last 10 commits from any repo in Desktop)
280
+ try {
281
+ const gitDirs = ['clawkit', 'clawkit-api', 'clawkit-package', 'dashboard'].map(d => join('/home', 'semo', 'Desktop', d));
282
+ for (const dir of gitDirs) {
283
+ if (!existsSync(join(dir, '.git'))) continue;
284
+ try {
285
+ const gitLog = execSync(`cd "${dir}" && git log --oneline --format="%ai|%s" -5 2>/dev/null`, { timeout: 3000, encoding: 'utf8' });
286
+ const repoName = dir.split('/').pop();
287
+ for (const line of gitLog.trim().split('\n').filter(Boolean)) {
288
+ const [date, ...msgParts] = line.split('|');
289
+ state.recentActivity.push({
290
+ timestamp: new Date(date.trim()).toISOString(),
291
+ message: `[${repoName}] ${msgParts.join('|').slice(0, 100)}`,
292
+ });
293
+ }
294
+ } catch {}
295
+ }
296
+ } catch {}
297
+
298
+ // Recent workspace file changes (last 24h)
299
+ try {
300
+ const recentFiles = execSync(`find "${workspace}" -name "*.md" -mmin -1440 -not -path "*/node_modules/*" -not -path "*/.git/*" -printf "%T@|%f\n" 2>/dev/null | sort -rn | head -5`, { timeout: 3000, encoding: 'utf8' });
301
+ for (const line of recentFiles.trim().split('\n').filter(Boolean)) {
302
+ const [ts, file] = line.split('|');
303
+ state.recentActivity.push({
304
+ timestamp: new Date(parseFloat(ts) * 1000).toISOString(),
305
+ message: `Updated ${file}`,
306
+ });
307
+ }
308
+ } catch {}
309
+
310
+ // Daily notes entries
242
311
  const today = new Date().toISOString().slice(0, 10);
243
312
  const yesterday = new Date(Date.now() - 86400000).toISOString().slice(0, 10);
244
313
  for (const day of [today, yesterday]) {
@@ -248,7 +317,7 @@ function gatherState(workspace) {
248
317
  if (existsSync(f)) {
249
318
  const lines = readFileSync(f, 'utf8').split('\n')
250
319
  .filter(l => l.startsWith('- ') || l.startsWith('* '))
251
- .slice(0, 5);
320
+ .slice(0, 3);
252
321
  for (const line of lines) {
253
322
  state.recentActivity.push({
254
323
  timestamp: `${day}T12:00:00Z`,
@@ -258,7 +327,62 @@ function gatherState(workspace) {
258
327
  }
259
328
  }
260
329
  }
261
- state.recentActivity = state.recentActivity.slice(0, 15);
330
+
331
+ // Sort by timestamp descending, dedupe, limit
332
+ state.recentActivity.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
333
+ state.recentActivity = state.recentActivity.slice(0, 20);
334
+
335
+ // --- Token Usage & Cost ---
336
+ state.usage = { inputTokens: 0, outputTokens: 0, contextUsed: 0, contextMax: 0, costEstimate: '', model: '', compactions: 0 };
337
+
338
+ // Try to read from OpenClaw gateway config for model
339
+ try {
340
+ const gwConfig = join(process.env.HOME || '', '.openclaw', 'config.yaml');
341
+ if (existsSync(gwConfig)) {
342
+ const cfg = readFileSync(gwConfig, 'utf8');
343
+ const modelMatch = cfg.match(/model:\s*["']?([^\s"']+)/);
344
+ if (modelMatch) state.usage.model = modelMatch[1];
345
+ }
346
+ } catch {}
347
+
348
+ // Try to get session stats from gateway HTTP API
349
+ try {
350
+ const statusOut = execSync('curl -s -m 3 http://127.0.0.1:18789/api/session/status 2>/dev/null || true', { timeout: 5000, encoding: 'utf8' });
351
+ const statusMatch = statusOut.match(/\{[\s\S]*\}/);
352
+ if (statusMatch) {
353
+ const sd = JSON.parse(statusMatch[0]);
354
+ if (sd.inputTokens || sd.tokensIn) state.usage.inputTokens = sd.inputTokens || sd.tokensIn || 0;
355
+ if (sd.outputTokens || sd.tokensOut) state.usage.outputTokens = sd.outputTokens || sd.tokensOut || 0;
356
+ if (sd.contextUsed) state.usage.contextUsed = sd.contextUsed;
357
+ if (sd.contextMax) state.usage.contextMax = sd.contextMax;
358
+ if (sd.compactions) state.usage.compactions = sd.compactions;
359
+ if (sd.model) state.usage.model = sd.model;
360
+ }
361
+ } catch {}
362
+
363
+ // If we couldn't get from CLI, estimate from daily note sizes
364
+ if (!state.usage.model) {
365
+ state.usage.model = state.preferences.defaultModel || 'unknown';
366
+ }
367
+
368
+ // Cost estimation based on model
369
+ const MODEL_COSTS = {
370
+ 'claude-opus-4-6': { input: 15, output: 75 }, // per 1M tokens
371
+ 'claude-opus-4': { input: 15, output: 75 },
372
+ 'claude-sonnet-4-5': { input: 3, output: 15 },
373
+ 'claude-sonnet-4': { input: 3, output: 15 },
374
+ 'claude-haiku-3-5': { input: 0.8, output: 4 },
375
+ 'gpt-4o': { input: 2.5, output: 10 },
376
+ 'gpt-4o-mini': { input: 0.15, output: 0.6 },
377
+ 'gemini-2.0-flash': { input: 0.1, output: 0.4 },
378
+ };
379
+ const modelKey = Object.keys(MODEL_COSTS).find(k => state.usage.model?.includes(k));
380
+ const costs = MODEL_COSTS[modelKey] || { input: 3, output: 15 };
381
+ const inCost = (state.usage.inputTokens / 1_000_000) * costs.input;
382
+ const outCost = (state.usage.outputTokens / 1_000_000) * costs.output;
383
+ state.usage.costEstimate = `$${(inCost + outCost).toFixed(4)}`;
384
+ state.usage.costPerMInput = costs.input;
385
+ state.usage.costPerMOutput = costs.output;
262
386
 
263
387
  return state;
264
388
  }