@1a35e1/sonar-cli 0.2.0 → 0.2.1

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
@@ -46,8 +46,8 @@ Ingest your first `tweets` and check to `monitor` progress.
46
46
  ```sh
47
47
  sonar ingest tweets
48
48
 
49
- sonar ingest monitor
50
- sonar ingest monitor --watch
49
+ sonar monitor
50
+ sonar monitor --watch
51
51
  ```
52
52
 
53
53
  ---
@@ -44,7 +44,7 @@ export default function Feed({ options: flags }) {
44
44
  ? ' • No bookmarks ingested yet. Run: sonar ingest bookmarks'
45
45
  : ` • No tweets matched your interests in the last ${windowLabel(flags.hours, flags.days)}.`,
46
46
  ' • Check interests are configured: sonar interests',
47
- ' • Ingest may be stale: sonar ingest tweets && sonar ingest monitor',
47
+ ' • Ingest may be stale: sonar ingest tweets && sonar monitor',
48
48
  ' • Widen the window: sonar feed --hours 48',
49
49
  ' • Account/quota status: sonar account',
50
50
  ].join('\n') + '\n');
@@ -68,7 +68,7 @@ export default function Feed({ options: flags }) {
68
68
  }
69
69
  if (data.length === 0) {
70
70
  const kind = flags.kind ?? 'default';
71
- return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { color: "yellow", children: "No tweets found." }), kind === 'bookmarks' ? (_jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsx(Text, { dimColor: true, children: "Your bookmarks feed is empty. Things to check:" }), _jsxs(Text, { dimColor: true, children: [" 1. Ingest bookmarks first: ", _jsx(Text, { color: "cyan", children: "sonar ingest bookmarks" })] }), _jsxs(Text, { dimColor: true, children: [" 2. Then monitor progress: ", _jsx(Text, { color: "cyan", children: "sonar ingest monitor --watch" })] })] })) : (_jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsxs(Text, { dimColor: true, children: ["No ", kind === 'followers' ? 'follower' : kind === 'following' ? 'following' : 'network', " tweets matched your interests in the last ", _jsx(Text, { color: "white", children: windowLabel(flags.hours, flags.days) }), "."] }), _jsx(Text, { dimColor: true, children: "Things to check:" }), _jsxs(Text, { dimColor: true, children: [" 1. Widen the window: ", _jsx(Text, { color: "cyan", children: "sonar feed --hours 48" }), " or ", _jsx(Text, { color: "cyan", children: "--days 7" })] }), _jsxs(Text, { dimColor: true, children: [" 2. Check interests exist: ", _jsx(Text, { color: "cyan", children: "sonar interests" })] }), _jsxs(Text, { dimColor: true, children: [" 3. Trigger ingest if stale: ", _jsx(Text, { color: "cyan", children: "sonar ingest tweets" })] }), _jsxs(Text, { dimColor: true, children: [" 4. Check ingest progress: ", _jsx(Text, { color: "cyan", children: "sonar ingest monitor" })] }), _jsxs(Text, { dimColor: true, children: [" 5. Run matching: ", _jsx(Text, { color: "cyan", children: "sonar interests match" })] })] })), _jsxs(Text, { dimColor: true, children: ["Account status and quota: ", _jsx(Text, { color: "cyan", children: "sonar account" })] })] }));
71
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { color: "yellow", children: "No tweets found." }), kind === 'bookmarks' ? (_jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsx(Text, { dimColor: true, children: "Your bookmarks feed is empty. Things to check:" }), _jsxs(Text, { dimColor: true, children: [" 1. Ingest bookmarks first: ", _jsx(Text, { color: "cyan", children: "sonar ingest bookmarks" })] }), _jsxs(Text, { dimColor: true, children: [" 2. Then monitor progress: ", _jsx(Text, { color: "cyan", children: "sonar monitor --watch" })] })] })) : (_jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsxs(Text, { dimColor: true, children: ["No ", kind === 'followers' ? 'follower' : kind === 'following' ? 'following' : 'network', " tweets matched your interests in the last ", _jsx(Text, { color: "white", children: windowLabel(flags.hours, flags.days) }), "."] }), _jsx(Text, { dimColor: true, children: "Things to check:" }), _jsxs(Text, { dimColor: true, children: [" 1. Widen the window: ", _jsx(Text, { color: "cyan", children: "sonar feed --hours 48" }), " or ", _jsx(Text, { color: "cyan", children: "--days 7" })] }), _jsxs(Text, { dimColor: true, children: [" 2. Check interests exist: ", _jsx(Text, { color: "cyan", children: "sonar interests" })] }), _jsxs(Text, { dimColor: true, children: [" 3. Trigger ingest if stale: ", _jsx(Text, { color: "cyan", children: "sonar ingest tweets" })] }), _jsxs(Text, { dimColor: true, children: [" 4. Check ingest progress: ", _jsx(Text, { color: "cyan", children: "sonar monitor" })] }), _jsxs(Text, { dimColor: true, children: [" 5. Run matching: ", _jsx(Text, { color: "cyan", children: "sonar interests match" })] })] })), _jsxs(Text, { dimColor: true, children: ["Account status and quota: ", _jsx(Text, { color: "cyan", children: "sonar account" })] })] }));
72
72
  }
