@ascenda-one/github-collector 0.1.8 → 0.1.10

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/dist/cli.js +562 -46
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -415,6 +415,93 @@ var require_out = __commonJS({
415
415
  }
416
416
  });
417
417
 
418
+ // ../packages/tool-kit/out/eventLog.js
419
+ var require_eventLog = __commonJS({
420
+ "../packages/tool-kit/out/eventLog.js"(exports) {
421
+ "use strict";
422
+ var __createBinding = exports && exports.__createBinding || (Object.create ? function(o, m, k, k2) {
423
+ if (k2 === void 0) k2 = k;
424
+ var desc = Object.getOwnPropertyDescriptor(m, k);
425
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
426
+ desc = { enumerable: true, get: function() {
427
+ return m[k];
428
+ } };
429
+ }
430
+ Object.defineProperty(o, k2, desc);
431
+ } : function(o, m, k, k2) {
432
+ if (k2 === void 0) k2 = k;
433
+ o[k2] = m[k];
434
+ });
435
+ var __setModuleDefault = exports && exports.__setModuleDefault || (Object.create ? function(o, v) {
436
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
437
+ } : function(o, v) {
438
+ o["default"] = v;
439
+ });
440
+ var __importStar = exports && exports.__importStar || /* @__PURE__ */ function() {
441
+ var ownKeys = function(o) {
442
+ ownKeys = Object.getOwnPropertyNames || function(o2) {
443
+ var ar = [];
444
+ for (var k in o2) if (Object.prototype.hasOwnProperty.call(o2, k)) ar[ar.length] = k;
445
+ return ar;
446
+ };
447
+ return ownKeys(o);
448
+ };
449
+ return function(mod) {
450
+ if (mod && mod.__esModule) return mod;
451
+ var result = {};
452
+ if (mod != null) {
453
+ for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
454
+ }
455
+ __setModuleDefault(result, mod);
456
+ return result;
457
+ };
458
+ }();
459
+ Object.defineProperty(exports, "__esModule", { value: true });
460
+ exports.EVENT_LOG_ENV_VAR = void 0;
461
+ exports.expandUserPath = expandUserPath;
462
+ exports.resolveEventLogPath = resolveEventLogPath;
463
+ exports.appendEventLog = appendEventLog;
464
+ var fs = __importStar(__require("fs"));
465
+ var os = __importStar(__require("os"));
466
+ var path = __importStar(__require("path"));
467
+ exports.EVENT_LOG_ENV_VAR = "ASCENDA_EVENT_LOG_FILE";
468
+ var MAX_BYTES = 5 * 1024 * 1024;
469
+ function expandUserPath(configured) {
470
+ const value = configured?.trim();
471
+ if (!value)
472
+ return void 0;
473
+ if (value === "~")
474
+ return os.homedir();
475
+ if (value.startsWith("~/"))
476
+ return path.join(os.homedir(), value.slice(2));
477
+ return path.resolve(value);
478
+ }
479
+ function resolveEventLogPath() {
480
+ return expandUserPath(process.env[exports.EVENT_LOG_ENV_VAR]);
481
+ }
482
+ function appendEventLog(logFilePath, entry) {
483
+ try {
484
+ rotateIfLarge(logFilePath);
485
+ const dir = path.dirname(logFilePath);
486
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
487
+ fs.appendFileSync(logFilePath, `${JSON.stringify(entry)}
488
+ `, { encoding: "utf8", mode: 384 });
489
+ if (process.platform !== "win32")
490
+ fs.chmodSync(logFilePath, 384);
491
+ } catch {
492
+ }
493
+ }
494
+ function rotateIfLarge(logFilePath) {
495
+ try {
496
+ if (fs.statSync(logFilePath).size < MAX_BYTES)
497
+ return;
498
+ fs.renameSync(logFilePath, `${logFilePath}.1`);
499
+ } catch {
500
+ }
501
+ }
502
+ }
503
+ });
504
+
418
505
  // ../packages/tool-kit/out/http.js
