@myapihq/cli 2.7.1 → 2.8.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.
Files changed (40) hide show
  1. package/dist/commands/account.js +15 -0
  2. package/dist/commands/audience.js +7 -5
  3. package/dist/commands/crm/companies.js +4 -4
  4. package/dist/commands/crm/contacts.js +4 -4
  5. package/dist/commands/crm/index.js +3 -0
  6. package/dist/commands/crm/origin-flag.test.d.ts +1 -0
  7. package/dist/commands/crm/origin-flag.test.js +38 -0
  8. package/dist/commands/crm/pagination.d.ts +2 -0
  9. package/dist/commands/crm/pagination.js +9 -0
  10. package/dist/commands/domain.js +11 -0
  11. package/dist/commands/feedback.d.ts +10 -0
  12. package/dist/commands/feedback.js +173 -0
  13. package/dist/commands/flag-reachability.test.d.ts +1 -0
  14. package/dist/commands/flag-reachability.test.js +274 -0
  15. package/dist/commands/fn.js +7 -2
  16. package/dist/commands/llm.d.ts +1 -0
  17. package/dist/commands/llm.js +25 -9
  18. package/dist/commands/task.js +12 -4
  19. package/dist/completion.js +2 -1
  20. package/dist/exposes.test.js +1 -0
  21. package/dist/index.js +7 -0
  22. package/dist/skills/my-api-hq/SKILL.md +25 -1
  23. package/dist/skills/my-audience-api/SKILL.md +5 -5
  24. package/dist/skills/my-auth-api/SKILL.md +8 -1
  25. package/dist/skills/my-company-api/SKILL.md +3 -3
  26. package/dist/skills/my-container-api/SKILL.md +25 -4
  27. package/dist/skills/my-crm-api/SKILL.md +6 -6
  28. package/dist/skills/my-database-api/SKILL.md +22 -1
  29. package/dist/skills/my-domain-api/SKILL.md +18 -1
  30. package/dist/skills/my-email-api/SKILL.md +36 -1
  31. package/dist/skills/my-function-api/SKILL.md +35 -1
  32. package/dist/skills/my-funnel-api/SKILL.md +6 -3
  33. package/dist/skills/my-git-api/SKILL.md +7 -1
  34. package/dist/skills/my-llm-api/SKILL.md +17 -4
  35. package/dist/skills/my-people-api/SKILL.md +3 -3
  36. package/dist/skills/my-pixel-api/SKILL.md +12 -1
  37. package/dist/skills/my-storage-api/SKILL.md +31 -2
  38. package/dist/skills/my-task-api/SKILL.md +9 -1
  39. package/dist/skills/my-webhook-api/SKILL.md +12 -1
  40. package/package.json +7 -2
