neodrop-cli 1.1.0 → 2.0.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 +6 -4
- package/bin/neodrop.mjs +257 -110
- package/lib/api.mjs +34 -27
- package/lib/chat.mjs +192 -0
- package/lib/credentials.mjs +17 -13
- package/lib/install-skill.mjs +12 -9
- package/lib/origins.mjs +12 -8
- package/lib/output.mjs +3 -3
- package/lib/web-urls.mjs +14 -8
- package/package.json +2 -2
- package/references/commands.md +99 -48
- package/references/troubleshooting.md +3 -1
package/bin/neodrop.mjs
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// neodrop CLI
|
|
3
|
-
//
|
|
2
|
+
// neodrop CLI entry point. Shared by AI agents and humans — talks to the Neodrop
|
|
3
|
+
// tRPC HTTP API directly (Bearer PAT auth), not over MCP.
|
|
4
4
|
//
|
|
5
|
-
//
|
|
5
|
+
// Invocation:
|
|
6
6
|
// npx neodrop-cli <command> [args...]
|
|
7
|
-
//
|
|
7
|
+
// or, when installed globally: neodrop <command> [args...]
|
|
8
8
|
//
|
|
9
|
-
//
|
|
10
|
-
// stdout = JSON
|
|
11
|
-
// stderr =
|
|
12
|
-
//
|
|
9
|
+
// Output:
|
|
10
|
+
// stdout = JSON (parse it directly); --pretty switches to indented JSON for humans
|
|
11
|
+
// stderr = logs / progress / error descriptions
|
|
12
|
+
// Exit codes: 0 success / 1 business error / 2 usage error
|
|
13
13
|
|
|
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,
|
|
@@ -29,14 +30,14 @@ import { channelUrl, postUrl, userUrl } from "../lib/web-urls.mjs";
|
|
|
29
30
|
const DEFAULT_SERVER = process.env.NEODROP_SERVER || "https://neodrop.ai";
|
|
30
31
|
const ENV_API_OVERRIDE = process.env.NEODROP_API;
|
|
31
32
|
|
|
32
|
-
//
|
|
33
|
+
// Usage error → exit code 2 (distinct from business error 1).
|
|
33
34
|
class UsageError extends Error {}
|
|
34
35
|
|
|
35
36
|
function sleep(ms) {
|
|
36
37
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
//
|
|
40
|
+
// Client identifier — shown on the consent page and settings/cli-tokens so the user can recognize it.
|
|
40
41
|
function detectClientName() {
|
|
41
42
|
const host = hostname() || "host";
|
|
42
43
|
const term = process.env.TERM_PROGRAM || "";
|
|
@@ -58,13 +59,13 @@ async function readStdin() {
|
|
|
58
59
|
return Buffer.concat(chunks).toString("utf-8");
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
// `--json '<input>'` / `--stdin`
|
|
62
|
+
// One of `--json '<input>'` / `--stdin` (mutually exclusive, checked by the caller); returns undefined if neither is given.
|
|
62
63
|
async function loadInput(values) {
|
|
63
64
|
if (values.json) {
|
|
64
65
|
try {
|
|
65
66
|
return JSON.parse(values.json);
|
|
66
67
|
} catch (err) {
|
|
67
|
-
throw new UsageError(`--json
|
|
68
|
+
throw new UsageError(`--json failed to parse: ${err.message}`);
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
71
|
if (values.stdin) {
|
|
@@ -73,7 +74,7 @@ async function loadInput(values) {
|
|
|
73
74
|
return undefined;
|
|
74
75
|
}
|
|
75
76
|
|
|
76
|
-
// parseArgs
|
|
77
|
+
// parseArgs wrapper: unknown flags / missing values become a UsageError (exit code 2).
|
|
77
78
|
function parse(argv, options) {
|
|
78
79
|
try {
|
|
79
80
|
return parseArgs({ args: argv, options, allowPositionals: true, strict: true });
|
|
@@ -85,7 +86,7 @@ function parse(argv, options) {
|
|
|
85
86
|
function toLimit(value) {
|
|
86
87
|
if (value === undefined) return undefined;
|
|
87
88
|
const n = Number(value);
|
|
88
|
-
if (!Number.isFinite(n)) throw new UsageError(`--limit
|
|
89
|
+
if (!Number.isFinite(n)) throw new UsageError(`--limit must be a number, got "${value}"`);
|
|
89
90
|
return n;
|
|
90
91
|
}
|
|
91
92
|
|
|
@@ -95,14 +96,14 @@ function requirePositional(positionals, index, hint) {
|
|
|
95
96
|
return value;
|
|
96
97
|
}
|
|
97
98
|
|
|
98
|
-
// ----
|
|
99
|
+
// ---- meta commands ----------------------------------------------------
|
|
99
100
|
|
|
100
|
-
//
|
|
101
|
-
// 1. startSession
|
|
102
|
-
//
|
|
103
|
-
// 2.
|
|
104
|
-
// 3.
|
|
105
|
-
// 4.
|
|
101
|
+
// Unified login: session-polling flow.
|
|
102
|
+
// 1. startSession returns sessionId + pollSecret + verification URL
|
|
103
|
+
// (pollSecret is held only in this process — the sole credential for polling the token, never in the URL)
|
|
104
|
+
// 2. print the URL for the user to open in a browser (nothing is auto-launched, no local server, no callback)
|
|
105
|
+
// 3. poll pollSession with the pollSecret until APPROVED / DENIED / EXPIRED
|
|
106
|
+
// 4. on success, write the token to ~/.neodrop/credentials.json
|
|
106
107
|
async function cmdLogin(argv) {
|
|
107
108
|
const { values } = parse(argv, {
|
|
108
109
|
server: { type: "string" },
|
|
@@ -111,39 +112,41 @@ async function cmdLogin(argv) {
|
|
|
111
112
|
});
|
|
112
113
|
|
|
113
114
|
const webOrigin = (values.server || DEFAULT_SERVER).replace(/\/+$/, "");
|
|
114
|
-
// --api
|
|
115
|
+
// explicit --api > NEODROP_API env > heuristic inference
|
|
115
116
|
const apiOrigin = values.api || ENV_API_OVERRIDE || inferApiOrigin(webOrigin);
|
|
116
117
|
const clientName = values.name || detectClientName();
|
|
117
118
|
|
|
118
119
|
note(`web = ${webOrigin}`);
|
|
119
120
|
note(`api = ${apiOrigin}`);
|
|
120
121
|
|
|
121
|
-
// 1.
|
|
122
|
+
// 1. start a session
|
|
122
123
|
const session = await trpcMutation({ apiOrigin, token: null }, "cliToken.startSession", {
|
|
123
124
|
clientName,
|
|
124
125
|
webOrigin,
|
|
125
126
|
});
|
|
126
127
|
const sessionId = session.sessionId;
|
|
127
|
-
// pollSecret
|
|
128
|
-
//
|
|
128
|
+
// pollSecret is a private claim credential the backend hands only to this CLI process; it is never in the
|
|
129
|
+
// verification URL. You must send it back when polling to claim the token. The URL carries only the sessionId,
|
|
130
|
+
// so a leaked screenshot / forward cannot claim the token.
|
|
129
131
|
const pollSecret = session.pollSecret;
|
|
130
132
|
const verificationUrl = session.verificationUrl;
|
|
131
133
|
const pollInterval = Math.max(1, Number(session.pollIntervalSeconds || 2));
|
|
132
134
|
|
|
133
135
|
note("");
|
|
134
|
-
note("👉
|
|
136
|
+
note("👉 Open the URL below in any browser (phone / laptop / this machine) to authorize:");
|
|
135
137
|
note("");
|
|
136
|
-
// URL
|
|
137
|
-
//
|
|
138
|
+
// The URL is printed flush-left on its own line (no indent): it contains a 256-bit ?session= string, always
|
|
139
|
+
// exceeds 80 columns and the terminal wraps it. Flush-left on its own line makes it easiest to triple-click /
|
|
140
|
+
// drag-select the whole line and copy the full URL in one go.
|
|
138
141
|
note(verificationUrl);
|
|
139
142
|
note("");
|
|
140
|
-
note("
|
|
141
|
-
note(`
|
|
142
|
-
note("
|
|
143
|
+
note(" (Long URLs wrap — when copying, select all the way through the trailing ?session=...)");
|
|
144
|
+
note(` Client name "${clientName}" (confirm it on the consent page — it's this CLI launch)`);
|
|
145
|
+
note(" The link is valid for 10 minutes. After approving, return to this terminal — the CLI detects it automatically.");
|
|
143
146
|
note("");
|
|
144
147
|
|
|
145
|
-
// 2.
|
|
146
|
-
const deadline = Date.now() + 10 * 60 * 1000; //
|
|
148
|
+
// 2. poll
|
|
149
|
+
const deadline = Date.now() + 10 * 60 * 1000; // aligned with the backend session lifetime
|
|
147
150
|
let waitedDots = 0;
|
|
148
151
|
let token = null;
|
|
149
152
|
let tokenId = null;
|
|
@@ -157,40 +160,40 @@ async function cmdLogin(argv) {
|
|
|
157
160
|
pollSecret,
|
|
158
161
|
});
|
|
159
162
|
} catch (err) {
|
|
160
|
-
// NOT_FOUND
|
|
161
|
-
throw new Error(
|
|
163
|
+
// NOT_FOUND usually means the backend already cleaned up the session; rethrow any other error too
|
|
164
|
+
throw new Error(`Failed to poll for authorization: ${err.message}`);
|
|
162
165
|
}
|
|
163
166
|
|
|
164
167
|
const status = res.status;
|
|
165
168
|
if (status === "APPROVED") {
|
|
166
169
|
if (res.alreadyClaimed) {
|
|
167
170
|
throw new Error(
|
|
168
|
-
"
|
|
171
|
+
"The authorization token was already claimed (rare — normally this CLI claims it itself). Please run `npx neodrop-cli login` again.",
|
|
169
172
|
);
|
|
170
173
|
}
|
|
171
174
|
token = res.token;
|
|
172
175
|
tokenId = res.tokenId;
|
|
173
176
|
const ea = res.tokenExpiresAt;
|
|
174
177
|
expiresAt = typeof ea === "string" ? ea : ea ? String(ea) : null;
|
|
175
|
-
if (!token) throw new Error("
|
|
178
|
+
if (!token) throw new Error("Authorization returned APPROVED but no token; please run `npx neodrop-cli login` again");
|
|
176
179
|
break;
|
|
177
180
|
}
|
|
178
181
|
if (status === "DENIED") {
|
|
179
|
-
throw new Error("
|
|
182
|
+
throw new Error("Authorization was denied by the user. If needed, run `npx neodrop-cli login` again and check the client name.");
|
|
180
183
|
}
|
|
181
184
|
if (status === "EXPIRED") {
|
|
182
|
-
throw new Error("
|
|
185
|
+
throw new Error("The authorization link expired (not approved within 10 minutes). Please run `npx neodrop-cli login` again.");
|
|
183
186
|
}
|
|
184
|
-
//
|
|
187
|
+
// still PENDING — print a progress dot (one dot every 5 polls, to avoid spamming)
|
|
185
188
|
waitedDots += 1;
|
|
186
189
|
if (waitedDots % 5 === 0) note(".", "");
|
|
187
190
|
}
|
|
188
191
|
|
|
189
|
-
if (!token) throw new Error("
|
|
192
|
+
if (!token) throw new Error("Timed out waiting for authorization. Please run `npx neodrop-cli login` again.");
|
|
190
193
|
|
|
191
194
|
note("");
|
|
192
195
|
|
|
193
|
-
// 3.
|
|
196
|
+
// 3. write the credential
|
|
194
197
|
writeCredentials({
|
|
195
198
|
webOrigin,
|
|
196
199
|
apiOrigin,
|
|
@@ -201,9 +204,9 @@ async function cmdLogin(argv) {
|
|
|
201
204
|
createdAt: new Date().toISOString(),
|
|
202
205
|
});
|
|
203
206
|
|
|
204
|
-
// 4.
|
|
207
|
+
// 4. verify + output
|
|
205
208
|
const me = await trpcQuery({ apiOrigin, token }, "user.getMe");
|
|
206
|
-
note(`✅
|
|
209
|
+
note(`✅ Logged in: ${me.email || me.id || "<unknown>"}`);
|
|
207
210
|
note(` credentials = ${credentialsPath()}`);
|
|
208
211
|
emit({
|
|
209
212
|
ok: true,
|
|
@@ -219,7 +222,7 @@ async function cmdLogin(argv) {
|
|
|
219
222
|
async function cmdLogout() {
|
|
220
223
|
const creds = readCredentials();
|
|
221
224
|
if (creds === null) {
|
|
222
|
-
note("
|
|
225
|
+
note("Not logged in — nothing to log out of.");
|
|
223
226
|
emit({ ok: true, alreadyLoggedOut: true });
|
|
224
227
|
return;
|
|
225
228
|
}
|
|
@@ -229,9 +232,9 @@ async function cmdLogout() {
|
|
|
229
232
|
id: creds.tokenId,
|
|
230
233
|
});
|
|
231
234
|
revoked = true;
|
|
232
|
-
note(`✅
|
|
235
|
+
note(`✅ Revoked ${creds.tokenId}`);
|
|
233
236
|
} catch (err) {
|
|
234
|
-
note(`⚠
|
|
237
|
+
note(`⚠ Revocation failed (clearing local credential anyway): ${err.message}`);
|
|
235
238
|
}
|
|
236
239
|
clearCredentials();
|
|
237
240
|
emit({ ok: true, revoked });
|
|
@@ -266,24 +269,24 @@ async function cmdTokensList() {
|
|
|
266
269
|
|
|
267
270
|
async function cmdTokensRevoke(argv) {
|
|
268
271
|
const { positionals } = parse(argv, {});
|
|
269
|
-
const id = requirePositional(positionals, 0, "
|
|
272
|
+
const id = requirePositional(positionals, 0, "Usage: neodrop tokens revoke <id>");
|
|
270
273
|
const { apiOrigin, token, creds } = authedCtx();
|
|
271
274
|
const r = await trpcMutation({ apiOrigin, token }, "cliToken.revoke", { id });
|
|
272
275
|
if (id === creds.tokenId) {
|
|
273
276
|
clearCredentials();
|
|
274
|
-
note("
|
|
277
|
+
note("(That was this machine's current token — the local credential has been cleared. You'll need to run neodrop login again.)");
|
|
275
278
|
}
|
|
276
279
|
emit(r);
|
|
277
280
|
}
|
|
278
281
|
|
|
279
|
-
// ----
|
|
282
|
+
// ---- channel commands -------------------------------------------------
|
|
280
283
|
|
|
281
284
|
async function cmdChannelsList(argv) {
|
|
282
285
|
const { values } = parse(argv, {
|
|
283
286
|
mine: { type: "boolean" },
|
|
284
287
|
limit: { type: "string" },
|
|
285
288
|
cursor: { type: "string" },
|
|
286
|
-
locale: { type: "string", default: "en" }, //
|
|
289
|
+
locale: { type: "string", default: "en" }, // default en, matching the web default locale
|
|
287
290
|
});
|
|
288
291
|
const { apiOrigin, token } = authedCtx();
|
|
289
292
|
if (values.mine) {
|
|
@@ -297,55 +300,106 @@ async function cmdChannelsList(argv) {
|
|
|
297
300
|
|
|
298
301
|
async function cmdChannelsGet(argv) {
|
|
299
302
|
const { positionals } = parse(argv, {});
|
|
300
|
-
const id = requirePositional(positionals, 0, "
|
|
303
|
+
const id = requirePositional(positionals, 0, "Usage: neodrop channels get <channelId>");
|
|
301
304
|
const { apiOrigin, token, creds } = authedCtx();
|
|
302
305
|
emit(await trpcQuery({ apiOrigin, token }, "channel.getById", { id }));
|
|
303
306
|
note(`🔗 ${channelUrl(creds.webOrigin, id)}`);
|
|
304
307
|
}
|
|
305
308
|
|
|
309
|
+
// Creating a channel = launching a creation Agent task (agentTask.create), the same pipeline as the web
|
|
310
|
+
// create wizard. A bare channel.create only leaves an empty shell stuck forever in DRAFT with no runnable
|
|
311
|
+
// config (issue #7), so the sugar command no longer exposes it; if you truly need a shell, use
|
|
312
|
+
// `api channel.create --mutation`.
|
|
313
|
+
const CREATE_CARRIERS = ["Article", "ImagePost", "Podcast", "Music", "Video"];
|
|
314
|
+
|
|
306
315
|
async function cmdChannelsCreate(argv) {
|
|
307
316
|
const { values } = parse(argv, {
|
|
308
317
|
name: { type: "string" },
|
|
318
|
+
prompt: { type: "string" },
|
|
309
319
|
description: { type: "string" },
|
|
310
|
-
type: { type: "string" },
|
|
311
320
|
locale: { type: "string" },
|
|
321
|
+
carrier: { type: "string" },
|
|
322
|
+
wait: { type: "boolean" },
|
|
312
323
|
json: { type: "string" },
|
|
313
324
|
stdin: { type: "boolean" },
|
|
314
325
|
});
|
|
315
326
|
if (values.json && values.stdin) {
|
|
316
|
-
throw new UsageError("--json
|
|
327
|
+
throw new UsageError("--json and --stdin are mutually exclusive, pass only one");
|
|
317
328
|
}
|
|
318
|
-
if (values.
|
|
319
|
-
throw new UsageError(
|
|
329
|
+
if (values.carrier && !CREATE_CARRIERS.includes(values.carrier)) {
|
|
330
|
+
throw new UsageError(`--carrier must be one of ${CREATE_CARRIERS.join(" | ")}`);
|
|
320
331
|
}
|
|
321
|
-
const
|
|
332
|
+
const ctx = authedCtx();
|
|
333
|
+
// --json / --stdin passes the raw agentTask.create input through (advanced use; field contract in references/commands.md)
|
|
322
334
|
let input = await loadInput(values);
|
|
323
335
|
if (input === undefined) {
|
|
324
336
|
if (!values.name) {
|
|
325
337
|
throw new UsageError(
|
|
326
|
-
|
|
327
|
-
"
|
|
328
|
-
"或:neodrop channels create --stdin",
|
|
338
|
+
'Usage: neodrop channels create --name <channel name> [--prompt "<creation brief>"] [--description <one-line summary>] [--locale en] [--carrier Article|ImagePost|Podcast|Music|Video] [--wait]\n' +
|
|
339
|
+
"Creation is asynchronous (an Agent generates the channel config, usually a few minutes): by default it returns the task immediately — poll with channels create-status <taskId>; --wait blocks until creation finishes.",
|
|
329
340
|
);
|
|
330
341
|
}
|
|
331
|
-
input = {
|
|
332
|
-
if (values.description) input.
|
|
333
|
-
if (values.
|
|
342
|
+
input = { channelName: values.name };
|
|
343
|
+
if (values.description) input.channelDescription = values.description;
|
|
344
|
+
if (values.prompt) input.description = values.prompt;
|
|
334
345
|
if (values.locale) input.locale = values.locale;
|
|
346
|
+
if (values.carrier) input.contentCarrier = values.carrier;
|
|
347
|
+
}
|
|
348
|
+
const task = await trpcMutation(ctx, "agentTask.create", input);
|
|
349
|
+
note(`✅ Creation task started: task=${task.id} channel=${task.channelId ?? "<pending>"}`);
|
|
350
|
+
if (task.channelId) note(`🔗 ${channelUrl(ctx.creds.webOrigin, task.channelId)}`);
|
|
351
|
+
|
|
352
|
+
if (!values.wait) {
|
|
353
|
+
note(" The channel config is generated asynchronously by an Agent (usually a few minutes). Poll: neodrop channels create-status " + task.id);
|
|
354
|
+
emit(task);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// --wait: poll to a terminal state (COMPLETED / FAILED), or stop at PAUSED (credits exhausted, awaiting top-up).
|
|
359
|
+
const deadline = Date.now() + 20 * 60 * 1000;
|
|
360
|
+
let current = task;
|
|
361
|
+
while (Date.now() < deadline) {
|
|
362
|
+
if (current.status === "COMPLETED" || current.status === "FAILED") break;
|
|
363
|
+
if (current.status === "PAUSED") {
|
|
364
|
+
note("⚠ Task paused (usually insufficient credits). It resumes automatically after a top-up; check later with channels create-status.");
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
await sleep(15 * 1000);
|
|
368
|
+
current = await trpcQuery(ctx, "agentTask.getById", { id: task.id });
|
|
369
|
+
note(`… status=${current.status}`);
|
|
335
370
|
}
|
|
336
|
-
|
|
371
|
+
if (current.status === "FAILED") process.exitCode = 1;
|
|
372
|
+
emit(current);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Query a creation task's status (agentTask.getById). status: PENDING/RUNNING → in progress;
|
|
376
|
+
// COMPLETED → done; FAILED → failed; PAUSED → paused (insufficient credits, resumes after top-up).
|
|
377
|
+
async function cmdChannelsCreateStatus(argv) {
|
|
378
|
+
const { positionals } = parse(argv, {});
|
|
379
|
+
const id = requirePositional(positionals, 0, "Usage: neodrop channels create-status <taskId>");
|
|
380
|
+
const { apiOrigin, token } = authedCtx();
|
|
381
|
+
emit(await trpcQuery({ apiOrigin, token }, "agentTask.getById", { id }));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Manually trigger a channel to produce one run of content (channel.triggerRun). The channel must have
|
|
385
|
+
// finished creation (or be DRAFT but already have a runnable config — the backend activates it automatically).
|
|
386
|
+
async function cmdChannelsRun(argv) {
|
|
387
|
+
const { positionals } = parse(argv, {});
|
|
388
|
+
const id = requirePositional(positionals, 0, "Usage: neodrop channels run <channelId>");
|
|
389
|
+
const { apiOrigin, token } = authedCtx();
|
|
390
|
+
emit(await trpcMutation({ apiOrigin, token }, "channel.triggerRun", { channelId: id }));
|
|
337
391
|
}
|
|
338
392
|
|
|
339
393
|
async function cmdChannelsSubscribe(argv) {
|
|
340
394
|
const { positionals } = parse(argv, {});
|
|
341
|
-
const id = requirePositional(positionals, 0, "
|
|
395
|
+
const id = requirePositional(positionals, 0, "Usage: neodrop channels subscribe <channelId>");
|
|
342
396
|
const { apiOrigin, token } = authedCtx();
|
|
343
397
|
emit(await trpcMutation({ apiOrigin, token }, "channel.subscribe", { channelId: id }));
|
|
344
398
|
}
|
|
345
399
|
|
|
346
400
|
async function cmdChannelsUnsubscribe(argv) {
|
|
347
401
|
const { positionals } = parse(argv, {});
|
|
348
|
-
const id = requirePositional(positionals, 0, "
|
|
402
|
+
const id = requirePositional(positionals, 0, "Usage: neodrop channels unsubscribe <channelId>");
|
|
349
403
|
const { apiOrigin, token } = authedCtx();
|
|
350
404
|
emit(await trpcMutation({ apiOrigin, token }, "channel.unsubscribe", { channelId: id }));
|
|
351
405
|
}
|
|
@@ -356,7 +410,7 @@ async function cmdChannelsSearch(argv) {
|
|
|
356
410
|
locale: { type: "string" },
|
|
357
411
|
strict: { type: "boolean" },
|
|
358
412
|
});
|
|
359
|
-
const query = requirePositional(positionals, 0, '
|
|
413
|
+
const query = requirePositional(positionals, 0, 'Usage: neodrop channels search "<query>"');
|
|
360
414
|
const { apiOrigin, token } = authedCtx();
|
|
361
415
|
const payload = { query };
|
|
362
416
|
const limit = toLimit(values.limit);
|
|
@@ -378,9 +432,9 @@ async function cmdChannelsByCategory(argv) {
|
|
|
378
432
|
locale: { type: "string" },
|
|
379
433
|
sort: { type: "string" },
|
|
380
434
|
});
|
|
381
|
-
const slug = requirePositional(positionals, 0, "
|
|
435
|
+
const slug = requirePositional(positionals, 0, "Usage: neodrop channels by-category <slug>");
|
|
382
436
|
if (values.sort && values.sort !== "latest" && values.sort !== "popular") {
|
|
383
|
-
throw new UsageError("--sort
|
|
437
|
+
throw new UsageError("--sort must be latest or popular");
|
|
384
438
|
}
|
|
385
439
|
const { apiOrigin, token } = authedCtx();
|
|
386
440
|
const payload = { categorySlug: slug };
|
|
@@ -392,8 +446,8 @@ async function cmdChannelsByCategory(argv) {
|
|
|
392
446
|
emit(await trpcQuery({ apiOrigin, token }, "channel.listByCategory", payload));
|
|
393
447
|
}
|
|
394
448
|
|
|
395
|
-
// ---- post
|
|
396
|
-
//
|
|
449
|
+
// ---- post commands ----------------------------------------------------
|
|
450
|
+
// The user-facing term is unified as post; the tRPC procedure names are still the backend contract `grain.*`, unchanged.
|
|
397
451
|
|
|
398
452
|
async function cmdPostsList(argv) {
|
|
399
453
|
const { values } = parse(argv, {
|
|
@@ -418,7 +472,7 @@ async function cmdPostsList(argv) {
|
|
|
418
472
|
emit(await trpcQuery({ apiOrigin, token }, "grain.list", payload));
|
|
419
473
|
return;
|
|
420
474
|
}
|
|
421
|
-
//
|
|
475
|
+
// otherwise listRecent (public feed)
|
|
422
476
|
const payload = { limit };
|
|
423
477
|
if (values.cursor) payload.cursor = values.cursor;
|
|
424
478
|
if (values.locale) payload.locale = values.locale;
|
|
@@ -427,7 +481,7 @@ async function cmdPostsList(argv) {
|
|
|
427
481
|
|
|
428
482
|
async function cmdPostsGet(argv) {
|
|
429
483
|
const { positionals } = parse(argv, {});
|
|
430
|
-
const id = requirePositional(positionals, 0, "
|
|
484
|
+
const id = requirePositional(positionals, 0, "Usage: neodrop posts get <postId>");
|
|
431
485
|
const { apiOrigin, token, creds } = authedCtx();
|
|
432
486
|
emit(await trpcQuery({ apiOrigin, token }, "grain.getById", { id }));
|
|
433
487
|
note(`🔗 ${postUrl(creds.webOrigin, id)}`);
|
|
@@ -439,7 +493,7 @@ async function cmdPostsSearch(argv) {
|
|
|
439
493
|
locale: { type: "string" },
|
|
440
494
|
strict: { type: "boolean" },
|
|
441
495
|
});
|
|
442
|
-
const query = requirePositional(positionals, 0, '
|
|
496
|
+
const query = requirePositional(positionals, 0, 'Usage: neodrop posts search "<query>"');
|
|
443
497
|
const { apiOrigin, token } = authedCtx();
|
|
444
498
|
const payload = { query };
|
|
445
499
|
const limit = toLimit(values.limit);
|
|
@@ -460,7 +514,84 @@ async function cmdFeed(argv) {
|
|
|
460
514
|
emit(await trpcQuery({ apiOrigin, token }, "grain.listSubscribed", payload));
|
|
461
515
|
}
|
|
462
516
|
|
|
463
|
-
// ----
|
|
517
|
+
// ---- chat commands ----------------------------------------------------
|
|
518
|
+
|
|
519
|
+
async function cmdChatSend(argv) {
|
|
520
|
+
const { values, positionals } = parse(argv, {
|
|
521
|
+
session: { type: "string" },
|
|
522
|
+
channel: { type: "string" },
|
|
523
|
+
locale: { type: "string", default: "en" },
|
|
524
|
+
timeout: { type: "string" },
|
|
525
|
+
"poll-interval": { type: "string" },
|
|
526
|
+
});
|
|
527
|
+
const text = requirePositional(
|
|
528
|
+
positionals,
|
|
529
|
+
0,
|
|
530
|
+
'Usage: neodrop chat "<message>" [--session <id> | --channel <id>] [--locale en] [--timeout <seconds>]\n' +
|
|
531
|
+
" neodrop chat history --session <id>",
|
|
532
|
+
);
|
|
533
|
+
if (values.session && values.channel) {
|
|
534
|
+
throw new UsageError("--session and --channel are mutually exclusive: --session continues an existing session, --channel gets that channel's assistant session");
|
|
535
|
+
}
|
|
536
|
+
const timeoutSec = values.timeout === undefined ? 600 : Number(values.timeout);
|
|
537
|
+
if (!Number.isFinite(timeoutSec) || timeoutSec <= 0) {
|
|
538
|
+
throw new UsageError(`--timeout must be a positive number of seconds, got "${values.timeout}"`);
|
|
539
|
+
}
|
|
540
|
+
const pollSec = values["poll-interval"] === undefined ? 2 : Number(values["poll-interval"]);
|
|
541
|
+
if (!Number.isFinite(pollSec) || pollSec <= 0) {
|
|
542
|
+
throw new UsageError(`--poll-interval must be a positive number of seconds, got "${values["poll-interval"]}"`);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const { apiOrigin, token } = authedCtx();
|
|
546
|
+
let sessionId = values.session;
|
|
547
|
+
if (!sessionId) {
|
|
548
|
+
sessionId = await resolveChatSession({ apiOrigin, token, channelId: values.channel });
|
|
549
|
+
note(`session ${sessionId} (continue with: neodrop chat "…" --session ${sessionId})`);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const result = await sendAndAwaitReply({
|
|
553
|
+
apiOrigin,
|
|
554
|
+
token,
|
|
555
|
+
sessionId,
|
|
556
|
+
text,
|
|
557
|
+
locale: values.locale,
|
|
558
|
+
timeoutMs: timeoutSec * 1000,
|
|
559
|
+
pollIntervalMs: pollSec * 1000,
|
|
560
|
+
});
|
|
561
|
+
if (!result.reply) {
|
|
562
|
+
note("⚠ No assistant text reply this turn (generation may have failed or been cancelled); newMessages holds every message added this turn.");
|
|
563
|
+
}
|
|
564
|
+
emit(result);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
async function cmdChatHistory(argv) {
|
|
568
|
+
const { values } = parse(argv, {
|
|
569
|
+
session: { type: "string" },
|
|
570
|
+
});
|
|
571
|
+
if (!values.session) {
|
|
572
|
+
throw new UsageError("Usage: neodrop chat history --session <id>");
|
|
573
|
+
}
|
|
574
|
+
const { apiOrigin, token } = authedCtx();
|
|
575
|
+
emit(
|
|
576
|
+
await trpcQuery({ apiOrigin, token }, "session.getMessages", {
|
|
577
|
+
sessionId: values.session,
|
|
578
|
+
}),
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
async function cmdChatSessions() {
|
|
583
|
+
const { apiOrigin, token } = authedCtx();
|
|
584
|
+
emit(await trpcQuery({ apiOrigin, token }, "session.list"));
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
async function cmdChat(argv) {
|
|
588
|
+
const sub = argv[0];
|
|
589
|
+
if (sub === "history") return cmdChatHistory(argv.slice(1));
|
|
590
|
+
if (sub === "sessions") return cmdChatSessions();
|
|
591
|
+
return cmdChatSend(argv);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// ---- escape hatch -----------------------------------------------------
|
|
464
595
|
|
|
465
596
|
async function cmdApi(argv) {
|
|
466
597
|
const { values, positionals } = parse(argv, {
|
|
@@ -469,9 +600,9 @@ async function cmdApi(argv) {
|
|
|
469
600
|
mutation: { type: "boolean" },
|
|
470
601
|
});
|
|
471
602
|
if (values.json && values.stdin) {
|
|
472
|
-
throw new UsageError("--json
|
|
603
|
+
throw new UsageError("--json and --stdin are mutually exclusive, pass only one");
|
|
473
604
|
}
|
|
474
|
-
const procedure = requirePositional(positionals, 0, "
|
|
605
|
+
const procedure = requirePositional(positionals, 0, "Usage: neodrop api <procedure> [--json '...' | --stdin] [--mutation]");
|
|
475
606
|
const { apiOrigin, token } = authedCtx();
|
|
476
607
|
const input = await loadInput(values);
|
|
477
608
|
if (values.mutation) {
|
|
@@ -481,63 +612,75 @@ async function cmdApi(argv) {
|
|
|
481
612
|
}
|
|
482
613
|
}
|
|
483
614
|
|
|
484
|
-
// ---- skill
|
|
615
|
+
// ---- skill install ----------------------------------------------------
|
|
485
616
|
|
|
486
617
|
async function cmdInstallSkill(argv) {
|
|
487
618
|
const { values } = parse(argv, {
|
|
488
619
|
dest: { type: "string" },
|
|
489
620
|
});
|
|
490
621
|
const { target, copied } = installSkill({ dest: values.dest });
|
|
491
|
-
note(`✅
|
|
492
|
-
note(`
|
|
493
|
-
note("
|
|
622
|
+
note(`✅ Installed skill to ${target}`);
|
|
623
|
+
note(` Copied: ${copied.join(", ")}`);
|
|
624
|
+
note(" After restarting Claude Code (or opening a new session), the AI will route Neodrop-related questions to this skill automatically.");
|
|
494
625
|
emit({ ok: true, target, copied });
|
|
495
626
|
}
|
|
496
627
|
|
|
497
|
-
// ----
|
|
628
|
+
// ---- help -------------------------------------------------------------
|
|
498
629
|
|
|
499
|
-
const HELP = `neodrop — Neodrop CLI
|
|
630
|
+
const HELP = `neodrop — Neodrop CLI (shared by AI agents and humans, stdout = JSON)
|
|
500
631
|
|
|
501
|
-
|
|
632
|
+
Usage:
|
|
502
633
|
npx neodrop-cli <command> [args...]
|
|
503
634
|
|
|
504
|
-
|
|
505
|
-
login [--server <url>] [--api <url>] [--name
|
|
506
|
-
logout
|
|
507
|
-
whoami
|
|
508
|
-
me
|
|
509
|
-
tokens list
|
|
510
|
-
tokens revoke <id>
|
|
511
|
-
install-skill [--dest <dir>]
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
635
|
+
Meta:
|
|
636
|
+
login [--server <url>] [--api <url>] [--name <name>] Authorize and write a PAT to ~/.neodrop/credentials.json
|
|
637
|
+
logout Revoke the PAT + delete the local credential
|
|
638
|
+
whoami Current token + user info
|
|
639
|
+
me Current user info (user.getMe)
|
|
640
|
+
tokens list List every PAT
|
|
641
|
+
tokens revoke <id> Revoke a specific PAT
|
|
642
|
+
install-skill [--dest <dir>] Install SKILL.md + references into the agent's skill dir
|
|
643
|
+
(default ${defaultSkillDest()})
|
|
644
|
+
|
|
645
|
+
Channels:
|
|
515
646
|
channels list [--mine] [--limit N] [--cursor C] [--locale L]
|
|
516
647
|
channels get <channelId>
|
|
517
|
-
channels create --name <X> [--
|
|
518
|
-
|
|
648
|
+
channels create --name <X> [--prompt "<brief>"] [--description <Y>] [--locale L]
|
|
649
|
+
[--carrier Article|ImagePost|Podcast|Music|Video] [--wait]
|
|
650
|
+
Launch the creation Agent (async, a few minutes)
|
|
651
|
+
channels create-status <taskId> Check a creation task's progress / result
|
|
652
|
+
channels run <channelId> Manually trigger one run of content
|
|
519
653
|
channels subscribe <channelId>
|
|
520
654
|
channels unsubscribe <channelId>
|
|
521
655
|
channels search "<query>" [--limit N] [--locale L] [--strict]
|
|
522
656
|
channels categories
|
|
523
657
|
channels by-category <slug> [--limit N] [--cursor C] [--locale L] [--sort latest|popular]
|
|
524
658
|
|
|
525
|
-
|
|
659
|
+
Content (Post):
|
|
526
660
|
posts list [--subscribed | --channel <id>] [--limit N] [--cursor C] [--locale L]
|
|
527
661
|
posts get <postId>
|
|
528
662
|
posts search "<query>" [--limit N] [--locale L] [--strict]
|
|
529
663
|
feed [--limit N] [--cursor C] = posts list --subscribed
|
|
530
664
|
|
|
531
|
-
|
|
665
|
+
Chat:
|
|
666
|
+
chat "<message>" [--session <id> | --channel <id>] [--locale L] [--timeout <seconds>]
|
|
667
|
+
Send a message to the AI assistant and wait for the full
|
|
668
|
+
reply (defaults to a new global-assistant session;
|
|
669
|
+
--session continues a session; --channel talks to that
|
|
670
|
+
channel's assistant)
|
|
671
|
+
chat history --session <id> View a session's full message list
|
|
672
|
+
chat sessions List my sessions
|
|
673
|
+
|
|
674
|
+
Escape hatch:
|
|
532
675
|
api <procedure> [--json '...' | --stdin] [--mutation]
|
|
533
676
|
|
|
534
|
-
|
|
535
|
-
--pretty
|
|
677
|
+
Global:
|
|
678
|
+
--pretty Indented JSON output (still valid JSON)
|
|
536
679
|
|
|
537
|
-
|
|
538
|
-
|
|
680
|
+
Environment variables: NEODROP_SERVER (web origin) / NEODROP_API (api origin)
|
|
681
|
+
See SKILL.md and references/ for more.`;
|
|
539
682
|
|
|
540
|
-
// ----
|
|
683
|
+
// ---- routing ----------------------------------------------------------
|
|
541
684
|
|
|
542
685
|
const TOKENS_SUB = {
|
|
543
686
|
list: cmdTokensList,
|
|
@@ -547,6 +690,8 @@ const CHANNELS_SUB = {
|
|
|
547
690
|
list: cmdChannelsList,
|
|
548
691
|
get: cmdChannelsGet,
|
|
549
692
|
create: cmdChannelsCreate,
|
|
693
|
+
"create-status": cmdChannelsCreateStatus,
|
|
694
|
+
run: cmdChannelsRun,
|
|
550
695
|
subscribe: cmdChannelsSubscribe,
|
|
551
696
|
unsubscribe: cmdChannelsUnsubscribe,
|
|
552
697
|
search: cmdChannelsSearch,
|
|
@@ -562,17 +707,17 @@ const POSTS_SUB = {
|
|
|
562
707
|
async function dispatchGroup(name, table, argv) {
|
|
563
708
|
const sub = argv[0];
|
|
564
709
|
if (!sub || sub === "--help" || sub === "-h") {
|
|
565
|
-
throw new UsageError(
|
|
710
|
+
throw new UsageError(`Usage: neodrop ${name} <${Object.keys(table).join(" | ")}>`);
|
|
566
711
|
}
|
|
567
712
|
const handler = table[sub];
|
|
568
713
|
if (!handler) {
|
|
569
|
-
throw new UsageError(
|
|
714
|
+
throw new UsageError(`Unknown subcommand ${name} ${sub} (available: ${Object.keys(table).join(" / ")})`);
|
|
570
715
|
}
|
|
571
716
|
await handler(argv.slice(1));
|
|
572
717
|
}
|
|
573
718
|
|
|
574
719
|
async function dispatch(rawArgs) {
|
|
575
|
-
//
|
|
720
|
+
// The global --pretty can appear anywhere: strip it out first, hand the rest to each command's parser.
|
|
576
721
|
const pretty = rawArgs.includes("--pretty");
|
|
577
722
|
setPretty(pretty);
|
|
578
723
|
const args = rawArgs.filter((a) => a !== "--pretty");
|
|
@@ -597,17 +742,19 @@ async function dispatch(rawArgs) {
|
|
|
597
742
|
return dispatchGroup("tokens", TOKENS_SUB, rest);
|
|
598
743
|
case "channels":
|
|
599
744
|
return dispatchGroup("channels", CHANNELS_SUB, rest);
|
|
600
|
-
case "grains": //
|
|
745
|
+
case "grains": // backward compatible: old command name, renamed to posts
|
|
601
746
|
case "posts":
|
|
602
747
|
return dispatchGroup("posts", POSTS_SUB, rest);
|
|
603
748
|
case "feed":
|
|
604
749
|
return cmdFeed(rest);
|
|
750
|
+
case "chat":
|
|
751
|
+
return cmdChat(rest);
|
|
605
752
|
case "api":
|
|
606
753
|
return cmdApi(rest);
|
|
607
754
|
case "install-skill":
|
|
608
755
|
return cmdInstallSkill(rest);
|
|
609
756
|
default:
|
|
610
|
-
throw new UsageError(
|
|
757
|
+
throw new UsageError(`Unknown command "${cmd}". Run neodrop --help to see every command.`);
|
|
611
758
|
}
|
|
612
759
|
}
|
|
613
760
|
|
|
@@ -620,7 +767,7 @@ async function main() {
|
|
|
620
767
|
process.exitCode = 2;
|
|
621
768
|
return;
|
|
622
769
|
}
|
|
623
|
-
// ApiError
|
|
770
|
+
// ApiError (tRPC business error) and other runtime errors share exit code 1
|
|
624
771
|
note(`✗ ${err.message}`);
|
|
625
772
|
process.exitCode = 1;
|
|
626
773
|
}
|