newsline-cli 0.1.2 → 0.1.3

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/newsline CHANGED
@@ -250,7 +250,7 @@ PY
250
250
 
251
251
  # apply the new language/topic on the next render, not after the old cache expires
252
252
  local cache="${NEWSLINE_CACHE:-$HOME/.cache/newsline}"
253
- rm -f "$cache/line" "$cache/feeds.resolved.json" "$cache/feeds.resolved.ok" 2>/dev/null
253
+ rm -f "$cache/line" "$cache/feeds.resolved.json" "$cache/feeds.resolved.ok" "$cache/notice" 2>/dev/null
254
254
 
255
255
  echo "✔ newsline installed (lang=$lang, topic=$topic)."
256
256
  [ -n "$base" ] && echo " composing with your existing status line — it stays visible above the news."
@@ -292,7 +292,7 @@ else:
292
292
  print("status line not managed by newsline; left as-is")
293
293
  PY
294
294
  local cache="${NEWSLINE_CACHE:-$HOME/.cache/newsline}"
295
- rm -f "$CONFIG" "$cache/line" "$cache/feeds.resolved.json" "$cache/feeds.resolved.ok" 2>/dev/null
295
+ rm -f "$CONFIG" "$cache/line" "$cache/feeds.resolved.json" "$cache/feeds.resolved.ok" "$cache/notice" 2>/dev/null
296
296
  echo "✔ newsline uninstalled."
297
297
  }
298
298
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "newsline-cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Locale-aware one-line news in your Claude Code status line — local, composes with your existing status line.",
5
5
  "bin": {
6
6
  "newsline": "newsline"
package/refresh.sh CHANGED
@@ -16,6 +16,9 @@ FEEDS="${NEWSLINE_FEEDS:-$HERE/feeds.json}"
16
16
  # through it so monetization can be switched on later WITHOUT re-shipping.
17
17
  ENDPOINT="${NEWSLINE_ENDPOINT:-https://newsline.thesockerrr.workers.dev/r}"
18
18
  COUNT="${NEWSLINE_COUNT:-15}" # how many headlines to cache for rotation
19
+ # Client version, sent to /feed as ?v= for the server-side update nudge.
20
+ # Bump together with package.json when tagging a release.
21
+ export NEWSLINE_VERSION="${NEWSLINE_VERSION:-0.1.0}"
19
22
  LOCK="$CACHE_DIR/refresh.lock"
20
23
  mkdir -p "$CACHE_DIR" 2>/dev/null
21
24
 
@@ -82,6 +85,34 @@ if [ "$need_resolve" = "1" ]; then
82
85
  fi
83
86
  fi
84
87
 
88
+ # --- server-driven update nudge: one extra line below the news --------------
89
+ # If the server flagged this client as too old (RESOLVED has "_update"),
90
+ # write the notice line statusline.sh appends; otherwise clear it. Skipped
91
+ # (cleared) when this client already meets minVersion, so the nudge stops
92
+ # right after an update even while the old RESOLVED is still cached.
93
+ NOTICE="$CACHE_DIR/notice"
94
+ msg=$(python3 -c '
95
+ import json, os, sys
96
+ try:
97
+ u = json.load(open(sys.argv[1])).get("_update") or {}
98
+ except Exception:
99
+ u = {}
100
+ def pv(s): return [int(x) if x.isdigit() else 0 for x in str(s).split(".")]
101
+ mv, cur = u.get("minVersion", ""), os.environ.get("NEWSLINE_VERSION", "")
102
+ print("" if (mv and cur and pv(cur) >= pv(mv)) else u.get("message", ""))
103
+ ' "$RESOLVED" 2>/dev/null | tr -d '\000-\037')
104
+ if [ -n "$msg" ]; then
105
+ case "$HERE" in # install channel by where we live
106
+ *"/node_modules/"*) ucmd="npm i -g newsline-cli" ;;
107
+ *[Cc]ellar*|*linuxbrew*) ucmd="brew upgrade newsline" ;;
108
+ *) ucmd="curl -fsSL https://raw.githubusercontent.com/itdar/cc-plugin/master/install.sh | sh" ;;
109
+ esac
110
+ # yellow line, command in bold bright yellow — mirrors Claude Code's own update nudge
111
+ printf '\033[33m[%s] Run: \033[1;93m%s\033[0m\n' "$msg" "$ucmd" > "$NOTICE.tmp" && mv -f "$NOTICE.tmp" "$NOTICE"
112
+ else
113
+ rm -f "$NOTICE" 2>/dev/null
114
+ fi
115
+
85
116
  # --- fetch + parse + build the line; write atomically, keep old cache on failure ---
86
117
  if python3 "$HERE/fetch.py" "$lang" "$RESOLVED" "$ENDPOINT" "$COUNT" > "$CACHE_DIR/line.tmp" 2>/dev/null \
87
118
  && [ -s "$CACHE_DIR/line.tmp" ]; then
