@tablation/crew 0.67.2 → 0.68.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 (2) hide show
  1. package/dist/cli.js +82 -1
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2662,6 +2662,14 @@ function resolveApiKey(r) {
2662
2662
  `route "${r.route}": no API key (looked for, in order: apiKey; ${r.apiKeyVar ?? "VAR"} in ${r.apiKeyFile ?? "<unset>"}; CREW_API_KEY in the environment)`
2663
2663
  );
2664
2664
  }
2665
+ function credentialSource(r) {
2666
+ if (r.apiKey) return KEYCHAIN_ELIGIBLE_ROUTES.has(r) ? "keychain" : "inline";
2667
+ if (r.apiKeyFile && r.apiKeyVar && existsSync7(r.apiKeyFile)) {
2668
+ const re = new RegExp(`^${r.apiKeyVar}=`);
2669
+ if (readFileSync6(r.apiKeyFile, "utf8").split("\n").some((l) => re.test(l))) return "apiKeyFile";
2670
+ }
2671
+ return process.env.CREW_API_KEY?.trim() ? "CREW_API_KEY" : "none";
2672
+ }
2665
2673
  var KEYCHAIN_ELIGIBLE_ROUTES = /* @__PURE__ */ new WeakSet();
2666
2674
  async function hydrateApiKeys(routes, getStore = getSessionStore) {
2667
2675
  let store;
@@ -11674,6 +11682,61 @@ function formatProjectsReport(report) {
11674
11682
  `;
11675
11683
  }
11676
11684
 
11685
+ // src/whoami.ts
11686
+ async function buildWhoami(routes, deps = {}) {
11687
+ const doFetch = deps.fetch ?? fetch;
11688
+ return Promise.all(routes.map(async (route) => {
11689
+ const credential = credentialSource(route);
11690
+ try {
11691
+ const key = resolveApiKey(route);
11692
+ const q = route.resolved ? `?workspaceId=${encodeURIComponent(route.resolved.workspaceId)}` : "";
11693
+ const res = await doFetch(`${route.baseUrl.replace(/\/+$/, "")}/api/auth/me${q}`, {
11694
+ headers: { Authorization: `Bearer ${key}`, "User-Agent": deps.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
11695
+ });
11696
+ if (res.status === 401 || res.status === 403) {
11697
+ return { route: route.route, ok: false, credential, error: `key rejected by ${route.baseUrl} (HTTP ${res.status}): expired or revoked, re-run crew connect` };
11698
+ }
11699
+ if (!res.ok) return { route: route.route, ok: false, credential, error: `/auth/me answered HTTP ${res.status}` };
11700
+ const me = await res.json();
11701
+ const operatorId = route.resolved?.operator ?? null;
11702
+ let operatorName = null;
11703
+ if (operatorId && deps.operatorName) {
11704
+ try {
11705
+ operatorName = await deps.operatorName(route, operatorId) ?? null;
11706
+ } catch {
11707
+ }
11708
+ }
11709
+ return {
11710
+ route: route.route,
11711
+ ok: true,
11712
+ credential,
11713
+ identity: { id: me.id ?? null, email: me.email ?? null, name: me.name ?? null },
11714
+ role: typeof me.role === "string" ? me.role : null,
11715
+ operator: { id: operatorId, name: operatorName }
11716
+ };
11717
+ } catch (e) {
11718
+ return { route: route.route, ok: false, credential, error: e.message };
11719
+ }
11720
+ }));
11721
+ }
11722
+ function formatWhoami(report) {
11723
+ const lines = [];
11724
+ for (const r of report) {
11725
+ lines.push(`route ${r.route}`);
11726
+ if (!r.ok) {
11727
+ lines.push(` error: ${r.error}`, ` credential: ${r.credential}`);
11728
+ continue;
11729
+ }
11730
+ const who2 = [r.identity.name, r.identity.email ? `<${r.identity.email}>` : null].filter(Boolean).join(" ");
11731
+ lines.push(` signed in as: ${who2 || r.identity.id || "(unknown)"}${r.identity.id ? ` (${r.identity.id})` : ""}`);
11732
+ lines.push(` role: ${r.role ?? "(unknown)"}`);
11733
+ lines.push(` operator: ${r.operator.id ? `${r.operator.name ?? "(unnamed)"} (${r.operator.id})` : "(not linked)"}`);
11734
+ lines.push(` credential: ${r.credential}`);
11735
+ }
11736
+ return `${lines.join("\n")}
11737
+ `;
11738
+ }
11739
+
11677
11740
  // src/cli.ts
11678
11741
  import { StaleWriteError as StaleWriteError9 } from "@tablation/client";
11679
11742
 
@@ -15477,6 +15540,7 @@ function usage() {
15477
15540
  crew watch [route] live view of what the crew is doing
15478
15541
  crew status [route] [--json] paused/running state; --json for a machine reader
15479
15542
  crew projects [--route R] [--json] the Projects rows each route can read (id, name, prefix, work mode)
15543
+ crew whoami [route] [--json] the signed-in identity, key role, operator Crew row and credential source per route
15480
15544
  crew reload make the running daemon re-read crew.yaml (--now: restart it immediately)
15481
15545
  crew doctor [route] [--fix] [--offline] [--json] preflight; --fix enables clean routes and offers crew install;
15482
15546
  --offline skips push probes; --json prints push-auth and tool checks only
@@ -15698,7 +15762,7 @@ if (command === "passenger-sync-daemon") {
15698
15762
  const DAEMON_SUBCOMMANDS = /* @__PURE__ */ new Set(["start", "stop", "status", "restart"]);
15699
15763
  const worktreeArgs = command === "worktree" ? splitWorktreeArgs(positional, cfg.routes.map((r) => r.route)) : void 0;
15700
15764
  const named = worktreeArgs ? worktreeArgs.route : command === "daemon" && DAEMON_SUBCOMMANDS.has(positional[1] ?? "") ? positional[2] : positional[1];
15701
- const fleetWide = command === "inbox" || command === "connect" || command === "disconnect" || command === "agents" || command === "skills" || command === "logbook" || command === "repos" || command === "projects" || releaseFleetWide || command === "pause" || command === "resume" || command === "reload" || FLEET_CAPABLE.has(command) && !named && cfg.routes.length > 1 || worktreeArgs !== void 0 && !named && cfg.routes.length > 1;
15765
+ const fleetWide = command === "inbox" || command === "connect" || command === "disconnect" || command === "agents" || command === "skills" || command === "logbook" || command === "repos" || command === "projects" || command === "whoami" || releaseFleetWide || command === "pause" || command === "resume" || command === "reload" || FLEET_CAPABLE.has(command) && !named && cfg.routes.length > 1 || worktreeArgs !== void 0 && !named && cfg.routes.length > 1;
15702
15766
  let route;
15703
15767
  try {
15704
15768
  route = fleetWide ? cfg.routes[0] : findRoute(cfg, named);
@@ -18902,6 +18966,23 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18902
18966
  if (results.some((r) => !r.ok)) process.exitCode = 1;
18903
18967
  break;
18904
18968
  }
18969
+ case "whoami": {
18970
+ const only = value("route") ?? positional[1];
18971
+ const wanted = only ? cfg.routes.filter((r) => r.route === only) : cfg.routes;
18972
+ if (only && wanted.length === 0) {
18973
+ process.stderr.write(`crew whoami: no route "${only}" (have: ${cfg.routes.map((r) => r.route).join(", ")})
18974
+ `);
18975
+ process.exit(2);
18976
+ }
18977
+ const report = await buildWhoami(wanted, {
18978
+ userAgent: cfg.ship.userAgent,
18979
+ operatorName: async (r, id) => (await new Tracker(r, cfg.ship).crewRows()).find((c) => c.id === id)?.name
18980
+ });
18981
+ process.stdout.write(flag("json") ? `${JSON.stringify(report.length === 1 && only ? report[0] : report, null, 2)}
18982
+ ` : formatWhoami(report));
18983
+ if (report.some((r) => !r.ok)) process.exitCode = 1;
18984
+ break;
18985
+ }
18905
18986
  case "projects": {
18906
18987
  const only = value("route") ?? positional[1];
18907
18988
  const wanted = only ? cfg.routes.filter((r) => r.route === only) : cfg.routes;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.67.2",
3
+ "version": "0.68.0",
4
4
  "description": "A standing team of headless agents that picks work off a Tablation board, does it on a machine you control, and reports back.",
5
5
  "type": "module",
6
6
  "license": "MIT",