@myapihq/cli 2.2.0 → 2.4.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/dist/commands/account.d.ts +1 -1
- package/dist/commands/account.js +1 -0
- package/dist/commands/authproduct.js +7 -3
- package/dist/commands/billing.js +7 -2
- package/dist/commands/container.js +4 -3
- package/dist/commands/database.js +4 -3
- package/dist/commands/domain.js +39 -14
- package/dist/commands/email/message.js +6 -3
- package/dist/commands/email/template.js +3 -3
- package/dist/commands/fn.js +4 -3
- package/dist/commands/funnel.js +33 -13
- package/dist/commands/git.js +108 -35
- package/dist/commands/image.js +7 -4
- package/dist/commands/keys-validation.test.js +5 -0
- package/dist/commands/keys.js +6 -2
- package/dist/commands/llm.js +41 -20
- package/dist/commands/login-validation.test.d.ts +1 -0
- package/dist/commands/login-validation.test.js +43 -0
- package/dist/commands/login.d.ts +14 -0
- package/dist/commands/login.js +447 -0
- package/dist/commands/org.d.ts +1 -1
- package/dist/commands/org.js +24 -7
- package/dist/commands/queue.js +4 -3
- package/dist/commands/setup.js +6 -1
- package/dist/commands/storage.js +1 -1
- package/dist/commands/workflow.js +5 -4
- package/dist/completion.js +3 -3
- package/dist/config.js +3 -0
- package/dist/errors.d.ts +3 -0
- package/dist/errors.js +62 -0
- package/dist/errors.test.d.ts +1 -0
- package/dist/errors.test.js +28 -0
- package/dist/exposes.test.js +1 -0
- package/dist/flags.js +5 -3
- package/dist/flags.test.js +1 -1
- package/dist/helpers.d.ts +1 -0
- package/dist/helpers.js +22 -0
- package/dist/index.js +10 -51
- package/dist/skills/my-api-hq/SKILL.md +2 -2
- package/dist/skills/my-domain-api/SKILL.md +5 -5
- package/dist/skills/my-email-verify-api/SKILL.md +6 -4
- package/dist/skills/my-funnel-api/SKILL.md +5 -4
- package/dist/skills/my-git-api/SKILL.md +14 -7
- package/dist/skills/my-image-api/SKILL.md +1 -1
- package/dist/skills/my-llm-api/SKILL.md +11 -11
- package/dist/skills/my-storage-api/SKILL.md +5 -5
- package/dist/skills/my-webhook-api/SKILL.md +1 -1
- package/dist/skills/my-workflow-api/SKILL.md +1 -1
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +13 -1
- package/package.json +3 -2
package/dist/commands/image.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { image as sdkImage } from '@myapihq/sdk';
|
|
2
2
|
import { requireConfig } from '../config.js';
|
|
3
3
|
import { success, error, printTable, info, printJson, banner } from '../output.js';
|
|
4
|
-
import { formatDate, pollJob } from '../utils.js';
|
|
4
|
+
import { formatDate, pollJob, retryFunds } from '../utils.js';
|
|
5
5
|
import { requireOrg, requireArg } from '../helpers.js';
|
|
6
6
|
export const EXPOSES = [
|
|
7
7
|
'POST /image/orgs/{org_id}/generate',
|
|
@@ -63,7 +63,10 @@ async function generate(promptArg, flags) {
|
|
|
63
63
|
// Cost is per-model — see `myapi image models`. Banner shown on stderr so
|
|
64
64
|
// --json output stays clean for piping.
|
|
65
65
|
banner(`› Generating image${payload.model ? ` (${payload.model})` : ''}…`);
|
|
66
|
-
const job = await sdkImage.generateImage(config.api_key, orgId, payload);
|
|
66
|
+
const job = await retryFunds(() => sdkImage.generateImage(config.api_key, orgId, payload));
|
|
67
|
+
// Surface the job id BEFORE polling — the job is already charged, so a
|
|
68
|
+
// poll timeout must leave the user a handle to recover it.
|
|
69
|
+
banner(`› Job ${job.job_id} submitted.`);
|
|
67
70
|
const final = await pollJob({
|
|
68
71
|
label: 'Generating image',
|
|
69
72
|
timeoutMs: 90_000,
|
|
@@ -72,7 +75,7 @@ async function generate(promptArg, flags) {
|
|
|
72
75
|
isDone: s => s.status === 'completed',
|
|
73
76
|
isFailed: s => s.status === 'failed',
|
|
74
77
|
failedMessage: 'Image generation failed',
|
|
75
|
-
timeoutMessage:
|
|
78
|
+
timeoutMessage: `Image generation timed out — the job may still complete. Check later with: myapi image get ${job.job_id}`,
|
|
76
79
|
});
|
|
77
80
|
if (flags.json) {
|
|
78
81
|
printJson(final);
|
|
@@ -207,7 +210,7 @@ The asset lands in your org's storage and is also visible via "myapi storage lis
|
|
|
207
210
|
if (usage)
|
|
208
211
|
info(`Usage: ${usage}`);
|
|
209
212
|
else
|
|
210
|
-
|
|
213
|
+
info(`Unknown subcommand: ${subcommand}. Run "myapi image --help" for the list.`);
|
|
211
214
|
return;
|
|
212
215
|
}
|
|
213
216
|
switch (subcommand) {
|
|
@@ -48,6 +48,11 @@ describe('_parseGrants', () => {
|
|
|
48
48
|
expect(typeof r).toBe('string');
|
|
49
49
|
expect(r).toMatch(/Invalid access "admin"/);
|
|
50
50
|
});
|
|
51
|
+
it('extra colon segments — no silent truncation to the first pair', () => {
|
|
52
|
+
const r = _parseGrants('email:read:write');
|
|
53
|
+
expect(typeof r).toBe('string');
|
|
54
|
+
expect(r).toMatch(/Invalid access "read:write"/);
|
|
55
|
+
});
|
|
51
56
|
it('an empty string', () => {
|
|
52
57
|
expect(typeof _parseGrants('')).toBe('string');
|
|
53
58
|
expect(_parseGrants('')).toMatch(/--grant was empty/);
|
package/dist/commands/keys.js
CHANGED
|
@@ -30,7 +30,11 @@ const KEY_KINDS = ['function', 'manual', 'account'];
|
|
|
30
30
|
export function _parseGrants(raw) {
|
|
31
31
|
const grants = {};
|
|
32
32
|
for (const part of raw.split(',').map(s => s.trim()).filter(Boolean)) {
|
|
33
|
-
|
|
33
|
+
// Split on the FIRST colon only, keeping the rest intact so malformed
|
|
34
|
+
// grants like "email:read:write" fail validation instead of silently
|
|
35
|
+
// truncating to "email:read".
|
|
36
|
+
const colon = part.indexOf(':');
|
|
37
|
+
const [slot, accessRaw] = colon >= 0 ? [part.slice(0, colon), part.slice(colon + 1)] : [part, 'write'];
|
|
34
38
|
if (!VALID_GRANT_TARGETS.has(slot)) {
|
|
35
39
|
return `Unknown slot "${slot}" in --grant. Valid slots: ${[...hq.GRANTABLE_SLOTS].join(', ')} (or "*").`;
|
|
36
40
|
}
|
|
@@ -87,7 +91,7 @@ function describeKey(k) {
|
|
|
87
91
|
// ── Subcommands ──────────────────────────────────────────────────────────────
|
|
88
92
|
export async function createNew(flags) {
|
|
89
93
|
const config = requireConfig();
|
|
90
|
-
let name = flags.name
|
|
94
|
+
let name = typeof flags.name === 'string' ? flags.name : '';
|
|
91
95
|
if (!name)
|
|
92
96
|
name = await ask('Enter a name for the new key: ');
|
|
93
97
|
if (!name.trim())
|
package/dist/commands/llm.js
CHANGED
|
@@ -3,6 +3,7 @@ import { llm as sdkLlm } from '@myapihq/sdk';
|
|
|
3
3
|
import { requireConfig } from '../config.js';
|
|
4
4
|
import { error, info, printTable, printJson } from '../output.js';
|
|
5
5
|
import { requireOrg } from '../helpers.js';
|
|
6
|
+
import { retryFunds } from '../utils.js';
|
|
6
7
|
export const EXPOSES = [
|
|
7
8
|
'POST /llm/orgs/{org_id}/complete',
|
|
8
9
|
'POST /llm/orgs/{org_id}/embed',
|
|
@@ -47,6 +48,25 @@ async function defaultChatModel(apiKey, orgId) {
|
|
|
47
48
|
}
|
|
48
49
|
return chat.id;
|
|
49
50
|
}
|
|
51
|
+
// Resolve the embed model from --model, else the live catalog. Unlike chat
|
|
52
|
+
// we don't silently pick a default when several are served — embedding
|
|
53
|
+
// vectors aren't cross-model compatible, so an ambiguous pick is a footgun.
|
|
54
|
+
// One served embed model → use it; none → catalog-truthful error (not a
|
|
55
|
+
// hardcoded "not available today"); many → make the caller choose.
|
|
56
|
+
async function resolveEmbedModel(apiKey, orgId, flags) {
|
|
57
|
+
if (typeof flags.model === 'string' && flags.model)
|
|
58
|
+
return flags.model;
|
|
59
|
+
const embeds = (await loadModels(apiKey, orgId)).filter(m => m.kind === 'embed');
|
|
60
|
+
if (embeds.length === 1)
|
|
61
|
+
return embeds[0].id;
|
|
62
|
+
if (embeds.length === 0) {
|
|
63
|
+
error('No embedding model is currently served — the catalog has none. Run "myapi llm models --kind embed" to check.');
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
error(`Multiple embedding models available — pass --model <id>. Options: ${embeds.map(m => m.id).join(', ')}`);
|
|
67
|
+
}
|
|
68
|
+
throw new Error('unreachable'); // satisfy the type checker
|
|
69
|
+
}
|
|
50
70
|
function readPromptArg(promptArg, flags) {
|
|
51
71
|
if (typeof flags.file === 'string' && flags.file) {
|
|
52
72
|
return fs.readFileSync(flags.file, 'utf-8');
|
|
@@ -123,13 +143,13 @@ async function complete(promptArg, flags) {
|
|
|
123
143
|
const stop = typeof flags.stop === 'string' && flags.stop
|
|
124
144
|
? flags.stop.split(',').map(s => s.trim()).filter(Boolean)
|
|
125
145
|
: undefined;
|
|
126
|
-
const res = await sdkLlm.complete(config.api_key, orgId, {
|
|
146
|
+
const res = await retryFunds(() => sdkLlm.complete(config.api_key, orgId, {
|
|
127
147
|
model,
|
|
128
148
|
messages,
|
|
129
149
|
max_tokens: typeof flags['max-tokens'] === 'number' ? flags['max-tokens'] : undefined,
|
|
130
150
|
temperature: typeof flags.temperature === 'number' ? flags.temperature : undefined,
|
|
131
151
|
stop,
|
|
132
|
-
});
|
|
152
|
+
}));
|
|
133
153
|
if (flags.json) {
|
|
134
154
|
printJson(res);
|
|
135
155
|
return;
|
|
@@ -140,14 +160,12 @@ async function complete(promptArg, flags) {
|
|
|
140
160
|
}
|
|
141
161
|
async function embed(inputArg, flags) {
|
|
142
162
|
const config = requireConfig();
|
|
143
|
-
const orgId = requireOrg(flags, config, 'myapi llm embed "<text>" --model <id> [--org <id>]');
|
|
144
|
-
const model =
|
|
145
|
-
if (!model)
|
|
146
|
-
error('Missing required flag: --model. Run "myapi llm models" to list embed models.');
|
|
163
|
+
const orgId = requireOrg(flags, config, 'myapi llm embed "<text>" [--model <id>] [--org <id>]');
|
|
164
|
+
const model = await resolveEmbedModel(config.api_key, orgId, flags);
|
|
147
165
|
const text = readPromptArg(inputArg, flags);
|
|
148
166
|
if (!text.trim())
|
|
149
167
|
error('Empty input. Pass <text> as an argument, "-" to read stdin, or --file <path>.');
|
|
150
|
-
const res = await sdkLlm.embed(config.api_key, orgId, { model, input: text });
|
|
168
|
+
const res = await retryFunds(() => sdkLlm.embed(config.api_key, orgId, { model, input: text }));
|
|
151
169
|
if (flags.json) {
|
|
152
170
|
printJson(res);
|
|
153
171
|
return;
|
|
@@ -194,7 +212,7 @@ async function classify(inputArg, flags) {
|
|
|
194
212
|
if (labels.length === 0)
|
|
195
213
|
error('--labels must contain at least one label');
|
|
196
214
|
const multi = flags.multi === true;
|
|
197
|
-
const res = await sdkLlm.classify(config.api_key, orgId, { input, labels, multi, tier: tierFromFlag(flags) });
|
|
215
|
+
const res = await retryFunds(() => sdkLlm.classify(config.api_key, orgId, { input, labels, multi, tier: tierFromFlag(flags) }));
|
|
198
216
|
if (flags.json) {
|
|
199
217
|
printJson(res);
|
|
200
218
|
return;
|
|
@@ -219,7 +237,7 @@ async function extract(inputArg, flags) {
|
|
|
219
237
|
error('Missing required flag: --schema <path|json>');
|
|
220
238
|
return;
|
|
221
239
|
}
|
|
222
|
-
const res = await sdkLlm.extract(config.api_key, orgId, { input, schema, tier: tierFromFlag(flags) });
|
|
240
|
+
const res = await retryFunds(() => sdkLlm.extract(config.api_key, orgId, { input, schema, tier: tierFromFlag(flags) }));
|
|
223
241
|
if (flags.json) {
|
|
224
242
|
printJson(res);
|
|
225
243
|
return;
|
|
@@ -237,11 +255,11 @@ async function summarize(inputArg, flags) {
|
|
|
237
255
|
if (style && !['brief', 'exec', 'bullet'].includes(style)) {
|
|
238
256
|
error('--style must be one of: brief, exec, bullet');
|
|
239
257
|
}
|
|
240
|
-
const res = await sdkLlm.summarize(config.api_key, orgId, {
|
|
258
|
+
const res = await retryFunds(() => sdkLlm.summarize(config.api_key, orgId, {
|
|
241
259
|
input,
|
|
242
260
|
style: style,
|
|
243
261
|
tier: tierFromFlag(flags),
|
|
244
|
-
});
|
|
262
|
+
}));
|
|
245
263
|
if (flags.json) {
|
|
246
264
|
printJson(res);
|
|
247
265
|
return;
|
|
@@ -256,19 +274,22 @@ async function draft(inputArg, flags) {
|
|
|
256
274
|
error('Missing required flag: --kind <email|message|reply|...>');
|
|
257
275
|
return;
|
|
258
276
|
}
|
|
277
|
+
// Capture the narrowed value — typeof narrowing doesn't survive into the
|
|
278
|
+
// retryFunds closure below.
|
|
279
|
+
const kind = flags.kind;
|
|
259
280
|
const input = readPromptArg(inputArg, flags);
|
|
260
281
|
const promptText = typeof flags.prompt === 'string' ? flags.prompt : '';
|
|
261
282
|
const ctx = parseContextFlag(flags);
|
|
262
283
|
if (!input.trim() && !promptText.trim() && (!ctx || Object.keys(ctx).length === 0)) {
|
|
263
284
|
error('draft needs at least one of: <source text> (arg or --file), --prompt, or --context.');
|
|
264
285
|
}
|
|
265
|
-
const res = await sdkLlm.draft(config.api_key, orgId, {
|
|
286
|
+
const res = await retryFunds(() => sdkLlm.draft(config.api_key, orgId, {
|
|
266
287
|
input: input || undefined,
|
|
267
|
-
kind
|
|
288
|
+
kind,
|
|
268
289
|
context: ctx,
|
|
269
290
|
prompt: promptText || undefined,
|
|
270
291
|
tier: tierFromFlag(flags),
|
|
271
|
-
});
|
|
292
|
+
}));
|
|
272
293
|
if (flags.json) {
|
|
273
294
|
printJson(res);
|
|
274
295
|
return;
|
|
@@ -283,8 +304,7 @@ const SUBCOMMAND_USAGE = {
|
|
|
283
304
|
[--stop <csv>] [--file <path>] [--org <id>] [--json]
|
|
284
305
|
|
|
285
306
|
Raw chat completion against a self-hosted catalog model. Without --model,
|
|
286
|
-
picks the first chat model from "myapi llm models"
|
|
287
|
-
Qwen/Qwen3-Coder-30B-A3B-Instruct).
|
|
307
|
+
picks the first chat model from "myapi llm models".
|
|
288
308
|
|
|
289
309
|
Pass "-" as the prompt to read from stdin. The reply goes to stdout; a
|
|
290
310
|
one-line usage footer (tokens + cost in cents) goes to stderr so it
|
|
@@ -293,10 +313,11 @@ const SUBCOMMAND_USAGE = {
|
|
|
293
313
|
Examples:
|
|
294
314
|
myapi llm complete "summarize: $(cat README.md)"
|
|
295
315
|
cat draft.md | myapi llm complete - --system "You are an editor" --max-tokens 200`,
|
|
296
|
-
embed: `myapi llm embed "<text>" --model <id> [--file <path>] [--org <id>] [--json]
|
|
316
|
+
embed: `myapi llm embed "<text>" [--model <id>] [--file <path>] [--org <id>] [--json]
|
|
297
317
|
|
|
298
|
-
|
|
299
|
-
|
|
318
|
+
Embed text into a dense vector. --model is optional when the catalog serves
|
|
319
|
+
exactly one embedding model; otherwise pass one from "myapi llm models
|
|
320
|
+
--kind embed". Returns EMBED_NOT_AVAILABLE if no embedding model is served.`,
|
|
300
321
|
models: `myapi llm models [--kind chat|embed] [--org <id>] [--json]
|
|
301
322
|
|
|
302
323
|
Lists the live model catalog with per-1M-token pricing in cents.
|
|
@@ -358,7 +379,7 @@ Subcommands:
|
|
|
358
379
|
classify Pick a label from a set
|
|
359
380
|
complete Raw chat completion against a catalog model
|
|
360
381
|
draft Write something (email | reply | message | …)
|
|
361
|
-
embed Raw embeddings
|
|
382
|
+
embed Raw embeddings against a catalog embed model
|
|
362
383
|
extract Pull structured data conforming to a JSON Schema
|
|
363
384
|
models List the live model catalog (id, kind, context, cents/1M)
|
|
364
385
|
summarize Summarize text (brief | exec | bullet)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Unit tests for the login command's pure PKCE/URL helpers.
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
import * as crypto from 'crypto';
|
|
4
|
+
import { generatePkce, buildAuthorizeUrl } from './login.js';
|
|
5
|
+
describe('generatePkce', () => {
|
|
6
|
+
it('challenge is base64url(sha256(verifier)) — S256', () => {
|
|
7
|
+
const { verifier, challenge } = generatePkce();
|
|
8
|
+
const expected = crypto.createHash('sha256').update(verifier).digest('base64url');
|
|
9
|
+
expect(challenge).toBe(expected);
|
|
10
|
+
});
|
|
11
|
+
it('verifier and state are base64url with sufficient entropy', () => {
|
|
12
|
+
const { verifier, state } = generatePkce();
|
|
13
|
+
// 32 random bytes → 43 base64url chars; 16 → 22. No padding, URL-safe.
|
|
14
|
+
expect(verifier).toMatch(/^[A-Za-z0-9_-]{43}$/);
|
|
15
|
+
expect(state).toMatch(/^[A-Za-z0-9_-]{22}$/);
|
|
16
|
+
});
|
|
17
|
+
it('never repeats across calls', () => {
|
|
18
|
+
const a = generatePkce();
|
|
19
|
+
const b = generatePkce();
|
|
20
|
+
expect(a.verifier).not.toBe(b.verifier);
|
|
21
|
+
expect(a.state).not.toBe(b.state);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
describe('buildAuthorizeUrl', () => {
|
|
25
|
+
it('carries the full OIDC + PKCE parameter set', () => {
|
|
26
|
+
const pkce = generatePkce();
|
|
27
|
+
const redirect = 'http://127.0.0.1:49152/callback';
|
|
28
|
+
const u = new URL(buildAuthorizeUrl('http://127.0.0.1:9999/authorize', redirect, pkce));
|
|
29
|
+
expect(u.pathname).toBe('/authorize');
|
|
30
|
+
expect(u.searchParams.get('client_id')).toBe('cl_operator_cli');
|
|
31
|
+
expect(u.searchParams.get('response_type')).toBe('code');
|
|
32
|
+
expect(u.searchParams.get('redirect_uri')).toBe(redirect);
|
|
33
|
+
expect(u.searchParams.get('state')).toBe(pkce.state);
|
|
34
|
+
expect(u.searchParams.get('code_challenge')).toBe(pkce.challenge);
|
|
35
|
+
expect(u.searchParams.get('code_challenge_method')).toBe('S256');
|
|
36
|
+
expect(u.searchParams.get('scope')).toBe('openid email profile');
|
|
37
|
+
});
|
|
38
|
+
it('preserves the loopback port in redirect_uri (RFC 8252 any-port)', () => {
|
|
39
|
+
const pkce = generatePkce();
|
|
40
|
+
const u = new URL(buildAuthorizeUrl('http://127.0.0.1:1234/authorize', 'http://127.0.0.1:65530/callback', pkce));
|
|
41
|
+
expect(u.searchParams.get('redirect_uri')).toContain(':65530');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { FlagSchema } from '../flags.js';
|
|
2
|
+
import type { Flags } from '../helpers.js';
|
|
3
|
+
import type { Exposes } from '../exposes.js';
|
|
4
|
+
export declare const EXPOSES: Exposes;
|
|
5
|
+
export declare const SCHEMA: FlagSchema;
|
|
6
|
+
export declare const HELP = "Usage: myapi login [--no-browser] [--mock]\n\nSigns in to MyAPI in your browser \u2014 Google, or an email code \u2014 and stores the\nsession in ~/.myapi/config.json. The account created/linked here is the same\naccount model as `myapi account setup`; anonymous setup stays available and\nunchanged.\n\nFlags:\n --no-browser Print the sign-in URL instead of opening a browser\n --mock Run the flow against an in-process mock (offline UX preview;\n nothing is saved, no network calls leave your machine)";
|
|
7
|
+
export interface Pkce {
|
|
8
|
+
verifier: string;
|
|
9
|
+
challenge: string;
|
|
10
|
+
state: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function generatePkce(): Pkce;
|
|
13
|
+
export declare function buildAuthorizeUrl(base: string, redirectUri: string, pkce: Pkce): string;
|
|
14
|
+
export declare function login(flags?: Flags): Promise<void>;
|