neodrop-cli 1.1.0 → 1.2.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 +17 -8
- package/SKILL.md +4 -3
- package/bin/neodrop.mjs +88 -0
- package/lib/chat.mjs +170 -0
- package/package.json +2 -2
- package/references/commands.md +73 -49
package/README.md
CHANGED
|
@@ -39,14 +39,23 @@ No browser is auto-launched, no local port is opened, no callback is needed —
|
|
|
39
39
|
|
|
40
40
|
## Commands
|
|
41
41
|
|
|
42
|
-
| Area | Commands |
|
|
43
|
-
|
|
44
|
-
| Identity | `me` · `whoami` · `tokens list` |
|
|
45
|
-
| Channels | `channels list` · `get` · `search` · `create` · `subscribe` · `unsubscribe` · `categories` · `by-category` |
|
|
46
|
-
| Posts | `posts list` · `get` · `search` · `feed` |
|
|
47
|
-
|
|
|
48
|
-
|
|
49
|
-
|
|
42
|
+
| Area | What you can do | Commands |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| **Identity** | Who am I, and manage my access tokens | `me` · `whoami` · `tokens list` · `tokens revoke <id>` |
|
|
45
|
+
| **Channels** | Browse, search, create and subscribe to channels | `channels list` · `get` · `search` · `create` · `subscribe` · `unsubscribe` · `categories` · `by-category` |
|
|
46
|
+
| **Posts** | Read and search content; view your subscribed feed | `posts list` · `get` · `search` · `feed` |
|
|
47
|
+
| **Chat** | Message a Neodrop AI assistant and get the full reply | `chat "<message>" [--session <id> \| --channel <id>]` · `chat history` · `chat sessions` |
|
|
48
|
+
| **Raw procedure** | Call any tRPC procedure with no sugar command | `api <procedure> [--json '…' \| --stdin] [--mutation]` |
|
|
49
|
+
|
|
50
|
+
Every command takes `--help` for its arguments and `--pretty` for indented JSON. Full reference with per-command tables: [`references/commands.md`](https://github.com/NeoDropAI/neodrop-skills/blob/main/skills/neodrop-cli/references/commands.md).
|
|
51
|
+
|
|
52
|
+
### Chat, in one line
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npx neodrop-cli chat "What's new in my subscriptions this week?"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Sends the message, blocks until the assistant's reply is fully generated (server-side, so it survives disconnects), and prints `{sessionId, reply, newMessages}` as JSON. Reuse the printed `sessionId` with `--session` to continue the conversation, or add `--channel <id>` to talk to a specific channel's assistant.
|
|
50
59
|
|
|
51
60
|
## Use as an AI-agent skill
|
|
52
61
|
|
package/SKILL.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: neodrop-cli
|
|
3
|
-
version: 1.
|
|
3
|
+
version: 1.2.0
|
|
4
4
|
tested_with:
|
|
5
|
-
neodrop_api: "2026-
|
|
5
|
+
neodrop_api: "2026-07"
|
|
6
6
|
node: ">=18"
|
|
7
|
-
description: Operate the Neodrop platform (neodrop.ai) as the current user — create channels, subscribe / unsubscribe, search public channels and content, browse categories, list your own channels,
|
|
7
|
+
description: Operate the Neodrop platform (neodrop.ai) as the current user — create channels, subscribe / unsubscribe, search public channels and content, browse categories, list your own channels, read post details, and chat with Neodrop's AI assistants (global or per-channel, sending a message and receiving the full reply). Trigger this skill whenever the user mentions Neodrop or neodrop.ai, "my channels" / "what am I subscribed to", "post" / "posts" (Neodrop's content units — also called "grain" / "grains", the legacy name), "create a channel" / "subscribe to this channel" / "search channels", "ask Neodrop's assistant" / "chat with my channel", public content / public feed / subscribed feed — or the same intent in any language. Always call `npx neodrop-cli <command>`; do NOT use fetch / curl / hand-rolled HTTP — this skill already handles auth, JSON serialization, error codes and locale defaults.
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# neodrop-cli skill
|
|
@@ -61,6 +61,7 @@ Full flow + security model + reusing credentials across machines: [`references/a
|
|
|
61
61
|
| Current user / token | `me` / `whoami` / `tokens list` | [`references/commands.md#identity`](references/commands.md#identity) |
|
|
62
62
|
| View / search / create / subscribe channels | `channels list/get/search/create/subscribe/unsubscribe`, `channels categories`, `channels by-category` | [`references/commands.md#channels`](references/commands.md#channels) |
|
|
63
63
|
| View / search post content | `posts list/get/search`, `feed` | [`references/commands.md#posts`](references/commands.md#posts) |
|
|
64
|
+
| Chat with the AI assistant (send a message, wait for the full reply) | `chat "<message>" [--session <id> \| --channel <id>]`, `chat history`, `chat sessions` | [`references/commands.md#chat`](references/commands.md#chat) |
|
|
64
65
|
| A procedure with no sugar command | `api <procedure> [--json '…' \| --stdin] [--mutation]` | [`references/commands.md#api`](references/commands.md#api) |
|
|
65
66
|
| User pasted a Neodrop URL and wants details | Map URL → id, call the matching `get` command | [`references/url-routing.md`](references/url-routing.md) |
|
|
66
67
|
| Failure / error | Read the error code on stderr | [`references/troubleshooting.md`](references/troubleshooting.md) |
|
package/bin/neodrop.mjs
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { hostname } from "node:os";
|
|
15
15
|
import { parseArgs } from "node:util";
|
|
16
16
|
import { ApiError, trpcMutation, trpcQuery } from "../lib/api.mjs";
|
|
17
|
+
import { resolveChatSession, sendAndAwaitReply } from "../lib/chat.mjs";
|
|
17
18
|
import {
|
|
18
19
|
clearCredentials,
|
|
19
20
|
credentialsPath,
|
|
@@ -460,6 +461,83 @@ async function cmdFeed(argv) {
|
|
|
460
461
|
emit(await trpcQuery({ apiOrigin, token }, "grain.listSubscribed", payload));
|
|
461
462
|
}
|
|
462
463
|
|
|
464
|
+
// ---- chat 命令 --------------------------------------------------------
|
|
465
|
+
|
|
466
|
+
async function cmdChatSend(argv) {
|
|
467
|
+
const { values, positionals } = parse(argv, {
|
|
468
|
+
session: { type: "string" },
|
|
469
|
+
channel: { type: "string" },
|
|
470
|
+
locale: { type: "string", default: "en" },
|
|
471
|
+
timeout: { type: "string" },
|
|
472
|
+
"poll-interval": { type: "string" },
|
|
473
|
+
});
|
|
474
|
+
const text = requirePositional(
|
|
475
|
+
positionals,
|
|
476
|
+
0,
|
|
477
|
+
'用法:neodrop chat "<message>" [--session <id> | --channel <id>] [--locale en] [--timeout 秒]\n' +
|
|
478
|
+
" neodrop chat history --session <id>",
|
|
479
|
+
);
|
|
480
|
+
if (values.session && values.channel) {
|
|
481
|
+
throw new UsageError("--session 与 --channel 互斥:--session 继续既有会话,--channel 拿该频道的助手会话");
|
|
482
|
+
}
|
|
483
|
+
const timeoutSec = values.timeout === undefined ? 600 : Number(values.timeout);
|
|
484
|
+
if (!Number.isFinite(timeoutSec) || timeoutSec <= 0) {
|
|
485
|
+
throw new UsageError(`--timeout 必须是正数秒,收到「${values.timeout}」`);
|
|
486
|
+
}
|
|
487
|
+
const pollSec = values["poll-interval"] === undefined ? 2 : Number(values["poll-interval"]);
|
|
488
|
+
if (!Number.isFinite(pollSec) || pollSec <= 0) {
|
|
489
|
+
throw new UsageError(`--poll-interval 必须是正数秒,收到「${values["poll-interval"]}」`);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const { apiOrigin, token } = authedCtx();
|
|
493
|
+
let sessionId = values.session;
|
|
494
|
+
if (!sessionId) {
|
|
495
|
+
sessionId = await resolveChatSession({ apiOrigin, token, channelId: values.channel });
|
|
496
|
+
note(`会话 ${sessionId}(继续对话:neodrop chat "…" --session ${sessionId})`);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const result = await sendAndAwaitReply({
|
|
500
|
+
apiOrigin,
|
|
501
|
+
token,
|
|
502
|
+
sessionId,
|
|
503
|
+
text,
|
|
504
|
+
locale: values.locale,
|
|
505
|
+
timeoutMs: timeoutSec * 1000,
|
|
506
|
+
pollIntervalMs: pollSec * 1000,
|
|
507
|
+
});
|
|
508
|
+
if (!result.reply) {
|
|
509
|
+
note("⚠ 本轮没有 assistant 文本回复(可能生成失败或被取消),newMessages 里是本轮全部新增消息。");
|
|
510
|
+
}
|
|
511
|
+
emit(result);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
async function cmdChatHistory(argv) {
|
|
515
|
+
const { values } = parse(argv, {
|
|
516
|
+
session: { type: "string" },
|
|
517
|
+
});
|
|
518
|
+
if (!values.session) {
|
|
519
|
+
throw new UsageError("用法:neodrop chat history --session <id>");
|
|
520
|
+
}
|
|
521
|
+
const { apiOrigin, token } = authedCtx();
|
|
522
|
+
emit(
|
|
523
|
+
await trpcQuery({ apiOrigin, token }, "session.getMessages", {
|
|
524
|
+
sessionId: values.session,
|
|
525
|
+
}),
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async function cmdChatSessions() {
|
|
530
|
+
const { apiOrigin, token } = authedCtx();
|
|
531
|
+
emit(await trpcQuery({ apiOrigin, token }, "session.list"));
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
async function cmdChat(argv) {
|
|
535
|
+
const sub = argv[0];
|
|
536
|
+
if (sub === "history") return cmdChatHistory(argv.slice(1));
|
|
537
|
+
if (sub === "sessions") return cmdChatSessions();
|
|
538
|
+
return cmdChatSend(argv);
|
|
539
|
+
}
|
|
540
|
+
|
|
463
541
|
// ---- 兜底通道 ---------------------------------------------------------
|
|
464
542
|
|
|
465
543
|
async function cmdApi(argv) {
|
|
@@ -528,6 +606,14 @@ const HELP = `neodrop — Neodrop CLI(AI agent 与人类共用,stdout = JSON
|
|
|
528
606
|
posts search "<query>" [--limit N] [--locale L] [--strict]
|
|
529
607
|
feed [--limit N] [--cursor C] = posts list --subscribed
|
|
530
608
|
|
|
609
|
+
对话(chat):
|
|
610
|
+
chat "<message>" [--session <id> | --channel <id>] [--locale L] [--timeout 秒]
|
|
611
|
+
发消息给 AI 助手并等完整回复(默认新建全局
|
|
612
|
+
助手会话;--session 继续对话;--channel 和
|
|
613
|
+
该频道的助手聊)
|
|
614
|
+
chat history --session <id> 查看会话全部消息
|
|
615
|
+
chat sessions 列出我的会话
|
|
616
|
+
|
|
531
617
|
兜底:
|
|
532
618
|
api <procedure> [--json '...' | --stdin] [--mutation]
|
|
533
619
|
|
|
@@ -602,6 +688,8 @@ async function dispatch(rawArgs) {
|
|
|
602
688
|
return dispatchGroup("posts", POSTS_SUB, rest);
|
|
603
689
|
case "feed":
|
|
604
690
|
return cmdFeed(rest);
|
|
691
|
+
case "chat":
|
|
692
|
+
return cmdChat(rest);
|
|
605
693
|
case "api":
|
|
606
694
|
return cmdApi(rest);
|
|
607
695
|
case "install-skill":
|
package/lib/chat.mjs
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// chat 能力:发一条消息给 Neodrop 的对话 agent,等回复生成完,拿完整回复。
|
|
2
|
+
//
|
|
3
|
+
// 链路(全部走用户 PAT,Bearer 鉴权):
|
|
4
|
+
// 1. 建 / 复用会话:session.create(全局助手)或
|
|
5
|
+
// session.getOrCreateChannelAssistant(--channel 频道助手)——tRPC mutation
|
|
6
|
+
// 2. 发送:POST /api/chat(非 tRPC 的 Hono 端点,后端 2026-07 起支持 PAT Bearer)。
|
|
7
|
+
// 响应是 SSE 流;回复由后端 BullMQ worker 生成、与本连接生命周期解耦,
|
|
8
|
+
// CLI 不解析流内容,只把「流读到 EOF」当作大概率完成的信号。
|
|
9
|
+
// 3. 收敛:轮询 session.getActiveChatTurn 直到 null(turn 已 terminal——
|
|
10
|
+
// SSE 中途断线也靠这层兜住),再 session.getMessages 取本次 user 消息
|
|
11
|
+
// 之后新增的消息作为回复。最终一致以 getMessages 为准,不信任流。
|
|
12
|
+
//
|
|
13
|
+
// 为什么不解析 SSE:UIMessage chunk 协议类型多且随主仓演进,CLI 的消费者是
|
|
14
|
+
// AI agent,要的是「一次拿到完整回复的 JSON」,不是逐 token 渲染。
|
|
15
|
+
|
|
16
|
+
import { randomUUID } from "node:crypto";
|
|
17
|
+
import { trpcMutation, trpcQuery } from "./api.mjs";
|
|
18
|
+
import { note } from "./output.mjs";
|
|
19
|
+
|
|
20
|
+
const USER_AGENT = "neodrop-cli/1.0 (+https://github.com/NeoDropAI/neodrop-skills)";
|
|
21
|
+
|
|
22
|
+
function sleep(ms) {
|
|
23
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// POST /api/chat 发送消息。成功返回 Response(SSE 已开始);HTTP >= 400 时
|
|
27
|
+
// 解析 JSON error 抛出(402 余额不足 / 401 未登录 / 400 消息非法等)。
|
|
28
|
+
async function postChatMessage({ apiOrigin, token, sessionId, text, locale, signal }) {
|
|
29
|
+
const url = `${apiOrigin.replace(/\/+$/, "")}/api/chat`;
|
|
30
|
+
const body = {
|
|
31
|
+
sessionId,
|
|
32
|
+
locale,
|
|
33
|
+
message: {
|
|
34
|
+
id: randomUUID(),
|
|
35
|
+
role: "user",
|
|
36
|
+
parts: [{ type: "text", text }],
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
const res = await fetch(url, {
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: {
|
|
42
|
+
"content-type": "application/json",
|
|
43
|
+
"user-agent": USER_AGENT,
|
|
44
|
+
authorization: `Bearer ${token}`,
|
|
45
|
+
},
|
|
46
|
+
body: JSON.stringify(body),
|
|
47
|
+
signal,
|
|
48
|
+
});
|
|
49
|
+
if (res.status >= 400) {
|
|
50
|
+
let payload = null;
|
|
51
|
+
try {
|
|
52
|
+
payload = await res.json();
|
|
53
|
+
} catch {
|
|
54
|
+
// 非 JSON 错误体,用 HTTP 状态兜底
|
|
55
|
+
}
|
|
56
|
+
const msg = (payload && (payload.error || payload.message)) || `HTTP ${res.status}`;
|
|
57
|
+
const code = (payload && payload.code) || "";
|
|
58
|
+
throw new Error(code ? `[${code}] ${msg}` : `发送失败:${msg}`);
|
|
59
|
+
}
|
|
60
|
+
return { response: res, userMessageId: body.message.id };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 读 SSE 流到 EOF(内容丢弃)。断线 / 超时都不算失败——后端 worker 与连接
|
|
64
|
+
// 解耦,最终状态交给 getActiveChatTurn 轮询收敛。
|
|
65
|
+
async function drainStream(response) {
|
|
66
|
+
try {
|
|
67
|
+
if (!response.body) return;
|
|
68
|
+
for await (const _chunk of response.body) {
|
|
69
|
+
// 只等 EOF,不解析
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
note("⚠ SSE 连接中断,转轮询等待回复…");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 轮询直到 session 没有 live 的 chat turn(回复已生成完 / 失败 / 取消)。
|
|
77
|
+
async function pollUntilTurnSettled({ apiOrigin, token, sessionId, deadline, intervalMs }) {
|
|
78
|
+
for (;;) {
|
|
79
|
+
const turn = await trpcQuery({ apiOrigin, token }, "session.getActiveChatTurn", {
|
|
80
|
+
sessionId,
|
|
81
|
+
});
|
|
82
|
+
if (!turn) return;
|
|
83
|
+
if (Date.now() > deadline) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`等待回复超时(turn=${turn.id} 仍在 ${turn.status})。稍后可用 chat history --session ${sessionId} 查看结果。`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
await sleep(intervalMs);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function extractText(parts) {
|
|
93
|
+
if (!Array.isArray(parts)) return "";
|
|
94
|
+
return parts
|
|
95
|
+
.filter((p) => p && p.type === "text" && typeof p.text === "string")
|
|
96
|
+
.map((p) => p.text)
|
|
97
|
+
.join("");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 发送一条消息并等待完整回复。
|
|
102
|
+
*
|
|
103
|
+
* @returns {Promise<{sessionId: string, reply: {text: string, parts: unknown[]} | null, newMessages: unknown[]}>}
|
|
104
|
+
* reply 是本次新增消息里最后一条 assistant 消息(text = 其 text parts 拼接);
|
|
105
|
+
* turn 失败等场景下可能没有 assistant 回复,reply 为 null,调用方看 newMessages 自行判断。
|
|
106
|
+
*/
|
|
107
|
+
export async function sendAndAwaitReply({
|
|
108
|
+
apiOrigin,
|
|
109
|
+
token,
|
|
110
|
+
sessionId,
|
|
111
|
+
text,
|
|
112
|
+
locale,
|
|
113
|
+
timeoutMs,
|
|
114
|
+
pollIntervalMs,
|
|
115
|
+
}) {
|
|
116
|
+
const deadline = Date.now() + timeoutMs;
|
|
117
|
+
const abort = AbortSignal.timeout(timeoutMs);
|
|
118
|
+
|
|
119
|
+
note(`→ 发送到 session ${sessionId} …`);
|
|
120
|
+
const { response, userMessageId } = await postChatMessage({
|
|
121
|
+
apiOrigin,
|
|
122
|
+
token,
|
|
123
|
+
sessionId,
|
|
124
|
+
text,
|
|
125
|
+
locale,
|
|
126
|
+
signal: abort,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
note("… 回复生成中");
|
|
130
|
+
await drainStream(response);
|
|
131
|
+
await pollUntilTurnSettled({
|
|
132
|
+
apiOrigin,
|
|
133
|
+
token,
|
|
134
|
+
sessionId,
|
|
135
|
+
deadline,
|
|
136
|
+
intervalMs: pollIntervalMs,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const messages = await trpcQuery({ apiOrigin, token }, "session.getMessages", {
|
|
140
|
+
sessionId,
|
|
141
|
+
});
|
|
142
|
+
// 本次 user 消息之后新增的所有消息 = 这轮回复(含 tool / data 卡片等中间产物)
|
|
143
|
+
const anchor = messages.findIndex((m) => m.id === userMessageId);
|
|
144
|
+
const newMessages = anchor >= 0 ? messages.slice(anchor + 1) : [];
|
|
145
|
+
const lastAssistant = [...newMessages]
|
|
146
|
+
.reverse()
|
|
147
|
+
.find((m) => m.role === "assistant");
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
sessionId,
|
|
151
|
+
reply: lastAssistant
|
|
152
|
+
? { text: extractText(lastAssistant.parts), parts: lastAssistant.parts }
|
|
153
|
+
: null,
|
|
154
|
+
newMessages,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** 建新会话(全局助手),或按 --channel 拿该频道的助手会话。 */
|
|
159
|
+
export async function resolveChatSession({ apiOrigin, token, channelId }) {
|
|
160
|
+
if (channelId) {
|
|
161
|
+
const session = await trpcMutation(
|
|
162
|
+
{ apiOrigin, token },
|
|
163
|
+
"session.getOrCreateChannelAssistant",
|
|
164
|
+
{ channelId },
|
|
165
|
+
);
|
|
166
|
+
return session.id;
|
|
167
|
+
}
|
|
168
|
+
const session = await trpcMutation({ apiOrigin, token }, "session.create", {});
|
|
169
|
+
return session.id;
|
|
170
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neodrop-cli",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Neodrop (neodrop.ai) CLI — let your AI agent (Claude Code / Cursor / Codex) browse channels, read posts, manage subscriptions
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "Neodrop (neodrop.ai) CLI — let your AI agent (Claude Code / Cursor / Codex) browse channels, read posts, manage subscriptions, create channels and chat with Neodrop's AI assistants as you. stdout is always valid JSON. npx neodrop-cli login and go.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"neodrop": "bin/neodrop.mjs"
|
package/references/commands.md
CHANGED
|
@@ -1,74 +1,98 @@
|
|
|
1
1
|
# neodrop-cli command reference
|
|
2
2
|
|
|
3
|
-
> Invocation convention:
|
|
4
|
-
> Equivalent explicit
|
|
5
|
-
>
|
|
3
|
+
> Invocation convention: `npx neodrop-cli <command>` (see [SKILL.md → How to invoke](../SKILL.md#how-to-invoke)).
|
|
4
|
+
> Equivalent explicit form: `npx -p neodrop-cli neodrop <command>`. Examples below are written as `neodrop <command>`.
|
|
5
|
+
>
|
|
6
|
+
> Every command prints **valid JSON to stdout**; logs, progress and `🔗` links go to stderr. Add `--pretty` for indented JSON, `--help` on any command for its arguments.
|
|
6
7
|
|
|
7
8
|
## identity
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
neodrop
|
|
12
|
-
neodrop
|
|
13
|
-
neodrop tokens
|
|
14
|
-
|
|
10
|
+
| Command | What it does |
|
|
11
|
+
|---|---|
|
|
12
|
+
| `neodrop me` | Current user info (`user.getMe`). |
|
|
13
|
+
| `neodrop whoami` | `me` plus token metadata — which credential is in use and when it expires. |
|
|
14
|
+
| `neodrop tokens list` | Every Personal Access Token you've issued. |
|
|
15
|
+
| `neodrop tokens revoke <id>` | Revoke a PAT. If it's this machine's token, the local credential is cleared too. |
|
|
15
16
|
|
|
16
17
|
## channels
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
**View & search**
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
neodrop channels list --
|
|
23
|
-
neodrop channels
|
|
24
|
-
neodrop channels
|
|
25
|
-
neodrop channels
|
|
21
|
+
| Command | What it does |
|
|
22
|
+
|---|---|
|
|
23
|
+
| `neodrop channels list --mine` | Channels you own. |
|
|
24
|
+
| `neodrop channels list --locale en --limit 20` | Public channels, paginated. |
|
|
25
|
+
| `neodrop channels get <channelId>` | Single channel detail, including `requirement.public`. |
|
|
26
|
+
| `neodrop channels categories` | All channel categories. |
|
|
27
|
+
| `neodrop channels by-category <category> --sort latest --limit 20` | Channels in a category. |
|
|
28
|
+
| `neodrop channels search "<query>" --locale en --limit 10` | Full-text search over the public pool. |
|
|
26
29
|
|
|
27
|
-
|
|
28
|
-
```
|
|
30
|
+
**Create & subscribe**
|
|
29
31
|
|
|
30
|
-
|
|
32
|
+
| Command | What it does |
|
|
33
|
+
|---|---|
|
|
34
|
+
| `neodrop channels create --name "<name>" --description "<desc>" --locale en` | Create a channel from flags. |
|
|
35
|
+
| `neodrop channels create --json '{"name":"X","locale":"en","type":"PRIVATE"}'` | Create a channel from a full JSON payload. |
|
|
36
|
+
| `neodrop channels subscribe <channelId>` | Subscribe to a channel. |
|
|
37
|
+
| `neodrop channels unsubscribe <channelId>` | Unsubscribe from a channel. |
|
|
31
38
|
|
|
32
|
-
|
|
33
|
-
neodrop channels create --name "AI industry tracker" --description "..." --locale en
|
|
34
|
-
neodrop channels create --json '{"name":"X","locale":"en","type":"PRIVATE"}'
|
|
35
|
-
neodrop channels subscribe <channelId>
|
|
36
|
-
neodrop channels unsubscribe <channelId>
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
**Default locale**: `channels list` defaults to `en`, matching the product's default public pool. `channels search` and `channels by-category` omit locale when `--locale` is not provided and let the backend decide. Pass `--locale en` (etc.) explicitly to query the public pool across locales.
|
|
39
|
+
> **Locale defaults**: `channels list` defaults to `en` (the product's default public pool). `channels search` / `by-category` omit locale unless you pass `--locale`, letting the backend decide. Pass `--locale en` (etc.) explicitly to query across locales.
|
|
40
40
|
|
|
41
41
|
## posts
|
|
42
42
|
|
|
43
|
-
> The content unit is
|
|
43
|
+
> The content unit is a **post**. The old name `grains` stays as a backward-compatible alias; new code should use `posts`.
|
|
44
|
+
|
|
45
|
+
| Command | What it does |
|
|
46
|
+
|---|---|
|
|
47
|
+
| `neodrop posts list --limit 10` | Public feed. |
|
|
48
|
+
| `neodrop posts list --subscribed --limit 10` | Posts from your subscriptions (same as `neodrop feed --limit 10`). |
|
|
49
|
+
| `neodrop posts list --channel <channelId> --limit 10` | Posts from one channel. |
|
|
50
|
+
| `neodrop posts get <postId>` | Single post detail. |
|
|
51
|
+
| `neodrop posts search "<query>" --limit 10` | Full-text search over posts. |
|
|
52
|
+
| `neodrop feed --limit 10` | Alias for `posts list --subscribed`. |
|
|
53
|
+
|
|
54
|
+
## chat
|
|
55
|
+
|
|
56
|
+
Talk to Neodrop's AI assistants as the current user. The command **blocks until the reply is fully generated** (replies come from a backend worker and survive disconnects), then prints the complete result as JSON.
|
|
57
|
+
|
|
58
|
+
| Command | What it does |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `neodrop chat "<message>"` | Send a message to the **global assistant** in a new session. |
|
|
61
|
+
| `neodrop chat "<message>" --session <sessionId>` | Continue an existing session. |
|
|
62
|
+
| `neodrop chat "<message>" --channel <channelId>` | Talk to a **channel assistant** (owner: can edit the channel's config; reader: Q&A about the channel). |
|
|
63
|
+
| `neodrop chat history --session <sessionId>` | Full message list of a session. |
|
|
64
|
+
| `neodrop chat sessions` | Your sessions — find one to continue. |
|
|
65
|
+
|
|
66
|
+
**Flags**
|
|
67
|
+
|
|
68
|
+
| Flag | Default | Purpose |
|
|
69
|
+
|---|---|---|
|
|
70
|
+
| `--session <id>` | — | Continue a specific session instead of starting a new one. |
|
|
71
|
+
| `--channel <id>` | — | Target a channel assistant instead of the global assistant. |
|
|
72
|
+
| `--locale <code>` | `en` | Language of the assistant's error messages. |
|
|
73
|
+
| `--timeout <seconds>` | `600` | Max total wait. On timeout the reply keeps generating server-side — fetch it later with `chat history`. |
|
|
44
74
|
|
|
45
|
-
|
|
46
|
-
neodrop posts list --limit 10 # public feed
|
|
47
|
-
neodrop posts list --subscribed --limit 10 # your subscriptions (= neodrop feed --limit 10)
|
|
48
|
-
neodrop posts list --channel <channelId> --limit 10
|
|
49
|
-
neodrop posts get <postId>
|
|
50
|
-
neodrop posts search "Apple Intelligence" --limit 10
|
|
75
|
+
**Output shape**: `{sessionId, reply: {text, parts} | null, newMessages: [...]}` — `reply.text` is the assistant's final text; `newMessages` is every message produced this turn (tool calls, cards, …). `reply` can be `null` if generation failed; inspect `newMessages` then.
|
|
51
76
|
|
|
52
|
-
|
|
53
|
-
|
|
77
|
+
- A new conversation prints its `sessionId` on stderr — **reuse it with `--session` for follow-ups**, don't start a fresh session per question.
|
|
78
|
+
- Chat consumes the account's AI credits like the web app does; a `402 insufficient_credits` error means the user must top up.
|
|
54
79
|
|
|
55
80
|
## api
|
|
56
81
|
|
|
57
82
|
For tRPC procedures with no sugar command, fall back to `api`:
|
|
58
83
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
neodrop api
|
|
62
|
-
|
|
63
|
-
|
|
84
|
+
| Command | What it does |
|
|
85
|
+
|---|---|
|
|
86
|
+
| `neodrop api <procedure>` | Call any tRPC **query** (GET). |
|
|
87
|
+
| `neodrop api <procedure> --json '{...}' --mutation` | Call a **write** — `--mutation` is required or the backend rejects it as a query. |
|
|
88
|
+
| `echo '{...}' \| neodrop api <procedure> --stdin --mutation` | Read the JSON input from stdin (great for heredocs / pipelines). |
|
|
64
89
|
|
|
65
|
-
- **Defaults to a GET query** —
|
|
66
|
-
-
|
|
67
|
-
- To find a procedure's full name, check the main repo's `packages/backend/src/api/trpc/routers.ts`, or probe with `curl /api/trpc/<router>.<procedure>?input=...` against a dev backend.
|
|
90
|
+
- **Defaults to a GET query** — every write MUST add `--mutation` explicitly.
|
|
91
|
+
- To find a procedure's full name, check the main repo's `packages/backend/src/api/trpc/routers.ts`, or probe `curl /api/trpc/<router>.<procedure>?input=...` against a dev backend.
|
|
68
92
|
|
|
69
|
-
## Global
|
|
93
|
+
## Global flags
|
|
70
94
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
95
|
+
| Flag | Purpose |
|
|
96
|
+
|---|---|
|
|
97
|
+
| `--pretty` | Indented JSON for humans (still valid JSON). |
|
|
98
|
+
| `--help` | Show a subcommand's arguments. |
|