@lambdacurry/arbor 0.21.41 → 0.21.43

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 +33 -0
  2. package/dist/arbor.js +410 -111
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -30,6 +30,39 @@ arbor auth <token> --url https://your-arbor.example
30
30
  arbor whoami
31
31
  ```
32
32
 
33
+ ## Named profiles
34
+
35
+ Keep human/owner and agent credentials in separate owner-only files instead of copying tokens or switching one shared config. Named profile credentials live under `~/.arbor/profiles/`; the token-free local registry stores only the configured default profile. Arbor still resolves the actual identity from the selected credential server-side.
36
+
37
+ ```bash
38
+ # Save credentials into separate profile files.
39
+ arbor auth <agent-token> --profile agent
40
+ arbor auth <owner-token> --profile owner
41
+
42
+ # Ordinary commands now use the least-privileged agent profile.
43
+ arbor profile set-default agent
44
+ arbor whoami
45
+
46
+ # Explicitly use the owner for one command only; the next command returns to the default.
47
+ arbor whoami --profile owner
48
+ arbor whoami
49
+
50
+ # Automation can select the same one-command profile without changing the default.
51
+ ARBOR_PROFILE=owner arbor whoami
52
+ ```
53
+
54
+ Without `ARBOR_CONFIG`, selection precedence is `--profile` → `ARBOR_PROFILE` → configured default → legacy `~/.arbor/config.json`. When `ARBOR_CONFIG` is set by itself it is decisive and bypasses the configured default; combining it with `--profile` or `ARBOR_PROFILE` fails as ambiguous instead of guessing. `ARBOR_TOKEN` and `ARBOR_API_URL` remain headless overrides after the credential target is selected.
55
+
56
+ Profile management never prints credential material:
57
+
58
+ ```bash
59
+ arbor profile list
60
+ arbor profile show agent
61
+ arbor profile set-default agent
62
+ arbor profile clear-default
63
+ arbor profile delete owner
64
+ ```
65
+
33
66
  ## Agent self-registration (AD-109)
34
67
 
35
68
  An agent can register _itself_ with a device-code-style handshake. A human mints a one-time **pairing key** in the Arbor web app (Settings → "Have an agent register itself") and hands it to the agent. The agent then:
package/dist/arbor.js CHANGED
@@ -16,7 +16,7 @@ var __export = (target, all) => {
16
16
  // package.json
17
17
  var package_default = {
18
18
  name: "@lambdacurry/arbor",
19
- version: "0.21.41",
19
+ version: "0.21.43",
20
20
  description: "The Arbor CLI — a shared workspace for people and agents. The human + headless-agent write path over Arbor's guarded operation surface.",
21
21
  keywords: [
22
22
  "agents",
@@ -18715,6 +18715,15 @@ function selectedArray(value, fields) {
18715
18715
  return;
18716
18716
  return value.map((entry) => selected(entry, fields) ?? {});
18717
18717
  }
18718
+ function omitNull(value) {
18719
+ return value === null ? undefined : value;
18720
+ }
18721
+ var PROVIDER_NATIVE_EXECUTION = new Set(["foreground", "background"]);
18722
+ function omitProviderNativeExecution(value) {
18723
+ if (typeof value === "string" && PROVIDER_NATIVE_EXECUTION.has(value))
18724
+ return;
18725
+ return omitNull(value);
18726
+ }
18718
18727
  var OUTPUT_SHAPERS = {
18719
18728
  contribute: (result) => defined([
18720
18729
  ["contributionId", result.contributionId],
@@ -18884,14 +18893,14 @@ var OUTPUT_SHAPERS = {
18884
18893
  ]);
18885
18894
  },
18886
18895
  computer_exec: (result, input) => defined([
18887
- ["stdout", result.stdout],
18888
- ["stderr", result.stderr],
18889
- ["exitCode", result.exitCode],
18890
- ["processId", result.processId],
18891
- ["status", result.status],
18892
- ["execution", result.execution],
18893
- ["nextAction", result.nextAction],
18894
- ["cwd", input.cwd === undefined ? result.cwd : undefined]
18896
+ ["stdout", omitNull(result.stdout)],
18897
+ ["stderr", omitNull(result.stderr)],
18898
+ ["exitCode", omitNull(result.exitCode)],
18899
+ ["processId", omitNull(result.processId)],
18900
+ ["status", omitNull(result.status)],
18901
+ ["execution", omitProviderNativeExecution(result.execution)],
18902
+ ["nextAction", omitNull(result.nextAction)],
18903
+ ["cwd", input.cwd === undefined ? omitNull(result.cwd) : undefined]
18895
18904
  ]),
18896
18905
  computer_process_read: (result) => defined([
18897
18906
  ["processId", result.processId],
@@ -18924,8 +18933,10 @@ function shapeActionOutput(actionName, value, input = {}) {
18924
18933
  if (!schema)
18925
18934
  throw new Error(`${actionName}: compact action is missing an output schema`);
18926
18935
  const parsed = schema.safeParse(publicValue);
18927
- if (!parsed.success)
18928
- throw new Error(`${actionName}: action returned an invalid public result`);
18936
+ if (!parsed.success) {
18937
+ const paths = parsed.error.issues.map((issue2) => issue2.path.map(String).join(".") || "root");
18938
+ throw new Error(`${actionName}: action returned an invalid public result (${paths.join(", ")})`);
18939
+ }
18929
18940
  return parsed.data;
18930
18941
  }
18931
18942
  // ../actions/src/tool-annotations.ts
@@ -21831,105 +21842,16 @@ var ACTIONS3 = [...ACTIONS2, ...RECOVERY_ACTIONS];
21831
21842
  import {
21832
21843
  chmodSync,
21833
21844
  existsSync,
21845
+ lstatSync,
21834
21846
  mkdirSync,
21835
21847
  readFileSync,
21848
+ readdirSync,
21836
21849
  rmSync,
21837
21850
  statSync,
21838
21851
  writeFileSync
21839
21852
  } from "node:fs";
21840
21853
  import { homedir } from "node:os";
21841
21854
  import { dirname, join } from "node:path";
21842
- var CONFIG_PATH = process.env.ARBOR_CONFIG ?? join(homedir(), ".arbor", "config.json");
21843
- var DEFAULT_API_URL = process.env.ARBOR_API_URL ?? "https://arborthreads.com";
21844
- var DEFAULT_CONFIG_PATH = join(homedir(), ".arbor", "config.json");
21845
- var DEFAULT_TOKEN_OVERWRITE_REFUSAL = "refusing to overwrite ~/.arbor/config.json — a token already exists. Set ARBOR_CONFIG to an isolated path (e.g. ~/.arbor/<agent>.config.json) and retry.";
21846
- var LOCK_TIMEOUT_MS = 5000;
21847
- var LOCK_STALE_MS = 30000;
21848
- var LOCK_POLL_MS = 20;
21849
- function sleepSync(ms) {
21850
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
21851
- }
21852
- function acquireConfigLock(configPath) {
21853
- const lockPath = `${configPath}.lock`;
21854
- const deadline = Date.now() + LOCK_TIMEOUT_MS;
21855
- for (;; ) {
21856
- try {
21857
- mkdirSync(lockPath);
21858
- return () => {
21859
- try {
21860
- rmSync(lockPath, { recursive: true, force: true });
21861
- } catch {}
21862
- };
21863
- } catch (err) {
21864
- if (err?.code !== "EEXIST")
21865
- throw err;
21866
- try {
21867
- if (Date.now() - statSync(lockPath).mtimeMs > LOCK_STALE_MS) {
21868
- rmSync(lockPath, { recursive: true, force: true });
21869
- continue;
21870
- }
21871
- } catch {
21872
- continue;
21873
- }
21874
- if (Date.now() >= deadline) {
21875
- throw new Error(`timed out waiting for another arbor process to finish writing ${configPath}. If none is running, remove ${lockPath}.`);
21876
- }
21877
- sleepSync(LOCK_POLL_MS);
21878
- }
21879
- }
21880
- }
21881
- function defaultConfigOverwriteBlocked(opts) {
21882
- const isolated = opts?.isolated ?? Boolean(process.env.ARBOR_CONFIG);
21883
- const path = opts?.path ?? DEFAULT_CONFIG_PATH;
21884
- if (isolated)
21885
- return false;
21886
- if (!existsSync(path))
21887
- return false;
21888
- try {
21889
- const cfg = JSON.parse(readFileSync(path, "utf8"));
21890
- return Boolean(cfg.token);
21891
- } catch {
21892
- return false;
21893
- }
21894
- }
21895
- function loadConfig() {
21896
- const envToken = process.env.ARBOR_TOKEN || undefined;
21897
- const envUrl = process.env.ARBOR_API_URL || undefined;
21898
- if (existsSync(CONFIG_PATH)) {
21899
- try {
21900
- const cfg = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
21901
- return { apiUrl: envUrl ?? (cfg.apiUrl || DEFAULT_API_URL), token: envToken ?? cfg.token };
21902
- } catch {}
21903
- }
21904
- return { apiUrl: envUrl ?? DEFAULT_API_URL, token: envToken };
21905
- }
21906
- function writeConfigGuarded(opts) {
21907
- mkdirSync(dirname(opts.configPath), { recursive: true });
21908
- const release = acquireConfigLock(opts.configPath);
21909
- try {
21910
- if (!opts.allowDefaultTokenOverwrite && defaultConfigOverwriteBlocked({ isolated: opts.isolated, path: opts.defaultPath })) {
21911
- throw new Error(DEFAULT_TOKEN_OVERWRITE_REFUSAL);
21912
- }
21913
- writeFileSync(opts.configPath, `${JSON.stringify(opts.cfg, null, 2)}
21914
- `, { mode: 384 });
21915
- chmodSync(opts.configPath, 384);
21916
- } finally {
21917
- release();
21918
- }
21919
- }
21920
- function saveConfig(cfg, opts) {
21921
- writeConfigGuarded({
21922
- cfg,
21923
- configPath: CONFIG_PATH,
21924
- defaultPath: DEFAULT_CONFIG_PATH,
21925
- isolated: Boolean(process.env.ARBOR_CONFIG),
21926
- allowDefaultTokenOverwrite: opts?.allowDefaultTokenOverwrite
21927
- });
21928
- }
21929
- function clearToken() {
21930
- const cfg = loadConfig();
21931
- saveConfig({ apiUrl: cfg.apiUrl }, { allowDefaultTokenOverwrite: true });
21932
- }
21933
21855
 
21934
21856
  // src/errors.ts
21935
21857
  var ERROR_CODES = new Set([
@@ -22069,6 +21991,310 @@ class UsageError extends Error {
22069
21991
  }
22070
21992
  }
22071
21993
 
21994
+ // src/config.ts
21995
+ var PROD_API_URL = "https://arborthreads.com";
21996
+ var PROFILE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
21997
+ function arborDir() {
21998
+ return join(homedir(), ".arbor");
21999
+ }
22000
+ function defaultConfigPath() {
22001
+ return join(arborDir(), "config.json");
22002
+ }
22003
+ function profilesDir() {
22004
+ return join(arborDir(), "profiles");
22005
+ }
22006
+ function registryPath() {
22007
+ return join(arborDir(), "profiles.json");
22008
+ }
22009
+ function namedConfigPath(name) {
22010
+ return join(profilesDir(), `${name}.json`);
22011
+ }
22012
+ var CONFIG_PATH = process.env.ARBOR_CONFIG ?? join(homedir(), ".arbor", "config.json");
22013
+ var DEFAULT_TOKEN_OVERWRITE_REFUSAL = "refusing to overwrite ~/.arbor/config.json — a token already exists. Set ARBOR_CONFIG to an isolated path (e.g. ~/.arbor/<agent>.config.json) and retry.";
22014
+ var LOCK_TIMEOUT_MS = 5000;
22015
+ var LOCK_STALE_MS = 30000;
22016
+ var LOCK_POLL_MS = 20;
22017
+ var activeTarget;
22018
+ function sleepSync(ms) {
22019
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
22020
+ }
22021
+ function acquireConfigLock(configPath) {
22022
+ const lockPath = `${configPath}.lock`;
22023
+ const deadline = Date.now() + LOCK_TIMEOUT_MS;
22024
+ for (;; ) {
22025
+ try {
22026
+ mkdirSync(lockPath);
22027
+ return () => {
22028
+ try {
22029
+ rmSync(lockPath, { recursive: true, force: true });
22030
+ } catch {}
22031
+ };
22032
+ } catch (err) {
22033
+ if (err?.code !== "EEXIST")
22034
+ throw err;
22035
+ try {
22036
+ if (Date.now() - statSync(lockPath).mtimeMs > LOCK_STALE_MS) {
22037
+ rmSync(lockPath, { recursive: true, force: true });
22038
+ continue;
22039
+ }
22040
+ } catch {
22041
+ continue;
22042
+ }
22043
+ if (Date.now() >= deadline) {
22044
+ throw new Error(`timed out waiting for another arbor process to finish writing ${configPath}. If none is running, remove ${lockPath}.`);
22045
+ }
22046
+ sleepSync(LOCK_POLL_MS);
22047
+ }
22048
+ }
22049
+ }
22050
+ function validateProfileName(name, field = "profile") {
22051
+ if (!PROFILE_NAME.test(name)) {
22052
+ throw new UsageError(`invalid profile name: ${JSON.stringify(name)}`, {
22053
+ reason: "validation.invalid_value",
22054
+ field
22055
+ });
22056
+ }
22057
+ }
22058
+ function rejectSymlink(path, label) {
22059
+ if (!existsSync(path))
22060
+ return;
22061
+ if (lstatSync(path).isSymbolicLink())
22062
+ throw new Error(`${label} must not be a symbolic link: ${path}`);
22063
+ }
22064
+ function ensureArborDir() {
22065
+ const dir = arborDir();
22066
+ rejectSymlink(dir, "Arbor config directory");
22067
+ mkdirSync(dir, { recursive: true, mode: 448 });
22068
+ chmodSync(dir, 448);
22069
+ }
22070
+ function ensureProfilesDir() {
22071
+ ensureArborDir();
22072
+ const dir = profilesDir();
22073
+ rejectSymlink(dir, "Arbor profiles directory");
22074
+ mkdirSync(dir, { recursive: true, mode: 448 });
22075
+ chmodSync(dir, 448);
22076
+ }
22077
+ function readRegistry() {
22078
+ const path = registryPath();
22079
+ if (!existsSync(path))
22080
+ return {};
22081
+ rejectSymlink(path, "Arbor profile registry");
22082
+ try {
22083
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
22084
+ if (parsed.defaultProfile !== undefined)
22085
+ validateProfileName(parsed.defaultProfile);
22086
+ return { defaultProfile: parsed.defaultProfile };
22087
+ } catch (err) {
22088
+ if (err instanceof UsageError)
22089
+ throw err;
22090
+ throw new Error(`could not read Arbor profile registry ${path}`);
22091
+ }
22092
+ }
22093
+ function writeRegistry(registry2) {
22094
+ ensureArborDir();
22095
+ const path = registryPath();
22096
+ rejectSymlink(path, "Arbor profile registry");
22097
+ const release = acquireConfigLock(path);
22098
+ try {
22099
+ const safe = registry2.defaultProfile ? { defaultProfile: registry2.defaultProfile } : {};
22100
+ writeFileSync(path, `${JSON.stringify(safe, null, 2)}
22101
+ `, { mode: 384 });
22102
+ chmodSync(path, 384);
22103
+ } finally {
22104
+ release();
22105
+ }
22106
+ }
22107
+ function profileExists(name, field = "profile") {
22108
+ validateProfileName(name, field);
22109
+ const path = namedConfigPath(name);
22110
+ if (!existsSync(path))
22111
+ return false;
22112
+ rejectSymlink(path, `Arbor profile ${name}`);
22113
+ return lstatSync(path).isFile();
22114
+ }
22115
+ function legacyTarget() {
22116
+ const defaultPath = defaultConfigPath();
22117
+ const envPath = process.env.ARBOR_CONFIG;
22118
+ return {
22119
+ path: envPath ?? defaultPath,
22120
+ defaultPath,
22121
+ isolated: Boolean(envPath),
22122
+ named: false
22123
+ };
22124
+ }
22125
+ function resolveTarget(opts) {
22126
+ const profileFlag = opts?.profileFlag;
22127
+ const envProfile = process.env.ARBOR_PROFILE;
22128
+ if (process.env.ARBOR_CONFIG && profileFlag !== undefined) {
22129
+ throw new UsageError("ARBOR_CONFIG and --profile are ambiguous; choose one credential selector", {
22130
+ reason: "validation.invalid_value",
22131
+ field: "profile"
22132
+ });
22133
+ }
22134
+ if (process.env.ARBOR_CONFIG && envProfile !== undefined) {
22135
+ throw new UsageError("ARBOR_CONFIG and ARBOR_PROFILE are ambiguous; choose one credential selector", { reason: "validation.invalid_value", field: "ARBOR_PROFILE" });
22136
+ }
22137
+ if (process.env.ARBOR_CONFIG)
22138
+ return legacyTarget();
22139
+ const registryDefault = readRegistry().defaultProfile;
22140
+ const selected2 = profileFlag ?? envProfile ?? registryDefault;
22141
+ if (selected2 === undefined)
22142
+ return legacyTarget();
22143
+ const field = profileFlag !== undefined ? "profile" : envProfile !== undefined ? "ARBOR_PROFILE" : "profile";
22144
+ validateProfileName(selected2, field);
22145
+ if (!opts?.allowMissingProfile && !profileExists(selected2, field)) {
22146
+ throw new UsageError(`unknown profile: ${selected2}`, {
22147
+ reason: "validation.invalid_value",
22148
+ field
22149
+ });
22150
+ }
22151
+ return {
22152
+ path: namedConfigPath(selected2),
22153
+ defaultPath: defaultConfigPath(),
22154
+ isolated: true,
22155
+ profile: selected2,
22156
+ named: true
22157
+ };
22158
+ }
22159
+ function configureConfigTarget(opts) {
22160
+ activeTarget = resolveTarget(opts);
22161
+ }
22162
+ function currentTarget() {
22163
+ return activeTarget ?? resolveTarget();
22164
+ }
22165
+ function getConfigPath() {
22166
+ return currentTarget().path;
22167
+ }
22168
+ function listProfiles() {
22169
+ const registry2 = readRegistry();
22170
+ const dir = profilesDir();
22171
+ if (!existsSync(dir))
22172
+ return { defaultProfile: registry2.defaultProfile, profiles: [] };
22173
+ rejectSymlink(dir, "Arbor profiles directory");
22174
+ const profiles2 = readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.name.endsWith(".json")).map((entry) => {
22175
+ const name = entry.name.slice(0, -5);
22176
+ validateProfileName(name);
22177
+ const path = join(dir, entry.name);
22178
+ if (entry.isSymbolicLink())
22179
+ throw new Error(`Arbor profile ${name} must not be a symbolic link: ${path}`);
22180
+ if (!entry.isFile())
22181
+ throw new Error(`Arbor profile ${name} is not a regular file: ${path}`);
22182
+ return { name, default: registry2.defaultProfile === name };
22183
+ }).sort((a, b) => a.name.localeCompare(b.name));
22184
+ return { defaultProfile: registry2.defaultProfile, profiles: profiles2 };
22185
+ }
22186
+ function profileShow(name) {
22187
+ validateProfileName(name);
22188
+ if (!profileExists(name)) {
22189
+ throw new UsageError(`unknown profile: ${name}`, {
22190
+ reason: "validation.invalid_value",
22191
+ field: "profile"
22192
+ });
22193
+ }
22194
+ const registry2 = readRegistry();
22195
+ const cfg = JSON.parse(readFileSync(namedConfigPath(name), "utf8"));
22196
+ return {
22197
+ name,
22198
+ default: registry2.defaultProfile === name,
22199
+ apiUrl: cfg.apiUrl || PROD_API_URL
22200
+ };
22201
+ }
22202
+ function setDefaultProfile(name) {
22203
+ if (name !== null) {
22204
+ validateProfileName(name);
22205
+ if (!profileExists(name)) {
22206
+ throw new UsageError(`unknown profile: ${name}`, {
22207
+ reason: "validation.invalid_value",
22208
+ field: "profile"
22209
+ });
22210
+ }
22211
+ }
22212
+ writeRegistry(name ? { defaultProfile: name } : {});
22213
+ }
22214
+ function clearDefaultProfile() {
22215
+ setDefaultProfile(null);
22216
+ }
22217
+ function deleteProfile(name) {
22218
+ validateProfileName(name);
22219
+ const registry2 = readRegistry();
22220
+ if (registry2.defaultProfile === name) {
22221
+ throw new UsageError(`cannot delete default profile ${name}; select another default or run profile clear-default first`, { reason: "validation.invalid_value", field: "profile" });
22222
+ }
22223
+ if (!profileExists(name)) {
22224
+ throw new UsageError(`unknown profile: ${name}`, {
22225
+ reason: "validation.invalid_value",
22226
+ field: "profile"
22227
+ });
22228
+ }
22229
+ const path = namedConfigPath(name);
22230
+ rejectSymlink(path, `Arbor profile ${name}`);
22231
+ rmSync(path);
22232
+ }
22233
+ function defaultConfigOverwriteBlocked(opts) {
22234
+ const target = currentTarget();
22235
+ const isolated = opts?.isolated ?? target.isolated;
22236
+ const path = opts?.path ?? target.defaultPath;
22237
+ if (isolated)
22238
+ return false;
22239
+ if (!existsSync(path))
22240
+ return false;
22241
+ try {
22242
+ const cfg = JSON.parse(readFileSync(path, "utf8"));
22243
+ return Boolean(cfg.token);
22244
+ } catch {
22245
+ return false;
22246
+ }
22247
+ }
22248
+ function loadConfig() {
22249
+ const target = currentTarget();
22250
+ const envToken = process.env.ARBOR_TOKEN || undefined;
22251
+ const envUrl = process.env.ARBOR_API_URL || undefined;
22252
+ if (target.named)
22253
+ rejectSymlink(target.path, `Arbor profile ${target.profile}`);
22254
+ if (existsSync(target.path)) {
22255
+ try {
22256
+ const cfg = JSON.parse(readFileSync(target.path, "utf8"));
22257
+ return { apiUrl: envUrl ?? (cfg.apiUrl || PROD_API_URL), token: envToken ?? cfg.token };
22258
+ } catch (err) {
22259
+ if (err instanceof Error && /symbolic link/i.test(err.message))
22260
+ throw err;
22261
+ }
22262
+ }
22263
+ return { apiUrl: envUrl ?? PROD_API_URL, token: envToken };
22264
+ }
22265
+ function writeConfigGuarded(opts) {
22266
+ mkdirSync(dirname(opts.configPath), { recursive: true });
22267
+ const release = acquireConfigLock(opts.configPath);
22268
+ try {
22269
+ if (!opts.allowDefaultTokenOverwrite && defaultConfigOverwriteBlocked({ isolated: opts.isolated, path: opts.defaultPath })) {
22270
+ throw new Error(DEFAULT_TOKEN_OVERWRITE_REFUSAL);
22271
+ }
22272
+ writeFileSync(opts.configPath, `${JSON.stringify(opts.cfg, null, 2)}
22273
+ `, { mode: 384 });
22274
+ chmodSync(opts.configPath, 384);
22275
+ } finally {
22276
+ release();
22277
+ }
22278
+ }
22279
+ function saveConfig(cfg, opts) {
22280
+ const target = currentTarget();
22281
+ if (target.named) {
22282
+ ensureProfilesDir();
22283
+ rejectSymlink(target.path, `Arbor profile ${target.profile}`);
22284
+ }
22285
+ writeConfigGuarded({
22286
+ cfg,
22287
+ configPath: target.path,
22288
+ defaultPath: target.defaultPath,
22289
+ isolated: target.isolated,
22290
+ allowDefaultTokenOverwrite: opts?.allowDefaultTokenOverwrite
22291
+ });
22292
+ }
22293
+ function clearToken() {
22294
+ const cfg = loadConfig();
22295
+ saveConfig({ apiUrl: cfg.apiUrl }, { allowDefaultTokenOverwrite: true });
22296
+ }
22297
+
22072
22298
  // src/source.ts
22073
22299
  var SNIFF = [
22074
22300
  ["CLAUDECODE", "Claude Code"],
@@ -22792,7 +23018,7 @@ async function runObjectVerb(positionals, flags, ctx) {
22792
23018
 
22793
23019
  // src/connect.ts
22794
23020
  var DEFAULT_POLL_INTERVAL_MS = 2000;
22795
- var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
23021
+ var sleep2 = (ms) => new Promise((r) => globalThis.setTimeout(r, ms));
22796
23022
  function bareConnectUrl(apiUrl, returnedUrl) {
22797
23023
  const fallback = `${apiUrl.replace(/\/$/, "")}/connect`;
22798
23024
  if (!returnedUrl)
@@ -22836,7 +23062,6 @@ async function connect(opts) {
22836
23062
  if (Date.now() > deadline) {
22837
23063
  throw new Error("pairing timed out — ask for a new pairing key and run `arbor connect` again");
22838
23064
  }
22839
- await sleep2(pollIntervalMs);
22840
23065
  const tokRes = await fetch(`${apiUrl}/api/agent/connect/token`, {
22841
23066
  method: "POST",
22842
23067
  headers,
@@ -22847,8 +23072,13 @@ async function connect(opts) {
22847
23072
  saveConfig({ apiUrl, token: tok.apiKey });
22848
23073
  return;
22849
23074
  }
22850
- if (tok.status === "pending")
23075
+ if (tok.status === "pending") {
23076
+ if (typeof globalThis.setTimeout !== "function") {
23077
+ throw new Error("approval pending — approve it, then rerun the same `arbor connect` command to finish saving this agent's credential");
23078
+ }
23079
+ await sleep2(pollIntervalMs);
22851
23080
  continue;
23081
+ }
22852
23082
  if (tok.status === "expired")
22853
23083
  throw new Error("pairing expired — ask for a new pairing key");
22854
23084
  if (tok.status === "consumed")
@@ -23000,6 +23230,11 @@ Usage: arbor <command> [--flags]
23000
23230
  login [--url <api-url>] authorize in your browser (device flow) → stores a token
23001
23231
  auth <token> [--url …] save a token directly (a PAT or agent key) — headless, no browser
23002
23232
  connect <pairing-key> register THIS agent: relay the URL+code to your human to approve (AD-109)
23233
+ profile list list configured profile names (never credentials)
23234
+ profile show <name> show safe local metadata for one profile
23235
+ profile set-default <name> configure the ordinary-command default profile
23236
+ profile clear-default intentionally return ordinary commands to legacy config
23237
+ profile delete <name> delete a non-default named profile
23003
23238
  orient print how to work well in Arbor (the room etiquette — when + why)
23004
23239
  whoami print who your token resolves to
23005
23240
  logout forget the stored token
@@ -23008,6 +23243,7 @@ Usage: arbor <command> [--flags]
23008
23243
  help show this help
23009
23244
 
23010
23245
  Global flags (any command):
23246
+ --profile <name> use one named credential for this command only
23011
23247
  --json emit a structured { ok, data|error, meta } envelope on stdout (machine mode)
23012
23248
  --quiet / --no-quiet suppress / force advisory text (auto-quiet under --json or a pipe)
23013
23249
  --fields a,b,arr:N keep only these TOP-LEVEL fields; an "arr:N" entry tail-slices an ARRAY field to
@@ -23063,6 +23299,15 @@ async function main() {
23063
23299
  const ctx = resolveCommandOutput(positionals, flags);
23064
23300
  const [cmd] = positionals;
23065
23301
  try {
23302
+ const profileFlag = stringFlag(flags.profile, "profile");
23303
+ if (cmd !== "profile") {
23304
+ configureConfigTarget({
23305
+ profileFlag,
23306
+ allowMissingProfile: cmd === "login" || cmd === "auth" || cmd === "connect"
23307
+ });
23308
+ } else if (profileFlag !== undefined) {
23309
+ throw new UsageError("--profile selects a credential for a command; profile management names its target positionally");
23310
+ }
23066
23311
  if (flags.version !== undefined && positionals.length === 0 || cmd === "version") {
23067
23312
  renderVersion(ctx);
23068
23313
  return;
@@ -23083,7 +23328,7 @@ async function main() {
23083
23328
  case "login": {
23084
23329
  await login({ url: stringFlag(flags.url, "url") });
23085
23330
  advise(`
