@lambdacurry/arbor 0.21.42 → 0.21.44
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/README.md +33 -0
- package/dist/arbor.js +419 -111
- 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.
|
|
19
|
+
version: "0.21.44",
|
|
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",
|
|
@@ -18081,7 +18081,20 @@ var MCP_OUTPUT_SCHEMAS = {
|
|
|
18081
18081
|
generations: exports_external.array(jsonObject),
|
|
18082
18082
|
sessions: exports_external.array(jsonObject),
|
|
18083
18083
|
lineage: exports_external.array(jsonObject),
|
|
18084
|
-
reprovisions: exports_external.array(jsonObject)
|
|
18084
|
+
reprovisions: exports_external.array(jsonObject),
|
|
18085
|
+
recoverySafety: exports_external.looseObject({
|
|
18086
|
+
state: exports_external.enum([
|
|
18087
|
+
"safe-to-restore",
|
|
18088
|
+
"possible-uncheckpointed-work",
|
|
18089
|
+
"unknown-mutation-outcome",
|
|
18090
|
+
"unknown-baseline"
|
|
18091
|
+
]),
|
|
18092
|
+
computerId: id,
|
|
18093
|
+
durableSnapshotId: id.nullable(),
|
|
18094
|
+
started: exports_external.number().int().min(0),
|
|
18095
|
+
acknowledged: exports_external.number().int().min(0),
|
|
18096
|
+
durable: exports_external.number().int().min(0).nullable()
|
|
18097
|
+
}).nullable().optional()
|
|
18085
18098
|
}),
|
|
18086
18099
|
computer_inventory: exports_external.looseObject({
|
|
18087
18100
|
computers: exports_external.array(jsonObject),
|
|
@@ -18715,6 +18728,15 @@ function selectedArray(value, fields) {
|
|
|
18715
18728
|
return;
|
|
18716
18729
|
return value.map((entry) => selected(entry, fields) ?? {});
|
|
18717
18730
|
}
|
|
18731
|
+
function omitNull(value) {
|
|
18732
|
+
return value === null ? undefined : value;
|
|
18733
|
+
}
|
|
18734
|
+
var PROVIDER_NATIVE_EXECUTION = new Set(["foreground", "background"]);
|
|
18735
|
+
function omitProviderNativeExecution(value) {
|
|
18736
|
+
if (typeof value === "string" && PROVIDER_NATIVE_EXECUTION.has(value))
|
|
18737
|
+
return;
|
|
18738
|
+
return omitNull(value);
|
|
18739
|
+
}
|
|
18718
18740
|
var OUTPUT_SHAPERS = {
|
|
18719
18741
|
contribute: (result) => defined([
|
|
18720
18742
|
["contributionId", result.contributionId],
|
|
@@ -18884,14 +18906,14 @@ var OUTPUT_SHAPERS = {
|
|
|
18884
18906
|
]);
|
|
18885
18907
|
},
|
|
18886
18908
|
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]
|
|
18909
|
+
["stdout", omitNull(result.stdout)],
|
|
18910
|
+
["stderr", omitNull(result.stderr)],
|
|
18911
|
+
["exitCode", omitNull(result.exitCode)],
|
|
18912
|
+
["processId", omitNull(result.processId)],
|
|
18913
|
+
["status", omitNull(result.status)],
|
|
18914
|
+
["execution", omitProviderNativeExecution(result.execution)],
|
|
18915
|
+
["nextAction", omitNull(result.nextAction)],
|
|
18916
|
+
["cwd", input.cwd === undefined ? omitNull(result.cwd) : undefined]
|
|
18895
18917
|
]),
|
|
18896
18918
|
computer_process_read: (result) => defined([
|
|
18897
18919
|
["processId", result.processId],
|
|
@@ -18924,8 +18946,10 @@ function shapeActionOutput(actionName, value, input = {}) {
|
|
|
18924
18946
|
if (!schema)
|
|
18925
18947
|
throw new Error(`${actionName}: compact action is missing an output schema`);
|
|
18926
18948
|
const parsed = schema.safeParse(publicValue);
|
|
18927
|
-
if (!parsed.success)
|
|
18928
|
-
|
|
18949
|
+
if (!parsed.success) {
|
|
18950
|
+
const paths = parsed.error.issues.map((issue2) => issue2.path.map(String).join(".") || "root");
|
|
18951
|
+
throw new Error(`${actionName}: action returned an invalid public result (${paths.join(", ")})`);
|
|
18952
|
+
}
|
|
18929
18953
|
return parsed.data;
|
|
18930
18954
|
}
|
|
18931
18955
|
// ../actions/src/tool-annotations.ts
|
|
@@ -19930,7 +19954,7 @@ var ACTION_DEFINITIONS = [
|
|
|
19930
19954
|
{
|
|
19931
19955
|
name: "computer_status",
|
|
19932
19956
|
title: "Inspect a computer",
|
|
19933
|
-
description: "Inspect the live PARENT Thread Computer's capabilities,
|
|
19957
|
+
description: "Inspect the live PARENT Thread Computer's capabilities, live can-I-exec readiness, processes, and profile. Transient session unavailability drops command capabilities and may retry computer_open once; a terminal unknown mutation is a non-retryable stop — this does not report isolated Run command state, so use computer_run_receipt for Run state or get_computer for restore-safety.",
|
|
19934
19958
|
inputSchema: {
|
|
19935
19959
|
computerSessionId: exports_external.string().describe("the computerSessionId returned by computer_open, cms_…")
|
|
19936
19960
|
},
|
|
@@ -20128,7 +20152,7 @@ var ACTION_DEFINITIONS = [
|
|
|
20128
20152
|
{
|
|
20129
20153
|
name: "get_computer",
|
|
20130
20154
|
title: "Read a Thread computer",
|
|
20131
|
-
description: "Inspect a Thread's durable Computer recipe/materialization state, compatibility, snapshot lineage, recent receipts, Runs, and reprovision outcomes; setup content and credentials stay redacted.
|
|
20155
|
+
description: "Inspect a Thread's durable Computer recipe/materialization state, compatibility, snapshot lineage, recoverySafety, recent receipts, Runs, and reprovision outcomes; setup content and credentials stay redacted. readiness here is restore-safety (decision-required means Arbor will not silently restore); use computer_status for whether the opened session can exec now.",
|
|
20132
20156
|
inputSchema: {
|
|
20133
20157
|
threadId: exports_external.string().describe("the Thread, thr_…"),
|
|
20134
20158
|
lineageLimit: exports_external.number().int().min(1).max(100).optional().describe("recent receipts/lineage rows (default 20)")
|
|
@@ -21831,105 +21855,16 @@ var ACTIONS3 = [...ACTIONS2, ...RECOVERY_ACTIONS];
|
|
|
21831
21855
|
import {
|
|
21832
21856
|
chmodSync,
|
|
21833
21857
|
existsSync,
|
|
21858
|
+
lstatSync,
|
|
21834
21859
|
mkdirSync,
|
|
21835
21860
|
readFileSync,
|
|
21861
|
+
readdirSync,
|
|
21836
21862
|
rmSync,
|
|
21837
21863
|
statSync,
|
|
21838
21864
|
writeFileSync
|
|
21839
21865
|
} from "node:fs";
|
|
21840
21866
|
import { homedir } from "node:os";
|
|
21841
21867
|
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
21868
|
|
|
21934
21869
|
// src/errors.ts
|
|
21935
21870
|
var ERROR_CODES = new Set([
|
|
@@ -22069,6 +22004,310 @@ class UsageError extends Error {
|
|
|
22069
22004
|
}
|
|
22070
22005
|
}
|
|
22071
22006
|
|
|
22007
|
+
// src/config.ts
|
|
22008
|
+
var PROD_API_URL = "https://arborthreads.com";
|
|
22009
|
+
var PROFILE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
22010
|
+
function arborDir() {
|
|
22011
|
+
return join(homedir(), ".arbor");
|
|
22012
|
+
}
|
|
22013
|
+
function defaultConfigPath() {
|
|
22014
|
+
return join(arborDir(), "config.json");
|
|
22015
|
+
}
|
|
22016
|
+
function profilesDir() {
|
|
22017
|
+
return join(arborDir(), "profiles");
|
|
22018
|
+
}
|
|
22019
|
+
function registryPath() {
|
|
22020
|
+
return join(arborDir(), "profiles.json");
|
|
22021
|
+
}
|
|
22022
|
+
function namedConfigPath(name) {
|
|
22023
|
+
return join(profilesDir(), `${name}.json`);
|
|
22024
|
+
}
|
|
22025
|
+
var CONFIG_PATH = process.env.ARBOR_CONFIG ?? join(homedir(), ".arbor", "config.json");
|
|
22026
|
+
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.";
|
|
22027
|
+
var LOCK_TIMEOUT_MS = 5000;
|
|
22028
|
+
var LOCK_STALE_MS = 30000;
|
|
22029
|
+
var LOCK_POLL_MS = 20;
|
|
22030
|
+
var activeTarget;
|
|
22031
|
+
function sleepSync(ms) {
|
|
22032
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
22033
|
+
}
|
|
22034
|
+
function acquireConfigLock(configPath) {
|
|
22035
|
+
const lockPath = `${configPath}.lock`;
|
|
22036
|
+
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
22037
|
+
for (;; ) {
|
|
22038
|
+
try {
|
|
22039
|
+
mkdirSync(lockPath);
|
|
22040
|
+
return () => {
|
|
22041
|
+
try {
|
|
22042
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
22043
|
+
} catch {}
|
|
22044
|
+
};
|
|
22045
|
+
} catch (err) {
|
|
22046
|
+
if (err?.code !== "EEXIST")
|
|
22047
|
+
throw err;
|
|
22048
|
+
try {
|
|
22049
|
+
if (Date.now() - statSync(lockPath).mtimeMs > LOCK_STALE_MS) {
|
|
22050
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
22051
|
+
continue;
|
|
22052
|
+
}
|
|
22053
|
+
} catch {
|
|
22054
|
+
continue;
|
|
22055
|
+
}
|
|
22056
|
+
if (Date.now() >= deadline) {
|
|
22057
|
+
throw new Error(`timed out waiting for another arbor process to finish writing ${configPath}. If none is running, remove ${lockPath}.`);
|
|
22058
|
+
}
|
|
22059
|
+
sleepSync(LOCK_POLL_MS);
|
|
22060
|
+
}
|
|
22061
|
+
}
|
|
22062
|
+
}
|
|
22063
|
+
function validateProfileName(name, field = "profile") {
|
|
22064
|
+
if (!PROFILE_NAME.test(name)) {
|
|
22065
|
+
throw new UsageError(`invalid profile name: ${JSON.stringify(name)}`, {
|
|
22066
|
+
reason: "validation.invalid_value",
|
|
22067
|
+
field
|
|
22068
|
+
});
|
|
22069
|
+
}
|
|
22070
|
+
}
|
|
22071
|
+
function rejectSymlink(path, label) {
|
|
22072
|
+
if (!existsSync(path))
|
|
22073
|
+
return;
|
|
22074
|
+
if (lstatSync(path).isSymbolicLink())
|
|
22075
|
+
throw new Error(`${label} must not be a symbolic link: ${path}`);
|
|
22076
|
+
}
|
|
22077
|
+
function ensureArborDir() {
|
|
22078
|
+
const dir = arborDir();
|
|
22079
|
+
rejectSymlink(dir, "Arbor config directory");
|
|
22080
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
22081
|
+
chmodSync(dir, 448);
|
|
22082
|
+
}
|
|
22083
|
+
function ensureProfilesDir() {
|
|
22084
|
+
ensureArborDir();
|
|
22085
|
+
const dir = profilesDir();
|
|
22086
|
+
rejectSymlink(dir, "Arbor profiles directory");
|
|
22087
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
22088
|
+
chmodSync(dir, 448);
|
|
22089
|
+
}
|
|
22090
|
+
function readRegistry() {
|
|
22091
|
+
const path = registryPath();
|
|
22092
|
+
if (!existsSync(path))
|
|
22093
|
+
return {};
|
|
22094
|
+
rejectSymlink(path, "Arbor profile registry");
|
|
22095
|
+
try {
|
|
22096
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
22097
|
+
if (parsed.defaultProfile !== undefined)
|
|
22098
|
+
validateProfileName(parsed.defaultProfile);
|
|
22099
|
+
return { defaultProfile: parsed.defaultProfile };
|
|
22100
|
+
} catch (err) {
|
|
22101
|
+
if (err instanceof UsageError)
|
|
22102
|
+
throw err;
|
|
22103
|
+
throw new Error(`could not read Arbor profile registry ${path}`);
|
|
22104
|
+
}
|
|
22105
|
+
}
|
|
22106
|
+
function writeRegistry(registry2) {
|
|
22107
|
+
ensureArborDir();
|
|
22108
|
+
const path = registryPath();
|
|
22109
|
+
rejectSymlink(path, "Arbor profile registry");
|
|
22110
|
+
const release = acquireConfigLock(path);
|
|
22111
|
+
try {
|
|
22112
|
+
const safe = registry2.defaultProfile ? { defaultProfile: registry2.defaultProfile } : {};
|
|
22113
|
+
writeFileSync(path, `${JSON.stringify(safe, null, 2)}
|
|
22114
|
+
`, { mode: 384 });
|
|
22115
|
+
chmodSync(path, 384);
|
|
22116
|
+
} finally {
|
|
22117
|
+
release();
|
|
22118
|
+
}
|
|
22119
|
+
}
|
|
22120
|
+
function profileExists(name, field = "profile") {
|
|
22121
|
+
validateProfileName(name, field);
|
|
22122
|
+
const path = namedConfigPath(name);
|
|
22123
|
+
if (!existsSync(path))
|
|
22124
|
+
return false;
|
|
22125
|
+
rejectSymlink(path, `Arbor profile ${name}`);
|
|
22126
|
+
return lstatSync(path).isFile();
|
|
22127
|
+
}
|
|
22128
|
+
function legacyTarget() {
|
|
22129
|
+
const defaultPath = defaultConfigPath();
|
|
22130
|
+
const envPath = process.env.ARBOR_CONFIG;
|
|
22131
|
+
return {
|
|
22132
|
+
path: envPath ?? defaultPath,
|
|
22133
|
+
defaultPath,
|
|
22134
|
+
isolated: Boolean(envPath),
|
|
22135
|
+
named: false
|
|
22136
|
+
};
|
|
22137
|
+
}
|
|
22138
|
+
function resolveTarget(opts) {
|
|
22139
|
+
const profileFlag = opts?.profileFlag;
|
|
22140
|
+
const envProfile = process.env.ARBOR_PROFILE;
|
|
22141
|
+
if (process.env.ARBOR_CONFIG && profileFlag !== undefined) {
|
|
22142
|
+
throw new UsageError("ARBOR_CONFIG and --profile are ambiguous; choose one credential selector", {
|
|
22143
|
+
reason: "validation.invalid_value",
|
|
22144
|
+
field: "profile"
|
|
22145
|
+
});
|
|
22146
|
+
}
|
|
22147
|
+
if (process.env.ARBOR_CONFIG && envProfile !== undefined) {
|
|
22148
|
+
throw new UsageError("ARBOR_CONFIG and ARBOR_PROFILE are ambiguous; choose one credential selector", { reason: "validation.invalid_value", field: "ARBOR_PROFILE" });
|
|
22149
|
+
}
|
|
22150
|
+
if (process.env.ARBOR_CONFIG)
|
|
22151
|
+
return legacyTarget();
|
|
22152
|
+
const registryDefault = readRegistry().defaultProfile;
|
|
22153
|
+
const selected2 = profileFlag ?? envProfile ?? registryDefault;
|
|
22154
|
+
if (selected2 === undefined)
|
|
22155
|
+
return legacyTarget();
|
|
22156
|
+
const field = profileFlag !== undefined ? "profile" : envProfile !== undefined ? "ARBOR_PROFILE" : "profile";
|
|
22157
|
+
validateProfileName(selected2, field);
|
|
22158
|
+
if (!opts?.allowMissingProfile && !profileExists(selected2, field)) {
|
|
22159
|
+
throw new UsageError(`unknown profile: ${selected2}`, {
|
|
22160
|
+
reason: "validation.invalid_value",
|
|
22161
|
+
field
|
|
22162
|
+
});
|
|
22163
|
+
}
|
|
22164
|
+
return {
|
|
22165
|
+
path: namedConfigPath(selected2),
|
|
22166
|
+
defaultPath: defaultConfigPath(),
|
|
22167
|
+
isolated: true,
|
|
22168
|
+
profile: selected2,
|
|
22169
|
+
named: true
|
|
22170
|
+
};
|
|
22171
|
+
}
|
|
22172
|
+
function configureConfigTarget(opts) {
|
|
22173
|
+
activeTarget = resolveTarget(opts);
|
|
22174
|
+
}
|
|
22175
|
+
function currentTarget() {
|
|
22176
|
+
return activeTarget ?? resolveTarget();
|
|
22177
|
+
}
|
|
22178
|
+
function getConfigPath() {
|
|
22179
|
+
return currentTarget().path;
|
|
22180
|
+
}
|
|
22181
|
+
function listProfiles() {
|
|
22182
|
+
const registry2 = readRegistry();
|
|
22183
|
+
const dir = profilesDir();
|
|
22184
|
+
if (!existsSync(dir))
|
|
22185
|
+
return { defaultProfile: registry2.defaultProfile, profiles: [] };
|
|
22186
|
+
rejectSymlink(dir, "Arbor profiles directory");
|
|
22187
|
+
const profiles2 = readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.name.endsWith(".json")).map((entry) => {
|
|
22188
|
+
const name = entry.name.slice(0, -5);
|
|
22189
|
+
validateProfileName(name);
|
|
22190
|
+
const path = join(dir, entry.name);
|
|
22191
|
+
if (entry.isSymbolicLink())
|
|
22192
|
+
throw new Error(`Arbor profile ${name} must not be a symbolic link: ${path}`);
|
|
22193
|
+
if (!entry.isFile())
|
|
22194
|
+
throw new Error(`Arbor profile ${name} is not a regular file: ${path}`);
|
|
22195
|
+
return { name, default: registry2.defaultProfile === name };
|
|
22196
|
+
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
22197
|
+
return { defaultProfile: registry2.defaultProfile, profiles: profiles2 };
|
|
22198
|
+
}
|
|
22199
|
+
function profileShow(name) {
|
|
22200
|
+
validateProfileName(name);
|
|
22201
|
+
if (!profileExists(name)) {
|
|
22202
|
+
throw new UsageError(`unknown profile: ${name}`, {
|
|
22203
|
+
reason: "validation.invalid_value",
|
|
22204
|
+
field: "profile"
|
|
22205
|
+
});
|
|
22206
|
+
}
|
|
22207
|
+
const registry2 = readRegistry();
|
|
22208
|
+
const cfg = JSON.parse(readFileSync(namedConfigPath(name), "utf8"));
|
|
22209
|
+
return {
|
|
22210
|
+
name,
|
|
22211
|
+
default: registry2.defaultProfile === name,
|
|
22212
|
+
apiUrl: cfg.apiUrl || PROD_API_URL
|
|
22213
|
+
};
|
|
22214
|
+
}
|
|
22215
|
+
function setDefaultProfile(name) {
|
|
22216
|
+
if (name !== null) {
|
|
22217
|
+
validateProfileName(name);
|
|
22218
|
+
if (!profileExists(name)) {
|
|
22219
|
+
throw new UsageError(`unknown profile: ${name}`, {
|
|
22220
|
+
reason: "validation.invalid_value",
|
|
22221
|
+
field: "profile"
|
|
22222
|
+
});
|
|
22223
|
+
}
|
|
22224
|
+
}
|
|
22225
|
+
writeRegistry(name ? { defaultProfile: name } : {});
|
|
22226
|
+
}
|
|
22227
|
+
function clearDefaultProfile() {
|
|
22228
|
+
setDefaultProfile(null);
|
|
22229
|
+
}
|
|
22230
|
+
function deleteProfile(name) {
|
|
22231
|
+
validateProfileName(name);
|
|
22232
|
+
const registry2 = readRegistry();
|
|
22233
|
+
if (registry2.defaultProfile === name) {
|
|
22234
|
+
throw new UsageError(`cannot delete default profile ${name}; select another default or run profile clear-default first`, { reason: "validation.invalid_value", field: "profile" });
|
|
22235
|
+
}
|
|
22236
|
+
if (!profileExists(name)) {
|
|
22237
|
+
throw new UsageError(`unknown profile: ${name}`, {
|
|
22238
|
+
reason: "validation.invalid_value",
|
|
22239
|
+
field: "profile"
|
|
22240
|
+
});
|
|
22241
|
+
}
|
|
22242
|
+
const path = namedConfigPath(name);
|
|
22243
|
+
rejectSymlink(path, `Arbor profile ${name}`);
|
|
22244
|
+
rmSync(path);
|
|
22245
|
+
}
|
|
22246
|
+
function defaultConfigOverwriteBlocked(opts) {
|
|
22247
|
+
const target = currentTarget();
|
|
22248
|
+
const isolated = opts?.isolated ?? target.isolated;
|
|
22249
|
+
const path = opts?.path ?? target.defaultPath;
|
|
22250
|
+
if (isolated)
|
|
22251
|
+
return false;
|
|
22252
|
+
if (!existsSync(path))
|
|
22253
|
+
return false;
|
|
22254
|
+
try {
|
|
22255
|
+
const cfg = JSON.parse(readFileSync(path, "utf8"));
|
|
22256
|
+
return Boolean(cfg.token);
|
|
22257
|
+
} catch {
|
|
22258
|
+
return false;
|
|
22259
|
+
}
|
|
22260
|
+
}
|
|
22261
|
+
function loadConfig() {
|
|
22262
|
+
const target = currentTarget();
|
|
22263
|
+
const envToken = process.env.ARBOR_TOKEN || undefined;
|
|
22264
|
+
const envUrl = process.env.ARBOR_API_URL || undefined;
|
|
22265
|
+
if (target.named)
|
|
22266
|
+
rejectSymlink(target.path, `Arbor profile ${target.profile}`);
|
|
22267
|
+
if (existsSync(target.path)) {
|
|
22268
|
+
try {
|
|
22269
|
+
const cfg = JSON.parse(readFileSync(target.path, "utf8"));
|
|
22270
|
+
return { apiUrl: envUrl ?? (cfg.apiUrl || PROD_API_URL), token: envToken ?? cfg.token };
|
|
22271
|
+
} catch (err) {
|
|
22272
|
+
if (err instanceof Error && /symbolic link/i.test(err.message))
|
|
22273
|
+
throw err;
|
|
22274
|
+
}
|
|
22275
|
+
}
|
|
22276
|
+
return { apiUrl: envUrl ?? PROD_API_URL, token: envToken };
|
|
22277
|
+
}
|
|
22278
|
+
function writeConfigGuarded(opts) {
|
|
22279
|
+
mkdirSync(dirname(opts.configPath), { recursive: true });
|
|
22280
|
+
const release = acquireConfigLock(opts.configPath);
|
|
22281
|
+
try {
|
|
22282
|
+
if (!opts.allowDefaultTokenOverwrite && defaultConfigOverwriteBlocked({ isolated: opts.isolated, path: opts.defaultPath })) {
|
|
22283
|
+
throw new Error(DEFAULT_TOKEN_OVERWRITE_REFUSAL);
|
|
22284
|
+
}
|
|
22285
|
+
writeFileSync(opts.configPath, `${JSON.stringify(opts.cfg, null, 2)}
|
|
22286
|
+
`, { mode: 384 });
|
|
22287
|
+
chmodSync(opts.configPath, 384);
|
|
22288
|
+
} finally {
|
|
22289
|
+
release();
|
|
22290
|
+
}
|
|
22291
|
+
}
|
|
22292
|
+
function saveConfig(cfg, opts) {
|
|
22293
|
+
const target = currentTarget();
|
|
22294
|
+
if (target.named) {
|
|
22295
|
+
ensureProfilesDir();
|
|
22296
|
+
rejectSymlink(target.path, `Arbor profile ${target.profile}`);
|
|
22297
|
+
}
|
|
22298
|
+
writeConfigGuarded({
|
|
22299
|
+
cfg,
|
|
22300
|
+
configPath: target.path,
|
|
22301
|
+
defaultPath: target.defaultPath,
|
|
22302
|
+
isolated: target.isolated,
|
|
22303
|
+
allowDefaultTokenOverwrite: opts?.allowDefaultTokenOverwrite
|
|
22304
|
+
});
|
|
22305
|
+
}
|
|
22306
|
+
function clearToken() {
|
|
22307
|
+
const cfg = loadConfig();
|
|
22308
|
+
saveConfig({ apiUrl: cfg.apiUrl }, { allowDefaultTokenOverwrite: true });
|
|
22309
|
+
}
|
|
22310
|
+
|
|
22072
22311
|
// src/source.ts
|
|
22073
22312
|
var SNIFF = [
|
|
22074
22313
|
["CLAUDECODE", "Claude Code"],
|
|
@@ -23004,6 +23243,11 @@ Usage: arbor <command> [--flags]
|
|
|
23004
23243
|
login [--url <api-url>] authorize in your browser (device flow) → stores a token
|
|
23005
23244
|
auth <token> [--url …] save a token directly (a PAT or agent key) — headless, no browser
|
|
23006
23245
|
connect <pairing-key> register THIS agent: relay the URL+code to your human to approve (AD-109)
|
|
23246
|
+
profile list list configured profile names (never credentials)
|
|
23247
|
+
profile show <name> show safe local metadata for one profile
|
|
23248
|
+
profile set-default <name> configure the ordinary-command default profile
|
|
23249
|
+
profile clear-default intentionally return ordinary commands to legacy config
|
|
23250
|
+
profile delete <name> delete a non-default named profile
|
|
23007
23251
|
orient print how to work well in Arbor (the room etiquette — when + why)
|
|
23008
23252
|
whoami print who your token resolves to
|
|
23009
23253
|
logout forget the stored token
|
|
@@ -23012,6 +23256,7 @@ Usage: arbor <command> [--flags]
|
|
|
23012
23256
|
help show this help
|
|
23013
23257
|
|
|
23014
23258
|
Global flags (any command):
|
|
23259
|
+
--profile <name> use one named credential for this command only
|
|
23015
23260
|
--json emit a structured { ok, data|error, meta } envelope on stdout (machine mode)
|
|
23016
23261
|
--quiet / --no-quiet suppress / force advisory text (auto-quiet under --json or a pipe)
|
|
23017
23262
|
--fields a,b,arr:N keep only these TOP-LEVEL fields; an "arr:N" entry tail-slices an ARRAY field to
|
|
@@ -23067,6 +23312,15 @@ async function main() {
|
|
|
23067
23312
|
const ctx = resolveCommandOutput(positionals, flags);
|
|
23068
23313
|
const [cmd] = positionals;
|
|
23069
23314
|
try {
|
|
23315
|
+
const profileFlag = stringFlag(flags.profile, "profile");
|
|
23316
|
+
if (cmd !== "profile") {
|
|
23317
|
+
configureConfigTarget({
|
|
23318
|
+
profileFlag,
|
|
23319
|
+
allowMissingProfile: cmd === "login" || cmd === "auth" || cmd === "connect"
|
|
23320
|
+
});
|
|
23321
|
+
} else if (profileFlag !== undefined) {
|
|
23322
|
+
throw new UsageError("--profile selects a credential for a command; profile management names its target positionally");
|
|
23323
|
+
}
|
|
23070
23324
|
if (flags.version !== undefined && positionals.length === 0 || cmd === "version") {
|
|
23071
23325
|
renderVersion(ctx);
|
|
23072
23326
|
return;
|
|
@@ -23087,7 +23341,7 @@ async function main() {
|
|
|
23087
23341
|
case "login": {
|
|
23088
23342
|
await login({ url: stringFlag(flags.url, "url") });
|
|
23089
23343
|
advise(`
|
|
23090
|
-
✓ Logged in (${
|
|
23344
|
+
✓ Logged in (${getConfigPath()}).
|
|
23091
23345
|
`, ctx);
|
|
23092
23346
|
await renderMe(ctx, "login");
|
|
23093
23347
|
return;
|
|
@@ -23098,7 +23352,7 @@ async function main() {
|
|
|
23098
23352
|
throw new UsageError("usage: arbor auth <token> [--url <api-url>]");
|
|
23099
23353
|
const url2 = stringFlag(flags.url, "url") ?? loadConfig().apiUrl;
|
|
23100
23354
|
saveConfig({ apiUrl: url2, token }, { allowDefaultTokenOverwrite: true });
|
|
23101
|
-
advise(` ✓ Token saved (${
|
|
23355
|
+
advise(` ✓ Token saved (${getConfigPath()}).
|
|
23102
23356
|
`, ctx);
|
|
23103
23357
|
await renderMe(ctx, "auth");
|
|
23104
23358
|
return;
|
|
@@ -23109,7 +23363,7 @@ async function main() {
|
|
|
23109
23363
|
throw new UsageError("usage: arbor connect <pairing-key> [--url <api-url>]");
|
|
23110
23364
|
await connect({ pairingKey: key, url: stringFlag(flags.url, "url") });
|
|
23111
23365
|
advise(`
|
|
23112
|
-
✓ Connected (${
|
|
23366
|
+
✓ Connected (${getConfigPath()}).
|
|
23113
23367
|
`, ctx);
|
|
23114
23368
|
await renderMe(ctx, "connect");
|
|
23115
23369
|
advise(`
|
|
@@ -23123,9 +23377,59 @@ ${CLI_NOTE}
|
|
|
23123
23377
|
}
|
|
23124
23378
|
case "logout":
|
|
23125
23379
|
clearToken();
|
|
23126
|
-
emitDual({ loggedOut: true }, `Logged out — token cleared from ${
|
|
23380
|
+
emitDual({ loggedOut: true }, `Logged out — token cleared from ${getConfigPath()}.
|
|
23127
23381
|
`, "logout", ctx);
|
|
23128
23382
|
return;
|
|
23383
|
+
case "profile": {
|
|
23384
|
+
const subcommand = positionals[1];
|
|
23385
|
+
if (subcommand === "list") {
|
|
23386
|
+
if (positionals.length !== 2)
|
|
23387
|
+
throw new UsageError("usage: arbor profile list");
|
|
23388
|
+
const data = listProfiles();
|
|
23389
|
+
const human = data.profiles.length ? `${data.profiles.map((profile3) => `${profile3.default ? "*" : " "} ${profile3.name}`).join(`
|
|
23390
|
+
`)}
|
|
23391
|
+
` : `No named profiles configured.
|
|
23392
|
+
`;
|
|
23393
|
+
emitDual(data, human, "profile list", ctx);
|
|
23394
|
+
return;
|
|
23395
|
+
}
|
|
23396
|
+
if (subcommand === "show") {
|
|
23397
|
+
const name = positionals[2];
|
|
23398
|
+
if (!name || positionals.length !== 3)
|
|
23399
|
+
throw new UsageError("usage: arbor profile show <name>");
|
|
23400
|
+
const data = profileShow(name);
|
|
23401
|
+
emitDual(data, `${data.name}${data.default ? " (default)" : ""} — ${data.apiUrl}
|
|
23402
|
+
`, "profile show", ctx);
|
|
23403
|
+
return;
|
|
23404
|
+
}
|
|
23405
|
+
if (subcommand === "set-default") {
|
|
23406
|
+
const name = positionals[2];
|
|
23407
|
+
if (!name || positionals.length !== 3)
|
|
23408
|
+
throw new UsageError("usage: arbor profile set-default <name>");
|
|
23409
|
+
setDefaultProfile(name);
|
|
23410
|
+
emitDual({ defaultProfile: name }, `Default profile: ${name}
|
|
23411
|
+
`, "profile set-default", ctx);
|
|
23412
|
+
return;
|
|
23413
|
+
}
|
|
23414
|
+
if (subcommand === "clear-default") {
|
|
23415
|
+
if (positionals.length !== 2)
|
|
23416
|
+
throw new UsageError("usage: arbor profile clear-default");
|
|
23417
|
+
clearDefaultProfile();
|
|
23418
|
+
emitDual({ defaultProfile: null }, `Default profile cleared; ordinary commands use legacy config.
|
|
23419
|
+
`, "profile clear-default", ctx);
|
|
23420
|
+
return;
|
|
23421
|
+
}
|
|
23422
|
+
if (subcommand === "delete") {
|
|
23423
|
+
const name = positionals[2];
|
|
23424
|
+
if (!name || positionals.length !== 3)
|
|
23425
|
+
throw new UsageError("usage: arbor profile delete <name>");
|
|
23426
|
+
deleteProfile(name);
|
|
23427
|
+
emitDual({ deleted: true, profile: name }, `Deleted profile: ${name}
|
|
23428
|
+
`, "profile delete", ctx);
|
|
23429
|
+
return;
|
|
23430
|
+
}
|
|
23431
|
+
throw new UsageError("usage: arbor profile <list|show|set-default|clear-default|delete> [name]");
|
|
23432
|
+
}
|
|
23129
23433
|
case "whoami":
|
|
23130
23434
|
await renderMe(ctx, "whoami");
|
|
23131
23435
|
return;
|
|
@@ -23135,8 +23439,12 @@ ${CLI_NOTE}
|
|
|
23135
23439
|
case "health":
|
|
23136
23440
|
await renderHealth(ctx);
|
|
23137
23441
|
return;
|
|
23138
|
-
default:
|
|
23139
|
-
|
|
23442
|
+
default: {
|
|
23443
|
+
const actionFlags = { ...flags };
|
|
23444
|
+
delete actionFlags.profile;
|
|
23445
|
+
await runObjectVerb(positionals, actionFlags, ctx);
|
|
23446
|
+
break;
|
|
23447
|
+
}
|
|
23140
23448
|
}
|
|
23141
23449
|
} catch (err) {
|
|
23142
23450
|
const action = matchCommand(positionals)?.action.name ?? cmd ?? "arbor";
|
package/package.json
CHANGED