newsline-cli 0.1.1 → 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/README.md CHANGED
@@ -1,86 +1,56 @@
1
- # newsline
1
+ <div align="center">
2
2
 
3
- Locale-aware one-line news in your Claude Code status line **fully local**, and it
4
- **composes with your existing status line** (e.g. an HUD) instead of replacing it.
3
+ **English** · [한국어](readme/ko.md) · [日本語](readme/ja.md) · [Español](readme/es.md) · [Français](readme/fr.md) · [Deutsch](readme/de.md) · [Português](readme/pt.md) · [中文](readme/zh.md)
5
4
 
6
- **English** · [한국어](readme/ko.md)
5
+ </div>
7
6
 
8
- ## What it is
7
+ # newsline
9
8
 
10
- A Claude Code status-line utility that shows a rotating, region-appropriate news
11
- headline at the bottom of your session. Everything runs locally on your machine
12
- locale detection, feed fetch, parsing, caching, rendering. It never reads your code,
13
- prompts, files, or Claude conversations.
9
+ **Waiting for Claude Code to finish? Read the latest one-line news right in your status line.**
10
+ A rotating regional headline sits at the bottom of your session so a long wait turns into a
11
+ quick news check. It shows *below* your existing status line (your HUD stays).
14
12
 
15
- ## Install
13
+ ## Install & run — one line
16
14
 
17
- **curl | sh** (macOS / Linux / WSL):
18
15
  ```sh
16
+ # curl (macOS / Linux / WSL) — installs and sets up right away
19
17
  curl -fsSL https://raw.githubusercontent.com/itdar/cc-plugin/master/install.sh | sh
20
- ```
21
18
 
22
- **Homebrew:**
23
- ```sh
24
- brew install itdar/tap/newsline
25
- ```
19
+ # Homebrew
20
+ brew install itdar/tap/newsline && newsline init
26
21
 
27
- **From source:**
28
- ```sh
29
- git clone https://github.com/itdar/cc-plugin && cd cc-plugin && ./install.sh
22
+ # npm
23
+ npm i -g newsline-cli && newsline init
30
24
  ```
31
25
 
32
- The **curl | sh** installer runs setup automaticallyit prompts you for a language and
33
- topic, then wires the status line. For **Homebrew** or **from source**, run setup yourself:
34
- ```sh
35
- newsline init
36
- ```
37
- `init` asks for a language and topic (a menu — just pick a number), then registers the
38
- status line, **keeping whatever status line you already have** (your HUD stays visible,
39
- news shows on the line below). It appears on your next message to Claude — Claude Code
40
- hot-reloads settings, so no restart is needed.
26
+ The news line appears on your **next message**no restart. Setup asks for a language & topic
27
+ and keeps your existing status line.
28
+
29
+ ## What it does
41
30
 
42
- _(Skip auto-setup during install with `NEWSLINE_NO_INIT=1`.)_
31
+ - **Your machine fetches and shows the news** — it never touches your code, prompts, files, or
32
+ Claude conversations.
33
+ - A small **edge service** picks the best regional sources and routes headline clicks, so sources
34
+ stay fresh without reinstalling. If it's unreachable, newsline **falls back to built-in feeds**.
35
+ - The status line is instant (served from a cache); refreshes run in the background.
43
36
 
44
37
  ## Configure
45
38
 
46
- Choose during `newsline init`, or edit `~/.config/newsline/config.json` (or set the env var):
39
+ Re-run `newsline init`, or edit `~/.config/newsline/config.json`:
47
40
 
48
- | Key / env var | Default | Meaning |
41
+ | Key | Default | Meaning |
49
42
  |---|---|---|
50
- | `lang` / `NEWSLINE_LANG` | `auto` | `ko` `ja` `en` `es` `fr` `de` `pt` `zh`, or `auto` |
51
- | `topic` / `NEWSLINE_TOPIC` | `general` | `general` `tech` `business` `world` `sports` `science` `health` `entertainment` |
52
- | `rotate` / `NEWSLINE_ROTATE` | `6` | seconds per headline |
53
- | `count` / `NEWSLINE_COUNT` | `15` | headlines cached for rotation |
54
- | `maxlen` / `NEWSLINE_MAXLEN` | `120` | max chars; `max` = no truncation; `NN%` ≈ of terminal width |
55
- | `icon` / `NEWSLINE_ICON` | `📰` | leading glyph; `none` to remove |
56
- | `linkhint` / `NEWSLINE_LINKHINT` | off | append a `↗` "opens externally" hint |
57
- | `api` / `NEWSLINE_API` | — | optional curation API (server-first, local fallback) |
58
- | `endpoint` / `NEWSLINE_ENDPOINT` | — | click-redirect wrapper |
59
-
60
- ## How it works
61
-
62
- - **`newsline render`** (what the status line runs) prints instantly from cache and never
63
- blocks; it kicks off a background refresh when the cache is stale.
64
- - **`newsline refresh`** fetches a keyless regional RSS/Atom feed locally, parses the top
65
- headlines, dedupes, and caches them.
66
- - Headlines are clickable [OSC 8] links. Whether a plain click vs `Cmd`/`Ctrl`+click opens
67
- them is decided by your terminal, not by newsline.
43
+ | `lang` | `auto` | `ko` `ja` `en` `es` `fr` `de` `pt` `zh`, or `auto` |
44
+ | `topic` | `general` | `general` `tech` `business` `world` `sports` `science` `health` `entertainment` |
45
+ | `rotate` | `6` | seconds per headline |
46
+ | `count` | `15` | headlines in rotation |
47
+ | `maxlen` | `120` | max characters (`max` = no cut) |
48
+ | `icon` | `📰` | leading glyph (`none` to hide) |
68
49
 
69
50
  ## Uninstall
70
51
 
71
52
  ```sh
72
- newsline uninstall # removes the wiring and restores your previous status line
53
+ newsline uninstall # restores your previous status line
73
54
  ```
74
55
 
75
- ## Privacy
76
-
77
- Runs locally; detects locale from your system settings (no IP geolocation); fetches public
78
- feeds directly; never accesses your code, prompts, files, or Claude conversations. Clicks
79
- pass through a redirect so the project can measure engagement and, in future, use affiliate
80
- links to fund itself — disclosed here.
81
-
82
- ## Requirements
83
-
84
- `bash`, `python3`. macOS and Linux natively; Windows via WSL or Git Bash.
85
-
86
- [OSC 8]: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
56
+ Needs `bash` + `python3` (macOS / Linux; Windows via WSL).
package/feeds.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "_comment": "locale language -> ordered keyless RSS/Atom feeds. National outlet first (in-language, higher quality), Google News locale feed last as a guaranteed fallback. fetch.py tries each in order; the first that yields an item wins, so a dead/blocked feed is skipped gracefully. All URLs here were reachability-validated. When adding feeds, also add the article-link domain to ../server/allowlist.json.",
2
+ "_comment": "locale language -> ordered keyless RSS/Atom feeds. National outlet first (in-language, higher quality), Google News locale feed last as a guaranteed fallback. fetch.py tries each in order; the first that yields an item wins, so a dead/blocked feed is skipped gracefully. All URLs here were reachability-validated. When adding feeds, also add the article-link domain to the server allowlist (cc-server/worker.js allowlistCfg).",
3
3
  "en": [
4
4
  "https://feeds.bbci.co.uk/news/rss.xml",
5
5
  "https://feeds.npr.org/1001/rss.xml",
package/newsline CHANGED
@@ -40,6 +40,15 @@ v=d.get(sys.argv[2])
40
40
  print("" if v is None else v)
41
41
  PY
42
42
  }