@@ -0,0 +1,274 @@
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 — the flag must be refused on BOTH paths', () => {
87
+ // The original bug: this branch dropped the options entirely. Now the flags
88
+ // are refused platform-wide, and the refusal has to fire here too — a
89
+ // refusal wired to one branch is the same defect wearing a different hat.
90
+ it('refuses --no-promote on the --source path, and never calls the SDK', async () => {
91
+ const { deploy } = await import('./container.js');
92
+ await run(() => deploy('c1', '', { source: './app', 'no-promote': true, org: ORG }));
93
+ expect(exitError).toMatch(/not honoured yet/);
94
+ expect(sdk.container.deployContainerSource).not.toHaveBeenCalled();
95
+ expect(sdk.container.deployContainer).not.toHaveBeenCalled();
96
+ });
97
+ it('refuses --smoke on the --source path', async () => {
98
+ const { deploy } = await import('./container.js');
99
+ await run(() => deploy('c1', '', { source: './app', smoke: 'GET / contains x', org: ORG }));
100
+ expect(exitError).toMatch(/not honoured yet/);
101
+ expect(sdk.container.deployContainerSource).not.toHaveBeenCalled();
102
+ });
103
+ it('refuses --no-promote on the image path', async () => {
104
+ const { deploy } = await import('./container.js');
105
+ await run(() => deploy('c1', 'img:v1', { 'no-promote': true, org: ORG }));
106
+ expect(exitError).toMatch(/not honoured yet/);
107
+ expect(sdk.container.deployContainer).not.toHaveBeenCalled();
108
+ });
109
+ // The refusal must not become a blanket block on deploying at all.
110
+ it('still deploys normally when neither flag is passed', async () => {
111
+ sdk.container.deployContainer.mockResolvedValue({
112
+ container_id: 'c1', revision_id: 'r1', url: 'https://x', status: 'active', scoped_api_key: 'k',
113
+ });
114
+ const { deploy } = await import('./container.js');
115
+ await run(() => deploy('c1', 'img:v1', { org: ORG }));
116
+ expect(exitError).toBeNull();
117
+ expect(sdk.container.deployContainer).toHaveBeenCalledWith('hq_live_test', ORG, 'c1', 'img:v1');
118
+ });
119
+ });
120
+ describe('crm pagination — --offset must reach the SDK', () => {
121
+ // Reported by a customer as returning page one forever. The API ignored it
122
+ // at the time; once fixed, the CLI had to actually send it, and only a
123
+ // handler-level test proves that.
124
+ beforeEach(() => {
125
+ sdk.crm.searchContacts.mockResolvedValue({ contacts: [], total: 0, has_more: false });
126
+ sdk.crm.searchCompanies.mockResolvedValue({ companies: [], total: 0, has_more: false });
127
+ });
128
+ it('passes --offset through on contacts list', async () => {
129
+ const { run: crmRun } = await import('./crm/index.js');
130
+ await run(() => crmRun('contacts', ['list'], { offset: 25, limit: 10, org: ORG }));
131
+ expect(sdk.crm.searchContacts).toHaveBeenCalledWith('hq_live_test', ORG, expect.objectContaining({ offset: 25, limit: 10 }));
132
+ });
133
+ it('passes --offset through on contacts search, alongside filters', async () => {
134
+ const { run: crmRun } = await import('./crm/index.js');
135
+ await run(() => crmRun('contacts', ['search'], { offset: 5, origin: 'webhook', org: ORG }));
136
+ expect(sdk.crm.searchContacts).toHaveBeenCalledWith('hq_live_test', ORG, expect.objectContaining({ offset: 5 }));
137
+ });
138
+ it('passes --offset through on companies', async () => {
139
+ const { run: crmRun } = await import('./crm/index.js');
140
+ await run(() => crmRun('companies', ['list'], { offset: 7, org: ORG }));
141
+ expect(sdk.crm.searchCompanies).toHaveBeenCalledWith('hq_live_test', ORG, expect.objectContaining({ offset: 7 }));
142
+ });
143
+ // The deprecated spelling must still reach the request, or the alias is a
144
+ // promise we are not keeping.
145
+ it('still honours the deprecated --source spelling for provenance', async () => {
146
+ const { run: crmRun } = await import('./crm/index.js');
147
+ await run(() => crmRun('contacts', ['search'], { source: 'goldfox', org: ORG }));
148
+ expect(sdk.crm.searchContacts).toHaveBeenCalledWith('hq_live_test', ORG, expect.objectContaining({ source: ['goldfox'] }));
149
+ });
150
+ });
151
+ describe('container logs — --scope must reach the SDK', () => {
152
+ it('sends scope=all when asked', async () => {
153
+ sdk.container.getContainerLogs.mockResolvedValue([]);
154
+ const { logs } = await import('./container.js');
155
+ await run(() => logs('c1', { scope: 'all', tail: 500, org: ORG }));
156
+ expect(sdk.container.getContainerLogs).toHaveBeenCalledWith('hq_live_test', ORG, 'c1', 500, 'all');
157
+ });
158
+ it('sends no scope by default, rather than the string "container"', async () => {
159
+ sdk.container.getContainerLogs.mockResolvedValue([]);
160
+ const { logs } = await import('./container.js');
161
+ await run(() => logs('c1', { org: ORG }));
162
+ expect(sdk.container.getContainerLogs).toHaveBeenCalledWith('hq_live_test', ORG, 'c1', undefined, undefined);
163
+ });
164
+ it('refuses an invalid scope instead of passing it on', async () => {
165
+ const { logs } = await import('./container.js');
166
+ await run(() => logs('c1', { scope: 'everything', org: ORG }));
167
+ expect(exitError).toMatch(/Invalid --scope/);
168
+ expect(sdk.container.getContainerLogs).not.toHaveBeenCalled();
169
+ });
170
+ });
171
+ describe('container create — --health-check is validated before the call', () => {
172
+ it('refuses /healthz client-side and never calls the SDK', async () => {
173
+ const { create } = await import('./container.js');
174
+ await run(() => create('probe', { 'health-check': '/healthz', org: ORG }));
175
+ expect(exitError).toMatch(/intercepts \/healthz/);
176
+ expect(sdk.container.createContainer).not.toHaveBeenCalled();
177
+ });
178
+ it('refuses a path that is not a path', async () => {
179
+ const { create } = await import('./container.js');
180
+ await run(() => create('probe', { 'health-check': 'livez', org: ORG }));
181
+ expect(exitError).toMatch(/must be a path/);
182
+ expect(sdk.container.createContainer).not.toHaveBeenCalled();
183
+ });
184
+ });
185
+ // ── Wider coverage ──────────────────────────────────────────────────────────
186
+ //
187
+ // The cases above are the ones a customer found. These are the same class of
188
+ // risk elsewhere: a flag that changes WHAT GETS WRITTEN, where losing it in
189
+ // transit produces a wrong record rather than an error.
190
+ //
191
+ // Coverage is partial and worth stating: this file exercises 8 commands of the
192
+ // 32 that take flags. It covers the ones where a dropped flag is silent and
193
+ // consequential. `list`/`get` verbs are omitted deliberately — a dropped
194
+ // filter there is visible in the output, which is a different and much
195
+ // cheaper failure.
196
+ describe('task create — flags that change the stored record', () => {
197
+ beforeEach(() => { sdk.task = { createTask: vi.fn().mockResolvedValue({ id: 't1' }), listTasks: vi.fn().mockResolvedValue([]) }; });
198
+ it('sends --dedup-key, which is what makes creation idempotent', async () => {
199
+ const { create } = await import('./task.js');
200
+ await run(() => create('do a thing', { 'dedup-key': 'evt-123', org: ORG }));
201
+ expect(sdk.task.createTask).toHaveBeenCalledWith('hq_live_test', ORG, expect.objectContaining({ dedupKey: 'evt-123' }));
202
+ });
203
+ it('sends --origin, and still honours the deprecated --source', async () => {
204
+ const { create } = await import('./task.js');
205
+ await run(() => create('x', { origin: 'agent', org: ORG }));
206
+ expect(sdk.task.createTask).toHaveBeenCalledWith('hq_live_test', ORG, expect.objectContaining({ source: 'agent' }));
207
+ vi.clearAllMocks();
208
+ await run(() => create('x', { source: 'legacy', org: ORG }));
209
+ expect(sdk.task.createTask).toHaveBeenCalledWith('hq_live_test', ORG, expect.objectContaining({ source: 'legacy' }));
210
+ });
211
+ });
212
+ describe('webhook create — the CRM ingest path must survive', () => {
213
+ beforeEach(() => { sdk.webhook = { createEndpoint: vi.fn().mockResolvedValue({ id: 'w1', url: 'u' }) }; });
214
+ // Losing this silently means submissions stop becoming contacts, with no
215
+ // error anywhere — the endpoint keeps accepting deliveries.
216
+ it('sends --crm-email-path', async () => {
217
+ const { create } = await import('./webhook.js');
218
+ await run(() => create('stripe', { 'crm-email-path': 'data.object.customer_email', org: ORG }));
219
+ const [, , , opts] = sdk.webhook.createEndpoint.mock.calls[0];
220
+ expect(opts).toMatchObject({ crm_email_path: 'data.object.customer_email' });
221
+ });
222
+ it('sends --forward-url', async () => {
223
+ const { create } = await import('./webhook.js');
224
+ await run(() => create('gh', { 'forward-url': 'https://example.com/hook', org: ORG }));
225
+ const [, , , opts] = sdk.webhook.createEndpoint.mock.calls[0];
226
+ expect(opts).toMatchObject({ forward_url: 'https://example.com/hook' });
227
+ });
228
+ });
229
+ describe('audience create — --from selects the dataset', () => {
230
+ beforeEach(() => { sdk.audience = { createAudience: vi.fn().mockResolvedValue({ id: 'a1', member_count: 0 }) }; });
231
+ // Picking the wrong dataset builds an audience of the wrong KIND of record.
232
+ // Nothing errors; the list is simply of companies when you wanted people.
233
+ it('sends --from', async () => {
234
+ const { run: audRun } = await import('./audience.js');
235
+ await run(() => audRun('create', ['my-list'], { from: 'company', filter: '{}', org: ORG }));
236
+ expect(sdk.audience.createAudience).toHaveBeenCalledWith('hq_live_test', ORG, expect.objectContaining({ source: 'company' }));
237
+ });
238
+ it('still honours the deprecated --source spelling', async () => {
239
+ const { run: audRun } = await import('./audience.js');
240
+ await run(() => audRun('create', ['my-list'], { source: 'people', filter: '{}', org: ORG }));
241
+ expect(sdk.audience.createAudience).toHaveBeenCalledWith('hq_live_test', ORG, expect.objectContaining({ source: 'people' }));
242
+ });
243
+ });
244
+ describe('git commit — authorship must not silently default', () => {
245
+ beforeEach(() => {
246
+ sdk.git = {
247
+ commit: vi.fn().mockResolvedValue({ sha: 'abc1234' }),
248
+ // The handler resolves the branch tip before committing; without this it
249
+ // refuses rather than guessing a base, which is the right behaviour and
250
+ // has to be satisfied to reach the call we are testing.
251
+ listRefs: vi.fn().mockResolvedValue({ branches: [{ name: 'main', sha: 'base123' }] }),
252
+ };
253
+ });
254
+ // Without these every agent-written commit is attributed to the key's
255
+ // account — wrong quietly rather than loudly.
256
+ it('sends --author-name and --author-email', async () => {
257
+ const { commit } = await import('./git.js');
258
+ await run(() => commit('repo', {
259
+ branch: 'main', message: 'm',
260
+ changes: '[{"path":"a.txt","content":"hi"}]',
261
+ 'author-name': 'Ada', 'author-email': 'ada@example.com', org: ORG,
262
+ }));
263
+ expect(sdk.git.commit).toHaveBeenCalled();
264
+ const payload = sdk.git.commit.mock.calls[0][3];
265
+ expect(payload.author).toMatchObject({ name: 'Ada', email: 'ada@example.com' });
266
+ });
267
+ it('omits author entirely when neither flag is given', async () => {
268
+ const { commit } = await import('./git.js');
269
+ await run(() => commit('repo', {
270
+ branch: 'main', message: 'm', changes: '[{"path":"a.txt","content":"hi"}]', org: ORG,
271
+ }));
272
+ expect(sdk.git.commit.mock.calls[0][3].author).toBeUndefined();
273
+ });
274
+ });
@@ -54,7 +54,10 @@ function summarizeFn(f) {
54
54
  id: f.id,
55
55
  name: f.name,
56
56
  trigger: f.trigger_type === 'cron' ? `cron ${f.cron_schedule ?? '?'}` : 'http',
57
- url: f.invocation_url || '(not deployed)',
57
+ // A cron function is never reachable over HTTP — its URL 404s. Printing
58
+ // one contradicted the docs more loudly than the docs denied it, and a
59
+ // user wrote a `fetch` handler for manual runs that could never fire.
60
+ url: f.trigger_type === 'cron' ? '— (cron: not HTTP-invocable)' : (f.invocation_url || '(not deployed)'),
58
61
  updated_at: f.updated_at,
59
62
  };
