@f-o-h/cli 0.1.2 → 0.1.3

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 (3) hide show
  1. package/README.md +5 -4
  2. package/dist/foh.js +170 -48
  3. package/package.json +39 -39
package/README.md CHANGED
@@ -4,7 +4,7 @@ AI-operator provisioning CLI for Front Of House.
4
4
 
5
5
  Public mirror: https://github.com/iiko38/front-of-house-cli
6
6
 
7
- Current published baseline: `@f-o-h/cli@0.1.2`
7
+ Current published baseline: `@f-o-h/cli@0.1.3`
8
8
 
9
9
  This mirror is a generated release artifact. The private product monorepo is not
10
10
  published here, and no open-source license is granted unless stated separately.
@@ -51,8 +51,9 @@ foh setup --org <org-id> --agent-template <template-id> --agent-name "Demo Agent
51
51
  ```
52
52
 
53
53
  `auth signup --web` opens the console signup page when possible and always
54
- prints the fallback URL. `auth login --web` does the same for sign-in. The CLI
55
- still requires explicit credential auth until device-code browser-token
56
- exchange is available.
54
+ prints the fallback URL. `auth login --web` starts browser device
55
+ authorization, opens `/cli-auth`, waits for console approval, and stores the
56
+ returned short-lived token. Credential auth remains available as fallback.
57
57
 
58
58
  The CLI defaults to the production API at `https://api.frontofhouse.okii.uk`.
59
+
package/dist/foh.js CHANGED
@@ -10163,11 +10163,11 @@ function buildCliAuthFallbackInstructions(signInUrl) {
10163
10163
  human: [
10164
10164
  `Open ${signInUrl}`,
10165
10165
  "Sign in to Front Of House.",
10166
- "Return to the terminal and authenticate the CLI with email/password until browser-token exchange is available."
10166
+ "Return to the terminal. If device approval is unavailable, authenticate the CLI with email/password."
10167
10167
  ],
10168
10168
  ai_agent: [
10169
10169
  "Show the sign_in_url to the user if browser opening is unavailable.",
10170
- "Ask the user for FOH email/password or an approved service-token path.",
10170
+ "Prefer browser device approval. Ask for email/password only if device approval is unavailable.",
10171
10171
  "Run the explicit CLI auth commands in next_commands.",
10172
10172
  "Never scrape browser cookies or local storage."
10173
10173
  ]
@@ -10179,7 +10179,7 @@ function buildCliSignupFallbackInstructions(signUpUrl) {
10179
10179
  `Open ${signUpUrl}`,
10180
10180
  "Create a Front Of House account.",
10181
10181
  "Confirm your email if prompted.",
10182
- "Return to the terminal and run `foh auth login --web --json` or credential login."
10182
+ "Return to the terminal and run `foh auth login --web --json`."
10183
10183
  ],
10184
10184
  ai_agent: [
10185
10185
  "Show the sign_up_url to the user if browser opening is unavailable.",
@@ -10227,11 +10227,22 @@ function emitBrowserAuthLink(opts) {
10227
10227
  opener_command: openResult.command,
10228
10228
  opener_error: openResult.error ?? null,
10229
10229
  cli_auth_required: true,
10230
- note: "Browser sign-in opens the console. Until device-code auth is implemented, authenticate this CLI with the explicit credential command after sign-in.",
10230
+ note: "Browser device auth is unavailable from this API. Authenticate this CLI with the explicit credential command after sign-in.",
10231
10231
  next_commands: nextCommands,
10232
10232
  text_fallback: buildCliAuthFallbackInstructions(signInUrl)
10233
10233
  }, { json: opts.json ?? false });
10234
10234
  }