73
73
  if (flags.interactive) {
74
74
  return _jsx(InteractiveFeedSession, { items: data, vendor: getVendor(flags.vendor) });
@@ -85,7 +85,7 @@ export default function Inbox({ options: flags }) {
85
85
  }
86
86
  if (data.length === 0) {
87
87
  const statusLabel = flags.all ? 'all statuses' : (flags.status ?? 'inbox');
88
- return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Text, { color: "yellow", children: ["Inbox is empty", statusLabel !== 'all statuses' ? ` (status: ${statusLabel})` : '', "."] }), _jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsx(Text, { dimColor: true, children: "Things to check:" }), flags.status && !flags.all && (_jsxs(Text, { dimColor: true, children: [" 1. Broaden scope: ", _jsx(Text, { color: "cyan", children: "sonar inbox --all" })] })), _jsxs(Text, { dimColor: true, children: [" ", flags.status && !flags.all ? '2' : '1', ". Interests defined? ", _jsx(Text, { color: "cyan", children: "sonar interests" })] }), _jsxs(Text, { dimColor: true, children: [" ", flags.status && !flags.all ? '3' : '2', ". Ingest recent tweets: ", _jsx(Text, { color: "cyan", children: "sonar ingest tweets" })] }), _jsxs(Text, { dimColor: true, children: [" ", flags.status && !flags.all ? '4' : '3', ". Run interest matching: ", _jsx(Text, { color: "cyan", children: "sonar interests match" })] }), _jsxs(Text, { dimColor: true, children: [" ", flags.status && !flags.all ? '5' : '4', ". Monitor job progress: ", _jsx(Text, { color: "cyan", children: "sonar ingest monitor" })] })] }), _jsxs(Text, { dimColor: true, children: ["Account status and quota: ", _jsx(Text, { color: "cyan", children: "sonar account" })] })] }));
88
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Text, { color: "yellow", children: ["Inbox is empty", statusLabel !== 'all statuses' ? ` (status: ${statusLabel})` : '', "."] }), _jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsx(Text, { dimColor: true, children: "Things to check:" }), flags.status && !flags.all && (_jsxs(Text, { dimColor: true, children: [" 1. Broaden scope: ", _jsx(Text, { color: "cyan", children: "sonar inbox --all" })] })), _jsxs(Text, { dimColor: true, children: [" ", flags.status && !flags.all ? '2' : '1', ". Interests defined? ", _jsx(Text, { color: "cyan", children: "sonar interests" })] }), _jsxs(Text, { dimColor: true, children: [" ", flags.status && !flags.all ? '3' : '2', ". Ingest recent tweets: ", _jsx(Text, { color: "cyan", children: "sonar ingest tweets" })] }), _jsxs(Text, { dimColor: true, children: [" ", flags.status && !flags.all ? '4' : '3', ". Run interest matching: ", _jsx(Text, { color: "cyan", children: "sonar interests match" })] }), _jsxs(Text, { dimColor: true, children: [" ", flags.status && !flags.all ? '5' : '4', ". Monitor job progress: ", _jsx(Text, { color: "cyan", children: "sonar monitor" })] })] }), _jsxs(Text, { dimColor: true, children: ["Account status and quota: ", _jsx(Text, { color: "cyan", children: "sonar account" })] })] }));
89
89
  }
