@moda-ai/cli 1.55.28 → 1.55.29

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/cli.js CHANGED
@@ -91,6 +91,7 @@ import {
91
91
  createCliOutput,
92
92
  embeddedTenantIdFromApiKey,
93
93
  errorToAgentError,
94
+ getActiveProfile,
94
95
  getApiRequestCount,
95
96
  getModaDir,
96
97
  isPlatformApiKey,
@@ -128,7 +129,7 @@ import {
128
129
  } from "./cli-0v6na3yp.js";
129
130
 
130
131
  // src/cli.ts
131
- import { randomUUID as randomUUID2 } from "node:crypto";
132
+ import { randomUUID as randomUUID3 } from "node:crypto";
132
133
  import { z } from "zod";
133
134
 
134
135
  // src/replays.ts
@@ -387,10 +388,223 @@ function spawnDetachedNode(script, options = {}, spawnImpl = spawn) {
387
388
  }
388
389
 
389
390
  // src/telemetry.ts
390
- import { randomUUID } from "node:crypto";
391
+ import { randomUUID as randomUUID2 } from "node:crypto";
392
+
393
+ // src/telemetry-spool.ts
394
+ import { createHash, randomUUID } from "node:crypto";
395
+ import {
396
+ closeSync,
397
+ constants as fsConstants,
398
+ fchmodSync,
399
+ mkdirSync,
400
+ openSync,
401
+ readFileSync,
402
+ renameSync,
403
+ rmSync,
404
+ statSync,
405
+ unlinkSync,
406
+ writeSync
407
+ } from "node:fs";
408
+ import { join } from "node:path";
409
+ var SPOOL_MAX_BYTES = 256 * 1024;
410
+ var FLUSH_MIN_EVENTS = 20;
411
+ var FLUSH_MAX_AGE_MS = 30000;
412
+ var SPOOL_MAX_EVENT_AGE_MS = 7 * 24 * 60 * 60 * 1000;
413
+ var FLUSH_LOCK_STALE_MS = 30000;
414
+ var MAX_BATCH_EVENTS = 50;
415
+ var SPOOL_LOCK_STALE_MS = 2000;
416
+ var SPOOL_LOCK_WAIT_MS = 200;
417
+ var SPOOL_FILE = "usage-spool.jsonl";
418
+ var LOCK_FILE = "flush.lock";
419
+ var SPOOL_LOCK_FILE = "spool.lock";
420
+ var O_NOFOLLOW = fsConstants.O_NOFOLLOW ?? 0;
421
+ function telemetrySpoolDir() {
422
+ return join(getModaDir(), "telemetry");
423
+ }
424
+ function keyFingerprint(apiKey) {
425
+ return createHash("sha256").update(`moda-cli-usage-spool\x00${apiKey}`).digest("hex").slice(0, 16);
426
+ }
427
+ function ensureDir(dir) {
428
+ mkdirSync(dir, { recursive: true, mode: 448 });
429
+ }
430
+ function appendLines(path, text) {
431
+ const fd = openSync(path, fsConstants.O_WRONLY | fsConstants.O_APPEND | fsConstants.O_CREAT | O_NOFOLLOW, 384);
432
+ try {
433
+ try {
434
+ fchmodSync(fd, 384);
435
+ } catch {}
436
+ writeSync(fd, text);
437
+ } finally {
438
+ closeSync(fd);
439
+ }
440
+ }
441
+ function sleepSync(ms) {
442
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
443
+ }
444
+ function createLockFile(path, staleMs, now) {
445
+ for (let attempt = 0;attempt < 2; attempt += 1) {
446
+ try {
447
+ const fd = openSync(path, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | O_NOFOLLOW, 384);
448
+ try {
449
+ writeSync(fd, String(process.pid));
450
+ } finally {
451
+ closeSync(fd);
452
+ }
453
+ return true;
454
+ } catch (error) {
455
+ if (error.code !== "EEXIST")
456
+ return false;
457
+ try {
458
+ if (now - statSync(path).mtimeMs < staleMs)
459
+ return false;
460
+ unlinkSync(path);
461
+ } catch {}
462
+ }
463
+ }
464
+ return false;
465
+ }
466
+ function withSpoolLock(dir, fn) {
467
+ const path = join(dir, SPOOL_LOCK_FILE);
468
+ const deadline = Date.now() + SPOOL_LOCK_WAIT_MS;
469
+ while (!createLockFile(path, SPOOL_LOCK_STALE_MS, Date.now())) {
470
+ if (Date.now() >= deadline)
471
+ throw new Error("telemetry spool is locked");
472
+ sleepSync(5);
473
+ }
474
+ try {
475
+ return fn();
476
+ } finally {
477
+ try {
478
+ unlinkSync(path);
479
+ } catch {}
480
+ }
481
+ }
482
+ function fileSize(path) {
483
+ try {
484
+ return statSync(path).size;
485
+ } catch {
486
+ return 0;
487
+ }
488
+ }
489
+ function parseEntries(text) {
490
+ const entries = [];
491
+ for (const line of text.split(`
492
+ `)) {
493
+ if (!line)
494
+ continue;
495
+ try {
496
+ const entry = JSON.parse(line);
497
+ if (entry && entry.v === 1 && typeof entry.url === "string" && typeof entry.profile === "string" && typeof entry.key === "string" && typeof entry.t === "number" && entry.event && typeof entry.event === "object") {
498
+ entries.push(entry);
499
+ }
500
+ } catch {}
501
+ }
502
+ return entries;
503
+ }
504
+ function sameGroup(a, b) {
505
+ return a.url === b.url && a.profile === b.profile && a.key === b.key;
506
+ }
507
+ function appendSpoolEntry(entry, dir = telemetrySpoolDir()) {
508
+ ensureDir(dir);
509
+ const path = join(dir, SPOOL_FILE);
510
+ const line = `${JSON.stringify(entry)}
511
+ `;
512
+ return withSpoolLock(dir, () => {
513
+ if (fileSize(path) + Buffer.byteLength(line) > SPOOL_MAX_BYTES)
514
+ return false;
515
+ appendLines(path, line);
516
+ return true;
517
+ });
518
+ }
519
+ function readSpool(dir = telemetrySpoolDir()) {
520
+ try {
521
+ return parseEntries(readFileSync(join(dir, SPOOL_FILE), "utf8"));
522
+ } catch {
523
+ return [];
524
+ }
525
+ }
526
+ function discardSpool(dir = telemetrySpoolDir()) {
527
+ try {
528
+ rmSync(join(dir, SPOOL_FILE), { force: true });
529
+ } catch {}
530
+ }
531
+ function flushDue(entries, group, now) {
532
+ let count = 0;
533
+ for (const entry of entries) {
534
+ if (!sameGroup(entry, group))
535
+ continue;
536
+ count += 1;
537
+ if (count >= FLUSH_MIN_EVENTS || now - entry.t >= FLUSH_MAX_AGE_MS)
538
+ return true;
539
+ }
540
+ return false;
541
+ }
542
+ function acquireFlushLock(dir = telemetrySpoolDir(), now = Date.now()) {
543
+ ensureDir(dir);
544
+ const path = join(dir, LOCK_FILE);
545
+ return createLockFile(path, FLUSH_LOCK_STALE_MS, now) ? path : undefined;
546
+ }
547
+ function releaseFlushLock(path) {
548
+ if (!path)
549
+ return;
550
+ try {
551
+ unlinkSync(path);
552
+ } catch {}
553
+ }
554
+ function claimSpoolGroup(group, dir = telemetrySpoolDir(), now = Date.now(), limit = Number.POSITIVE_INFINITY) {
555
+ try {
556
+ return withSpoolLock(dir, () => claimLocked(group, dir, now, limit));
557
+ } catch {
558
+ return [];
559
+ }
560
+ }
561
+ function claimLocked(group, dir, now, limit) {
562
+ const path = join(dir, SPOOL_FILE);
563
+ const claim = join(dir, `${SPOOL_FILE}.${process.pid}.${randomUUID()}.claim`);
564
+ try {
565
+ renameSync(path, claim);
566
+ } catch {
567
+ return [];
568
+ }
569
+ let entries = [];
570
+ try {
571
+ entries = parseEntries(readFileSync(claim, "utf8"));
572
+ } catch {} finally {
573
+ rmSync(claim, { force: true });
574
+ }
575
+ const mine = [];
576
+ const others = [];
577
+ for (const entry of entries) {
578
+ const age = now - entry.t;
579
+ if (age > SPOOL_MAX_EVENT_AGE_MS || age < -FLUSH_LOCK_STALE_MS)
580
+ continue;
581
+ (sameGroup(entry, group) && mine.length < limit ? mine : others).push(entry);
582
+ }
583
+ if (others.length > 0) {
584
+ try {
585
+ const text = others.map((entry) => `${JSON.stringify(entry)}
586
+ `).join("");
587
+ if (fileSize(path) + Buffer.byteLength(text) <= SPOOL_MAX_BYTES)
588
+ appendLines(path, text);
589
+ } catch {}
590
+ }
591
+ return mine;
592
+ }
593
+ function batchBodies(entries, now = Date.now()) {
594
+ const bodies = [];
595
+ for (let start = 0;start < entries.length; start += MAX_BATCH_EVENTS) {
596
+ const events = entries.slice(start, start + MAX_BATCH_EVENTS).map((entry) => ({ ...entry.event, age_ms: Math.min(SPOOL_MAX_EVENT_AGE_MS, Math.max(0, Math.round(now - entry.t))) }));
597
+ bodies.push(JSON.stringify(events));
598
+ }
599
+ return bodies;
600
+ }
601
+
602
+ // src/telemetry.ts
391
603
  var INLINE_TELEMETRY_TIMEOUT_MS = 500;
392
604
  var DETACHED_TELEMETRY_TIMEOUT_MS = 5000;
393
- var INVOCATION_ID = randomUUID();
605
+ var DETACHED_TELEMETRY_HARD_STOP_MS = 20000;
606
+ var MAX_FLUSH_BATCHES = Math.floor(DETACHED_TELEMETRY_HARD_STOP_MS / (2 * DETACHED_TELEMETRY_TIMEOUT_MS));
607
+ var INVOCATION_ID = randomUUID2();
394
608
  var TELEMETRY_FLAG = "no-telemetry";
395
609
  var TELEMETRY_TRANSPORT_ENV = "MODA_CLI_TELEMETRY_TRANSPORT";
396
610
  var MAX_TELEMETRY_DURATION_MS = 86400000;
@@ -560,48 +774,103 @@ function safeCode(value) {
560
774
  var TELEMETRY_SENDER_SCRIPT = [
561
775
  "try {",
562
776
  " let raw = '';",
563
- ` const quit = setTimeout(() => process.exit(0), ${DETACHED_TELEMETRY_TIMEOUT_MS + 1000});`,
564
- " const done = () => clearTimeout(quit);",
777
+ " let lock;",
778
+ " const release = () => { if (typeof lock === 'string') { try { require('node:fs').unlinkSync(lock); } catch {} lock = undefined; } };",
779
+ ` const quit = setTimeout(() => { release(); process.exit(0); }, ${DETACHED_TELEMETRY_HARD_STOP_MS});`,
780
+ " const done = () => { release(); clearTimeout(quit); };",
781
+ " const post = (r, body) => new Promise((resolve) => {",
782
+ " try {",
783
+ " const u = new URL(r.url);",
784
+ " const lib = require(u.protocol === 'https:' ? 'node:https' : 'node:http');",
785
+ " const headers = Object.assign({}, r.headers, { 'Content-Length': Buffer.byteLength(body) });",
786
+ ` const req = lib.request(u, { method: 'POST', headers, agent: false, timeout: ${DETACHED_TELEMETRY_TIMEOUT_MS} }, (res) => {`,
787
+ " res.on('error', () => resolve(0));",
788
+ " res.on('end', () => resolve(res.statusCode || 0));",
789
+ " res.resume();",
790
+ " });",
791
+ " req.on('timeout', () => req.destroy());",
792
+ " req.on('error', () => resolve(0));",
793
+ " req.end(body);",
794
+ " } catch { resolve(0); }",
795
+ " });",
796
+ " const run = async (requests) => {",
797
+ " for (const r of requests) {",
798
+ " if (!r || typeof r.url !== 'string' || typeof r.body !== 'string' || !r.headers || typeof r.headers !== 'object') continue;",
799
+ " const status = await post(r, r.body);",
800
+ " if (status !== 400 || r.body[0] !== '[') continue;",
801
+ " let events = [];",
802
+ " try { events = JSON.parse(r.body); } catch {}",
803
+ " await Promise.all((Array.isArray(events) ? events : []).map((event) => post(r, JSON.stringify(event))));",
804
+ " }",
805
+ " };",
565
806
  " process.stdin.setEncoding('utf8');",
566
807
  " process.stdin.on('error', () => {});",
567
808
  " process.stdin.on('data', (chunk) => { raw += chunk; });",
568
809
  " process.stdin.on('end', () => {",
569
- " try {",
570
- " const r = raw ? JSON.parse(raw) : null;",
571
- " raw = '';",
572
- " if (!r || typeof r.url !== 'string' || typeof fetch !== 'function') return done();",
573
- " const c = new AbortController();",
574
- ` const t = setTimeout(() => c.abort(), ${DETACHED_TELEMETRY_TIMEOUT_MS});`,
575
- " fetch(r.url, { method: 'POST', headers: r.headers, body: r.body, signal: c.signal })",
576
- " .then((res) => (res.body && res.body.cancel ? res.body.cancel() : undefined))",
577
- " .catch(() => {})",
578
- " .finally(() => { clearTimeout(t); done(); });",
579
- " } catch { done(); }",
810
+ " let job = null;",
811
+ " try { job = raw ? JSON.parse(raw) : null; } catch {}",
812
+ " raw = '';",
813
+ " if (job && typeof job.lock === 'string') lock = job.lock;",
814
+ " const requests = job && Array.isArray(job.requests) ? job.requests : [];",
815
+ " run(requests).catch(() => {}).finally(done);",
580
816
  " });",
581
817
  "} catch {}"
582
818
  ].join(`
583
819
  `);
584
- function spawnDetachedTelemetrySender(request, spawnImpl = spawn2) {
585
- return spawnDetachedNode(TELEMETRY_SENDER_SCRIPT, { stdin: JSON.stringify(request), dropEnv: ["MODA_API_KEY"] }, spawnImpl);
820
+ function spawnDetachedTelemetrySender(job, spawnImpl = spawn2) {
821
+ return spawnDetachedNode(TELEMETRY_SENDER_SCRIPT, { stdin: JSON.stringify(job), dropEnv: ["MODA_API_KEY"] }, spawnImpl);
822
+ }
823
+ async function sendTelemetryJobInline(job) {
824
+ try {
825
+ for (const pending of job.requests)
826
+ await sendInline(pending);
827
+ } finally {
828
+ releaseFlushLock(job.lock);
829
+ }
586
830
  }
587
831
  async function sendInline(request) {
832
+ const status = await postInline(request);
833
+ if (status !== 400 || !request.body.startsWith("["))
834
+ return;
835
+ let events = [];
836
+ try {
837
+ events = JSON.parse(request.body);
838
+ } catch {}
839
+ if (!Array.isArray(events))
840
+ return;
841
+ await Promise.all(events.map((event) => postInline({ ...request, body: JSON.stringify(event) })));
842
+ }
843
+ async function postInline(request) {
588
844
  const controller = new AbortController;
589
845
  const timeout = setTimeout(() => controller.abort(), INLINE_TELEMETRY_TIMEOUT_MS);
590
846
  if (typeof timeout.unref === "function") {
591
847
  timeout.unref();
592
848
  }
593
849
  try {
594
- await fetch(request.url, {
850
+ const response = await fetch(request.url, {
595
851
  method: "POST",
596
852
  headers: request.headers,
597
853
  body: request.body,
598
854
  signal: controller.signal
599
855
  });
600
- } catch {} finally {
856
+ return response.status;
857
+ } catch {
858
+ return 0;
859
+ } finally {
601
860
  clearTimeout(timeout);
602
861
  }
603
862
  }
604
- function buildCliUsageRequest(input) {
863
+ function usageRequest(baseUrl, apiKey, body) {
864
+ return {
865
+ url: `${baseUrl}/api/v1/data/cli-usage`,
866
+ headers: {
867
+ ...apiKeyHeaders(apiKey),
868
+ "Content-Type": "application/json"
869
+ },
870
+ body
871
+ };
872
+ }
873
+ function buildCliUsageEvent(input) {
605
874
  const baseUrl = resolveModaBaseUrlStrict(input.profile ? { profile: input.profile } : {});
606
875
  if (!baseUrl)
607
876
  return;
@@ -613,12 +882,8 @@ function buildCliUsageRequest(input) {
613
882
  delete adoption[key];
614
883
  }
615
884
  return {
616
- url: `${baseUrl}/api/v1/data/cli-usage`,
617
- headers: {
618
- ...apiKeyHeaders(input.apiKey),
619
- "Content-Type": "application/json"
620
- },
621
- body: JSON.stringify({
885
+ baseUrl,
886
+ event: {
622
887
  invocation_id: INVOCATION_ID,
623
888
  command: safeCode(input.command.slice(0, 64)) ?? "unknown",
624
889
  status: input.status,
@@ -637,27 +902,79 @@ function buildCliUsageRequest(input) {
637
902
  ...((alias) => alias ? { legacy_alias: alias } : {})(safeCode(sanitizeReasonCode(input.legacyAlias?.replace(/ /g, ":"), 64)))
638
903
  },
639
904
  ...adoption
640
- })
905
+ }
906
+ };
907
+ }
908
+ function telemetryProfileName(profile) {
909
+ try {
910
+ return getActiveProfile(profile ? { profile } : {}).name;
911
+ } catch {
912
+ return "";
913
+ }
914
+ }
915
+ function queueCliUsageEvent(request, group, event, now) {
916
+ const dir = telemetrySpoolDir();
917
+ const entry = { v: 1, ...group, t: now, event };
918
+ let unspooled = [];
919
+ try {
920
+ if (!appendSpoolEntry(entry, dir))
921
+ unspooled = [entry];
922
+ } catch {
923
+ return { requests: [request] };
924
+ }
925
+ const due = unspooled.length > 0 || envOptOutSet(process.env.CI) || flushDue(readSpool(dir), group, now);
926
+ if (!due)
927
+ return;
928
+ const lock = acquireFlushLock(dir, now);
929
+ if (!lock)
930
+ return unspooled.length > 0 ? { requests: [request] } : undefined;
931
+ const limit = MAX_FLUSH_BATCHES * MAX_BATCH_EVENTS - unspooled.length;
932
+ const entries = [...claimSpoolGroup(group, dir, now, limit), ...unspooled];
933
+ if (entries.length === 0) {
934
+ releaseFlushLock(lock);
935
+ return;
936
+ }
937
+ return {
938
+ requests: batchBodies(entries, now).map((body) => ({ url: request.url, headers: request.headers, body })),
939
+ lock
641
940
  };
642
941
  }
942
+ function discardSpoolOnEnvOptOut() {
943
+ if (!isTelemetryDisabled({}))
944
+ return false;
945
+ discardSpool();
946
+ return true;
947
+ }
643
948
  async function sendCliUsageTelemetry(input) {
644
949
  try {
950
+ if (discardSpoolOnEnvOptOut())
951
+ return;
645
952
  if (!input.apiKey || isTelemetryDisabled(input.flags)) {
646
953
  return;
647
954
  }
648
- const request = buildCliUsageRequest({ ...input, apiKey: input.apiKey });
649
- if (!request)
955
+ const built = buildCliUsageEvent(input);
956
+ if (!built)
650
957
  return;
651
- if (process.env[TELEMETRY_TRANSPORT_ENV] !== "inline" && spawnDetachedTelemetrySender(request)) {
958
+ const request = usageRequest(built.baseUrl, input.apiKey, JSON.stringify(built.event));
959
+ if (process.env[TELEMETRY_TRANSPORT_ENV] === "inline") {
960
+ await sendInline(request);
652
961
  return;
653
962
  }
654
- await sendInline(request);
963
+ const group = {
964
+ url: built.baseUrl,
965
+ profile: telemetryProfileName(input.profile),
966
+ key: keyFingerprint(input.apiKey)
967
+ };
968
+ const job = queueCliUsageEvent(request, group, built.event, Date.now());
969
+ if (!job || spawnDetachedTelemetrySender(job))
970
+ return;
971
+ await sendTelemetryJobInline(job);
655
972
  } catch {}
656
973
  }
657
974
 
658
975
  // src/update-check.ts
659
- import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
660
- import { join } from "node:path";
976
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "node:fs";
977
+ import { join as join2 } from "node:path";
661
978
  var UPDATE_CHECK_FLAG = "no-update-check";
662
979
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
663
980
  var REGISTRY_TIMEOUT_MS = 1500;
@@ -670,11 +987,11 @@ function isUpdateCheckDisabled(flags, env = process.env) {
670
987
  return optOutFlagSet(flags, UPDATE_CHECK_FLAG) || envSwitchedOff(env.MODA_CLI_UPDATE_CHECK) || envOptOutSet(env.DO_NOT_TRACK) || envOptOutSet(env.CI);
671
988
  }
672
989
  function cachePath(options = {}) {
673
- return join(getModaDir(options), "update-check.json");
990
+ return join2(getModaDir(options), "update-check.json");
674
991
  }
675
992
  function readCache(options = {}) {
676
993
  try {
677
- const parsed = JSON.parse(readFileSync(cachePath(options), "utf8"));
994
+ const parsed = JSON.parse(readFileSync2(cachePath(options), "utf8"));
678
995
  if (parsed && typeof parsed === "object" && typeof parsed.checked_at === "number" && typeof parsed.latest === "string") {
679
996
  return parsed;
680
997
  }
@@ -683,7 +1000,7 @@ function readCache(options = {}) {
683
1000
  }
684
1001
  function writeCache(cache, options = {}) {
685
1002
  try {
686
- mkdirSync(getModaDir(options), { recursive: true });
1003
+ mkdirSync2(getModaDir(options), { recursive: true });
687
1004
  writeFileSync(cachePath(options), `${JSON.stringify(cache, null, 2)}
688
1005
  `, "utf8");
689
1006
  } catch {}
@@ -5445,7 +5762,7 @@ async function runCommand(command, positional, flags, positionals = positional ?
5445
5762
  }
5446
5763
  const data = await callDataAPI(reviewPath, {
5447
5764
  method: "POST",
5448
- body: JSON.stringify({ ...reviewBody, request_nonce: randomUUID2() }),
5765
+ body: JSON.stringify({ ...reviewBody, request_nonce: randomUUID3() }),
5449
5766
  headers: { "Content-Type": "application/json" }
5450
5767
  });
5451
5768
  context.output.writeData(data);
@@ -5538,7 +5855,7 @@ async function runCommand(command, positional, flags, positionals = positional ?
5538
5855
  });
5539
5856
  break;
5540
5857
  }
5541
- const sentBody = request.body && request.nonce ? { ...request.body, request_nonce: randomUUID2() } : request.body;
5858
+ const sentBody = request.body && request.nonce ? { ...request.body, request_nonce: randomUUID3() } : request.body;
5542
5859
  const data = await callDataAPI(request.path, {
5543
5860
  method: request.method,
5544
5861
  ...sentBody ? { body: JSON.stringify(sentBody), headers: { "Content-Type": "application/json" } } : {}
@@ -7100,6 +7417,8 @@ async function dispatchParsedCommandInner(typedParsed, startedAt) {
7100
7417
  const switchError = normalizeBooleanFlags(flags);
7101
7418
  const typedFlags = stripUpdateCheckFlag(stripTelemetryFlag(flags));
7102
7419
  const telemetryDisabled = isTelemetryDisabled(flags);
7420
+ if (telemetryDisabled)
7421
+ discardSpoolOnEnvOptOut();
7103
7422
  const updateCheckDisabled = isUpdateCheckDisabled(flags);
7104
7423
  if (command === "--version" || command === "-V" || flags.version === "true") {
7105
7424
  console.log(`moda-cli ${VERSION}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moda-ai/cli",
3
- "version": "1.55.28",
3
+ "version": "1.55.29",
4
4
  "description": "CLI for Moda: production monitoring and evals for AI agents.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema_version": "moda.skill_index.v1",
3
- "bundled_at": "2026-10-15T21:39:30.375Z",
4
- "cli_version": "1.55.28",
3
+ "bundled_at": "2026-10-15T21:58:38.008Z",
4
+ "cli_version": "1.55.29",
5
5
  "skills": [
6
6
  {
7
7
  "id": "integration-cloudflare-think",