pinglet-cli 0.6.1 → 0.6.2

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.
@@ -102,6 +102,10 @@ function rotateSpinner(state, now) {
102
102
  visibleMs: prev.visibleMs,
103
103
  });
104
104
  prev.visibleMs = 0; // A failed replacement must not account this interval twice.
105
+ // The event is already on disk. Persist the reset now: if arming below throws
106
+ // (e.g. settings.json is temporarily unparseable) the tick ends before the
107
+ // final saveState and the next tick would settle the same interval again.
108
+ (0, cache_1.saveState)(state);
105
109
  }
106
110
  const message = (0, picker_1.pickMessage)((0, cache_1.loadFeedMessages)(), state);
107
111
  if (!message) {
@@ -82,17 +82,45 @@ function enqueueNotification(args) {
82
82
  }
83
83
  }
84
84
  }
85
- /** Caller holds the short state lock. Failed work stays on disk for retry. */
85
+ const MAX_NOTIFICATION_AGE_MS = 60 * 60_000;
86
+ /** Caller holds the short state lock. Failed work stays on disk for retry.
87
+ * One unreadable or stale file must never stop the hook: unparseable files are
88
+ * discarded (their notification cannot be recovered), a turn-complete notice
89
+ * older than an hour is useless and discarded, and a delivery failure keeps
90
+ * its file for the next drain without affecting the other files.
91
+ */
86
92
  function drainNotifications() {
87
93
  if (!fs.existsSync(config_1.CONFIG_PATH))
88
94
  return;
89
95
  const config = (0, config_1.loadConfig)();
90
96
  for (const name of fs.readdirSync(config_1.PINGLET_DIR).filter(name => name.startsWith(PREFIX) && name.endsWith(".json"))) {
91
97
  const file = path.join(config_1.PINGLET_DIR, name);
92
- const event = JSON.parse(fs.readFileSync(file, "utf8"));
93
- if (config.adapters.codex?.installedAt === event.installedAt && config.apiBaseUrl === event.apiBaseUrl) {
94
- (0, notify_1.runNotify)([JSON.stringify({ type: event.type })], "evt_" + name.slice(PREFIX.length, -5));
98
+ let event = null;
99
+ let stale = false;
100
+ try {
101
+ stale = Date.now() - fs.statSync(file).mtimeMs > MAX_NOTIFICATION_AGE_MS;
102
+ const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
103
+ if (parsed && typeof parsed === "object" && typeof parsed.type === "string")
104
+ event = parsed;
105
+ }
106
+ catch (error) {
107
+ if ((0, storage_1.isMissing)(error))
108
+ continue; // Removed concurrently (purge/uninstall).
109
+ }
110
+ if (event && !stale && config.adapters.codex?.installedAt === event.installedAt && config.apiBaseUrl === event.apiBaseUrl) {
111
+ try {
112
+ (0, notify_1.runNotify)([JSON.stringify({ type: event.type })], "evt_" + name.slice(PREFIX.length, -5));
113
+ }
114
+ catch {
115
+ continue; // Keep the file; the same eventId is reused on retry.
116
+ }
117
+ }
118
+ try {
119
+ fs.unlinkSync(file);
120
+ }
121
+ catch (error) {
122
+ if (!(0, storage_1.isMissing)(error))
123
+ throw error;
95
124
  }
96
- fs.unlinkSync(file); // Stale installation notifications must not survive reinstall.
97
125
  }
98
126
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinglet-cli",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "Short messages from other developers shown in your Claude Code spinner while the AI thinks — zero tokens, never reads your code. Claude Code / Codex integration",
5
5
  "license": "MIT",
6
6
  "repository": {