@askexenow/exe-os 0.8.83 → 0.8.86
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/dist/bin/backfill-conversations.js +746 -595
- package/dist/bin/backfill-responses.js +745 -594
- package/dist/bin/backfill-vectors.js +312 -226
- package/dist/bin/cleanup-stale-review-tasks.js +154 -21
- package/dist/bin/cli.js +14678 -12676
- package/dist/bin/exe-agent-config.js +242 -0
- package/dist/bin/exe-agent.js +100 -91
- package/dist/bin/exe-assign.js +1003 -854
- package/dist/bin/exe-boot.js +1420 -485
- package/dist/bin/exe-call.js +10 -0
- package/dist/bin/exe-cloud.js +29 -6
- package/dist/bin/exe-dispatch.js +572 -271
- package/dist/bin/exe-doctor.js +403 -6
- package/dist/bin/exe-export-behaviors.js +175 -72
- package/dist/bin/exe-forget.js +102 -3
- package/dist/bin/exe-gateway.js +796 -292
- package/dist/bin/exe-healthcheck.js +134 -1
- package/dist/bin/exe-heartbeat.js +172 -36
- package/dist/bin/exe-kill.js +175 -72
- package/dist/bin/exe-launch-agent.js +189 -76
- package/dist/bin/exe-link.js +927 -82
- package/dist/bin/exe-new-employee.js +60 -8
- package/dist/bin/exe-pending-messages.js +151 -19
- package/dist/bin/exe-pending-notifications.js +97 -2
- package/dist/bin/exe-pending-reviews.js +155 -22
- package/dist/bin/exe-rename.js +564 -23
- package/dist/bin/exe-review.js +231 -73
- package/dist/bin/exe-search.js +995 -228
- package/dist/bin/exe-session-cleanup.js +4930 -1664
- package/dist/bin/exe-settings.js +20 -5
- package/dist/bin/exe-start-codex.js +2598 -0
- package/dist/bin/exe-start.sh +15 -3
- package/dist/bin/exe-status.js +154 -21
- package/dist/bin/exe-team.js +97 -2
- package/dist/bin/git-sweep.js +1180 -363
- package/dist/bin/graph-backfill.js +175 -72
- package/dist/bin/graph-export.js +175 -72
- package/dist/bin/install.js +60 -7
- package/dist/bin/list-providers.js +1 -0
- package/dist/bin/scan-tasks.js +1185 -367
- package/dist/bin/setup.js +914 -270
- package/dist/bin/shard-migrate.js +175 -72
- package/dist/bin/update.js +1 -0
- package/dist/bin/wiki-sync.js +175 -72
- package/dist/gateway/index.js +792 -285
- package/dist/hooks/bug-report-worker.js +445 -135
- package/dist/hooks/commit-complete.js +1178 -361
- package/dist/hooks/error-recall.js +994 -228
- package/dist/hooks/ingest-worker.js +1799 -1234
- package/dist/hooks/ingest.js +3 -0
- package/dist/hooks/instructions-loaded.js +707 -97
- package/dist/hooks/notification.js +699 -89
- package/dist/hooks/post-compact.js +757 -109
- package/dist/hooks/pre-compact.js +1061 -244
- package/dist/hooks/pre-tool-use.js +787 -130
- package/dist/hooks/prompt-ingest-worker.js +242 -101
- package/dist/hooks/prompt-submit.js +1121 -299
- package/dist/hooks/response-ingest-worker.js +242 -101
- package/dist/hooks/session-end.js +4063 -397
- package/dist/hooks/session-start.js +1071 -254
- package/dist/hooks/stop.js +768 -120
- package/dist/hooks/subagent-stop.js +757 -109
- package/dist/hooks/summary-worker.js +1706 -1011
- package/dist/index.js +1821 -1098
- package/dist/lib/agent-config.js +167 -0
- package/dist/lib/cloud-sync.js +932 -88
- package/dist/lib/consolidation.js +2 -1
- package/dist/lib/database.js +642 -87
- package/dist/lib/db-daemon-client.js +503 -0
- package/dist/lib/device-registry.js +547 -7
- package/dist/lib/embedder.js +14 -28
- package/dist/lib/employee-templates.js +84 -74
- package/dist/lib/employees.js +9 -0
- package/dist/lib/exe-daemon-client.js +16 -29
- package/dist/lib/exe-daemon.js +2733 -1575
- package/dist/lib/hybrid-search.js +995 -228
- package/dist/lib/identity.js +87 -67
- package/dist/lib/keychain.js +9 -1
- package/dist/lib/messaging.js +103 -40
- package/dist/lib/reminders.js +91 -74
- package/dist/lib/runtime-table.js +16 -0
- package/dist/lib/schedules.js +96 -2
- package/dist/lib/session-wrappers.js +22 -0
- package/dist/lib/skill-learning.js +103 -85
- package/dist/lib/store.js +234 -73
- package/dist/lib/tasks.js +348 -134
- package/dist/lib/tmux-routing.js +422 -208
- package/dist/lib/token-spend.js +273 -0
- package/dist/lib/ws-client.js +11 -0
- package/dist/mcp/server.js +5742 -696
- package/dist/mcp/tools/complete-reminder.js +94 -77
- package/dist/mcp/tools/create-reminder.js +94 -77
- package/dist/mcp/tools/create-task.js +375 -152
- package/dist/mcp/tools/deactivate-behavior.js +95 -77
- package/dist/mcp/tools/list-reminders.js +94 -77
- package/dist/mcp/tools/list-tasks.js +99 -31
- package/dist/mcp/tools/send-message.js +108 -45
- package/dist/mcp/tools/update-task.js +162 -77
- package/dist/runtime/index.js +1075 -258
- package/dist/tui/App.js +1333 -506
- package/package.json +6 -1
- package/src/commands/exe/agent-config.md +27 -0
- package/src/commands/exe/cc-doctor.md +10 -0
|
@@ -163,15 +163,22 @@ function getClient() {
|
|
|
163
163
|
if (!_resilientClient) {
|
|
164
164
|
throw new Error("Database client not initialized. Call initDatabase() first.");
|
|
165
165
|
}
|
|
166
|
+
if (process.env.EXE_IS_DAEMON === "1") {
|
|
167
|
+
return _resilientClient;
|
|
168
|
+
}
|
|
169
|
+
if (_daemonClient && _daemonClient._isDaemonActive()) {
|
|
170
|
+
return _daemonClient;
|
|
171
|
+
}
|
|
166
172
|
return _resilientClient;
|
|
167
173
|
}
|
|
168
|
-
var _resilientClient;
|
|
174
|
+
var _resilientClient, _daemonClient;
|
|
169
175
|
var init_database = __esm({
|
|
170
176
|
"src/lib/database.ts"() {
|
|
171
177
|
"use strict";
|
|
172
178
|
init_db_retry();
|
|
173
179
|
init_employees();
|
|
174
180
|
_resilientClient = null;
|
|
181
|
+
_daemonClient = null;
|
|
175
182
|
}
|
|
176
183
|
});
|
|
177
184
|
|
|
@@ -357,18 +364,54 @@ var init_provider_table = __esm({
|
|
|
357
364
|
}
|
|
358
365
|
});
|
|
359
366
|
|
|
360
|
-
// src/lib/
|
|
361
|
-
|
|
367
|
+
// src/lib/runtime-table.ts
|
|
368
|
+
var RUNTIME_TABLE;
|
|
369
|
+
var init_runtime_table = __esm({
|
|
370
|
+
"src/lib/runtime-table.ts"() {
|
|
371
|
+
"use strict";
|
|
372
|
+
RUNTIME_TABLE = {
|
|
373
|
+
codex: {
|
|
374
|
+
binary: "codex",
|
|
375
|
+
launchMode: "exec",
|
|
376
|
+
autoApproveFlag: "--full-auto",
|
|
377
|
+
inlineFlag: "--no-alt-screen",
|
|
378
|
+
apiKeyEnv: "OPENAI_API_KEY",
|
|
379
|
+
defaultModel: "gpt-5.4"
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// src/lib/agent-config.ts
|
|
386
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync3, mkdirSync } from "fs";
|
|
362
387
|
import path4 from "path";
|
|
388
|
+
var AGENT_CONFIG_PATH, DEFAULT_MODELS;
|
|
389
|
+
var init_agent_config = __esm({
|
|
390
|
+
"src/lib/agent-config.ts"() {
|
|
391
|
+
"use strict";
|
|
392
|
+
init_config();
|
|
393
|
+
init_runtime_table();
|
|
394
|
+
AGENT_CONFIG_PATH = path4.join(EXE_AI_DIR, "agent-config.json");
|
|
395
|
+
DEFAULT_MODELS = {
|
|
396
|
+
claude: "claude-opus-4",
|
|
397
|
+
codex: RUNTIME_TABLE.codex?.defaultModel ?? "gpt-5.4",
|
|
398
|
+
opencode: "minimax-m2.7"
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// src/lib/intercom-queue.ts
|
|
404
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, renameSync as renameSync3, existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
|
|
405
|
+
import path5 from "path";
|
|
363
406
|
import os4 from "os";
|
|
364
407
|
function ensureDir() {
|
|
365
|
-
const dir =
|
|
366
|
-
if (!
|
|
408
|
+
const dir = path5.dirname(QUEUE_PATH);
|
|
409
|
+
if (!existsSync4(dir)) mkdirSync2(dir, { recursive: true });
|
|
367
410
|
}
|
|
368
411
|
function readQueue() {
|
|
369
412
|
try {
|
|
370
|
-
if (!
|
|
371
|
-
return JSON.parse(
|
|
413
|
+
if (!existsSync4(QUEUE_PATH)) return [];
|
|
414
|
+
return JSON.parse(readFileSync4(QUEUE_PATH, "utf8"));
|
|
372
415
|
} catch {
|
|
373
416
|
return [];
|
|
374
417
|
}
|
|
@@ -376,7 +419,7 @@ function readQueue() {
|
|
|
376
419
|
function writeQueue(queue) {
|
|
377
420
|
ensureDir();
|
|
378
421
|
const tmp = `${QUEUE_PATH}.tmp`;
|
|
379
|
-
|
|
422
|
+
writeFileSync3(tmp, JSON.stringify(queue, null, 2));
|
|
380
423
|
renameSync3(tmp, QUEUE_PATH);
|
|
381
424
|
}
|
|
382
425
|
function queueIntercom(targetSession, reason) {
|
|
@@ -400,31 +443,31 @@ var QUEUE_PATH, TTL_MS, INTERCOM_LOG;
|
|
|
400
443
|
var init_intercom_queue = __esm({
|
|
401
444
|
"src/lib/intercom-queue.ts"() {
|
|
402
445
|
"use strict";
|
|
403
|
-
QUEUE_PATH =
|
|
446
|
+
QUEUE_PATH = path5.join(os4.homedir(), ".exe-os", "intercom-queue.json");
|
|
404
447
|
TTL_MS = 60 * 60 * 1e3;
|
|
405
|
-
INTERCOM_LOG =
|
|
448
|
+
INTERCOM_LOG = path5.join(os4.homedir(), ".exe-os", "intercom.log");
|
|
406
449
|
}
|
|
407
450
|
});
|
|
408
451
|
|
|
409
452
|
// src/lib/license.ts
|
|
410
|
-
import { readFileSync as
|
|
453
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
|
|
411
454
|
import { randomUUID } from "crypto";
|
|
412
|
-
import
|
|
455
|
+
import path6 from "path";
|
|
413
456
|
import { jwtVerify, importSPKI } from "jose";
|
|
414
457
|
var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH;
|
|
415
458
|
var init_license = __esm({
|
|
416
459
|
"src/lib/license.ts"() {
|
|
417
460
|
"use strict";
|
|
418
461
|
init_config();
|
|
419
|
-
LICENSE_PATH =
|
|
420
|
-
CACHE_PATH =
|
|
421
|
-
DEVICE_ID_PATH =
|
|
462
|
+
LICENSE_PATH = path6.join(EXE_AI_DIR, "license.key");
|
|
463
|
+
CACHE_PATH = path6.join(EXE_AI_DIR, "license-cache.json");
|
|
464
|
+
DEVICE_ID_PATH = path6.join(EXE_AI_DIR, "device-id");
|
|
422
465
|
}
|
|
423
466
|
});
|
|
424
467
|
|
|
425
468
|
// src/lib/plan-limits.ts
|
|
426
|
-
import { readFileSync as
|
|
427
|
-
import
|
|
469
|
+
import { readFileSync as readFileSync6, existsSync as existsSync6 } from "fs";
|
|
470
|
+
import path7 from "path";
|
|
428
471
|
var CACHE_PATH2;
|
|
429
472
|
var init_plan_limits = __esm({
|
|
430
473
|
"src/lib/plan-limits.ts"() {
|
|
@@ -433,13 +476,13 @@ var init_plan_limits = __esm({
|
|
|
433
476
|
init_employees();
|
|
434
477
|
init_license();
|
|
435
478
|
init_config();
|
|
436
|
-
CACHE_PATH2 =
|
|
479
|
+
CACHE_PATH2 = path7.join(EXE_AI_DIR, "license-cache.json");
|
|
437
480
|
}
|
|
438
481
|
});
|
|
439
482
|
|
|
440
483
|
// src/lib/tmux-routing.ts
|
|
441
|
-
import { readFileSync as
|
|
442
|
-
import
|
|
484
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, existsSync as existsSync7, appendFileSync } from "fs";
|
|
485
|
+
import path8 from "path";
|
|
443
486
|
import os5 from "os";
|
|
444
487
|
import { fileURLToPath } from "url";
|
|
445
488
|
function getMySession() {
|
|
@@ -480,7 +523,7 @@ function extractRootExe(name) {
|
|
|
480
523
|
}
|
|
481
524
|
function getParentExe(sessionKey) {
|
|
482
525
|
try {
|
|
483
|
-
const data = JSON.parse(
|
|
526
|
+
const data = JSON.parse(readFileSync7(path8.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
|
|
484
527
|
return data.parentExe || null;
|
|
485
528
|
} catch {
|
|
486
529
|
return null;
|
|
@@ -504,32 +547,50 @@ function isEmployeeAlive(sessionName) {
|
|
|
504
547
|
}
|
|
505
548
|
function readDebounceState() {
|
|
506
549
|
try {
|
|
507
|
-
if (!
|
|
508
|
-
|
|
550
|
+
if (!existsSync7(DEBOUNCE_FILE)) return {};
|
|
551
|
+
const raw = JSON.parse(readFileSync7(DEBOUNCE_FILE, "utf8"));
|
|
552
|
+
const state = {};
|
|
553
|
+
for (const [key, val] of Object.entries(raw)) {
|
|
554
|
+
if (typeof val === "number") {
|
|
555
|
+
state[key] = { lastSent: val, pending: 0 };
|
|
556
|
+
} else if (val && typeof val === "object" && "lastSent" in val) {
|
|
557
|
+
state[key] = val;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return state;
|
|
509
561
|
} catch {
|
|
510
562
|
return {};
|
|
511
563
|
}
|
|
512
564
|
}
|
|
513
565
|
function writeDebounceState(state) {
|
|
514
566
|
try {
|
|
515
|
-
if (!
|
|
516
|
-
|
|
567
|
+
if (!existsSync7(SESSION_CACHE)) mkdirSync4(SESSION_CACHE, { recursive: true });
|
|
568
|
+
writeFileSync5(DEBOUNCE_FILE, JSON.stringify(state));
|
|
517
569
|
} catch {
|
|
518
570
|
}
|
|
519
571
|
}
|
|
520
572
|
function isDebounced(targetSession) {
|
|
521
573
|
const state = readDebounceState();
|
|
522
|
-
const
|
|
523
|
-
|
|
574
|
+
const entry = state[targetSession];
|
|
575
|
+
const lastSent = entry?.lastSent ?? 0;
|
|
576
|
+
if (Date.now() - lastSent < INTERCOM_DEBOUNCE_MS) {
|
|
577
|
+
if (!state[targetSession]) state[targetSession] = { lastSent, pending: 0 };
|
|
578
|
+
state[targetSession].pending++;
|
|
579
|
+
writeDebounceState(state);
|
|
580
|
+
return true;
|
|
581
|
+
}
|
|
582
|
+
return false;
|
|
524
583
|
}
|
|
525
584
|
function recordDebounce(targetSession) {
|
|
526
585
|
const state = readDebounceState();
|
|
527
|
-
state[targetSession]
|
|
586
|
+
const batched = state[targetSession]?.pending ?? 0;
|
|
587
|
+
state[targetSession] = { lastSent: Date.now(), pending: 0 };
|
|
528
588
|
const cutoff = Date.now() - DEBOUNCE_CLEANUP_AGE_MS;
|
|
529
589
|
for (const key of Object.keys(state)) {
|
|
530
|
-
if ((state[key] ?? 0) < cutoff) delete state[key];
|
|
590
|
+
if ((state[key]?.lastSent ?? 0) < cutoff) delete state[key];
|
|
531
591
|
}
|
|
532
592
|
writeDebounceState(state);
|
|
593
|
+
return batched;
|
|
533
594
|
}
|
|
534
595
|
function logIntercom(msg) {
|
|
535
596
|
const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
|
|
@@ -570,7 +631,7 @@ function sendIntercom(targetSession) {
|
|
|
570
631
|
return "skipped_exe";
|
|
571
632
|
}
|
|
572
633
|
if (isDebounced(targetSession)) {
|
|
573
|
-
logIntercom(`DEBOUNCE \u2192 ${targetSession} (
|
|
634
|
+
logIntercom(`DEBOUNCE \u2192 ${targetSession} (nudge batched, task safe in DB)`);
|
|
574
635
|
return "debounced";
|
|
575
636
|
}
|
|
576
637
|
try {
|
|
@@ -582,14 +643,14 @@ function sendIntercom(targetSession) {
|
|
|
582
643
|
const sessionState = getSessionState(targetSession);
|
|
583
644
|
if (sessionState === "no_claude") {
|
|
584
645
|
queueIntercom(targetSession, "claude not running in session");
|
|
585
|
-
recordDebounce(targetSession);
|
|
586
|
-
logIntercom(`QUEUED \u2192 ${targetSession} (no claude process
|
|
646
|
+
const batched2 = recordDebounce(targetSession);
|
|
647
|
+
logIntercom(`QUEUED \u2192 ${targetSession} (no claude process)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
|
|
587
648
|
return "queued";
|
|
588
649
|
}
|
|
589
650
|
if (sessionState === "thinking" || sessionState === "tool") {
|
|
590
651
|
queueIntercom(targetSession, "session busy at send time");
|
|
591
|
-
recordDebounce(targetSession);
|
|
592
|
-
logIntercom(`QUEUED \u2192 ${targetSession} (session busy
|
|
652
|
+
const batched2 = recordDebounce(targetSession);
|
|
653
|
+
logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
|
|
593
654
|
return "queued";
|
|
594
655
|
}
|
|
595
656
|
if (transport.isPaneInCopyMode(targetSession)) {
|
|
@@ -597,8 +658,8 @@ function sendIntercom(targetSession) {
|
|
|
597
658
|
transport.sendKeys(targetSession, "q");
|
|
598
659
|
}
|
|
599
660
|
transport.sendKeys(targetSession, "/exe-intercom");
|
|
600
|
-
recordDebounce(targetSession);
|
|
601
|
-
logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
|
|
661
|
+
const batched = recordDebounce(targetSession);
|
|
662
|
+
logIntercom(`DELIVERED \u2192 ${targetSession}${batched > 0 ? ` [${batched} nudges batched during debounce]` : ""} (fire-and-forget)`);
|
|
602
663
|
return "delivered";
|
|
603
664
|
} catch {
|
|
604
665
|
logIntercom(`FAIL \u2192 ${targetSession}`);
|
|
@@ -615,15 +676,17 @@ var init_tmux_routing = __esm({
|
|
|
615
676
|
init_cc_agent_support();
|
|
616
677
|
init_mcp_prefix();
|
|
617
678
|
init_provider_table();
|
|
679
|
+
init_agent_config();
|
|
680
|
+
init_runtime_table();
|
|
618
681
|
init_intercom_queue();
|
|
619
682
|
init_plan_limits();
|
|
620
683
|
init_employees();
|
|
621
|
-
SPAWN_LOCK_DIR =
|
|
622
|
-
SESSION_CACHE =
|
|
684
|
+
SPAWN_LOCK_DIR = path8.join(os5.homedir(), ".exe-os", "spawn-locks");
|
|
685
|
+
SESSION_CACHE = path8.join(os5.homedir(), ".exe-os", "session-cache");
|
|
623
686
|
VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
|
|
624
687
|
INTERCOM_DEBOUNCE_MS = 3e4;
|
|
625
|
-
INTERCOM_LOG2 =
|
|
626
|
-
DEBOUNCE_FILE =
|
|
688
|
+
INTERCOM_LOG2 = path8.join(os5.homedir(), ".exe-os", "intercom.log");
|
|
689
|
+
DEBOUNCE_FILE = path8.join(SESSION_CACHE, "intercom-debounce.json");
|
|
627
690
|
DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
|
|
628
691
|
BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
|
|
629
692
|
}
|
|
@@ -776,16 +839,16 @@ async function markFailed(messageId, reason) {
|
|
|
776
839
|
|
|
777
840
|
// src/adapters/claude/active-agent.ts
|
|
778
841
|
init_config();
|
|
779
|
-
import { readFileSync as
|
|
842
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, unlinkSync as unlinkSync2, readdirSync } from "fs";
|
|
780
843
|
import { execSync as execSync4 } from "child_process";
|
|
781
|
-
import
|
|
844
|
+
import path9 from "path";
|
|
782
845
|
|
|
783
846
|
// src/adapters/claude/session-key.ts
|
|
784
847
|
init_session_key();
|
|
785
848
|
|
|
786
849
|
// src/adapters/claude/active-agent.ts
|
|
787
850
|
init_employees();
|
|
788
|
-
var CACHE_DIR =
|
|
851
|
+
var CACHE_DIR = path9.join(EXE_AI_DIR, "session-cache");
|
|
789
852
|
var STALE_MS = 24 * 60 * 60 * 1e3;
|
|
790
853
|
function isNameWithOptionalInstance(candidate, baseName) {
|
|
791
854
|
if (candidate === baseName) return true;
|
|
@@ -830,12 +893,12 @@ function resolveActiveAgentFromTmuxSession(sessionName) {
|
|
|
830
893
|
return null;
|
|
831
894
|
}
|
|
832
895
|
function getMarkerPath() {
|
|
833
|
-
return
|
|
896
|
+
return path9.join(CACHE_DIR, `active-agent-${getSessionKey()}.json`);
|
|
834
897
|
}
|
|
835
898
|
function getActiveAgent() {
|
|
836
899
|
try {
|
|
837
900
|
const markerPath = getMarkerPath();
|
|
838
|
-
const raw =
|
|
901
|
+
const raw = readFileSync8(markerPath, "utf8");
|
|
839
902
|
const data = JSON.parse(raw);
|
|
840
903
|
if (data.agentId) {
|
|
841
904
|
if (data.startedAt) {
|