@myapihq/cli 2.3.1 → 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 +14 -10
- 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-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',
|
|
@@ -142,13 +143,13 @@ async function complete(promptArg, flags) {
|
|
|
142
143
|
const stop = typeof flags.stop === 'string' && flags.stop
|
|
143
144
|
? flags.stop.split(',').map(s => s.trim()).filter(Boolean)
|
|
144
145
|
: undefined;
|
|
145
|
-
const res = await sdkLlm.complete(config.api_key, orgId, {
|
|
146
|
+
const res = await retryFunds(() => sdkLlm.complete(config.api_key, orgId, {
|
|
146
147
|
model,
|
|
147
148
|
messages,
|
|
148
149
|
max_tokens: typeof flags['max-tokens'] === 'number' ? flags['max-tokens'] : undefined,
|
|
149
150
|
temperature: typeof flags.temperature === 'number' ? flags.temperature : undefined,
|
|
150
151
|
stop,
|
|
151
|
-
});
|
|
152
|
+
}));
|
|
152
153
|
if (flags.json) {
|
|
153
154
|
printJson(res);
|
|
154
155
|
return;
|
|
@@ -164,7 +165,7 @@ async function embed(inputArg, flags) {
|
|
|
164
165
|
const text = readPromptArg(inputArg, flags);
|
|
165
166
|
if (!text.trim())
|
|
166
167
|
error('Empty input. Pass <text> as an argument, "-" to read stdin, or --file <path>.');
|
|
167
|
-
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 }));
|
|
168
169
|
if (flags.json) {
|
|
169
170
|
printJson(res);
|
|
170
171
|
return;
|
|
@@ -211,7 +212,7 @@ async function classify(inputArg, flags) {
|
|
|
211
212
|
if (labels.length === 0)
|
|
212
213
|
error('--labels must contain at least one label');
|
|
213
214
|
const multi = flags.multi === true;
|
|
214
|
-
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) }));
|
|
215
216
|
if (flags.json) {
|
|
216
217
|
printJson(res);
|
|
217
218
|
return;
|
|
@@ -236,7 +237,7 @@ async function extract(inputArg, flags) {
|
|
|
236
237
|
error('Missing required flag: --schema <path|json>');
|
|
237
238
|
return;
|
|
238
239
|
}
|
|
239
|
-
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) }));
|
|
240
241
|
if (flags.json) {
|
|
241
242
|
printJson(res);
|
|
242
243
|
return;
|
|
@@ -254,11 +255,11 @@ async function summarize(inputArg, flags) {
|
|
|
254
255
|
if (style && !['brief', 'exec', 'bullet'].includes(style)) {
|
|
255
256
|
error('--style must be one of: brief, exec, bullet');
|
|
256
257
|
}
|
|
257
|
-
const res = await sdkLlm.summarize(config.api_key, orgId, {
|
|
258
|
+
const res = await retryFunds(() => sdkLlm.summarize(config.api_key, orgId, {
|
|
258
259
|
input,
|
|
259
260
|
style: style,
|
|
260
261
|
tier: tierFromFlag(flags),
|
|
261
|
-
});
|
|
262
|
+
}));
|
|
262
263
|
if (flags.json) {
|
|
263
264
|
printJson(res);
|
|
264
265
|
return;
|
|
@@ -273,19 +274,22 @@ async function draft(inputArg, flags) {
|
|
|
273
274
|
error('Missing required flag: --kind <email|message|reply|...>');
|
|
274
275
|
return;
|
|
275
276
|
}
|
|
277
|
+
// Capture the narrowed value — typeof narrowing doesn't survive into the
|
|
278
|
+
// retryFunds closure below.
|
|
279
|
+
const kind = flags.kind;
|
|
276
280
|
const input = readPromptArg(inputArg, flags);
|
|
277
281
|
const promptText = typeof flags.prompt === 'string' ? flags.prompt : '';
|
|
278
282
|
const ctx = parseContextFlag(flags);
|
|
279
283
|
if (!input.trim() && !promptText.trim() && (!ctx || Object.keys(ctx).length === 0)) {
|
|
280
284
|
error('draft needs at least one of: <source text> (arg or --file), --prompt, or --context.');
|
|
281
285
|
}
|
|
282
|
-
const res = await sdkLlm.draft(config.api_key, orgId, {
|
|
286
|
+
const res = await retryFunds(() => sdkLlm.draft(config.api_key, orgId, {
|
|
283
287
|
input: input || undefined,
|
|
284
|
-
kind
|
|
288
|
+
kind,
|
|
285
289
|
context: ctx,
|
|
286
290
|
prompt: promptText || undefined,
|
|
287
291
|
tier: tierFromFlag(flags),
|
|
288
|
-
});
|
|
292
|
+
}));
|
|
289
293
|
if (flags.json) {
|
|
290
294
|
printJson(res);
|
|
291
295
|
return;
|
|
@@ -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>;
|