@dench.com/cli 2.2.3 → 2.2.4

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/ads.ts +63 -47
  2. package/package.json +1 -1
package/ads.ts CHANGED
@@ -936,9 +936,16 @@ function spinnerVerbFromCache(): string | null {
936
936
  function refreshClaudeSpinnerVerb(): void {
937
937
  if (!claudeInstalled()) return;
938
938
  const verb = spinnerVerbFromCache();
939
- if (!verb) return;
940
939
  const settings = readJson<Record<string, unknown>>(CLAUDE_SETTINGS, {});
941
- settings.spinnerVerbs = { mode: "replace", verbs: [verb] };
940
+ if (verb) {
941
+ settings.spinnerVerbs = { mode: "replace", verbs: [verb] };
942
+ } else if (settings.spinnerVerbs) {
943
+ // No live ad → drop the sponsored thinking verb so Claude Code stops
944
+ // showing a stale ad on every spin.
945
+ delete settings.spinnerVerbs;
946
+ } else {
947
+ return;
948
+ }
942
949
  atomicWrite(CLAUDE_SETTINGS, `${JSON.stringify(settings, null, 2)}\n`);
943
950
  }
944
951
 
@@ -1138,47 +1145,26 @@ function shellSnippet(
1138
1145
  ): string {
1139
1146
  if (kind === "fish") {
1140
1147
  return `${SHELL_MARK_START}
1141
- # Prints one sponsored line above each prompt shows in any terminal
1142
- # (Codex, Cursor, openclaw, hermes, …). Remove this block to disable.
1143
- function _dench_ads
1144
- set -l line (${nodeBin()} ${RENDER_SCRIPT} 2>/dev/null | string collect)
1145
- test -n "$line"; and printf '%s\\n' "$line"
1146
- end
1147
- if not functions -q _dench_ads_original_prompt
1148
- if functions -q fish_prompt
1149
- functions -c fish_prompt _dench_ads_original_prompt
1150
- else if functions -q fish_default_prompt
1151
- functions -c fish_default_prompt _dench_ads_original_prompt
1152
- end
1153
- end
1154
- if functions -q _dench_ads_original_prompt
1155
- function fish_prompt
1156
- _dench_ads
1157
- _dench_ads_original_prompt
1158
- end
1159
- else
1160
- function fish_prompt
1161
- _dench_ads
1162
- end
1148
+ # Prints one sponsored line ONCE when a new interactive shell opens (not on
1149
+ # every prompt) — shows in any terminal (Codex, Cursor, plain shell, …).
1150
+ # Remove this block to disable.
1151
+ if status is-interactive
1152
+ set -l _dench_line (${nodeBin()} ${RENDER_SCRIPT} 2>/dev/null | string collect)
1153
+ test -n "$_dench_line"; and printf '%s\\n' "$_dench_line"
1163
1154
  end
1164
1155
  ${SHELL_MARK_END}`;
1165
1156
  }
1166
1157
  return `${SHELL_MARK_START}
1167
- # Prints one sponsored line above each prompt shows in any terminal
1168
- # (Codex, Cursor, openclaw, hermes, …). Remove this block to disable.
1169
- _dench_ads() {
1170
- local line
1171
- line="$(${nodeBin()} ${RENDER_SCRIPT} 2>/dev/null)"
1172
- [ -n "$line" ] && printf '%s\\n' "$line"
1173
- }
1174
- if [ -n "$ZSH_VERSION" ]; then
1175
- autoload -Uz add-zsh-hook 2>/dev/null && add-zsh-hook precmd _dench_ads
1176
- elif [ -n "$BASH_VERSION" ]; then
1177
- case "$PROMPT_COMMAND" in
1178
- *_dench_ads*) ;;
1179
- *) PROMPT_COMMAND="_dench_ads;$PROMPT_COMMAND" ;;
1180
- esac
1181
- fi
1158
+ # Prints one sponsored line ONCE when a new interactive shell opens (not on
1159
+ # every prompt) — shows in any terminal (Codex, Cursor, plain shell, …).
1160
+ # Remove this block to disable.
1161
+ case "$-" in
1162
+ *i*)
1163
+ _dench_line="$(${nodeBin()} ${RENDER_SCRIPT} 2>/dev/null)"
1164
+ [ -n "$_dench_line" ] && printf '%s\\n' "$_dench_line"
1165
+ unset _dench_line
1166
+ ;;
1167
+ esac
1182
1168
  ${SHELL_MARK_END}`;
1183
1169
  }
1184
1170
 
@@ -1595,7 +1581,7 @@ async function refresh(authToken?: string): Promise<string> {
1595
1581
  saveAdSource(); // let the detached background worker know where to refetch
1596
1582
  let ad: Ad | null = null;
1597
1583
  let flushed = 0;
1598
- let src: "live" | "demo" = "demo";
1584
+ let src: "live" | "demo" | "none" = "none";
1599
1585
  let keepExistingCache = false;
1600
1586
  if (endpoint) {
1601
1587
  const previousAd = loadAd({ allowExpired: true });
@@ -1604,14 +1590,33 @@ async function refresh(authToken?: string): Promise<string> {
1604
1590
  const fetched = await fetchAd(endpoint);
1605
1591
  ad = fetched.ad;
1606
1592
  flushed = await flushImpressions(endpoint, authToken, ad ?? previousAd);
1607
- if (ad) src = "live";
1608
- else if (previousAd) {
1609
- ad = previousAd;
1610
- src = isDemoAdId(previousAd.id) ? "demo" : "live";
1611
- keepExistingCache = true;
1593
+ if (ad) {
1594
+ src = "live";
1595
+ } else if (previousAd && !isDemoAdId(previousAd.id)) {
1596
+ // The endpoint had no campaign this poll — keep a recently-live creative
1597
+ // briefly so the line doesn't flicker between fetches, but ONLY if it
1598
+ // hasn't expired. Past expiry with no live campaign → clear it (below).
1599
+ if (previousAd.expiresAt && Date.now() <= previousAd.expiresAt) {
1600
+ ad = previousAd;
1601
+ src = "live";
1602
+ keepExistingCache = true;
1603
+ }
1612
1604
  }
1605
+ // CRITICAL: when pointed at a real endpoint that returns NO campaign, show
1606
+ // NOTHING. Never fall back to bundled demo ads in production — that would
1607
+ // display a fake "Linear"/"Vercel" line nobody paid for. Demo ads are only
1608
+ // for the no-endpoint (offline/local) path below.
1609
+ if (!ad) {
1610
+ clearAdCache();
1611
+ refreshClaudeSpinnerVerb();
1612
+ return "No live campaign — nothing to show.";
1613
+ }
1614
+ } else {
1615
+ // No endpoint configured at all (offline/dev) → bundled demo rotation so
1616
+ // the feature is demonstrable with zero backend.
1617
+ ad = pickDemoAd();
1618
+ src = "demo";
1613
1619
  }
1614
- if (!ad) ad = pickDemoAd();
1615
1620
  if (!keepExistingCache) {
1616
1621
  ad.expiresAt = Date.now() + DEFAULT_TTL_MS;
1617
1622
  atomicWrite(AD_CACHE, JSON.stringify(ad));
@@ -1622,6 +1627,17 @@ async function refresh(authToken?: string): Promise<string> {
1622
1627
  return `Cached ${src} ad: ${ad.label}${flushMsg}`;
1623
1628
  }
1624
1629
 
1630
+ // Remove the cached ad so the hooks/render print nothing when no campaign is
1631
+ // live. Best-effort; the render script already treats a missing cache as
1632
+ // "print nothing".
1633
+ function clearAdCache(): void {
1634
+ try {
1635
+ if (existsSync(AD_CACHE)) unlinkSync(AD_CACHE);
1636
+ } catch {
1637
+ // best-effort
1638
+ }
1639
+ }
1640
+
1625
1641
  // ---------------------------------------------------------------------------
1626
1642
  // status
1627
1643
  // ---------------------------------------------------------------------------
@@ -1998,7 +2014,7 @@ Usage:
1998
2014
  claude → Claude Code's official statusLine hook (~/.claude/settings.json)
1999
2015
  codex → Codex CLI lifecycle hooks (~/.codex/hooks.json); shows the
2000
2016
  line as a systemMessage on SessionStart + after each turn
2001
- shell → a precmd line for any terminal (zsh/bash/fish); needs --apply
2017
+ shell → one line at shell startup, any terminal (zsh/bash/fish); needs --apply
2002
2018
  agentmd → append an instruction block to ./CLAUDE.md and ./AGENTS.md
2003
2019
  so the agent runs \`dench ads show\` during its turn
2004
2020
  all → all of the above
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dench.com/cli",
3
- "version": "2.2.3",
3
+ "version": "2.2.4",
4
4
  "description": "Dench agent workspace CLI. v2 unifies auth behind `dench signin`; the legacy `dench login` / `dench onboard` / `dench setup` / `dench what-can-i-do` / `dench register` commands now error with a redirect.",
5
5
  "type": "module",
6
6
  "bin": {