60
63
  }
@@ -128,7 +131,9 @@ export async function get(id, flags) {
128
131
  info(`ID: ${fn.id}`);
129
132
  info(`Name: ${fn.name}`);
130
133
  info(`Trigger: ${fn.trigger_type}${fn.cron_schedule ? ` (${fn.cron_schedule})` : ''}`);
131
- info(`Invocation URL: ${fn.invocation_url || '(not deployed)'}`);
134
+ info(fn.trigger_type === 'cron'
135
+ ? 'Invocation URL: — (cron functions are not reachable over HTTP)'
136
+ : `Invocation URL: ${fn.invocation_url || '(not deployed)'}`);
132
137
  info(`Created: ${fn.created_at}`);
133
138
  info(`Updated: ${fn.updated_at}`);
134
139
  }
@@ -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>;
@@ -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
- function parseContextFlag(flags) {
107
- if (typeof flags.context !== 'string' || !flags.context)
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(flags.context);
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
- error('--context must be a JSON object');
119
+ }
120
+ error(`${flagName} must be a JSON object`);
114
121
  return undefined;
115
122
  }
116
123
  catch {
117
- error('--context must be valid JSON');
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>"] [--context <json>] ["<source text>"] [--tier <t>] [--org <id>]');
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 --context.');
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
- context: ctx,
304
+ facts: ctx,
305
+ directives: parseDirectivesFlag(flags),
290
306
  prompt: promptText || undefined,
291
307
  tier: tierFromFlag(flags),
292
308
  }));