package/resolve.py CHANGED
@@ -2,9 +2,11 @@
2
2
  """newsline · resolve which feeds to use — server-first, local fallback, topic-aware.
3
3
 
4
4
  Tries the operator's curation API (NEWSLINE_API) with a short timeout, sending only
5
- COARSE CONTEXT (lang, country, localtime, dow, tz, topic) — never personal data. On
6
- ANY failure it falls back to the bundled feeds.json. If a topic preference is set
7
- (NEWSLINE_TOPIC), a keyless Google News topic feed is prepended so that topic leads.
5
+ COARSE CONTEXT (lang, country, localtime, dow, tz, topic) — never personal data.
6
+ The server's list is used AS-IS (it handles topic: vertical partners first, then
7
+ Google's topic feed). Only when the server is unreachable does the local fallback
8
+ kick in: bundled feeds.json, with a keyless Google News topic feed prepended if a
9
+ topic preference is set (NEWSLINE_TOPIC) so the topic still leads offline.
8
10
 
9
11
  Prints a feeds.json-shaped object fetch.py consumes: {"<lang>": [...], "default": [...]}
10
12
 
@@ -62,6 +64,7 @@ def local_feeds(lang, path):
62
64
 
63
65
 
64
66
  def server_feeds(lang, api_base):
67
+ """Return (feeds_or_None, update_or_None) from the curation API."""
65
68
  params = {
66
69
  "lang": lang,
67
70
  "country": os.environ.get("NEWSLINE_COUNTRY", ""),
@@ -69,18 +72,22 @@ def server_feeds(lang, api_base):
69
72
  "dow": os.environ.get("NEWSLINE_DOW", ""),
70
73
  "tz": os.environ.get("NEWSLINE_TZ", ""),
71
74
  "topic": os.environ.get("NEWSLINE_TOPIC", ""),
75
+ "v": os.environ.get("NEWSLINE_VERSION", ""),
72
76
  }
73
77
  url = api_base.rstrip("/") + "/feed?" + urllib.parse.urlencode(params)
74
78
  req = urllib.request.Request(url, headers={"User-Agent": "newsline/0.1"})
75
79
  obj = json.loads(urllib.request.urlopen(req, timeout=TIMEOUT).read())
80
+ update = obj.get("update")
81
+ if not isinstance(update, dict):
82
+ update = None
76
83
  feeds = obj.get("feeds")
77
84
  if isinstance(feeds, list):
78
85
  # fetch.py urlopens these — accept only http(s) URLs
79
86
  feeds = [x for x in feeds
80
87
  if isinstance(x, str) and x.startswith(("http://", "https://"))]
81
- if feeds:
82
- return feeds
83
- return None
88
+ else:
89
+ feeds = None
90
+ return (feeds or None), update
84
91
 
85
92
 
86
93
  def main():
@@ -91,22 +98,26 @@ def main():
91
98
 
92
99
  local_urls, default = local_feeds(lang, local_path)
93
100
 
94
- feeds = None
101
+ feeds = update = None
95
102
  if api_base:
96
103
  try:
97
- feeds = server_feeds(lang, api_base) # server-first
104
+ feeds, update = server_feeds(lang, api_base) # server-first, used AS-IS
98
105
  except Exception:
99
- feeds = None # any issue -> local fallback
100
- feeds = feeds or local_urls or default
101
-
102
- # topic preference leads (keyless Google News topic feed), general as backup
103
- tfeed = topic_feed(lang, os.environ.get("NEWSLINE_COUNTRY", ""), os.environ.get("NEWSLINE_TOPIC", ""))
104
- if tfeed:
105
- feeds = [tfeed] + [f for f in feeds if f != tfeed]
106
+ feeds, update = None, None # any issue -> local fallback
107
+ if not feeds:
108
+ # server unreachable: bundled feeds, topic preference prepended locally
109
+ feeds = local_urls or default
110
+ tfeed = topic_feed(lang, os.environ.get("NEWSLINE_COUNTRY", ""),
111
+ os.environ.get("NEWSLINE_TOPIC", ""))
112
+ if tfeed:
113
+ feeds = [tfeed] + [f for f in feeds if f != tfeed]
106
114
 
107
115
  if not feeds:
108
116
  return 1
109
- sys.stdout.write(json.dumps({lang: feeds, "default": default or feeds}, ensure_ascii=False))
117
+ out = {lang: feeds, "default": default or feeds}
118
+ if update:
119
+ out["_update"] = update # refresh.sh turns this into the notice line
120
+ sys.stdout.write(json.dumps(out, ensure_ascii=False))
110
121
  return 0
111
122
 
112
123
 
package/statusline.sh CHANGED
@@ -42,3 +42,10 @@ if [ -s "$CACHE_FILE" ]; then
42
42
  else
43
43
  printf '📰 …' # first run, before the first refresh lands
44
44
  fi
45
+
46
+ # --- server-driven update nudge (written/cleared by refresh.sh), below the news ---
47
+ NOTICE_FILE="$CACHE_DIR/notice"
48
+ if [ -s "$NOTICE_FILE" ]; then
49
+ [ -s "$CACHE_FILE" ] || printf '\n' # placeholder path prints without a newline
50
+ head -1 "$NOTICE_FILE"
51
+ fi