23086
- ✓ Logged in (${CONFIG_PATH}).
23331
+ ✓ Logged in (${getConfigPath()}).
23087
23332
  `, ctx);
23088
23333
  await renderMe(ctx, "login");
23089
23334
  return;
@@ -23094,7 +23339,7 @@ async function main() {
23094
23339
  throw new UsageError("usage: arbor auth <token> [--url <api-url>]");
23095
23340
  const url2 = stringFlag(flags.url, "url") ?? loadConfig().apiUrl;
23096
23341
  saveConfig({ apiUrl: url2, token }, { allowDefaultTokenOverwrite: true });
23097
- advise(` ✓ Token saved (${CONFIG_PATH}).
23342
+ advise(` ✓ Token saved (${getConfigPath()}).
23098
23343
  `, ctx);
23099
23344
  await renderMe(ctx, "auth");
23100
23345
  return;
@@ -23105,7 +23350,7 @@ async function main() {
23105
23350
  throw new UsageError("usage: arbor connect <pairing-key> [--url <api-url>]");
23106
23351
  await connect({ pairingKey: key, url: stringFlag(flags.url, "url") });
23107
23352
  advise(`
23108
- ✓ Connected (${CONFIG_PATH}).
23353
+ ✓ Connected (${getConfigPath()}).
23109
23354
  `, ctx);
23110
23355
  await renderMe(ctx, "connect");
23111
23356
  advise(`
@@ -23119,9 +23364,59 @@ ${CLI_NOTE}
23119
23364
  }
23120
23365
  case "logout":
23121
23366
  clearToken();
23122
- emitDual({ loggedOut: true }, `Logged out — token cleared from ${CONFIG_PATH}.
23367
+ emitDual({ loggedOut: true }, `Logged out — token cleared from ${getConfigPath()}.
23123
23368
  `, "logout", ctx);
23124
23369
  return;
23370
+ case "profile": {
23371
+ const subcommand = positionals[1];
23372
+ if (subcommand === "list") {
23373
+ if (positionals.length !== 2)
23374
+ throw new UsageError("usage: arbor profile list");
23375
+ const data = listProfiles();
23376
+ const human = data.profiles.length ? `${data.profiles.map((profile3) => `${profile3.default ? "*" : " "} ${profile3.name}`).join(`
23377
+ `)}
23378
+ ` : `No named profiles configured.
23379
+ `;
23380
+ emitDual(data, human, "profile list", ctx);
23381
+ return;
23382
+ }
23383
+ if (subcommand === "show") {
23384
+ const name = positionals[2];
23385
+ if (!name || positionals.length !== 3)
23386
+ throw new UsageError("usage: arbor profile show <name>");
23387
+ const data = profileShow(name);
23388
+ emitDual(data, `${data.name}${data.default ? " (default)" : ""} — ${data.apiUrl}
23389
+ `, "profile show", ctx);
23390
+ return;
23391
+ }
23392
+ if (subcommand === "set-default") {
23393
+ const name = positionals[2];
23394
+ if (!name || positionals.length !== 3)
23395
+ throw new UsageError("usage: arbor profile set-default <name>");
23396
+ setDefaultProfile(name);
23397
+ emitDual({ defaultProfile: name }, `Default profile: ${name}
23398
+ `, "profile set-default", ctx);
23399
+ return;
23400
+ }
23401
+ if (subcommand === "clear-default") {
23402
+ if (positionals.length !== 2)
23403
+ throw new UsageError("usage: arbor profile clear-default");
23404
+ clearDefaultProfile();
23405
+ emitDual({ defaultProfile: null }, `Default profile cleared; ordinary commands use legacy config.
23406
+ `, "profile clear-default", ctx);
23407
+ return;
23408
+ }
23409
+ if (subcommand === "delete") {
23410
+ const name = positionals[2];
23411
+ if (!name || positionals.length !== 3)
23412
+ throw new UsageError("usage: arbor profile delete <name>");
23413
+ deleteProfile(name);
23414
+ emitDual({ deleted: true, profile: name }, `Deleted profile: ${name}
23415
+ `, "profile delete", ctx);
23416
+ return;
23417
+ }
23418
+ throw new UsageError("usage: arbor profile <list|show|set-default|clear-default|delete> [name]");
23419
+ }
23125
23420
  case "whoami":
23126
23421
  await renderMe(ctx, "whoami");
23127
23422
  return;
@@ -23131,8 +23426,12 @@ ${CLI_NOTE}
23131
23426
  case "health":
23132
23427
  await renderHealth(ctx);
23133
23428
  return;
23134
- default:
23135
- await runObjectVerb(positionals, flags, ctx);
23429
+ default: {
23430
+ const actionFlags = { ...flags };
23431
+ delete actionFlags.profile;
23432
+ await runObjectVerb(positionals, actionFlags, ctx);
23433
+ break;
23434
+ }
23136
23435
  }
23137
23436
  } catch (err) {
23138
23437
  const action = matchCommand(positionals)?.action.name ?? cmd ?? "arbor";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lambdacurry/arbor",
3
- "version": "0.21.41",
3
+ "version": "0.21.43",
4
4
  "description": "The Arbor CLI — a shared workspace for people and agents. The human + headless-agent write path over Arbor's guarded operation surface.",
5
5
  "keywords": [
6
6
  "agents",