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