90
90
  if (flags.interactive) {
91
91
  return _jsx(InteractiveInboxSession, { items: data, vendor: getVendor(flags.vendor) });
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Text, Box } from 'ink';
3
3
  export default function Index() {
4
- return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, children: "Sonar CLI" }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Commands:" }), _jsx(Text, { children: " feed Scored tweet feed from your network" }), _jsx(Text, { children: " inbox Suggestions matching your interests" }), _jsx(Text, { children: " interests Manage interests" }), _jsx(Text, { children: " \u2514\u2500\u2500 create Create a new interest" }), _jsx(Text, { children: " \u2514\u2500\u2500 update Update an interest" }), _jsx(Text, { children: " \u2514\u2500\u2500 match Match interests to ingested tweets" }), _jsx(Text, { children: " ingest Ingest tweets and bookmarks" }), _jsx(Text, { children: " \u2514\u2500\u2500 tweets Ingest recent tweets from social graph" }), _jsx(Text, { children: " \u2514\u2500\u2500 bookmarks Ingest X bookmarks" }), _jsx(Text, { children: " monitor Job queue monitor and account status" }), _jsx(Text, { children: " config Show or set CLI config" }), _jsx(Text, { children: " account Account info and plan usage" })] }), _jsxs(Text, { dimColor: true, children: ["Run ", _jsx(Text, { color: "cyan", children: "sonar <command> --help" }), " for command-specific options."] })] }));
4
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, children: "Sonar CLI" }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Commands:" }), _jsx(Text, { children: " feed Scored tweet feed from your network" }), _jsx(Text, { children: " inbox Suggestions matching your interests" }), _jsx(Text, { children: " interests Manage interests" }), _jsx(Text, { children: " \u2514\u2500\u2500 create Create a new interest" }), _jsx(Text, { children: " \u2514\u2500\u2500 update Update an interest" }), _jsx(Text, { children: " \u2514\u2500\u2500 match Match interests to ingested tweets" }), _jsx(Text, { children: " ingest Ingest tweets and bookmarks" }), _jsx(Text, { children: " \u2514\u2500\u2500 tweets Ingest recent tweets from social graph" }), _jsx(Text, { children: " \u2514\u2500\u2500 bookmarks Ingest X bookmarks" }), _jsx(Text, { children: " monitor Job queue monitor and account status" }), _jsx(Text, { children: " config Show or set CLI config" }), _jsx(Text, { children: " account Account info and plan usage" }), _jsx(Text, { children: " quickstart First-run setup wizard" })] }), _jsxs(Text, { dimColor: true, children: ["Run ", _jsx(Text, { color: "cyan", children: "sonar <command> --help" }), " for command-specific options."] })] }));
5
5
  }
@@ -19,7 +19,7 @@ export default function IndexBookmarks() {
19
19
  setError(`Ingest trigger timed out after ${INGEST_TIMEOUT_MS / 1000}s.\n` +
20
20
  'The server accepted the request but did not respond in time.\n' +
21
21
  'Next steps:\n' +
22
- ' • Run "sonar ingest monitor" — the job may still be queued\n' +
22
+ ' • Run "sonar monitor" — the job may still be queued\n' +
23
23
  ' • Check SONAR_API_URL points to the correct endpoint\n' +
24
24
  ' • Verify the server is healthy and retry');
25
25
  }, INGEST_TIMEOUT_MS);
@@ -47,7 +47,7 @@ export default function IndexBookmarks() {
47
47
  };
48
48
  }, []);
49
49
  if (error) {
50
- return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Text, { color: timedOut ? 'yellow' : 'red', children: [timedOut ? '⚠ ' : 'Error: ', error] }), timedOut && (_jsxs(Text, { dimColor: true, children: ["Tip: run ", _jsx(Text, { color: "cyan", children: "sonar ingest monitor" }), " to check whether the job was queued despite the timeout."] }))] }));
50
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Text, { color: timedOut ? 'yellow' : 'red', children: [timedOut ? '⚠ ' : 'Error: ', error] }), timedOut && (_jsxs(Text, { dimColor: true, children: ["Tip: run ", _jsx(Text, { color: "cyan", children: "sonar monitor" }), " to check whether the job was queued despite the timeout."] }))] }));
51
51
  }
