@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.
- package/dist/commands/account.js +15 -0
- package/dist/commands/audience.js +7 -5
- package/dist/commands/crm/companies.js +4 -4
- package/dist/commands/crm/contacts.js +4 -4
- package/dist/commands/crm/index.js +3 -0
- package/dist/commands/crm/origin-flag.test.d.ts +1 -0
- package/dist/commands/crm/origin-flag.test.js +38 -0
- package/dist/commands/crm/pagination.d.ts +2 -0
- package/dist/commands/crm/pagination.js +9 -0
- package/dist/commands/domain.js +11 -0
- package/dist/commands/feedback.d.ts +10 -0
- package/dist/commands/feedback.js +173 -0
- package/dist/commands/flag-reachability.test.d.ts +1 -0
- package/dist/commands/flag-reachability.test.js +274 -0
- package/dist/commands/fn.js +7 -2
- package/dist/commands/llm.d.ts +1 -0
- package/dist/commands/llm.js +25 -9
- package/dist/commands/task.js +12 -4
- package/dist/completion.js +2 -1
- package/dist/exposes.test.js +1 -0
- package/dist/index.js +7 -0
- package/dist/skills/my-api-hq/SKILL.md +25 -1
- package/dist/skills/my-audience-api/SKILL.md +5 -5
- package/dist/skills/my-auth-api/SKILL.md +8 -1
- package/dist/skills/my-company-api/SKILL.md +3 -3
- package/dist/skills/my-container-api/SKILL.md +25 -4
- package/dist/skills/my-crm-api/SKILL.md +6 -6
- package/dist/skills/my-database-api/SKILL.md +22 -1
- package/dist/skills/my-domain-api/SKILL.md +18 -1
- package/dist/skills/my-email-api/SKILL.md +36 -1
- package/dist/skills/my-function-api/SKILL.md +35 -1
- package/dist/skills/my-funnel-api/SKILL.md +6 -3
- package/dist/skills/my-git-api/SKILL.md +7 -1
- package/dist/skills/my-llm-api/SKILL.md +17 -4
- package/dist/skills/my-people-api/SKILL.md +3 -3
- package/dist/skills/my-pixel-api/SKILL.md +12 -1
- package/dist/skills/my-storage-api/SKILL.md +31 -2
- package/dist/skills/my-task-api/SKILL.md +9 -1
- package/dist/skills/my-webhook-api/SKILL.md +12 -1
- 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
|
+
});
|
package/dist/commands/fn.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
}
|
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/commands/task.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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]>] [--
|
|
239
|
-
'list': 'myapi task list [--status <s>] [--tag <t>] [--importance <i>] [--assignee <email>] [--
|
|
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>]',
|
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
|
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-
|
|
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-
|
|
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> --
|
|
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
|
-
--
|
|
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
|
-
--
|
|
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
|
-
--
|
|
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-
|
|
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-
|
|
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
|
-
--
|
|
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 `--
|
|
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.
|