@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,94 @@
|
|
|
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 { generateInterest } 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
|
+
async function run() {
|
|
49
|
+
try {
|
|
50
|
+
let name = flags.name;
|
|
51
|
+
let description = flags.description ?? null;
|
|
52
|
+
let keywords = flags.keywords ? flags.keywords.split(',').map((k) => k.trim()) : null;
|
|
53
|
+
let relatedTopics = flags.topics ? flags.topics.split(',').map((t) => t.trim()) : null;
|
|
54
|
+
if (flags.fromPrompt) {
|
|
55
|
+
const vendor = getVendor(flags.vendor);
|
|
56
|
+
const generated = await generateInterest(flags.fromPrompt, vendor);
|
|
57
|
+
name = generated.name;
|
|
58
|
+
description = generated.description;
|
|
59
|
+
keywords = generated.keywords;
|
|
60
|
+
relatedTopics = generated.relatedTopics;
|
|
61
|
+
}
|
|
62
|
+
if (!name) {
|
|
63
|
+
setError('--name or --from-prompt is required');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const result = await gql(CREATE_MUTATION, {
|
|
67
|
+
nanoId: null,
|
|
68
|
+
name,
|
|
69
|
+
description,
|
|
70
|
+
keywords,
|
|
71
|
+
relatedTopics,
|
|
72
|
+
});
|
|
73
|
+
if (flags.json) {
|
|
74
|
+
process.stdout.write(JSON.stringify(result.createOrUpdateProject, null, 2) + '\n');
|
|
75
|
+
process.exit(0);
|
|
76
|
+
}
|
|
77
|
+
setData(result.createOrUpdateProject);
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
run();
|
|
84
|
+
}, []);
|
|
85
|
+
if (error)
|
|
86
|
+
return _jsxs(Text, { color: "red", children: ["Error: ", error] });
|
|
87
|
+
if (!data) {
|
|
88
|
+
const label = flags.fromPrompt
|
|
89
|
+
? `Generating interest via ${getVendor(flags.vendor)}...`
|
|
90
|
+
: 'Creating interest...';
|
|
91
|
+
return _jsx(Spinner, { label: label });
|
|
92
|
+
}
|
|
93
|
+
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(', ') })] }))] }));
|
|
94
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
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 { generateInterest } 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
|
+
async function run() {
|
|
75
|
+
try {
|
|
76
|
+
const isPatch = !!(flags.addKeywords || flags.removeKeywords || flags.addTopics || flags.removeTopics);
|
|
77
|
+
let name = flags.name;
|
|
78
|
+
let description = flags.description ?? null;
|
|
79
|
+
let keywords = flags.keywords ? flags.keywords.split(',').map((k) => k.trim()) : null;
|
|
80
|
+
let relatedTopics = flags.topics ? flags.topics.split(',').map((t) => t.trim()) : null;
|
|
81
|
+
if (isPatch) {
|
|
82
|
+
const existing = await fetchById(flags.id);
|
|
83
|
+
name = flags.name ?? existing.name;
|
|
84
|
+
description = flags.description ?? existing.description ?? null;
|
|
85
|
+
const addKw = flags.addKeywords ? flags.addKeywords.split(',').map((k) => k.trim()).filter(Boolean) : [];
|
|
86
|
+
const removeKw = flags.removeKeywords ? new Set(flags.removeKeywords.split(',').map((k) => k.trim())) : new Set();
|
|
87
|
+
const existingKw = existing.keywords ?? [];
|
|
88
|
+
keywords = [...new Set([...existingKw.filter((k) => !removeKw.has(k)), ...addKw])];
|
|
89
|
+
const addT = flags.addTopics ? flags.addTopics.split(',').map((t) => t.trim()).filter(Boolean) : [];
|
|
90
|
+
const removeT = flags.removeTopics ? new Set(flags.removeTopics.split(',').map((t) => t.trim())) : new Set();
|
|
91
|
+
const existingT = existing.relatedTopics ?? [];
|
|
92
|
+
relatedTopics = [...new Set([...existingT.filter((t) => !removeT.has(t)), ...addT])];
|
|
93
|
+
}
|
|
94
|
+
else if (flags.fromPrompt) {
|
|
95
|
+
const vendor = getVendor(flags.vendor);
|
|
96
|
+
const generated = await generateInterest(flags.fromPrompt, vendor);
|
|
97
|
+
name = generated.name;
|
|
98
|
+
description = generated.description;
|
|
99
|
+
keywords = generated.keywords;
|
|
100
|
+
relatedTopics = generated.relatedTopics;
|
|
101
|
+
}
|
|
102
|
+
if (!name) {
|
|
103
|
+
const existing = await fetchById(flags.id);
|
|
104
|
+
name = existing.name;
|
|
105
|
+
if (!description)
|
|
106
|
+
description = existing.description ?? null;
|
|
107
|
+
if (!keywords)
|
|
108
|
+
keywords = existing.keywords ?? null;
|
|
109
|
+
if (!relatedTopics)
|
|
110
|
+
relatedTopics = existing.relatedTopics ?? null;
|
|
111
|
+
}
|
|
112
|
+
const result = await gql(UPDATE_MUTATION, {
|
|
113
|
+
nanoId: flags.id,
|
|
114
|
+
name,
|
|
115
|
+
description,
|
|
116
|
+
keywords,
|
|
117
|
+
relatedTopics,
|
|
118
|
+
});
|
|
119
|
+
if (flags.json) {
|
|
120
|
+
process.stdout.write(JSON.stringify(result.createOrUpdateProject, null, 2) + '\n');
|
|
121
|
+
process.exit(0);
|
|
122
|
+
}
|
|
123
|
+
setData(result.createOrUpdateProject);
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
run();
|
|
130
|
+
}, []);
|
|
131
|
+
if (error)
|
|
132
|
+
return _jsxs(Text, { color: "red", children: ["Error: ", error] });
|
|
133
|
+
if (!data) {
|
|
134
|
+
const label = flags.fromPrompt
|
|
135
|
+
? `Generating interest via ${getVendor(flags.vendor)}...`
|
|
136
|
+
: (flags.addKeywords || flags.removeKeywords || flags.addTopics || flags.removeTopics)
|
|
137
|
+
? 'Updating interest...'
|
|
138
|
+
: 'Updating interest...';
|
|
139
|
+
return _jsx(Spinner, { label: label });
|
|
140
|
+
}
|
|
141
|
+
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(', ') })] }))] }));
|
|
142
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import zod from 'zod';
|
|
4
|
+
import { Box, Text, useApp } from 'ink';
|
|
5
|
+
import { getToken, getApiUrl } from '../lib/config.js';
|
|
6
|
+
import { gql } from '../lib/client.js';
|
|
7
|
+
import { Spinner } from '../components/Spinner.js';
|
|
8
|
+
import { AccountCard } from '../components/AccountCard.js';
|
|
9
|
+
export const options = zod.object({
|
|
10
|
+
watch: zod.boolean().default(false).describe('Poll and refresh every 2 seconds'),
|
|
11
|
+
});
|
|
12
|
+
const POLL_INTERVAL = 2000;
|
|
13
|
+
const QUEUE_LABELS = {
|
|
14
|
+
tweets: 'Tweets',
|
|
15
|
+
bookmarks: 'Bookmarks',
|
|
16
|
+
social_graph: 'Social graph',
|
|
17
|
+
suggestions: 'Suggestions',
|
|
18
|
+
};
|
|
19
|
+
export default function Monitor({ options: flags }) {
|
|
20
|
+
const { exit } = useApp();
|
|
21
|
+
const [data, setData] = useState(null);
|
|
22
|
+
const [error, setError] = useState(null);
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
const token = getToken();
|
|
25
|
+
const baseUrl = getApiUrl().replace(/\/graphql$/, '');
|
|
26
|
+
async function fetchStatus() {
|
|
27
|
+
try {
|
|
28
|
+
const [statusRes, meRes] = await Promise.all([
|
|
29
|
+
fetch(`${baseUrl}/indexing/status`, {
|
|
30
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
31
|
+
}),
|
|
32
|
+
gql(`
|
|
33
|
+
query MonitorStatus {
|
|
34
|
+
me {
|
|
35
|
+
accountId
|
|
36
|
+
email
|
|
37
|
+
xHandle
|
|
38
|
+
xid
|
|
39
|
+
isPayingCustomer
|
|
40
|
+
indexingAccounts
|
|
41
|
+
indexedTweets
|
|
42
|
+
pendingEmbeddings
|
|
43
|
+
twitterIndexedAt
|
|
44
|
+
refreshedSuggestionsAt
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
`),
|
|
48
|
+
]);
|
|
49
|
+
if (!statusRes.ok)
|
|
50
|
+
throw new Error(`HTTP ${statusRes.status} from ${baseUrl}`);
|
|
51
|
+
const status = await statusRes.json();
|
|
52
|
+
setData({ me: meRes.me, queues: status.queues });
|
|
53
|
+
setError(null);
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
fetchStatus();
|
|
60
|
+
if (!flags.watch)
|
|
61
|
+
return;
|
|
62
|
+
const timer = setInterval(fetchStatus, POLL_INTERVAL);
|
|
63
|
+
return () => clearInterval(timer);
|
|
64
|
+
}, []);
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (!flags.watch && data !== null)
|
|
67
|
+
exit();
|
|
68
|
+
}, [data]);
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (!flags.watch && error !== null)
|
|
71
|
+
exit(new Error(error));
|
|
72
|
+
}, [error]);
|
|
73
|
+
if (error)
|
|
74
|
+
return _jsxs(Text, { color: "red", children: ["Error: ", error] });
|
|
75
|
+
if (!data)
|
|
76
|
+
return _jsx(Spinner, { label: "Loading ingest status..." });
|
|
77
|
+
const { me, queues } = data;
|
|
78
|
+
const entries = Object.entries(queues);
|
|
79
|
+
const hasActivity = entries.length > 0 || me.pendingEmbeddings > 0;
|
|
80
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(AccountCard, { me: me }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: "cyan", children: "Job Queues" }), !hasActivity ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: "green", children: "No active ingest jobs." }), _jsxs(Text, { color: "green", children: ["Run ", _jsx(Text, { color: "cyan", children: "sonar interests match" }), " to start surface relevant tweets."] })] })) : (_jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsxs(Box, { gap: 2, marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: ('Queue').padEnd(16) }), _jsx(Text, { bold: true, color: "cyan", children: 'Running'.padEnd(10) }), _jsx(Text, { bold: true, color: "cyan", children: "Queued" })] }), entries.map(([name, counts]) => (_jsxs(Box, { gap: 2, children: [_jsx(Text, { children: (QUEUE_LABELS[name] ?? name).padEnd(16) }), _jsx(Text, { color: counts.running > 0 ? 'green' : 'white', children: String(counts.running).padEnd(10) }), _jsx(Text, { color: counts.queued > 0 ? 'yellow' : 'white', children: counts.queued })] }, name)))] }))] })] }));
|
|
81
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { formatDistanceToNow } from 'date-fns';
|
|
4
|
+
export function AccountCard({ me }) {
|
|
5
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: "cyan", children: "Account" }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "handle: " }), "@", me.xHandle] }), me.email && _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "email: " }), me.email] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "plan: " }), me.isPayingCustomer ? 'Pro' : 'Free'] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "indexing accounts: " }), me.indexingAccounts] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "indexed tweets: " }), me.indexedTweets.toLocaleString()] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "embeddings queue: " }), _jsx(Text, { color: me.pendingEmbeddings > 0 ? 'yellow' : 'green', children: me.pendingEmbeddings })] }), me.twitterIndexedAt && (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "last indexed: " }), formatDistanceToNow(new Date(me.twitterIndexedAt), { addSuffix: true })] })), me.refreshedSuggestionsAt && (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "suggestions refreshed: " }), formatDistanceToNow(new Date(me.refreshedSuggestionsAt), { addSuffix: true })] }))] }));
|
|
6
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useCallback } from 'react';
|
|
3
|
+
import { Box, Text, useInput, useStdout } from 'ink';
|
|
4
|
+
import { Spinner } from './Spinner.js';
|
|
5
|
+
import { TweetCard } from '../commands/feed.js';
|
|
6
|
+
import { gql } from '../lib/client.js';
|
|
7
|
+
import { generateReply } from '../lib/ai.js';
|
|
8
|
+
import { getFeedWidth } from '../lib/config.js';
|
|
9
|
+
const UPDATE_SUGGESTION_MUTATION = `
|
|
10
|
+
mutation UpdateSuggestion($suggestionId: ID!, $status: SuggestionStatus!) {
|
|
11
|
+
updateSuggestion(input: { suggestionId: $suggestionId, status: $status }) {
|
|
12
|
+
suggestionId
|
|
13
|
+
status
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
`;
|
|
17
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
18
|
+
function relativeTime(dateStr) {
|
|
19
|
+
const diff = Date.now() - new Date(dateStr).getTime();
|
|
20
|
+
const mins = Math.floor(diff / 60000);
|
|
21
|
+
if (mins < 60)
|
|
22
|
+
return `${mins}m`;
|
|
23
|
+
const hours = Math.floor(mins / 60);
|
|
24
|
+
if (hours < 24)
|
|
25
|
+
return `${hours}h`;
|
|
26
|
+
return `${Math.floor(hours / 24)}d`;
|
|
27
|
+
}
|
|
28
|
+
function Divider({ width }) {
|
|
29
|
+
return _jsx(Text, { dimColor: true, children: '─'.repeat(Math.min(width - 2, 72)) });
|
|
30
|
+
}
|
|
31
|
+
// ─── Shared hook ──────────────────────────────────────────────────────────────
|
|
32
|
+
function useInteractiveState(total, vendor) {
|
|
33
|
+
const [currentIndex, setCurrentIndex] = useState(0);
|
|
34
|
+
const [mode, setMode] = useState('view');
|
|
35
|
+
const [replyInput, setReplyInput] = useState('');
|
|
36
|
+
const [replyDraft, setReplyDraft] = useState('');
|
|
37
|
+
const [statusMessage, setStatusMessage] = useState('');
|
|
38
|
+
const goNext = useCallback(() => {
|
|
39
|
+
setCurrentIndex((i) => Math.min(i + 1, total - 1));
|
|
40
|
+
setMode('view');
|
|
41
|
+
setReplyDraft('');
|
|
42
|
+
setStatusMessage('');
|
|
43
|
+
}, [total]);
|
|
44
|
+
const goPrev = useCallback(() => {
|
|
45
|
+
setCurrentIndex((i) => Math.max(i - 1, 0));
|
|
46
|
+
setMode('view');
|
|
47
|
+
setReplyDraft('');
|
|
48
|
+
setStatusMessage('');
|
|
49
|
+
}, []);
|
|
50
|
+
const startReply = useCallback(() => {
|
|
51
|
+
setReplyInput('');
|
|
52
|
+
setMode('reply-input');
|
|
53
|
+
}, []);
|
|
54
|
+
const dismissDraft = useCallback(() => {
|
|
55
|
+
setReplyDraft('');
|
|
56
|
+
setMode('view');
|
|
57
|
+
}, []);
|
|
58
|
+
const handleReply = useCallback(async (tweetText, angle) => {
|
|
59
|
+
setMode('reply-loading');
|
|
60
|
+
try {
|
|
61
|
+
const result = await generateReply(tweetText, angle, vendor);
|
|
62
|
+
setReplyDraft(result.reply);
|
|
63
|
+
setMode('reply-draft');
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
setStatusMessage(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
67
|
+
setMode('view');
|
|
68
|
+
}
|
|
69
|
+
}, [vendor]);
|
|
70
|
+
return {
|
|
71
|
+
currentIndex,
|
|
72
|
+
mode,
|
|
73
|
+
replyInput,
|
|
74
|
+
replyDraft,
|
|
75
|
+
statusMessage,
|
|
76
|
+
setReplyInput,
|
|
77
|
+
setMode,
|
|
78
|
+
setStatusMessage,
|
|
79
|
+
goNext,
|
|
80
|
+
goPrev,
|
|
81
|
+
startReply,
|
|
82
|
+
dismissDraft,
|
|
83
|
+
handleReply,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
export function InteractiveFeedSession({ items, vendor }) {
|
|
87
|
+
const { stdout } = useStdout();
|
|
88
|
+
const termWidth = stdout.columns ?? 100;
|
|
89
|
+
const cardWidth = getFeedWidth();
|
|
90
|
+
const { currentIndex, mode, replyInput, replyDraft, statusMessage, setReplyInput, setMode, setStatusMessage, goNext, goPrev, startReply, dismissDraft, handleReply, } = useInteractiveState(items.length, vendor);
|
|
91
|
+
const current = items[currentIndex];
|
|
92
|
+
useInput((input, key) => {
|
|
93
|
+
if (mode === 'reply-loading')
|
|
94
|
+
return;
|
|
95
|
+
if (mode === 'reply-input') {
|
|
96
|
+
if (key.return) {
|
|
97
|
+
handleReply(current.tweet.text, replyInput);
|
|
98
|
+
}
|
|
99
|
+
else if (key.escape) {
|
|
100
|
+
setMode('view');
|
|
101
|
+
setReplyInput('');
|
|
102
|
+
}
|
|
103
|
+
else if (key.backspace || key.delete) {
|
|
104
|
+
setReplyInput((s) => s.slice(0, -1));
|
|
105
|
+
}
|
|
106
|
+
else if (input && !key.ctrl && !key.meta) {
|
|
107
|
+
setReplyInput((s) => s + input);
|
|
108
|
+
}
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (mode === 'reply-draft') {
|
|
112
|
+
if (input === 'r') {
|
|
113
|
+
handleReply(current.tweet.text, '');
|
|
114
|
+
}
|
|
115
|
+
else if (key.escape) {
|
|
116
|
+
dismissDraft();
|
|
117
|
+
}
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
// view mode
|
|
121
|
+
if (input === 'n' || key.rightArrow || input === ' ') {
|
|
122
|
+
goNext();
|
|
123
|
+
}
|
|
124
|
+
else if (input === 'p' || key.leftArrow) {
|
|
125
|
+
goPrev();
|
|
126
|
+
}
|
|
127
|
+
else if (input === 'r') {
|
|
128
|
+
startReply();
|
|
129
|
+
}
|
|
130
|
+
else if (input === 's') {
|
|
131
|
+
setStatusMessage('star — coming soon');
|
|
132
|
+
}
|
|
133
|
+
else if (input === 'a') {
|
|
134
|
+
setStatusMessage('analyze — coming soon');
|
|
135
|
+
}
|
|
136
|
+
else if (input === 'q') {
|
|
137
|
+
process.exit(0);
|
|
138
|
+
}
|
|
139
|
+
}, { isActive: mode !== 'reply-loading' });
|
|
140
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { dimColor: true, children: [' ', currentIndex + 1, " / ", items.length, ' · ', "feed --interactive"] }) }), _jsx(TweetCard, { item: current, termWidth: termWidth, cardWidth: cardWidth, isLast: true }), mode === 'reply-draft' && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Divider, { width: termWidth }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Draft reply:" }), _jsx(Box, { marginTop: 1, paddingLeft: 2, width: Math.min(termWidth, 80), children: _jsx(Text, { wrap: "wrap", children: replyDraft }) })] })] })), statusMessage && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "green", children: statusMessage }) })), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Divider, { width: termWidth }), mode === 'reply-input' ? (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "Angle (Enter to auto-generate, Esc to cancel): " }), _jsx(Text, { children: replyInput }), _jsx(Text, { color: "cyan", children: "\u2588" })] })) : mode === 'reply-loading' ? (_jsx(Box, { marginTop: 1, children: _jsx(Spinner, { label: "Generating reply..." }) })) : mode === 'reply-draft' ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "[r] new draft [Esc] dismiss" }) })) : (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "[n]ext [p]rev [s]tar [r]eply [a]nalyze [q]uit" }) }))] })] }));
|
|
141
|
+
}
|
|
142
|
+
// ─── Suggestion Card ──────────────────────────────────────────────────────────
|
|
143
|
+
function scoreColor(score) {
|
|
144
|
+
if (score >= 0.7)
|
|
145
|
+
return 'green';
|
|
146
|
+
if (score >= 0.4)
|
|
147
|
+
return 'yellow';
|
|
148
|
+
return 'white';
|
|
149
|
+
}
|
|
150
|
+
function statusColor(status) {
|
|
151
|
+
switch (status.toLowerCase()) {
|
|
152
|
+
case 'inbox': return 'cyan';
|
|
153
|
+
case 'read': return 'green';
|
|
154
|
+
case 'skipped': return 'gray';
|
|
155
|
+
case 'later': return 'yellow';
|
|
156
|
+
case 'archived': return 'magenta';
|
|
157
|
+
default: return 'white';
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function SuggestionCard({ item, termWidth }) {
|
|
161
|
+
const handle = item.tweet.user.username ?? item.tweet.user.displayName;
|
|
162
|
+
const author = `@${handle}`;
|
|
163
|
+
const profileUrl = `https://x.com/${handle}`;
|
|
164
|
+
const tweetUrl = `https://x.com/${handle}/status/${item.tweet.xid}`;
|
|
165
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "cyan", bold: true, children: relativeTime(item.tweet.createdAt) }), _jsx(Text, { dimColor: true, children: " \u00B7 " }), _jsx(Text, { color: scoreColor(item.score), children: item.score.toFixed(2) }), _jsx(Text, { dimColor: true, children: " \u00B7 " }), _jsx(Text, { color: statusColor(item.status), children: item.status.toLowerCase() }), item.projectsMatched > 0 && (_jsxs(Text, { dimColor: true, children: [" \u00B7 ", item.projectsMatched, " interest", item.projectsMatched !== 1 ? 's' : ''] }))] }), _jsxs(Box, { children: [_jsxs(Text, { color: "gray", children: ['└', " "] }), _jsx(Text, { color: "blueBright", bold: true, children: author })] }), _jsx(Box, { paddingLeft: 2, width: Math.min(termWidth, 82), marginTop: 1, children: _jsx(Text, { wrap: "wrap", children: item.tweet.text }) }), _jsx(Box, { marginLeft: 2, marginTop: 1, children: _jsxs(Text, { dimColor: true, children: [profileUrl, " \u00B7 ", tweetUrl] }) })] }));
|
|
166
|
+
}
|
|
167
|
+
const INBOX_STATUS_KEYS = {
|
|
168
|
+
R: 'READ',
|
|
169
|
+
S: 'SKIPPED',
|
|
170
|
+
L: 'LATER',
|
|
171
|
+
A: 'ARCHIVED',
|
|
172
|
+
};
|
|
173
|
+
export function InteractiveInboxSession({ items, vendor }) {
|
|
174
|
+
const { stdout } = useStdout();
|
|
175
|
+
const termWidth = stdout.columns ?? 100;
|
|
176
|
+
const [isActing, setIsActing] = useState(false);
|
|
177
|
+
const { currentIndex, mode, replyInput, replyDraft, statusMessage, setReplyInput, setMode, setStatusMessage, goNext, goPrev, startReply, dismissDraft, handleReply, } = useInteractiveState(items.length, vendor);
|
|
178
|
+
const current = items[currentIndex];
|
|
179
|
+
const handleStatusUpdate = useCallback(async (status) => {
|
|
180
|
+
setIsActing(true);
|
|
181
|
+
try {
|
|
182
|
+
await gql(UPDATE_SUGGESTION_MUTATION, { suggestionId: current.suggestionId, status });
|
|
183
|
+
setStatusMessage(`✓ marked as ${status.toLowerCase()}`);
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
setStatusMessage(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
setIsActing(false);
|
|
190
|
+
}
|
|
191
|
+
}, [current.suggestionId, setStatusMessage]);
|
|
192
|
+
useInput((input, key) => {
|
|
193
|
+
if (isActing || mode === 'reply-loading')
|
|
194
|
+
return;
|
|
195
|
+
if (mode === 'reply-input') {
|
|
196
|
+
if (key.return) {
|
|
197
|
+
handleReply(current.tweet.text, replyInput);
|
|
198
|
+
}
|
|
199
|
+
else if (key.escape) {
|
|
200
|
+
setMode('view');
|
|
201
|
+
setReplyInput('');
|
|
202
|
+
}
|
|
203
|
+
else if (key.backspace || key.delete) {
|
|
204
|
+
setReplyInput((s) => s.slice(0, -1));
|
|
205
|
+
}
|
|
206
|
+
else if (input && !key.ctrl && !key.meta) {
|
|
207
|
+
setReplyInput((s) => s + input);
|
|
208
|
+
}
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (mode === 'reply-draft') {
|
|
212
|
+
if (input === 'r') {
|
|
213
|
+
handleReply(current.tweet.text, '');
|
|
214
|
+
}
|
|
215
|
+
else if (key.escape) {
|
|
216
|
+
dismissDraft();
|
|
217
|
+
}
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
// view mode
|
|
221
|
+
if (input === 'n' || key.rightArrow || input === ' ') {
|
|
222
|
+
goNext();
|
|
223
|
+
}
|
|
224
|
+
else if (input === 'p' || key.leftArrow) {
|
|
225
|
+
goPrev();
|
|
226
|
+
}
|
|
227
|
+
else if (input === 'r') {
|
|
228
|
+
startReply();
|
|
229
|
+
}
|
|
230
|
+
else if (input === 'a') {
|
|
231
|
+
setStatusMessage('analyze — coming soon');
|
|
232
|
+
}
|
|
233
|
+
else if (input === 'q') {
|
|
234
|
+
process.exit(0);
|
|
235
|
+
}
|
|
236
|
+
else if (INBOX_STATUS_KEYS[input]) {
|
|
237
|
+
handleStatusUpdate(INBOX_STATUS_KEYS[input]);
|
|
238
|
+
}
|
|
239
|
+
}, { isActive: !isActing && mode !== 'reply-loading' });
|
|
240
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { dimColor: true, children: [' ', currentIndex + 1, " / ", items.length, ' · ', "inbox --interactive"] }) }), _jsx(SuggestionCard, { item: current, termWidth: termWidth }), mode === 'reply-draft' && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Divider, { width: termWidth }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Draft reply:" }), _jsx(Box, { marginTop: 1, paddingLeft: 2, width: Math.min(termWidth, 80), children: _jsx(Text, { wrap: "wrap", children: replyDraft }) })] })] })), statusMessage && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "green", children: statusMessage }) })), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Divider, { width: termWidth }), isActing ? (_jsx(Box, { marginTop: 1, children: _jsx(Spinner, { label: "Updating..." }) })) : mode === 'reply-input' ? (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "Angle (Enter to auto-generate, Esc to cancel): " }), _jsx(Text, { children: replyInput }), _jsx(Text, { color: "cyan", children: "\u2588" })] })) : mode === 'reply-loading' ? (_jsx(Box, { marginTop: 1, children: _jsx(Spinner, { label: "Generating reply..." }) })) : mode === 'reply-draft' ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "[r] new draft [Esc] dismiss" }) })) : (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "[n]ext [p]rev [r]eply [a]nalyze [R]ead [S]kip [L]ater [A]rchive [q]uit" }) }))] })] }));
|
|
241
|
+
}
|