43
+ cfg_get_obj() { # cfg_get_obj <key> — print an object-valued config key as JSON
44
+ python3 - "$CONFIG" "$1" <<'PY' 2>/dev/null
45
+ import json,sys
46
+ try: d=json.load(open(sys.argv[1]))
47
+ except Exception: d={}
48
+ v=d.get(sys.argv[2])
49
+ print(json.dumps(v,ensure_ascii=False) if isinstance(v,dict) else "")
50
+ PY
51
+ }
43
52
  settings_statusline_cmd() { # print current statusLine.command of a settings file
44
53
  python3 - "$1" <<'PY' 2>/dev/null
45
54
  import json,sys
@@ -49,22 +58,37 @@ sl=d.get("statusLine")
49
58
  print(sl.get("command","") if isinstance(sl,dict) else "")
50
59
  PY
51
60
  }
61
+ settings_statusline_json() { # print the whole statusLine object of a settings file
62
+ python3 - "$1" <<'PY' 2>/dev/null
63
+ import json,sys
64
+ try: d=json.load(open(sys.argv[1]))
65
+ except Exception: d={}
66
+ sl=d.get("statusLine")
67
+ print(json.dumps(sl,ensure_ascii=False) if isinstance(sl,dict) and sl.get("command") else "")
68
+ PY
69
+ }
52
70
 
71
+ # One python spawn for the whole config (render runs on every status-line tick;
72
+ # per-key cfg_get calls here would cost ~10 interpreter startups per tick).
53
73
  load_config() {
54
74
  [ -f "$CONFIG" ] || return 0
55
- local lang topic rotate count api endpoint maxlen icon linkhint
56
- lang=$(cfg_get lang); topic=$(cfg_get topic); rotate=$(cfg_get rotate)
57
- count=$(cfg_get count); api=$(cfg_get api); endpoint=$(cfg_get endpoint)
58
- maxlen=$(cfg_get maxlen); icon=$(cfg_get icon); linkhint=$(cfg_get linkhint)
59
- [ -n "$lang" ] && [ "$lang" != "auto" ] && export NEWSLINE_LANG="$lang"
60
- [ -n "$topic" ] && export NEWSLINE_TOPIC="$topic"
61
- [ -n "$rotate" ] && export NEWSLINE_ROTATE="$rotate"
62
- [ -n "$count" ] && export NEWSLINE_COUNT="$count"
63
- [ -n "$maxlen" ] && export NEWSLINE_MAXLEN="$maxlen"
64
- [ -n "$icon" ] && export NEWSLINE_ICON="$icon"
65
- [ -n "$linkhint" ] && export NEWSLINE_LINKHINT="$linkhint"
66
- [ -n "$api" ] && export NEWSLINE_API="$api"
67
- [ -n "$endpoint" ] && export NEWSLINE_ENDPOINT="$endpoint"
75
+ local exports
76
+ exports=$(python3 - "$CONFIG" <<'PY' 2>/dev/null
77
+ import json,sys,shlex
78
+ env={"lang":"NEWSLINE_LANG","topic":"NEWSLINE_TOPIC","rotate":"NEWSLINE_ROTATE",
79
+ "count":"NEWSLINE_COUNT","maxlen":"NEWSLINE_MAXLEN","icon":"NEWSLINE_ICON",
80
+ "linkhint":"NEWSLINE_LINKHINT","api":"NEWSLINE_API","endpoint":"NEWSLINE_ENDPOINT",
81
+ "base_statusline":"NEWSLINE_BASE"}
82
+ try: d=json.load(open(sys.argv[1]))
83
+ except Exception: d={}
84
+ for k,name in env.items():
85
+ v=d.get(k)
86
+ if v is None or v=="": continue
87
+ if k=="lang" and v=="auto": continue
88
+ print("export %s=%s" % (name, shlex.quote(str(v))))
89
+ PY
90
+ )
91
+ [ -n "$exports" ] && eval "$exports"
68
92
  }
69
93
 
