runwork 0.17.1 → 0.18.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/LICENSE CHANGED
@@ -1,11 +1,11 @@
1
- Copyright (c) 2025-2026 Runwork All rights reserved.
1
+ Copyright (c) 2025-2026 Runwork, Inc. All rights reserved.
2
2
 
3
3
  This software and associated documentation files (the "Software") are the
4
- proprietary and confidential property of Runwork
4
+ proprietary and confidential property of Runwork, Inc.
5
5
 
6
6
  No part of the Software may be copied, modified, distributed, sublicensed,
7
7
  sold, or otherwise made available to any third party without the prior
8
- written consent of Runwork
8
+ written consent of Runwork, Inc.
9
9
 
10
10
  You may use the Software solely as a tool to interact with the Runwork
11
11
  platform in accordance with the Runwork Terms of Service.
@@ -44,6 +44,15 @@ export interface JobResult {
44
44
  /** Additional result data (varies by job) */
45
45
  [key: string]: unknown;
46
46
  }
47
+ /**
48
+ * Wire-safe schedule definition: what crosses the RPC boundary into
49
+ * SchedulerDO. Deliberately excludes the handler function; over DO RPC a
50
+ * function arrives as a callback stub, and invoking a stubbed handler
51
+ * serializes JobContext back to the calling isolate, where env bindings (R2
52
+ * buckets, DO namespaces) cannot be serialized (AGENTCLI-6). Handlers are
53
+ * bound inside the DO from its own isolate's module registry.
54
+ */
55
+ export type ScheduledJobDefinition = Omit<ScheduledJob, 'handler'> & Partial<Pick<ScheduledJob, 'handler'>>;
47
56
  export interface JobLogger {
48
57
  info(message: string, data?: Record<string, unknown>): void;
49
58
  warn(message: string, data?: Record<string, unknown>): void;
@@ -134,7 +143,7 @@ export declare class SchedulerDO extends DurableObject<Env> {
134
143
  * Initialize the job scheduler with job definitions.
135
144
  * Can be called again after HMR to sync new/changed job definitions.
136
145
  */
137
- initializeJobScheduler(jobs: ScheduledJob[]): Promise<void>;
146
+ initializeJobScheduler(jobs: ScheduledJobDefinition[]): Promise<void>;
138
147
  /**
139
148
  * Handle alarm - execute due jobs
140
149
  */
package/dist/index.js CHANGED
@@ -5811,6 +5811,15 @@ export interface JobResult {
5811
5811
  /** Additional result data (varies by job) */
5812
5812
  [key: string]: unknown;
5813
5813
  }
5814
+ /**
5815
+ * Wire-safe schedule definition: what crosses the RPC boundary into
5816
+ * SchedulerDO. Deliberately excludes the handler function; over DO RPC a
5817
+ * function arrives as a callback stub, and invoking a stubbed handler
5818
+ * serializes JobContext back to the calling isolate, where env bindings (R2
5819
+ * buckets, DO namespaces) cannot be serialized (AGENTCLI-6). Handlers are
5820
+ * bound inside the DO from its own isolate's module registry.
5821
+ */
5822
+ export type ScheduledJobDefinition = Omit<ScheduledJob, 'handler'> & Partial<Pick<ScheduledJob, 'handler'>>;
5814
5823
  export interface JobLogger {
5815
5824
  info(message: string, data?: Record<string, unknown>): void;
5816
5825
  warn(message: string, data?: Record<string, unknown>): void;
@@ -5901,7 +5910,7 @@ export declare class SchedulerDO extends DurableObject<Env> {
5901
5910
  * Initialize the job scheduler with job definitions.
5902
5911
  * Can be called again after HMR to sync new/changed job definitions.
5903
5912
  */
5904
- initializeJobScheduler(jobs: ScheduledJob[]): Promise<void>;
5913
+ initializeJobScheduler(jobs: ScheduledJobDefinition[]): Promise<void>;
5905
5914
  /**
5906
5915
  * Handle alarm - execute due jobs
5907
5916
  */
@@ -7561,7 +7570,7 @@ function createKeyboardListener() {
7561
7570
  }
7562
7571
 
7563
7572
  // src/generated/version.ts
7564
- var VERSION = "0.17.1";
7573
+ var VERSION = "0.18.0";
7565
7574
 
7566
7575
  // src/commands/dev.ts
7567
7576
  var exports_dev = {};
@@ -8921,6 +8930,21 @@ var deployCommand = new Command5("deploy").description("Deploy the current app t
8921
8930
  }
8922
8931
  const deployedSha = getHeadSha(cwd);
8923
8932
  const finishedAt = new Date().toISOString();
8933
+ const shaVerdict = assessBuiltSha(deployedSha, result.builtFromSha);
8934
+ if (shaVerdict === "mismatch") {
8935
+ const detail = `The platform built this deploy from ${result.builtFromSha.slice(0, 7)} but your pushed HEAD is ${deployedSha.slice(0, 7)}. Production is NOT running your latest code.`;
8936
+ writeDeployStatus(cwd, { state: "failed", startedAt: childStartedAt, finishedAt, error: detail });
8937
+ deployFinalized = true;
8938
+ if (useJson) {
8939
+ jsonOut(buildErrorResponse("deploy", "Deployed artifact does not match pushed commit", detail, [
8940
+ "Run `runwork dev --restart --detach` to resync the build sandbox, then retry `runwork deploy`",
8941
+ "Verify with `runwork logs --production --once` which version is actually serving"
8942
+ ]));
8943
+ process.exit(1);
8944
+ }
8945
+ console.error(`Deploy verification failed: ${detail}`);
8946
+ process.exit(1);
8947
+ }
8924
8948
  if (deployedSha) {
8925
8949
  writeDeployState(cwd, { sha: deployedSha, deployedAt: finishedAt, url: deploymentUrl });
8926
8950
  }
@@ -8937,7 +8961,9 @@ var deployCommand = new Command5("deploy").description("Deploy the current app t
8937
8961
  deployed,
8938
8962
  url: deploymentUrl,
8939
8963
  appName: config.appName,
8940
- deployedSha
8964
+ deployedSha,
8965
+ builtFromSha: result.builtFromSha,
8966
+ shaVerified: shaVerdict === "match"
8941
8967
  },
8942
8968
  guide: buildDeployGuide(),
8943
8969
  ...deployGuardWarning ? { warning: deployGuardWarning } : {}
@@ -8947,8 +8973,13 @@ var deployCommand = new Command5("deploy").description("Deploy the current app t
8947
8973
  }
8948
8974
  console.log(`Deployed: ${deploymentUrl}`);
8949
8975
  if (deployedSha)
8950
- console.log(dim(`Commit: ${deployedSha.slice(0, 7)}`));
8976
+ console.log(dim(`Commit: ${deployedSha.slice(0, 7)}${shaVerdict === "match" ? " (verified)" : shaVerdict === "unverified" ? " (provenance unverified by server)" : ""}`));
8951
8977
  });
8978
+ function assessBuiltSha(localSha, builtFromSha) {
8979
+ if (!localSha || !builtFromSha)
8980
+ return "unverified";
8981
+ return builtFromSha === localSha ? "match" : "mismatch";
8982
+ }
8952
8983
  function printDeployStatus(cwd, useJson) {
8953
8984
  const status = readDeployStatus(cwd);
8954
8985
  const summary = getDeploySummary(cwd);
@@ -9950,7 +9981,7 @@ Used by your team (${teamOnly.length} more):
9950
9981
  console.log(` ${t.canonicalId} - Used by ${display}`);
9951
9982
  }
9952
9983
  console.log(`
9953
- Connect at https://runwork.ai/workspace-settings?section=integrations`);
9984
+ Connect at https://runwork.ai/integrations`);
9954
9985
  }
9955
9986
  } catch (err) {
9956
9987
  console.error("Failed to list integrations:", err instanceof Error ? err.message : err);
@@ -11415,6 +11446,22 @@ function formatCombinedDigest(sessions, opts = { days: 7 }) {
11415
11446
  `);
11416
11447
  }
11417
11448
 
11449
+ // src/agents/utils/skill-name.ts
11450
+ function canonicalSkillName(raw) {
11451
+ const afterPrefix = raw.includes(":") ? raw.slice(raw.lastIndexOf(":") + 1) : raw;
11452
+ return afterPrefix.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
11453
+ }
11454
+ function skillNameFromPath(path2) {
11455
+ const trimmed = path2.trim().replace(/\/SKILL\.md$/i, "");
11456
+ if (!trimmed.includes("/") || /[<>]/.test(trimmed))
11457
+ return null;
11458
+ const segment = trimmed.replace(/\/+$/, "").split("/").pop();
11459
+ if (!segment)
11460
+ return null;
11461
+ const canonical = canonicalSkillName(segment);
11462
+ return canonical || null;
11463
+ }
11464
+
11418
11465
  // src/agents/utils/json-config.ts
11419
11466
  import { readFileSync as readFileSync22, writeFileSync as writeFileSync14, mkdirSync as mkdirSync13, existsSync as existsSync25 } from "fs";
11420
11467
  import { dirname as dirname5 } from "path";
@@ -12138,6 +12185,7 @@ ${instructions}`;
12138
12185
  let outputTokens = 0;
12139
12186
  let cacheReadTokens = 0;
12140
12187
  let latestActivityMs = 0;
12188
+ const activeDays = new Set;
12141
12189
  const seenEntries = new Set;
12142
12190
  let cwdEntries;
12143
12191
  try {
@@ -12191,6 +12239,7 @@ ${instructions}`;
12191
12239
  sessionHadActivity = true;
12192
12240
  if (ts > latestActivityMs)
12193
12241
  latestActivityMs = ts;
12242
+ activeDays.add(new Date(ts).toISOString().slice(0, 10));
12194
12243
  const type = entry.type;
12195
12244
  if (type === "user" || type === "assistant") {
12196
12245
  const msg = entry.message;
@@ -12243,6 +12292,7 @@ ${instructions}`;
12243
12292
  aiLinesAdded: 0,
12244
12293
  aiLinesRemoved: 0,
12245
12294
  lastActiveAt: latestActivityMs > 0 ? new Date(latestActivityMs).toISOString() : null,
12295
+ activeDays: [...activeDays].sort(),
12246
12296
  extra: { inputTokens, outputTokens, cacheReadTokens }
12247
12297
  };
12248
12298
  } catch {
@@ -12347,7 +12397,7 @@ ${instructions}`;
12347
12397
  } catch {
12348
12398
  continue;
12349
12399
  }
12350
- let pendingCommand = null;
12400
+ const pendingToolUses = [];
12351
12401
  for (const line of content.split(`
12352
12402
  `)) {
12353
12403
  if (!line)
@@ -12365,39 +12415,48 @@ ${instructions}`;
12365
12415
  if (!Number.isFinite(ts) || ts <= sinceMs)
12366
12416
  continue;
12367
12417
  const type = entry.type;
12368
- if (type === "assistant") {
12369
- const msg = entry.message;
12370
- if (msg && typeof msg === "object") {
12371
- const blocks = Array.isArray(msg.content) ? msg.content : [];
12372
- for (const block of blocks) {
12373
- if (block && block.type === "tool_use" && block.name === "Skill") {
12374
- const input = block.input;
12375
- if (input && typeof input.skill === "string") {
12376
- recordSkill(input.skill, ts);
12377
- }
12418
+ const msg = entry.message;
12419
+ if (type === "assistant" && msg && typeof msg === "object") {
12420
+ const blocks = Array.isArray(msg.content) ? msg.content : [];
12421
+ for (const block of blocks) {
12422
+ if (block && block.type === "tool_use" && block.name === "Skill") {
12423
+ const input = block.input;
12424
+ if (input && typeof input.skill === "string") {
12425
+ const name = canonicalSkillName(input.skill);
12426
+ if (name)
12427
+ pendingToolUses.push({ name, ts });
12378
12428
  }
12379
12429
  }
12380
12430
  }
12381
12431
  }
12382
- if (type === "user") {
12383
- const msg = entry.message;
12384
- const msgContent = msg?.content;
12385
- const text2 = typeof msgContent === "string" ? msgContent : "";
12386
- if (pendingCommand && text2.startsWith("Base directory for this skill:")) {
12387
- recordSkill(pendingCommand.name, pendingCommand.ts);
12388
- pendingCommand = null;
12389
- continue;
12390
- }
12391
- if (pendingCommand && type === "user") {
12392
- pendingCommand = null;
12432
+ if (type === "user" && msg && typeof msg === "object") {
12433
+ const texts = [];
12434
+ if (typeof msg.content === "string") {
12435
+ texts.push(msg.content);
12436
+ } else if (Array.isArray(msg.content)) {
12437
+ for (const block of msg.content) {
12438
+ if (block && typeof block === "object" && block.type === "text" && typeof block.text === "string") {
12439
+ texts.push(block.text);
12440
+ }
12441
+ }
12393
12442
  }
12394
- const match = text2.match(/<command-name>\/?([^<]+)<\/command-name>/);
12395
- if (match) {
12396
- const cmdName = match[1].replace(/^\//, "");
12397
- pendingCommand = { name: cmdName, ts };
12443
+ for (const text2 of texts) {
12444
+ const m = text2.match(/^Base directory for this skill: (.+)$/m);
12445
+ if (!m)
12446
+ continue;
12447
+ const name = skillNameFromPath(m[1]);
12448
+ if (!name)
12449
+ continue;
12450
+ const pendingIdx = pendingToolUses.findIndex((p) => p.name === name);
12451
+ if (pendingIdx >= 0)
12452
+ pendingToolUses.splice(pendingIdx, 1);
12453
+ recordSkill(name, ts);
12398
12454
  }
12399
12455
  }
12400
12456
  }
12457
+ for (const pending of pendingToolUses) {
12458
+ recordSkill(pending.name, pending.ts);
12459
+ }
12401
12460
  }
12402
12461
  }
12403
12462
  if (skillCounts.size === 0)
@@ -12971,11 +13030,14 @@ class ClaudeDesktopAdapter {
12971
13030
  const modelsUsed = new Set;
12972
13031
  let maxMcpTools = 0;
12973
13032
  const agentSessionsDir = join23(claudeAppDir, "local-agent-mode-sessions");
13033
+ const activeDays = new Set;
12974
13034
  if (existsSync29(agentSessionsDir)) {
12975
13035
  this.walkSessionDirs(agentSessionsDir, sinceMs, (session) => {
12976
13036
  const sessionTime = session.lastActivityAt ?? session.createdAt ?? 0;
12977
- if (sessionTime > sinceMs)
13037
+ if (sessionTime > sinceMs) {
12978
13038
  sessionCount++;
13039
+ activeDays.add(new Date(sessionTime).toISOString().slice(0, 10));
13040
+ }
12979
13041
  if (sessionTime > latestActivity)
12980
13042
  latestActivity = sessionTime;
12981
13043
  if (session.model)
@@ -12991,8 +13053,10 @@ class ClaudeDesktopAdapter {
12991
13053
  if (existsSync29(codeSessionsDir)) {
12992
13054
  this.walkSessionDirs(codeSessionsDir, sinceMs, (session) => {
12993
13055
  const sessionTime = session.lastActivityAt ?? session.createdAt ?? 0;
12994
- if (sessionTime > sinceMs)
13056
+ if (sessionTime > sinceMs) {
12995
13057
  sessionCount++;
13058
+ activeDays.add(new Date(sessionTime).toISOString().slice(0, 10));
13059
+ }
12996
13060
  if (sessionTime > latestActivity)
12997
13061
  latestActivity = sessionTime;
12998
13062
  if (session.model)
@@ -13025,6 +13089,7 @@ class ClaudeDesktopAdapter {
13025
13089
  aiLinesAdded: 0,
13026
13090
  aiLinesRemoved: 0,
13027
13091
  lastActiveAt: latestActivity > 0 ? new Date(latestActivity).toISOString() : null,
13092
+ activeDays: activeDays.size > 0 ? [...activeDays].sort() : undefined,
13028
13093
  modelsUsed: Array.from(modelsUsed),
13029
13094
  mcpToolCount: maxMcpTools > 0 ? maxMcpTools : undefined,
13030
13095
  extra: scheduledTaskRuns > 0 ? { scheduledTaskRuns } : undefined
@@ -13033,46 +13098,6 @@ class ClaudeDesktopAdapter {
13033
13098
  return null;
13034
13099
  }
13035
13100
  }
13036
- async readSessionDigests(sinceISO) {
13037
- try {
13038
- const os2 = platform3();
13039
- const claudeAppDir = os2 === "darwin" ? join23(homedir6(), "Library", "Application Support", "Claude") : os2 === "win32" ? join23(process.env.APPDATA || join23(homedir6(), "AppData", "Roaming"), "Claude") : join23(homedir6(), ".config", "Claude");
13040
- const sessionsDir = join23(claudeAppDir, "claude-code-sessions");
13041
- if (!existsSync29(sessionsDir))
13042
- return null;
13043
- const sinceMs = sinceISO ? new Date(sinceISO).getTime() : 0;
13044
- let files;
13045
- try {
13046
- files = readdirSync6(sessionsDir).filter((f) => f.endsWith(".jsonl"));
13047
- } catch {
13048
- return null;
13049
- }
13050
- const digests = [];
13051
- for (const file of files) {
13052
- const filePath = join23(sessionsDir, file);
13053
- let stat;
13054
- try {
13055
- stat = statSync4(filePath);
13056
- } catch {
13057
- continue;
13058
- }
13059
- if (stat.mtimeMs <= sinceMs)
13060
- continue;
13061
- let content;
13062
- try {
13063
- content = readFileSync25(filePath, "utf-8");
13064
- } catch {
13065
- continue;
13066
- }
13067
- const digest = extractClaudeJsonlSession(content, this.slug, file.replace(/\.jsonl$/, ""));
13068
- if (digest)
13069
- digests.push(digest);
13070
- }
13071
- return digests;
13072
- } catch {
13073
- return null;
13074
- }
13075
- }
13076
13101
  async readVersion() {
13077
13102
  try {
13078
13103
  const os2 = platform3();
@@ -13093,53 +13118,6 @@ class ClaudeDesktopAdapter {
13093
13118
  return null;
13094
13119
  }
13095
13120
  }
13096
- async readSkillUsage(lastSyncAt) {
13097
- try {
13098
- const baseDir = getCoworkBaseDir();
13099
- if (!existsSync29(baseDir))
13100
- return null;
13101
- const sinceMs = lastSyncAt ? new Date(lastSyncAt).getTime() : 0;
13102
- const skillCounts = new Map;
13103
- const agentSessionsDir = baseDir;
13104
- this.walkSessionDirs(agentSessionsDir, sinceMs, (session) => {
13105
- const sessionTime = session.lastActivityAt ?? session.createdAt ?? 0;
13106
- if (sessionTime <= sinceMs)
13107
- return;
13108
- const mcpTools = session.enabledMcpTools;
13109
- if (!mcpTools || typeof mcpTools !== "object")
13110
- return;
13111
- for (const toolName of Object.keys(mcpTools)) {
13112
- const skillMatch = toolName.match(/skill_([A-Za-z0-9_]+)/);
13113
- if (!skillMatch)
13114
- continue;
13115
- const rawName = skillMatch[1].replace(/_/g, " ").trim();
13116
- if (!rawName)
13117
- continue;
13118
- const existing = skillCounts.get(rawName);
13119
- if (existing) {
13120
- existing.count++;
13121
- if (sessionTime > existing.lastTs)
13122
- existing.lastTs = sessionTime;
13123
- } else {
13124
- skillCounts.set(rawName, { count: 1, lastTs: sessionTime });
13125
- }
13126
- }
13127
- });
13128
- if (skillCounts.size === 0)
13129
- return null;
13130
- const results = [];
13131
- for (const [skillName, { count, lastTs }] of skillCounts) {
13132
- results.push({
13133
- skillName,
13134
- count,
13135
- lastUsedAt: new Date(lastTs).toISOString()
13136
- });
13137
- }
13138
- return results;
13139
- } catch {
13140
- return null;
13141
- }
13142
- }
13143
13121
  walkSessionDirs(baseDir, _sinceMs, onSession) {
13144
13122
  try {
13145
13123
  for (const orgDir of readdirSync6(baseDir)) {
@@ -13420,14 +13398,18 @@ ${instructions}`;
13420
13398
  try {
13421
13399
  const sinceMs = lastSyncAt ? new Date(lastSyncAt).getTime() : 0;
13422
13400
  const globalDbPath = this.globalStorageDbPath();
13423
- let sessionCount = 0;
13401
+ let newComposersWithoutId = 0;
13402
+ const activeComposerIds = new Set;
13403
+ const activeDays = new Set;
13404
+ let messageCount = 0;
13405
+ let toolCallCount = 0;
13424
13406
  let totalLinesAdded = 0;
13425
13407
  let totalLinesRemoved = 0;
13426
13408
  let latestActivity = 0;
13427
13409
  let agenticSessions = 0;
13428
13410
  let chatSessions = 0;
13429
13411
  if (existsSync30(globalDbPath)) {
13430
- const composerResult = queryReadonlySqlite(globalDbPath, `SELECT value FROM cursorDiskKV WHERE key LIKE 'composerData-%'`);
13412
+ const composerResult = queryReadonlySqlite(globalDbPath, `SELECT value FROM cursorDiskKV WHERE key LIKE 'composerData:%'`);
13431
13413
  if (composerResult) {
13432
13414
  for (const line of composerResult.split(`
13433
13415
  `)) {
@@ -13437,7 +13419,11 @@ ${instructions}`;
13437
13419
  const data = JSON.parse(line);
13438
13420
  const createdAt = data.createdAt ?? 0;
13439
13421
  if (createdAt > sinceMs) {
13440
- sessionCount++;
13422
+ if (typeof data.composerId === "string")
13423
+ activeComposerIds.add(data.composerId);
13424
+ else
13425
+ newComposersWithoutId++;
13426
+ activeDays.add(new Date(createdAt).toISOString().slice(0, 10));
13441
13427
  totalLinesAdded += data.totalLinesAdded ?? 0;
13442
13428
  totalLinesRemoved += data.totalLinesRemoved ?? 0;
13443
13429
  if (data.unifiedMode === "agent" || data.isAgentic)
@@ -13452,7 +13438,34 @@ ${instructions}`;
13452
13438
  }
13453
13439
  }
13454
13440
  }
13441
+ const bubbleResult = queryReadonlySqlite(globalDbPath, `SELECT json_object('k', key, 'type', json_extract(value,'$.type'), 'ts', json_extract(value,'$.createdAt'), 'tool', json_extract(value,'$.toolFormerData.name')) FROM cursorDiskKV WHERE key LIKE 'bubbleId:%'`);
13442
+ if (bubbleResult) {
13443
+ for (const line of bubbleResult.split(`
13444
+ `)) {
13445
+ if (!line.trim())
13446
+ continue;
13447
+ try {
13448
+ const b = JSON.parse(line);
13449
+ const ts = typeof b.ts === "string" ? Date.parse(b.ts) : NaN;
13450
+ if (!Number.isFinite(ts) || ts <= sinceMs)
13451
+ continue;
13452
+ if (ts > latestActivity)
13453
+ latestActivity = ts;
13454
+ activeDays.add(new Date(ts).toISOString().slice(0, 10));
13455
+ const parts = (b.k ?? "").split(":");
13456
+ if (parts.length >= 3)
13457
+ activeComposerIds.add(parts[1]);
13458
+ if (b.tool)
13459
+ toolCallCount++;
13460
+ else if (b.type === 1 || b.type === 2)
13461
+ messageCount++;
13462
+ } catch {
13463
+ continue;
13464
+ }
13465
+ }
13466
+ }
13455
13467
  }
13468
+ const sessionCount = newComposersWithoutId + activeComposerIds.size;
13456
13469
  const trackingDbPath = join24(homedir7(), ".cursor", "ai-tracking", "ai-code-tracking.db");
13457
13470
  let aiCommitCount = 0;
13458
13471
  let avgAiPercent = 0;
@@ -13465,17 +13478,19 @@ ${instructions}`;
13465
13478
  avgAiPercent = parseFloat(parts[1]) || 0;
13466
13479
  }
13467
13480
  }
13468
- if (sessionCount === 0 && latestActivity <= sinceMs)
13481
+ const hasNewActivity = sessionCount > 0 || messageCount > 0;
13482
+ if (!hasNewActivity && latestActivity <= sinceMs)
13469
13483
  return null;
13470
13484
  return {
13471
- hasNewActivity: sessionCount > 0,
13485
+ hasNewActivity,
13472
13486
  sessionCount,
13473
- messageCount: 0,
13474
- toolCallCount: 0,
13487
+ messageCount,
13488
+ toolCallCount,
13475
13489
  tokensUsed: 0,
13476
13490
  aiLinesAdded: totalLinesAdded,
13477
13491
  aiLinesRemoved: totalLinesRemoved,
13478
13492
  lastActiveAt: latestActivity > 0 ? new Date(latestActivity).toISOString() : null,
13493
+ activeDays: activeDays.size > 0 ? [...activeDays].sort() : undefined,
13479
13494
  extra: {
13480
13495
  agenticSessions,
13481
13496
  chatSessions,
@@ -13710,6 +13725,8 @@ var AGENT_REGISTRY = [
13710
13725
  },
13711
13726
  manualSetup: {
13712
13727
  title: "Install Runwork plugin",
13728
+ action: "Install Runwork plugin",
13729
+ preposition: "for",
13713
13730
  showAfter: "connecting",
13714
13731
  downloadArtifact: {
13715
13732
  label: "Download plugin",
@@ -13717,7 +13734,7 @@ var AGENT_REGISTRY = [
13717
13734
  command: "build-plugin"
13718
13735
  },
13719
13736
  steps: [
13720
- { id: "download", label: "Click Download plugin above to save runwork-plugin.zip" },
13737
+ { id: "download", label: "Download the plugin. It saves runwork-plugin.zip to your Downloads folder.", control: "download-artifact" },
13721
13738
  { id: "open", label: "Open Claude Desktop and sign in if you haven't already" },
13722
13739
  { id: "cowork", label: "Go to the Cowork tab (top of the window)" },
13723
13740
  { id: "customize", label: "Click Customize in the sidebar" },
@@ -13891,6 +13908,8 @@ var AGENT_REGISTRY = [
13891
13908
  },
13892
13909
  manualSetup: {
13893
13910
  title: "Connect Runwork in ChatGPT",
13911
+ action: "Create Runwork connector",
13912
+ preposition: "in",
13894
13913
  showAfter: "connecting",
13895
13914
  copyValue: { label: "Workspace MCP URL", token: "{mcpUrl}" },
13896
13915
  steps: [
@@ -13898,7 +13917,7 @@ var AGENT_REGISTRY = [
13898
13917
  { id: "settings-apps", label: "Open the profile menu (bottom-left), then Settings, then Apps" },
13899
13918
  { id: "dev-mode", label: 'Click "Advanced settings" and turn on "Developer mode" (skip if it is already on)' },
13900
13919
  { id: "create-app", label: 'Go back to Apps and click "Create app"' },
13901
- { id: "fill", label: 'In the New App dialog, set Name to "Runwork", keep Connection on "Server URL", and paste the Workspace MCP URL above into the Server URL field' },
13920
+ { id: "fill", label: 'In the New App dialog, set Name to "Runwork", keep Connection on "Server URL", then paste this Workspace MCP URL into the Server URL field:', control: "copy-value" },
13902
13921
  { id: "oauth", label: 'Leave Authentication set to "OAuth", check "I understand and want to continue", then click Create' },
13903
13922
  { id: "authorize", label: "Complete the OAuth sign-in to authorize Runwork for your account" },
13904
13923
  { id: "confirm", label: 'In a new chat, ask "Do you have access to Runwork tools?" to confirm (or check Settings, then Apps, for Runwork)' }
@@ -13908,6 +13927,8 @@ var AGENT_REGISTRY = [
13908
13927
  key: "team",
13909
13928
  label: "Team / Enterprise",
13910
13929
  title: "Connect the Runwork app in ChatGPT",
13930
+ action: "Connect the Runwork app",
13931
+ preposition: "in",
13911
13932
  steps: [
13912
13933
  { id: "open", label: "Open chatgpt.com and sign in" },
13913
13934
  { id: "settings-apps", label: "Open the profile menu (bottom-left), then Settings, then Apps" },
@@ -13920,12 +13941,14 @@ var AGENT_REGISTRY = [
13920
13941
  key: "personal",
13921
13942
  label: "Personal",
13922
13943
  title: "Create the Runwork connector in ChatGPT",
13944
+ action: "Create Runwork connector",
13945
+ preposition: "in",
13923
13946
  steps: [
13924
13947
  { id: "open", label: "Open chatgpt.com and sign in" },
13925
13948
  { id: "settings-apps", label: "Open the profile menu (bottom-left), then Settings, then Apps" },
13926
13949
  { id: "dev-mode", label: 'Click "Advanced settings" and turn on "Developer mode" (skip if it is already on)' },
13927
13950
  { id: "create-app", label: 'Go back to Apps and click "Create app"' },
13928
- { id: "fill", label: 'In the New App dialog, set Name to "Runwork", keep Connection on "Server URL", and paste the Workspace MCP URL above into the Server URL field' },
13951
+ { id: "fill", label: 'In the New App dialog, set Name to "Runwork", keep Connection on "Server URL", then paste this Workspace MCP URL into the Server URL field:', control: "copy-value" },
13929
13952
  { id: "oauth", label: 'Leave Authentication set to "OAuth", check "I understand and want to continue", then click Create' },
13930
13953
  { id: "authorize", label: "Complete the OAuth sign-in to authorize Runwork for your account" },
13931
13954
  { id: "confirm", label: 'In a new chat, ask "Do you have access to Runwork tools?" to confirm (or check Settings, then Apps, for Runwork)' }
@@ -13965,6 +13988,8 @@ var AGENT_REGISTRY = [
13965
13988
  },
13966
13989
  manualSetup: {
13967
13990
  title: "Connect Runwork in ChatGPT",
13991
+ action: "Create Runwork connector",
13992
+ preposition: "in",
13968
13993
  showAfter: "connecting",
13969
13994
  copyValue: { label: "Workspace MCP URL", token: "{mcpUrl}" },
13970
13995
  steps: [
@@ -13973,7 +13998,7 @@ var AGENT_REGISTRY = [
13973
13998
  { id: "settings-apps", label: "Open the profile menu (bottom-left), then Settings, then Apps" },
13974
13999
  { id: "dev-mode", label: 'Click "Advanced settings" and turn on "Developer mode" (skip if it is already on)' },
13975
14000
  { id: "create-app", label: 'Go back to Apps and click "Create app"' },
13976
- { id: "fill", label: 'In the New App dialog, set Name to "Runwork", keep Connection on "Server URL", and paste the Workspace MCP URL above into the Server URL field' },
14001
+ { id: "fill", label: 'In the New App dialog, set Name to "Runwork", keep Connection on "Server URL", then paste this Workspace MCP URL into the Server URL field:', control: "copy-value" },
13977
14002
  { id: "oauth", label: 'Leave Authentication set to "OAuth", check "I understand and want to continue", then click Create' },
13978
14003
  { id: "authorize", label: "Complete the OAuth sign-in to authorize Runwork for your account" },
13979
14004
  { id: "confirm", label: 'Open the ChatGPT desktop app and, in a new chat, ask "Do you have access to Runwork tools?" to confirm' }
@@ -13983,6 +14008,8 @@ var AGENT_REGISTRY = [
13983
14008
  key: "team",
13984
14009
  label: "Team / Enterprise",
13985
14010
  title: "Connect the Runwork app in ChatGPT",
14011
+ action: "Connect the Runwork app",
14012
+ preposition: "in",
13986
14013
  steps: [
13987
14014
  { id: "install", label: "Install the ChatGPT desktop app from chatgpt.com/download if you have not already" },
13988
14015
  { id: "open", label: "Open the ChatGPT desktop app and sign in" },
@@ -13996,13 +14023,15 @@ var AGENT_REGISTRY = [
13996
14023
  key: "personal",
13997
14024
  label: "Personal",
13998
14025
  title: "Create the Runwork connector in ChatGPT",
14026
+ action: "Create Runwork connector",
14027
+ preposition: "in",
13999
14028
  steps: [
14000
14029
  { id: "install", label: "Install the ChatGPT desktop app from chatgpt.com/download if you have not already" },
14001
14030
  { id: "open-web", label: "Connectors are created on chatgpt.com: open it in your browser and sign in" },
14002
14031
  { id: "settings-apps", label: "Open the profile menu (bottom-left), then Settings, then Apps" },
14003
14032
  { id: "dev-mode", label: 'Click "Advanced settings" and turn on "Developer mode" (skip if it is already on)' },
14004
14033
  { id: "create-app", label: 'Go back to Apps and click "Create app"' },
14005
- { id: "fill", label: 'In the New App dialog, set Name to "Runwork", keep Connection on "Server URL", and paste the Workspace MCP URL above into the Server URL field' },
14034
+ { id: "fill", label: 'In the New App dialog, set Name to "Runwork", keep Connection on "Server URL", then paste this Workspace MCP URL into the Server URL field:', control: "copy-value" },
14006
14035
  { id: "oauth", label: 'Leave Authentication set to "OAuth", check "I understand and want to continue", then click Create' },
14007
14036
  { id: "authorize", label: "Complete the OAuth sign-in to authorize Runwork for your account" },
14008
14037
  { id: "confirm", label: 'Open the ChatGPT desktop app and, in a new chat, ask "Do you have access to Runwork tools?" to confirm' }
@@ -14492,50 +14521,162 @@ class CodexAdapter {
14492
14521
  return null;
14493
14522
  const sinceMs = lastSyncAt ? new Date(lastSyncAt).getTime() : 0;
14494
14523
  const sinceSec = Math.floor(sinceMs / 1000);
14524
+ const rollout = this.scanRolloutActivity(sinceMs);
14525
+ let sessionCount = rollout.sessionCount;
14526
+ let tokensUsed = rollout.tokensUsed;
14527
+ let latestMs = rollout.latestMs;
14528
+ let versions = [];
14495
14529
  const dbPath = join28(codexDir, "state_5.sqlite");
14496
- if (!existsSync33(dbPath))
14497
- return null;
14498
- const countResult = queryReadonlySqlite(dbPath, `SELECT count(*) FROM threads WHERE updated_at > ${sinceSec}`);
14499
- const sessionCount = parseInt(countResult) || 0;
14500
- const tokensResult = queryReadonlySqlite(dbPath, `SELECT coalesce(sum(tokens_used), 0) FROM threads WHERE updated_at > ${sinceSec}`);
14501
- const tokensUsed = parseInt(tokensResult) || 0;
14502
- const latestResult = queryReadonlySqlite(dbPath, `SELECT max(updated_at) FROM threads`);
14503
- const latestSec = parseInt(latestResult) || 0;
14504
- const versionsResult = queryReadonlySqlite(dbPath, `SELECT DISTINCT cli_version FROM threads WHERE cli_version IS NOT NULL ORDER BY created_at DESC LIMIT 3`);
14505
- const versions = versionsResult ? versionsResult.split(/[\r\n]+/).filter(Boolean) : [];
14506
- let messageCount = 0;
14507
- const historyPath = join28(codexDir, "history.jsonl");
14508
- if (existsSync33(historyPath)) {
14509
- const content = readFileSync26(historyPath, "utf-8").trim();
14510
- if (content) {
14511
- for (const line of content.split(/[\r\n]+/)) {
14512
- try {
14513
- const entry = JSON.parse(line);
14514
- if (entry.ts && entry.ts > sinceSec)
14515
- messageCount++;
14516
- } catch {
14517
- continue;
14530
+ if (existsSync33(dbPath)) {
14531
+ const countResult = queryReadonlySqlite(dbPath, `SELECT count(*) FROM threads WHERE updated_at > ${sinceSec}`);
14532
+ const dbSessionCount = parseInt(countResult) || 0;
14533
+ const tokensResult = queryReadonlySqlite(dbPath, `SELECT coalesce(sum(tokens_used), 0) FROM threads WHERE updated_at > ${sinceSec}`);
14534
+ const dbTokens = parseInt(tokensResult) || 0;
14535
+ const latestResult = queryReadonlySqlite(dbPath, `SELECT max(updated_at) FROM threads`);
14536
+ const latestSec = parseInt(latestResult) || 0;
14537
+ const versionsResult = queryReadonlySqlite(dbPath, `SELECT DISTINCT cli_version FROM threads WHERE cli_version IS NOT NULL ORDER BY created_at DESC LIMIT 3`);
14538
+ versions = versionsResult ? versionsResult.split(/[\r\n]+/).filter(Boolean) : [];
14539
+ sessionCount = Math.max(dbSessionCount, rollout.sessionCount);
14540
+ if (dbTokens > 0)
14541
+ tokensUsed = dbTokens;
14542
+ if (latestSec * 1000 > latestMs)
14543
+ latestMs = latestSec * 1000;
14544
+ }
14545
+ let messageCount = rollout.messageCount;
14546
+ if (messageCount === 0) {
14547
+ const historyPath = join28(codexDir, "history.jsonl");
14548
+ if (existsSync33(historyPath)) {
14549
+ const content = readFileSync26(historyPath, "utf-8").trim();
14550
+ if (content) {
14551
+ for (const line of content.split(/[\r\n]+/)) {
14552
+ try {
14553
+ const entry = JSON.parse(line);
14554
+ if (entry.ts && entry.ts > sinceSec)
14555
+ messageCount++;
14556
+ } catch {
14557
+ continue;
14558
+ }
14518
14559
  }
14519
14560
  }
14520
14561
  }
14521
14562
  }
14522
- if (sessionCount === 0 && latestSec * 1000 <= sinceMs)
14563
+ const hasNewActivity = sessionCount > 0 || messageCount > 0;
14564
+ if (!hasNewActivity && latestMs <= sinceMs)
14523
14565
  return null;
14524
14566
  return {
14525
- hasNewActivity: sessionCount > 0,
14567
+ hasNewActivity,
14526
14568
  sessionCount,
14527
14569
  messageCount,
14528
- toolCallCount: 0,
14570
+ toolCallCount: rollout.toolCallCount,
14529
14571
  tokensUsed,
14530
14572
  aiLinesAdded: 0,
14531
14573
  aiLinesRemoved: 0,
14532
- lastActiveAt: latestSec > 0 ? new Date(latestSec * 1000).toISOString() : null,
14574
+ lastActiveAt: latestMs > 0 ? new Date(latestMs).toISOString() : null,
14575
+ activeDays: rollout.activeDays.length > 0 ? rollout.activeDays : undefined,
14533
14576
  extra: versions.length > 0 ? { cliVersions: versions } : undefined
14534
14577
  };
14535
14578
  } catch {
14536
14579
  return null;
14537
14580
  }
14538
14581
  }
14582
+ scanRolloutActivity(sinceMs) {
14583
+ const days = new Set;
14584
+ const result = {
14585
+ sessionCount: 0,
14586
+ messageCount: 0,
14587
+ toolCallCount: 0,
14588
+ tokensUsed: 0,
14589
+ latestMs: 0,
14590
+ activeDays: []
14591
+ };
14592
+ const sessionsDir = join28(homedir11(), ".codex", "sessions");
14593
+ if (!existsSync33(sessionsDir))
14594
+ return result;
14595
+ const files = [];
14596
+ const walk = (dir) => {
14597
+ let entries;
14598
+ try {
14599
+ entries = readdirSync9(dir, { withFileTypes: true });
14600
+ } catch {
14601
+ return;
14602
+ }
14603
+ for (const e of entries) {
14604
+ const full = join28(dir, e.name);
14605
+ if (e.isDirectory())
14606
+ walk(full);
14607
+ else if (e.name.startsWith("rollout-") && e.name.endsWith(".jsonl"))
14608
+ files.push(full);
14609
+ }
14610
+ };
14611
+ walk(sessionsDir);
14612
+ for (const file of files) {
14613
+ let stat;
14614
+ try {
14615
+ stat = statSync5(file);
14616
+ } catch {
14617
+ continue;
14618
+ }
14619
+ if (stat.mtimeMs <= sinceMs)
14620
+ continue;
14621
+ let content;
14622
+ try {
14623
+ content = readFileSync26(file, "utf-8");
14624
+ } catch {
14625
+ continue;
14626
+ }
14627
+ let fileHadActivity = false;
14628
+ let fileTokens = 0;
14629
+ for (const line of content.split(`
14630
+ `)) {
14631
+ if (!line.trim())
14632
+ continue;
14633
+ let o;
14634
+ try {
14635
+ o = JSON.parse(line);
14636
+ } catch {
14637
+ continue;
14638
+ }
14639
+ const ts = typeof o.timestamp === "string" ? Date.parse(o.timestamp) : NaN;
14640
+ if (!Number.isFinite(ts) || ts <= sinceMs)
14641
+ continue;
14642
+ const p = o.payload;
14643
+ if (!p || typeof p !== "object")
14644
+ continue;
14645
+ const pt = p.type;
14646
+ if (o.type === "event_msg") {
14647
+ if (pt === "user_message" || pt === "agent_message") {
14648
+ result.messageCount++;
14649
+ fileHadActivity = true;
14650
+ if (ts > result.latestMs)
14651
+ result.latestMs = ts;
14652
+ days.add(new Date(ts).toISOString().slice(0, 10));
14653
+ } else if (pt === "token_count") {
14654
+ const info = p.info;
14655
+ const usage = info?.total_token_usage;
14656
+ if (usage) {
14657
+ const total = (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0);
14658
+ if (total > fileTokens)
14659
+ fileTokens = total;
14660
+ }
14661
+ if (ts > result.latestMs)
14662
+ result.latestMs = ts;
14663
+ }
14664
+ } else if (o.type === "response_item" && (pt === "function_call" || pt === "tool_search_call")) {
14665
+ result.toolCallCount++;
14666
+ fileHadActivity = true;
14667
+ if (ts > result.latestMs)
14668
+ result.latestMs = ts;
14669
+ days.add(new Date(ts).toISOString().slice(0, 10));
14670
+ }
14671
+ }
14672
+ if (fileHadActivity) {
14673
+ result.sessionCount++;
14674
+ result.tokensUsed += fileTokens;
14675
+ }
14676
+ }
14677
+ result.activeDays = [...days].sort();
14678
+ return result;
14679
+ }
14539
14680
  async readSessionDigests(sinceISO) {
14540
14681
  try {
14541
14682
  const sessionsDir = join28(homedir11(), ".codex", "sessions");
@@ -14653,9 +14794,10 @@ class CodexAdapter {
14653
14794
  } catch {
14654
14795
  return;
14655
14796
  }
14797
+ const seenInFile = new Set;
14656
14798
  for (const line of content.split(`
14657
14799
  `)) {
14658
- if (!line)
14800
+ if (!line || !line.includes("SKILL.md"))
14659
14801
  continue;
14660
14802
  let entry;
14661
14803
  try {
@@ -14663,35 +14805,37 @@ class CodexAdapter {
14663
14805
  } catch {
14664
14806
  continue;
14665
14807
  }
14666
- if (entry.type !== "session_meta")
14808
+ if (entry.type !== "response_item")
14667
14809
  continue;
14668
14810
  const payload = entry.payload;
14669
14811
  if (!payload)
14670
14812
  continue;
14813
+ const pt = payload.type;
14814
+ if (pt !== "function_call" && pt !== "custom_tool_call")
14815
+ continue;
14671
14816
  const tsRaw = entry.timestamp;
14672
14817
  const ts = typeof tsRaw === "string" ? Date.parse(tsRaw) : 0;
14673
- if (ts <= sinceMs)
14818
+ if (!Number.isFinite(ts) || ts <= sinceMs)
14674
14819
  continue;
14675
- const instructions = payload.instructions;
14676
- if (typeof instructions !== "string")
14820
+ const args = typeof payload.arguments === "string" ? payload.arguments : JSON.stringify(payload.arguments ?? "");
14821
+ const m = args.match(/([^\s"']*\/skills\/[^\s"']*\/)SKILL\.md/);
14822
+ if (!m)
14677
14823
  continue;
14678
- const skillRegex = /^- ([^:]+):\s+.+\(file:\s+(.+\/SKILL\.md)\)/gm;
14679
- let match;
14680
- while ((match = skillRegex.exec(instructions)) !== null) {
14681
- const skillName = match[1].trim();
14682
- const skillPath = match[2];
14683
- if (skillPath.includes("/.system/"))
14684
- continue;
14685
- const existing = skillCounts.get(skillName);
14686
- if (existing) {
14687
- existing.count++;
14688
- if (ts > existing.lastTs)
14689
- existing.lastTs = ts;
14690
- } else {
14691
- skillCounts.set(skillName, { count: 1, lastTs: ts });
14692
- }
14824
+ const skillDir = m[1];
14825
+ if (skillDir.includes("/plugins/cache/openai-") || skillDir.includes("/.system/"))
14826
+ continue;
14827
+ const skillName = skillNameFromPath(skillDir);
14828
+ if (!skillName || seenInFile.has(skillName))
14829
+ continue;
14830
+ seenInFile.add(skillName);
14831
+ const existing = skillCounts.get(skillName);
14832
+ if (existing) {
14833
+ existing.count++;
14834
+ if (ts > existing.lastTs)
14835
+ existing.lastTs = ts;
14836
+ } else {
14837
+ skillCounts.set(skillName, { count: 1, lastTs: ts });
14693
14838
  }
14694
- break;
14695
14839
  }
14696
14840
  }
14697
14841
  registerDesktopWorkspace(workspacePath, label) {
@@ -14865,7 +15009,7 @@ class ClineAdapter {
14865
15009
  }
14866
15010
 
14867
15011
  // src/agents/gemini.ts
14868
- import { existsSync as existsSync35, mkdirSync as mkdirSync22, readdirSync as readdirSync11, readFileSync as readFileSync28, rmSync as rmSync10, writeFileSync as writeFileSync23 } from "fs";
15012
+ import { existsSync as existsSync35, mkdirSync as mkdirSync22, readdirSync as readdirSync11, readFileSync as readFileSync28, rmSync as rmSync10, statSync as statSync6, writeFileSync as writeFileSync23 } from "fs";
14869
15013
  import { join as join30 } from "path";
14870
15014
  import { homedir as homedir13 } from "os";
14871
15015
  class GeminiAdapter {
@@ -14943,6 +15087,86 @@ class GeminiAdapter {
14943
15087
  mkdirSync22(join30(settingsPath, ".."), { recursive: true });
14944
15088
  writeFileSync23(settingsPath, JSON.stringify(settings, null, 2));
14945
15089
  }
15090
+ async readUsageStats(lastSyncAt) {
15091
+ try {
15092
+ const tmpDir = join30(homedir13(), ".gemini", "tmp");
15093
+ if (!existsSync35(tmpDir))
15094
+ return null;
15095
+ const sinceMs = lastSyncAt ? new Date(lastSyncAt).getTime() : 0;
15096
+ let sessionCount = 0;
15097
+ let messageCount = 0;
15098
+ let latestMs = 0;
15099
+ const activeDays = new Set;
15100
+ let projects;
15101
+ try {
15102
+ projects = readdirSync11(tmpDir, { withFileTypes: true });
15103
+ } catch {
15104
+ return null;
15105
+ }
15106
+ for (const project of projects) {
15107
+ if (!project.isDirectory())
15108
+ continue;
15109
+ const chatsDir = join30(tmpDir, project.name, "chats");
15110
+ let files;
15111
+ try {
15112
+ files = readdirSync11(chatsDir, { withFileTypes: true });
15113
+ } catch {
15114
+ continue;
15115
+ }
15116
+ for (const file of files) {
15117
+ if (!file.name.startsWith("session-") || !file.name.endsWith(".json"))
15118
+ continue;
15119
+ const filePath = join30(chatsDir, file.name);
15120
+ let stat;
15121
+ try {
15122
+ stat = statSync6(filePath);
15123
+ } catch {
15124
+ continue;
15125
+ }
15126
+ if (stat.mtimeMs <= sinceMs)
15127
+ continue;
15128
+ let session;
15129
+ try {
15130
+ session = JSON.parse(readFileSync28(filePath, "utf-8"));
15131
+ } catch {
15132
+ continue;
15133
+ }
15134
+ if (!Array.isArray(session.messages))
15135
+ continue;
15136
+ let sessionHadActivity = false;
15137
+ for (const m of session.messages) {
15138
+ const ts = typeof m.timestamp === "string" ? Date.parse(m.timestamp) : NaN;
15139
+ if (!Number.isFinite(ts) || ts <= sinceMs)
15140
+ continue;
15141
+ if (m.type === "user" || m.type === "gemini") {
15142
+ messageCount++;
15143
+ sessionHadActivity = true;
15144
+ if (ts > latestMs)
15145
+ latestMs = ts;
15146
+ activeDays.add(new Date(ts).toISOString().slice(0, 10));
15147
+ }
15148
+ }
15149
+ if (sessionHadActivity)
15150
+ sessionCount++;
15151
+ }
15152
+ }
15153
+ if (sessionCount === 0)
15154
+ return null;
15155
+ return {
15156
+ hasNewActivity: true,
15157
+ sessionCount,
15158
+ messageCount,
15159
+ toolCallCount: 0,
15160
+ tokensUsed: 0,
15161
+ aiLinesAdded: 0,
15162
+ aiLinesRemoved: 0,
15163
+ lastActiveAt: latestMs > 0 ? new Date(latestMs).toISOString() : null,
15164
+ activeDays: [...activeDays].sort()
15165
+ };
15166
+ } catch {
15167
+ return null;
15168
+ }
15169
+ }
14946
15170
  async cleanup(scope, manifest) {
14947
15171
  if (scope === "user") {
14948
15172
  removeRunworkMcpServers(join30(homedir13(), ".gemini", "settings.json"), "mcpServers");
@@ -17119,6 +17343,12 @@ var RUNWORK_AGENT_DEFAULTS = {
17119
17343
  var AGENT_DEFAULTS_SCHEMA_VERSION = 1;
17120
17344
 
17121
17345
  // src/commands/sync-telemetry.ts
17346
+ var TELEMETRY_BACKFILL_MS = 30 * 24 * 60 * 60 * 1000;
17347
+ function resolveTelemetrySince(state, nowMs = Date.now()) {
17348
+ if (state.lastTelemetryAt)
17349
+ return state.lastTelemetryAt;
17350
+ return new Date(nowMs - TELEMETRY_BACKFILL_MS).toISOString();
17351
+ }
17122
17352
  async function collectTelemetryEvents(params) {
17123
17353
  const now = params.now ?? new Date().toISOString();
17124
17354
  const events = [];
@@ -17129,7 +17359,7 @@ async function collectTelemetryEvents(params) {
17129
17359
  let error;
17130
17360
  if (adapter2.readUsageStats) {
17131
17361
  try {
17132
- stats = await adapter2.readUsageStats(params.state.lastSyncAt ?? null);
17362
+ stats = await adapter2.readUsageStats(params.since);
17133
17363
  if (stats && stats.hasNewActivity) {
17134
17364
  events.push({
17135
17365
  eventType: "local_agent.usage",
@@ -17142,6 +17372,7 @@ async function collectTelemetryEvents(params) {
17142
17372
  aiLinesAdded: stats.aiLinesAdded,
17143
17373
  aiLinesRemoved: stats.aiLinesRemoved,
17144
17374
  lastActiveAt: stats.lastActiveAt,
17375
+ activeDays: stats.activeDays,
17145
17376
  modelsUsed: stats.modelsUsed,
17146
17377
  mcpToolCount: stats.mcpToolCount,
17147
17378
  ...stats.extra
@@ -17156,7 +17387,7 @@ async function collectTelemetryEvents(params) {
17156
17387
  }
17157
17388
  if (adapter2.readSkillUsage) {
17158
17389
  try {
17159
- skills = await adapter2.readSkillUsage(params.state.lastSyncAt ?? null);
17390
+ skills = await adapter2.readSkillUsage(params.since);
17160
17391
  if (skills && skills.length > 0) {
17161
17392
  for (const entry of skills) {
17162
17393
  events.push({
@@ -18481,6 +18712,7 @@ async function syncFromState(state, statePath2, credentials, opts) {
18481
18712
  const telemetry = await collectTelemetryEvents({
18482
18713
  adapters,
18483
18714
  state,
18715
+ since: resolveTelemetrySince(state),
18484
18716
  syncedSkillsCount: remoteSkills.length,
18485
18717
  syncedMcpServersCount: mcpEntries.length,
18486
18718
  teamInstructionsApplied: false,
@@ -18860,21 +19092,25 @@ async function syncFromState(state, statePath2, credentials, opts) {
18860
19092
  state.skillHashes = mergedHashes;
18861
19093
  writeFileSync29(statePath2, JSON.stringify(state, null, 2));
18862
19094
  try {
19095
+ const telemetryNow = new Date().toISOString();
18863
19096
  const telemetry = await collectTelemetryEvents({
18864
19097
  adapters,
18865
19098
  state,
19099
+ since: resolveTelemetrySince(state, Date.parse(telemetryNow)),
18866
19100
  syncedSkillsCount: remoteSkills.length,
18867
19101
  syncedMcpServersCount: mcpEntries.length,
18868
19102
  teamInstructionsApplied,
18869
- agentConfigsApplied
19103
+ agentConfigsApplied,
19104
+ now: telemetryNow
18870
19105
  });
18871
19106
  if (opts.verbose)
18872
19107
  printTelemetryVerbose(telemetry);
18873
19108
  await client.reportTelemetry(state.workspaceId, telemetry.events);
19109
+ state.lastTelemetryAt = telemetryNow;
18874
19110
  if (telemetry.healthReported) {
18875
19111
  state.lastHealthReportAt = new Date().toISOString();
18876
- writeFileSync29(statePath2, JSON.stringify(state, null, 2));
18877
19112
  }
19113
+ writeFileSync29(statePath2, JSON.stringify(state, null, 2));
18878
19114
  } catch {}
18879
19115
  const failedNote = summary.adaptersFailed > 0 ? ` (${summary.adaptersFailed} failed)` : "";
18880
19116
  if (isVerbose()) {
@@ -20261,7 +20497,7 @@ async function applyDoctorFixes(ctx, failingNames) {
20261
20497
  }
20262
20498
 
20263
20499
  // src/agents/runtime-detection.ts
20264
- import { existsSync as existsSync46, readFileSync as readFileSync39, statSync as statSync6, readdirSync as readdirSync13 } from "fs";
20500
+ import { existsSync as existsSync46, readFileSync as readFileSync39, statSync as statSync7, readdirSync as readdirSync13 } from "fs";
20265
20501
  import { homedir as homedir23 } from "os";
20266
20502
  import { join as join42 } from "path";
20267
20503
  var RUNWORK_SESSIONS_DIR = join42(homedir23(), ".runwork", "sessions");
@@ -20372,7 +20608,7 @@ function findCodexRolloutFile(threadId) {
20372
20608
  const full = join42(dir, entry);
20373
20609
  let s;
20374
20610
  try {
20375
- s = statSync6(full);
20611
+ s = statSync7(full);
20376
20612
  } catch {
20377
20613
  continue;
20378
20614
  }
@@ -20409,7 +20645,7 @@ function findNewestClaudeCodeSession() {
20409
20645
  continue;
20410
20646
  const full = join42(projectPath, file);
20411
20647
  try {
20412
- const s = statSync6(full);
20648
+ const s = statSync7(full);
20413
20649
  if (!best || s.mtimeMs > best.mtime) {
20414
20650
  best = {
20415
20651
  sessionId: file.replace(/\.jsonl$/, ""),
@@ -20442,7 +20678,7 @@ function findNewestCodexRollout() {
20442
20678
  const full = join42(dir, entry);
20443
20679
  let s;
20444
20680
  try {
20445
- s = statSync6(full);
20681
+ s = statSync7(full);
20446
20682
  } catch {
20447
20683
  continue;
20448
20684
  }
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "runwork",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "description": "CLI for Runwork: develop, preview, and deploy Runwork apps from your local machine.",
5
5
  "license": "UNLICENSED",
6
- "author": "Runwork <info@runwork.ai> (https://www.runwork.ai)",
6
+ "author": "Runwork, Inc. <info@runwork.ai> (https://www.runwork.ai)",
7
7
  "homepage": "https://www.runwork.ai",
8
8
  "keywords": [
9
9
  "runwork",