@1a35e1/sonar-cli 0.2.0 → 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 -39
- 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/components/InterestCard.js +0 -10
|
@@ -1,103 +0,0 @@
|
|
|
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
|
-
import { Table } from '../../components/Table.js';
|
|
8
|
-
import { InteractiveInboxSession } from '../../components/InteractiveSession.js';
|
|
9
|
-
import { getVendor } from '../../lib/config.js';
|
|
10
|
-
export const options = zod.object({
|
|
11
|
-
status: zod.string().optional().describe('Filter by status: inbox|later|replied|archived'),
|
|
12
|
-
limit: zod.number().default(20).describe('Result limit'),
|
|
13
|
-
all: zod.boolean().default(false).describe('Show all statuses'),
|
|
14
|
-
json: zod.boolean().default(false).describe('Raw JSON output'),
|
|
15
|
-
interactive: zod.boolean().default(false).describe('Interactive session mode'),
|
|
16
|
-
vendor: zod.string().optional().describe('AI vendor: openai|anthropic'),
|
|
17
|
-
});
|
|
18
|
-
const LIST_QUERY = `
|
|
19
|
-
query Inbox($status: SuggestionStatus, $limit: Int) {
|
|
20
|
-
suggestions(status: $status, limit: $limit) {
|
|
21
|
-
suggestionId
|
|
22
|
-
score
|
|
23
|
-
projectsMatched
|
|
24
|
-
status
|
|
25
|
-
tweet {
|
|
26
|
-
xid
|
|
27
|
-
text
|
|
28
|
-
createdAt
|
|
29
|
-
user {
|
|
30
|
-
displayName
|
|
31
|
-
username
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
`;
|
|
37
|
-
function relativeTime(dateStr) {
|
|
38
|
-
const diff = Date.now() - new Date(dateStr).getTime();
|
|
39
|
-
const mins = Math.floor(diff / 60000);
|
|
40
|
-
if (mins < 60)
|
|
41
|
-
return `${mins}m`;
|
|
42
|
-
const hours = Math.floor(mins / 60);
|
|
43
|
-
if (hours < 24)
|
|
44
|
-
return `${hours}h`;
|
|
45
|
-
return `${Math.floor(hours / 24)}d`;
|
|
46
|
-
}
|
|
47
|
-
export default function Inbox({ options: flags }) {
|
|
48
|
-
const [data, setData] = useState(null);
|
|
49
|
-
const [error, setError] = useState(null);
|
|
50
|
-
useEffect(() => {
|
|
51
|
-
async function run() {
|
|
52
|
-
try {
|
|
53
|
-
const status = flags.all ? null : (flags.status?.toUpperCase() ?? 'INBOX');
|
|
54
|
-
const result = await gql(LIST_QUERY, {
|
|
55
|
-
status,
|
|
56
|
-
limit: flags.limit,
|
|
57
|
-
});
|
|
58
|
-
if (flags.json) {
|
|
59
|
-
if (result.suggestions.length === 0) {
|
|
60
|
-
const statusLabel = flags.all ? 'all statuses' : (flags.status ?? 'inbox');
|
|
61
|
-
process.stderr.write([
|
|
62
|
-
`[sonar inbox] Empty result for status=${statusLabel} — possible causes:`,
|
|
63
|
-
' • No interests defined. Run: sonar interests create --from-prompt "..."',
|
|
64
|
-
' • Ingest and matching have not run. Run: sonar ingest tweets && sonar interests match',
|
|
65
|
-
' • All inbox items were already actioned. Try: sonar inbox --all',
|
|
66
|
-
' • Account/quota issue. Run: sonar account',
|
|
67
|
-
].join('\n') + '\n');
|
|
68
|
-
}
|
|
69
|
-
process.stdout.write(JSON.stringify(result.suggestions, null, 2) + '\n');
|
|
70
|
-
process.exit(0);
|
|
71
|
-
}
|
|
72
|
-
setData(result.suggestions);
|
|
73
|
-
}
|
|
74
|
-
catch (err) {
|
|
75
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
run();
|
|
79
|
-
}, []);
|
|
80
|
-
if (error) {
|
|
81
|
-
return _jsxs(Text, { color: "red", children: ["Error: ", error] });
|
|
82
|
-
}
|
|
83
|
-
if (!data) {
|
|
84
|
-
return _jsx(Spinner, { label: "Fetching inbox..." });
|
|
85
|
-
}
|
|
86
|
-
if (data.length === 0) {
|
|
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" })] })] }));
|
|
89
|
-
}
|
|
90
|
-
if (flags.interactive) {
|
|
91
|
-
return _jsx(InteractiveInboxSession, { items: data, vendor: getVendor(flags.vendor) });
|
|
92
|
-
}
|
|
93
|
-
const rows = data.map((s) => ({
|
|
94
|
-
id: s.suggestionId.slice(0, 8),
|
|
95
|
-
score: s.score.toFixed(2),
|
|
96
|
-
interests: s.projectsMatched,
|
|
97
|
-
age: relativeTime(s.tweet.createdAt),
|
|
98
|
-
author: `@${s.tweet.user.username ?? s.tweet.user.displayName}`,
|
|
99
|
-
tweet: s.tweet.text.replace(/\n/g, ' ').slice(0, 80),
|
|
100
|
-
}));
|
|
101
|
-
const label = flags.all ? 'All' : (flags.status ? flags.status.toLowerCase() : 'Inbox');
|
|
102
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, children: label }), _jsxs(Text, { dimColor: true, children: [" (", data.length, ")"] })] }), _jsx(Table, { rows: rows, columns: ['id', 'score', 'interests', 'age', 'author', 'tweet'] })] }));
|
|
103
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
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 { Text } from 'ink';
|
|
5
|
-
import { gql } from '../../lib/client.js';
|
|
6
|
-
import { Spinner } from '../../components/Spinner.js';
|
|
7
|
-
export const options = zod.object({
|
|
8
|
-
id: zod.string().describe('Suggestion ID to mark as read'),
|
|
9
|
-
});
|
|
10
|
-
const UPDATE_MUTATION = `
|
|
11
|
-
mutation UpdateSuggestion($suggestionId: ID!, $status: SuggestionStatus!) {
|
|
12
|
-
updateSuggestion(input: { suggestionId: $suggestionId, status: $status }) {
|
|
13
|
-
suggestionId
|
|
14
|
-
status
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
`;
|
|
18
|
-
export default function InboxRead({ options: flags }) {
|
|
19
|
-
const [result, setResult] = useState(null);
|
|
20
|
-
const [error, setError] = useState(null);
|
|
21
|
-
useEffect(() => {
|
|
22
|
-
async function run() {
|
|
23
|
-
try {
|
|
24
|
-
const res = await gql(UPDATE_MUTATION, {
|
|
25
|
-
suggestionId: flags.id,
|
|
26
|
-
status: 'READ',
|
|
27
|
-
});
|
|
28
|
-
setResult(res.updateSuggestion);
|
|
29
|
-
}
|
|
30
|
-
catch (err) {
|
|
31
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
run();
|
|
35
|
-
}, []);
|
|
36
|
-
if (error)
|
|
37
|
-
return _jsxs(Text, { color: "red", children: ["Error: ", error] });
|
|
38
|
-
if (!result)
|
|
39
|
-
return _jsx(Spinner, { label: "Updating..." });
|
|
40
|
-
return (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: result.suggestionId.slice(0, 8) }), ' → ', _jsx(Text, { color: "green", children: result.status.toLowerCase() })] }));
|
|
41
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useRef, useState } from 'react';
|
|
3
|
-
import { Box, Text } from 'ink';
|
|
4
|
-
import { gql } from '../../lib/client.js';
|
|
5
|
-
import { Spinner } from '../../components/Spinner.js';
|
|
6
|
-
import { RefreshTip } from '../../components/RefreshTip.js';
|
|
7
|
-
/** How long (ms) to wait for the ingest mutation before giving up. */
|
|
8
|
-
const INGEST_TIMEOUT_MS = 15_000;
|
|
9
|
-
export default function IndexBookmarks() {
|
|
10
|
-
const [queued, setQueued] = useState(null);
|
|
11
|
-
const [error, setError] = useState(null);
|
|
12
|
-
const [timedOut, setTimedOut] = useState(false);
|
|
13
|
-
const deadlineRef = useRef(null);
|
|
14
|
-
useEffect(() => {
|
|
15
|
-
// Hard wall-clock timeout — catches cases where the gql call itself
|
|
16
|
-
// hangs (e.g. server accepts the connection but never sends a response).
|
|
17
|
-
deadlineRef.current = setTimeout(() => {
|
|
18
|
-
setTimedOut(true);
|
|
19
|
-
setError(`Ingest trigger timed out after ${INGEST_TIMEOUT_MS / 1000}s.\n` +
|
|
20
|
-
'The server accepted the request but did not respond in time.\n' +
|
|
21
|
-
'Next steps:\n' +
|
|
22
|
-
' • Run "sonar ingest monitor" — the job may still be queued\n' +
|
|
23
|
-
' • Check SONAR_API_URL points to the correct endpoint\n' +
|
|
24
|
-
' • Verify the server is healthy and retry');
|
|
25
|
-
}, INGEST_TIMEOUT_MS);
|
|
26
|
-
async function run() {
|
|
27
|
-
try {
|
|
28
|
-
const res = await gql(`
|
|
29
|
-
mutation IndexBookmarks {
|
|
30
|
-
indexBookmarks
|
|
31
|
-
}
|
|
32
|
-
`);
|
|
33
|
-
if (deadlineRef.current)
|
|
34
|
-
clearTimeout(deadlineRef.current);
|
|
35
|
-
setQueued(res.indexBookmarks);
|
|
36
|
-
}
|
|
37
|
-
catch (err) {
|
|
38
|
-
if (deadlineRef.current)
|
|
39
|
-
clearTimeout(deadlineRef.current);
|
|
40
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
run();
|
|
44
|
-
return () => {
|
|
45
|
-
if (deadlineRef.current)
|
|
46
|
-
clearTimeout(deadlineRef.current);
|
|
47
|
-
};
|
|
48
|
-
}, []);
|
|
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."] }))] }));
|
|
51
|
-
}
|
|
52
|
-
if (queued === null)
|
|
53
|
-
return _jsx(Spinner, { label: "Triggering bookmark indexing..." });
|
|
54
|
-
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "index_bookmarks: " }), _jsx(Text, { children: queued ? '✓ queued' : '✗ failed to queue — check server logs' })] }), !queued && (_jsxs(Text, { dimColor: true, children: ["The server returned false. Verify your API key and account status with ", _jsx(Text, { color: "cyan", children: "sonar account" }), "."] })), _jsx(RefreshTip, {})] }));
|
|
55
|
-
}
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Text } from 'ink';
|
|
3
|
-
export default function Ingest() {
|
|
4
|
-
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, children: "sonar ingest" }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Subcommands:" }), _jsx(Text, { children: " tweets Ingest recent tweets from your network" }), _jsx(Text, { children: " bookmarks Ingest X bookmarks" })] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Examples:" }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "sonar ingest tweets" })] })] })] }));
|
|
5
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useRef, useState } from 'react';
|
|
3
|
-
import { Box, Text } from 'ink';
|
|
4
|
-
import { gql } from '../../lib/client.js';
|
|
5
|
-
import { Spinner } from '../../components/Spinner.js';
|
|
6
|
-
import { RefreshTip } from '../../components/RefreshTip.js';
|
|
7
|
-
/** How long (ms) to wait for the ingest mutation before giving up. */
|
|
8
|
-
const INGEST_TIMEOUT_MS = 15_000;
|
|
9
|
-
export default function IndexTweets() {
|
|
10
|
-
const [queued, setQueued] = useState(null);
|
|
11
|
-
const [error, setError] = useState(null);
|
|
12
|
-
const [timedOut, setTimedOut] = useState(false);
|
|
13
|
-
const deadlineRef = useRef(null);
|
|
14
|
-
useEffect(() => {
|
|
15
|
-
// Hard wall-clock timeout — catches cases where the gql call itself
|
|
16
|
-
// hangs (e.g. server accepts the connection but never sends a response).
|
|
17
|
-
deadlineRef.current = setTimeout(() => {
|
|
18
|
-
setTimedOut(true);
|
|
19
|
-
setError(`Ingest trigger timed out after ${INGEST_TIMEOUT_MS / 1000}s.\n` +
|
|
20
|
-
'The server accepted the request but did not respond in time.\n' +
|
|
21
|
-
'Next steps:\n' +
|
|
22
|
-
' • Run "sonar ingest monitor" — the job may still be queued\n' +
|
|
23
|
-
' • Check SONAR_API_URL points to the correct endpoint\n' +
|
|
24
|
-
' • Verify the server is healthy and retry');
|
|
25
|
-
}, INGEST_TIMEOUT_MS);
|
|
26
|
-
async function run() {
|
|
27
|
-
try {
|
|
28
|
-
const res = await gql(`
|
|
29
|
-
mutation IndexTweets {
|
|
30
|
-
indexTweets
|
|
31
|
-
}
|
|
32
|
-
`);
|
|
33
|
-
if (deadlineRef.current)
|
|
34
|
-
clearTimeout(deadlineRef.current);
|
|
35
|
-
setQueued(res.indexTweets);
|
|
36
|
-
}
|
|
37
|
-
catch (err) {
|
|
38
|
-
if (deadlineRef.current)
|
|
39
|
-
clearTimeout(deadlineRef.current);
|
|
40
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
run();
|
|
44
|
-
return () => {
|
|
45
|
-
if (deadlineRef.current)
|
|
46
|
-
clearTimeout(deadlineRef.current);
|
|
47
|
-
};
|
|
48
|
-
}, []);
|
|
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."] }))] }));
|
|
51
|
-
}
|
|
52
|
-
if (queued === null)
|
|
53
|
-
return _jsx(Spinner, { label: "Triggering tweet indexing..." });
|
|
54
|
-
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "index_tweets: " }), _jsx(Text, { children: queued ? '✓ queued' : '✗ failed to queue — check server logs' })] }), !queued && (_jsxs(Text, { dimColor: true, children: ["The server returned false. Verify your API key and account status with ", _jsx(Text, { color: "cyan", children: "sonar account" }), "."] })), _jsx(RefreshTip, {})] }));
|
|
55
|
-
}
|
|
@@ -1,107 +0,0 @@
|
|
|
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 { generateInterest, OPENAI_TIMEOUT_MS, ANTHROPIC_TIMEOUT_MS } from '../../lib/ai.js';
|
|
7
|
-
import { getVendor } from '../../lib/config.js';
|
|
8
|
-
import { Spinner } from '../../components/Spinner.js';
|
|
9
|
-
export const options = zod.object({
|
|
10
|
-
name: zod.string().optional().describe('Interest name'),
|
|
11
|
-
description: zod.string().optional().describe('Interest description'),
|
|
12
|
-
keywords: zod.string().optional().describe('Comma-separated keywords'),
|
|
13
|
-
topics: zod.string().optional().describe('Comma-separated related topics'),
|
|
14
|
-
fromPrompt: zod.string().optional().describe('Generate fields from a natural language prompt'),
|
|
15
|
-
vendor: zod.string().optional().describe('AI vendor: openai|anthropic'),
|
|
16
|
-
json: zod.boolean().default(false).describe('Raw JSON output'),
|
|
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
|
-
id: nanoId
|
|
34
|
-
name
|
|
35
|
-
description
|
|
36
|
-
keywords
|
|
37
|
-
relatedTopics
|
|
38
|
-
version
|
|
39
|
-
createdAt
|
|
40
|
-
updatedAt
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
`;
|
|
44
|
-
export default function InterestsCreate({ options: flags }) {
|
|
45
|
-
const [data, setData] = useState(null);
|
|
46
|
-
const [error, setError] = useState(null);
|
|
47
|
-
useEffect(() => {
|
|
48
|
-
if (!error || !flags.json)
|
|
49
|
-
return;
|
|
50
|
-
process.stderr.write(`${error}\n`);
|
|
51
|
-
process.exit(1);
|
|
52
|
-
}, [error, flags.json]);
|
|
53
|
-
useEffect(() => {
|
|
54
|
-
async function run() {
|
|
55
|
-
try {
|
|
56
|
-
let name = flags.name;
|
|
57
|
-
let description = flags.description ?? null;
|
|
58
|
-
let keywords = flags.keywords ? flags.keywords.split(',').map((k) => k.trim()) : null;
|
|
59
|
-
let relatedTopics = flags.topics ? flags.topics.split(',').map((t) => t.trim()) : null;
|
|
60
|
-
if (flags.fromPrompt) {
|
|
61
|
-
const vendor = getVendor(flags.vendor);
|
|
62
|
-
const generated = await generateInterest(flags.fromPrompt, vendor);
|
|
63
|
-
name = generated.name;
|
|
64
|
-
description = generated.description;
|
|
65
|
-
keywords = generated.keywords;
|
|
66
|
-
relatedTopics = generated.relatedTopics;
|
|
67
|
-
}
|
|
68
|
-
if (!name) {
|
|
69
|
-
setError('--name or --from-prompt is required');
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
const result = await gql(CREATE_MUTATION, {
|
|
73
|
-
nanoId: null,
|
|
74
|
-
name,
|
|
75
|
-
description,
|
|
76
|
-
keywords,
|
|
77
|
-
relatedTopics,
|
|
78
|
-
});
|
|
79
|
-
if (flags.json) {
|
|
80
|
-
process.stdout.write(JSON.stringify(result.createOrUpdateProject, null, 2) + '\n');
|
|
81
|
-
process.exit(0);
|
|
82
|
-
}
|
|
83
|
-
setData(result.createOrUpdateProject);
|
|
84
|
-
}
|
|
85
|
-
catch (err) {
|
|
86
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
run();
|
|
90
|
-
}, []);
|
|
91
|
-
if (error) {
|
|
92
|
-
if (flags.json)
|
|
93
|
-
return _jsx(_Fragment, {});
|
|
94
|
-
return _jsxs(Text, { color: "red", children: ["Error: ", error] });
|
|
95
|
-
}
|
|
96
|
-
if (!data) {
|
|
97
|
-
if (flags.json)
|
|
98
|
-
return _jsx(_Fragment, {});
|
|
99
|
-
const vendor = getVendor(flags.vendor);
|
|
100
|
-
const timeoutSec = (vendor === 'openai' ? OPENAI_TIMEOUT_MS : ANTHROPIC_TIMEOUT_MS) / 1000;
|
|
101
|
-
const label = flags.fromPrompt
|
|
102
|
-
? `Generating interest via ${vendor}... (may take up to ${timeoutSec}s${vendor === 'openai' ? ' with web search' : ''})`
|
|
103
|
-
: 'Creating interest...';
|
|
104
|
-
return _jsx(Spinner, { label: label });
|
|
105
|
-
}
|
|
106
|
-
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 }), data.keywords && data.keywords.length > 0 && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "keywords:" }), _jsx(Text, { children: data.keywords.join(', ') })] })), data.relatedTopics && data.relatedTopics.length > 0 && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "topics: " }), _jsx(Text, { children: data.relatedTopics.join(', ') })] }))] }));
|
|
107
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
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 { InterestCard } from '../../components/InterestCard.js';
|
|
8
|
-
export const options = zod.object({
|
|
9
|
-
json: zod.boolean().default(false).describe('Raw JSON output'),
|
|
10
|
-
});
|
|
11
|
-
const QUERY = `
|
|
12
|
-
query Interests {
|
|
13
|
-
projects {
|
|
14
|
-
id: nanoId
|
|
15
|
-
name
|
|
16
|
-
description
|
|
17
|
-
keywords
|
|
18
|
-
relatedTopics
|
|
19
|
-
version
|
|
20
|
-
createdAt
|
|
21
|
-
updatedAt
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
`;
|
|
25
|
-
export default function Interests({ options: flags }) {
|
|
26
|
-
const [data, setData] = useState(null);
|
|
27
|
-
const [error, setError] = useState(null);
|
|
28
|
-
const { stdout } = useStdout();
|
|
29
|
-
const termWidth = stdout.columns ?? 100;
|
|
30
|
-
useEffect(() => {
|
|
31
|
-
async function run() {
|
|
32
|
-
try {
|
|
33
|
-
const result = await gql(QUERY);
|
|
34
|
-
if (flags.json) {
|
|
35
|
-
process.stdout.write(JSON.stringify(result.projects, null, 2) + '\n');
|
|
36
|
-
process.exit(0);
|
|
37
|
-
}
|
|
38
|
-
setData(result.projects);
|
|
39
|
-
}
|
|
40
|
-
catch (err) {
|
|
41
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
run();
|
|
45
|
-
}, []);
|
|
46
|
-
if (error) {
|
|
47
|
-
return _jsxs(Text, { color: "red", children: ["Error: ", error] });
|
|
48
|
-
}
|
|
49
|
-
if (!data) {
|
|
50
|
-
return _jsx(Spinner, { label: "Fetching interests..." });
|
|
51
|
-
}
|
|
52
|
-
if (data.length === 0) {
|
|
53
|
-
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { children: "No interests found. Create one from a prompt:" }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: " sonar interests create --from-prompt \"I want to follow the AI agents ecosystem\"" }), _jsx(Text, { dimColor: true, children: " sonar interests create --from-prompt \"Rust and systems programming\" --vendor anthropic" }), _jsx(Text, { dimColor: true, children: " sonar interests create --from-prompt \"DeFi protocols and on-chain finance\"" }), _jsx(Text, { dimColor: true, children: " sonar interests create --from-prompt \"Climate tech and carbon markets\"" })] }), _jsx(Text, { dimColor: true, children: "Or manually: sonar interests create --name \"My Interest\" --keywords \"kw1,kw2\" --topics \"topic1\"" })] }));
|
|
54
|
-
}
|
|
55
|
-
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Interests" }), _jsxs(Text, { dimColor: true, children: [" (", data.length, ")"] })] }), data.map((p, i) => (_jsx(InterestCard, { interest: p, termWidth: termWidth, isLast: i === data.length - 1 }, p.id))), _jsx(Text, { dimColor: true, children: "tip: --json for raw output \u00B7 match: sonar interests match --days 3 \u00B7 update: sonar interests update --id <id> --from-prompt \"...\"" })] }));
|
|
56
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
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
|
-
import { RefreshTip } from '../../components/RefreshTip.js';
|
|
8
|
-
export const options = zod.object({
|
|
9
|
-
days: zod.number().optional().describe('Tweet window in days (default: 1, capped by plan)'),
|
|
10
|
-
});
|
|
11
|
-
export default function InterestsMatch({ options: flags }) {
|
|
12
|
-
const [queued, setQueued] = useState(null);
|
|
13
|
-
const [error, setError] = useState(null);
|
|
14
|
-
useEffect(() => {
|
|
15
|
-
async function run() {
|
|
16
|
-
try {
|
|
17
|
-
const res = await gql(`mutation RegenerateSuggestions($days: Int) {
|
|
18
|
-
regenerateSuggestions(days: $days)
|
|
19
|
-
}`, { days: flags.days ?? 1 });
|
|
20
|
-
setQueued(res.regenerateSuggestions);
|
|
21
|
-
}
|
|
22
|
-
catch (err) {
|
|
23
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
run();
|
|
27
|
-
}, []);
|
|
28
|
-
if (error)
|
|
29
|
-
return _jsxs(Text, { color: "red", children: ["Error: ", error] });
|
|
30
|
-
if (queued === null)
|
|
31
|
-
return _jsx(Spinner, { label: "Matching interests against tweets..." });
|
|
32
|
-
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "interests match: " }), _jsx(Text, { children: queued ? '✓ queued' : '✗ failed' })] }), _jsx(RefreshTip, {})] }));
|
|
33
|
-
}
|
|
@@ -1,153 +0,0 @@
|
|
|
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 { generateInterest, OPENAI_TIMEOUT_MS, ANTHROPIC_TIMEOUT_MS } from '../../lib/ai.js';
|
|
7
|
-
import { getVendor } from '../../lib/config.js';
|
|
8
|
-
import { Spinner } from '../../components/Spinner.js';
|
|
9
|
-
export const options = zod.object({
|
|
10
|
-
id: zod.string().describe('Interest ID to update'),
|
|
11
|
-
name: zod.string().optional().describe('New name'),
|
|
12
|
-
description: zod.string().optional().describe('New description'),
|
|
13
|
-
keywords: zod.string().optional().describe('Comma-separated keywords (full replace)'),
|
|
14
|
-
topics: zod.string().optional().describe('Comma-separated related topics (full replace)'),
|
|
15
|
-
addKeywords: zod.string().optional().describe('Comma-separated keywords to add'),
|
|
16
|
-
removeKeywords: zod.string().optional().describe('Comma-separated keywords to remove'),
|
|
17
|
-
addTopics: zod.string().optional().describe('Comma-separated topics to add'),
|
|
18
|
-
removeTopics: zod.string().optional().describe('Comma-separated topics to remove'),
|
|
19
|
-
fromPrompt: zod.string().optional().describe('Regenerate all fields from a prompt'),
|
|
20
|
-
vendor: zod.string().optional().describe('AI vendor: openai|anthropic'),
|
|
21
|
-
json: zod.boolean().default(false).describe('Raw JSON output'),
|
|
22
|
-
});
|
|
23
|
-
const QUERY = `
|
|
24
|
-
query Interests {
|
|
25
|
-
projects {
|
|
26
|
-
id: nanoId
|
|
27
|
-
name
|
|
28
|
-
description
|
|
29
|
-
keywords
|
|
30
|
-
relatedTopics
|
|
31
|
-
version
|
|
32
|
-
createdAt
|
|
33
|
-
updatedAt
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
`;
|
|
37
|
-
const UPDATE_MUTATION = `
|
|
38
|
-
mutation CreateOrUpdateInterest(
|
|
39
|
-
$nanoId: String
|
|
40
|
-
$name: String!
|
|
41
|
-
$description: String
|
|
42
|
-
$keywords: [String!]
|
|
43
|
-
$relatedTopics: [String!]
|
|
44
|
-
) {
|
|
45
|
-
createOrUpdateProject(input: {
|
|
46
|
-
nanoId: $nanoId
|
|
47
|
-
name: $name
|
|
48
|
-
description: $description
|
|
49
|
-
keywords: $keywords
|
|
50
|
-
relatedTopics: $relatedTopics
|
|
51
|
-
}) {
|
|
52
|
-
id: nanoId
|
|
53
|
-
name
|
|
54
|
-
description
|
|
55
|
-
keywords
|
|
56
|
-
relatedTopics
|
|
57
|
-
version
|
|
58
|
-
createdAt
|
|
59
|
-
updatedAt
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
`;
|
|
63
|
-
async function fetchById(id) {
|
|
64
|
-
const result = await gql(QUERY);
|
|
65
|
-
const found = result.projects.find((p) => p.id === id);
|
|
66
|
-
if (!found)
|
|
67
|
-
throw new Error(`Interest with id "${id}" not found`);
|
|
68
|
-
return found;
|
|
69
|
-
}
|
|
70
|
-
export default function InterestsUpdate({ options: flags }) {
|
|
71
|
-
const [data, setData] = useState(null);
|
|
72
|
-
const [error, setError] = useState(null);
|
|
73
|
-
useEffect(() => {
|
|
74
|
-
if (!error || !flags.json)
|
|
75
|
-
return;
|
|
76
|
-
process.stderr.write(`${error}\n`);
|
|
77
|
-
process.exit(1);
|
|
78
|
-
}, [error, flags.json]);
|
|
79
|
-
useEffect(() => {
|
|
80
|
-
async function run() {
|
|
81
|
-
try {
|
|
82
|
-
const isPatch = !!(flags.addKeywords || flags.removeKeywords || flags.addTopics || flags.removeTopics);
|
|
83
|
-
let name = flags.name;
|
|
84
|
-
let description = flags.description ?? null;
|
|
85
|
-
let keywords = flags.keywords ? flags.keywords.split(',').map((k) => k.trim()) : null;
|
|
86
|
-
let relatedTopics = flags.topics ? flags.topics.split(',').map((t) => t.trim()) : null;
|
|
87
|
-
if (isPatch) {
|
|
88
|
-
const existing = await fetchById(flags.id);
|
|
89
|
-
name = flags.name ?? existing.name;
|
|
90
|
-
description = flags.description ?? existing.description ?? null;
|
|
91
|
-
const addKw = flags.addKeywords ? flags.addKeywords.split(',').map((k) => k.trim()).filter(Boolean) : [];
|
|
92
|
-
const removeKw = flags.removeKeywords ? new Set(flags.removeKeywords.split(',').map((k) => k.trim())) : new Set();
|
|
93
|
-
const existingKw = existing.keywords ?? [];
|
|
94
|
-
keywords = [...new Set([...existingKw.filter((k) => !removeKw.has(k)), ...addKw])];
|
|
95
|
-
const addT = flags.addTopics ? flags.addTopics.split(',').map((t) => t.trim()).filter(Boolean) : [];
|
|
96
|
-
const removeT = flags.removeTopics ? new Set(flags.removeTopics.split(',').map((t) => t.trim())) : new Set();
|
|
97
|
-
const existingT = existing.relatedTopics ?? [];
|
|
98
|
-
relatedTopics = [...new Set([...existingT.filter((t) => !removeT.has(t)), ...addT])];
|
|
99
|
-
}
|
|
100
|
-
else if (flags.fromPrompt) {
|
|
101
|
-
const vendor = getVendor(flags.vendor);
|
|
102
|
-
const generated = await generateInterest(flags.fromPrompt, vendor);
|
|
103
|
-
name = generated.name;
|
|
104
|
-
description = generated.description;
|
|
105
|
-
keywords = generated.keywords;
|
|
106
|
-
relatedTopics = generated.relatedTopics;
|
|
107
|
-
}
|
|
108
|
-
if (!name) {
|
|
109
|
-
const existing = await fetchById(flags.id);
|
|
110
|
-
name = existing.name;
|
|
111
|
-
if (!description)
|
|
112
|
-
description = existing.description ?? null;
|
|
113
|
-
if (!keywords)
|
|
114
|
-
keywords = existing.keywords ?? null;
|
|
115
|
-
if (!relatedTopics)
|
|
116
|
-
relatedTopics = existing.relatedTopics ?? null;
|
|
117
|
-
}
|
|
118
|
-
const result = await gql(UPDATE_MUTATION, {
|
|
119
|
-
nanoId: flags.id,
|
|
120
|
-
name,
|
|
121
|
-
description,
|
|
122
|
-
keywords,
|
|
123
|
-
relatedTopics,
|
|
124
|
-
});
|
|
125
|
-
if (flags.json) {
|
|
126
|
-
process.stdout.write(JSON.stringify(result.createOrUpdateProject, null, 2) + '\n');
|
|
127
|
-
process.exit(0);
|
|
128
|
-
}
|
|
129
|
-
setData(result.createOrUpdateProject);
|
|
130
|
-
}
|
|
131
|
-
catch (err) {
|
|
132
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
run();
|
|
136
|
-
}, []);
|
|
137
|
-
if (error) {
|
|
138
|
-
if (flags.json)
|
|
139
|
-
return _jsx(_Fragment, {});
|
|
140
|
-
return _jsxs(Text, { color: "red", children: ["Error: ", error] });
|
|
141
|
-
}
|
|
142
|
-
if (!data) {
|
|
143
|
-
if (flags.json)
|
|
144
|
-
return _jsx(_Fragment, {});
|
|
145
|
-
const vendor = getVendor(flags.vendor);
|
|
146
|
-
const timeoutSec = (vendor === 'openai' ? OPENAI_TIMEOUT_MS : ANTHROPIC_TIMEOUT_MS) / 1000;
|
|
147
|
-
const label = flags.fromPrompt
|
|
148
|
-
? `Generating interest via ${vendor}... (may take up to ${timeoutSec}s${vendor === 'openai' ? ' with web search' : ''})`
|
|
149
|
-
: 'Updating interest...';
|
|
150
|
-
return _jsx(Spinner, { label: label });
|
|
151
|
-
}
|
|
152
|
-
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 updated"] })] }), data.description && _jsx(Text, { dimColor: true, children: data.description }), data.keywords && data.keywords.length > 0 && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "keywords:" }), _jsx(Text, { children: data.keywords.join(', ') })] })), data.relatedTopics && data.relatedTopics.length > 0 && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "topics: " }), _jsx(Text, { children: data.relatedTopics.join(', ') })] }))] }));
|
|
153
|
-
}
|