@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,93 +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, useApp } from 'ink';
5
- import { getToken, getApiUrl } from '../lib/config.js';
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
- watch: zod.boolean().default(false).describe('Poll and refresh every 2 seconds'),
11
- });
12
- const POLL_INTERVAL = 2000;
13
- const QUEUE_LABELS = {
14
- tweets: 'Tweets',
15
- bookmarks: 'Bookmarks',
16
- social_graph: 'Social graph',
17
- suggestions: 'Suggestions',
18
- };
19
- export default function Monitor({ options: flags }) {
20
- const { exit } = useApp();
21
- const [data, setData] = useState(null);
22
- const [error, setError] = useState(null);
23
- useEffect(() => {
24
- const token = getToken();
25
- const baseUrl = getApiUrl().replace(/\/graphql$/, '');
26
- async function fetchStatus() {
27
- const controller = new AbortController();
28
- const timer = setTimeout(() => controller.abort(), 10_000);
29
- try {
30
- const [statusRes, meRes] = await Promise.all([
31
- fetch(`${baseUrl}/indexing/status`, {
32
- signal: controller.signal,
33
- headers: { Authorization: `Bearer ${token}` },
34
- }),
35
- gql(`
36
- query MonitorStatus {
37
- me {
38
- accountId
39
- email
40
- xHandle
41
- xid
42
- isPayingCustomer
43
- indexingAccounts
44
- indexedTweets
45
- pendingEmbeddings
46
- twitterIndexedAt
47
- refreshedSuggestionsAt
48
- }
49
- }
50
- `),
51
- ]);
52
- clearTimeout(timer);
53
- if (!statusRes.ok)
54
- throw new Error(`HTTP ${statusRes.status} from ${baseUrl}`);
55
- const status = await statusRes.json();
56
- setData({ me: meRes.me, queues: status.queues });
57
- setError(null);
58
- }
59
- catch (err) {
60
- clearTimeout(timer);
61
- if (err instanceof DOMException && err.name === 'AbortError') {
62
- setError('Monitor request timed out (10s). ' +
63
- 'The server may be overloaded. ' +
64
- 'Check SONAR_API_URL or retry without --watch.');
65
- }
66
- else {
67
- setError(err instanceof Error ? err.message : String(err));
68
- }
69
- }
70
- }
71
- fetchStatus();
72
- if (!flags.watch)
73
- return;
74
- const timer = setInterval(fetchStatus, POLL_INTERVAL);
75
- return () => clearInterval(timer);
76
- }, []);
77
- useEffect(() => {
78
- if (!flags.watch && data !== null)
79
- exit();
80
- }, [data]);
81
- useEffect(() => {
82
- if (!flags.watch && error !== null)
83
- exit(new Error(error));
84
- }, [error]);
85
- if (error)
86
- return _jsxs(Text, { color: "red", children: ["Error: ", error] });
87
- if (!data)
88
- return _jsx(Spinner, { label: "Loading ingest status..." });
89
- const { me, queues } = data;
90
- const entries = Object.entries(queues);
91
- const hasActivity = entries.length > 0 || me.pendingEmbeddings > 0;
92
- return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(AccountCard, { me: me }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: "cyan", children: "Job Queues" }), !hasActivity ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: "green", children: "No active ingest jobs." }), _jsxs(Text, { color: "green", children: ["Run ", _jsx(Text, { color: "cyan", children: "sonar interests match" }), " to start surface relevant tweets."] })] })) : (_jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsxs(Box, { gap: 2, marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: ('Queue').padEnd(16) }), _jsx(Text, { bold: true, color: "cyan", children: 'Running'.padEnd(10) }), _jsx(Text, { bold: true, color: "cyan", children: "Queued" })] }), entries.map(([name, counts]) => (_jsxs(Box, { gap: 2, children: [_jsx(Text, { children: (QUEUE_LABELS[name] ?? name).padEnd(16) }), _jsx(Text, { color: counts.running > 0 ? 'green' : 'white', children: String(counts.running).padEnd(10) }), _jsx(Text, { color: counts.queued > 0 ? 'yellow' : 'white', children: counts.queued })] }, name)))] }))] })] }));
93
- }
@@ -1,231 +0,0 @@
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
- }
@@ -1,10 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Box, Text } from 'ink';
3
- export function InterestCard({ interest, termWidth, isLast }) {
4
- const updatedAt = new Date(interest.updatedAt).toLocaleDateString('en-US', {
5
- month: 'short',
6
- day: 'numeric',
7
- year: 'numeric',
8
- });
9
- return (_jsxs(Box, { flexDirection: "column", marginBottom: isLast ? 0 : 1, width: termWidth, children: [_jsxs(Box, { children: [_jsx(Text, { color: "cyan", bold: true, children: interest.name }), _jsxs(Text, { dimColor: true, children: [" v", interest.version, " \u00B7 ", interest.id, " \u00B7 ", updatedAt] })] }), interest.description && (_jsxs(Box, { children: [_jsxs(Text, { color: "gray", children: ['└', " "] }), _jsx(Text, { wrap: "wrap", children: interest.description })] })), interest.keywords && interest.keywords.length > 0 && (_jsxs(Box, { marginLeft: 2, children: [_jsx(Text, { dimColor: true, children: "keywords " }), _jsx(Text, { color: "yellow", children: interest.keywords.join(' ') })] })), interest.relatedTopics && interest.relatedTopics.length > 0 && (_jsxs(Box, { marginLeft: 2, children: [_jsx(Text, { dimColor: true, children: "topics " }), _jsx(Text, { children: interest.relatedTopics.join(' ') })] })), !isLast && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: '─'.repeat(Math.min(termWidth - 2, 72)) }) }))] }));
10
- }