@1a35e1/sonar-cli 0.2.1 → 0.3.5

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 (43) hide show
  1. package/README.md +151 -265
  2. package/dist/commands/{inbox/archive.js → archive.js} +2 -2
  3. package/dist/commands/config/data/download.js +2 -2
  4. package/dist/commands/config/data/sync.js +2 -2
  5. package/dist/commands/config/nuke.js +20 -2
  6. package/dist/commands/feed.js +105 -155
  7. package/dist/commands/index.js +172 -4
  8. package/dist/commands/{inbox/later.js → later.js} +2 -2
  9. package/dist/commands/refresh.js +41 -0
  10. package/dist/commands/{inbox/skip.js → skip.js} +2 -2
  11. package/dist/commands/status.js +128 -0
  12. package/dist/commands/sync/bookmarks.js +35 -0
  13. package/dist/commands/topics/add.js +71 -0
  14. package/dist/commands/topics/delete.js +42 -0
  15. package/dist/commands/topics/edit.js +97 -0
  16. package/dist/commands/topics/index.js +54 -0
  17. package/dist/commands/topics/suggest.js +125 -0
  18. package/dist/commands/topics/view.js +48 -0
  19. package/dist/components/AccountCard.js +1 -1
  20. package/dist/components/Banner.js +11 -0
  21. package/dist/components/InteractiveSession.js +95 -210
  22. package/dist/components/Spinner.js +5 -4
  23. package/dist/components/TopicCard.js +15 -0
  24. package/dist/components/TweetCard.js +76 -0
  25. package/dist/lib/ai.js +85 -0
  26. package/dist/lib/client.js +66 -40
  27. package/dist/lib/config.js +3 -2
  28. package/dist/lib/data-queries.js +1 -3
  29. package/dist/lib/skill.js +66 -226
  30. package/package.json +13 -3
  31. package/dist/commands/account.js +0 -75
  32. package/dist/commands/inbox/index.js +0 -103
  33. package/dist/commands/inbox/read.js +0 -41
  34. package/dist/commands/ingest/bookmarks.js +0 -55
  35. package/dist/commands/ingest/index.js +0 -5
  36. package/dist/commands/ingest/tweets.js +0 -55
  37. package/dist/commands/interests/create.js +0 -107
  38. package/dist/commands/interests/index.js +0 -56
  39. package/dist/commands/interests/match.js +0 -33
  40. package/dist/commands/interests/update.js +0 -153
  41. package/dist/commands/monitor.js +0 -93
  42. package/dist/commands/quickstart.js +0 -231
  43. package/dist/components/InterestCard.js +0 -10
@@ -1,55 +1,81 @@
1
1
  import { getApiUrl, getToken } from './config.js';
2
+ const MAX_RETRIES = Math.max(0, Number(process.env.SONAR_MAX_RETRIES) || 3);
3
+ function sleep(ms) {
4
+ return new Promise(resolve => setTimeout(resolve, ms));
5
+ }
6
+ function retryDelay(attempt) {
7
+ const base = Math.min(1000 * 2 ** attempt, 10_000);
8
+ return base + Math.random() * 500;
9
+ }
2
10
  /**
3
11
  * Execute a GraphQL request against the Sonar API.
4
12
  *
13
+ * Retries transient failures (network errors, 5xx) with jittered exponential
14
+ * backoff. Deterministic failures (4xx, GraphQL errors) throw immediately.
15
+ * Control retries via SONAR_MAX_RETRIES env var (default 3, 0 to disable).
16
+ *
5
17
  * A hard timeout (default 20 s) is applied via AbortController so that the
6
- * process never hangs silently when the server is unresponsive. The timeout
7
- * is intentionally surfaced as a distinct error so callers can give operators
8
- * an actionable message (e.g. "check server health / retry").
18
+ * process never hangs silently when the server is unresponsive.
9
19
  */