52
52
  if (queued === null)
53
53
  return _jsx(Spinner, { label: "Triggering bookmark indexing..." });
@@ -19,7 +19,7 @@ export default function IndexTweets() {
19
19
  setError(`Ingest trigger timed out after ${INGEST_TIMEOUT_MS / 1000}s.\n` +
20
20
  'The server accepted the request but did not respond in time.\n' +
21
21
  'Next steps:\n' +
22
- ' • Run "sonar ingest monitor" — the job may still be queued\n' +
22
+ ' • Run "sonar monitor" — the job may still be queued\n' +
23
23
  ' • Check SONAR_API_URL points to the correct endpoint\n' +
24
24
  ' • Verify the server is healthy and retry');
25
25
  }, INGEST_TIMEOUT_MS);
@@ -47,7 +47,7 @@ export default function IndexTweets() {
47
47
  };
48
48
  }, []);
49
49
  if (error) {
50
- return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Text, { color: timedOut ? 'yellow' : 'red', children: [timedOut ? '⚠ ' : 'Error: ', error] }), timedOut && (_jsxs(Text, { dimColor: true, children: ["Tip: run ", _jsx(Text, { color: "cyan", children: "sonar ingest monitor" }), " to check whether the job was queued despite the timeout."] }))] }));
50
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Text, { color: timedOut ? 'yellow' : 'red', children: [timedOut ? '⚠ ' : 'Error: ', error] }), timedOut && (_jsxs(Text, { dimColor: true, children: ["Tip: run ", _jsx(Text, { color: "cyan", children: "sonar monitor" }), " to check whether the job was queued despite the timeout."] }))] }));
51
51
  }
52
52
  if (queued === null)
53
53
  return _jsx(Spinner, { label: "Triggering tweet indexing..." });
