@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.
Files changed (42) hide show
  1. package/README.md +151 -166
  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 -39
  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/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
- }