@myapihq/cli 2.7.2 → 2.9.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/container.d.ts +1 -0
- package/dist/commands/container.js +69 -47
- package/dist/commands/feedback.d.ts +10 -0
- package/dist/commands/feedback.js +174 -0
- package/dist/commands/flag-reachability.test.d.ts +1 -0
- package/dist/commands/flag-reachability.test.js +287 -0
- package/dist/commands/llm.d.ts +1 -0
- package/dist/commands/llm.js +25 -9
- package/dist/completion.js +2 -1
- package/dist/exposes.test.js +1 -0
- package/dist/index.js +7 -0
- package/dist/skills/my-function-api/SKILL.md +7 -1
- package/dist/skills/my-llm-api/SKILL.md +17 -4
- package/dist/skills/my-storage-api/SKILL.md +12 -6
- package/package.json +6 -2
|
@@ -17,6 +17,7 @@ export declare function deploy(id: string, image: string, flags: Flags): Promise
|
|
|
17
17
|
export declare function _parseSmoke(raw: string): sdkContainer.SmokeCheck;
|
|
18
18
|
export declare function revisions(id: string, flags: Flags): Promise<void>;
|
|
19
19
|
export declare function promote(id: string, revision: string | undefined, flags: Flags): Promise<void>;
|
|
20
|
+
export declare function rollback(id: string, flags: Flags): Promise<void>;
|
|
20
21
|
export declare function logs(id: string, flags: Flags): Promise<void>;
|
|
21
22
|
export declare function domain(id: string, domainArg: string | undefined, flags: Flags): Promise<void>;
|
|
22
23
|
export declare function buildLogs(id: string | undefined, flags: Flags): Promise<void>;
|
|
@@ -239,17 +239,16 @@ export async function deploy(id, image, flags) {
|
|
|
239
239
|
// write their own; a guard that silently passes makes them stop. Restore
|
|
240
240
|
// these the moment the upstream fix lands — see
|
|
241
241
|
// docs/cross-repo-prompts/backend-consolidated-2026-07-28.md.
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
'
|
|
248
|
-
'
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
'Reported upstream; this message goes away when the flag works.');
|
|
242
|
+
// --smoke is still not honoured; --no-promote was fixed upstream on
|
|
243
|
+
// 2026-07-28 and works on both deploy paths. Refusing what does not work
|
|
244
|
+
// beats accepting it, and un-refusing what does beats obstructing.
|
|
245
|
+
if (typeof flags.smoke === 'string') {
|
|
246
|
+
error('--smoke is not honoured yet, so this CLI refuses it rather than letting you believe a deploy was checked.\n\n' +
|
|
247
|
+
'Use --no-promote instead, which now works:\n' +
|
|
248
|
+
' 1. myapi container deploy <id> <image> --no-promote\n' +
|
|
249
|
+
' 2. curl the revision URL it prints, for a string only a real build emits\n' +
|
|
250
|
+
' 3. myapi container promote <id> <revision>\n\n' +
|
|
251
|
+
'That is the same verify-then-promote, done by you rather than the platform.');
|
|
253
252
|
}
|
|
254
253
|
const source = typeof flags.source === 'string' ? flags.source : undefined;
|
|
255
254
|
// --image is an alias for the positional image ref.
|
|
@@ -258,6 +257,11 @@ export async function deploy(id, image, flags) {
|
|
|
258
257
|
if (source && image) {
|
|
259
258
|
error('Pass either an image ref or --source, not both.');
|
|
260
259
|
}
|
|
260
|
+
// Built once, passed to BOTH branches. The original bug was building these
|
|
261
|
+
// inside the image branch only, so a --source deploy dropped them silently.
|
|
262
|
+
const deployOpts = {};
|
|
263
|
+
if (flags['no-promote'] === true)
|
|
264
|
+
deployOpts.promote = false;
|
|
261
265
|
// ── Source-build path (async) ───────────────────────────────────────────
|
|
262
266
|
if (source) {
|
|
263
267
|
let tarball;
|
|
@@ -281,7 +285,7 @@ export async function deploy(id, image, flags) {
|
|
|
281
285
|
else {
|
|
282
286
|
error(`--source must be a directory or a .tar/.tar.gz/.tgz archive — got ${source}`);
|
|
283
287
|
}
|
|
284
|
-
const start = await sdkContainer.deployContainerSource(config.api_key, orgId, id, tarball, filename);
|
|
288
|
+
const start = await sdkContainer.deployContainerSource(config.api_key, orgId, id, tarball, filename, deployOpts);
|
|
285
289
|
if (flags.json && start.status !== 'building') {
|
|
286
290
|
printJson(start);
|
|
287
291
|
return;
|
|
@@ -320,10 +324,7 @@ export async function deploy(id, image, flags) {
|
|
|
320
324
|
// ── Pre-built image path (sync) ─────────────────────────────────────────
|
|
321
325
|
if (!image)
|
|
322
326
|
error('Missing image ref.\nUsage: myapi container deploy <id> <image-ref>\n or: myapi container deploy <id> --source <dir|tar>\n\n→ <image-ref> is a pre-built container image (e.g. a registry path).');
|
|
323
|
-
|
|
324
|
-
// until the upstream fix lands. The SDK still carries them so the wiring is
|
|
325
|
-
// one commit away, and sdk-container.test.ts keeps them covered.
|
|
326
|
-
const result = await sdkContainer.deployContainer(config.api_key, orgId, id, image);
|
|
327
|
+
const result = await sdkContainer.deployContainer(config.api_key, orgId, id, image, deployOpts);
|
|
327
328
|
if (flags.json) {
|
|
328
329
|
printJson(result);
|
|
329
330
|
return;
|
|
@@ -331,9 +332,18 @@ export async function deploy(id, image, flags) {
|
|
|
331
332
|
// An unpromoted revision must NOT read like a completed deploy. A response
|
|
332
333
|
// that looked the same either way is how an agent concludes it has shipped
|
|
333
334
|
// when it has not — the original outage in miniature.
|
|
334
|
-
//
|
|
335
|
-
//
|
|
336
|
-
//
|
|
335
|
+
// Read `promoted` rather than assuming --no-promote was honoured. On a
|
|
336
|
+
// container's FIRST deploy there is nothing already serving to hold traffic,
|
|
337
|
+
// so the platform promotes anyway and says so — reporting "not serving"
|
|
338
|
+
// there would be the lie this flag exists to prevent.
|
|
339
|
+
if (flags['no-promote'] === true && result.promoted !== false) {
|
|
340
|
+
success(`Deployed container ${id} (revision ${result.revision_id})`);
|
|
341
|
+
info('');
|
|
342
|
+
info('Note: --no-promote was NOT applied. This container had nothing already');
|
|
343
|
+
info('serving, so withholding traffic would have left it answering nothing.');
|
|
344
|
+
info(`URL: ${result.url}`);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
337
347
|
if (result.promoted === false) {
|
|
338
348
|
success(`Revision ${result.revision_id} built — NOT serving traffic`);
|
|
339
349
|
info(`Test it: ${result.revision_url ?? '(no revision URL returned)'}`);
|
|
@@ -417,13 +427,14 @@ export async function revisions(id, flags) {
|
|
|
417
427
|
// state.
|
|
418
428
|
//
|
|
419
429
|
// Say so rather than render a table that reads as "nothing is live".
|
|
430
|
+
// The 0%-everywhere reporting bug was fixed upstream on 2026-07-28 (a v2
|
|
431
|
+
// traffic target of type LATEST carries no revision name, so 100% was filed
|
|
432
|
+
// under ""). Keeping a narrower check: all-zero on an active container is
|
|
433
|
+
// still worth flagging, it is just no longer expected.
|
|
420
434
|
if (revs.length > 0 && revs.every(r => !r.serving && !r.traffic_percent) && container?.status === 'active') {
|
|
421
435
|
info('');
|
|
422
|
-
info('Note:
|
|
423
|
-
info('
|
|
424
|
-
info('today, so this is not limited to older ones — an earlier version of this message');
|
|
425
|
-
info('said redeploying fixes it, which was wrong.');
|
|
426
|
-
info('promote depends on this data and currently fails. Reported upstream.');
|
|
436
|
+
info('Note: no revision reports any traffic while this container is active.');
|
|
437
|
+
info('That should not happen — report it rather than trusting the column.');
|
|
427
438
|
}
|
|
428
439
|
}
|
|
429
440
|
// promote moves all traffic to one revision. Omitting the revision rolls back
|
|
@@ -447,16 +458,25 @@ export async function promote(id, revision, flags) {
|
|
|
447
458
|
if (res.message)
|
|
448
459
|
info(res.message);
|
|
449
460
|
}
|
|
450
|
-
//
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
'
|
|
459
|
-
|
|
461
|
+
// rollback moves traffic to the previous ready revision. The API models it as
|
|
462
|
+
// `promote` with no revision named — one operation, two targets — but
|
|
463
|
+
// `rollback` is the word someone types during an incident, so it is a verb
|
|
464
|
+
// here even though the SDK has a single function.
|
|
465
|
+
export async function rollback(id, flags) {
|
|
466
|
+
const config = requireConfig();
|
|
467
|
+
const orgId = requireOrg(flags, config, 'myapi container rollback <id> [--org <id>]');
|
|
468
|
+
if (!id)
|
|
469
|
+
error('Missing id.\nUsage: myapi container rollback <id>');
|
|
470
|
+
const res = await sdkContainer.promoteRevision(config.api_key, orgId, id);
|
|
471
|
+
if (flags.json) {
|
|
472
|
+
printJson(res);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
success(`Rolled back ${id} to the previous ready revision`);
|
|
476
|
+
if (res.serving)
|
|
477
|
+
info(`Now serving: ${res.serving}`);
|
|
478
|
+
if (res.message)
|
|
479
|
+
info(res.message);
|
|
460
480
|
}
|
|
461
481
|
// logs prints the container's recent runtime logs, newest first. By default
|
|
462
482
|
// this is the container's own stdout/stderr; --scope all adds the platform
|
|
@@ -565,19 +585,25 @@ Two ways to deploy:
|
|
|
565
585
|
built server-side (typically ~4 minutes), then deployed.
|
|
566
586
|
Asynchronous — the CLI polls until it's live.
|
|
567
587
|
|
|
568
|
-
|
|
588
|
+
Options:
|
|
589
|
+
--no-promote Build the revision without giving it traffic, on either
|
|
590
|
+
deploy path. Prints a revision URL to test at, then:
|
|
591
|
+
myapi container promote <id> <revision>
|
|
569
592
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
A guard that silently passes is worse than no guard.
|
|
593
|
+
On a container's FIRST deploy there is nothing already
|
|
594
|
+
serving, so traffic is NOT withheld and the output says so.
|
|
573
595
|
|
|
574
|
-
|
|
575
|
-
|
|
596
|
+
--smoke Still refused — the platform accepts it and does nothing.
|
|
597
|
+
--no-promote gives you the same verify-then-promote by hand.
|
|
576
598
|
|
|
577
599
|
Examples:
|
|
578
600
|
myapi container deploy <id> registry.example.com/my-app:v2
|
|
579
601
|
myapi container deploy <id> --source ./my-app
|
|
580
|
-
myapi container deploy <id> --
|
|
602
|
+
myapi container deploy <id> <image> --no-promote`,
|
|
603
|
+
'rollback': `myapi container rollback <id> [--org <id>] [--json]
|
|
604
|
+
|
|
605
|
+
Move traffic back to the previous ready revision. Seconds, no rebuild.
|
|
606
|
+
Refuses rather than guessing when it cannot tell what is serving.`,
|
|
581
607
|
'revisions': `myapi container revisions <id> [--org <id>] [--json]
|
|
582
608
|
|
|
583
609
|
Every revision the runtime currently holds, newest first, with the traffic
|
|
@@ -629,6 +655,7 @@ Subcommands:
|
|
|
629
655
|
logs <id> Show recent runtime logs (--tail <n>, --scope all) — see build-logs for build failures
|
|
630
656
|
promote <id> <rev> Move all traffic to a revision (seconds, no rebuild)
|
|
631
657
|
revisions <id> List revisions and the traffic each takes
|
|
658
|
+
rollback <id> Move traffic back to the previous ready revision
|
|
632
659
|
|
|
633
660
|
All commands accept --org <id> (or set default: myapi config set-org <id>).`);
|
|
634
661
|
return;
|
|
@@ -652,12 +679,7 @@ All commands accept --org <id> (or set default: myapi config set-org <id>).`);
|
|
|
652
679
|
case 'promote': return promote(args[0], args[1], flags);
|
|
653
680
|
case 'domain': return domain(args[0], args[1], flags);
|
|
654
681
|
case 'delete': return del(args[0], flags);
|
|
655
|
-
|
|
656
|
-
// NOT a verb here — the API models rollback as `promote` with no revision.
|
|
657
|
-
// A bare "unknown subcommand" would cost minutes at the worst possible
|
|
658
|
-
// moment, so say what to do instead, and be honest that the underlying
|
|
659
|
-
// call is currently broken rather than let someone discover that live.
|
|
660
|
-
case 'rollback': return rollbackGuidance(args[0]);
|
|
682
|
+
case 'rollback': return rollback(args[0], flags);
|
|
661
683
|
default: error(`Unknown subcommand: ${subcommand}. Run "myapi container --help" for a list of valid subcommands.`);
|
|
662
684
|
}
|
|
663
685
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
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 function list(flags: Flags): Promise<void>;
|
|
7
|
+
export declare function create(bodyArg: string | undefined, flags: Flags): Promise<void>;
|
|
8
|
+
export declare function resolve(id: string, flags: Flags): Promise<void>;
|
|
9
|
+
export declare function widget(sub: string | undefined, arg: string | undefined, flags: Flags): Promise<void>;
|
|
10
|
+
export declare function run(subcommand: string | undefined, args: string[], flags: Flags): Promise<void>;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// `myapi feedback` — collect what the people using your product tell you.
|
|
2
|
+
//
|
|
3
|
+
// The slot shipped on 2026-07-28 with five endpoints and no CLI. A primitive
|
|
4
|
+
// with no surface is one nobody finds, which is the failure this repo spent a
|
|
5
|
+
// week fixing: two customers concluded a shipped capability did not exist
|
|
6
|
+
// because nothing they read mentioned it.
|
|
7
|
+
import { feedback as sdkFeedback } from '@myapihq/sdk';
|
|
8
|
+
import { requireConfig } from '../config.js';
|
|
9
|
+
import { success, error, info, printTable, printJson, banner } from '../output.js';
|
|
10
|
+
import { formatDate } from '../utils.js';
|
|
11
|
+
import { requireOrg, requireArg, confirmDestructive } from '../helpers.js';
|
|
12
|
+
export const EXPOSES = [
|
|
13
|
+
'GET /feedback/orgs/{org_id}/items',
|
|
14
|
+
'POST /feedback/orgs/{org_id}/items',
|
|
15
|
+
'POST /feedback/orgs/{org_id}/items/{id}/resolve',
|
|
16
|
+
'POST /feedback/orgs/{org_id}/widgets',
|
|
17
|
+
'DELETE /feedback/orgs/{org_id}/widgets/{id}',
|
|
18
|
+
];
|
|
19
|
+
export const SCHEMA = {
|
|
20
|
+
kind: 'string',
|
|
21
|
+
status: 'string',
|
|
22
|
+
body: 'string',
|
|
23
|
+
'page-url': 'string',
|
|
24
|
+
route: 'string',
|
|
25
|
+
origins: 'string',
|
|
26
|
+
};
|
|
27
|
+
// The platform's enum, checked against the schema rather than invented.
|
|
28
|
+
const KINDS = ['bug', 'issue', 'suggestion'];
|
|
29
|
+
export async function list(flags) {
|
|
30
|
+
const config = requireConfig();
|
|
31
|
+
const orgId = requireOrg(flags, config, 'myapi feedback list [--kind <k>] [--status open|resolved] [--org <id>]');
|
|
32
|
+
if (flags.kind !== undefined && !KINDS.includes(flags.kind)) {
|
|
33
|
+
error(`Invalid --kind "${flags.kind}". Use one of: ${KINDS.join(', ')}.`);
|
|
34
|
+
}
|
|
35
|
+
if (flags.status !== undefined && flags.status !== 'open' && flags.status !== 'resolved') {
|
|
36
|
+
error(`Invalid --status "${flags.status}". Use "open" or "resolved".`);
|
|
37
|
+
}
|
|
38
|
+
const page = await sdkFeedback.listFeedback(config.api_key, orgId, {
|
|
39
|
+
kind: flags.kind,
|
|
40
|
+
status: flags.status,
|
|
41
|
+
limit: typeof flags.limit === 'number' ? flags.limit : undefined,
|
|
42
|
+
offset: typeof flags.offset === 'number' ? flags.offset : undefined,
|
|
43
|
+
});
|
|
44
|
+
if (flags.json) {
|
|
45
|
+
printJson(page);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
// `total` here is the match count, not the page size — the platform states
|
|
49
|
+
// that explicitly, having been bitten by the opposite in CRM search.
|
|
50
|
+
const shown = page.items?.length ?? 0;
|
|
51
|
+
info(shown === page.total ? `${shown} item${shown === 1 ? '' : 's'}` : `${shown} of ${page.total} items`);
|
|
52
|
+
printTable((page.items ?? []).map(i => ({
|
|
53
|
+
id: i.id,
|
|
54
|
+
kind: i.kind,
|
|
55
|
+
status: i.status,
|
|
56
|
+
body: i.body.length > 60 ? `${i.body.slice(0, 57)}…` : i.body,
|
|
57
|
+
route: i.route ?? i.page_url ?? '',
|
|
58
|
+
created: i.created_at ? formatDate(i.created_at) : '',
|
|
59
|
+
})), { flags, empty: 'No feedback yet. Put a widget on a page: myapi feedback widget create <name>' });
|
|
60
|
+
if (page.has_more)
|
|
61
|
+
info('More available — raise --limit or pass --offset.');
|
|
62
|
+
}
|
|
63
|
+
export async function create(bodyArg, flags) {
|
|
64
|
+
const config = requireConfig();
|
|
65
|
+
const orgId = requireOrg(flags, config, 'myapi feedback create "<text>" --kind <k> [--org <id>]');
|
|
66
|
+
const body = bodyArg ?? flags.body;
|
|
67
|
+
requireArg(body, 'text', 'myapi feedback create "<text>" --kind bug');
|
|
68
|
+
const kind = flags.kind ?? 'issue';
|
|
69
|
+
if (!KINDS.includes(kind)) {
|
|
70
|
+
error(`Invalid --kind "${kind}". Use one of: ${KINDS.join(', ')}.\n\n→ Kind is what the PERSON says it is. "bug" is a claim that the product is broken; do not infer it from the wording.`);
|
|
71
|
+
}
|
|
72
|
+
const item = await sdkFeedback.createFeedback(config.api_key, orgId, {
|
|
73
|
+
kind: kind,
|
|
74
|
+
body: body,
|
|
75
|
+
page_url: typeof flags['page-url'] === 'string' ? flags['page-url'] : undefined,
|
|
76
|
+
route: typeof flags.route === 'string' ? flags.route : undefined,
|
|
77
|
+
});
|
|
78
|
+
if (flags.json) {
|
|
79
|
+
printJson(item);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
success(`Recorded ${item.kind}: ${item.id}`);
|
|
83
|
+
}
|
|
84
|
+
export async function resolve(id, flags) {
|
|
85
|
+
const config = requireConfig();
|
|
86
|
+
const orgId = requireOrg(flags, config, 'myapi feedback resolve <id> [--org <id>]');
|
|
87
|
+
if (!id)
|
|
88
|
+
error('Missing id.\nUsage: myapi feedback resolve <id>');
|
|
89
|
+
await sdkFeedback.resolveFeedback(config.api_key, orgId, id);
|
|
90
|
+
// Already-resolved and not-found answer identically, so this is not proof
|
|
91
|
+
// the id existed. Say so rather than implying a state change happened.
|
|
92
|
+
success(`Resolved ${id}`);
|
|
93
|
+
info('(An unknown id answers the same way, so this is not confirmation the item existed.)');
|
|
94
|
+
}
|
|
95
|
+
export async function widget(sub, arg, flags) {
|
|
96
|
+
const config = requireConfig();
|
|
97
|
+
const orgId = requireOrg(flags, config, 'myapi feedback widget create <name> | revoke <id>');
|
|
98
|
+
if (sub === 'create') {
|
|
99
|
+
requireArg(arg, 'name', 'myapi feedback widget create <name> [--origins a.com,b.com]');
|
|
100
|
+
const origins = typeof flags.origins === 'string'
|
|
101
|
+
? flags.origins.split(',').map(s => s.trim()).filter(Boolean)
|
|
102
|
+
: undefined;
|
|
103
|
+
const w = await sdkFeedback.createWidget(config.api_key, orgId, arg, origins);
|
|
104
|
+
if (flags.json) {
|
|
105
|
+
printJson(w);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
success(`Widget created: ${w.id}`);
|
|
109
|
+
info(`Key: ${w.key}`);
|
|
110
|
+
info('');
|
|
111
|
+
// Saying this plainly matters: a value that looks like a credential and is
|
|
112
|
+
// not one gets treated as a secret, and then nobody puts it in the page.
|
|
113
|
+
info('This key is PUBLIC. It ships in your page source and authenticates nobody —');
|
|
114
|
+
info('it names your org so a visitor can submit without signing in.');
|
|
115
|
+
if (!origins?.length) {
|
|
116
|
+
banner('No --origins set, so any site can post through this key. Set them unless that is intended.');
|
|
117
|
+
}
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (sub === 'revoke') {
|
|
121
|
+
requireArg(arg, 'id', 'myapi feedback widget revoke <id>');
|
|
122
|
+
await confirmDestructive(flags, `revoke widget ${arg} (submissions with it stop immediately)`, 'myapi feedback widget revoke <id> [--yes] [--org <id>]');
|
|
123
|
+
await sdkFeedback.revokeWidget(config.api_key, orgId, arg);
|
|
124
|
+
success(`Widget ${arg} revoked`);
|
|
125
|
+
info('Feedback already collected through it is kept.');
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
error('Usage: myapi feedback widget create <name> [--origins <list>]\n myapi feedback widget revoke <id>');
|
|
129
|
+
}
|
|
130
|
+
const SUBCOMMAND_USAGE = {
|
|
131
|
+
'list': `myapi feedback list [--kind bug|issue|suggestion] [--status open|resolved]
|
|
132
|
+
[--limit N] [--offset N] [--org <id>] [--json]
|
|
133
|
+
|
|
134
|
+
Newest first. \`total\` is the number of matches, not the page size.`,
|
|
135
|
+
'create': `myapi feedback create "<text>" --kind <k> [--page-url <url>] [--route <path>] [--org <id>]
|
|
136
|
+
|
|
137
|
+
--kind is what the person reporting says it is, not what the text sounds like.`,
|
|
138
|
+
'resolve': 'myapi feedback resolve <id> [--org <id>]',
|
|
139
|
+
'widget': `myapi feedback widget create <name> [--origins a.com,b.com] [--org <id>]
|
|
140
|
+
myapi feedback widget revoke <id> [--yes] [--org <id>]
|
|
141
|
+
|
|
142
|
+
The key a widget mints is PUBLIC — it ships in page source and authenticates
|
|
143
|
+
nobody. --origins stops another site posting through it.`,
|
|
144
|
+
};
|
|
145
|
+
export async function run(subcommand, args, flags) {
|
|
146
|
+
if (!subcommand || (flags.help && !subcommand)) {
|
|
147
|
+
info(`Usage: myapi feedback <subcommand>
|
|
148
|
+
|
|
149
|
+
Collect feedback from the people using what you built. A widget key lets a
|
|
150
|
+
page submit without a credential; you list, filter and resolve the results.
|
|
151
|
+
|
|
152
|
+
Subcommands:
|
|
153
|
+
create "<text>" Record one piece of feedback (--kind bug|issue|suggestion)
|
|
154
|
+
list List feedback, newest first (--kind, --status, --limit, --offset)
|
|
155
|
+
resolve <id> Close a piece of feedback
|
|
156
|
+
widget create <name> Mint a PUBLIC widget key for a site (--origins to restrict)
|
|
157
|
+
widget revoke <id> Revoke a widget key; collected feedback is kept
|
|
158
|
+
|
|
159
|
+
All commands accept --org <id> (or set default: myapi config set-org <id>).`);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (flags.help) {
|
|
163
|
+
const usage = SUBCOMMAND_USAGE[subcommand];
|
|
164
|
+
info(usage ? `Usage: ${usage}` : `Unknown subcommand: ${subcommand}. Run "myapi feedback --help" for the list.`);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
switch (subcommand) {
|
|
168
|
+
case 'create': return create(args[0], flags);
|
|
169
|
+
case 'list': return list(flags);
|
|
170
|
+
case 'resolve': return resolve(args[0], flags);
|
|
171
|
+
case 'widget': return widget(args[0], args[1], flags);
|
|
172
|
+
default: error(`Unknown subcommand: ${subcommand}. Run "myapi feedback --help" for a list of valid subcommands.`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
// Does the flag REACH the call, or does it only parse?
|
|
2
|
+
//
|
|
3
|
+
// This file exists because of a bug we shipped and a customer found.
|
|
4
|
+
//
|
|
5
|
+
// `container deploy --no-promote` was wired to the pre-built-image path and
|
|
6
|
+
// not to the `--source` path. `deployContainerSource()` was called without the
|
|
7
|
+
// options object, so on a source build the flag was accepted and silently
|
|
8
|
+
// discarded. Both flags were declared in the command schema, so the
|
|
9
|
+
// foreign-flag check stayed quiet too. A team deployed with it, the build took
|
|
10
|
+
// 100% of traffic anyway, and they reported it.
|
|
11
|
+
//
|
|
12
|
+
// The tests we had at the time all passed. They tested `_parseSmoke` (a pure
|
|
13
|
+
// parser) and `deployContainer` (the SDK function). Nothing tested the CLI
|
|
14
|
+
// handler, so nothing noticed that one of its two branches never passed the
|
|
15
|
+
// options along. The backend hit the identical shape the same day and put it
|
|
16
|
+
// better than we can:
|
|
17
|
+
//
|
|
18
|
+
// "Counting call sites proves the helper is CALLED, not that it is REACHED."
|
|
19
|
+
//
|
|
20
|
+
// Ours is: testing the parser proves the flag PARSES, not that it is SENT.
|
|
21
|
+
//
|
|
22
|
+
// So this file tests HANDLERS, with the SDK mocked, asserting what actually
|
|
23
|
+
// arrives at the boundary. Every case below is a real field report. When you
|
|
24
|
+
// add a flag that changes a request, add a case here — a unit test on its
|
|
25
|
+
// parser is not cover.
|
|
26
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
27
|
+
const ORG = '11111111-1111-4111-8111-111111111111';
|
|
28
|
+
// The SDK is mocked wholesale so we can see exactly what the handler passes.
|
|
29
|
+
// Typed loosely on purpose: the later describes assign whole slot objects
|
|
30
|
+
// (sdk.task = {...}) and TS would otherwise reject each one. The assertions
|
|
31
|
+
// still check real shapes.
|
|
32
|
+
const sdk = vi.hoisted(() => ({
|
|
33
|
+
container: {
|
|
34
|
+
deployContainer: vi.fn(),
|
|
35
|
+
deployContainerSource: vi.fn(),
|
|
36
|
+
getContainerLogs: vi.fn(),
|
|
37
|
+
listContainers: vi.fn(),
|
|
38
|
+
getContainer: vi.fn(),
|
|
39
|
+
listRevisions: vi.fn(),
|
|
40
|
+
promoteRevision: vi.fn(),
|
|
41
|
+
createContainer: vi.fn(),
|
|
42
|
+
},
|
|
43
|
+
crm: { searchContacts: vi.fn(), searchCompanies: vi.fn() },
|
|
44
|
+
fn: { listFunctions: vi.fn() },
|
|
45
|
+
// Top-level SDK exports the handlers reach for. Mocking the module
|
|
46
|
+
// wholesale drops anything not listed, and the failure reads as a missing
|
|
47
|
+
// export rather than a missing mock — so they are enumerated explicitly.
|
|
48
|
+
withFundsRetry: vi.fn(async (f) => f()),
|
|
49
|
+
MyApiError: class MyApiError extends Error {
|
|
50
|
+
code = '';
|
|
51
|
+
status = 0;
|
|
52
|
+
},
|
|
53
|
+
}));
|
|
54
|
+
vi.mock('@myapihq/sdk', () => sdk);
|
|
55
|
+
vi.mock('../config.js', () => ({
|
|
56
|
+
requireConfig: () => ({ api_key: 'hq_live_test', default_org: ORG }),
|
|
57
|
+
loadConfig: () => ({ api_key: 'hq_live_test', default_org: ORG }),
|
|
58
|
+
CONFIG_DIR: '/tmp/nowhere',
|
|
59
|
+
}));
|
|
60
|
+
let exitError;
|
|
61
|
+
beforeEach(async () => {
|
|
62
|
+
exitError = null;
|
|
63
|
+
vi.clearAllMocks();
|
|
64
|
+
const output = await import('../output.js');
|
|
65
|
+
vi.spyOn(output, 'error').mockImplementation(((m) => {
|
|
66
|
+
exitError = m;
|
|
67
|
+
throw new Error('__EXIT__');
|
|
68
|
+
}));
|
|
69
|
+
vi.spyOn(output, 'info').mockImplementation(() => { });
|
|
70
|
+
vi.spyOn(output, 'success').mockImplementation(() => { });
|
|
71
|
+
vi.spyOn(output, 'printTable').mockImplementation(() => { });
|
|
72
|
+
vi.spyOn(output, 'printJson').mockImplementation(() => { });
|
|
73
|
+
vi.spyOn(output, 'banner').mockImplementation(() => { });
|
|
74
|
+
});
|
|
75
|
+
afterEach(() => vi.restoreAllMocks());
|
|
76
|
+
// Runs a handler and swallows the synthetic exit thrown by a mocked error().
|
|
77
|
+
async function run(fn) {
|
|
78
|
+
try {
|
|
79
|
+
await fn();
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
if (e?.message !== '__EXIT__')
|
|
83
|
+
throw e;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
describe('container deploy — --no-promote must REACH both paths', () => {
|
|
87
|
+
// This is the original bug in its final form. --no-promote was wired to the
|
|
88
|
+
// image branch only, so a --source deploy accepted it and dropped it, and
|
|
89
|
+
// the build took 100% of traffic. The platform has since fixed its side and
|
|
90
|
+
// the flag works on both paths — so the assertion flips from "is refused" to
|
|
91
|
+
// "arrives", and the bug it guards is the same one either way.
|
|
92
|
+
const DEPLOYED = {
|
|
93
|
+
container_id: 'c1', revision_id: 'r1', url: 'https://x',
|
|
94
|
+
status: 'active', scoped_api_key: 'k', promoted: false,
|
|
95
|
+
revision_url: 'https://rev---x.run.app',
|
|
96
|
+
};
|
|
97
|
+
it('sends promote:false on the image path', async () => {
|
|
98
|
+
sdk.container.deployContainer.mockResolvedValue(DEPLOYED);
|
|
99
|
+
const { deploy } = await import('./container.js');
|
|
100
|
+
await run(() => deploy('c1', 'img:v1', { 'no-promote': true, org: ORG }));
|
|
101
|
+
expect(sdk.container.deployContainer).toHaveBeenCalledWith('hq_live_test', ORG, 'c1', 'img:v1', { promote: false });
|
|
102
|
+
});
|
|
103
|
+
// The branch that shipped broken. It reads a real tarball, so the assertion
|
|
104
|
+
// is that the options object reaches the call — argument 6.
|
|
105
|
+
it('sends promote:false on the --source path', async () => {
|
|
106
|
+
sdk.container.deployContainerSource.mockResolvedValue({ container_id: 'c1', revision_id: 'r1', status: 'building' });
|
|
107
|
+
sdk.container.getContainer.mockResolvedValue({ id: 'c1', status: 'active', url: 'https://x' });
|
|
108
|
+
const { _isTarball } = await import('./container.js');
|
|
109
|
+
void _isTarball;
|
|
110
|
+
const { deploy } = await import('./container.js');
|
|
111
|
+
const fsp = await import('node:fs/promises');
|
|
112
|
+
const tmp = `${process.env.TMPDIR ?? '/tmp'}/reach-${Date.now()}.tar.gz`;
|
|
113
|
+
await fsp.writeFile(tmp, 'not-a-real-tarball');
|
|
114
|
+
await run(() => deploy('c1', '', { source: tmp, 'no-promote': true, org: ORG }));
|
|
115
|
+
await fsp.rm(tmp, { force: true });
|
|
116
|
+
expect(sdk.container.deployContainerSource).toHaveBeenCalled();
|
|
117
|
+
expect(sdk.container.deployContainerSource.mock.calls[0][5]).toEqual({ promote: false });
|
|
118
|
+
});
|
|
119
|
+
it('sends no options at all when the flag is absent', async () => {
|
|
120
|
+
sdk.container.deployContainer.mockResolvedValue({ ...DEPLOYED, promoted: true });
|
|
121
|
+
const { deploy } = await import('./container.js');
|
|
122
|
+
await run(() => deploy('c1', 'img:v1', { org: ORG }));
|
|
123
|
+
expect(sdk.container.deployContainer).toHaveBeenCalledWith('hq_live_test', ORG, 'c1', 'img:v1', {});
|
|
124
|
+
});
|
|
125
|
+
// --smoke is still not honoured upstream, and refusing beats accepting.
|
|
126
|
+
it('still refuses --smoke, on either path', async () => {
|
|
127
|
+
const { deploy } = await import('./container.js');
|
|
128
|
+
await run(() => deploy('c1', 'img:v1', { smoke: 'GET / contains x', org: ORG }));
|
|
129
|
+
expect(exitError).toMatch(/--smoke is not honoured yet/);
|
|
130
|
+
expect(sdk.container.deployContainer).not.toHaveBeenCalled();
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
describe('crm pagination — --offset must reach the SDK', () => {
|
|
134
|
+
// Reported by a customer as returning page one forever. The API ignored it
|
|
135
|
+
// at the time; once fixed, the CLI had to actually send it, and only a
|
|
136
|
+
// handler-level test proves that.
|
|
137
|
+
beforeEach(() => {
|
|
138
|
+
sdk.crm.searchContacts.mockResolvedValue({ contacts: [], total: 0, has_more: false });
|
|
139
|
+
sdk.crm.searchCompanies.mockResolvedValue({ companies: [], total: 0, has_more: false });
|
|
140
|
+
});
|
|
141
|
+
it('passes --offset through on contacts list', async () => {
|
|
142
|
+
const { run: crmRun } = await import('./crm/index.js');
|
|
143
|
+
await run(() => crmRun('contacts', ['list'], { offset: 25, limit: 10, org: ORG }));
|
|
144
|
+
expect(sdk.crm.searchContacts).toHaveBeenCalledWith('hq_live_test', ORG, expect.objectContaining({ offset: 25, limit: 10 }));
|
|
145
|
+
});
|
|
146
|
+
it('passes --offset through on contacts search, alongside filters', async () => {
|
|
147
|
+
const { run: crmRun } = await import('./crm/index.js');
|
|
148
|
+
await run(() => crmRun('contacts', ['search'], { offset: 5, origin: 'webhook', org: ORG }));
|
|
149
|
+
expect(sdk.crm.searchContacts).toHaveBeenCalledWith('hq_live_test', ORG, expect.objectContaining({ offset: 5 }));
|
|
150
|
+
});
|
|
151
|
+
it('passes --offset through on companies', async () => {
|
|
152
|
+
const { run: crmRun } = await import('./crm/index.js');
|
|
153
|
+
await run(() => crmRun('companies', ['list'], { offset: 7, org: ORG }));
|
|
154
|
+
expect(sdk.crm.searchCompanies).toHaveBeenCalledWith('hq_live_test', ORG, expect.objectContaining({ offset: 7 }));
|
|
155
|
+
});
|
|
156
|
+
// The deprecated spelling must still reach the request, or the alias is a
|
|
157
|
+
// promise we are not keeping.
|
|
158
|
+
it('still honours the deprecated --source spelling for provenance', async () => {
|
|
159
|
+
const { run: crmRun } = await import('./crm/index.js');
|
|
160
|
+
await run(() => crmRun('contacts', ['search'], { source: 'goldfox', org: ORG }));
|
|
161
|
+
expect(sdk.crm.searchContacts).toHaveBeenCalledWith('hq_live_test', ORG, expect.objectContaining({ source: ['goldfox'] }));
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
describe('container logs — --scope must reach the SDK', () => {
|
|
165
|
+
it('sends scope=all when asked', async () => {
|
|
166
|
+
sdk.container.getContainerLogs.mockResolvedValue([]);
|
|
167
|
+
const { logs } = await import('./container.js');
|
|
168
|
+
await run(() => logs('c1', { scope: 'all', tail: 500, org: ORG }));
|
|
169
|
+
expect(sdk.container.getContainerLogs).toHaveBeenCalledWith('hq_live_test', ORG, 'c1', 500, 'all');
|
|
170
|
+
});
|
|
171
|
+
it('sends no scope by default, rather than the string "container"', async () => {
|
|
172
|
+
sdk.container.getContainerLogs.mockResolvedValue([]);
|
|
173
|
+
const { logs } = await import('./container.js');
|
|
174
|
+
await run(() => logs('c1', { org: ORG }));
|
|
175
|
+
expect(sdk.container.getContainerLogs).toHaveBeenCalledWith('hq_live_test', ORG, 'c1', undefined, undefined);
|
|
176
|
+
});
|
|
177
|
+
it('refuses an invalid scope instead of passing it on', async () => {
|
|
178
|
+
const { logs } = await import('./container.js');
|
|
179
|
+
await run(() => logs('c1', { scope: 'everything', org: ORG }));
|
|
180
|
+
expect(exitError).toMatch(/Invalid --scope/);
|
|
181
|
+
expect(sdk.container.getContainerLogs).not.toHaveBeenCalled();
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
describe('container create — --health-check is validated before the call', () => {
|
|
185
|
+
it('refuses /healthz client-side and never calls the SDK', async () => {
|
|
186
|
+
const { create } = await import('./container.js');
|
|
187
|
+
await run(() => create('probe', { 'health-check': '/healthz', org: ORG }));
|
|
188
|
+
expect(exitError).toMatch(/intercepts \/healthz/);
|
|
189
|
+
expect(sdk.container.createContainer).not.toHaveBeenCalled();
|
|
190
|
+
});
|
|
191
|
+
it('refuses a path that is not a path', async () => {
|
|
192
|
+
const { create } = await import('./container.js');
|
|
193
|
+
await run(() => create('probe', { 'health-check': 'livez', org: ORG }));
|
|
194
|
+
expect(exitError).toMatch(/must be a path/);
|
|
195
|
+
expect(sdk.container.createContainer).not.toHaveBeenCalled();
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
// ── Wider coverage ──────────────────────────────────────────────────────────
|
|
199
|
+
//
|
|
200
|
+
// The cases above are the ones a customer found. These are the same class of
|
|
201
|
+
// risk elsewhere: a flag that changes WHAT GETS WRITTEN, where losing it in
|
|
202
|
+
// transit produces a wrong record rather than an error.
|
|
203
|
+
//
|
|
204
|
+
// Coverage is partial and worth stating: this file exercises 8 commands of the
|
|
205
|
+
// 32 that take flags. It covers the ones where a dropped flag is silent and
|
|
206
|
+
// consequential. `list`/`get` verbs are omitted deliberately — a dropped
|
|
207
|
+
// filter there is visible in the output, which is a different and much
|
|
208
|
+
// cheaper failure.
|
|
209
|
+
describe('task create — flags that change the stored record', () => {
|
|
210
|
+
beforeEach(() => { sdk.task = { createTask: vi.fn().mockResolvedValue({ id: 't1' }), listTasks: vi.fn().mockResolvedValue([]) }; });
|
|
211
|
+
it('sends --dedup-key, which is what makes creation idempotent', async () => {
|
|
212
|
+
const { create } = await import('./task.js');
|
|
213
|
+
await run(() => create('do a thing', { 'dedup-key': 'evt-123', org: ORG }));
|
|
214
|
+
expect(sdk.task.createTask).toHaveBeenCalledWith('hq_live_test', ORG, expect.objectContaining({ dedupKey: 'evt-123' }));
|
|
215
|
+
});
|
|
216
|
+
it('sends --origin, and still honours the deprecated --source', async () => {
|
|
217
|
+
const { create } = await import('./task.js');
|
|
218
|
+
await run(() => create('x', { origin: 'agent', org: ORG }));
|
|
219
|
+
expect(sdk.task.createTask).toHaveBeenCalledWith('hq_live_test', ORG, expect.objectContaining({ source: 'agent' }));
|
|
220
|
+
vi.clearAllMocks();
|
|
221
|
+
await run(() => create('x', { source: 'legacy', org: ORG }));
|
|
222
|
+
expect(sdk.task.createTask).toHaveBeenCalledWith('hq_live_test', ORG, expect.objectContaining({ source: 'legacy' }));
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
describe('webhook create — the CRM ingest path must survive', () => {
|
|
226
|
+
beforeEach(() => { sdk.webhook = { createEndpoint: vi.fn().mockResolvedValue({ id: 'w1', url: 'u' }) }; });
|
|
227
|
+
// Losing this silently means submissions stop becoming contacts, with no
|
|
228
|
+
// error anywhere — the endpoint keeps accepting deliveries.
|
|
229
|
+
it('sends --crm-email-path', async () => {
|
|
230
|
+
const { create } = await import('./webhook.js');
|
|
231
|
+
await run(() => create('stripe', { 'crm-email-path': 'data.object.customer_email', org: ORG }));
|
|
232
|
+
const [, , , opts] = sdk.webhook.createEndpoint.mock.calls[0];
|
|
233
|
+
expect(opts).toMatchObject({ crm_email_path: 'data.object.customer_email' });
|
|
234
|
+
});
|
|
235
|
+
it('sends --forward-url', async () => {
|
|
236
|
+
const { create } = await import('./webhook.js');
|
|
237
|
+
await run(() => create('gh', { 'forward-url': 'https://example.com/hook', org: ORG }));
|
|
238
|
+
const [, , , opts] = sdk.webhook.createEndpoint.mock.calls[0];
|
|
239
|
+
expect(opts).toMatchObject({ forward_url: 'https://example.com/hook' });
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
describe('audience create — --from selects the dataset', () => {
|
|
243
|
+
beforeEach(() => { sdk.audience = { createAudience: vi.fn().mockResolvedValue({ id: 'a1', member_count: 0 }) }; });
|
|
244
|
+
// Picking the wrong dataset builds an audience of the wrong KIND of record.
|
|
245
|
+
// Nothing errors; the list is simply of companies when you wanted people.
|
|
246
|
+
it('sends --from', async () => {
|
|
247
|
+
const { run: audRun } = await import('./audience.js');
|
|
248
|
+
await run(() => audRun('create', ['my-list'], { from: 'company', filter: '{}', org: ORG }));
|
|
249
|
+
expect(sdk.audience.createAudience).toHaveBeenCalledWith('hq_live_test', ORG, expect.objectContaining({ source: 'company' }));
|
|
250
|
+
});
|
|
251
|
+
it('still honours the deprecated --source spelling', async () => {
|
|
252
|
+
const { run: audRun } = await import('./audience.js');
|
|
253
|
+
await run(() => audRun('create', ['my-list'], { source: 'people', filter: '{}', org: ORG }));
|
|
254
|
+
expect(sdk.audience.createAudience).toHaveBeenCalledWith('hq_live_test', ORG, expect.objectContaining({ source: 'people' }));
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
describe('git commit — authorship must not silently default', () => {
|
|
258
|
+
beforeEach(() => {
|
|
259
|
+
sdk.git = {
|
|
260
|
+
commit: vi.fn().mockResolvedValue({ sha: 'abc1234' }),
|
|
261
|
+
// The handler resolves the branch tip before committing; without this it
|
|
262
|
+
// refuses rather than guessing a base, which is the right behaviour and
|
|
263
|
+
// has to be satisfied to reach the call we are testing.
|
|
264
|
+
listRefs: vi.fn().mockResolvedValue({ branches: [{ name: 'main', sha: 'base123' }] }),
|
|
265
|
+
};
|
|
266
|
+
});
|
|
267
|
+
// Without these every agent-written commit is attributed to the key's
|
|
268
|
+
// account — wrong quietly rather than loudly.
|
|
269
|
+
it('sends --author-name and --author-email', async () => {
|
|
270
|
+
const { commit } = await import('./git.js');
|
|
271
|
+
await run(() => commit('repo', {
|
|
272
|
+
branch: 'main', message: 'm',
|
|
273
|
+
changes: '[{"path":"a.txt","content":"hi"}]',
|
|
274
|
+
'author-name': 'Ada', 'author-email': 'ada@example.com', org: ORG,
|
|
275
|
+
}));
|
|
276
|
+
expect(sdk.git.commit).toHaveBeenCalled();
|
|
277
|
+
const payload = sdk.git.commit.mock.calls[0][3];
|
|
278
|
+
expect(payload.author).toMatchObject({ name: 'Ada', email: 'ada@example.com' });
|
|
279
|
+
});
|
|
280
|
+
it('omits author entirely when neither flag is given', async () => {
|
|
281
|
+
const { commit } = await import('./git.js');
|
|
282
|
+
await run(() => commit('repo', {
|
|
283
|
+
branch: 'main', message: 'm', changes: '[{"path":"a.txt","content":"hi"}]', org: ORG,
|
|
284
|
+
}));
|
|
285
|
+
expect(sdk.git.commit.mock.calls[0][3].author).toBeUndefined();
|
|
286
|
+
});
|
|
287
|
+
});
|
package/dist/commands/llm.d.ts
CHANGED
|
@@ -3,4 +3,5 @@ import { type Flags } from '../helpers.js';
|
|
|
3
3
|
import type { Exposes } from '../exposes.js';
|
|
4
4
|
export declare const EXPOSES: Exposes;
|
|
5
5
|
export declare const SCHEMA: FlagSchema;
|
|
6
|
+
export declare function _parseJsonObjectFlag(raw: unknown, flagName: string): Record<string, unknown> | undefined;
|
|
6
7
|
export declare function run(subcommand: string | undefined, args: string[], flags: Flags): Promise<void>;
|
package/dist/commands/llm.js
CHANGED
|
@@ -24,6 +24,9 @@ export const SCHEMA = {
|
|
|
24
24
|
schema: 'string',
|
|
25
25
|
style: 'string',
|
|
26
26
|
kind: 'string',
|
|
27
|
+
facts: 'string',
|
|
28
|
+
directives: 'string',
|
|
29
|
+
// Deprecated alias for --facts; undocumented, removable next minor.
|
|
27
30
|
context: 'string',
|
|
28
31
|
prompt: 'string',
|
|
29
32
|
tier: 'string',
|
|
@@ -103,21 +106,33 @@ function parseSchemaFlag(flags) {
|
|
|
103
106
|
return null;
|
|
104
107
|
}
|
|
105
108
|
}
|
|
106
|
-
|
|
107
|
-
|
|
109
|
+
// --facts is referent data quoted into the prompt as reference; --directives
|
|
110
|
+
// are writer controls (tone, max_words, format). The platform split them
|
|
111
|
+
// because they are trusted differently, and `--context` predated the split.
|
|
112
|
+
export function _parseJsonObjectFlag(raw, flagName) {
|
|
113
|
+
if (typeof raw !== 'string' || !raw)
|
|
108
114
|
return undefined;
|
|
109
115
|
try {
|
|
110
|
-
const parsed = JSON.parse(
|
|
111
|
-
if (typeof parsed === 'object' && parsed !== null)
|
|
116
|
+
const parsed = JSON.parse(raw);
|
|
117
|
+
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
|
112
118
|
return parsed;
|
|
113
|
-
|
|
119
|
+
}
|
|
120
|
+
error(`${flagName} must be a JSON object`);
|
|
114
121
|
return undefined;
|
|
115
122
|
}
|
|
116
123
|
catch {
|
|
117
|
-
error(
|
|
124
|
+
error(`${flagName} must be valid JSON`);
|
|
118
125
|
return undefined;
|
|
119
126
|
}
|
|
120
127
|
}
|
|
128
|
+
function parseContextFlag(flags) {
|
|
129
|
+
// --facts wins; --context is the old spelling and still works.
|
|
130
|
+
return _parseJsonObjectFlag(flags.facts, '--facts')
|
|
131
|
+
?? _parseJsonObjectFlag(flags.context, '--context');
|
|
132
|
+
}
|
|
133
|
+
function parseDirectivesFlag(flags) {
|
|
134
|
+
return _parseJsonObjectFlag(flags.directives, '--directives');
|
|
135
|
+
}
|
|
121
136
|
function tierFromFlag(flags) {
|
|
122
137
|
if (typeof flags.tier !== 'string' || !flags.tier)
|
|
123
138
|
return undefined;
|
|
@@ -269,7 +284,7 @@ async function summarize(inputArg, flags) {
|
|
|
269
284
|
}
|
|
270
285
|
async function draft(inputArg, flags) {
|
|
271
286
|
const config = requireConfig();
|
|
272
|
-
const orgId = requireOrg(flags, config, 'myapi llm draft --kind <what> [--prompt "<instructions>"] [--
|
|
287
|
+
const orgId = requireOrg(flags, config, 'myapi llm draft --kind <what> [--prompt "<instructions>"] [--facts <json>] ["<source text>"] [--tier <t>] [--org <id>]');
|
|
273
288
|
if (typeof flags.kind !== 'string' || !flags.kind) {
|
|
274
289
|
error('Missing required flag: --kind <email|message|reply|...>');
|
|
275
290
|
return;
|
|
@@ -281,12 +296,13 @@ async function draft(inputArg, flags) {
|
|
|
281
296
|
const promptText = typeof flags.prompt === 'string' ? flags.prompt : '';
|
|
282
297
|
const ctx = parseContextFlag(flags);
|
|
283
298
|
if (!input.trim() && !promptText.trim() && (!ctx || Object.keys(ctx).length === 0)) {
|
|
284
|
-
error('draft needs at least one of: <source text> (arg or --file), --prompt, or --
|
|
299
|
+
error('draft needs at least one of: <source text> (arg or --file), --prompt, or --facts.');
|
|
285
300
|
}
|
|
286
301
|
const res = await retryFunds(() => sdkLlm.draft(config.api_key, orgId, {
|
|
287
302
|
input: input || undefined,
|
|
288
303
|
kind,
|
|
289
|
-
|
|
304
|
+
facts: ctx,
|
|
305
|
+
directives: parseDirectivesFlag(flags),
|
|
290
306
|
prompt: promptText || undefined,
|
|
291
307
|
tier: tierFromFlag(flags),
|
|
292
308
|
}));
|
package/dist/completion.js
CHANGED
|
@@ -29,7 +29,7 @@ export const COMMANDS = [
|
|
|
29
29
|
'account', 'audience', 'auth', 'billing', 'company', 'completion', 'config', 'container',
|
|
30
30
|
'crm', 'database', 'domain', 'email', 'fn', 'funnel', 'git', 'help', 'image',
|
|
31
31
|
'doctor', 'install-skills', 'keys', 'llm', 'login', 'org', 'payments', 'people', 'pixel',
|
|
32
|
-
'queue', 'setup', 'status', 'storage', 'task', 'update', 'url', 'webhook', 'whoami',
|
|
32
|
+
'feedback', 'queue', 'setup', 'status', 'storage', 'task', 'update', 'url', 'webhook', 'whoami',
|
|
33
33
|
'workflow',
|
|
34
34
|
];
|
|
35
35
|
// command → subcommands, for `myapi <command> <TAB>`. Mirrors each
|
|
@@ -61,6 +61,7 @@ export const SUBCOMMANDS = {
|
|
|
61
61
|
payments: ['connect', 'status', 'charge', 'list', 'get', 'refund'],
|
|
62
62
|
container: ['create', 'deploy', 'list', 'get', 'logs', 'domain', 'delete'],
|
|
63
63
|
git: ['create', 'list', 'get', 'delete', 'refs', 'log', 'show', 'tree', 'blob', 'diff', 'commit', 'create-branch', 'delete-branch', 'tag', 'merge', 'repack'],
|
|
64
|
+
feedback: ['create', 'list', 'resolve', 'widget'],
|
|
64
65
|
queue: ['create', 'list', 'get', 'delete', 'enqueue', 'jobs', 'job'],
|
|
65
66
|
task: ['create', 'list', 'get', 'claim', 'extend', 'resolve', 'fail', 'cancel'],
|
|
66
67
|
completion: ['install', 'uninstall'],
|
package/dist/exposes.test.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -41,6 +41,7 @@ import * as paymentsCmd from './commands/payments.js';
|
|
|
41
41
|
import * as containerCmd from './commands/container.js';
|
|
42
42
|
import * as gitCmd from './commands/git.js';
|
|
43
43
|
import * as queueCmd from './commands/queue.js';
|
|
44
|
+
import * as feedbackCmd from './commands/feedback.js';
|
|
44
45
|
import * as taskCmd from './commands/task.js';
|
|
45
46
|
import * as doctorCmd from './commands/doctor.js';
|
|
46
47
|
import * as loginCmd from './commands/login.js';
|
|
@@ -85,6 +86,7 @@ const COMMAND_SCHEMAS = {
|
|
|
85
86
|
people: peopleCmd.SCHEMA,
|
|
86
87
|
pixel: pixelCmd.SCHEMA,
|
|
87
88
|
queue: queueCmd.SCHEMA,
|
|
89
|
+
feedback: feedbackCmd.SCHEMA,
|
|
88
90
|
status: statusCmd.SCHEMA,
|
|
89
91
|
storage: storageCmd.SCHEMA,
|
|
90
92
|
task: taskCmd.SCHEMA,
|
|
@@ -134,6 +136,7 @@ const COMBINED_SCHEMA = {
|
|
|
134
136
|
...containerCmd.SCHEMA,
|
|
135
137
|
...gitCmd.SCHEMA,
|
|
136
138
|
...queueCmd.SCHEMA,
|
|
139
|
+
...feedbackCmd.SCHEMA,
|
|
137
140
|
...taskCmd.SCHEMA,
|
|
138
141
|
...doctorCmd.SCHEMA,
|
|
139
142
|
...loginCmd.SCHEMA,
|
|
@@ -258,6 +261,9 @@ async function main() {
|
|
|
258
261
|
case 'queue':
|
|
259
262
|
await queueCmd.run(subcommand, restArgs, flags);
|
|
260
263
|
break;
|
|
264
|
+
case 'feedback':
|
|
265
|
+
await feedbackCmd.run(subcommand, restArgs, flags);
|
|
266
|
+
break;
|
|
261
267
|
case 'task':
|
|
262
268
|
await taskCmd.run(subcommand, restArgs, flags);
|
|
263
269
|
break;
|
|
@@ -463,6 +469,7 @@ Commands:
|
|
|
463
469
|
doctor Org-wide consistency check — funnels, webhooks, domains, containers
|
|
464
470
|
domain Manage domain configurations
|
|
465
471
|
email Manage mailboxes, send/read email, templates, and campaigns
|
|
472
|
+
feedback Collect feedback from the people using what you built
|
|
466
473
|
fn Create and deploy functions on the edge runtime
|
|
467
474
|
funnel Manage websites (publish pages, custom domains, funnels)
|
|
468
475
|
git Hosted git repositories — repos, commits, branches, history
|
|
@@ -4,7 +4,7 @@ version: 1.0.0
|
|
|
4
4
|
description: >
|
|
5
5
|
Deploy JavaScript functions to the MyAPI edge runtime. Register a function, upload a single-file JS bundle, get a live HTTP invocation URL or run it on a cron schedule. Each function gets a scoped capability key for cross-slot calls.
|
|
6
6
|
triggers: [function, deploy function, edge function, serverless, cloudflare worker, cron, scoped api key, capability key, invocation url, bundle]
|
|
7
|
-
checksum: sha256-
|
|
7
|
+
checksum: sha256-a29b3c96645c59317c0896c9a886d9904bdb2ebde9eb8582d0cb7071d0b9ac2c
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# MyFunctionAPI
|
|
@@ -129,4 +129,10 @@ curl -H "Authorization: Bearer $SCOPED_KEY" \
|
|
|
129
129
|
- Deploy rotates the scoped API key on every call — re-capture the printed value if other systems use it.
|
|
130
130
|
- `myapi fn env <id> --set KEY=VALUE,OTHER=VALUE` sets several secrets in one call instead of one command each.
|
|
131
131
|
|
|
132
|
+
**`--scope` is create-only, so treat it as permanent.** There is no
|
|
133
|
+
`fn scope --add`. Adding a slot later means delete + recreate, which mints a
|
|
134
|
+
**new function id and a new invocation URL**, breaking every reference already
|
|
135
|
+
handed out — docs, front-end config, webhooks, anything given to a third
|
|
136
|
+
party. Decide the full slot list before you publish the URL.
|
|
137
|
+
|
|
132
138
|
Run `myapi fn --help` or `myapi fn <subcommand> --help` for full flag reference.
|
|
@@ -7,7 +7,7 @@ description: >
|
|
|
7
7
|
(classify / extract / summarize / draft) that hide the model behind a
|
|
8
8
|
task. Pricing in cents per 1M tokens; charged from your MyAPI balance.
|
|
9
9
|
triggers: [llm, completion, chat, embed, embedding, inference, classify, extract, summarize, draft, qwen]
|
|
10
|
-
checksum: sha256-
|
|
10
|
+
checksum: sha256-2a9070e118134aea98a1be986d29f9c4e3a969aa960e7dfc87137bbc490ac04e
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
# MyLLMAPI
|
|
@@ -21,9 +21,9 @@ Pricing is cents per 1M tokens at the actual upstream rate, debited from your My
|
|
|
21
21
|
|
|
22
22
|
## Capabilities
|
|
23
23
|
<!-- llm:start -->
|
|
24
|
-
|
|
24
|
+
For workflow tasks — summarize, classify, extract, draft. The reply goes to stdout and the usage footer (tokens, cost, finish reason) to stderr, so `myapi llm complete ... | jq` works.
|
|
25
25
|
|
|
26
|
-
**Don't use this as your own model.** If you
|
|
26
|
+
**Don't use this as your own model.** If you are an agent reading this, you already have a more capable model. Reach for the LLM verbs when scripting a recurring step where a small, cheap model is the right tool — not for one-shot reasoning you can do yourself.
|
|
27
27
|
|
|
28
28
|
Reach for raw `complete` when shape matters (you build the `messages` array and set `max_tokens`/`temperature`/`stop`); reach for a verb when you want a *result* and don't care which model produced it.
|
|
29
29
|
|
|
@@ -88,7 +88,7 @@ The model/provider is **never** named in the verb response — the verb is the c
|
|
|
88
88
|
|
|
89
89
|
### OpenAI-compatible drop-in
|
|
90
90
|
|
|
91
|
-
`POST /llm/orgs/{org_id}/chat/completions` (
|
|
91
|
+
`POST /llm/orgs/{org_id}/chat/completions` (alias `/v1/chat/completions`) takes and returns the OpenAI shape — **no envelope**. Same catalog and pricing as `complete`. Use it when an existing OpenAI SDK or LangChain integration should point at MyAPI unchanged.
|
|
92
92
|
|
|
93
93
|
```python
|
|
94
94
|
from openai import OpenAI
|
|
@@ -160,3 +160,16 @@ INTENT=$(printf '%s' "$BODY" | myapi llm classify - \
|
|
|
160
160
|
- **Self-hosted raw, server-picked verbs.** Raw runs on MyAPI's TPU; verbs route wherever the server picks.
|
|
161
161
|
- **Cost + latency.** `usage.cost_cents` is authoritative — no markup. Varies by tier: 200–600 ms to first token, 1–3 s end-to-end.
|
|
162
162
|
- **Live catalog, no streaming, no BYOK.** Don't hard-code ids — `models` is truth (CLI auto-picks if `--model` omitted). Full reply only.
|
|
163
|
+
|
|
164
|
+
## `--facts` vs `--directives` on draft
|
|
165
|
+
|
|
166
|
+
`--facts '<json>'` is referent data, quoted as reference and never as
|
|
167
|
+
instructions (recipient, dates, amounts). `--directives '<json>'` is writer
|
|
168
|
+
controls only: tone, max_words, format, style. They are trusted differently.
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
myapi llm draft --kind email --prompt "the invoice is due" \
|
|
172
|
+
--facts '{"to":"Ada"}' --directives '{"tone":"warm"}'
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
`--context` is the old name for `--facts`; accepted, deprecated upstream.
|
|
@@ -4,7 +4,7 @@ version: 1.0.0
|
|
|
4
4
|
description: >
|
|
5
5
|
Edge-hosted asset storage. Upload a local file of any content type directly, or have the server fetch from a public URL. Each asset gets a stable public CDN URL.
|
|
6
6
|
triggers: [storage, upload, ingest, asset, cdn, image hosting, file upload, get-url, download, public url]
|
|
7
|
-
checksum: sha256-
|
|
7
|
+
checksum: sha256-1f3e7f6de3435e0334a808b9d2faa978c9cb70982509a376abc0e539cb44e1ed
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# MyStorageAPI
|
|
@@ -93,11 +93,17 @@ Both produce identical asset records — `list` doesn't distinguish.
|
|
|
93
93
|
fetch it; there is no private mode, no signed URL, and no revocation. The id
|
|
94
94
|
being long and random is **not** access control — treat the URL as public the
|
|
95
95
|
moment it exists.
|
|
96
|
-
- **
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
96
|
+
- **Private assets exist as of 2026-07-28** — upload with `visibility: private`,
|
|
97
|
+
or `PATCH` an existing asset to close an exposure, which takes effect
|
|
98
|
+
immediately. Fetch it with a signed URL (15 minutes default, 24 hours max);
|
|
99
|
+
`revoke-links` kills every link already handed out, including unexpired ones.
|
|
100
|
+
A private upload deliberately returns **no plain `url`**, because that URL
|
|
101
|
+
does not serve the file and would look like the answer.
|
|
102
|
+
- **Encrypting before upload is still worth doing for the strictest cases.**
|
|
103
|
+
Private assets protect against the internet; client-side encryption protects
|
|
104
|
+
against the platform, and those are different threat models. A team shipping
|
|
105
|
+
Swiss lease documents used AES-256-GCM envelope encryption with a
|
|
106
|
+
per-document data key wrapped under a master key in function secrets.
|
|
101
107
|
- Delete is immediate and unrecoverable — run `myapi storage list` first to confirm the asset, pass `--org` explicitly, and pass `--yes` in non-interactive runs.
|
|
102
108
|
- The URL is permanent until you `myapi storage delete <id>` — embed it freely.
|
|
103
109
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myapihq/cli",
|
|
3
3
|
"license": "Apache-2.0",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.9.0",
|
|
5
5
|
"description": "MyAPI command-line interface",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -38,11 +38,15 @@
|
|
|
38
38
|
"audit:doctor": "npm run build && node scripts/audit-doctor.js",
|
|
39
39
|
"lint:docs": "node scripts/lint-docs.js",
|
|
40
40
|
"lint:skill-coverage": "node scripts/lint-skill-coverage.js",
|
|
41
|
+
"bench:discoverability": "node scripts/bench-discoverability.js",
|
|
42
|
+
"lint:claims": "node scripts/verify-claims.js --lint",
|
|
43
|
+
"verify:claims": "npm run build && node scripts/verify-claims.js",
|
|
44
|
+
"audit:fields": "npm run build && node scripts/audit-field-honoured.js",
|
|
41
45
|
"lint:skills": "node scripts/copy-skills.js && node scripts/lint-skills.js",
|
|
42
46
|
"lint:skills:strict": "node scripts/copy-skills.js && node scripts/lint-skills.js --strict"
|
|
43
47
|
},
|
|
44
48
|
"dependencies": {
|
|
45
|
-
"@myapihq/sdk": "^2.
|
|
49
|
+
"@myapihq/sdk": "^2.9.0"
|
|
46
50
|
},
|
|
47
51
|
"devDependencies": {
|
|
48
52
|
"@types/node": "^25.6.0",
|