@1a35e1/sonar-cli 0.2.1 → 0.3.4
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 +151 -166
- package/dist/commands/{inbox/archive.js → archive.js} +2 -2
- package/dist/commands/config/data/download.js +2 -2
- package/dist/commands/config/data/sync.js +2 -2
- package/dist/commands/config/nuke.js +20 -2
- package/dist/commands/feed.js +105 -155
- package/dist/commands/index.js +172 -4
- package/dist/commands/{inbox/later.js → later.js} +2 -2
- package/dist/commands/refresh.js +41 -0
- package/dist/commands/{inbox/skip.js → skip.js} +2 -2
- package/dist/commands/status.js +128 -0
- package/dist/commands/sync/bookmarks.js +35 -0
- package/dist/commands/topics/add.js +71 -0
- package/dist/commands/topics/delete.js +42 -0
- package/dist/commands/topics/edit.js +97 -0
- package/dist/commands/topics/index.js +54 -0
- package/dist/commands/topics/suggest.js +125 -0
- package/dist/commands/topics/view.js +48 -0
- package/dist/components/AccountCard.js +1 -1
- package/dist/components/Banner.js +11 -0
- package/dist/components/InteractiveSession.js +95 -210
- package/dist/components/Spinner.js +5 -4
- package/dist/components/TopicCard.js +15 -0
- package/dist/components/TweetCard.js +76 -0
- package/dist/lib/ai.js +85 -0
- package/dist/lib/client.js +66 -40
- package/dist/lib/config.js +3 -2
- package/dist/lib/data-queries.js +1 -3
- package/dist/lib/skill.js +66 -226
- package/package.json +13 -3
- package/dist/commands/account.js +0 -75
- package/dist/commands/inbox/index.js +0 -103
- package/dist/commands/inbox/read.js +0 -41
- package/dist/commands/ingest/bookmarks.js +0 -55
- package/dist/commands/ingest/index.js +0 -5
- package/dist/commands/ingest/tweets.js +0 -55
- package/dist/commands/interests/create.js +0 -107
- package/dist/commands/interests/index.js +0 -56
- package/dist/commands/interests/match.js +0 -33
- package/dist/commands/interests/update.js +0 -153
- package/dist/commands/monitor.js +0 -93
- package/dist/commands/quickstart.js +0 -231
- package/dist/components/InterestCard.js +0 -10
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import zod from 'zod';
|
|
4
|
+
import { Box, Text, useApp, useInput } from 'ink';
|
|
5
|
+
import { formatDistanceToNow } from 'date-fns';
|
|
6
|
+
import { getToken, getApiUrl } from '../lib/config.js';
|
|
7
|
+
import { gql } from '../lib/client.js';
|
|
8
|
+
import { Spinner } from '../components/Spinner.js';
|
|
9
|
+
export const options = zod.object({
|
|
10
|
+
watch: zod.boolean().default(false).describe('Poll and refresh every 2 seconds'),
|
|
11
|
+
json: zod.boolean().default(false).describe('Raw JSON output'),
|
|
12
|
+
});
|
|
13
|
+
const POLL_INTERVAL = 2000;
|
|
14
|
+
const QUEUE_LABELS = {
|
|
15
|
+
tweets: 'Tweets',
|
|
16
|
+
bookmarks: 'Bookmarks',
|
|
17
|
+
social_graph: 'Social graph',
|
|
18
|
+
suggestions: 'Suggestions',
|
|
19
|
+
default: 'Pipeline',
|
|
20
|
+
topics: 'Topics',
|
|
21
|
+
};
|
|
22
|
+
const GQL_QUERY = `
|
|
23
|
+
query Status {
|
|
24
|
+
me {
|
|
25
|
+
accountId email xHandle xid isPayingCustomer
|
|
26
|
+
indexingAccounts indexedTweets pendingEmbeddings
|
|
27
|
+
twitterIndexedAt refreshedSuggestionsAt
|
|
28
|
+
}
|
|
29
|
+
suggestionCounts {
|
|
30
|
+
inbox later archived total
|
|
31
|
+
}
|
|
32
|
+
usage {
|
|
33
|
+
plan
|
|
34
|
+
interests { used limit atLimit }
|
|
35
|
+
bookmarksEnabled
|
|
36
|
+
suggestionRefreshes { used limit atLimit resetsAt }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
`;
|
|
40
|
+
function timeAgo(iso) {
|
|
41
|
+
if (!iso)
|
|
42
|
+
return 'never';
|
|
43
|
+
return formatDistanceToNow(new Date(iso), { addSuffix: true });
|
|
44
|
+
}
|
|
45
|
+
export default function Status({ options: flags }) {
|
|
46
|
+
const { exit } = useApp();
|
|
47
|
+
const [data, setData] = useState(null);
|
|
48
|
+
const [error, setError] = useState(null);
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
const token = getToken();
|
|
51
|
+
const baseUrl = getApiUrl().replace(/\/graphql$/, '');
|
|
52
|
+
async function fetchStatus() {
|
|
53
|
+
const controller = new AbortController();
|
|
54
|
+
const timer = setTimeout(() => controller.abort(), 10_000);
|
|
55
|
+
try {
|
|
56
|
+
const [statusRes, gqlRes] = await Promise.all([
|
|
57
|
+
fetch(`${baseUrl}/indexing/status`, {
|
|
58
|
+
signal: controller.signal,
|
|
59
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
60
|
+
}),
|
|
61
|
+
gql(GQL_QUERY),
|
|
62
|
+
]);
|
|
63
|
+
clearTimeout(timer);
|
|
64
|
+
if (!statusRes.ok)
|
|
65
|
+
throw new Error(`HTTP ${statusRes.status}`);
|
|
66
|
+
const status = await statusRes.json();
|
|
67
|
+
if (flags.json) {
|
|
68
|
+
process.stdout.write(JSON.stringify({ ...gqlRes, queues: status.queues }, null, 2) + '\n');
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}
|
|
71
|
+
setData({ me: gqlRes.me, queues: status.queues, pipeline: status.pipeline ?? null, usage: gqlRes.usage, suggestionCounts: gqlRes.suggestionCounts });
|
|
72
|
+
setError(null);
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
clearTimeout(timer);
|
|
76
|
+
if (err instanceof DOMException && err.name === 'AbortError') {
|
|
77
|
+
setError('Request timed out (10s)');
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
fetchStatus();
|
|
85
|
+
if (!flags.watch)
|
|
86
|
+
return;
|
|
87
|
+
const timer = setInterval(fetchStatus, POLL_INTERVAL);
|
|
88
|
+
return () => clearInterval(timer);
|
|
89
|
+
}, []);
|
|
90
|
+
useEffect(() => { if (!flags.watch && data !== null)
|
|
91
|
+
exit(); }, [data]);
|
|
92
|
+
useEffect(() => { if (!flags.watch && error !== null)
|
|
93
|
+
exit(new Error(error)); }, [error]);
|
|
94
|
+
const [refreshing, setRefreshing] = useState(false);
|
|
95
|
+
const [refreshMsg, setRefreshMsg] = useState(null);
|
|
96
|
+
useInput((input, key) => {
|
|
97
|
+
if (!flags.watch)
|
|
98
|
+
return;
|
|
99
|
+
if (input === 'r' && !refreshing) {
|
|
100
|
+
setRefreshing(true);
|
|
101
|
+
setRefreshMsg(null);
|
|
102
|
+
gql('mutation Refresh { refresh(days: 1) }')
|
|
103
|
+
.then(() => {
|
|
104
|
+
setRefreshMsg('pipeline queued');
|
|
105
|
+
setRefreshing(false);
|
|
106
|
+
})
|
|
107
|
+
.catch((err) => {
|
|
108
|
+
setRefreshMsg(err instanceof Error ? err.message : String(err));
|
|
109
|
+
setRefreshing(false);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
if (input === 'q')
|
|
113
|
+
exit();
|
|
114
|
+
}, { isActive: flags.watch });
|
|
115
|
+
if (error)
|
|
116
|
+
return _jsxs(Text, { color: "red", children: ["Error: ", error] });
|
|
117
|
+
if (!data)
|
|
118
|
+
return _jsx(Spinner, { label: "Loading..." });
|
|
119
|
+
const { me, queues, pipeline, usage, suggestionCounts } = data;
|
|
120
|
+
const entries = Object.entries(queues);
|
|
121
|
+
const hasActivity = entries.length > 0 || me.pendingEmbeddings > 0 || (pipeline !== null && pipeline.status === 'running');
|
|
122
|
+
const embedded = me.indexedTweets - me.pendingEmbeddings;
|
|
123
|
+
const embedPct = me.indexedTweets > 0 ? Math.round((embedded / me.indexedTweets) * 100) : 100;
|
|
124
|
+
const BAR_WIDTH = 20;
|
|
125
|
+
const filledCount = Math.round((embedPct / 100) * BAR_WIDTH);
|
|
126
|
+
const progressBar = '█'.repeat(filledCount) + '░'.repeat(BAR_WIDTH - filledCount);
|
|
127
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, color: "cyan", children: ["@", me.xHandle] }), _jsxs(Text, { dimColor: true, children: [me.indexedTweets.toLocaleString(), " tweets", ' · ', "indexed ", timeAgo(me.twitterIndexedAt), ' · ', "refreshed ", timeAgo(me.refreshedSuggestionsAt)] })] }), me.pendingEmbeddings > 0 && (_jsx(Box, { flexDirection: "column", children: _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "embeddings " }), _jsx(Text, { color: embedPct === 100 ? 'green' : 'yellow', children: progressBar }), _jsxs(Text, { dimColor: true, children: [" ", embedPct, "% "] }), _jsxs(Text, { dimColor: true, children: ["(", embedded.toLocaleString(), "/", me.indexedTweets.toLocaleString(), ")"] })] }) })), _jsxs(Box, { borderStyle: "round", borderColor: "gray", flexDirection: "column", paddingX: 1, children: [usage && (_jsxs(Box, { gap: 2, children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "plan " }), _jsx(Text, { color: usage.plan === 'trial' ? 'yellow' : 'green', children: usage.plan })] }), _jsx(Text, { dimColor: true, children: "\u2502" }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "topics " }), _jsxs(Text, { color: usage.interests.atLimit ? 'red' : undefined, children: [usage.interests.used, usage.interests.limit !== null ? `/${usage.interests.limit}` : ''] })] }), _jsx(Text, { dimColor: true, children: "\u2502" }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "refreshes " }), usage.suggestionRefreshes.limit !== null ? (_jsxs(Text, { color: usage.suggestionRefreshes.atLimit ? 'red' : undefined, children: [usage.suggestionRefreshes.used, "/", usage.suggestionRefreshes.limit] })) : (_jsx(Text, { color: "green", children: "unlimited" }))] })] })), _jsxs(Box, { gap: 2, children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "inbox " }), _jsx(Text, { color: suggestionCounts.inbox > 0 ? 'green' : undefined, children: suggestionCounts.inbox })] }), _jsx(Text, { dimColor: true, children: "\u2502" }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "later " }), suggestionCounts.later] }), _jsx(Text, { dimColor: true, children: "\u2502" }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "archived " }), suggestionCounts.archived] })] })] }), pipeline && pipeline.status === 'running' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, dimColor: true, children: "PIPELINE" }), pipeline.steps.map((step, i) => (_jsxs(Text, { children: [_jsx(Text, { color: "green", children: " \u2713 " }), _jsx(Text, { children: step.label }), _jsxs(Text, { dimColor: true, children: [" (", step.duration, "s)"] })] }, i))), pipeline.current !== '' && (_jsxs(Box, { children: [_jsx(Text, { children: " " }), _jsx(Spinner, { label: pipeline.current })] }))] })), pipeline && pipeline.status === 'complete' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, dimColor: true, children: "PIPELINE" }), pipeline.steps.map((step, i) => (_jsxs(Text, { children: [_jsx(Text, { color: "green", children: " \u2713 " }), _jsx(Text, { children: step.label }), _jsxs(Text, { dimColor: true, children: [" (", step.duration, "s)"] })] }, i))), _jsxs(Text, { color: "green", children: [" \u2713 Complete (", pipeline.total_duration, "s)"] })] })), pipeline && pipeline.status === 'failed' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, dimColor: true, children: "PIPELINE" }), pipeline.steps.map((step, i) => (_jsxs(Text, { children: [_jsx(Text, { color: "green", children: " \u2713 " }), _jsx(Text, { children: step.label }), _jsxs(Text, { dimColor: true, children: [" (", step.duration, "s)"] })] }, i))), _jsx(Text, { color: "red", children: " \u2717 Failed" })] })), me.pendingEmbeddings > 0 && !(pipeline && pipeline.status === 'running') && (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: 'Embeddings'.padEnd(16) }), _jsxs(Text, { color: "yellow", children: ["\u25CF ", me.pendingEmbeddings.toLocaleString(), " pending"] })] })), entries.filter(([name]) => name !== 'default').length > 0 && !(pipeline && pipeline.status === 'running') && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, dimColor: true, children: "QUEUES" }), entries.filter(([name]) => name !== 'default').map(([name, counts]) => (_jsxs(Text, { children: [_jsxs(Text, { dimColor: true, children: [" ", (QUEUE_LABELS[name] ?? name).padEnd(16)] }), counts.running > 0 && _jsxs(Text, { color: "green", children: ["\u25B6 ", counts.running, " running "] }), counts.queued > 0 && _jsxs(Text, { color: "yellow", children: ["\u25CF ", counts.queued, " queued "] }), (counts.deferred ?? 0) > 0 && _jsxs(Text, { color: "blue", children: ["\u25C6 ", counts.deferred, " pending "] })] }, name)))] })), !hasActivity && (_jsxs(Text, { dimColor: true, children: ["idle \u2014 run ", _jsx(Text, { color: "cyan", children: "sonar refresh" }), " to trigger pipeline"] })), flags.watch && (_jsxs(Box, { gap: 2, children: [refreshing && _jsx(Text, { color: "yellow", children: "refreshing..." }), refreshMsg && _jsx(Text, { color: "green", children: refreshMsg }), _jsxs(Text, { dimColor: true, children: ["press ", _jsx(Text, { color: "cyan", children: "r" }), " to refresh \u00B7 ", _jsx(Text, { color: "cyan", children: "q" }), " to quit"] })] }))] }));
|
|
128
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { Box, Text, useApp } from 'ink';
|
|
4
|
+
import { gql } from '../../lib/client.js';
|
|
5
|
+
import { Spinner } from '../../components/Spinner.js';
|
|
6
|
+
export default function SyncBookmarks() {
|
|
7
|
+
const { exit } = useApp();
|
|
8
|
+
const [status, setStatus] = useState('pending');
|
|
9
|
+
const [error, setError] = useState(null);
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
async function run() {
|
|
12
|
+
setStatus('running');
|
|
13
|
+
try {
|
|
14
|
+
await gql('mutation SyncBookmarks { syncBookmarks }');
|
|
15
|
+
setStatus('ok');
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
setStatus('failed');
|
|
19
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
run();
|
|
23
|
+
}, []);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (status === 'ok' || status === 'failed')
|
|
26
|
+
exit();
|
|
27
|
+
}, [status]);
|
|
28
|
+
if (status === 'running') {
|
|
29
|
+
return _jsx(Spinner, { label: "Syncing bookmarks..." });
|
|
30
|
+
}
|
|
31
|
+
if (status === 'failed') {
|
|
32
|
+
return _jsxs(Text, { color: "red", children: ["Error: ", error] });
|
|
33
|
+
}
|
|
34
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "green", children: "\u2713 Bookmark sync queued" }), _jsxs(Text, { dimColor: true, children: ["Run ", _jsx(Text, { color: "cyan", children: "sonar status --watch" }), " to monitor progress."] })] }));
|
|
35
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import zod from 'zod';
|
|
4
|
+
import { Box, Text } from 'ink';
|
|
5
|
+
import { gql } from '../../lib/client.js';
|
|
6
|
+
import { Spinner } from '../../components/Spinner.js';
|
|
7
|
+
export const args = zod.tuple([
|
|
8
|
+
zod.string().describe('Topic name or phrase'),
|
|
9
|
+
]);
|
|
10
|
+
export const options = zod.object({
|
|
11
|
+
description: zod.string().optional().describe('Optional description (auto-generated if omitted)'),
|
|
12
|
+
json: zod.boolean().default(false).describe('Raw JSON output'),
|
|
13
|
+
});
|
|
14
|
+
const CREATE_MUTATION = `
|
|
15
|
+
mutation CreateOrUpdateTopic(
|
|
16
|
+
$name: String!
|
|
17
|
+
$description: String
|
|
18
|
+
) {
|
|
19
|
+
createOrUpdateTopic(input: {
|
|
20
|
+
name: $name
|
|
21
|
+
description: $description
|
|
22
|
+
}) {
|
|
23
|
+
id: nanoId
|
|
24
|
+
name
|
|
25
|
+
description
|
|
26
|
+
version
|
|
27
|
+
createdAt
|
|
28
|
+
updatedAt
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
`;
|
|
32
|
+
export default function TopicsAdd({ args: [name], options: flags }) {
|
|
33
|
+
const [data, setData] = useState(null);
|
|
34
|
+
const [error, setError] = useState(null);
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (!error || !flags.json)
|
|
37
|
+
return;
|
|
38
|
+
process.stderr.write(`${error}\n`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}, [error, flags.json]);
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
async function run() {
|
|
43
|
+
try {
|
|
44
|
+
const result = await gql(CREATE_MUTATION, {
|
|
45
|
+
name,
|
|
46
|
+
description: flags.description ?? null,
|
|
47
|
+
});
|
|
48
|
+
if (flags.json) {
|
|
49
|
+
process.stdout.write(JSON.stringify(result.createOrUpdateTopic, null, 2) + '\n');
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
|
52
|
+
setData(result.createOrUpdateTopic);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
run();
|
|
59
|
+
}, []);
|
|
60
|
+
if (error) {
|
|
61
|
+
if (flags.json)
|
|
62
|
+
return _jsx(_Fragment, {});
|
|
63
|
+
return _jsxs(Text, { color: "red", children: ["Error: ", error] });
|
|
64
|
+
}
|
|
65
|
+
if (!data) {
|
|
66
|
+
if (flags.json)
|
|
67
|
+
return _jsx(_Fragment, {});
|
|
68
|
+
return _jsx(Spinner, { label: "Creating topic (expanding via AI)..." });
|
|
69
|
+
}
|
|
70
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { bold: true, color: "cyan", children: data.name }), _jsxs(Text, { dimColor: true, children: ["v", data.version, " \u00B7 ", data.id, " \u00B7 created"] })] }), data.description && _jsx(Text, { dimColor: true, children: data.description }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "tip run " }), _jsx(Text, { color: "cyan", children: "sonar refresh" }), _jsx(Text, { dimColor: true, children: " to match this topic against recent tweets" })] })] }));
|
|
71
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import zod from 'zod';
|
|
4
|
+
import { Box, Text } from 'ink';
|
|
5
|
+
import { gql } from '../../lib/client.js';
|
|
6
|
+
import { Spinner } from '../../components/Spinner.js';
|
|
7
|
+
export const args = zod.tuple([
|
|
8
|
+
zod.string().describe('Topic ID'),
|
|
9
|
+
]);
|
|
10
|
+
export const options = zod.object({
|
|
11
|
+
json: zod.boolean().default(false).describe('Raw JSON output'),
|
|
12
|
+
});
|
|
13
|
+
const DELETE_MUTATION = `
|
|
14
|
+
mutation DeleteTopic($nanoId: String!) {
|
|
15
|
+
deleteTopic(nanoId: $nanoId)
|
|
16
|
+
}
|
|
17
|
+
`;
|
|
18
|
+
export default function TopicDelete({ args: [id], options: flags }) {
|
|
19
|
+
const [done, setDone] = useState(false);
|
|
20
|
+
const [error, setError] = useState(null);
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
async function run() {
|
|
23
|
+
try {
|
|
24
|
+
await gql(DELETE_MUTATION, { nanoId: id });
|
|
25
|
+
if (flags.json) {
|
|
26
|
+
process.stdout.write(JSON.stringify({ deleted: id }) + '\n');
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
setDone(true);
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
run();
|
|
36
|
+
}, []);
|
|
37
|
+
if (error)
|
|
38
|
+
return _jsxs(Text, { color: "red", children: ["Error: ", error] });
|
|
39
|
+
if (!done)
|
|
40
|
+
return _jsx(Spinner, { label: "Deleting topic..." });
|
|
41
|
+
return (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "green", children: "Deleted" }), _jsx(Text, { dimColor: true, children: id })] }));
|
|
42
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import zod from 'zod';
|
|
4
|
+
import { Box, Text } from 'ink';
|
|
5
|
+
import { gql } from '../../lib/client.js';
|
|
6
|
+
import { Spinner } from '../../components/Spinner.js';
|
|
7
|
+
export const args = zod.tuple([
|
|
8
|
+
zod.string().describe('Topic ID'),
|
|
9
|
+
]);
|
|
10
|
+
export const options = zod.object({
|
|
11
|
+
name: zod.string().optional().describe('New name'),
|
|
12
|
+
description: zod.string().optional().describe('New description'),
|
|
13
|
+
json: zod.boolean().default(false).describe('Raw JSON output'),
|
|
14
|
+
});
|
|
15
|
+
const QUERY = `
|
|
16
|
+
query Topics {
|
|
17
|
+
topics {
|
|
18
|
+
id: nanoId
|
|
19
|
+
name
|
|
20
|
+
description
|
|
21
|
+
version
|
|
22
|
+
createdAt
|
|
23
|
+
updatedAt
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
`;
|
|
27
|
+
const UPDATE_MUTATION = `
|
|
28
|
+
mutation CreateOrUpdateTopic(
|
|
29
|
+
$nanoId: String
|
|
30
|
+
$name: String!
|
|
31
|
+
$description: String
|
|
32
|
+
) {
|
|
33
|
+
createOrUpdateTopic(input: {
|
|
34
|
+
nanoId: $nanoId
|
|
35
|
+
name: $name
|
|
36
|
+
description: $description
|
|
37
|
+
}) {
|
|
38
|
+
id: nanoId
|
|
39
|
+
name
|
|
40
|
+
description
|
|
41
|
+
version
|
|
42
|
+
createdAt
|
|
43
|
+
updatedAt
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
`;
|
|
47
|
+
async function fetchById(id) {
|
|
48
|
+
const result = await gql(QUERY);
|
|
49
|
+
const found = result.topics.find((p) => p.id === id);
|
|
50
|
+
if (!found)
|
|
51
|
+
throw new Error(`Topic "${id}" not found. Run: sonar topics`);
|
|
52
|
+
return found;
|
|
53
|
+
}
|
|
54
|
+
export default function TopicEdit({ args: [id], options: flags }) {
|
|
55
|
+
const [data, setData] = useState(null);
|
|
56
|
+
const [error, setError] = useState(null);
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (!error || !flags.json)
|
|
59
|
+
return;
|
|
60
|
+
process.stderr.write(`${error}\n`);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}, [error, flags.json]);
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
async function run() {
|
|
65
|
+
try {
|
|
66
|
+
const existing = await fetchById(id);
|
|
67
|
+
const name = flags.name ?? existing.name;
|
|
68
|
+
const description = flags.description ?? existing.description ?? null;
|
|
69
|
+
const result = await gql(UPDATE_MUTATION, {
|
|
70
|
+
nanoId: id,
|
|
71
|
+
name,
|
|
72
|
+
description,
|
|
73
|
+
});
|
|
74
|
+
if (flags.json) {
|
|
75
|
+
process.stdout.write(JSON.stringify(result.createOrUpdateTopic, null, 2) + '\n');
|
|
76
|
+
process.exit(0);
|
|
77
|
+
}
|
|
78
|
+
setData(result.createOrUpdateTopic);
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
run();
|
|
85
|
+
}, []);
|
|
86
|
+
if (error) {
|
|
87
|
+
if (flags.json)
|
|
88
|
+
return _jsx(_Fragment, {});
|
|
89
|
+
return _jsxs(Text, { color: "red", children: ["Error: ", error] });
|
|
90
|
+
}
|
|
91
|
+
if (!data) {
|
|
92
|
+
if (flags.json)
|
|
93
|
+
return _jsx(_Fragment, {});
|
|
94
|
+
return _jsx(Spinner, { label: "Updating topic..." });
|
|
95
|
+
}
|
|
96
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: data.name }), _jsxs(Text, { dimColor: true, children: ["v", data.version, " \u00B7 ", data.id, " \u00B7 updated"] })] }), data.description && _jsxs(Text, { dimColor: true, children: [data.description.slice(0, 160), "..."] })] }));
|
|
97
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import zod from 'zod';
|
|
4
|
+
import { Box, Text, useStdout } from 'ink';
|
|
5
|
+
import { gql } from '../../lib/client.js';
|
|
6
|
+
import { Spinner } from '../../components/Spinner.js';
|
|
7
|
+
import { TopicCard } from '../../components/TopicCard.js';
|
|
8
|
+
export const options = zod.object({
|
|
9
|
+
json: zod.boolean().default(false).describe('Raw JSON output'),
|
|
10
|
+
});
|
|
11
|
+
const QUERY = `
|
|
12
|
+
query Topics {
|
|
13
|
+
topics {
|
|
14
|
+
id: nanoId
|
|
15
|
+
name
|
|
16
|
+
description
|
|
17
|
+
version
|
|
18
|
+
createdAt
|
|
19
|
+
updatedAt
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
`;
|
|
23
|
+
export default function Topics({ options: flags }) {
|
|
24
|
+
const [data, setData] = useState(null);
|
|
25
|
+
const [error, setError] = useState(null);
|
|
26
|
+
const { stdout } = useStdout();
|
|
27
|
+
const termWidth = stdout.columns ?? 100;
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
async function run() {
|
|
30
|
+
try {
|
|
31
|
+
const result = await gql(QUERY);
|
|
32
|
+
if (flags.json) {
|
|
33
|
+
process.stdout.write(JSON.stringify(result.topics, null, 2) + '\n');
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
setData(result.topics);
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
run();
|
|
43
|
+
}, []);
|
|
44
|
+
if (error) {
|
|
45
|
+
return _jsxs(Text, { color: "red", children: ["Error: ", error] });
|
|
46
|
+
}
|
|
47
|
+
if (!data) {
|
|
48
|
+
return _jsx(Spinner, { label: "Fetching topics..." });
|
|
49
|
+
}
|
|
50
|
+
if (data.length === 0) {
|
|
51
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { children: "No topics found. Add one:" }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: " sonar topics add \"AI agents\"" }), _jsx(Text, { dimColor: true, children: " sonar topics add \"Rust and systems programming\"" }), _jsx(Text, { dimColor: true, children: " sonar topics add \"DeFi protocols\"" })] })] }));
|
|
52
|
+
}
|
|
53
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Topics" }), _jsxs(Text, { dimColor: true, children: [" (", data.length, ")"] })] }), data.map((p, i) => (_jsx(TopicCard, { topic: p, termWidth: termWidth, isLast: i === data.length - 1 }, p.id))), _jsx(Text, { dimColor: true, children: "view: sonar topics view <id> \u00B7 edit: sonar topics edit <id> --name \"new name\"" })] }));
|
|
54
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
3
|
+
import zod from 'zod';
|
|
4
|
+
import { Box, Text, useInput } from 'ink';
|
|
5
|
+
import { gql } from '../../lib/client.js';
|
|
6
|
+
import { getVendor } from '../../lib/config.js';
|
|
7
|
+
import { generateTopicSuggestions } from '../../lib/ai.js';
|
|
8
|
+
import { Spinner } from '../../components/Spinner.js';
|
|
9
|
+
export const options = zod.object({
|
|
10
|
+
vendor: zod.string().optional().describe('AI vendor: openai|anthropic'),
|
|
11
|
+
count: zod.number().optional().describe('Number of suggestions (default: 5)'),
|
|
12
|
+
json: zod.boolean().default(false).describe('Raw JSON output'),
|
|
13
|
+
});
|
|
14
|
+
const TOPICS_QUERY = `
|
|
15
|
+
query Topics {
|
|
16
|
+
topics { id: nanoId name description }
|
|
17
|
+
}
|
|
18
|
+
`;
|
|
19
|
+
const FEED_QUERY = `
|
|
20
|
+
query Feed($hours: Int, $limit: Int) {
|
|
21
|
+
feed(hours: $hours, limit: $limit) {
|
|
22
|
+
tweet { text }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
`;
|
|
26
|
+
const CREATE_MUTATION = `
|
|
27
|
+
mutation CreateOrUpdateTopic($name: String!, $description: String) {
|
|
28
|
+
createOrUpdateTopic(input: { name: $name, description: $description }) {
|
|
29
|
+
id: nanoId name description version createdAt updatedAt
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
`;
|
|
33
|
+
export default function TopicsSuggest({ options: flags }) {
|
|
34
|
+
const vendor = getVendor(flags.vendor);
|
|
35
|
+
const count = flags.count ?? 5;
|
|
36
|
+
const [phase, setPhase] = useState('loading');
|
|
37
|
+
const [suggestions, setSuggestions] = useState([]);
|
|
38
|
+
const [index, setIndex] = useState(0);
|
|
39
|
+
const [accepted, setAccepted] = useState([]);
|
|
40
|
+
const [error, setError] = useState(null);
|
|
41
|
+
const [saving, setSaving] = useState(false);
|
|
42
|
+
// Phase 1: Fetch context, Phase 2: Generate suggestions
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
async function run() {
|
|
45
|
+
try {
|
|
46
|
+
const [topicsRes, feedRes] = await Promise.all([
|
|
47
|
+
gql(TOPICS_QUERY),
|
|
48
|
+
gql(FEED_QUERY, { hours: 24, limit: 15 }),
|
|
49
|
+
]);
|
|
50
|
+
const existingNames = topicsRes.topics.map(t => t.name);
|
|
51
|
+
const tweetTexts = feedRes.feed.map(f => f.tweet.text);
|
|
52
|
+
setPhase('suggesting');
|
|
53
|
+
const results = await generateTopicSuggestions(existingNames, tweetTexts, count, vendor);
|
|
54
|
+
if (flags.json) {
|
|
55
|
+
process.stdout.write(JSON.stringify(results, null, 2) + '\n');
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
setSuggestions(results);
|
|
59
|
+
setPhase('reviewing');
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
run();
|
|
66
|
+
}, []);
|
|
67
|
+
const current = suggestions[index];
|
|
68
|
+
const acceptCurrent = useCallback(async () => {
|
|
69
|
+
if (!current || saving)
|
|
70
|
+
return;
|
|
71
|
+
setSaving(true);
|
|
72
|
+
try {
|
|
73
|
+
await gql(CREATE_MUTATION, {
|
|
74
|
+
name: current.name,
|
|
75
|
+
description: current.description,
|
|
76
|
+
});
|
|
77
|
+
setAccepted(prev => [...prev, current.name]);
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
process.stderr.write(`Failed to save "${current.name}": ${err instanceof Error ? err.message : String(err)}\n`);
|
|
81
|
+
}
|
|
82
|
+
setSaving(false);
|
|
83
|
+
if (index + 1 >= suggestions.length) {
|
|
84
|
+
setPhase('done');
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
setIndex(i => i + 1);
|
|
88
|
+
}
|
|
89
|
+
}, [current, index, suggestions.length, saving]);
|
|
90
|
+
const skipCurrent = useCallback(() => {
|
|
91
|
+
if (saving)
|
|
92
|
+
return;
|
|
93
|
+
if (index + 1 >= suggestions.length) {
|
|
94
|
+
setPhase('done');
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
setIndex(i => i + 1);
|
|
98
|
+
}
|
|
99
|
+
}, [index, suggestions.length, saving]);
|
|
100
|
+
useInput((input) => {
|
|
101
|
+
if (phase !== 'reviewing')
|
|
102
|
+
return;
|
|
103
|
+
if (input === 'y')
|
|
104
|
+
acceptCurrent();
|
|
105
|
+
else if (input === 'n')
|
|
106
|
+
skipCurrent();
|
|
107
|
+
else if (input === 'q')
|
|
108
|
+
setPhase('done');
|
|
109
|
+
}, { isActive: phase === 'reviewing' && !saving });
|
|
110
|
+
if (error)
|
|
111
|
+
return _jsxs(Text, { color: "red", children: ["Error: ", error] });
|
|
112
|
+
if (phase === 'loading') {
|
|
113
|
+
return _jsx(Spinner, { label: "Analyzing your interests and feed..." });
|
|
114
|
+
}
|
|
115
|
+
if (phase === 'suggesting') {
|
|
116
|
+
return _jsx(Spinner, { label: `Generating ${count} topic suggestions via ${vendor}...` });
|
|
117
|
+
}
|
|
118
|
+
if (phase === 'done') {
|
|
119
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { color: "green", children: accepted.length > 0
|
|
120
|
+
? `Added ${accepted.length} topic${accepted.length === 1 ? '' : 's'}`
|
|
121
|
+
: 'No topics added' }), accepted.map(name => (_jsxs(Text, { dimColor: true, children: [" + ", name] }, name))), accepted.length > 0 && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "tip run " }), _jsx(Text, { color: "cyan", children: "sonar refresh" }), _jsx(Text, { dimColor: true, children: " to match new topics against recent tweets" })] }))] }));
|
|
122
|
+
}
|
|
123
|
+
// Phase: reviewing
|
|
124
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { dimColor: true, children: ["[", index + 1, "/", suggestions.length, "]"] }), accepted.length > 0 && _jsxs(Text, { color: "green", children: [" ", accepted.length, " accepted"] })] }), _jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsx(Text, { bold: true, color: "cyan", children: current.name }), current.description && (_jsx(Box, { marginTop: 1, paddingLeft: 2, children: _jsxs(Text, { wrap: "wrap", children: [current.description.slice(0, 300), current.description.length > 300 ? '...' : ''] }) })), current.keywords.length > 0 && (_jsxs(Box, { marginTop: 1, paddingLeft: 2, children: [_jsx(Text, { dimColor: true, children: "keywords " }), _jsx(Text, { color: "yellow", children: current.keywords.slice(0, 10).join(' ') })] }))] }), _jsx(Box, { marginTop: 1, gap: 3, children: saving ? (_jsx(Spinner, { label: "Saving..." })) : (_jsxs(_Fragment, { children: [_jsxs(Text, { dimColor: true, children: [_jsx(Text, { color: "white", children: "y" }), " accept"] }), _jsxs(Text, { dimColor: true, children: [_jsx(Text, { color: "white", children: "n" }), " skip"] }), _jsxs(Text, { dimColor: true, children: [_jsx(Text, { color: "white", children: "q" }), " quit"] })] })) })] }));
|
|
125
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import zod from 'zod';
|
|
4
|
+
import { Box, Text } from 'ink';
|
|
5
|
+
import { gql } from '../../lib/client.js';
|
|
6
|
+
import { Spinner } from '../../components/Spinner.js';
|
|
7
|
+
export const args = zod.tuple([
|
|
8
|
+
zod.string().describe('Topic ID'),
|
|
9
|
+
]);
|
|
10
|
+
const QUERY = `
|
|
11
|
+
query Topics {
|
|
12
|
+
topics {
|
|
13
|
+
id: nanoId
|
|
14
|
+
name
|
|
15
|
+
description
|
|
16
|
+
version
|
|
17
|
+
createdAt
|
|
18
|
+
updatedAt
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
`;
|
|
22
|
+
export default function TopicView({ args: [id] }) {
|
|
23
|
+
const [data, setData] = useState(null);
|
|
24
|
+
const [error, setError] = useState(null);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
async function run() {
|
|
27
|
+
try {
|
|
28
|
+
const result = await gql(QUERY);
|
|
29
|
+
const found = result.topics.find((p) => p.id === id);
|
|
30
|
+
if (!found)
|
|
31
|
+
throw new Error(`Topic "${id}" not found. Run: sonar topics`);
|
|
32
|
+
setData(found);
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
run();
|
|
39
|
+
}, []);
|
|
40
|
+
if (error)
|
|
41
|
+
return _jsxs(Text, { color: "red", children: ["Error: ", error] });
|
|
42
|
+
if (!data)
|
|
43
|
+
return _jsx(Spinner, { label: "Loading topic..." });
|
|
44
|
+
const updatedAt = new Date(data.updatedAt).toLocaleDateString('en-US', {
|
|
45
|
+
month: 'short', day: 'numeric', year: 'numeric',
|
|
46
|
+
});
|
|
47
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: data.name }), _jsxs(Text, { dimColor: true, children: ["v", data.version, " \u00B7 ", data.id, " \u00B7 ", updatedAt] })] }), data.description && (_jsx(Box, { paddingLeft: 2, children: _jsx(Text, { wrap: "wrap", children: data.description }) })), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["edit: sonar topics edit ", data.id, " --name \"new name\""] }), _jsxs(Text, { dimColor: true, children: ["delete: sonar topics delete ", data.id] })] })] }));
|
|
48
|
+
}
|
|
@@ -2,5 +2,5 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import { formatDistanceToNow } from 'date-fns';
|
|
4
4
|
export function AccountCard({ me }) {
|
|
5
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: "cyan", children: "Account" }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "handle: " }), "@", me.xHandle] }), me.email && _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "email: " }), me.email] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "plan: " }), me.isPayingCustomer ? 'Pro' : 'Free'] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "indexing accounts: " }), me.indexingAccounts] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "indexed tweets: " }), me.indexedTweets.toLocaleString()] }),
|
|
5
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: "cyan", children: "Account" }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "handle: " }), "@", me.xHandle] }), me.email && _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "email: " }), me.email] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "plan: " }), me.isPayingCustomer ? 'Pro' : 'Free'] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "indexing accounts: " }), me.indexingAccounts] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "indexed tweets: " }), me.indexedTweets.toLocaleString()] }), me.twitterIndexedAt && (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "last indexed: " }), formatDistanceToNow(new Date(me.twitterIndexedAt), { addSuffix: true })] })), me.refreshedSuggestionsAt && (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "suggestions refreshed: " }), formatDistanceToNow(new Date(me.refreshedSuggestionsAt), { addSuffix: true })] }))] }));
|
|
6
6
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Text } from 'ink';
|
|
3
|
+
const LOGO = `
|
|
4
|
+
_|_|_| _|_| _| _| _|_| _|_|_|
|
|
5
|
+
_| _| _| _|_| _| _| _| _| _|
|
|
6
|
+
_|_| _| _| _| _| _| _|_|_|_| _|_|_|
|
|
7
|
+
_| _| _| _| _|_| _| _| _| _|
|
|
8
|
+
_|_|_| _|_| _| _| _| _| _| _|`.trimStart();
|
|
9
|
+
export function Banner() {
|
|
10
|
+
return _jsx(Text, { dimColor: true, children: LOGO });
|
|
11
|
+
}
|