@1a35e1/sonar-cli 0.1.0
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 +422 -0
- package/dist/cli.js +4 -0
- package/dist/commands/account.js +75 -0
- package/dist/commands/config/data/download.js +53 -0
- package/dist/commands/config/data/path.js +11 -0
- package/dist/commands/config/data/sql.js +12 -0
- package/dist/commands/config/data/sync.js +85 -0
- package/dist/commands/config/env.js +15 -0
- package/dist/commands/config/index.js +12 -0
- package/dist/commands/config/nuke.js +19 -0
- package/dist/commands/config/set.js +38 -0
- package/dist/commands/config/setup.js +29 -0
- package/dist/commands/config/skill.js +15 -0
- package/dist/commands/feed.js +172 -0
- package/dist/commands/inbox/archive.js +41 -0
- package/dist/commands/inbox/index.js +92 -0
- package/dist/commands/inbox/later.js +41 -0
- package/dist/commands/inbox/read.js +41 -0
- package/dist/commands/inbox/skip.js +41 -0
- package/dist/commands/index.js +5 -0
- package/dist/commands/ingest/bookmarks.js +31 -0
- package/dist/commands/ingest/index.js +5 -0
- package/dist/commands/ingest/tweets.js +31 -0
- package/dist/commands/interests/create.js +94 -0
- package/dist/commands/interests/index.js +56 -0
- package/dist/commands/interests/match.js +33 -0
- package/dist/commands/interests/update.js +142 -0
- package/dist/commands/monitor.js +81 -0
- package/dist/components/AccountCard.js +6 -0
- package/dist/components/InteractiveSession.js +241 -0
- package/dist/components/InterestCard.js +10 -0
- package/dist/components/RefreshTip.js +5 -0
- package/dist/components/Spinner.js +14 -0
- package/dist/components/Table.js +23 -0
- package/dist/lib/ai.js +160 -0
- package/dist/lib/client.js +33 -0
- package/dist/lib/config.js +74 -0
- package/dist/lib/data-queries.js +61 -0
- package/dist/lib/db.js +73 -0
- package/dist/lib/skill.js +290 -0
- package/dist/types/sonar.js +42 -0
- package/package.json +47 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
import { Text } from 'ink';
|
|
4
|
+
const maskSensitive = (value) => {
|
|
5
|
+
return value.replace(/[^a-zA-Z0-9]/g, '*').slice(0, 4) + '***' + value.slice(-4);
|
|
6
|
+
};
|
|
7
|
+
export default function Env() {
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
process.stdout.write(`SONAR_API_KEY=${maskSensitive(process.env.SONAR_API_KEY ?? '')}\n`);
|
|
10
|
+
process.stdout.write(`SONAR_AI_VENDOR=${process.env.SONAR_AI_VENDOR}\n`);
|
|
11
|
+
process.stdout.write(`SONAR_FEED_RENDER=${process.env.SONAR_FEED_RENDER}\n`);
|
|
12
|
+
process.stdout.write(`SONAR_FEED_WIDTH=${process.env.SONAR_FEED_WIDTH}\n`);
|
|
13
|
+
}, []);
|
|
14
|
+
return _jsx(Text, { dimColor: true, children: "Environment variables:" });
|
|
15
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
import { Text } from 'ink';
|
|
4
|
+
import { readConfig } from '../../lib/config.js';
|
|
5
|
+
export default function Config() {
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
const cfg = readConfig();
|
|
8
|
+
process.stdout.write(`${JSON.stringify({ apiUrl: cfg.apiUrl, vendor: cfg.vendor ?? 'openai', feedRender: cfg.feedRender ?? 'card', feedWidth: cfg.feedWidth ?? 80, hasToken: !!cfg.token }, null, 2)}\n`);
|
|
9
|
+
process.exit(0);
|
|
10
|
+
}, []);
|
|
11
|
+
return _jsx(Text, { dimColor: true, children: "Reading config..." });
|
|
12
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
import { configExists, deleteConfig, deleteDatabase } from '../../lib/config.js';
|
|
4
|
+
import { Text } from 'ink';
|
|
5
|
+
import zod from 'zod';
|
|
6
|
+
export const options = zod.object({
|
|
7
|
+
confirm: zod.boolean().default(false).describe('Pass to confirm deletion'),
|
|
8
|
+
});
|
|
9
|
+
export default function Nuke({ options: flags }) {
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (configExists() && flags.confirm) {
|
|
12
|
+
deleteConfig();
|
|
13
|
+
deleteDatabase();
|
|
14
|
+
process.stdout.write('Workspace deleted at ~/.sonar/config.json and ~/.sonar/database.sqlite\n');
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
}, []);
|
|
18
|
+
return _jsxs(Text, { dimColor: true, children: ["Tip. (pass ", _jsx(Text, { color: "cyan", children: "--confirm" }), " to nuke)"] });
|
|
19
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
import zod from 'zod';
|
|
4
|
+
import { Text } from 'ink';
|
|
5
|
+
import { writeConfig, getVendor } from '../../lib/config.js';
|
|
6
|
+
export const options = zod.object({
|
|
7
|
+
key: zod.string().describe('Config key: vendor, feed-render, feed-width'),
|
|
8
|
+
value: zod.string().describe('Value to set'),
|
|
9
|
+
});
|
|
10
|
+
export default function ConfigSet({ options: flags }) {
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const { key, value } = flags;
|
|
13
|
+
if (key === 'vendor') {
|
|
14
|
+
const vendor = getVendor(value);
|
|
15
|
+
writeConfig({ vendor });
|
|
16
|
+
process.stdout.write(`Vendor preference set to "${vendor}" in ~/.sonar/config.json\n`);
|
|
17
|
+
process.exit(0);
|
|
18
|
+
}
|
|
19
|
+
if (key === 'feed-render') {
|
|
20
|
+
writeConfig({ feedRender: value });
|
|
21
|
+
process.stdout.write(`Feed render set to "${value}" in ~/.sonar/config.json\n`);
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
if (key === 'feed-width') {
|
|
25
|
+
const n = Number(value);
|
|
26
|
+
if (!Number.isInteger(n) || n < 20) {
|
|
27
|
+
process.stderr.write('feed-width must be an integer >= 20\n');
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
writeConfig({ feedWidth: n });
|
|
31
|
+
process.stdout.write(`Feed width set to ${n} in ~/.sonar/config.json\n`);
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
process.stderr.write(`Unknown config key "${key}". Supported keys: vendor, feed-render, feed-width\n`);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}, []);
|
|
37
|
+
return _jsx(Text, { dimColor: true, children: "Updating config..." });
|
|
38
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
import { Text } from 'ink';
|
|
4
|
+
import { writeConfig, configExists } from '../../lib/config.js';
|
|
5
|
+
import zod from 'zod';
|
|
6
|
+
export const options = zod.object({
|
|
7
|
+
key: zod.string().describe('API key to use').optional(),
|
|
8
|
+
});
|
|
9
|
+
export default function Setup({ options: flags }) {
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (configExists()) {
|
|
12
|
+
process.stderr.write('Workspace already initialised at ~/.sonar/config.json\n');
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
const apiKey = flags.key || process.env.SONAR_API_KEY;
|
|
16
|
+
const apiUrl = process.env.SONAR_API_URL;
|
|
17
|
+
if (!apiKey) {
|
|
18
|
+
process.stderr.write('SONAR_API_KEY is not set. Generate a key at https://sonar.8640p.info\n');
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
writeConfig({
|
|
22
|
+
token: apiKey,
|
|
23
|
+
...(apiUrl ? { apiUrl } : {}),
|
|
24
|
+
});
|
|
25
|
+
process.stdout.write('Workspace initialised at ~/.sonar/config.json\n');
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}, []);
|
|
28
|
+
return _jsx(Text, { dimColor: true, children: "Initialising workspace..." });
|
|
29
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
import zod from 'zod';
|
|
4
|
+
import { Text } from 'ink';
|
|
5
|
+
import { writeSkillTo } from '../../lib/skill.js';
|
|
6
|
+
export const options = zod.object({
|
|
7
|
+
install: zod.boolean().default(false).describe('Install to ~/.claude/skills/sonar/SKILL.md'),
|
|
8
|
+
dest: zod.string().optional().describe('Write to a custom path'),
|
|
9
|
+
});
|
|
10
|
+
export default function Skill({ options: flags }) {
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
writeSkillTo(flags.dest, flags.install);
|
|
13
|
+
}, []);
|
|
14
|
+
return _jsx(Text, { dimColor: true, children: "Generating SKILL.md..." });
|
|
15
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import zod from 'zod';
|
|
4
|
+
import { Box, Text, useStdout } from 'ink';
|
|
5
|
+
import Link from 'ink-link';
|
|
6
|
+
import { Spinner } from '../components/Spinner.js';
|
|
7
|
+
import { Table } from '../components/Table.js';
|
|
8
|
+
import { InteractiveFeedSession } from '../components/InteractiveSession.js';
|
|
9
|
+
import { gql } from '../lib/client.js';
|
|
10
|
+
import { getFeedRender, getFeedWidth, getVendor } from '../lib/config.js';
|
|
11
|
+
export const options = zod.object({
|
|
12
|
+
hours: zod.number().optional().describe('Look back N hours (default: 12)'),
|
|
13
|
+
days: zod.number().optional().describe('Look back N days'),
|
|
14
|
+
limit: zod.number().optional().describe('Result limit (default: 20)'),
|
|
15
|
+
kind: zod.string().optional().describe('Feed source: default|bookmarks|followers|following'),
|
|
16
|
+
render: zod.string().optional().describe('Output layout: card|table'),
|
|
17
|
+
width: zod.number().optional().describe('Card width in columns'),
|
|
18
|
+
json: zod.boolean().default(false).describe('Raw JSON output'),
|
|
19
|
+
interactive: zod.boolean().default(false).describe('Interactive session mode'),
|
|
20
|
+
vendor: zod.string().optional().describe('AI vendor: openai|anthropic'),
|
|
21
|
+
});
|
|
22
|
+
export default function Feed({ options: flags }) {
|
|
23
|
+
const [data, setData] = useState(null);
|
|
24
|
+
const [error, setError] = useState(null);
|
|
25
|
+
const { stdout } = useStdout();
|
|
26
|
+
const termWidth = stdout.columns ?? 100;
|
|
27
|
+
const render = getFeedRender(flags.render);
|
|
28
|
+
const cardWidth = getFeedWidth(flags.width);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
async function run() {
|
|
31
|
+
try {
|
|
32
|
+
const result = await gql(FEED_QUERY, {
|
|
33
|
+
hours: flags.hours ?? null,
|
|
34
|
+
days: flags.days ?? null,
|
|
35
|
+
limit: flags.limit ?? 20,
|
|
36
|
+
kind: flags.kind ?? 'default',
|
|
37
|
+
});
|
|
38
|
+
if (flags.json) {
|
|
39
|
+
process.stdout.write(`${JSON.stringify(result.feed, null, 2)}\n`);
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
|
+
setData(result.feed);
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
run();
|
|
49
|
+
}, [flags.hours, flags.days, flags.limit, flags.json, flags.kind]);
|
|
50
|
+
if (error) {
|
|
51
|
+
return _jsxs(Text, { color: "red", children: ["Error: ", error] });
|
|
52
|
+
}
|
|
53
|
+
if (!data) {
|
|
54
|
+
return _jsx(Spinner, { label: "Fetching feed..." });
|
|
55
|
+
}
|
|
56
|
+
if (data.length === 0) {
|
|
57
|
+
return _jsx(Text, { dimColor: true, children: "No tweets found in this window." });
|
|
58
|
+
}
|
|
59
|
+
if (flags.interactive) {
|
|
60
|
+
return _jsx(InteractiveFeedSession, { items: data, vendor: getVendor(flags.vendor) });
|
|
61
|
+
}
|
|
62
|
+
const win = windowLabel(flags.hours, flags.days);
|
|
63
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "white", children: flags.kind === 'bookmarks'
|
|
64
|
+
? 'Bookmarks Feed'
|
|
65
|
+
: flags.kind === 'followers'
|
|
66
|
+
? 'Followers Feed'
|
|
67
|
+
: flags.kind === 'following'
|
|
68
|
+
? 'Following Feed'
|
|
69
|
+
: 'Network Feed' }), flags.kind !== 'bookmarks' && (_jsxs(Text, { dimColor: true, children: [' · ', "last ", win] })), _jsxs(Text, { dimColor: true, children: [" (", data.length, ")"] })] }), _jsx(Text, { dimColor: true, children: '─'.repeat(Math.min(termWidth - 2, 72)) })] }), render === 'table' ? (_jsx(FeedTable, { data: data })) : (_jsx(Box, { flexDirection: "column", children: data.map((item, i) => (_jsx(TweetCard, { item: item, termWidth: termWidth, cardWidth: cardWidth, isLast: i === data.length - 1 }, item.tweet.id))) })), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "tip adjust window \u2192 " }), _jsx(Text, { color: "cyan", children: "sonar feed --hours 24" })] })] }));
|
|
70
|
+
}
|
|
71
|
+
// ─── Query ────────────────────────────────────────────────────────────────────
|
|
72
|
+
const FEED_QUERY = `
|
|
73
|
+
query Feed($hours: Int, $days: Int, $limit: Int, $kind: String) {
|
|
74
|
+
feed(hours: $hours, days: $days, limit: $limit, kind: $kind) {
|
|
75
|
+
score
|
|
76
|
+
matchedKeywords
|
|
77
|
+
tweet {
|
|
78
|
+
id
|
|
79
|
+
xid
|
|
80
|
+
text
|
|
81
|
+
createdAt
|
|
82
|
+
likeCount
|
|
83
|
+
retweetCount
|
|
84
|
+
replyCount
|
|
85
|
+
user {
|
|
86
|
+
displayName
|
|
87
|
+
username
|
|
88
|
+
followersCount
|
|
89
|
+
followingCount
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
`;
|
|
95
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
96
|
+
function windowLabel(hours, days) {
|
|
97
|
+
if (hours)
|
|
98
|
+
return `${hours}h`;
|
|
99
|
+
if (days)
|
|
100
|
+
return `${days}d`;
|
|
101
|
+
return '12h';
|
|
102
|
+
}
|
|
103
|
+
function formatTimestamp(dateStr) {
|
|
104
|
+
const d = new Date(dateStr);
|
|
105
|
+
const month = d.toLocaleString('en-US', { month: 'short' });
|
|
106
|
+
const day = d.getDate();
|
|
107
|
+
const hours = d.getHours();
|
|
108
|
+
const mins = d.getMinutes().toString().padStart(2, '0');
|
|
109
|
+
const ampm = hours >= 12 ? 'pm' : 'am';
|
|
110
|
+
const h = hours % 12 || 12;
|
|
111
|
+
return `${month} ${day} · ${h}:${mins}${ampm}`;
|
|
112
|
+
}
|
|
113
|
+
function relativeTime(dateStr) {
|
|
114
|
+
const diff = Date.now() - new Date(dateStr).getTime();
|
|
115
|
+
const mins = Math.floor(diff / 60000);
|
|
116
|
+
if (mins < 60)
|
|
117
|
+
return `${mins}m`;
|
|
118
|
+
const hours = Math.floor(mins / 60);
|
|
119
|
+
if (hours < 24)
|
|
120
|
+
return `${hours}h`;
|
|
121
|
+
return `${Math.floor(hours / 24)}d`;
|
|
122
|
+
}
|
|
123
|
+
function formatCount(n) {
|
|
124
|
+
if (n == null)
|
|
125
|
+
return null;
|
|
126
|
+
if (n >= 1_000_000)
|
|
127
|
+
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
128
|
+
if (n >= 1_000)
|
|
129
|
+
return `${(n / 1_000).toFixed(1)}k`;
|
|
130
|
+
return String(n);
|
|
131
|
+
}
|
|
132
|
+
function scoreColor(score) {
|
|
133
|
+
if (score >= 0.7)
|
|
134
|
+
return 'green';
|
|
135
|
+
if (score >= 0.4)
|
|
136
|
+
return 'yellow';
|
|
137
|
+
return 'white';
|
|
138
|
+
}
|
|
139
|
+
function linkifyMentions(text) {
|
|
140
|
+
return text.replace(/@(\w+)/g, (match, handle) => {
|
|
141
|
+
const url = `https://x.com/${handle}`;
|
|
142
|
+
return `\x1b]8;;${url}\x07\x1b[94m${match}\x1b[39m\x1b]8;;\x07`;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
function TweetText({ text }) {
|
|
146
|
+
return _jsx(Text, { wrap: "wrap", children: linkifyMentions(text) });
|
|
147
|
+
}
|
|
148
|
+
export function TweetCard({ item, termWidth, cardWidth, isLast }) {
|
|
149
|
+
const { tweet, score } = item;
|
|
150
|
+
const handle = tweet.user.username ?? tweet.user.displayName;
|
|
151
|
+
const author = `@${handle}`;
|
|
152
|
+
const bodyBoxWidth = Math.min(cardWidth + 2, termWidth);
|
|
153
|
+
const profileUrl = `https://x.com/${handle}`;
|
|
154
|
+
const tweetUrl = `https://x.com/${handle}/status/${tweet.id}`;
|
|
155
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: isLast ? 0 : 1, width: termWidth, children: [_jsxs(Box, { children: [_jsx(Link, { url: tweetUrl, fallback: false, children: _jsx(Text, { color: "cyan", bold: true, children: formatTimestamp(tweet.createdAt) }) }), _jsxs(Text, { dimColor: true, children: [" ", relativeTime(tweet.createdAt)] }), score > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: " \u00B7 " }), _jsx(Text, { color: scoreColor(score), children: score.toFixed(2) })] }))] }), _jsxs(Box, { children: [_jsxs(Text, { color: "gray", children: ['└', " "] }), _jsx(Link, { url: profileUrl, fallback: false, children: _jsx(Text, { color: "blueBright", bold: true, children: author }) }), formatCount(tweet.user.followersCount) && (_jsxs(_Fragment, { children: [_jsxs(Text, { dimColor: true, children: [' ', formatCount(tweet.user.followersCount), " followers"] }), formatCount(tweet.user.followingCount) && (_jsxs(Text, { dimColor: true, children: [' ', "\u00B7 ", formatCount(tweet.user.followingCount), " following"] }))] }))] }), _jsx(Box, { paddingLeft: 2, width: bodyBoxWidth, marginTop: 1, children: _jsx(TweetText, { text: tweet.text }) }), _jsxs(Box, { marginLeft: 2, marginTop: 1, children: [_jsxs(Text, { color: "red", children: ["\u2665 ", tweet.likeCount] }), _jsx(Text, { dimColor: true, children: " " }), _jsxs(Text, { color: "green", children: ["\u21BA ", tweet.retweetCount] }), tweet.replyCount > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: " " }), _jsxs(Text, { dimColor: true, children: ["\u21A9 ", tweet.replyCount] })] }))] }), item.matchedKeywords.length > 0 && (_jsxs(Box, { marginLeft: 2, children: [_jsx(Text, { dimColor: true, children: "keywords " }), _jsx(Text, { color: "yellow", children: item.matchedKeywords.join(' ') })] })), _jsxs(Box, { marginLeft: 2, children: [_jsx(Link, { url: profileUrl, fallback: false, children: _jsx(Text, { dimColor: true, children: profileUrl }) }), _jsx(Text, { dimColor: true, children: " \u00B7 " }), _jsx(Link, { url: tweetUrl, fallback: false, children: _jsx(Text, { dimColor: true, children: tweetUrl }) })] }), !isLast && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: '─'.repeat(Math.min(termWidth - 2, 72)) }) }))] }));
|
|
156
|
+
}
|
|
157
|
+
function osc8Link(url, label) {
|
|
158
|
+
return `\x1b]8;;${url}\x07${label}\x1b]8;;\x07`;
|
|
159
|
+
}
|
|
160
|
+
function FeedTable({ data }) {
|
|
161
|
+
const rows = data.map((item) => {
|
|
162
|
+
const handle = item.tweet.user.username ?? item.tweet.user.displayName;
|
|
163
|
+
const tweetUrl = `https://x.com/${handle}/status/${item.tweet.id}`;
|
|
164
|
+
return {
|
|
165
|
+
age: osc8Link(tweetUrl, relativeTime(item.tweet.createdAt)),
|
|
166
|
+
score: item.score > 0 ? item.score.toFixed(2) : '—',
|
|
167
|
+
author: `@${handle}`,
|
|
168
|
+
tweet: item.tweet.text.replace(/\n/g, ' ').slice(0, 80),
|
|
169
|
+
};
|
|
170
|
+
});
|
|
171
|
+
return _jsx(Table, { rows: rows, columns: ['age', 'score', 'author', 'tweet'] });
|
|
172
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
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 archive'),
|
|
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 InboxArchive({ 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: 'ARCHIVED',
|
|
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
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
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
|
+
process.stdout.write(JSON.stringify(result.suggestions, null, 2) + '\n');
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
setData(result.suggestions);
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
run();
|
|
69
|
+
}, []);
|
|
70
|
+
if (error) {
|
|
71
|
+
return _jsxs(Text, { color: "red", children: ["Error: ", error] });
|
|
72
|
+
}
|
|
73
|
+
if (!data) {
|
|
74
|
+
return _jsx(Spinner, { label: "Fetching inbox..." });
|
|
75
|
+
}
|
|
76
|
+
if (data.length === 0) {
|
|
77
|
+
return _jsx(Text, { dimColor: true, children: "Inbox is empty." });
|
|
78
|
+
}
|
|
79
|
+
if (flags.interactive) {
|
|
80
|
+
return _jsx(InteractiveInboxSession, { items: data, vendor: getVendor(flags.vendor) });
|
|
81
|
+
}
|
|
82
|
+
const rows = data.map((s) => ({
|
|
83
|
+
id: s.suggestionId.slice(0, 8),
|
|
84
|
+
score: s.score.toFixed(2),
|
|
85
|
+
interests: s.projectsMatched,
|
|
86
|
+
age: relativeTime(s.tweet.createdAt),
|
|
87
|
+
author: `@${s.tweet.user.username ?? s.tweet.user.displayName}`,
|
|
88
|
+
tweet: s.tweet.text.replace(/\n/g, ' ').slice(0, 80),
|
|
89
|
+
}));
|
|
90
|
+
const label = flags.all ? 'All' : (flags.status ? flags.status.toLowerCase() : 'Inbox');
|
|
91
|
+
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'] })] }));
|
|
92
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
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 save for later'),
|
|
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 InboxLater({ 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: 'LATER',
|
|
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
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
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 skip'),
|
|
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 InboxSkip({ 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: 'SKIPPED',
|
|
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
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Text, Box } from 'ink';
|
|
3
|
+
export default function Index() {
|
|
4
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, children: "Sonar CLI" }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Commands:" }), _jsx(Text, { children: " feed Scored tweet feed from your network" }), _jsx(Text, { children: " inbox Suggestions matching your interests" }), _jsx(Text, { children: " interests Manage interests" }), _jsx(Text, { children: " \u2514\u2500\u2500 create Create a new interest" }), _jsx(Text, { children: " \u2514\u2500\u2500 update Update an interest" }), _jsx(Text, { children: " \u2514\u2500\u2500 match Match interests to ingested tweets" }), _jsx(Text, { children: " ingest Ingest tweets and bookmarks" }), _jsx(Text, { children: " \u2514\u2500\u2500 tweets Ingest recent tweets from social graph" }), _jsx(Text, { children: " \u2514\u2500\u2500 bookmarks Ingest X bookmarks" }), _jsx(Text, { children: " monitor Job queue monitor and account status" }), _jsx(Text, { children: " config Show or set CLI config" }), _jsx(Text, { children: " account Account info and plan usage" })] }), _jsxs(Text, { dimColor: true, children: ["Run ", _jsx(Text, { color: "cyan", children: "sonar <command> --help" }), " for command-specific options."] })] }));
|
|
5
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, 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
|
+
export default function IndexBookmarks() {
|
|
8
|
+
const [queued, setQueued] = useState(null);
|
|
9
|
+
const [error, setError] = useState(null);
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
async function run() {
|
|
12
|
+
try {
|
|
13
|
+
const res = await gql(`
|
|
14
|
+
mutation IndexBookmarks {
|
|
15
|
+
indexBookmarks
|
|
16
|
+
}
|
|
17
|
+
`);
|
|
18
|
+
setQueued(res.indexBookmarks);
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
run();
|
|
25
|
+
}, []);
|
|
26
|
+
if (error)
|
|
27
|
+
return _jsxs(Text, { color: "red", children: ["Error: ", error] });
|
|
28
|
+
if (queued === null)
|
|
29
|
+
return _jsx(Spinner, { label: "Triggering bookmark indexing..." });
|
|
30
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "index_bookmarks: " }), _jsx(Text, { children: queued ? '✓ queued' : '✗ failed' })] }), _jsx(RefreshTip, {})] }));
|
|
31
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, 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
|
+
export default function IndexTweets() {
|
|
8
|
+
const [queued, setQueued] = useState(null);
|
|
9
|
+
const [error, setError] = useState(null);
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
async function run() {
|
|
12
|
+
try {
|
|
13
|
+
const res = await gql(`
|
|
14
|
+
mutation IndexTweets {
|
|
15
|
+
indexTweets
|
|
16
|
+
}
|
|
17
|
+
`);
|
|
18
|
+
setQueued(res.indexTweets);
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
run();
|
|
25
|
+
}, []);
|
|
26
|
+
if (error)
|
|
27
|
+
return _jsxs(Text, { color: "red", children: ["Error: ", error] });
|
|
28
|
+
if (queued === null)
|
|
29
|
+
return _jsx(Spinner, { label: "Triggering tweet indexing..." });
|
|
30
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "index_tweets: " }), _jsx(Text, { children: queued ? '✓ queued' : '✗ failed' })] }), _jsx(RefreshTip, {})] }));
|
|
31
|
+
}
|