10235
+ function writeDeviceProgress(event, jsonMode) {
10236
+ const line = jsonMode ? JSON.stringify({ event: "device_auth_started", ...event }, null, 2) : [
10237
+ "Browser auth started.",
10238
+ `Open: ${event["verification_uri_complete"]}`,
10239
+ `Code: ${event["user_code"]}`,
10240
+ "Waiting for browser approval..."
10241
+ ].join("\n");
10242
+ const stream = jsonMode ? process.stderr : process.stdout;
10243
+ stream.write(`${line}
10244
+ `);
10245
+ }
10235
10246
  function emitBrowserSignupLink(opts) {
10236
10247
  const consoleUrl = resolveConsoleBaseUrl(opts.consoleUrl);
10237
10248
  const signUpUrl = buildConsoleSignUpUrl(consoleUrl);
@@ -10307,13 +10318,154 @@ async function maybeSelectDefaultOrg(orgs, jsonMode) {
10307
10318
  `);
10308
10319
  }
10309
10320
  }
10321
+ async function fetchOrgMemberships(apiUrl, token) {
10322
+ try {
10323
+ const orgsRes = await fetch(`${apiUrl}/v1/console/auth/my-orgs`, {
10324
+ headers: { Authorization: `Bearer ${token}` }
10325
+ });
10326
+ if (orgsRes.ok) {
10327
+ const orgsData = await orgsRes.json();
10328
+ return {
10329
+ orgs: Array.isArray(orgsData.orgs) ? orgsData.orgs : [],
10330
+ available: true
10331
+ };
10332
+ }
10333
+ } catch {
10334
+ }
10335
+ return { orgs: [], available: false };
10336
+ }
10337
+ async function storeAuthenticatedSession(params) {
10338
+ const { orgs, available } = await fetchOrgMemberships(params.apiUrl, params.token);
10339
+ let autoOrgId;
10340
+ const usableOrgs = orgs.filter((org) => isUsableOrgId(org.org_id));
10341
+ if (usableOrgs.length === 1) {
10342
+ autoOrgId = usableOrgs[0].org_id;
10343
+ } else if (usableOrgs.length > 1) {
10344
+ autoOrgId = await maybeSelectDefaultOrg(orgs, params.jsonMode);
10345
+ }
10346
+ saveCredentials({
10347
+ apiUrl: params.apiUrl,
10348
+ token: params.token,
10349
+ expiresAt: params.expiresAt,
10350
+ orgId: autoOrgId
10351
+ });
10352
+ const output = {
10353
+ status: "authenticated",
10354
+ apiUrl: params.apiUrl,
10355
+ expires_at: params.expiresAt
10356
+ };
10357
+ if (autoOrgId) {
10358
+ output["default_org_id"] = autoOrgId;
10359
+ output["note"] = "Default org stored; --org flag is now optional.";
10360
+ } else if (orgs.length > 1) {
10361
+ output["note"] = `${orgs.length} orgs found. Run: foh org use --org <id> to set a default.`;
10362
+ } else if (orgs.length === 0 && available) {
10363
+ output["note"] = "No orgs found. Run: foh org create --name <name> to create one.";
10364
+ } else {
10365
+ output["note"] = "Authenticated, but org discovery is unavailable. Run: foh org list when API connectivity is healthy.";
10366
+ }
10367
+ return output;
10368
+ }
10369
+ function sleep(ms) {
10370
+ return new Promise((resolve5) => setTimeout(resolve5, ms));
10371
+ }
10372
+ async function runDeviceLogin(opts) {
10373
+ const jsonMode = Boolean(opts.json);
10374
+ let startRes;
10375
+ try {
10376
+ startRes = await fetch(`${opts.apiUrl}/v1/console/auth/device/start`, {
10377
+ method: "POST",
10378
+ headers: { "Content-Type": "application/json" },
10379
+ body: JSON.stringify({ client: "foh-cli" })
10380
+ });
10381
+ } catch {
10382
+ emitBrowserAuthLink({
10383
+ consoleUrl: opts.consoleUrl,
10384
+ json: opts.json
10385
+ });
10386
+ return;
10387
+ }
10388
+ if (!startRes.ok) {
10389
+ emitBrowserAuthLink({
10390
+ consoleUrl: opts.consoleUrl,
10391
+ json: opts.json
10392
+ });
10393
+ return;
10394
+ }
10395
+ const start = await startRes.json();
10396
+ const openResult = openUrl(start.verification_uri_complete);
10397
+ const startedPacket = {
10398
+ status: "browser_device_auth_started",
10399
+ sign_in_url: start.verification_uri_complete,
10400
+ verification_uri: start.verification_uri,
10401
+ verification_uri_complete: start.verification_uri_complete,
10402
+ user_code: start.user_code,
10403
+ opened: openResult.attempted,
10404
+ opener_command: openResult.command,
10405
+ opener_error: openResult.error ?? null,
10406
+ expires_in: start.expires_in,
10407
+ poll_interval_seconds: start.interval
10408
+ };
10409
+ if (opts.wait === false) {
10410
+ format({
10411
+ ...startedPacket,
10412
+ next_commands: [
10413
+ "Complete approval in the opened browser window.",
10414
+ "Then rerun: foh auth login --web --json"
10415
+ ]
10416
+ }, { json: jsonMode });
10417
+ return;
10418
+ }
10419
+ writeDeviceProgress(startedPacket, jsonMode);
10420
+ const timeoutSeconds = Math.max(1, Number(opts.timeoutSeconds ?? start.expires_in) || start.expires_in);
10421
+ const deadline = Date.now() + Math.min(timeoutSeconds, start.expires_in) * 1e3;
10422
+ const intervalMs = Math.max(0.05, Number(start.interval) || 2) * 1e3;
10423
+ while (Date.now() < deadline) {
10424
+ await sleep(intervalMs);
10425
+ const pollRes = await fetch(`${opts.apiUrl}/v1/console/auth/device/poll`, {
10426
+ method: "POST",
10427
+ headers: { "Content-Type": "application/json" },
10428
+ body: JSON.stringify({ device_code: start.device_code })
10429
+ });
10430
+ const poll = await pollRes.json().catch(() => ({}));
10431
+ if (pollRes.status === 202 || poll.status === "authorization_pending") {
10432
+ continue;
10433
+ }
10434
+ if (pollRes.ok && poll.token && poll.expires_at) {
10435
+ const output = await storeAuthenticatedSession({
10436
+ apiUrl: opts.apiUrl,
10437
+ token: poll.token,
10438
+ expiresAt: poll.expires_at,
10439
+ jsonMode
10440
+ });
10441
+ format({
10442
+ ...output,
10443
+ auth_method: "browser_device"
10444
+ }, { json: jsonMode });
10445
+ return;
10446
+ }
10447
+ throw new FohError({
10448
+ step: "auth.login.web",
10449
+ error: poll.error || poll.code || `HTTP ${pollRes.status}`,
10450
+ remediation: "Restart browser auth with: foh auth login --web"
10451
+ });
10452
+ }
10453
+ throw new FohError({
10454
+ step: "auth.login.web",
10455
+ error: "Timed out waiting for browser approval",
10456
+ remediation: "Complete browser approval faster, or rerun: foh auth login --web"
10457
+ });
10458
+ }
10310
10459
  function registerAuth(program3) {
10311
10460
  const auth = program3.command("auth").description("Manage CLI authentication");
10312
- auth.command("login").description("Authenticate with the FOH API and store a token locally").option("--email <email>", "FOH account email").option("--password <password>", "FOH account password").option("--web", "Open browser sign-in and print text fallback commands").option("--browser", "Alias for --web").option("--wizard", "Run guided login wizard prompts").option("--console-url <url>", "Console sign-in URL override").option("--api-url <url>", "Internal API base URL override (operators only)").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
10461
+ auth.command("login").description("Authenticate with the FOH API and store a token locally").option("--email <email>", "FOH account email").option("--password <password>", "FOH account password").option("--web", "Open browser sign-in and print text fallback commands").option("--browser", "Alias for --web").option("--wizard", "Run guided login wizard prompts").option("--console-url <url>", "Console sign-in URL override").option("--api-url <url>", "Internal API base URL override (operators only)").option("--timeout-seconds <seconds>", "Maximum seconds to wait for browser approval").option("--no-wait", "Only print/open the browser approval link; do not poll").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
10313
10462
  if ((opts.web || opts.browser) && !opts.email && !opts.password) {
10314
- emitBrowserAuthLink({
10463
+ await runDeviceLogin({
10464
+ apiUrl: resolveApiBaseUrl(opts.apiUrl),
10315
10465
  consoleUrl: opts.consoleUrl,
10316
- json: opts.json
10466
+ json: opts.json,
10467
+ wait: opts.wait,
10468
+ timeoutSeconds: opts.timeoutSeconds
10317
10469
  });
10318
10470
  return;
10319
10471
  }
@@ -10347,42 +10499,12 @@ function registerAuth(program3) {
10347
10499
  });
10348
10500
  }
10349
10501
  const data = await res.json();
10350
- let orgs = [];
10351
- let orgDiscoveryAvailable = false;
10352
- try {
10353
- const orgsRes = await fetch(`${apiUrl}/v1/console/auth/my-orgs`, {
10354
- headers: { Authorization: `Bearer ${data.token}` }
10355
- });
10356
- if (orgsRes.ok) {
10357
- const orgsData = await orgsRes.json();
10358
- orgs = Array.isArray(orgsData.orgs) ? orgsData.orgs : [];
10359
- orgDiscoveryAvailable = true;
10360
- }
10361
- } catch {
10362
- }
10363
- let autoOrgId;
10364
- const usableOrgs = orgs.filter((org) => isUsableOrgId(org.org_id));
10365
- if (usableOrgs.length === 1) {
10366
- autoOrgId = usableOrgs[0].org_id;
10367
- } else if (usableOrgs.length > 1) {
10368
- autoOrgId = await maybeSelectDefaultOrg(orgs, Boolean(opts.json));
10369
- }
10370
- saveCredentials({ apiUrl, token: data.token, expiresAt: data.expires_at, orgId: autoOrgId });
10371
- const output = {
10372
- status: "authenticated",
10502
+ const output = await storeAuthenticatedSession({
10373
10503
  apiUrl,
10374
- expires_at: data.expires_at
10375
- };
10376
- if (autoOrgId) {
10377
- output["default_org_id"] = autoOrgId;
10378
- output["note"] = "Default org stored; --org flag is now optional.";
10379
- } else if (orgs.length > 1) {
10380
- output["note"] = `${orgs.length} orgs found. Run: foh org use --org <id> to set a default.`;
10381
- } else if (orgs.length === 0 && orgDiscoveryAvailable) {
10382
- output["note"] = "No orgs found. Run: foh org create --name <name> to create one.";
10383
- } else {
10384
- output["note"] = "Authenticated, but org discovery is unavailable. Run: foh org list when API connectivity is healthy.";
10385
- }
10504
+ token: data.token,
10505
+ expiresAt: data.expires_at,
10506
+ jsonMode: Boolean(opts.json)
10507
+ });
10386
10508
  format(output, { json: opts.json ?? false });
10387
10509
  }));
10388
10510
  auth.command("signup").description("Open account signup and print text fallback commands").option("--web", "Open browser signup and print text fallback commands", true).option("--browser", "Alias for --web").option("--console-url <url>", "Console signup URL override").option("--json", "Output as JSON").action((opts) => {
@@ -10779,10 +10901,10 @@ async function pollUntil(check2, opts) {
10779
10901
  const label = opts.label ?? step;
10780
10902
  process.stderr.write(import_picocolors2.default.dim(` ${label}: waiting... (${Math.round(elapsed / 1e3)}s)
10781
10903
  `));
10782
- await sleep(opts.intervalMs);
10904
+ await sleep2(opts.intervalMs);
10783
10905
  }
10784
10906
  }
10785
- function sleep(ms) {
10907
+ function sleep2(ms) {
10786
10908
  return new Promise((resolve5) => setTimeout(resolve5, ms));
10787
10909
  }
10788
10910
 
@@ -32228,7 +32350,7 @@ var StdioServerTransport = class {
32228
32350
  };
32229
32351
 
32230
32352
  // src/lib/cli-version.ts
32231
- var CLI_VERSION = "0.1.2";
32353
+ var CLI_VERSION = "0.1.3";
32232
32354
 
32233
32355
  // src/commands/mcp-serve.ts
32234
32356
  var DEFAULT_TIMEOUT_MS = 12e4;
@@ -35848,7 +35970,7 @@ async function runGuidedStart(apiUrlOverride, executeCommand) {
35848
35970
  }
35849
35971
  if (!state.orgId) {
35850
35972
  process.stdout.write("Step 2/3: Set default org\n");
35851
- const orgs = await fetchOrgMemberships(apiUrlOverride);
35973
+ const orgs = await fetchOrgMemberships2(apiUrlOverride);
35852
35974
  if (orgs.length === 1) {
35853
35975
  const code = await executeCommand(["org", "use", "--org", orgs[0].org_id], apiUrlOverride);
35854
35976
  if (code !== 0) process.stdout.write(`org use exited with code ${code}.
@@ -35874,7 +35996,7 @@ async function runGuidedStart(apiUrlOverride, executeCommand) {
35874
35996
  process.stdout.write(`org create exited with code ${createCode}.
35875
35997
  `);
35876
35998
  } else {
35877
- const refreshedOrgs = await fetchOrgMemberships(apiUrlOverride);
35999
+ const refreshedOrgs = await fetchOrgMemberships2(apiUrlOverride);
35878
36000
  if (refreshedOrgs.length === 1) {
35879
36001
  const useCode = await executeCommand(["org", "use", "--org", refreshedOrgs[0].org_id], apiUrlOverride);
35880
36002
  if (useCode !== 0) process.stdout.write(`org use exited with code ${useCode}.
@@ -35904,7 +36026,7 @@ async function maybeRunTenantStatus(apiUrlOverride, executeCommand) {
35904
36026
  `);
35905
36027
  }
35906
36028
  }
35907
- async function fetchOrgMemberships(apiUrlOverride) {
36029
+ async function fetchOrgMemberships2(apiUrlOverride) {
35908
36030
  try {
35909
36031
  const creds = loadCredentials(apiUrlOverride);
35910
36032
  const res = await fetch(`${creds.apiUrl}/v1/console/auth/my-orgs`, {
package/package.json CHANGED
@@ -1,39 +1,39 @@
1
- {
2
- "name": "@f-o-h/cli",
3
- "version": "0.1.2",
4
- "description": "FOH CLI - AI-operator provisioning tool for Front Of House",
5
- "license": "UNLICENSED",
6
- "bin": {
7
- "foh": "dist/foh.js"
8
- },
9
- "main": "dist/foh.js",
10
- "files": [
11
- "dist/",
12
- "README.md",
13
- "package.json"
14
- ],
15
- "publishConfig": {
16
- "access": "public"
17
- },
18
- "engines": {
19
- "node": ">=18"
20
- },
21
- "scripts": {
22
- "build": "node build.mjs",
23
- "test": "vitest run",
24
- "typecheck": "tsc --noEmit"
25
- },
26
- "dependencies": {
27
- "@modelcontextprotocol/sdk": "^1.29.0",
28
- "commander": "^12.1.0",
29
- "js-yaml": "^4.1.1",
30
- "picocolors": "^1.1.1",
31
- "zod": "^4.3.6"
32
- },
33
- "devDependencies": {
34
- "@types/js-yaml": "^4.0.9",
35
- "@types/node": "^22.0.0",
36
- "esbuild": "^0.24.0",
37
- "vitest": "^2.0.0"
38
- }
39
- }
1
+ {
2
+ "name": "@f-o-h/cli",
3
+ "version": "0.1.3",
4
+ "description": "FOH CLI - AI-operator provisioning tool for Front Of House",
5
+ "license": "UNLICENSED",
6
+ "bin": {
7
+ "foh": "dist/foh.js"
8
+ },
9
+ "main": "dist/foh.js",
10
+ "files": [
11
+ "dist/",
12
+ "README.md",
13
+ "package.json"
14
+ ],
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "engines": {
19
+ "node": ">=18"
20
+ },
21
+ "scripts": {
22
+ "build": "node build.mjs",
23
+ "test": "vitest run",
24
+ "typecheck": "tsc --noEmit"
25
+ },
26
+ "dependencies": {
27
+ "@modelcontextprotocol/sdk": "^1.29.0",
28
+ "commander": "^12.1.0",
29
+ "js-yaml": "^4.1.1",
30
+ "picocolors": "^1.1.1",
31
+ "zod": "^4.3.6"
32
+ },
33
+ "devDependencies": {
34
+ "@types/js-yaml": "^4.0.9",
35
+ "@types/node": "^22.0.0",
36
+ "esbuild": "^0.24.0",
37
+ "vitest": "^2.0.0"
38
+ }
39
+ }