claudenews 1.0.0

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 ADDED
@@ -0,0 +1,97 @@
1
+ # claudenews
2
+
3
+ News headlines in your Claude Code spinner. Instead of "Thinking..." you get real headlines from Hacker News, Reddit, Lobsters, dev.to, and GitHub Trending.
4
+
5
+ ```
6
+ * [HN] UUID package coming to Go standard library
7
+ ● Searching for 1 pattern, reading 7 files...
8
+ ```
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npx claudenews
14
+ ```
15
+
16
+ Or install globally:
17
+
18
+ ```bash
19
+ npm install -g claudenews
20
+ claudenews
21
+ ```
22
+
23
+ That's it. First run automatically:
24
+ - Creates config at `~/.claudenews/config.json`
25
+ - Fetches headlines from your enabled sources
26
+ - Installs Claude Code hooks for automatic refresh
27
+ - Writes headlines to your spinner
28
+
29
+ ## Pick your sources
30
+
31
+ Run `claudenews` to open the source picker:
32
+
33
+ ```
34
+ claudenews — pick your news sources
35
+
36
+ > [x] [HN] Hacker News news.ycombinator.com
37
+ [ ] [/r] Reddit reddit.com
38
+ [ ] [🦞] Lobsters lobste.rs
39
+ [ ] [dev] dev.to dev.to
40
+ [ ] [GH] GitHub Trending github.com/trending
41
+
42
+ [Space] Toggle [r] Refresh [q] Quit built by souls.zip
43
+ ```
44
+
45
+ Headlines show up in your Claude Code spinner with source prefixes:
46
+
47
+ - `[HN]` — Hacker News
48
+ - `[/r]` — Reddit (r/programming, r/technology)
49
+ - `[🦞]` — Lobsters
50
+ - `[dev]` — dev.to
51
+ - `[GH]` — GitHub Trending
52
+
53
+ ## How it works
54
+
55
+ - **Session start hook** fetches fresh headlines every time you start Claude Code
56
+ - **Pre-tool-use hook** refreshes if headlines are older than 30 minutes
57
+ - Headlines are written to `spinnerVerbs` in `~/.claude/settings.json`
58
+ - Claude Code reads spinner verbs at session startup
59
+
60
+ ## Configuration
61
+
62
+ Config lives at `~/.claudenews/config.json`:
63
+
64
+ ```json
65
+ {
66
+ "headlineCount": 15,
67
+ "refreshIntervalMinutes": 30,
68
+ "enabledSources": ["hackernews"],
69
+ "redditSubs": ["programming", "technology"]
70
+ }
71
+ ```
72
+
73
+ - **headlineCount** — headlines per source (default: 15)
74
+ - **refreshIntervalMinutes** — how often to fetch new headlines (default: 30)
75
+ - **enabledSources** — which sources to pull from
76
+ - **redditSubs** — subreddits to include when Reddit is enabled
77
+
78
+ ## Uninstall
79
+
80
+ ```bash
81
+ claudenews --uninstall
82
+ ```
83
+
84
+ Removes hooks, restores default spinner, and deletes config.
85
+
86
+ ## Requirements
87
+
88
+ - Node.js 18+
89
+ - Claude Code
90
+
91
+ ## License
92
+
93
+ MIT
94
+
95
+ ---
96
+
97
+ Built by [souls.zip](https://souls.zip)
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import '../dist/cli.js';
3
+
package/dist/app.d.ts ADDED
@@ -0,0 +1 @@
1
+ export default function App(): import("react/jsx-runtime").JSX.Element;
package/dist/app.js ADDED
@@ -0,0 +1,80 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect } from 'react';
3
+ import { Box, Text, useInput, useApp } from 'ink';
4
+ import { readConfig, writeConfig } from './lib/config.js';
5
+ import { getAllSources } from './lib/sources/index.js';
6
+ import { refresh } from './lib/refresh.js';
7
+ export default function App() {
8
+ const { exit } = useApp();
9
+ const [sources, setSources] = useState([]);
10
+ const [enabled, setEnabled] = useState([]);
11
+ const [cursor, setCursor] = useState(0);
12
+ const [status, setStatus] = useState('');
13
+ const [refreshing, setRefreshing] = useState(false);
14
+ useEffect(() => {
15
+ (async () => {
16
+ const config = await readConfig();
17
+ setEnabled(config.enabledSources);
18
+ setSources(getAllSources());
19
+ // Auto-refresh on open
20
+ try {
21
+ await refresh();
22
+ setStatus('Headlines updated — restart Claude Code to apply');
23
+ }
24
+ catch { }
25
+ })();
26
+ }, []);
27
+ // Periodic refresh
28
+ useEffect(() => {
29
+ const id = setInterval(async () => {
30
+ try {
31
+ await refresh();
32
+ setStatus('Headlines updated — restart Claude Code to apply');
33
+ }
34
+ catch { }
35
+ }, 30 * 60 * 1000);
36
+ return () => clearInterval(id);
37
+ }, []);
38
+ useInput((input, key) => {
39
+ if (input === 'q')
40
+ return exit();
41
+ if (key.upArrow || input === 'k')
42
+ setCursor(i => Math.max(0, i - 1));
43
+ if (key.downArrow || input === 'j')
44
+ setCursor(i => Math.min(sources.length - 1, i + 1));
45
+ if (input === ' ') {
46
+ const src = sources[cursor];
47
+ if (!src)
48
+ return;
49
+ setEnabled(prev => {
50
+ const next = prev.includes(src.id)
51
+ ? prev.filter(id => id !== src.id)
52
+ : [...prev, src.id];
53
+ setStatus('Saving...');
54
+ readConfig().then(config => {
55
+ writeConfig({ ...config, enabledSources: next })
56
+ .then(() => refresh())
57
+ .then(() => setStatus('Updated — restart Claude Code to apply'))
58
+ .catch(() => setStatus('Failed to refresh'));
59
+ });
60
+ return next;
61
+ });
62
+ }
63
+ if (input === 'r' && !refreshing) {
64
+ setRefreshing(true);
65
+ setStatus('Refreshing headlines...');
66
+ refresh()
67
+ .then(() => setStatus('Refreshed — restart Claude Code to apply'))
68
+ .catch(() => setStatus('Failed to refresh'))
69
+ .finally(() => setRefreshing(false));
70
+ }
71
+ });
72
+ if (sources.length === 0) {
73
+ return _jsx(Text, { dimColor: true, children: "Loading..." });
74
+ }
75
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, children: "claudenews" }), _jsx(Text, { dimColor: true, children: " \u2014 pick your news sources" })] }), sources.map((src, i) => {
76
+ const active = i === cursor;
77
+ const on = enabled.includes(src.id);
78
+ return (_jsxs(Box, { children: [_jsxs(Text, { bold: active, children: [active ? '> ' : ' ', on ? '[x]' : '[ ]', " ", src.prefix, " ", src.name] }), _jsxs(Text, { dimColor: true, children: [" ", src.url] })] }, src.id));
79
+ }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "[Space] Toggle [r] Refresh [q] Quit | by souls.zip & @tolibear_" }) }), status && (_jsx(Box, { marginTop: 0, children: _jsx(Text, { dimColor: true, children: status }) }))] }));
80
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,40 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { mkdir, rm } from 'node:fs/promises';
3
+ import React from 'react';
4
+ import { render } from 'ink';
5
+ import { getConfigDir, readConfig, writeConfig } from './lib/config.js';
6
+ import { refresh } from './lib/refresh.js';
7
+ import { installHooks, removeHooks, restoreDefaultVerbs } from './lib/settings.js';
8
+ import App from './app.js';
9
+ async function autoSetup() {
10
+ const configPath = `${getConfigDir()}/config.json`;
11
+ if (existsSync(configPath))
12
+ return;
13
+ await mkdir(getConfigDir(), { recursive: true });
14
+ await writeConfig(await readConfig());
15
+ try {
16
+ await refresh();
17
+ }
18
+ catch { }
19
+ await installHooks();
20
+ }
21
+ async function uninstall() {
22
+ console.log('Uninstalling claudenews...\n');
23
+ await removeHooks();
24
+ await restoreDefaultVerbs();
25
+ try {
26
+ await rm(getConfigDir(), { recursive: true });
27
+ console.log(`Removed ${getConfigDir()}`);
28
+ }
29
+ catch { }
30
+ console.log('Done! Spinner restored to defaults.');
31
+ }
32
+ async function main() {
33
+ if (process.argv.includes('--uninstall')) {
34
+ await uninstall();
35
+ return;
36
+ }
37
+ await autoSetup();
38
+ render(React.createElement(App));
39
+ }
40
+ main();
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ import { readConfig } from '../lib/config.js';
2
+ import { refresh, isStale } from '../lib/refresh.js';
3
+ async function main() {
4
+ let input = '';
5
+ for await (const chunk of process.stdin)
6
+ input += chunk;
7
+ try {
8
+ const config = await readConfig();
9
+ if (isStale(config))
10
+ await refresh();
11
+ }
12
+ catch { }
13
+ }
14
+ main();
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ import { refresh } from '../lib/refresh.js';
2
+ async function main() {
3
+ let input = '';
4
+ for await (const chunk of process.stdin)
5
+ input += chunk;
6
+ try {
7
+ await refresh();
8
+ }
9
+ catch { }
10
+ }
11
+ main();
@@ -0,0 +1,10 @@
1
+ export interface Config {
2
+ headlineCount: number;
3
+ refreshIntervalMinutes: number;
4
+ enabledSources: string[];
5
+ redditSubs: string[];
6
+ lastRefresh?: string;
7
+ }
8
+ export declare function getConfigDir(): string;
9
+ export declare function readConfig(): Promise<Config>;
10
+ export declare function writeConfig(config: Config): Promise<void>;
@@ -0,0 +1,29 @@
1
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ const defaultConfig = {
5
+ headlineCount: 15,
6
+ refreshIntervalMinutes: 30,
7
+ enabledSources: ['hackernews'],
8
+ redditSubs: ['programming', 'technology'],
9
+ };
10
+ export function getConfigDir() {
11
+ return join(homedir(), '.claudenews');
12
+ }
13
+ function getConfigPath() {
14
+ return join(getConfigDir(), 'config.json');
15
+ }
16
+ export async function readConfig() {
17
+ try {
18
+ const raw = await readFile(getConfigPath(), 'utf-8');
19
+ return { ...defaultConfig, ...JSON.parse(raw) };
20
+ }
21
+ catch {
22
+ return { ...defaultConfig };
23
+ }
24
+ }
25
+ export async function writeConfig(config) {
26
+ const dir = getConfigDir();
27
+ await mkdir(dir, { recursive: true });
28
+ await writeFile(getConfigPath(), JSON.stringify(config, null, 2) + '\n');
29
+ }
@@ -0,0 +1,5 @@
1
+ export declare function refresh(): Promise<void>;
2
+ export declare function isStale(config: {
3
+ lastRefresh?: string;
4
+ refreshIntervalMinutes: number;
5
+ }): boolean;
@@ -0,0 +1,23 @@
1
+ import { readConfig, writeConfig } from './config.js';
2
+ import { fetchAllHeadlines, getPrefix } from './sources/index.js';
3
+ import { writeSpinnerVerbs } from './settings.js';
4
+ export async function refresh() {
5
+ const config = await readConfig();
6
+ const headlines = await fetchAllHeadlines(config.enabledSources, config.headlineCount);
7
+ const MAX_LEN = 80;
8
+ const verbs = headlines
9
+ .map(h => {
10
+ const prefix = getPrefix(h.source);
11
+ const full = `${prefix} ${h.title}`;
12
+ return full.length > MAX_LEN ? `${full.slice(0, MAX_LEN - 1)}…` : full;
13
+ })
14
+ .filter(v => v.length > 0);
15
+ await writeSpinnerVerbs(verbs);
16
+ await writeConfig({ ...config, lastRefresh: new Date().toISOString() });
17
+ }
18
+ export function isStale(config) {
19
+ if (!config.lastRefresh)
20
+ return true;
21
+ const age = Date.now() - new Date(config.lastRefresh).getTime();
22
+ return age > config.refreshIntervalMinutes * 60 * 1000;
23
+ }
@@ -0,0 +1,4 @@
1
+ export declare function writeSpinnerVerbs(verbs: string[]): Promise<void>;
2
+ export declare function restoreDefaultVerbs(): Promise<void>;
3
+ export declare function installHooks(): Promise<void>;
4
+ export declare function removeHooks(): Promise<void>;
@@ -0,0 +1,69 @@
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { join, resolve, dirname } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ const SETTINGS_PATH = join(homedir(), '.claude', 'settings.json');
6
+ async function readSettings() {
7
+ try {
8
+ const raw = await readFile(SETTINGS_PATH, 'utf-8');
9
+ return JSON.parse(raw);
10
+ }
11
+ catch {
12
+ return {};
13
+ }
14
+ }
15
+ async function saveSettings(settings) {
16
+ await writeFile(SETTINGS_PATH, JSON.stringify(settings, null, 2) + '\n');
17
+ }
18
+ export async function writeSpinnerVerbs(verbs) {
19
+ const settings = await readSettings();
20
+ settings.spinnerVerbs = { mode: 'replace', verbs };
21
+ await saveSettings(settings);
22
+ }
23
+ export async function restoreDefaultVerbs() {
24
+ const settings = await readSettings();
25
+ delete settings.spinnerVerbs;
26
+ await saveSettings(settings);
27
+ }
28
+ function getDistDir() {
29
+ return resolve(dirname(fileURLToPath(import.meta.url)), '..');
30
+ }
31
+ function getHookCommand(scriptName) {
32
+ return `node "${join(getDistDir(), 'hooks', scriptName)}"`;
33
+ }
34
+ const MARKER = 'claudenews';
35
+ export async function installHooks() {
36
+ const settings = await readSettings();
37
+ if (!settings.hooks)
38
+ settings.hooks = {};
39
+ for (const [event, script, timeout] of [
40
+ ['SessionStart', 'on-session-start.js', 15000],
41
+ ['PreToolUse', 'on-pre-tool-use.js', 10000],
42
+ ]) {
43
+ const cmd = getHookCommand(script);
44
+ if (!settings.hooks[event])
45
+ settings.hooks[event] = [];
46
+ const existing = settings.hooks[event].find(e => e.hooks?.some(h => h.command.includes(MARKER)));
47
+ const hookEntry = { type: 'command', command: cmd, timeout };
48
+ if (existing) {
49
+ existing.hooks = [hookEntry];
50
+ }
51
+ else {
52
+ settings.hooks[event].push({ hooks: [hookEntry] });
53
+ }
54
+ }
55
+ await saveSettings(settings);
56
+ }
57
+ export async function removeHooks() {
58
+ const settings = await readSettings();
59
+ if (!settings.hooks)
60
+ return;
61
+ for (const event of Object.keys(settings.hooks)) {
62
+ settings.hooks[event] = settings.hooks[event].filter(e => !e.hooks?.some(h => h.command.includes(MARKER)));
63
+ if (settings.hooks[event].length === 0)
64
+ delete settings.hooks[event];
65
+ }
66
+ if (Object.keys(settings.hooks).length === 0)
67
+ delete settings.hooks;
68
+ await saveSettings(settings);
69
+ }
@@ -0,0 +1,3 @@
1
+ import type { SourceDef } from '../types.js';
2
+ declare const devto: SourceDef;
3
+ export default devto;
@@ -0,0 +1,20 @@
1
+ const devto = {
2
+ id: 'devto',
3
+ name: 'dev.to',
4
+ prefix: '[dev]',
5
+ url: 'dev.to',
6
+ defaultEnabled: false,
7
+ async fetch(count) {
8
+ const ctrl = new AbortController();
9
+ const t = setTimeout(() => ctrl.abort(), 10_000);
10
+ try {
11
+ const r = await fetch(`https://dev.to/api/articles?per_page=${count}`, { signal: ctrl.signal });
12
+ const items = await r.json();
13
+ return items.map(i => ({ title: i.title, source: 'devto' }));
14
+ }
15
+ finally {
16
+ clearTimeout(t);
17
+ }
18
+ },
19
+ };
20
+ export default devto;
@@ -0,0 +1,3 @@
1
+ import type { SourceDef } from '../types.js';
2
+ declare const githubTrending: SourceDef;
3
+ export default githubTrending;
@@ -0,0 +1,23 @@
1
+ const githubTrending = {
2
+ id: 'github',
3
+ name: 'GitHub Trending',
4
+ prefix: '[GH]',
5
+ url: 'github.com/trending',
6
+ defaultEnabled: false,
7
+ async fetch(count) {
8
+ const ctrl = new AbortController();
9
+ const t = setTimeout(() => ctrl.abort(), 10_000);
10
+ try {
11
+ const r = await fetch('https://ghapi.huchen.dev/repositories?since=daily', { signal: ctrl.signal });
12
+ const repos = await r.json();
13
+ return repos.slice(0, count).map(i => ({
14
+ title: i.description ? `${i.author}/${i.name} — ${i.description}` : `${i.author}/${i.name}`,
15
+ source: 'github',
16
+ }));
17
+ }
18
+ finally {
19
+ clearTimeout(t);
20
+ }
21
+ },
22
+ };
23
+ export default githubTrending;
@@ -0,0 +1,3 @@
1
+ import type { SourceDef } from '../types.js';
2
+ declare const hackernews: SourceDef;
3
+ export default hackernews;
@@ -0,0 +1,24 @@
1
+ const hackernews = {
2
+ id: 'hackernews',
3
+ name: 'Hacker News',
4
+ prefix: '[HN]',
5
+ url: 'news.ycombinator.com',
6
+ defaultEnabled: true,
7
+ async fetch(count) {
8
+ const ctrl = new AbortController();
9
+ const t = setTimeout(() => ctrl.abort(), 10_000);
10
+ try {
11
+ const res = await fetch('https://hacker-news.firebaseio.com/v0/topstories.json', { signal: ctrl.signal });
12
+ const ids = await res.json();
13
+ const items = await Promise.all(ids.slice(0, count).map(async (id) => {
14
+ const r = await fetch(`https://hacker-news.firebaseio.com/v0/item/${id}.json`, { signal: ctrl.signal });
15
+ return r.json();
16
+ }));
17
+ return items.filter(i => i?.title).map(i => ({ title: i.title, source: 'hackernews' }));
18
+ }
19
+ finally {
20
+ clearTimeout(t);
21
+ }
22
+ },
23
+ };
24
+ export default hackernews;
@@ -0,0 +1,4 @@
1
+ import type { Headline, SourceDef } from '../types.js';
2
+ export declare function getAllSources(): SourceDef[];
3
+ export declare function getPrefix(source: string): string;
4
+ export declare function fetchAllHeadlines(enabledIds: string[], countPerSource: number): Promise<Headline[]>;
@@ -0,0 +1,27 @@
1
+ import hackernews from './hackernews.js';
2
+ import reddit from './reddit.js';
3
+ import lobsters from './lobsters.js';
4
+ import devto from './devto.js';
5
+ import githubTrending from './github-trending.js';
6
+ const allSources = [hackernews, reddit, lobsters, devto, githubTrending];
7
+ export function getAllSources() {
8
+ return allSources;
9
+ }
10
+ export function getPrefix(source) {
11
+ return allSources.find(s => s.id === source)?.prefix ?? `[${source}]`;
12
+ }
13
+ export async function fetchAllHeadlines(enabledIds, countPerSource) {
14
+ const sources = allSources.filter(s => enabledIds.includes(s.id));
15
+ const results = await Promise.allSettled(sources.map(s => s.fetch(countPerSource)));
16
+ const headlines = [];
17
+ for (const result of results) {
18
+ if (result.status === 'fulfilled')
19
+ headlines.push(...result.value);
20
+ }
21
+ // Fisher-Yates shuffle
22
+ for (let i = headlines.length - 1; i > 0; i--) {
23
+ const j = Math.floor(Math.random() * (i + 1));
24
+ [headlines[i], headlines[j]] = [headlines[j], headlines[i]];
25
+ }
26
+ return headlines;
27
+ }
@@ -0,0 +1,3 @@
1
+ import type { SourceDef } from '../types.js';
2
+ declare const lobsters: SourceDef;
3
+ export default lobsters;
@@ -0,0 +1,20 @@
1
+ const lobsters = {
2
+ id: 'lobsters',
3
+ name: 'Lobsters',
4
+ prefix: '[🦞]',
5
+ url: 'lobste.rs',
6
+ defaultEnabled: false,
7
+ async fetch(count) {
8
+ const ctrl = new AbortController();
9
+ const t = setTimeout(() => ctrl.abort(), 10_000);
10
+ try {
11
+ const r = await fetch('https://lobste.rs/hottest.json', { signal: ctrl.signal });
12
+ const items = await r.json();
13
+ return items.slice(0, count).map(i => ({ title: i.title, source: 'lobsters' }));
14
+ }
15
+ finally {
16
+ clearTimeout(t);
17
+ }
18
+ },
19
+ };
20
+ export default lobsters;
@@ -0,0 +1,3 @@
1
+ import type { SourceDef } from '../types.js';
2
+ declare const reddit: SourceDef;
3
+ export default reddit;
@@ -0,0 +1,28 @@
1
+ import { readConfig } from '../config.js';
2
+ const reddit = {
3
+ id: 'reddit',
4
+ name: 'Reddit',
5
+ prefix: '[/r]',
6
+ url: 'reddit.com',
7
+ defaultEnabled: false,
8
+ async fetch(count) {
9
+ const ctrl = new AbortController();
10
+ const t = setTimeout(() => ctrl.abort(), 10_000);
11
+ try {
12
+ const config = await readConfig();
13
+ const perSub = Math.max(1, Math.floor(count / config.redditSubs.length));
14
+ const results = await Promise.all(config.redditSubs.map(async (sub) => {
15
+ const r = await fetch(`https://www.reddit.com/r/${sub}/hot.json?limit=${perSub}`, {
16
+ signal: ctrl.signal,
17
+ headers: { 'User-Agent': 'claudenews/1.0' },
18
+ });
19
+ return r.json();
20
+ }));
21
+ return results.flatMap(r => r.data.children.map(c => ({ title: c.data.title, source: 'reddit' })));
22
+ }
23
+ finally {
24
+ clearTimeout(t);
25
+ }
26
+ },
27
+ };
28
+ export default reddit;
@@ -0,0 +1,12 @@
1
+ export interface Headline {
2
+ title: string;
3
+ source: string;
4
+ }
5
+ export interface SourceDef {
6
+ id: string;
7
+ name: string;
8
+ prefix: string;
9
+ url: string;
10
+ defaultEnabled: boolean;
11
+ fetch(count: number): Promise<Headline[]>;
12
+ }
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "claudenews",
3
+ "version": "1.0.0",
4
+ "description": "News headlines in your Claude Code spinner",
5
+ "type": "module",
6
+ "bin": {
7
+ "claudenews": "./bin/claudenews.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "dev": "tsc --watch",
12
+ "prepublishOnly": "npm run build"
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "bin"
17
+ ],
18
+ "keywords": [
19
+ "claude",
20
+ "claude-code",
21
+ "news",
22
+ "spinner",
23
+ "hackernews",
24
+ "cli",
25
+ "tui"
26
+ ],
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/tolibear/claudenews"
30
+ },
31
+ "author": "souls.zip",
32
+ "homepage": "https://github.com/tolibear/claudenews",
33
+ "dependencies": {
34
+ "ink": "^5.2.0",
35
+ "react": "^18.3.0"
36
+ },
37
+ "devDependencies": {
38
+ "typescript": "^5.7.0",
39
+ "@types/react": "^18.3.0",
40
+ "@types/node": "^22.0.0"
41
+ },
42
+ "engines": {
43
+ "node": ">=18.0.0"
44
+ },
45
+ "license": "MIT"
46
+ }