70
94
  detect_lang() {
@@ -110,11 +134,10 @@ choose() {
110
134
  # ---- subcommands ----
111
135
  cmd_render() { # base (existing HUD) first, newsline below — never blocks
112
136
  load_config
113
- local base input=""
114
- base=$(cfg_get base_statusline)
137
+ local input=""
115
138
  [ -t 0 ] || input=$(cat 2>/dev/null) # consume the session JSON if piped
116
- if [ -n "$base" ]; then
117
- printf '%s' "$input" | eval "$base" 2>/dev/null # your HUD, unchanged
139
+ if [ -n "${NEWSLINE_BASE:-}" ]; then
140
+ printf '%s' "$input" | eval "$NEWSLINE_BASE" 2>/dev/null # your HUD, unchanged
118
141
  printf '\n'
119
142
  fi
120
143
  "$HERE/statusline.sh" # the news line, below
@@ -172,60 +195,104 @@ cmd_init() {
172
195
 
173
196
  # Always KEEP an existing status line and show newsline below it (no prompt).
174
197
  # Only --no-compose (or an explicit --base) changes this.
175
- local existing base=""
198
+ local existing base="" base_obj=""
176
199
  if [ -n "$base_override" ]; then
177
200
  base="$base_override" # caller supplied the base explicitly
178
201
  elif [ "$compose" != "no" ]; then
179
202
  existing=$(settings_statusline_cmd "$SETTINGS")
180
203
  if [ -n "$existing" ] && ! printf '%s' "$existing" | grep -q "newsline"; then
181
204
  base="$existing" # first run: capture the real status line
205
+ base_obj=$(settings_statusline_json "$SETTINGS") # verbatim, for uninstall
182
206
  else
183
207
  base=$(cfg_get base_statusline) # re-init: KEEP the base we already saved
208
+ base_obj=$(cfg_get_obj base_statusline_obj)
184
209
  fi
185
210
  fi
186
211
 
187
212
  mkdir -p "$CONFIG_DIR"
188
- python3 - "$CONFIG" "$lang" "$topic" "$rotate" "$count" "$api" "$endpoint" "$base" <<'PY'
189
- import json,sys
190
- _,path,lang,topic,rotate,count,api,endpoint,base=sys.argv
191
- d={"lang":lang,"topic":topic,"rotate":int(rotate),"count":int(count)}
213
+ python3 - "$CONFIG" "$lang" "$topic" "$rotate" "$count" "$api" "$endpoint" "$base" "$base_obj" <<'PY' || { echo "✗ could not write $CONFIG" >&2; return 1; }
214
+ import json,os,sys
215
+ _,path,lang,topic,rotate,count,api,endpoint,base,base_obj=sys.argv
216
+ try:
217
+ d={"lang":lang,"topic":topic,"rotate":int(rotate),"count":int(count)}
218
+ except ValueError:
219
+ sys.stderr.write("newsline: --rotate/--count must be integers\n"); sys.exit(3)
192
220
  if api: d["api"]=api
193
221
  if endpoint: d["endpoint"]=endpoint
194
222
  if base: d["base_statusline"]=base
195
- json.dump(d,open(path,"w"),indent=2,ensure_ascii=False)
223
+ if base_obj:
224
+ try: d["base_statusline_obj"]=json.loads(base_obj)
225
+ except Exception: pass
226
+ tmp=path+".tmp"
227
+ with open(tmp,"w") as f: json.dump(d,f,indent=2,ensure_ascii=False)
228
+ os.replace(tmp,path)
196
229
  PY
197
230
 
198
- python3 - "$SETTINGS" "$RENDER_CMD" <<'PY'
231
+ python3 - "$SETTINGS" "$RENDER_CMD" <<'PY' || { echo "✗ could not update $SETTINGS — fix the file and re-run: newsline init" >&2; return 1; }
199
232
  import json,os,sys
200
233
  path,cmd=sys.argv[1],sys.argv[2]
201
- try: d=json.load(open(path))
202
- except Exception: d={}
234
+ d={}
235
+ if os.path.exists(path):
236
+ try:
237
+ with open(path) as f: raw=f.read().strip()
238
+ d=json.loads(raw) if raw else {}
239
+ if not isinstance(d,dict): raise ValueError("settings root is not an object")
240
+ except Exception:
241
+ # never clobber a settings file we can't parse
242
+ sys.stderr.write("newsline: %s is not valid JSON; refusing to modify it\n" % path)
243
+ sys.exit(3)
203
244
  d["statusLine"]={"type":"command","command":cmd,"refreshInterval":5}
204
245
  os.makedirs(os.path.dirname(path) or ".",exist_ok=True)
205
- json.dump(d,open(path,"w"),indent=2,ensure_ascii=False)
246
+ tmp=path+".tmp"
247
+ with open(tmp,"w") as f: json.dump(d,f,indent=2,ensure_ascii=False)
248
+ os.replace(tmp,path)
206
249
  PY
207
250
 
251
+ # apply the new language/topic on the next render, not after the old cache expires
252
+ local cache="${NEWSLINE_CACHE:-$HOME/.cache/newsline}"
253
+ rm -f "$cache/line" "$cache/feeds.resolved.json" "$cache/feeds.resolved.ok" "$cache/notice" 2>/dev/null
254
+
208
255
  echo "✔ newsline installed (lang=$lang, topic=$topic)."
209
256
  [ -n "$base" ] && echo " composing with your existing status line — it stays visible above the news."
210
257
  echo " It appears on your next message to Claude — no restart needed."
211
258
  }
212
259
 
213
260
  cmd_uninstall() {
214
- local base; base=$(cfg_get base_statusline)
215
- python3 - "$SETTINGS" "$base" <<'PY'
216
- import json,sys
217
- path,base=sys.argv[1],sys.argv[2]
218
- try: d=json.load(open(path))
219
- except Exception: d={}
261
+ local base base_obj
262
+ base=$(cfg_get base_statusline)
263
+ base_obj=$(cfg_get_obj base_statusline_obj)
264
+ python3 - "$SETTINGS" "$base" "$base_obj" <<'PY'
265
+ import json,os,sys
266
+ path,base,base_obj=sys.argv[1],sys.argv[2],sys.argv[3]
267
+ try:
268
+ with open(path) as f: raw=f.read().strip()
269
+ d=json.loads(raw) if raw else {}
270
+ if not isinstance(d,dict): raise ValueError("settings root is not an object")
271
+ except FileNotFoundError:
272
+ print("no settings file; nothing to revert"); sys.exit(0)
273
+ except Exception:
274
+ print("settings file is not valid JSON; left as-is"); sys.exit(0)
220
275
  sl=d.get("statusLine"); cmd=sl.get("command","") if isinstance(sl,dict) else ""
221
276
  if "newsline" in cmd:
222
- if base: d["statusLine"]={"type":"command","command":base}
223
- else: d.pop("statusLine",None)
224
- json.dump(d,open(path,"w"),indent=2,ensure_ascii=False); print("reverted status line")
277
+ obj=None
278
+ if base_obj:
279
+ try: obj=json.loads(base_obj)
280
+ except Exception: obj=None
281
+ if isinstance(obj,dict) and obj.get("command"):
282
+ d["statusLine"]=obj # full original (keeps padding etc.)
283
+ elif base:
284
+ d["statusLine"]={"type":"command","command":base}
285
+ else:
286
+ d.pop("statusLine",None)
287
+ tmp=path+".tmp"
288
+ with open(tmp,"w") as f: json.dump(d,f,indent=2,ensure_ascii=False)
289
+ os.replace(tmp,path)
290
+ print("reverted status line")
225
291
  else:
226
292
  print("status line not managed by newsline; left as-is")
227
293
  PY
228
- rm -f "$CONFIG" 2>/dev/null
294
+ local cache="${NEWSLINE_CACHE:-$HOME/.cache/newsline}"
295
+ rm -f "$CONFIG" "$cache/line" "$cache/feeds.resolved.json" "$cache/feeds.resolved.ok" "$cache/notice" 2>/dev/null
229
296
  echo "✔ newsline uninstalled."
230
297
  }
231
298
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "newsline-cli",
3
- "version": "0.1.1",
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/readme/de.md ADDED
@@ -0,0 +1,58 @@
1
+ <div align="center">
2
+
3
+ [English](../README.md) · [한국어](ko.md) · [日本語](ja.md) · [Español](es.md) · [Français](fr.md) · **Deutsch** · [Português](pt.md) · [中文](zh.md)
4
+
5
+ </div>
6
+
7
+ # newsline
8
+
9
+ **Wartest du darauf, dass Claude Code fertig wird? Lies die neuesten Ein-Zeilen-News direkt in deiner Statusleiste.**
10
+ Eine rotierende regionale Schlagzeile sitzt am unteren Rand deiner Sitzung – so wird aus langem Warten
11
+ ein schneller Blick in die Nachrichten. Sie erscheint *unter* deiner bestehenden Statusleiste (dein HUD
12
+ bleibt erhalten).
13
+
14
+ ## Installieren & starten — eine Zeile
15
+
16
+ ```sh
17
+ # curl (macOS / Linux / WSL) — installiert und richtet sofort ein
18
+ curl -fsSL https://raw.githubusercontent.com/itdar/cc-plugin/master/install.sh | sh
19
+
20
+ # Homebrew
21
+ brew install itdar/tap/newsline && newsline init
22
+
23
+ # npm
24
+ npm i -g newsline-cli && newsline init
25
+ ```
26
+
27
+ Die News-Zeile erscheint bei deiner **nächsten Nachricht** – kein Neustart nötig. Die Einrichtung fragt
28
+ nach Sprache und Thema und behält deine bestehende Statusleiste bei.
29
+
30
+ ## Was es macht
31
+
32
+ - **Dein Rechner holt und zeigt die Nachrichten** – er rührt niemals deinen Code, deine Prompts, Dateien
33
+ oder Claude-Unterhaltungen an.
34
+ - Ein kleiner **Edge-Dienst** wählt die besten regionalen Quellen aus und leitet Klicks auf Schlagzeilen
35
+ weiter, sodass die Quellen ohne Neuinstallation aktuell bleiben. Ist er nicht erreichbar, **greift
36
+ newsline auf die integrierten Feeds zurück**.
37
+ - Die Statusleiste ist sofort da (aus einem Cache); Aktualisierungen laufen im Hintergrund.
38
+
39
+ ## Konfigurieren
40
+
41
+ Führe `newsline init` erneut aus oder bearbeite `~/.config/newsline/config.json`:
42
+
43
+ | Schlüssel | Standard | Bedeutung |
44
+ |---|---|---|
45
+ | `lang` | `auto` | `ko` `ja` `en` `es` `fr` `de` `pt` `zh` oder `auto` |
46
+ | `topic` | `general` | `general` `tech` `business` `world` `sports` `science` `health` `entertainment` |
47
+ | `rotate` | `6` | Sekunden pro Schlagzeile |
48
+ | `count` | `15` | Schlagzeilen im Wechsel |
49
+ | `maxlen` | `120` | maximale Zeichen (`max` = kein Abschneiden) |
50
+ | `icon` | `📰` | vorangestelltes Symbol (`none` zum Ausblenden) |
51
+
52
+ ## Deinstallieren
53
+
54
+ ```sh
55
+ newsline uninstall # stellt deine vorherige Statusleiste wieder her
56
+ ```
57
+
58
+ Benötigt `bash` + `python3` (macOS / Linux; Windows über WSL).
package/readme/es.md ADDED
@@ -0,0 +1,58 @@
1
+ <div align="center">
2
+
3
+ [English](../README.md) · [한국어](ko.md) · [日本語](ja.md) · **Español** · [Français](fr.md) · [Deutsch](de.md) · [Português](pt.md) · [中文](zh.md)
4
+
5
+ </div>
6
+
7
+ # newsline
8
+
9
+ **¿Esperando a que Claude Code termine? Lee las últimas noticias en una línea, justo en tu barra de estado.**
10
+ Un titular regional rotativo aparece en la parte inferior de tu sesión, así una larga espera se convierte
11
+ en un vistazo rápido a las noticias. Se muestra *debajo* de tu barra de estado actual (tu HUD se mantiene).
12
+
13
+ ## Instalar y ejecutar — una línea
14
+
15
+ ```sh
16
+ # curl (macOS / Linux / WSL) — instala y configura al instante
17
+ curl -fsSL https://raw.githubusercontent.com/itdar/cc-plugin/master/install.sh | sh
18
+
19
+ # Homebrew
20
+ brew install itdar/tap/newsline && newsline init
21
+
22
+ # npm
23
+ npm i -g newsline-cli && newsline init
24
+ ```
25
+
26
+ La línea de noticias aparece en tu **próximo mensaje**, sin reiniciar. La configuración te pregunta el
27
+ idioma y el tema, y conserva tu barra de estado actual.
28
+
29
+ ## Qué hace
30
+
31
+ - **Tu equipo obtiene y muestra las noticias** — nunca toca tu código, prompts, archivos ni
32
+ conversaciones con Claude.
33
+ - Un pequeño **servicio edge** elige las mejores fuentes regionales y enruta los clics en los titulares,
34
+ de modo que las fuentes se mantienen actualizadas sin reinstalar. Si no está disponible, newsline
35
+ **recurre a los feeds integrados**.
36
+ - La barra de estado es instantánea (servida desde una caché); las actualizaciones se ejecutan en
37
+ segundo plano.
38
+
39
+ ## Configurar
40
+
41
+ Vuelve a ejecutar `newsline init` o edita `~/.config/newsline/config.json`:
42
+
43
+ | Clave | Predeterminado | Significado |
44
+ |---|---|---|
45
+ | `lang` | `auto` | `ko` `ja` `en` `es` `fr` `de` `pt` `zh`, o `auto` |
46
+ | `topic` | `general` | `general` `tech` `business` `world` `sports` `science` `health` `entertainment` |
47
+ | `rotate` | `6` | segundos por titular |
48
+ | `count` | `15` | titulares en rotación |
49
+ | `maxlen` | `120` | caracteres máximos (`max` = sin recorte) |
50
+ | `icon` | `📰` | icono inicial (`none` para ocultarlo) |
51
+
52
+ ## Desinstalar
53
+
54
+ ```sh
55
+ newsline uninstall # restaura tu barra de estado anterior
56
+ ```
57
+
58
+ Necesita `bash` + `python3` (macOS / Linux; Windows mediante WSL).
package/readme/fr.md ADDED
@@ -0,0 +1,58 @@
1
+ <div align="center">
2
+
3
+ [English](../README.md) · [한국어](ko.md) · [日本語](ja.md) · [Español](es.md) · **Français** · [Deutsch](de.md) · [Português](pt.md) · [中文](zh.md)
4
+
5
+ </div>
6
+
7
+ # newsline
8
+
9
+ **En attendant que Claude Code termine ? Lisez l'actualité en une ligne, directement dans votre barre d'état.**
10
+ Un titre régional défile en bas de votre session : une longue attente se transforme en un coup d'œil
11
+ rapide à l'actualité. Il s'affiche *sous* votre barre d'état existante (votre HUD reste en place).
12
+
13
+ ## Installer et lancer — une ligne
14
+
15
+ ```sh
16
+ # curl (macOS / Linux / WSL) — installe et configure immédiatement
17
+ curl -fsSL https://raw.githubusercontent.com/itdar/cc-plugin/master/install.sh | sh
18
+
19
+ # Homebrew
20
+ brew install itdar/tap/newsline && newsline init
21
+
22
+ # npm
23
+ npm i -g newsline-cli && newsline init
24
+ ```
25
+
26
+ La ligne d'actualité apparaît dès votre **prochain message** — sans redémarrage. La configuration vous
27
+ demande une langue et un thème, et conserve votre barre d'état existante.
28
+
29
+ ## Ce que ça fait
30
+
31
+ - **Votre machine récupère et affiche l'actualité** — elle ne touche jamais à votre code, vos prompts,
32
+ vos fichiers ni vos conversations avec Claude.
33
+ - Un petit **service edge** sélectionne les meilleures sources régionales et route les clics sur les
34
+ titres, afin que les sources restent à jour sans réinstallation. S'il est injoignable, newsline
35
+ **bascule sur les flux intégrés**.
36
+ - La barre d'état est instantanée (servie depuis un cache) ; les rafraîchissements s'exécutent en
37
+ arrière-plan.
38
+
39
+ ## Configurer
40
+
41
+ Relancez `newsline init`, ou modifiez `~/.config/newsline/config.json` :
42
+
43
+ | Clé | Par défaut | Signification |
44
+ |---|---|---|
45
+ | `lang` | `auto` | `ko` `ja` `en` `es` `fr` `de` `pt` `zh`, ou `auto` |
46
+ | `topic` | `general` | `general` `tech` `business` `world` `sports` `science` `health` `entertainment` |
47
+ | `rotate` | `6` | secondes par titre |
48
+ | `count` | `15` | titres en rotation |
49
+ | `maxlen` | `120` | caractères maximum (`max` = sans coupure) |
50
+ | `icon` | `📰` | icône de tête (`none` pour masquer) |
51
+
52
+ ## Désinstaller
53
+
54
+ ```sh
55
+ newsline uninstall # restaure votre barre d'état précédente
56
+ ```
57
+
58
+ Nécessite `bash` + `python3` (macOS / Linux ; Windows via WSL).
package/readme/ja.md ADDED
@@ -0,0 +1,57 @@
1
+ <div align="center">
2
+
3
+ [English](../README.md) · [한국어](ko.md) · **日本語** · [Español](es.md) · [Français](fr.md) · [Deutsch](de.md) · [Português](pt.md) · [中文](zh.md)
4
+
5
+ </div>
6
+
7
+ # newsline
8
+
9
+ **Claude Code の処理が終わるのを待っていますか?最新のひとことニュースをステータスラインで読みましょう。**
10
+ 地域のヘッドラインがセッション下部で切り替わり表示され、長い待ち時間がサッとニュースをチェックする
11
+ 時間に変わります。既存のステータスライン(HUD)の **下の行に** 表示されるので、HUD はそのまま残ります。
12
+
13
+ ## インストールと実行 — 一行で
14
+
15
+ ```sh
16
+ # curl (macOS / Linux / WSL) — インストールとセットアップを一気に
17
+ curl -fsSL https://raw.githubusercontent.com/itdar/cc-plugin/master/install.sh | sh
18
+
19
+ # Homebrew
20
+ brew install itdar/tap/newsline && newsline init
21
+
22
+ # npm
23
+ npm i -g newsline-cli && newsline init
24
+ ```
25
+
26
+ ニュース行は **次のメッセージ** で表示されます — 再起動は不要です。セットアップで言語とトピックを
27
+ 尋ね、既存のステータスラインはそのまま保持します。
28
+
29
+ ## 何をするか
30
+
31
+ - **ニュースの取得と表示はあなたのマシン上で** 行われます — コード・プロンプト・ファイル・Claude との
32
+ 会話には一切触れません。
33
+ - 小さな **エッジサービス** が地域に最適なソースを選び、ヘッドラインのクリックをルーティングします。
34
+ おかげで再インストールなしでソースが最新に保たれ、エッジに接続できない場合は **内蔵フィードに自動
35
+ フォールバック** します。
36
+ - ステータスラインはキャッシュから **即座に** 表示され、更新はバックグラウンドで実行されます。
37
+
38
+ ## 設定
39
+
40
+ `newsline init` を再実行するか、`~/.config/newsline/config.json` を編集します:
41
+
42
+ | キー | 既定値 | 意味 |
43
+ |---|---|---|
44
+ | `lang` | `auto` | `ko` `ja` `en` `es` `fr` `de` `pt` `zh`、または `auto` |
45
+ | `topic` | `general` | `general` `tech` `business` `world` `sports` `science` `health` `entertainment` |
46
+ | `rotate` | `6` | ヘッドラインごとの表示秒数 |
47
+ | `count` | `15` | 切り替えるヘッドライン数 |
48
+ | `maxlen` | `120` | 最大文字数(`max` = 無制限) |
49
+ | `icon` | `📰` | 先頭アイコン(`none` で非表示) |
50
+
51
+ ## アンインストール
52
+
53
+ ```sh
54
+ newsline uninstall # 以前のステータスラインを復元します
55
+ ```
56
+
57
+ `bash` と `python3` が必要です(macOS / Linux、Windows は WSL 経由)。
package/readme/ko.md CHANGED
@@ -1,83 +1,55 @@
1
- # newsline
1
+ <div align="center">
2
2
 
3
- Claude Code 상태줄에 지역·언어에 맞는 뉴스 **완전 로컬**, 그리고 기존 상태줄(예: HUD)
4
- **덮지 않고 그 아래에 붙입니다(compose)**.
3
+ [English](../README.md) · **한국어** · [日本語](ja.md) · [Español](es.md) · [Français](fr.md) · [Deutsch](de.md) · [Português](pt.md) · [中文](zh.md)
5
4
 
6
- [English](../README.md) · **한국어**
5
+ </div>
7
6
 
8
- ## 무엇인가
7
+ # newsline
9
8
 
10
- 세션 하단에 지역 언어 뉴스 헤드라인을 **회전 표시**하는 Claude Code 상태줄 유틸리티입니다.
11
- 로케일 감지·피드 수집·파싱·캐시·렌더링 전부 **로컬에서** 돌아갑니다. 코드·프롬프트·파일·Claude
12
- 대화는 절대 읽지 않습니다.
9
+ **Claude Code 작업 끝나길 기다리는 동안, 상태줄에서 최신 한줄 뉴스를 확인하세요.**
10
+ 작업을 기다리는 시간이 뉴스 번 훑는 시간이 됩니다 — 세션 하단에 지역 맞춤 헤드라인이
11
+ 회전 표시됩니다. 기존 상태줄(HUD) **아래 줄에** 붙어서, HUD는 그대로 보입니다.
13
12
 
14
- ## 설치
13
+ ## 설치 & 실행 — 한 줄
15
14
 
16
- **curl | sh** (macOS / Linux / WSL):
17
15
  ```sh
16
+ # curl (macOS / Linux / WSL) — 설치 + 셋업까지 바로
18
17
  curl -fsSL https://raw.githubusercontent.com/itdar/cc-plugin/master/install.sh | sh
19
- ```
20
18
 
21
- **Homebrew:**
22
- ```sh
23
- brew install itdar/tap/newsline
24
- ```
19
+ # Homebrew
20
+ brew install itdar/tap/newsline && newsline init
25
21
 
26
- **소스에서:**
27
- ```sh
28
- git clone https://github.com/itdar/cc-plugin && cd cc-plugin && ./install.sh
22
+ # npm
23
+ npm i -g newsline-cli && newsline init
29
24
  ```
30
25
 
31
- **curl | sh** 설치는 셋업까지 **자동**입니다언어·분야를 물어본 상태줄을 연결해요.
32
- **Homebrew**나 **소스** 설치는 직접 실행:
33
- ```sh
34
- newsline init
35
- ```
36
- `init`이 언어·분야를 물어보고(메뉴에서 번호 선택), 상태줄을 등록합니다. **기존 상태줄은 그대로
37
- 두고**(HUD 유지) 그 아래 줄에 뉴스를 보여줍니다. Claude Code가 설정을 핫리로드하므로 **재시작
38
- 없이 다음 메시지 때 바로** 나타납니다.
26
+ 뉴스 줄은 **다음 메시지 때** 나타납니다재시작 불필요. 셋업은 언어·분야를 물어보고, 기존
27
+ 상태줄은 유지합니다.
28
+
29
+ ## 무엇을 하나
39
30
 
40
- _(설치 자동 셋업을 끄려면 `NEWSLINE_NO_INIT=1`.)_
31
+ - **뉴스 수집·표시는 컴퓨터에서** 이뤄집니다 — 코드·프롬프트·파일·Claude 대화는 건드리지 않아요.
32
+ - 작은 **엣지 서비스**가 지역에 맞는 최적 소스를 골라주고 헤드라인 클릭을 라우팅합니다. 덕분에
33
+ 재설치 없이 소스가 최신으로 유지되고, 엣지가 불통이면 **내장 피드로 자동 폴백**합니다.
34
+ - 상태줄은 캐시에서 **즉시** 표시되고, 갱신은 백그라운드에서 돕니다.
41
35
 
42
36
  ## 설정
43
37
 
44
- `newsline init`에서 고르거나, `~/.config/newsline/config.json`을 편집(또는 환경변수 설정):
38
+ `newsline init` 재실행 또는 `~/.config/newsline/config.json` 편집:
45
39
 
46
- | 키 / 환경변수 | 기본값 | 의미 |
40
+ | 키 | 기본값 | 의미 |
47
41
  |---|---|---|
48
- | `lang` / `NEWSLINE_LANG` | `auto` | `ko` `ja` `en` `es` `fr` `de` `pt` `zh` 또는 `auto` |
49
- | `topic` / `NEWSLINE_TOPIC` | `general` | `general` `tech` `business` `world` `sports` `science` `health` `entertainment` |
50
- | `rotate` / `NEWSLINE_ROTATE` | `6` | 헤드라인당 노출 초 |
51
- | `count` / `NEWSLINE_COUNT` | `15` | 회전용으로 캐시할 헤드라인 수 |
52
- | `maxlen` / `NEWSLINE_MAXLEN` | `120` | 최대 글자수; `max`=무제한; `NN%`≈터미널 폭 비율 |
53
- | `icon` / `NEWSLINE_ICON` | `📰` | 앞 아이콘; `none`이면 제거 |
54
- | `linkhint` / `NEWSLINE_LINKHINT` | off | 끝에 `↗`(외부 열기) 힌트 추가 |
55
- | `api` / `NEWSLINE_API` | — | 선택: 큐레이션 API (서버 우선, 실패 시 로컬 폴백) |
56
- | `endpoint` / `NEWSLINE_ENDPOINT` | — | 클릭 리다이렉트 래퍼 |
57
-
58
- ## 동작 방식
59
-
60
- - **`newsline render`** (상태줄이 호출) — 캐시에서 **즉시** 출력하고 절대 막지 않으며, 캐시가
61
- 낡았으면 백그라운드 refresh를 트리거합니다.
62
- - **`newsline refresh`** — keyless 지역 RSS/Atom 피드를 로컬에서 받아 상위 헤드라인을 파싱·중복
63
- 제거·캐시합니다.
64
- - 헤드라인은 클릭 가능한 [OSC 8] 링크입니다. 그냥 클릭 vs `Cmd`/`Ctrl`+클릭으로 열리는지는
65
- newsline이 아니라 **터미널 설정**이 결정합니다.
42
+ | `lang` | `auto` | `ko` `ja` `en` `es` `fr` `de` `pt` `zh` 또는 `auto` |
43
+ | `topic` | `general` | `general` `tech` `business` `world` `sports` `science` `health` `entertainment` |
44
+ | `rotate` | `6` | 헤드라인당 노출 초 |
45
+ | `count` | `15` | 회전 헤드라인 수 |
46
+ | `maxlen` | `120` | 최대 글자수 (`max`=무제한) |
47
+ | `icon` | `📰` | 앞 아이콘 (`none`이면 제거) |
66
48
 
67
49
  ## 제거
68
50
 
69
51
  ```sh
70
- newsline uninstall # 연결을 제거하고 이전 상태줄로 복구
52
+ newsline uninstall # 이전 상태줄로 복구
71
53
  ```
72
54
 
73
- ## 프라이버시
74
-
75
- 로컬에서 실행되며, 로케일은 시스템 설정에서 감지(IP 지오로케이션 없음), 공개 피드를 직접
76
- 받아옵니다. 코드·프롬프트·파일·Claude 대화는 접근하지 않습니다. 클릭은 리다이렉트를 거치는데,
77
- 이는 참여도 측정과 향후 제휴 링크(운영 자금)를 위한 것이며 여기에 명시합니다.
78
-
79
- ## 요구사항
80
-
81
- `bash`, `python3`. macOS·Linux는 기본 지원, Windows는 WSL 또는 Git Bash.
82
-
83
- [OSC 8]: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
55
+ `bash` + `python3` 필요 (macOS / Linux; Windows는 WSL).
package/readme/pt.md ADDED
@@ -0,0 +1,57 @@
1
+ <div align="center">
2
+
3
+ [English](../README.md) · [한국어](ko.md) · [日本語](ja.md) · [Español](es.md) · [Français](fr.md) · [Deutsch](de.md) · **Português** · [中文](zh.md)
4
+
5
+ </div>
6
+
7
+ # newsline
8
+
9
+ **Esperando o Claude Code terminar? Leia as últimas notícias em uma linha, direto na sua barra de status.**
10
+ Uma manchete regional rotativa fica na parte de baixo da sua sessão — então uma longa espera vira uma
11
+ olhada rápida nas notícias. Ela aparece *abaixo* da sua barra de status atual (seu HUD permanece).
12
+
13
+ ## Instalar e executar — uma linha
14
+
15
+ ```sh
16
+ # curl (macOS / Linux / WSL) — instala e configura na hora
17
+ curl -fsSL https://raw.githubusercontent.com/itdar/cc-plugin/master/install.sh | sh
18
+
19
+ # Homebrew
20
+ brew install itdar/tap/newsline && newsline init
21
+
22
+ # npm
23
+ npm i -g newsline-cli && newsline init
24
+ ```
25
+
26
+ A linha de notícias aparece na sua **próxima mensagem** — sem reiniciar. A configuração pergunta o
27
+ idioma e o tema, e mantém a sua barra de status atual.
28
+
29
+ ## O que faz
30
+
31
+ - **Sua máquina busca e exibe as notícias** — ela nunca toca no seu código, prompts, arquivos ou
32
+ conversas com o Claude.
33
+ - Um pequeno **serviço edge** escolhe as melhores fontes regionais e roteia os cliques nas manchetes,
34
+ mantendo as fontes atualizadas sem reinstalar. Se estiver inacessível, o newsline **usa os feeds
35
+ integrados como alternativa**.
36
+ - A barra de status é instantânea (servida de um cache); as atualizações rodam em segundo plano.
37
+
38
+ ## Configurar
39
+
40
+ Execute `newsline init` novamente ou edite `~/.config/newsline/config.json`:
41
+
42
+ | Chave | Padrão | Significado |
43
+ |---|---|---|
44
+ | `lang` | `auto` | `ko` `ja` `en` `es` `fr` `de` `pt` `zh`, ou `auto` |
45
+ | `topic` | `general` | `general` `tech` `business` `world` `sports` `science` `health` `entertainment` |
46
+ | `rotate` | `6` | segundos por manchete |
47
+ | `count` | `15` | manchetes na rotação |
48
+ | `maxlen` | `120` | máximo de caracteres (`max` = sem corte) |
49
+ | `icon` | `📰` | ícone inicial (`none` para ocultar) |
50
+
51
+ ## Desinstalar
52
+
53
+ ```sh
54
+ newsline uninstall # restaura a sua barra de status anterior
55
+ ```
56
+
57
+ Precisa de `bash` + `python3` (macOS / Linux; Windows via WSL).
package/readme/zh.md ADDED
@@ -0,0 +1,54 @@
1
+ <div align="center">
2
+
3
+ [English](../README.md) · [한국어](ko.md) · [日本語](ja.md) · [Español](es.md) · [Français](fr.md) · [Deutsch](de.md) · [Português](pt.md) · **中文**
4
+
5
+ </div>
6
+
7
+ # newsline
8
+
9
+ **等待 Claude Code 完成任务时,直接在状态栏里读一行最新新闻。**
10
+ 一条轮播的区域头条位于会话底部——漫长的等待就变成了快速浏览新闻的时间。它显示在你现有状态栏的
11
+ *下方*(你的 HUD 保持不变)。
12
+
13
+ ## 安装并运行 — 一行搞定
14
+
15
+ ```sh
16
+ # curl (macOS / Linux / WSL) — 安装并立即完成设置
17
+ curl -fsSL https://raw.githubusercontent.com/itdar/cc-plugin/master/install.sh | sh
18
+
19
+ # Homebrew
20
+ brew install itdar/tap/newsline && newsline init
21
+
22
+ # npm
23
+ npm i -g newsline-cli && newsline init
24
+ ```
25
+
26
+ 新闻行会在你的 **下一条消息** 时出现——无需重启。设置会询问语言和主题,并保留你现有的状态栏。
27
+
28
+ ## 它做什么
29
+
30
+ - **抓取和显示新闻都在你的电脑上完成**——绝不触碰你的代码、提示词、文件或与 Claude 的对话。
31
+ - 一个小型 **边缘服务** 挑选最合适的区域新闻源并路由头条点击,因此无需重装即可保持新闻源常新。
32
+ 若无法连接,newsline 会 **回退到内置源**。
33
+ - 状态栏即时显示(来自缓存);刷新在后台进行。
34
+
35
+ ## 配置
36
+
37
+ 重新运行 `newsline init`,或编辑 `~/.config/newsline/config.json`:
38
+
39
+ | 键 | 默认值 | 含义 |
40
+ |---|---|---|
41
+ | `lang` | `auto` | `ko` `ja` `en` `es` `fr` `de` `pt` `zh`,或 `auto` |
42
+ | `topic` | `general` | `general` `tech` `business` `world` `sports` `science` `health` `entertainment` |
43
+ | `rotate` | `6` | 每条头条显示秒数 |
44
+ | `count` | `15` | 轮播头条数量 |
45
+ | `maxlen` | `120` | 最大字符数(`max` = 不截断) |
46
+ | `icon` | `📰` | 前置图标(`none` 为隐藏) |
47
+
48
+ ## 卸载
49
+
50
+ ```sh
51
+ newsline uninstall # 恢复你之前的状态栏
52
+ ```
53
+
54
+ 需要 `bash` + `python3`(macOS / Linux;Windows 通过 WSL)。
package/refresh.sh CHANGED
@@ -16,10 +16,20 @@ 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
 
22
25
  # --- single-flight: if another refresh is already running, bail quietly ---
26
+ # A refresh killed before its EXIT trap runs (SIGKILL, reboot, closed terminal)
27
+ # leaves the lock behind; expire locks older than 120s so refreshes never wedge.
28
+ if [ -d "$LOCK" ]; then
29
+ now=$(date +%s)
30
+ lmt=$(stat -f %m "$LOCK" 2>/dev/null || stat -c %Y "$LOCK" 2>/dev/null || echo 0)
31
+ [ $((now - lmt)) -gt 120 ] && rmdir "$LOCK" 2>/dev/null
32
+ fi
23
33
  if ! mkdir "$LOCK" 2>/dev/null; then exit 0; fi
24
34
  trap 'rmdir "$LOCK" 2>/dev/null' EXIT
25
35
 
@@ -49,14 +59,58 @@ esac
49
59
  # --- server-first feed selection (COARSE context only), local fallback ---
50
60
  # resolve.py tries $NEWSLINE_API/feed; on any failure it returns local feeds.json.
51
61
  # We send lang/country/localtime/dow/tz only — no personal data, no tracking id.
62
+ # The resolved list is reused for NEWSLINE_API_TTL seconds (feed curation changes
63
+ # rarely) so the curation API is hit at most hourly, not on every refresh.
52
64
  RESOLVED="$CACHE_DIR/feeds.resolved.json"
65
+ RESOLVE_STAMP="$CACHE_DIR/feeds.resolved.ok"
66
+ RESOLVE_TTL="${NEWSLINE_API_TTL:-3600}"
53
67
  export NEWSLINE_COUNTRY="$country"
54
68
  export NEWSLINE_LOCALTIME="$(date +%H%M)"
55
69
  export NEWSLINE_DOW="$(date +%u)"
56
70
  export NEWSLINE_TZ="$(date +%z)"
57
- if ! python3 "$HERE/resolve.py" "$lang" "$FEEDS" "${NEWSLINE_API:-https://newsline.thesockerrr.workers.dev}" > "$RESOLVED" 2>/dev/null \
58
- || [ ! -s "$RESOLVED" ]; then
59
- cp "$FEEDS" "$RESOLVED" 2>/dev/null # last-ditch fallback to bundled feeds
71
+ need_resolve=1
72
+ if [ -s "$RESOLVED" ] && [ -f "$RESOLVE_STAMP" ]; then
73
+ now=$(date +%s)
74
+ smt=$(stat -f %m "$RESOLVE_STAMP" 2>/dev/null || stat -c %Y "$RESOLVE_STAMP" 2>/dev/null || echo 0)
75
+ [ $((now - smt)) -lt "$RESOLVE_TTL" ] && need_resolve=0
76
+ fi
77
+ if [ "$need_resolve" = "1" ]; then
78
+ if python3 "$HERE/resolve.py" "$lang" "$FEEDS" "${NEWSLINE_API:-https://newsline.thesockerrr.workers.dev}" > "$RESOLVED.tmp" 2>/dev/null \
79
+ && [ -s "$RESOLVED.tmp" ]; then
80
+ mv -f "$RESOLVED.tmp" "$RESOLVED"
81
+ touch "$RESOLVE_STAMP"
82
+ else
83
+ rm -f "$RESOLVED.tmp" 2>/dev/null
84
+ [ -s "$RESOLVED" ] || cp "$FEEDS" "$RESOLVED" 2>/dev/null # last-ditch fallback to bundled feeds
85
+ fi
86
+ fi
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
60
114
  fi
61
115
 
62
116
  # --- fetch + parse + build the line; write atomically, keep old cache on failure ---
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
 
@@ -32,6 +34,10 @@ GN_TOPICS = {
32
34
  }
33
35
  LANG_COUNTRY = {"ko": "KR", "ja": "JP", "en": "US", "de": "DE",
34
36
  "fr": "FR", "es": "ES", "pt": "BR", "zh": "CN"}
37
+ # Google News wants regioned codes for some languages (bare "pt"/"zh" 404s or
38
+ # serves the wrong edition).
39
+ GN_HL = {"pt": "pt-BR", "zh": "zh-CN", "en": "en-US"}
40
+ GN_CEID_LANG = {"pt": "pt-419", "zh": "zh-Hans"}
35
41
 
36
42
 
37
43
  def topic_feed(lang, country, topic):
@@ -42,8 +48,10 @@ def topic_feed(lang, country, topic):
42
48
  if not t:
43
49
  return None
44
50
  gl = country or LANG_COUNTRY.get(lang, lang.upper())
51
+ hl = GN_HL.get(lang, lang)
52
+ cl = GN_CEID_LANG.get(lang, lang)
45
53
  return (f"https://news.google.com/rss/headlines/section/topic/{t}"
46
- f"?hl={lang}&gl={gl}&ceid={gl}:{lang}")
54
+ f"?hl={hl}&gl={gl}&ceid={gl}:{cl}")
47
55
 
48
56
 
49
57
  def local_feeds(lang, path):
@@ -56,6 +64,7 @@ def local_feeds(lang, path):
56
64
 
57
65
 
58
66
  def server_feeds(lang, api_base):
67
+ """Return (feeds_or_None, update_or_None) from the curation API."""
59
68
  params = {
60
69
  "lang": lang,
61
70
  "country": os.environ.get("NEWSLINE_COUNTRY", ""),
@@ -63,14 +72,22 @@ def server_feeds(lang, api_base):
63
72
  "dow": os.environ.get("NEWSLINE_DOW", ""),
64
73
  "tz": os.environ.get("NEWSLINE_TZ", ""),
65
74
  "topic": os.environ.get("NEWSLINE_TOPIC", ""),
75
+ "v": os.environ.get("NEWSLINE_VERSION", ""),
66
76
  }
67
77
  url = api_base.rstrip("/") + "/feed?" + urllib.parse.urlencode(params)
68
78
  req = urllib.request.Request(url, headers={"User-Agent": "newsline/0.1"})
69
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
70
83
  feeds = obj.get("feeds")
71
- if isinstance(feeds, list) and feeds and all(isinstance(x, str) for x in feeds):
72
- return feeds
73
- return None
84
+ if isinstance(feeds, list):
85
+ # fetch.py urlopens these — accept only http(s) URLs
86
+ feeds = [x for x in feeds
87
+ if isinstance(x, str) and x.startswith(("http://", "https://"))]
88
+ else:
89
+ feeds = None
90
+ return (feeds or None), update
74
91
 
75
92
 
76
93
  def main():
@@ -81,22 +98,26 @@ def main():
81
98
 
82
99
  local_urls, default = local_feeds(lang, local_path)
83
100
 
84
- feeds = None
101
+ feeds = update = None
85
102
  if api_base:
86
103
  try:
87
- feeds = server_feeds(lang, api_base) # server-first
104
+ feeds, update = server_feeds(lang, api_base) # server-first, used AS-IS
88
105
  except Exception:
89
- feeds = None # any issue -> local fallback
90
- feeds = feeds or local_urls or default
91
-
92
- # topic preference leads (keyless Google News topic feed), general as backup
93
- tfeed = topic_feed(lang, os.environ.get("NEWSLINE_COUNTRY", ""), os.environ.get("NEWSLINE_TOPIC", ""))
94
- if tfeed:
95
- 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]
96
114
 
97
115
  if not feeds:
98
116
  return 1
99
- 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))
100
121
  return 0
101
122
 
102
123
 
package/statusline.sh CHANGED
@@ -31,6 +31,8 @@ if [ -s "$CACHE_FILE" ]; then
31
31
  n=$(awk 'END{print NR}' "$CACHE_FILE" 2>/dev/null)
32
32
  if [ "${n:-0}" -gt 0 ] 2>/dev/null; then
33
33
  rot="${NEWSLINE_ROTATE:-6}" # seconds each headline stays up
34
+ case "$rot" in ''|*[!0-9]*) rot=6 ;; esac
35
+ [ "$rot" -ge 1 ] || rot=6 # 0 would divide by zero below
34
36
  now="${NEWSLINE_NOW:-$(date +%s)}" # NEWSLINE_NOW overrides clock (tests/debug)
35
37
  idx=$(( (now / rot) % n + 1 ))
36
38
  sed -n "${idx}p" "$CACHE_FILE"
@@ -40,3 +42,10 @@ if [ -s "$CACHE_FILE" ]; then
40
42
  else
41
43
  printf '📰 …' # first run, before the first refresh lands
42
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