@@ -0,0 +1,231 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import { Box, Text, useApp, useInput } from 'ink';
4
+ import { gql } from '../lib/client.js';
5
+ import { readConfig } from '../lib/config.js';
6
+ import { Spinner } from '../components/Spinner.js';
7
+ // ─── Queries / Mutations ──────────────────────────────────────────────────────
8
+ const BOOTSTRAP_QUERY = `
9
+ query QuickstartBootstrap {
10
+ me {
11
+ xHandle
12
+ }
13
+ projects {
14
+ id: nanoId
15
+ }
16
+ }
17
+ `;
18
+ const CREATE_MUTATION = `
19
+ mutation CreateOrUpdateInterest(
20
+ $nanoId: String
21
+ $name: String!
22
+ $description: String
23
+ $keywords: [String!]
24
+ $relatedTopics: [String!]
25
+ ) {
26
+ createOrUpdateProject(input: {
27
+ nanoId: $nanoId
28
+ name: $name
29
+ description: $description
30
+ keywords: $keywords
31
+ relatedTopics: $relatedTopics
32
+ }) {
33
+ nanoId
34
+ }
35
+ }
36
+ `;
37
+ const INGEST_MUTATION = `
38
+ mutation IndexTweets {
39
+ indexTweets
40
+ }
41
+ `;
42
+ const INBOX_QUERY = `
43
+ query QuickstartInbox($status: SuggestionStatus, $limit: Int) {
44
+ suggestions(status: $status, limit: $limit) {
45
+ suggestionId
46
+ score
47
+ projectsMatched
48
+ status
49
+ tweet {
50
+ xid
51
+ text
52
+ createdAt
53
+ user {
54
+ displayName
55
+ username
56
+ }
57
+ }
58
+ }
59
+ }
60
+ `;
61
+ // ─── Starter interest suggestions ────────────────────────────────────────────
62
+ /**
63
+ * Returns 3 sensible starter interest drafts. In the future this could use
64
+ * the user's X bio / pinned tweet, but for now these are broadly useful
65
+ * defaults for the typical Sonar user (tech-forward Twitter crowd).
66
+ */
67
+ function buildStarterSuggestions(_xHandle) {
68
+ return [
69
+ {
70
+ name: 'AI and machine learning',
71
+ description: 'Breakthroughs, papers, tools, and discussion around AI, LLMs, and machine learning.',
72
+ keywords: ['LLM', 'AI agents', 'machine learning', 'GPT', 'fine-tuning', 'inference'],
73
+ relatedTopics: ['artificial intelligence', 'deep learning', 'foundation models'],
74
+ },
75
+ {
76
+ name: 'Software engineering and developer tools',
77
+ description: 'New frameworks, libraries, OSS releases, and engineering practices worth tracking.',
78
+ keywords: ['open source', 'TypeScript', 'Rust', 'developer tools', 'CLI', 'API design'],
79
+ relatedTopics: ['software development', 'devex', 'programming'],
80
+ },
81
+ {
82
+ name: 'Tech startups and product launches',
83
+ description: 'Funding rounds, product launches, founder insights, and market moves in tech.',
84
+ keywords: ['startup', 'YC', 'product launch', 'founder', 'seed round', 'SaaS'],
85
+ relatedTopics: ['venture capital', 'entrepreneurship', 'B2B software'],
86
+ },
87
+ ];
88
+ }
89
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
90
+ function relativeTime(dateStr) {
91
+ const ts = new Date(dateStr).getTime();
92
+ if (isNaN(ts))
93
+ return '?';
94
+ const diff = Math.max(0, Date.now() - ts);
95
+ const mins = Math.floor(diff / 60000);
96
+ if (mins < 60)
97
+ return `${mins}m`;
98
+ const hours = Math.floor(mins / 60);
99
+ if (hours < 24)
100
+ return `${hours}h`;
101
+ return `${Math.floor(hours / 24)}d`;
102
+ }
103
+ function hasToken() {
104
+ if (process.env.SONAR_API_KEY?.trim())
105
+ return true;
106
+ const config = readConfig();
107
+ return Boolean(config.token?.trim());
108
+ }
109
+ // ─── Sub-renders ──────────────────────────────────────────────────────────────
110
+ function UnauthenticatedView() {
111
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, color: "yellow", children: "\u26A0 Not authenticated" }), _jsxs(Text, { children: ["Sonar needs an API key to get started. Get one at", ' ', _jsx(Text, { color: "cyan", children: "https://sonar.8640p.info" })] }), _jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsx(Text, { dimColor: true, children: "Then run one of:" }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "SONAR_API_KEY=<key> sonar quickstart" }), " (one-off)"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "sonar config setup --key <key>" }), " (persist to ~/.sonar/config.json)"] })] })] }));
112
+ }
113
+ function ConfirmView({ me, suggestions, onConfirm, onAbort, }) {
114
+ useInput((input, key) => {
115
+ if (key.return || input === 'y' || input === 'Y') {
116
+ onConfirm();
117
+ }
118
+ else if (input === 'n' || input === 'N' || key.escape) {
119
+ onAbort();
120
+ }
121
+ });
122
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Welcome to Sonar," }), _jsxs(Text, { bold: true, children: ["@", me.xHandle, "!"] })] }), _jsx(Text, { children: "You have no interests set up yet. Here are 3 starter suggestions to get your inbox going:" }), suggestions.map((s, i) => (_jsxs(Box, { flexDirection: "column", gap: 0, paddingLeft: 2, children: [_jsxs(Box, { gap: 1, children: [_jsxs(Text, { color: "cyan", children: [i + 1, "."] }), _jsx(Text, { bold: true, children: s.name })] }), _jsx(Box, { paddingLeft: 4, children: _jsx(Text, { dimColor: true, children: s.description }) }), _jsxs(Box, { gap: 1, paddingLeft: 4, children: [_jsx(Text, { dimColor: true, children: "keywords:" }), _jsx(Text, { dimColor: true, children: s.keywords.slice(0, 4).join(', ') })] })] }, s.name))), _jsxs(Box, { marginTop: 1, gap: 1, children: [_jsx(Text, { dimColor: true, children: "Create these interests and kick off indexing?" }), _jsx(Text, { bold: true, color: "cyan", children: "[Y/n]" })] }), _jsxs(Text, { dimColor: true, children: ["tip: customise later with", ' ', _jsx(Text, { color: "cyan", children: "sonar interests create --from-prompt \"...\"" })] })] }));
123
+ }
124
+ function CreatingView({ suggestions, progress }) {
125
+ return (_jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsxs(Box, { gap: 1, marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Setting up interests" }), _jsxs(Text, { dimColor: true, children: ["(", progress, "/", suggestions.length, ")"] })] }), suggestions.map((s, i) => (_jsxs(Box, { gap: 1, children: [i < progress ? (_jsx(Text, { color: "green", children: "\u2713" })) : i === progress ? (_jsx(Spinner, { label: "" })) : (_jsx(Text, { dimColor: true, children: "\u00B7" })), _jsx(Text, { dimColor: i > progress, color: i < progress ? 'green' : undefined, children: s.name })] }, s.name)))] }));
126
+ }
127
+ function InboxView({ items, created }) {
128
+ if (items.length === 0) {
129
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [created ? (_jsx(Text, { color: "green", children: "\u2713 Interests created and indexing triggered!" })) : (_jsx(Text, { color: "green", children: "\u2713 Your interests are set up \u2014 indexing is in progress." })), _jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsx(Text, { children: "Your inbox is empty right now \u2014 indexing takes a few minutes." }), _jsxs(Text, { dimColor: true, children: ["Check back shortly with: ", _jsx(Text, { color: "cyan", children: "sonar inbox" })] })] }), _jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsxs(Text, { dimColor: true, children: ["Monitor indexing progress: ", _jsx(Text, { color: "cyan", children: "sonar monitor" })] }), _jsxs(Text, { dimColor: true, children: ["Browse your full inbox: ", _jsx(Text, { color: "cyan", children: "sonar inbox" })] }), _jsxs(Text, { dimColor: true, children: ["Edit interests: ", _jsx(Text, { color: "cyan", children: "sonar interests" })] })] })] }));
130
+ }
131
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { color: "green", children: "\u2713 You're all set! Here's your inbox:" }), items.slice(0, 10).map((s) => {
132
+ const handle = s.tweet.user.username ?? s.tweet.user.displayName;
133
+ return (_jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { color: "cyan", children: relativeTime(s.tweet.createdAt) }), _jsx(Text, { color: "green", children: s.score.toFixed(2) }), _jsxs(Text, { dimColor: true, children: ["@", handle] })] }), _jsx(Box, { paddingLeft: 2, width: 80, children: _jsx(Text, { wrap: "wrap", dimColor: true, children: s.tweet.text.replace(/\n/g, ' ').slice(0, 120) }) })] }, s.suggestionId));
134
+ }), items.length > 10 && (_jsxs(Text, { dimColor: true, children: ["\u2026 and ", items.length - 10, " more. Run ", _jsx(Text, { color: "cyan", children: "sonar inbox" }), " to see all."] })), _jsxs(Text, { dimColor: true, children: ["Interactive mode: ", _jsx(Text, { color: "cyan", children: "sonar inbox --interactive" }), ' · ', "Full inbox: ", _jsx(Text, { color: "cyan", children: "sonar inbox" })] })] }));
135
+ }
136
+ // ─── Main component ───────────────────────────────────────────────────────────
137
+ export default function Quickstart() {
138
+ const { exit } = useApp();
139
+ const [phase, setPhase] = useState({ type: 'loading' });
140
+ const abortedRef = useRef(false);
141
+ const confirmedRef = useRef(false);
142
+ // ── Bootstrap: check auth + fetch me + projects ──────────────────────────
143
+ useEffect(() => {
144
+ if (!hasToken()) {
145
+ setPhase({ type: 'unauthenticated' });
146
+ return;
147
+ }
148
+ async function bootstrap() {
149
+ try {
150
+ const result = await gql(BOOTSTRAP_QUERY);
151
+ if (!result.me) {
152
+ setPhase({ type: 'unauthenticated' });
153
+ return;
154
+ }
155
+ // If interests already exist, jump straight to inbox
156
+ if (result.projects.length > 0) {
157
+ const inbox = await gql(INBOX_QUERY, {
158
+ status: 'INBOX',
159
+ limit: 20,
160
+ });
161
+ setPhase({ type: 'inbox', items: inbox.suggestions, created: false });
162
+ return;
163
+ }
164
+ // No interests — propose starters
165
+ const suggestions = buildStarterSuggestions(result.me.xHandle);
166
+ setPhase({ type: 'confirm', me: result.me, suggestions });
167
+ }
168
+ catch (err) {
169
+ setPhase({ type: 'error', message: err instanceof Error ? err.message : String(err) });
170
+ }
171
+ }
172
+ bootstrap();
173
+ }, []);
174
+ // ── Create interests + ingest (triggered from confirm handler) ────────────
175
+ const handleConfirm = async (suggestions) => {
176
+ if (confirmedRef.current)
177
+ return;
178
+ confirmedRef.current = true;
179
+ setPhase({ type: 'creating', suggestions, progress: 0 });
180
+ try {
181
+ // Create each interest sequentially so progress counter is accurate
182
+ for (let i = 0; i < suggestions.length; i++) {
183
+ if (abortedRef.current)
184
+ return;
185
+ const s = suggestions[i];
186
+ await gql(CREATE_MUTATION, {
187
+ nanoId: null,
188
+ name: s.name,
189
+ description: s.description,
190
+ keywords: s.keywords,
191
+ relatedTopics: s.relatedTopics,
192
+ });
193
+ setPhase({ type: 'creating', suggestions, progress: i + 1 });
194
+ }
195
+ // Trigger ingest
196
+ setPhase({ type: 'ingesting' });
197
+ await gql(INGEST_MUTATION);
198
+ // Fetch initial inbox (may be empty — that's fine)
199
+ const inbox = await gql(INBOX_QUERY, {
200
+ status: 'INBOX',
201
+ limit: 20,
202
+ });
203
+ setPhase({ type: 'inbox', items: inbox.suggestions, created: true });
204
+ }
205
+ catch (err) {
206
+ setPhase({ type: 'error', message: err instanceof Error ? err.message : String(err) });
207
+ }
208
+ };
209
+ const handleAbort = () => {
210
+ abortedRef.current = true;
211
+ process.stdout.write('\nAborted. Run sonar quickstart again whenever you\'re ready.\n');
212
+ exit();
213
+ };
214
+ // ── Render ─────────────────────────────────────────────────────────────────
215
+ switch (phase.type) {
216
+ case 'loading':
217
+ return _jsx(Spinner, { label: "Loading your Sonar profile..." });
218
+ case 'unauthenticated':
219
+ return _jsx(UnauthenticatedView, {});
220
+ case 'error':
221
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Text, { color: "red", children: ["Error: ", phase.message] }), _jsxs(Text, { dimColor: true, children: ["Check your connection and API key, then retry: ", _jsx(Text, { color: "cyan", children: "sonar quickstart" })] })] }));
222
+ case 'confirm':
223
+ return (_jsx(ConfirmView, { me: phase.me, suggestions: phase.suggestions, onConfirm: () => handleConfirm(phase.suggestions), onAbort: handleAbort }));
224
+ case 'creating':
225
+ return _jsx(CreatingView, { suggestions: phase.suggestions, progress: phase.progress });
226
+ case 'ingesting':
227
+ return _jsx(Spinner, { label: "Triggering tweet indexing..." });
228
+ case 'inbox':
229
+ return _jsx(InboxView, { items: phase.items, created: phase.created });
230
+ }
231
+ }
@@ -18,6 +18,7 @@ export async function gql(query, variables = {}, flags = {}) {
18
18
  if (flags.debug) {
19
19
  console.error(url, query, variables);
20
20
  }
21
+ console.log('url', url);
21
22
  res = await fetch(url, {
22
23
  method: 'POST',
23
24
  signal: controller.signal,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1a35e1/sonar-cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "X/Twitter social graph CLI for signal filtering and curation",
5
5
  "type": "module",
6
6
  "bin": {