419
506
  var require_http = __commonJS({
420
507
  "../packages/tool-kit/out/http.js"(exports) {
@@ -424,6 +511,7 @@ var require_http = __commonJS({
424
511
  exports.createPairingSession = createPairingSession;
425
512
  exports.getPairingStatus = getPairingStatus;
426
513
  exports.renewToolToken = renewToolToken;
514
+ exports.isRetryableStatus = isRetryableStatus;
427
515
  exports.postToolEvent = postToolEvent;
428
516
  exports.postToolEventsBatch = postToolEventsBatch;
429
517
  exports.parseIngestResponse = parseIngestResponse;
@@ -469,26 +557,34 @@ var require_http = __commonJS({
469
557
  throw new AscendaApiError(response.status, void 0, await response.text());
470
558
  return await response.json();
471
559
  }
560
+ function isRetryableStatus(status) {
561
+ return status === 408 || status === 429 || status !== void 0 && status >= 500 && status <= 599;
562
+ }
472
563
  async function postToolEvent(apiBaseUrl, eventWriteToken, payload, signal) {
473
- const response = await fetch(`${apiBaseUrl}/v1/tool-events`, {
474
- method: "POST",
475
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${eventWriteToken}` },
476
- body: JSON.stringify(payload),
477
- signal
478
- });
479
- return parseIngestResponse(response);
564
+ return sendIngest(`${apiBaseUrl}/v1/tool-events`, eventWriteToken, JSON.stringify(payload), signal);
480
565
  }
481
- async function postToolEventsBatch(apiBaseUrl, eventWriteToken, payloads) {
482
- const response = await fetch(`${apiBaseUrl}/v1/tool-events/batch`, {
483
- method: "POST",
484
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${eventWriteToken}` },
485
- body: JSON.stringify({ events: payloads })
486
- });
487
- return parseIngestResponse(response);
566
+ async function postToolEventsBatch(apiBaseUrl, eventWriteToken, payloads, signal) {
567
+ return sendIngest(`${apiBaseUrl}/v1/tool-events/batch`, eventWriteToken, JSON.stringify({ events: payloads }), signal);
568
+ }
569
+ async function sendIngest(url, eventWriteToken, body, signal) {
570
+ try {
571
+ const response = await fetch(url, {
572
+ method: "POST",
573
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${eventWriteToken}` },
574
+ body,
575
+ signal
576
+ });
577
+ return parseIngestResponse(response);
578
+ } catch (error) {
579
+ return {
580
+ result: "transport_error",
581
+ detail: error instanceof Error ? `${error.name}: ${error.message}` : String(error)
582
+ };
583
+ }
488
584
  }
489
585
  async function parseIngestResponse(response) {
490
586
  if (response.ok)
491
- return "accepted";
587
+ return { result: "accepted", httpStatus: response.status };
492
588
  const body = await response.text();
493
589
  let errorCode;
494
590
  try {
@@ -496,13 +592,14 @@ var require_http = __commonJS({
496
592
  } catch {
497
593
  errorCode = void 0;
498
594
  }
595
+ const base2 = { httpStatus: response.status, errorCode, detail: body || void 0 };
499
596
  if (response.status === 401)
500
- return "auth_failed";
597
+ return { ...base2, result: "auth_failed" };
501
598
  if (response.status === 403 && errorCode === "consent_missing_or_expired")
502
- return "consent_missing";
599
+ return { ...base2, result: "consent_missing" };
503
600
  if (response.status === 400 || response.status === 422)
504
- return "validation_failed";
505
- throw new AscendaApiError(response.status, errorCode, body);
601
+ return { ...base2, result: "validation_failed" };
602
+ return { ...base2, result: "transport_error" };
506
603
  }
507
604
  }
508
605
  });
@@ -552,6 +649,7 @@ var require_tokenStore = __commonJS({
552
649
  exports.defaultTokenFilePath = defaultTokenFilePath2;
553
650
  exports.persistEventWriteToken = persistEventWriteToken2;
554
651
  exports.readTokenFile = readTokenFile2;
652
+ exports.sanitizeFilePart = sanitizeFilePart;
555
653
  var fs = __importStar(__require("fs"));
556
654
  var os = __importStar(__require("os"));
557
655
  var path = __importStar(__require("path"));
@@ -583,15 +681,144 @@ var require_tokenStore = __commonJS({
583
681
  }
584
682
  });
585
683
 
684
+ // ../packages/tool-kit/out/stateStore.js
685
+ var require_stateStore = __commonJS({
686
+ "../packages/tool-kit/out/stateStore.js"(exports) {
687
+ "use strict";
688
+ var __createBinding = exports && exports.__createBinding || (Object.create ? function(o, m, k, k2) {
689
+ if (k2 === void 0) k2 = k;
690
+ var desc = Object.getOwnPropertyDescriptor(m, k);
691
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
692
+ desc = { enumerable: true, get: function() {
693
+ return m[k];
694
+ } };
695
+ }
696
+ Object.defineProperty(o, k2, desc);
697
+ } : function(o, m, k, k2) {
698
+ if (k2 === void 0) k2 = k;
699
+ o[k2] = m[k];
700
+ });
701
+ var __setModuleDefault = exports && exports.__setModuleDefault || (Object.create ? function(o, v) {
702
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
703
+ } : function(o, v) {
704
+ o["default"] = v;
705
+ });
706
+ var __importStar = exports && exports.__importStar || /* @__PURE__ */ function() {
707
+ var ownKeys = function(o) {
708
+ ownKeys = Object.getOwnPropertyNames || function(o2) {
709
+ var ar = [];
710
+ for (var k in o2) if (Object.prototype.hasOwnProperty.call(o2, k)) ar[ar.length] = k;
711
+ return ar;
712
+ };
713
+ return ownKeys(o);
714
+ };
715
+ return function(mod) {
716
+ if (mod && mod.__esModule) return mod;
717
+ var result = {};
718
+ if (mod != null) {
719
+ for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
720
+ }
721
+ __setModuleDefault(result, mod);
722
+ return result;
723
+ };
724
+ }();
725
+ Object.defineProperty(exports, "__esModule", { value: true });
726
+ exports.defaultStateFilePath = defaultStateFilePath;
727
+ exports.readCollectorState = readCollectorState;
728
+ exports.recordSendOutcome = recordSendOutcome;
729
+ exports.shouldAnnounceFailure = shouldAnnounceFailure;
730
+ exports.markFailureNotified = markFailureNotified;
731
+ var fs = __importStar(__require("fs"));
732
+ var os = __importStar(__require("os"));
733
+ var path = __importStar(__require("path"));
734
+ var tokenStore_1 = require_tokenStore();
735
+ function defaultStateFilePath(toolInstallationId) {
736
+ return path.join(os.homedir(), ".ascenda", "state", `${(0, tokenStore_1.sanitizeFilePart)(toolInstallationId)}.json`);
737
+ }
738
+ function readCollectorState(stateFilePath) {
739
+ try {
740
+ if (!fs.existsSync(stateFilePath))
741
+ return void 0;
742
+ const raw = fs.readFileSync(stateFilePath, "utf8").trim();
743
+ if (!raw)
744
+ return void 0;
745
+ const parsed = JSON.parse(raw);
746
+ if (!parsed || typeof parsed !== "object")
747
+ return void 0;
748
+ return parsed;
749
+ } catch {
750
+ return void 0;
751
+ }
752
+ }
753
+ function recordSendOutcome(stateFilePath, toolInstallationId, outcome, detail = {}) {
754
+ const now = (/* @__PURE__ */ new Date()).toISOString();
755
+ const previous = readCollectorState(stateFilePath);
756
+ const accepted = outcome === "accepted";
757
+ const next = {
758
+ toolInstallationId,
759
+ lastAttemptAt: now,
760
+ lastSuccessAt: accepted ? now : previous?.lastSuccessAt,
761
+ lastOutcome: outcome,
762
+ consecutiveFailures: accepted ? 0 : (previous?.consecutiveFailures ?? 0) + 1,
763
+ ...detail.httpStatus !== void 0 ? { httpStatus: detail.httpStatus } : {},
764
+ ...detail.errorCode !== void 0 ? { errorCode: detail.errorCode } : {},
765
+ ...detail.detail !== void 0 ? { detail: truncate(detail.detail) } : {},
766
+ // A success closes the episode; a failure either opens one or continues
767
+ // the one already open. Carrying `notifiedFailingSince` across a
768
+ // continuing episode is what keeps the notice to once per outage.
769
+ ...accepted ? {} : { failingSince: previous?.failingSince ?? now },
770
+ ...accepted ? {} : previous?.notifiedFailingSince !== void 0 ? { notifiedFailingSince: previous.notifiedFailingSince } : {}
771
+ };
772
+ writeStateFile(stateFilePath, next);
773
+ return next;
774
+ }
775
+ function shouldAnnounceFailure(state) {
776
+ if (!state || state.consecutiveFailures === 0 || !state.failingSince)
777
+ return false;
778
+ return state.notifiedFailingSince !== state.failingSince;
779
+ }
780
+ function markFailureNotified(stateFilePath, state) {
781
+ if (!state.failingSince)
782
+ return;
783
+ writeStateFile(stateFilePath, { ...state, notifiedFailingSince: state.failingSince });
784
+ }
785
+ function writeStateFile(stateFilePath, state) {
786
+ const temporaryPath = `${stateFilePath}.${process.pid}.tmp`;
787
+ try {
788
+ const dir = path.dirname(stateFilePath);
789
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
790
+ fs.writeFileSync(temporaryPath, `${JSON.stringify(state, null, 2)}
791
+ `, { encoding: "utf8", mode: 384 });
792
+ fs.renameSync(temporaryPath, stateFilePath);
793
+ if (process.platform !== "win32")
794
+ fs.chmodSync(stateFilePath, 384);
795
+ } catch {
796
+ try {
797
+ if (fs.existsSync(temporaryPath))
798
+ fs.unlinkSync(temporaryPath);
799
+ } catch {
800
+ }
801
+ }
802
+ }
803
+ function truncate(value) {
804
+ const collapsed = value.replace(/\s+/g, " ").trim();
805
+ return collapsed.length > 200 ? `${collapsed.slice(0, 197)}...` : collapsed;
806
+ }
807
+ }
808
+ });
809
+
586
810
  // ../packages/tool-kit/out/eventSender.js
587
811
  var require_eventSender = __commonJS({
588
812
  "../packages/tool-kit/out/eventSender.js"(exports) {
589
813
  "use strict";
590
814
  Object.defineProperty(exports, "__esModule", { value: true });
591
815
  exports.AscendaEventSender = exports.AscendaSemanticEventError = void 0;
816
+ exports.buildEventPayload = buildEventPayload;
592
817
  var tool_contract_1 = require_out();
818
+ var eventLog_1 = require_eventLog();
593
819
  var http_1 = require_http();
594
820
  var tokenStore_1 = require_tokenStore();
821
+ var stateStore_1 = require_stateStore();
595
822
  var AscendaSemanticEventError = class extends Error {
596
823
  constructor(message) {
597
824
  super(message);
@@ -599,27 +826,32 @@ var require_eventSender = __commonJS({
599
826
  }
600
827
  };
601
828
  exports.AscendaSemanticEventError = AscendaSemanticEventError;
829
+ function buildEventPayload(identity, mapped) {
830
+ return {
831
+ toolInstallationId: identity.toolInstallationId,
832
+ source: identity.source,
833
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
834
+ sessionId: identity.sessionId ?? void 0,
835
+ workspaceHash: identity.workspaceHash ?? void 0,
836
+ consentScope: tool_contract_1.ASCENDA_CONSENT_SCOPE,
837
+ provenance: tool_contract_1.ASCENDA_PROVENANCE,
838
+ privacyMode: "metadata_only",
839
+ ...mapped,
840
+ metadata: mapped.metadata ?? {}
841
+ };
842
+ }
843
+ var DEFAULT_TIMEOUT_MS = 5e3;
844
+ var RETRY_DELAY_MS = 250;
602
845
  var AscendaEventSender2 = class {
603
846
  config;
604
847
  eventWriteToken;
848
+ lastState;
605
849
  constructor(config) {
606
850
  this.config = config;
607
851
  this.eventWriteToken = config.eventWriteToken;
608
852
  }
609
853
  async send(mapped) {
610
- const payload = {
611
- toolInstallationId: this.config.toolInstallationId,
612
- source: this.config.source,
613
- occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
614
- sessionId: this.config.sessionId ?? void 0,
615
- workspaceHash: this.config.workspaceHash ?? void 0,
616
- consentScope: tool_contract_1.ASCENDA_CONSENT_SCOPE,
617
- provenance: tool_contract_1.ASCENDA_PROVENANCE,
618
- privacyMode: "metadata_only",
619
- ...mapped,
620
- metadata: mapped.metadata ?? {}
621
- };
622
- return this.post(payload);
854
+ return this.post(buildEventPayload(this.config, mapped));
623
855
  }
624
856
  /**
625
857
  * Sends one of the six agent-observed types (dark-flow-gap-analysis §2.1).
@@ -690,32 +922,263 @@ var require_eventSender = __commonJS({
690
922
  };
691
923
  return this.post(payload);
692
924
  }
925
+ /**
926
+ * The single choke point every event passes through, and therefore the only
927
+ * honest place to journal one. Recording here rather than in each adapter is
928
+ * deliberate: Claude Code, Codex, the GitHub collector and the MCP server all
929
+ * send through this method, and the defect being fixed showed up in three
930
+ * separate components because each was left to notice its own failures.
931
+ */
693
932
  async post(payload) {
694
- let result = await (0, http_1.postToolEvent)(this.config.apiBaseUrl, this.eventWriteToken, payload, this.signal());
695
- if (result === "auth_failed") {
696
- const renewed = await this.renewEventToken();
697
- if (!renewed)
698
- return "auth_failed";
699
- result = await (0, http_1.postToolEvent)(this.config.apiBaseUrl, this.eventWriteToken, payload, this.signal());
933
+ const outcome = await this.attempt(payload);
934
+ this.lastState = (0, stateStore_1.recordSendOutcome)(this.stateFilePath(), this.config.toolInstallationId, outcome.result, {
935
+ httpStatus: outcome.httpStatus,
936
+ errorCode: outcome.errorCode,
937
+ detail: outcome.detail
938
+ });
939
+ this.log(payload, outcome.result);
940
+ return outcome.result;
941
+ }
942
+ /**
943
+ * Two recoveries, each tried once. A rejected token gets a renewal and one
944
+ * replay — rotation is expected and should heal unattended. A transport
945
+ * error gets one retry, because the common cases (a restarting instance, a
946
+ * proxy blip, a 429) clear in well under a second and the alternative is
947
+ * losing the event outright.
948
+ */
949
+ async attempt(payload) {
950
+ const outcome = await (0, http_1.postToolEvent)(this.config.apiBaseUrl, this.eventWriteToken, payload, this.signal());
951
+ if (outcome.result === "auth_failed") {
952
+ if (!await this.renewEventToken())
953
+ return outcome;
954
+ return (0, http_1.postToolEvent)(this.config.apiBaseUrl, this.eventWriteToken, payload, this.signal());
700
955
  }
701
- return result;
956
+ if (outcome.result === "transport_error" && (outcome.httpStatus === void 0 || (0, http_1.isRetryableStatus)(outcome.httpStatus))) {
957
+ await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
958
+ return (0, http_1.postToolEvent)(this.config.apiBaseUrl, this.eventWriteToken, payload, this.signal());
959
+ }
960
+ return outcome;
961
+ }
962
+ /**
963
+ * The state written by the most recent send, so a caller can decide whether
964
+ * to surface a one-time notice without re-reading the journal it just wrote.
965
+ */
966
+ get state() {
967
+ return this.lastState;
968
+ }
969
+ stateFilePath() {
970
+ return this.config.stateFilePath ?? (0, stateStore_1.defaultStateFilePath)(this.config.toolInstallationId);
971
+ }
972
+ /** Never throws: a renewal that errors is a failed renewal, not a failed turn. */
973
+ /**
974
+ * Every send path funnels through {@link post}, so semantic and
975
+ * collaboration signals are logged on the same terms as host events — the
976
+ * log would be misleading as an audit of what left the machine otherwise.
977
+ *
978
+ * An unreachable backend used to be logged as `other` from a catch block.
979
+ * It is now `transport_error` through the ordinary path, because the
980
+ * transport returns that outcome instead of throwing.
981
+ */
982
+ log(payload, delivery) {
983
+ const logFile = this.config.eventLogFile === void 0 ? (0, eventLog_1.resolveEventLogPath)() : this.config.eventLogFile;
984
+ if (!logFile)
985
+ return;
986
+ (0, eventLog_1.appendEventLog)(logFile, { loggedAt: (/* @__PURE__ */ new Date()).toISOString(), delivery, payload });
702
987
  }
703
988
  async renewEventToken() {
704
- const renewed = await (0, http_1.renewToolToken)(this.config.apiBaseUrl, this.eventWriteToken, this.signal());
705
- if (!renewed)
989
+ try {
990
+ const renewed = await (0, http_1.renewToolToken)(this.config.apiBaseUrl, this.eventWriteToken, this.signal());
991
+ if (!renewed)
992
+ return false;
993
+ this.eventWriteToken = renewed.eventWriteToken;
994
+ (0, tokenStore_1.persistEventWriteToken)(this.config.tokenFilePath, renewed.eventWriteToken);
995
+ return true;
996
+ } catch {
706
997
  return false;
707
- this.eventWriteToken = renewed.eventWriteToken;
708
- (0, tokenStore_1.persistEventWriteToken)(this.config.tokenFilePath, renewed.eventWriteToken);
709
- return true;
998
+ }
710
999
  }
711
1000
  signal() {
712
- return this.config.timeoutMs ? AbortSignal.timeout(this.config.timeoutMs) : void 0;
1001
+ return AbortSignal.timeout(this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS);
713
1002
  }
714
1003
  };
715
1004
  exports.AscendaEventSender = AscendaEventSender2;
716
1005
  }
717
1006
  });
718
1007
 
1008
+ // ../packages/tool-kit/out/hookAdapter.js
1009
+ var require_hookAdapter = __commonJS({
1010
+ "../packages/tool-kit/out/hookAdapter.js"(exports) {
1011
+ "use strict";
1012
+ Object.defineProperty(exports, "__esModule", { value: true });
1013
+ exports.DEFAULT_API_BASE_URL = void 0;
1014
+ exports.loadCliAgentConfig = loadCliAgentConfig;
1015
+ exports.deliverHookEvents = deliverHookEvents;
1016
+ var eventLog_1 = require_eventLog();
1017
+ var eventSender_1 = require_eventSender();
1018
+ var tokenStore_1 = require_tokenStore();
1019
+ exports.DEFAULT_API_BASE_URL = "https://api.ascenda.one";
1020
+ function loadCliAgentConfig(toolType, sessionIdFromHook) {
1021
+ const apiBaseUrl = (process.env.ASCENDA_API_BASE_URL ?? exports.DEFAULT_API_BASE_URL).replace(/\/$/, "");
1022
+ const toolInstallationIdRaw = process.env.ASCENDA_TOOL_INSTALLATION_ID;
1023
+ if (!toolInstallationIdRaw)
1024
+ throw new Error("Missing ASCENDA_TOOL_INSTALLATION_ID");
1025
+ const toolInstallationId = toolInstallationIdRaw.trim().includes(":") ? toolInstallationIdRaw.trim() : `${toolType}:${toolInstallationIdRaw.trim()}`;
1026
+ const tokenFilePath = process.env.ASCENDA_EVENT_WRITE_TOKEN_FILE ?? (0, tokenStore_1.defaultTokenFilePath)(toolInstallationId);
1027
+ const fileToken = (0, tokenStore_1.readTokenFile)(tokenFilePath);
1028
+ const eventWriteToken = fileToken ?? process.env.ASCENDA_EVENT_WRITE_TOKEN;
1029
+ if (!eventWriteToken)
1030
+ throw new Error("Missing ASCENDA_EVENT_WRITE_TOKEN (or token file)");
1031
+ if (!fileToken)
1032
+ (0, tokenStore_1.persistEventWriteToken)(tokenFilePath, eventWriteToken);
1033
+ return {
1034
+ apiBaseUrl,
1035
+ toolInstallationId,
1036
+ eventWriteToken,
1037
+ tokenFilePath,
1038
+ sessionId: process.env.ASCENDA_SESSION_ID ?? sessionIdFromHook ?? null,
1039
+ workspaceHash: process.env.ASCENDA_WORKSPACE_HASH ?? null,
1040
+ // Agents await command hooks; fail fast rather than stall the user's turn.
1041
+ timeoutMs: parsePositiveInt(process.env.ASCENDA_HTTP_TIMEOUT_MS) ?? 3e3
1042
+ };
1043
+ }
1044
+ async function deliverHookEvents(events, options) {
1045
+ if (events.length === 0)
1046
+ return;
1047
+ const notice = options.onNotice ?? ((message) => console.error(message));
1048
+ let config;
1049
+ try {
1050
+ config = loadCliAgentConfig(options.toolType, options.sessionId);
1051
+ } catch (error) {
1052
+ const logFile = (0, eventLog_1.resolveEventLogPath)();
1053
+ if (!logFile)
1054
+ throw error;
1055
+ for (const event of events) {
1056
+ (0, eventLog_1.appendEventLog)(logFile, {
1057
+ loggedAt: (/* @__PURE__ */ new Date()).toISOString(),
1058
+ delivery: "not_sent",
1059
+ payload: (0, eventSender_1.buildEventPayload)({
1060
+ toolInstallationId: `${options.toolType}:unpaired`,
1061
+ source: options.source,
1062
+ sessionId: options.sessionId ?? null,
1063
+ workspaceHash: process.env.ASCENDA_WORKSPACE_HASH ?? null
1064
+ }, event)
1065
+ });
1066
+ }
1067
+ return;
1068
+ }
1069
+ const sender = new eventSender_1.AscendaEventSender({
1070
+ apiBaseUrl: config.apiBaseUrl,
1071
+ toolInstallationId: config.toolInstallationId,
1072
+ source: options.source,
1073
+ eventWriteToken: config.eventWriteToken,
1074
+ tokenFilePath: config.tokenFilePath,
1075
+ sessionId: config.sessionId,
1076
+ workspaceHash: config.workspaceHash,
1077
+ timeoutMs: config.timeoutMs
1078
+ });
1079
+ for (const event of events) {
1080
+ const result = await sender.send(event);
1081
+ if (result === "accepted")
1082
+ continue;
1083
+ if (result === "consent_missing") {
1084
+ notice("Ascenda telemetry paused: renew IDE telemetry consent in the Ascenda app.");
1085
+ } else if (result === "auth_failed") {
1086
+ notice("Ascenda telemetry paused: connection revoked or expired. Re-pair via an Ascenda IDE extension or pairing-sim.");
1087
+ } else if (result === "transport_error") {
1088
+ notice("Ascenda telemetry paused: the ingest endpoint could not be reached. Your work is unaffected.");
1089
+ } else {
1090
+ notice(`Ascenda telemetry rejected: ${result}`);
1091
+ }
1092
+ return;
1093
+ }
1094
+ }
1095
+ function parsePositiveInt(value) {
1096
+ const n = Number(value);
1097
+ return Number.isInteger(n) && n > 0 ? n : void 0;
1098
+ }
1099
+ }
1100
+ });
1101
+
1102
+ // ../packages/tool-kit/out/turnState.js
1103
+ var require_turnState = __commonJS({
1104
+ "../packages/tool-kit/out/turnState.js"(exports) {
1105
+ "use strict";
1106
+ var __createBinding = exports && exports.__createBinding || (Object.create ? function(o, m, k, k2) {
1107
+ if (k2 === void 0) k2 = k;
1108
+ var desc = Object.getOwnPropertyDescriptor(m, k);
1109
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
1110
+ desc = { enumerable: true, get: function() {
1111
+ return m[k];
1112
+ } };
1113
+ }
1114
+ Object.defineProperty(o, k2, desc);
1115
+ } : function(o, m, k, k2) {
1116
+ if (k2 === void 0) k2 = k;
1117
+ o[k2] = m[k];
1118
+ });
1119
+ var __setModuleDefault = exports && exports.__setModuleDefault || (Object.create ? function(o, v) {
1120
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
1121
+ } : function(o, v) {
1122
+ o["default"] = v;
1123
+ });
1124
+ var __importStar = exports && exports.__importStar || /* @__PURE__ */ function() {
1125
+ var ownKeys = function(o) {
1126
+ ownKeys = Object.getOwnPropertyNames || function(o2) {
1127
+ var ar = [];
1128
+ for (var k in o2) if (Object.prototype.hasOwnProperty.call(o2, k)) ar[ar.length] = k;
1129
+ return ar;
1130
+ };
1131
+ return ownKeys(o);
1132
+ };
1133
+ return function(mod) {
1134
+ if (mod && mod.__esModule) return mod;
1135
+ var result = {};
1136
+ if (mod != null) {
1137
+ for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
1138
+ }
1139
+ __setModuleDefault(result, mod);
1140
+ return result;
1141
+ };
1142
+ }();
1143
+ Object.defineProperty(exports, "__esModule", { value: true });
1144
+ exports.recordTurnStart = recordTurnStart;
1145
+ exports.consumeTurnDurationMs = consumeTurnDurationMs;
1146
+ var fs = __importStar(__require("fs"));
1147
+ var os = __importStar(__require("os"));
1148
+ var path = __importStar(__require("path"));
1149
+ var stateDir = () => process.env.ASCENDA_STATE_DIR ?? path.join(os.homedir(), ".ascenda", "state");
1150
+ function turnFile(agent, sessionId) {
1151
+ return path.join(stateDir(), `${sanitize(agent)}-turn-${sanitize(sessionId)}`);
1152
+ }
1153
+ function sanitize(value) {
1154
+ return value.replace(/[^a-zA-Z0-9._-]/g, "_");
1155
+ }
1156
+ function recordTurnStart(agent, sessionId, now = Date.now()) {
1157
+ if (!sessionId)
1158
+ return;
1159
+ try {
1160
+ fs.mkdirSync(stateDir(), { recursive: true });
1161
+ fs.writeFileSync(turnFile(agent, sessionId), String(now), { encoding: "utf8", mode: 384 });
1162
+ } catch {
1163
+ }
1164
+ }
1165
+ function consumeTurnDurationMs(agent, sessionId, now = Date.now()) {
1166
+ if (!sessionId)
1167
+ return void 0;
1168
+ const file = turnFile(agent, sessionId);
1169
+ try {
1170
+ const started = Number(fs.readFileSync(file, "utf8").trim());
1171
+ fs.rmSync(file, { force: true });
1172
+ if (!Number.isFinite(started) || started <= 0 || started > now)
1173
+ return void 0;
1174
+ return now - started;
1175
+ } catch {
1176
+ return void 0;
1177
+ }
1178
+ }
1179
+ }
1180
+ });
1181
+
719
1182
  // ../packages/tool-kit/out/salt.js
720
1183
  var require_salt = __commonJS({
721
1184
  "../packages/tool-kit/out/salt.js"(exports) {
@@ -930,7 +1393,8 @@ var require_out2 = __commonJS({
930
1393
  "../packages/tool-kit/out/index.js"(exports) {
931
1394
  "use strict";
932
1395
  Object.defineProperty(exports, "__esModule", { value: true });
933
- exports.parseIngestResponse = exports.postToolEventsBatch = exports.postToolEvent = exports.renewToolToken = exports.getPairingStatus = exports.createPairingSession = exports.AscendaApiError = exports.liveBusSocketCandidates = exports.liveBusSocketPath = exports.bucketPromptSize = exports.emitLiveSignal = exports.hashWithMachineSalt = exports.readOrCreateMachineSalt = exports.machineSaltFilePath = exports.readTokenFile = exports.persistEventWriteToken = exports.defaultTokenFilePath = exports.AscendaSemanticEventError = exports.AscendaEventSender = exports.looksLikeCorrection = exports.outcomeForHook = exports.inferOutcome = exports.getNestedNumber = exports.getNestedString = exports.getNested = exports.getNumber = exports.getString = exports.isAfterHours = exports.bucketDurationMs = exports.bucketLinesChanged = exports.invitesDebrief = exports.classifyWorkMilestone = exports.isReworkGitAction = exports.classifyGitAction = exports.isVerificationCommand = exports.classifyCommand = void 0;
1396
+ exports.postToolEventsBatch = exports.postToolEvent = exports.renewToolToken = exports.getPairingStatus = exports.createPairingSession = exports.AscendaApiError = exports.liveBusSocketCandidates = exports.liveBusSocketPath = exports.bucketPromptSize = exports.emitLiveSignal = exports.hashWithMachineSalt = exports.readOrCreateMachineSalt = exports.machineSaltFilePath = exports.markFailureNotified = exports.shouldAnnounceFailure = exports.recordSendOutcome = exports.readCollectorState = exports.defaultStateFilePath = exports.readTokenFile = exports.persistEventWriteToken = exports.defaultTokenFilePath = exports.recordTurnStart = exports.consumeTurnDurationMs = exports.loadCliAgentConfig = exports.deliverHookEvents = exports.DEFAULT_API_BASE_URL = exports.resolveEventLogPath = exports.expandUserPath = exports.appendEventLog = exports.EVENT_LOG_ENV_VAR = exports.buildEventPayload = exports.AscendaSemanticEventError = exports.AscendaEventSender = exports.looksLikeCorrection = exports.outcomeForHook = exports.inferOutcome = exports.getNestedNumber = exports.getNestedString = exports.getNested = exports.getNumber = exports.getString = exports.isAfterHours = exports.bucketDurationMs = exports.bucketLinesChanged = exports.invitesDebrief = exports.classifyWorkMilestone = exports.isReworkGitAction = exports.classifyGitAction = exports.isVerificationCommand = exports.classifyCommand = void 0;
1397
+ exports.isRetryableStatus = exports.parseIngestResponse = void 0;
934
1398
  var commandClassifier_1 = require_commandClassifier();
935
1399
  Object.defineProperty(exports, "classifyCommand", { enumerable: true, get: function() {
936
1400
  return commandClassifier_1.classifyCommand;
@@ -995,6 +1459,39 @@ var require_out2 = __commonJS({
995
1459
  Object.defineProperty(exports, "AscendaSemanticEventError", { enumerable: true, get: function() {
996
1460
  return eventSender_1.AscendaSemanticEventError;
997
1461
  } });
1462
+ Object.defineProperty(exports, "buildEventPayload", { enumerable: true, get: function() {
1463
+ return eventSender_1.buildEventPayload;
1464
+ } });
1465
+ var eventLog_1 = require_eventLog();
1466
+ Object.defineProperty(exports, "EVENT_LOG_ENV_VAR", { enumerable: true, get: function() {
1467
+ return eventLog_1.EVENT_LOG_ENV_VAR;
1468
+ } });
1469
+ Object.defineProperty(exports, "appendEventLog", { enumerable: true, get: function() {
1470
+ return eventLog_1.appendEventLog;
1471
+ } });
1472
+ Object.defineProperty(exports, "expandUserPath", { enumerable: true, get: function() {
1473
+ return eventLog_1.expandUserPath;
1474
+ } });
1475
+ Object.defineProperty(exports, "resolveEventLogPath", { enumerable: true, get: function() {
1476
+ return eventLog_1.resolveEventLogPath;
1477
+ } });
1478
+ var hookAdapter_1 = require_hookAdapter();
1479
+ Object.defineProperty(exports, "DEFAULT_API_BASE_URL", { enumerable: true, get: function() {
1480
+ return hookAdapter_1.DEFAULT_API_BASE_URL;
1481
+ } });
1482
+ Object.defineProperty(exports, "deliverHookEvents", { enumerable: true, get: function() {
1483
+ return hookAdapter_1.deliverHookEvents;
1484
+ } });
1485
+ Object.defineProperty(exports, "loadCliAgentConfig", { enumerable: true, get: function() {
1486
+ return hookAdapter_1.loadCliAgentConfig;
1487
+ } });
1488
+ var turnState_1 = require_turnState();
1489
+ Object.defineProperty(exports, "consumeTurnDurationMs", { enumerable: true, get: function() {
1490
+ return turnState_1.consumeTurnDurationMs;
1491
+ } });
1492
+ Object.defineProperty(exports, "recordTurnStart", { enumerable: true, get: function() {
1493
+ return turnState_1.recordTurnStart;
1494
+ } });
998
1495
  var tokenStore_1 = require_tokenStore();
999
1496
  Object.defineProperty(exports, "defaultTokenFilePath", { enumerable: true, get: function() {
1000
1497
  return tokenStore_1.defaultTokenFilePath;
@@ -1005,6 +1502,22 @@ var require_out2 = __commonJS({
1005
1502
  Object.defineProperty(exports, "readTokenFile", { enumerable: true, get: function() {
1006
1503
  return tokenStore_1.readTokenFile;
1007
1504
  } });
1505
+ var stateStore_1 = require_stateStore();
1506
+ Object.defineProperty(exports, "defaultStateFilePath", { enumerable: true, get: function() {
1507
+ return stateStore_1.defaultStateFilePath;
1508
+ } });
1509
+ Object.defineProperty(exports, "readCollectorState", { enumerable: true, get: function() {
1510
+ return stateStore_1.readCollectorState;
1511
+ } });
1512
+ Object.defineProperty(exports, "recordSendOutcome", { enumerable: true, get: function() {
1513
+ return stateStore_1.recordSendOutcome;
1514
+ } });
1515
+ Object.defineProperty(exports, "shouldAnnounceFailure", { enumerable: true, get: function() {
1516
+ return stateStore_1.shouldAnnounceFailure;
1517
+ } });
1518
+ Object.defineProperty(exports, "markFailureNotified", { enumerable: true, get: function() {
1519
+ return stateStore_1.markFailureNotified;
1520
+ } });
1008
1521
  var salt_1 = require_salt();
1009
1522
  Object.defineProperty(exports, "machineSaltFilePath", { enumerable: true, get: function() {
1010
1523
  return salt_1.machineSaltFilePath;
@@ -1050,6 +1563,9 @@ var require_out2 = __commonJS({
1050
1563
  Object.defineProperty(exports, "parseIngestResponse", { enumerable: true, get: function() {
1051
1564
  return http_1.parseIngestResponse;
1052
1565
  } });
1566
+ Object.defineProperty(exports, "isRetryableStatus", { enumerable: true, get: function() {
1567
+ return http_1.isRetryableStatus;
1568
+ } });
1053
1569
  }
1054
1570
  });
1055
1571
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "publishConfig": { "access": "public" },
3
3
  "name": "@ascenda-one/github-collector",
4
- "version": "0.1.8",
4
+ "version": "0.1.10",
5
5
  "description": "Code-forge collaboration collector for Ascenda work telemetry — first-person review and pull-request signals.",
6
6
  "repository": {
7
7
  "type": "git",