@@ -15,6 +15,12 @@ export const EXPOSES = [
15
15
  'POST /task/orgs/{org_id}/tasks/{id}/fail',
16
16
  'POST /task/orgs/{org_id}/tasks/{id}/resolve',
17
17
  ];
18
+ // --source meant five different things across the CLI. Here it is --origin;
19
+ // the old name still works and is undocumented. See crm/pagination.ts.
20
+ function taskOrigin(flags) {
21
+ const v = flags.origin !== undefined ? flags.origin : flags.source;
22
+ return typeof v === 'string' ? v : undefined;
23
+ }
18
24
  export const SCHEMA = {
19
25
  body: 'string',
20
26
  importance: 'string',
@@ -24,6 +30,8 @@ export const SCHEMA = {
24
30
  'depends-on': 'string',
25
31
  'dedup-key': 'string',
26
32
  'resolve-on': 'string',
33
+ origin: 'string',
34
+ // Deprecated alias for --origin; undocumented, removable next minor.
27
35
  source: 'string',
28
36
  status: 'string',
29
37
  limit: 'number',
@@ -106,7 +114,7 @@ export async function create(description, flags) {
106
114
  dependsOn: _splitList(flags['depends-on']),
107
115
  dedupKey: typeof flags['dedup-key'] === 'string' ? flags['dedup-key'] : undefined,
108
116
  resolveOn,
109
- source: typeof flags.source === 'string' ? flags.source : undefined,
117
+ source: taskOrigin(flags),
110
118
  });
111
119
  if (flags.json) {
112
120
  printJson(t);
@@ -125,7 +133,7 @@ export async function list(flags) {
125
133
  tag: typeof flags.tag === 'string' ? flags.tag : undefined,
126
134
  importance: typeof flags.importance === 'string' ? flags.importance : undefined,
127
135
  assignee: typeof flags.assignee === 'string' ? flags.assignee : undefined,
128
- source: typeof flags.source === 'string' ? flags.source : undefined,
136
+ source: taskOrigin(flags),
129
137
  limit: typeof flags.limit === 'number' ? flags.limit : undefined,
130
138
  });
131
139
  if (flags.json) {
@@ -235,8 +243,8 @@ export async function cancel(id, flags) {
235
243
  }
236
244
  // ── Dispatcher ───────────────────────────────────────────────────────────────
237
245
  const SUBCOMMAND_USAGE = {
238
- 'create': 'myapi task create "<description>" [--body <md|@file>] [--importance <i>] [--due <rfc3339>] [--assignee <email>] [--tag <t,t>] [--depends-on <id,id>] [--dedup-key <k>] [--resolve-on <event[:field=value]>] [--source <s>] [--org <id>]',
239
- 'list': 'myapi task list [--status <s>] [--tag <t>] [--importance <i>] [--assignee <email>] [--source <s>] [--limit <n>] [--org <id>] [--json]',
246
+ 'create': 'myapi task create "<description>" [--body <md|@file>] [--importance <i>] [--due <rfc3339>] [--assignee <email>] [--tag <t,t>] [--depends-on <id,id>] [--dedup-key <k>] [--resolve-on <event[:field=value]>] [--origin <s>] [--org <id>]',
247
+ 'list': 'myapi task list [--status <s>] [--tag <t>] [--importance <i>] [--assignee <email>] [--origin <s>] [--limit <n>] [--org <id>] [--json]',
240
248
  'get': 'myapi task get <id> [--body] [--org <id>] [--json]\n\n--body additionally fetches the Markdown body tier (a separate read).',
241
249
  'claim': 'myapi task claim <id> [--lease <seconds>] [--worker <name>] [--org <id>]',
242
250
  'extend': 'myapi task extend <id> [--lease <seconds>] [--org <id>]',
@@ -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'],
@@ -42,6 +42,7 @@ const COMMAND_MODULES = [
42
42
  './commands/payments.js',
43
43
  './commands/container.js',
44
44
  './commands/git.js',
45
+ './commands/feedback.js',
45
46
  './commands/queue.js',
46
47
  './commands/task.js',
47
48
  './commands/doctor.js',
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
  Auth, organizations, and billing hub. Start here to get an api_key and org_id — every other service depends on both.
6
6
  triggers: [api key, account, organization, org, billing, balance, topup, credits, setup, defaults, brand, sync brand, doctor, health check, is my org healthy]
7
- checksum: sha256-80bee1daf601eb33abc5d66b7f8053b20abec1c9c0ec07f26e86e26a2d3bdb78
7
+ checksum: sha256-fab324491446026e1600ac531bfbb164c10d46a617e528e6886be74b7dbdb4b1
8
8
  ---
9
9
 
10
10
  # MyApiHQ
@@ -98,4 +98,28 @@ Each org gets a free preview subdomain (`*.makeautonomous.com`) usable before re
98
98
  - API keys have format `hq_live_...` and are sent as `Authorization: Bearer <key>`.
99
99
  - `org sync-brand` is async (scrapes the site, polls the job).
100
100
 
101
+ ## Minting a least-privilege API key
102
+
103
+ A key's authority is inline and always a subset of the key that mints it, so
104
+ you can hand work a key that cannot exceed its job:
105
+
106
+ ```bash
107
+ myapi keys create --name ci --grant funnel:write,storage:read
108
+ myapi keys create --name readonly --grant '*:read' # read anything, write nothing
109
+ myapi keys create --name billing-fn --org <id> --grant email --spend-cap 25
110
+ ```
111
+
112
+ - `--grant <list>` — `slot:read` / `slot:write`; a bare slot means write, `*`
113
+ means all. **Omitting `--grant` mints an unrestricted key.**
114
+ - `--org <id>` — lock it to one org. Omit for account-wide.
115
+ - `--spend-cap <usd>` — hard ceiling; `0` means the key cannot spend at all.
116
+ - `keys revoke-all --kind function|manual|account` narrows the kill switch.
117
+
118
+ ## Org profile fields
119
+
120
+ `myapi org create <name>` also takes `--tagline`, `--description`,
121
+ `--business-sector` and `--logo-url`. They populate the org's public profile
122
+ and the funnel created alongside it, so setting them at create avoids editing
123
+ two places later.
124
+
101
125
  Run `myapi --help` or `myapi <command> --help` for full flag reference.
@@ -4,7 +4,7 @@ version: 1.0.0
4
4
  description: >
5
5
  Saved audiences = named Goldfox-filter snapshots over the people or company database. Build a target list once, name it, reuse it across campaigns, refresh to re-evaluate against current data. The persistence layer on top of my-people-api + my-company-api.
6
6
  triggers: [audience, segment, target list, saved filter, goldfox, lead list, abm list, refresh, members, prospect database]
7
- checksum: sha256-5d9a2731d587c476eff2955241001fecb8481bbc5e567f06b8e9e76a079eda92
7
+ checksum: sha256-a2c7142e880932ac2aa53cdb29b8632ce2c4d5ef33c3f8b6df705f7e987ab6af
8
8
  ---
9
9
 
10
10
  # MyAudienceAPI
@@ -70,7 +70,7 @@ Allowed values:
70
70
  <!-- generated:start -->
71
71
  | Command | What it does |
72
72
  |---|---|
73
- | `myapi audience create <name> --source <people\|company> --filter '<json>' [--description <text>]` | Save a Goldfox filter as a named audience; returns id + initial member_count |
73
+ | `myapi audience create <name> --from <people\|company> --filter '<json>' [--description <text>]` | Save a Goldfox filter as a named audience; returns id + initial member_count |
74
74
  | `myapi audience list` | List all audiences in the org |
75
75
  | `myapi audience get <id>` | Single audience (name, filter, member_count, timestamps) |
76
76
  | `myapi audience update <id> [--name <x>] [--description <y>] [--filter '<json>']` | Patch name/description/filter; member_count re-evaluates if filter changes |
@@ -84,7 +84,7 @@ Allowed values:
84
84
  ```bash
85
85
  # 1. Create the audience
86
86
  AID=$(myapi audience create "EU decision makers w/ corporate emails" \
87
- --source people \
87
+ --from people \
88
88
  --filter '{"seniority":["c_level","vp_director"],"country":["DE","FR","GB"],"email_type":["corporate"]}' \
89
89
  --json | jq -r .id)
90
90
 
@@ -108,7 +108,7 @@ myapi audience delete $AID
108
108
  ```bash
109
109
  # Build with quality controls — definitive links + registered companies + careers signal
110
110
  AID=$(myapi audience create "EU growth-stage decision makers" \
111
- --source people \
111
+ --from people \
112
112
  --filter '{"seniority":["c_level","vp_director"],"country":["DE","FR","GB"],"email_type":["corporate"],"min_link_confidence":0.9,"has_careers_page":true,"is_registered_entity":true}' \
113
113
  --json | jq -r .id)
114
114
 
@@ -123,7 +123,7 @@ myapi audience refresh $AID
123
123
 
124
124
  ```bash
125
125
  myapi audience create "EU growth-stage SaaS accounts" \
126
- --source company \
126
+ --from company \
127
127
  --filter '{"country":["DE","FR","GB","NL"],"has_careers_page":true,"has_decision_maker":true,"is_registered_entity":true,"min_source_count":3}'
128
128
  ```
129
129
  <!-- llm:end -->
@@ -4,7 +4,7 @@ version: 1.0.0
4
4
  description: >
5
5
  Add authentication to apps you build on MyAPI — a managed OIDC identity provider for your app's END USERS (à la Kinde/Auth0). One auth tenant per org; register OIDC clients; sign users in with managed Google or the hosted login page; verify RS256 tokens against the tenant JWKS.
6
6
  triggers: [auth, authentication, login, sign-in, oidc, oauth, jwt, jwks, sso, google sign-in, user accounts, identity provider, kinde, auth0, clerk]
7
- checksum: sha256-b5eca82bb59647677fdb5796778329160572598ac6914887768fd33529cdfe84
7
+ checksum: sha256-5eeacc463d7323f85a330639b7073f735ccc80bddfb8217c9c15c56989b8a99a
8
8
  ---
9
9
 
10
10
  # MyAuthAPI
@@ -114,4 +114,11 @@ myapi auth client list
114
114
  `http://localhost…` for local dev).
115
115
  - `402 INSUFFICIENT_FUNDS` = empty wallet → `myapi billing topup <amount>` (or keep it funded automatically: `myapi billing auto-recharge set`). `402 SPEND_CAP_EXCEEDED` = you hit your account spend ceiling → raise it with `myapi billing spend-cap`.
116
116
 
117
+ ## Anonymous accounts
118
+
119
+ `myapi account setup --anonymous` skips registration and creates an account
120
+ with no email — for throwaway or machine-owned orgs. It cannot receive
121
+ password resets or magic links, so attach a real identity before anything
122
+ depends on it. `myapi status` shows `Type: anonymous`.
123
+
117
124
  **End-to-end example:** `examples/authenticated-app/` walks the full seam — hosted login → token verification → per-user KV record → deployed container — including the parts that cost real users hours (verify the id_token for identity; the access token is a bearer credential for `<issuer>/userinfo`).
@@ -4,7 +4,7 @@ version: 1.0.0
4
4
  description: >
5
5
  Company database backed by the Goldfox crawl. Filter companies by Goldfox confidence tier, country/TLD consistency, behavioral page signals (has_careers_page, has_investors_page, has_shop_page, has_c_level, has_decision_maker), legal-entity status, headcount, and source-URL count. Account-based targeting and B2B firmographics.
6
6
  triggers: [companies, accounts, firmographics, abm, search, filter, goldfox, careers signal, investors, c-level, shop, b2b targeting]
7
- checksum: sha256-67dd4b65a9b0cc7acf7c1e4262bc66d2b58ff11ed227a24299924e0b620712f0
7
+ checksum: sha256-4754de8a66bf13e7e2f2a23acc407ccec147e142abf8adc670ae7962ca860a74
8
8
  ---
9
9
 
10
10
  # MyCompanyAPI
@@ -90,7 +90,7 @@ myapi company get auroracloud.com --include-people 5
90
90
  ```bash
91
91
  # Save filter as audience (companies)
92
92
  AID=$(myapi audience create "EU growth-stage SaaS" \
93
- --source company \
93
+ --from company \
94
94
  --filter '{"country":["DE","FR","GB","NL"],"has_careers_page":true,"has_decision_maker":true,"min_source_count":3}' \
95
95
  --json | jq -r .id)
96
96
 
@@ -105,6 +105,6 @@ myapi audience members $AID --limit 50 --json > accounts.json
105
105
  - `seniority`, `email_type`, and `min_link_confidence` are people-only — passing them on company search is silently ignored.
106
106
  - `include_people` only works on company search/get; people-source already embeds company by default.
107
107
  - `keyword` is a substring match on the company's **domain** — use `--keyword stripe` to find domains containing "stripe".
108
- - For a persistent account list, use `my-audience-api` with `--source company`.
108
+ - For a persistent account list, use `my-audience-api` with `--from company`.
109
109
 
110
110
  Run `myapi company --help` for full flag reference.