@myapihq/cli 1.0.27 → 1.0.29

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.
@@ -3,7 +3,7 @@ import * as os from 'os';
3
3
  import * as path from 'path';
4
4
  import * as readline from 'readline';
5
5
 
6
- import { loadConfig, saveConfig } from '../config.js';
6
+ import { loadConfig, saveConfig, addAccount, loadFullConfig } from '../config.js';
7
7
  import { info, success } from '../output.js';
8
8
 
9
9
  const API_BASE = process.env.MYAPI_API_BASE ?? 'https://api.myapihq.com';
@@ -54,30 +54,29 @@ export async function installSkills(): Promise<void> {
54
54
  return;
55
55
  }
56
56
 
57
- fs.mkdirSync(SKILLS_CANONICAL, { recursive: true });
58
-
57
+ // Each bundled file is <skill-name>.md — install as <skill-name>/SKILL.md
59
58
  const files = fs.readdirSync(BUNDLED_SKILLS_DIR).filter(f => f.endsWith('.md'));
60
- for (const file of files) {
61
- const src = path.join(BUNDLED_SKILLS_DIR, file);
62
- const dst = path.join(SKILLS_CANONICAL, file);
63
- fs.copyFileSync(src, dst);
59
+ const skills = files.map(f => f.replace(/\.md$/, ''));
60
+
61
+ // Write canonical copies: ~/.agents/skills/myapi/<skill-name>/SKILL.md
62
+ for (const skill of skills) {
63
+ const skillDir = path.join(SKILLS_CANONICAL, skill);
64
+ fs.mkdirSync(skillDir, { recursive: true });
65
+ fs.copyFileSync(path.join(BUNDLED_SKILLS_DIR, `${skill}.md`), path.join(skillDir, 'SKILL.md'));
64
66
  }
65
67
 
68
+ // Symlink each skill directory into agent dirs
66
69
  for (const [agent, dir] of Object.entries(AGENT_DIRS)) {
67
70
  try {
68
71
  fs.mkdirSync(dir, { recursive: true });
69
- for (const file of files) {
70
- const link = path.join(dir, file);
71
- const target = path.join(SKILLS_CANONICAL, file);
72
+ for (const skill of skills) {
73
+ const link = path.join(dir, skill);
74
+ const target = path.join(SKILLS_CANONICAL, skill);
72
75
  try {
73
- if (fs.existsSync(link)) {
74
- const stat = fs.lstatSync(link);
75
- if (stat.isSymbolicLink() && fs.readlinkSync(link) === target) continue;
76
- fs.unlinkSync(link);
77
- }
76
+ try { fs.rmSync(link, { recursive: true, force: true }); } catch { /* ignore */ }
78
77
  fs.symlinkSync(target, link);
79
78
  } catch {
80
- // Non-fatal: skip this agent dir if permissions are wrong.
79
+ // Non-fatal.
81
80
  }
82
81
  }
83
82
  info(` ✓ ${agent}`);
@@ -96,6 +95,7 @@ async function registeredFlow(rl: readline.Interface): Promise<{
96
95
  account_id: string;
97
96
  default_org: string;
98
97
  default_funnel: string;
98
+ email: string;
99
99
  }> {
100
100
  const email = (await ask(rl, '› Email? ')).trim();
101
101
 
@@ -110,7 +110,7 @@ async function registeredFlow(rl: readline.Interface): Promise<{
110
110
  default_funnel: string;
111
111
  };
112
112
 
113
- return data;
113
+ return { ...data, email };
114
114
  }
115
115
 
116
116
  // ---------------------------------------------------------------------------
@@ -143,28 +143,33 @@ export async function setup() {
143
143
 
144
144
  const existing = loadConfig();
145
145
 
146
- // Already configured — just verify the key is still valid.
146
+ // Already configured — ask before adding a new account.
147
147
  if (existing?.api_key) {
148
- try {
149
- const res = await fetch(`${API_BASE}/hq/account/me`, {
150
- headers: { Authorization: `Bearer ${existing.api_key}` },
151
- });
152
- if (res.ok) {
153
- info(`› Already configured · account ${existing.account_id}`);
154
- // Re-ask skills only if preference was never recorded.
155
- if (existing.skills_installed === undefined) {
156
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
157
- const ans = await ask(rl, '› Install the MyAPI skills pack? (Y/n) ');
158
- rl.close();
159
- if (yn(ans)) {
160
- await installSkills();
161
- success('› Skills installed.');
162
- }
163
- saveConfig({ ...existing, skills_installed: yn(ans) });
148
+ const full = loadFullConfig();
149
+ const total = full?.accounts.length ?? 1;
150
+ const activeLabel = existing.email ?? existing.account_id;
151
+ const activeType = existing.is_anonymous ? 'anonymous' : 'registered';
152
+ const othersNote = total > 1 ? ` · ${total - 1} more saved` : '';
153
+
154
+ info(`› Connected: ${activeLabel} (${activeType})${othersNote}`);
155
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
156
+ const add = await ask(rl, '› Connect a new account? (y/N) ');
157
+ if (!yn(add, false)) {
158
+ if (!existing.skills_installed) {
159
+ const ans = await ask(rl, '› Install the MyAPI skills pack? (Y/n) ');
160
+ rl.close();
161
+ if (yn(ans)) {
162
+ await installSkills();
163
+ success('› Skills installed.');
164
+ saveConfig({ ...existing, skills_installed: true });
164
165
  }
165
- return;
166
+ } else {
167
+ rl.close();
166
168
  }
167
- } catch { /* fall through to full setup */ }
169
+ return;
170
+ }
171
+ rl.close();
172
+ // Fall through to setup flow — new account will be added to the list.
168
173
  }
169
174
 
170
175
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
@@ -175,6 +180,7 @@ export async function setup() {
175
180
  let defaultFunnel = '';
176
181
  let subdomainUrl = '';
177
182
  let isAnonymous = false;
183
+ let email = '';
178
184
 
179
185
  try {
180
186
  const createAns = await ask(rl, '› Create an account? (Y/n) ');
@@ -185,6 +191,7 @@ export async function setup() {
185
191
  accountId = data.account_id;
186
192
  defaultOrg = data.default_org;
187
193
  defaultFunnel = data.default_funnel;
194
+ email = data.email;
188
195
  } else {
189
196
  info('› Continuing as anonymous — sites ship to *.makeautonomous.com');
190
197
  const data = await anonymousFlow();
@@ -199,10 +206,11 @@ export async function setup() {
199
206
  const skillsAns = await ask(rl, '› Install the MyAPI skills pack? (Y/n) ');
200
207
  const wantsSkills = yn(skillsAns);
201
208
 
202
- saveConfig({
209
+ addAccount({
203
210
  api_key: apiKey,
204
211
  account_id: accountId,
205
212
  pin: '',
213
+ email: email || undefined,
206
214
  default_org: defaultOrg,
207
215
  default_funnel: defaultFunnel,
208
216
  is_anonymous: isAnonymous,
@@ -215,6 +223,51 @@ export async function setup() {
215
223
  await installSkills();
216
224
  }
217
225
 
226
+ // Validate key and ensure org/funnel defaults are correct.
227
+ try {
228
+ const auth = { Authorization: `Bearer ${apiKey}` };
229
+ const meRes = await fetch(`${API_BASE}/hq/account/me`, { headers: auth });
230
+ if (!meRes.ok) {
231
+ info('› Warning: could not verify API key — check your connection.');
232
+ } else {
233
+ // Verify default org exists; if not, fetch the most recent one.
234
+ let resolvedOrg = defaultOrg;
235
+ let resolvedFunnel = defaultFunnel;
236
+
237
+ const orgRes = await fetch(`${API_BASE}/hq/orgs`, { headers: auth });
238
+ if (orgRes.ok) {
239
+ const orgs = (await orgRes.json() as any)?.data ?? [];
240
+ if (orgs.length > 0) {
241
+ const latest = orgs[orgs.length - 1];
242
+ if (!resolvedOrg || !orgs.find((o: any) => o.id === resolvedOrg)) {
243
+ resolvedOrg = latest.id;
244
+ info(`› Auto-selected org: ${latest.name} (${resolvedOrg})`);
245
+ }
246
+
247
+ // Verify default funnel exists within the org.
248
+ const fRes = await fetch(`${API_BASE}/funnel/orgs/${resolvedOrg}/funnels`, { headers: auth });
249
+ if (fRes.ok) {
250
+ const funnels = (await fRes.json() as any)?.data ?? [];
251
+ if (funnels.length > 0 && (!resolvedFunnel || !funnels.find((f: any) => f.id === resolvedFunnel))) {
252
+ resolvedFunnel = funnels[funnels.length - 1].id;
253
+ info(`› Auto-selected funnel: ${resolvedFunnel}`);
254
+ }
255
+ }
256
+
257
+ if (resolvedOrg !== defaultOrg || resolvedFunnel !== defaultFunnel) {
258
+ const full = loadFullConfig()!;
259
+ const idx = full.active;
260
+ full.accounts[idx].default_org = resolvedOrg;
261
+ full.accounts[idx].default_funnel = resolvedFunnel;
262
+ fs.writeFileSync(path.join(os.homedir(), '.myapi', 'config.json'), JSON.stringify(full, null, 2), { mode: 0o600 });
263
+ defaultOrg = resolvedOrg;
264
+ defaultFunnel = resolvedFunnel;
265
+ }
266
+ }
267
+ }
268
+ }
269
+ } catch { /* non-fatal */ }
270
+
218
271
  if (isAnonymous) {
219
272
  success('› Setup complete. No card, no email, ready to ship.');
220
273
  if (subdomainUrl) info(`› Your funnel: ${subdomainUrl}`);
@@ -1,31 +1,18 @@
1
1
  import { execSync } from 'child_process';
2
- import { loadConfig, saveConfig } from '../config.js';
3
2
  import { info, success } from '../output.js';
4
3
  import { installSkills } from './setup.js';
5
4
 
6
5
  const REGISTRY_URL = 'https://registry.npmjs.org/@myapihq/cli/latest';
7
- const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
8
6
 
9
7
  // checkForUpdate runs silently in the background on every command.
10
8
  // Auto-installs if a newer version is available.
11
9
  export async function checkForUpdate(currentVersion: string): Promise<void> {
12
- const config = loadConfig();
13
- const now = Date.now();
14
-
15
- if (config?.last_update_check && now - config.last_update_check < CHECK_INTERVAL_MS) {
16
- return;
17
- }
18
-
19
10
  try {
20
11
  const res = await fetch(REGISTRY_URL, { signal: AbortSignal.timeout(3000) });
21
12
  if (!res.ok) return;
22
13
  const data = await res.json() as { version?: string };
23
14
  const latest = data.version;
24
15
 
25
- if (config) {
26
- saveConfig({ ...config, last_update_check: now });
27
- }
28
-
29
16
  if (latest && isNewer(latest, currentVersion)) {
30
17
  info(`\n› New version available (${currentVersion} → ${latest}) — installing…`);
31
18
  execSync('npm install -g @myapihq/cli', { stdio: 'pipe' });
package/src/config.ts CHANGED
@@ -2,56 +2,122 @@ import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import * as os from 'os';
4
4
 
5
- export interface Config {
5
+ export interface AccountEntry {
6
6
  api_key: string;
7
7
  account_id: string;
8
8
  pin: string;
9
+ email?: string;
9
10
  default_org?: string;
10
11
  default_funnel?: string;
11
12
  default_domain?: string;
12
13
  is_anonymous?: boolean;
13
14
  skills_installed?: boolean;
14
- last_update_check?: number;
15
+ }
16
+
17
+ // Flat interface used by all command files — represents the active account.
18
+ export interface Config extends AccountEntry {
15
19
  autocomplete_setup?: boolean;
16
20
  }
17
21
 
18
- const CONFIG_DIR = path.join(os.homedir(), '.myapi');
19
- const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
22
+ export interface FullConfig {
23
+ active: number;
24
+ accounts: AccountEntry[];
25
+ autocomplete_setup?: boolean;
26
+ }
20
27
 
21
- export function loadConfig(): Config | null {
22
- let fileConfig: Partial<Config> = {};
28
+ export const CONFIG_DIR = path.join(os.homedir(), '.myapi');
29
+ export const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
30
+
31
+ function ensureDir() {
32
+ if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
33
+ }
34
+
35
+ function readRaw(): any {
23
36
  try {
24
- if (fs.existsSync(CONFIG_FILE)) {
25
- const data = fs.readFileSync(CONFIG_FILE, 'utf-8');
26
- fileConfig = JSON.parse(data) as Partial<Config>;
27
- }
28
- } catch (err) {
29
- // Ignore read errors
30
- }
37
+ if (fs.existsSync(CONFIG_FILE)) return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
38
+ } catch { /* ignore */ }
39
+ return null;
40
+ }
41
+
42
+ function isFullConfig(raw: any): raw is FullConfig {
43
+ return raw && Array.isArray(raw.accounts);
44
+ }
45
+
46
+ // Migrate a flat (legacy) config to the new multi-account shape.
47
+ function migrate(raw: any): FullConfig {
48
+ const { autocomplete_setup, ...account } = raw;
49
+ return { active: 0, accounts: [account as AccountEntry], autocomplete_setup };
50
+ }
31
51
 
52
+ export function loadFullConfig(): FullConfig | null {
53
+ const raw = readRaw();
54
+ if (!raw) return null;
55
+ return isFullConfig(raw) ? raw : migrate(raw);
56
+ }
57
+
58
+ // loadConfig returns the active account merged with globals — unchanged interface for all callers.
59
+ export function loadConfig(): Config | null {
32
60
  const envKey = process.env.MYAPI_KEY;
33
- if (envKey) {
34
- fileConfig.api_key = envKey;
35
- }
61
+ const full = loadFullConfig();
36
62
 
37
- if (Object.keys(fileConfig).length === 0) {
38
- return null;
39
- }
63
+ if (!full && envKey) return { api_key: envKey, account_id: '', pin: '' };
64
+ if (!full) return null;
65
+
66
+ const active = full.accounts[full.active] ?? full.accounts[0];
67
+ if (!active) return null;
40
68
 
41
- return fileConfig as Config;
69
+ const config: Config = {
70
+ ...active,
71
+ autocomplete_setup: full.autocomplete_setup,
72
+ };
73
+ if (envKey) config.api_key = envKey;
74
+ return config;
42
75
  }
43
76
 
44
77
  export function saveConfig(config: Config): void {
45
- if (!fs.existsSync(CONFIG_DIR)) {
46
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
78
+ ensureDir();
79
+ const full = loadFullConfig() ?? { active: 0, accounts: [] };
80
+ const { autocomplete_setup, ...account } = config;
81
+ if (full.accounts.length === 0) full.accounts.push(account as AccountEntry);
82
+ else full.accounts[full.active] = account as AccountEntry;
83
+ if (autocomplete_setup !== undefined) full.autocomplete_setup = autocomplete_setup;
84
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(full, null, 2), { mode: 0o600 });
85
+ }
86
+
87
+ export function addAccount(account: AccountEntry): number {
88
+ ensureDir();
89
+ const full = loadFullConfig() ?? { active: 0, accounts: [] };
90
+ // Replace if same account_id already exists.
91
+ const existing = full.accounts.findIndex(a => a.account_id === account.account_id);
92
+ if (existing >= 0) {
93
+ full.accounts[existing] = account;
94
+ full.active = existing;
95
+ } else {
96
+ full.accounts.push(account);
97
+ full.active = full.accounts.length - 1;
47
98
  }
48
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
99
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(full, null, 2), { mode: 0o600 });
100
+ return full.active;
101
+ }
102
+
103
+ export function switchAccount(index: number): boolean {
104
+ const full = loadFullConfig();
105
+ if (!full || index < 0 || index >= full.accounts.length) return false;
106
+ full.active = index;
107
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(full, null, 2), { mode: 0o600 });
108
+ return true;
109
+ }
110
+
111
+ export function listAccounts(): Array<AccountEntry & { index: number; active: boolean }> {
112
+ const full = loadFullConfig();
113
+ if (!full) return [];
114
+ return full.accounts.map((a, i) => ({ ...a, index: i, active: i === full.active }));
49
115
  }
50
116
 
51
117
  export function requireConfig(): Config {
52
118
  const config = loadConfig();
53
119
  if (!config || !config.api_key) {
54
- console.error("No API key found. Provide MYAPI_KEY env var or run: myapi setup");
120
+ console.error("No API key found. Provide MYAPI_KEY env var or run: myapi auth setup");
55
121
  process.exit(1);
56
122
  }
57
123
  return config;
package/src/index.ts CHANGED
@@ -12,11 +12,11 @@ import * as keysCmd from './commands/keys.js';
12
12
  import * as billingCmd from './commands/billing.js';
13
13
  import * as orgCmd from './commands/org.js';
14
14
  import * as setupCmd from './commands/setup.js';
15
- import * as configCmd from './commands/config.js';
16
- import * as authCmd from './commands/auth.js';
17
15
  import * as updateCmd from './commands/update.js';
18
16
  import * as domainCmd from './commands/domain.js';
19
17
  import * as funnelCmd from './commands/funnel.js';
18
+ import * as authCmd from './commands/auth.js';
19
+ import * as configCmd from './commands/config.js';
20
20
 
21
21
  async function main() {
22
22
  // Fire-and-forget auto-update check — never blocks the command.
@@ -36,8 +36,8 @@ async function main() {
36
36
  if (args.length === 0) {
37
37
  const config = loadConfig();
38
38
  if (!config?.api_key) {
39
- info('No account found. Run: myapi setup');
40
- process.exit(0);
39
+ info('No account found. Run: myapi auth setup');
40
+ info('');
41
41
  }
42
42
  printHelp();
43
43
  process.exit(0);
@@ -46,13 +46,25 @@ async function main() {
46
46
  const [command, subcommand, ...restArgs] = args;
47
47
  try {
48
48
  switch (command) {
49
- case 'setup':
50
- await setupCmd.setup();
51
- break;
52
49
  case 'auth':
53
- if (subcommand === 'signup') await authCmd.signup();
50
+ if (!subcommand || flags.help) {
51
+ info('Usage: myapi auth <subcommand>\n\nSubcommands:\n setup Configure your account\n whoami Show current account\n signup Upgrade anonymous account to registered\n switch Switch between accounts\n config Manage CLI defaults (org_id, domain…)\n install-skills Install the MyAPI skills pack\n api-keys Manage API keys');
52
+ break;
53
+ }
54
+ if (subcommand === 'setup') await setupCmd.setup();
54
55
  else if (subcommand === 'whoami') await authCmd.whoami();
55
- else info('Usage: myapi auth <signup|whoami>');
56
+ else if (subcommand === 'signup') await authCmd.signup();
57
+ else if (subcommand === 'switch') await authCmd.switchCmd();
58
+ else if (subcommand === 'install-skills') {
59
+ await setupCmd.installSkills();
60
+ success('› Skills installed.');
61
+ } else if (subcommand === 'config') await configCmd.run(restArgs[0], restArgs.slice(1), flags);
62
+ else if (subcommand === 'api-keys') {
63
+ if (!restArgs[0]) { info('Usage: myapi auth api-keys <list|create|revoke>'); break; }
64
+ if (restArgs[0] === 'create') await keysCmd.createNew(flags);
65
+ else if (restArgs[0] === 'list') await keysCmd.list(flags);
66
+ else if (restArgs[0] === 'revoke') await keysCmd.revoke(restArgs[1], flags);
67
+ } else info('Unknown subcommand. Run: myapi auth --help');
56
68
  break;
57
69
  case 'update':
58
70
  await updateCmd.update();
@@ -90,9 +102,6 @@ async function main() {
90
102
  else if (subcommand === 'setup') await billingCmd.setup(flags);
91
103
  else printHelp();
92
104
  break;
93
- case 'config':
94
- await configCmd.run(subcommand, restArgs, flags);
95
- break;
96
105
  case 'domain':
97
106
  await domainCmd.run(subcommand, restArgs, flags);
98
107
  break;
@@ -113,6 +122,13 @@ async function main() {
113
122
  }
114
123
 
115
124
  function printHelp() {
125
+ const config = loadConfig();
126
+ const quickStart = config?.api_key
127
+ ? `Quick start:
128
+ myapi funnel create
129
+ echo '<h1>Hello!</h1>' | myapi funnel push <funnel_id> /`
130
+ : `Quick start:
131
+ myapi setup`;
116
132
  info(`myapi - MyAPI command-line interface
117
133
 
118
134
  Usage: myapi <command> [subcommand] [args]
@@ -122,19 +138,14 @@ Commands:
122
138
  keys Manage API keys
123
139
  billing Check balance and manage billing
124
140
  org Manage organizations
125
- setup Configure credentials and install skills
126
- config Manage CLI defaults like org_id and domain
127
- auth Manage authentication (signup, whoami)
141
+ setup Configure account · whoami · signup · config · install-skills
128
142
  update Update CLI and skills to the latest version
129
143
  domain Manage domain configurations
130
144
  funnel Manage headless funnels and pages
131
145
 
132
146
  Run "myapi <command> --help" for subcommand help.
133
147
 
134
- Quick start:
135
- myapi setup
136
- myapi funnel create
137
- echo '<h1>Hello!</h1>' | myapi funnel push <funnel_id> /`);
148
+ ${quickStart}`);
138
149
  }
139
150
 
140
151
  main().catch(err => { error(err.message || (typeof err === 'object' ? JSON.stringify(err) : String(err))); });
@@ -11,7 +11,7 @@ These endpoints manage the database records and structural configuration of funn
11
11
 
12
12
  - `GET /funnel/orgs/{org_id}/funnels`
13
13
  Lists all funnels for the specified organization.
14
- - `POST /funnel/orgs/{org_id}/funnels/create-raw`
14
+ - `POST /funnel/orgs/{org_id}/funnels`
15
15
  Creates a new funnel entry. Expects basic configuration metadata (name, domain, etc.). Body: `{ "domain": "example.com" }`
16
16
  - `GET /funnel/orgs/{org_id}/funnels/{id}`
17
17
  Retrieves the metadata and configuration details of a specific funnel.