10
20
  export async function gql(query, variables = {}, flags = {}) {
11
21
  const token = getToken();
12
22
  const url = getApiUrl();
13
23
  const timeoutMs = flags.timeoutMs ?? 20_000;
14
- const controller = new AbortController();
15
- const timer = setTimeout(() => controller.abort(), timeoutMs);
16
- let res;
17
- try {
18
- if (flags.debug) {
19
- console.error(url, query, variables);
24
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
25
+ const controller = new AbortController();
26
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
27
+ let res;
28
+ try {
29
+ if (flags.debug) {
30
+ console.error(url, query, variables);
31
+ }
32
+ res = await fetch(url, {
33
+ method: 'POST',
34
+ signal: controller.signal,
35
+ headers: {
36
+ 'Content-Type': 'application/json',
37
+ Authorization: `Bearer ${token}`,
38
+ },
39
+ body: JSON.stringify({ query, variables }),
40
+ });
20
41
  }
21
- console.log('url', url);
22
- res = await fetch(url, {
23
- method: 'POST',
24
- signal: controller.signal,
25
- headers: {
26
- 'Content-Type': 'application/json',
27
- Authorization: `Bearer ${token}`,
28
- },
29
- body: JSON.stringify({ query, variables }),
30
- });
31
- }
32
- catch (err) {
33
- clearTimeout(timer);
34
- if (err instanceof DOMException && err.name === 'AbortError') {
35
- throw new Error(`Request timed out after ${timeoutMs / 1000}s. ` +
36
- 'The server may be overloaded or unreachable. ' +
37
- 'Check SONAR_API_URL, your network connection, and retry.');
42
+ catch (err) {
43
+ clearTimeout(timer);
44
+ if (attempt < MAX_RETRIES) {
45
+ if (flags.debug)
46
+ console.error(`Retry ${attempt + 1}/${MAX_RETRIES} after network error`);
47
+ await sleep(retryDelay(attempt));
48
+ continue;
49
+ }
50
+ if (err instanceof DOMException && err.name === 'AbortError') {
51
+ throw new Error(`Request timed out after ${timeoutMs / 1000}s. ` +
52
+ 'The server may be overloaded or unreachable. ' +
53
+ 'Check SONAR_API_URL, your network connection, and retry.');
54
+ }
55
+ throw new Error('Unable to reach server, please try again shortly.');
38
56
  }
39
- throw new Error('Unable to reach server, please try again shortly.');
40
- }
41
- finally {
42
- clearTimeout(timer);
43
- }
44
- if (!res.ok) {
45
- if (flags.debug) {
46
- console.error(JSON.stringify(await res.json(), null, 2));
57
+ finally {
58
+ clearTimeout(timer);
47
59
  }
48
- throw new Error(`HTTP ${res.status}: ${res.statusText}`);
49
- }
50
- const json = (await res.json());
51
- if (json.errors && json.errors.length > 0) {
52
- throw new Error(json.errors[0].message);
60
+ // 5xx transient, retry
61
+ if (res.status >= 500 && attempt < MAX_RETRIES) {
62
+ if (flags.debug)
63
+ console.error(`Retry ${attempt + 1}/${MAX_RETRIES} after HTTP ${res.status}`);
64
+ await sleep(retryDelay(attempt));
65
+ continue;
66
+ }
67
+ // 4xx — deterministic, throw immediately
68
+ if (!res.ok) {
69
+ if (flags.debug) {
70
+ console.error(JSON.stringify(await res.json(), null, 2));
71
+ }
72
+ throw new Error(`HTTP ${res.status}: ${res.statusText}`);
73
+ }
74
+ const json = (await res.json());
75
+ if (json.errors && json.errors.length > 0) {
76
+ throw new Error(json.errors[0].message);
77
+ }
78
+ return json.data;
53
79
  }
54
- return json.data;
80
+ throw new Error('Unexpected retry exhaustion');
55
81
  }
@@ -1,6 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
3
  import { join } from 'node:path';
4
+ import { DB_PATH } from './db.js';
4
5
  const CONFIG_DIR = join(homedir(), '.sonar');
5
6
  const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
6
7
  export function readConfig() {
@@ -24,8 +25,8 @@ export function deleteConfig() {
24
25
  }
25
26
  }
26
27
  export function deleteDatabase() {
27
- if (existsSync(join(CONFIG_DIR, 'database.sqlite'))) {
28
- unlinkSync(join(CONFIG_DIR, 'database.sqlite'));
28
+ if (existsSync(DB_PATH)) {
29
+ unlinkSync(DB_PATH);
29
30
  }
30
31
  }
31
32
  export function writeConfig(config) {
@@ -48,12 +48,10 @@ export const SUGGESTIONS_QUERY = `
48
48
  `;
49
49
  export const INTERESTS_QUERY = `
50
50
  query DataInterests {
51
- projects {
51
+ topics {
52
52
  id: nanoId
53
53
  name
54
54
  description
55
- keywords
56
- relatedTopics
57
55
  createdAt
58
56
  updatedAt
59
57
  }
package/dist/lib/skill.js CHANGED
@@ -3,7 +3,7 @@ import { join, dirname } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
4
  const SKILL_CONTENT = `---
5
5
  name: sonar
6
- description: Sonar CLI — manage interests, suggestions, indexing jobs, and account config for the Sonar social intelligence platform. Use when the user asks about their Sonar account, wants to create/list interests, check suggestions, trigger indexing, or configure the CLI.
6
+ description: Sonar CLI — view and triage your feed, manage topics, trigger refresh jobs, and manage local Sonar config/data.
7
7
  homepage: https://sonar.sh
8
8
  user-invocable: true
9
9
  allowed-tools: Bash
@@ -13,261 +13,101 @@ metadata: {"openclaw":{"emoji":"📡","primaryEnv":"SONAR_API_KEY","requires":{"
13
13
 
14
14
  # Sonar CLI
15
15
 
16
- Sonar is a social intelligence platform. Use the \`sonar\` CLI to manage the user's account.
16
+ All commands are invoked as: \`sonar <command> [subcommand] [flags]\`.
17
17
 
18
- All commands are invoked as: \`sonar <command> [subcommand] [flags]\`
19
-
20
- ---
21
-
22
- ## Account & Config
18
+ ## Core usage
23
19
 
24
20
  \`\`\`bash
25
- # Show account info, plan usage, and suggestion counts
26
- sonar account
27
-
28
- # Show current CLI config (API URL, vendor, token presence)
29
- sonar config
30
-
31
- # Set AI vendor preference for --from-prompt (saved to ~/.sonar/config.json)
32
- sonar config set vendor openai # or: anthropic
33
-
34
- # Initialise workspace from environment variables
35
- # Requires: SONAR_API_KEY
36
- sonar config setup
21
+ # Default view (combined ranked stream from feed + inbox)
22
+ sonar
23
+ sonar --hours 24
24
+ sonar --days 3
25
+ sonar --kind default # default | bookmarks | followers | following
26
+ sonar --limit 50
27
+ sonar --render card # card | table
28
+ sonar --width 100
29
+ sonar --json
30
+ sonar --no-interactive
37
31
  \`\`\`
38
32
 
39
- ---
40
-
41
- ## Interests
42
-
43
- Interests are named topic areas with keywords and related topics that drive suggestion matching.
33
+ ## Topic management
44
34
 
45
35
  \`\`\`bash
46
- # List all interests
47
- sonar interests
48
-
49
- # Create manually
50
- sonar interests create --name "AI Agents" --description "LLM-based agents and tooling" \\
51
- --keywords "agents,llm,tools,mcp" --topics "machine learning,AI safety"
52
-
53
- # Generate fields from a natural language prompt (uses OPENAI_API_KEY or ANTHROPIC_API_KEY)
54
- sonar interests create --from-prompt "I want to follow the Rust ecosystem and systems programming"
55
-
56
- # Generate with a specific vendor (overrides config preference)
57
- sonar interests create --from-prompt "DeFi and crypto protocols" --vendor anthropic
58
-
59
- # Update an existing interest (full replace)
60
- sonar interests update --id <id> --name "New Name" --keywords "kw1,kw2"
61
-
62
- # Add keywords to an existing interest (fetches current, merges, sends full list)
63
- sonar interests update --id <id> --add-keywords "mcp,a2a,langgraph"
64
-
65
- # Remove keywords from an existing interest
66
- sonar interests update --id <id> --remove-keywords "old-term,deprecated-kw"
67
-
68
- # Add and remove keywords in one shot
69
- sonar interests update --id <id> --add-keywords "vibe-coding" --remove-keywords "cursor"
70
-
71
- # Same flags work for related topics
72
- sonar interests update --id <id> --add-topics "AI safety" --remove-topics "machine learning"
73
-
74
- # Combine keyword/topic patching with a name change
75
- sonar interests update --id <id> --name "New Name" --add-keywords "new-kw"
76
-
77
- # Regenerate all fields from a new prompt (replaces everything)
78
- sonar interests update --id <id> --from-prompt "Rust and WebAssembly tooling"
79
-
80
- # Output raw JSON (agent-friendly)
81
- sonar interests --json
36
+ # List topics
37
+ sonar topics
38
+ sonar topics --json
39
+
40
+ # Add/update topics
41
+ sonar topics add "AI agents"
42
+ sonar topics add "Rust systems programming" --description "..."
43
+ sonar topics edit --id <topic_id> --name "New Name"
44
+ sonar topics edit --id <topic_id> --description "Updated description"
45
+ sonar topics edit --id <topic_id> --json
82
46
  \`\`\`
83
47
 
84
- **AI vendor resolution order:**
85
- 1. \`--vendor\` flag
86
- 2. \`SONAR_AI_VENDOR\` environment variable
87
- 3. \`vendor\` in \`~/.sonar/config.json\` (set via \`sonar config set vendor\`)
88
- 4. Defaults to \`openai\`
89
-
90
- Required env vars: \`OPENAI_API_KEY\` (OpenAI) or \`ANTHROPIC_API_KEY\` (Anthropic)
91
-
92
- ---
93
-
94
- ## Feed
95
-
96
- Scored tweet feed from your social network, filtered by interests.
48
+ ## Pipeline and triage
97
49
 
98
50
  \`\`\`bash
99
- # Show feed (default: last 12h, limit 20, card layout)
100
- sonar feed
101
-
102
- # Time window
103
- sonar feed --hours 24
104
- sonar feed --days 3
105
-
106
- # Limit results
107
- sonar feed --limit 50
108
-
109
- # Output layout
110
- sonar feed --render card # default — rich card view
111
- sonar feed --render table # compact table view
112
- sonar feed --width 100 # card body width in columns
113
-
114
- # Raw JSON output (agent-friendly)
115
- sonar feed --json
51
+ # Trigger full refresh pipeline
52
+ sonar refresh
53
+
54
+ # Monitor account + queues
55
+ sonar status
56
+ sonar status --watch
57
+ sonar status --json
58
+
59
+ # Suggestion actions
60
+ sonar archive --id <suggestion_id>
61
+ sonar later --id <suggestion_id>
62
+ sonar skip --id <suggestion_id>
116
63
  \`\`\`
117
64
 
118
- ---
119
-
120
- ## Suggestions (inbox)
65
+ ## Config and local data
121
66
 
122
67
  \`\`\`bash
123
- # List suggestions (default: inbox, limit 20)
124
- sonar inbox
125
-
126
- # Filter by status
127
- sonar inbox --status inbox
128
- sonar inbox --status later
129
- sonar inbox --status replied
130
- sonar inbox --status archived
131
-
132
- # Change limit
133
- sonar inbox --limit 50
134
-
135
- # Update a suggestion's status (positional id replaced with --id flag)
136
- sonar inbox read --id <id>
137
- sonar inbox skip --id <id>
138
- sonar inbox later --id <id>
139
- sonar inbox archive --id <id>
140
-
141
- # Raw JSON output
142
- sonar inbox --json
143
- \`\`\`
144
-
145
- ---
146
-
147
- ## Ingest
148
-
149
- Trigger background jobs to ingest data.
150
-
151
- \`\`\`bash
152
- # Trigger specific jobs
153
- sonar ingest tweets # Ingest recent tweets from social graph
154
- sonar ingest bookmarks # Ingest X bookmarks (requires OAuth token)
155
- sonar interests match # Match interests against ingested tweets (default: last 24h)
156
-
157
- # Match tweet window (capped by plan: free=3d, pro=7d, enterprise=14d)
158
- sonar interests match --days 1 # default
159
- sonar interests match --days 3 # broader window (free plan max)
160
- sonar interests match --days 7 # pro plan max
161
-
162
- # Show current job queue counts (one-shot)
163
- sonar monitor
164
-
165
- # Live polling view of job queues
166
- sonar monitor --watch
167
- \`\`\`
168
-
169
- ---
170
-
171
- ## Local Data
172
-
173
- Sync feed, suggestions, and interests to a local SQLite DB (\`~/.sonar/data.db\`) for offline querying.
174
-
175
- \`\`\`bash
176
- # Full download — wipes and repopulates ~/.sonar/data.db
68
+ # Show and setup config
69
+ sonar config
70
+ sonar config setup key=<API_KEY>
71
+ sonar config env
72
+ sonar config set vendor openai
73
+ sonar config set vendor anthropic
74
+ sonar config set feed-render card
75
+ sonar config set feed-width 100
76
+
77
+ # Local sqlite data
177
78
  sonar config data download
178
-
179
- # Incremental sync — upserts records newer than last sync
180
79
  sonar config data sync
181
-
182
- # Open an interactive sqlite3 REPL
80
+ sonar config data path
183
81
  sonar config data sql
82
+ sonar config data backup [--out <path>]
83
+ sonar config data restore --from <backup_path> [--to <path>]
84
+ sonar config data verify [--path <db_path>]
184
85
 
185
- # Print path to the local DB file
186
- sonar config data path
86
+ # Export this skill file
87
+ sonar config skill --install
187
88
  \`\`\`
188
89
 
189
- ### Schema
190
-
191
- \`\`\`sql
192
- -- Core tweet content (shared by feed and suggestions)
193
- tweets (
194
- id TEXT PRIMARY KEY, -- Sonar tweet UUID
195
- xid TEXT, -- Twitter/X tweet ID
196
- text TEXT,
197
- created_at TEXT,
198
- like_count INTEGER,
199
- retweet_count INTEGER,
200
- reply_count INTEGER,
201
- author_username TEXT,
202
- author_display_name TEXT,
203
- author_followers_count INTEGER,
204
- author_following_count INTEGER
205
- )
90
+ ## Other commands
206
91
 
207
- -- Feed items (scored, keyword-matched tweets)
208
- feed_items (
209
- tweet_id TEXT PRIMARY KEY, -- FK → tweets.id
210
- score REAL,
211
- matched_keywords TEXT, -- JSON array of strings
212
- synced_at TEXT
213
- )
214
-
215
- -- Inbox suggestions
216
- suggestions (
217
- suggestion_id TEXT PRIMARY KEY,
218
- tweet_id TEXT, -- FK → tweets.id
219
- score REAL,
220
- status TEXT, -- INBOX | READ | SKIPPED | LATER | ARCHIVED
221
- relevance TEXT,
222
- projects_matched TEXT, -- JSON (count of matched interests)
223
- metadata TEXT, -- JSON
224
- synced_at TEXT
225
- )
226
-
227
- -- Interests (topics/keywords that drive matching)
228
- interests (
229
- id TEXT PRIMARY KEY, -- nanoId
230
- name TEXT,
231
- description TEXT,
232
- keywords TEXT, -- JSON array
233
- topics TEXT, -- JSON array
234
- created_at TEXT,
235
- updated_at TEXT,
236
- synced_at TEXT
237
- )
92
+ \`\`\`bash
93
+ # Queue bookmark sync
94
+ sonar sync bookmarks
238
95
 
239
- -- Internal sync state
240
- sync_state (
241
- key TEXT PRIMARY KEY, -- e.g. "last_synced_at"
242
- value TEXT
243
- )
96
+ # Delete local config + local DB (requires explicit confirmation)
97
+ sonar config nuke --confirm
244
98
  \`\`\`
245
99
 
246
- ---
247
-
248
- ## Environment Variables
100
+ ## Environment variables
249
101
 
250
102
  | Variable | Purpose |
251
103
  |---|---|
252
- | \`SONAR_API_KEY\` | API key for authentication (overrides config file) |
253
- | \`SONAR_API_URL\` | Backend URL (default: \`http://localhost:8000/graphql\`) |
254
- | \`SONAR_AI_VENDOR\` | AI vendor for \`--from-prompt\` (overrides config file) |
104
+ | \`SONAR_API_KEY\` | API key for auth (overrides config file token) |
105
+ | \`SONAR_API_URL\` | Backend URL (defaults to production GraphQL endpoint) |
106
+ | \`SONAR_AI_VENDOR\` | Vendor override for AI-assisted operations (\`openai\` or \`anthropic\`) |
107
+ | \`SONAR_FEED_RENDER\` | Default feed renderer override |
108
+ | \`SONAR_FEED_WIDTH\` | Default card width override |
255
109
  | \`OPENAI_API_KEY\` | Required when vendor is \`openai\` |
256
110
  | \`ANTHROPIC_API_KEY\` | Required when vendor is \`anthropic\` |
257
-
258
- ---
259
-
260
- ## Config file
261
-
262
- Stored at \`~/.sonar/config.json\`:
263
-
264
- \`\`\`json
265
- {
266
- "token": "snr_...",
267
- "apiUrl": "https://api.sonar.sh/graphql",
268
- "vendor": "openai"
269
- }
270
- \`\`\`
271
111
  `;
272
112
  const DEFAULT_INSTALL_PATH = join(homedir(), '.claude', 'skills', 'sonar', 'SKILL.md');
273
113
  export function writeSkillTo(dest, install) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@1a35e1/sonar-cli",
3
- "version": "0.2.1",
4
- "description": "X/Twitter social graph CLI for signal filtering and curation",
3
+ "version": "0.3.5",
4
+ "description": "X social graph CLI for signal filtering and curation",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "sonar": "dist/cli.js"
@@ -13,6 +13,10 @@
13
13
  "engines": {
14
14
  "node": ">=20"
15
15
  },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/1a35e1/sonar-cli"
19
+ },
16
20
  "publishConfig": {
17
21
  "access": "public"
18
22
  },
@@ -44,6 +48,12 @@
44
48
  "types": "graphql-codegen --config codegen.ts",
45
49
  "sonar": "tsx src/cli.ts",
46
50
  "build": "tsc",
47
- "typecheck": "tsc --noEmit"
51
+ "typecheck": "tsc --noEmit",
52
+ "drift:schema:check": "node scripts/check-schema-drift.mjs",
53
+ "drift:surface:update": "node scripts/update-command-surface-snapshot.mjs",
54
+ "drift:surface:check": "node scripts/check-command-surface-snapshot.mjs",
55
+ "drift:docs:check": "node scripts/check-doc-command-parity.mjs",
56
+ "drift:data:check": "node scripts/check-data-compat.mjs",
57
+ "drift:check": "pnpm drift:surface:check && pnpm drift:docs:check && pnpm drift:data:check && pnpm drift:schema:check"
48
58
  }
49
59
  }
@@ -1,75 +0,0 @@
1
- import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useEffect, useState } from 'react';
3
- import zod from 'zod';
4
- import { Box, Text } from 'ink';
5
- import { formatDistanceToNow } from 'date-fns';
6
- import { gql } from '../lib/client.js';
7
- import { Spinner } from '../components/Spinner.js';
8
- import { AccountCard } from '../components/AccountCard.js';
9
- export const options = zod.object({
10
- json: zod.boolean().default(false).describe('Raw JSON output'),
11
- debug: zod.boolean().default(false).describe('Debug mode'),
12
- });
13
- const QUERY = `
14
- query Status {
15
- me {
16
- accountId
17
- email
18
- xHandle
19
- xid
20
- isPayingCustomer
21
- indexingAccounts
22
- indexedTweets
23
- pendingEmbeddings
24
- twitterIndexedAt
25
- refreshedSuggestionsAt
26
- }
27
- suggestionCounts {
28
- inbox
29
- later
30
- replied
31
- read
32
- skipped
33
- archived
34
- total
35
- }
36
- usage {
37
- plan
38
- interests { used limit atLimit }
39
- apiKeys { used limit atLimit }
40
- bookmarksEnabled
41
- socialGraphDegrees
42
- socialGraphMaxUsers
43
- suggestionRefreshes { used limit atLimit resetsAt }
44
- }
45
- }
46
- `;
47
- export default function Account({ options: flags }) {
48
- const [data, setData] = useState(null);
49
- const [error, setError] = useState(null);
50
- useEffect(() => {
51
- async function run() {
52
- try {
53
- const result = await gql(QUERY, {}, { debug: flags.debug });
54
- if (flags.json) {
55
- process.stdout.write(JSON.stringify(result, null, 2) + '\n');
56
- process.exit(0);
57
- }
58
- setData(result);
59
- }
60
- catch (err) {
61
- if (flags.debug) {
62
- console.error(JSON.stringify(err, null, 2));
63
- }
64
- setError(err instanceof Error ? err.message : String(err));
65
- }
66
- }
67
- run();
68
- }, []);
69
- if (error)
70
- return _jsxs(Text, { color: "red", children: ["Error: ", error] });
71
- if (!data)
72
- return _jsx(Spinner, { label: "Fetching account..." });
73
- const { me, suggestionCounts, usage } = data;
74
- return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [me ? _jsx(AccountCard, { me: me }) : (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: "cyan", children: "Account" }), _jsx(Text, { dimColor: true, children: "Not authenticated" })] })), usage && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: "cyan", children: "Plan" }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "plan: " }), _jsx(Text, { color: usage.plan === 'free' ? 'yellow' : 'green', children: usage.plan })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "interests: " }), _jsxs(Text, { color: usage.interests.atLimit ? 'red' : undefined, children: [usage.interests.used, usage.interests.limit !== null ? `/${usage.interests.limit}` : ''] })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "api keys: " }), _jsxs(Text, { color: usage.apiKeys.atLimit ? 'red' : undefined, children: [usage.apiKeys.used, usage.apiKeys.limit !== null ? `/${usage.apiKeys.limit}` : ''] })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "bookmarks: " }), usage.bookmarksEnabled ? _jsx(Text, { color: "green", children: "enabled" }) : _jsx(Text, { dimColor: true, children: "upgrade to unlock" })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "social graph: " }), usage.socialGraphDegrees, " degree", usage.socialGraphDegrees !== 1 ? 's' : '', usage.socialGraphMaxUsers !== null ? `, up to ${usage.socialGraphMaxUsers.toLocaleString()} users` : ', unlimited'] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "suggestion refreshes: " }), usage.suggestionRefreshes.limit !== null ? (_jsxs(_Fragment, { children: [_jsxs(Text, { color: usage.suggestionRefreshes.atLimit ? 'red' : undefined, children: [usage.suggestionRefreshes.used, "/", usage.suggestionRefreshes.limit] }), usage.suggestionRefreshes.resetsAt && (_jsxs(Text, { dimColor: true, children: [' ', "(resets ", formatDistanceToNow(new Date(usage.suggestionRefreshes.resetsAt), { addSuffix: true }), ")"] }))] })) : (_jsx(Text, { color: "green", children: "unlimited" }))] })] })), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: "cyan", children: "Suggestions" }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "inbox: " }), _jsx(Text, { color: suggestionCounts.inbox > 0 ? 'green' : undefined, children: suggestionCounts.inbox })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "later: " }), suggestionCounts.later] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "replied: " }), suggestionCounts.replied] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "archived: " }), suggestionCounts.archived] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "total: " }), suggestionCounts.total] })] })] }));
75
- }