@anyforge/control 0.2.4 → 0.4.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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@anyforge/control",
3
- "version": "0.2.4",
4
- "description": "AnyForge Control CLI — governed LLM proxy for Claude Code, Cursor, Cline, and any agent that talks to OpenAI- or Anthropic-shape APIs.",
3
+ "version": "0.4.0",
4
+ "description": "AnyForge Control CLI \u2014 governed LLM proxy for any agent, and a scriptable client for AnyForge approvals, crews, work items and workflows.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://anyforge.ai",
7
7
  "repository": {
@@ -23,14 +23,16 @@
23
23
  "README.md"
24
24
  ],
25
25
  "scripts": {
26
- "build": "tsc && chmod +x dist/cli.js",
26
+ "build": "esbuild src/cli.ts --bundle --minify --platform=node --format=esm --outfile=dist/cli.js --packages=external && chmod +x dist/cli.js",
27
+ "type-check": "tsc --noEmit",
28
+ "test": "tsx --test src/commands/*.test.ts",
27
29
  "dev": "tsx src/cli.ts",
28
30
  "prepublishOnly": "npm run build",
29
31
  "publish:beta": "npm publish --access public --tag beta",
30
32
  "publish:stable": "npm publish --access public"
31
33
  },
32
34
  "engines": {
33
- "node": ">=18"
35
+ "node": ">=22"
34
36
  },
35
37
  "dependencies": {
36
38
  "kleur": "^4.1.5",
@@ -38,9 +40,10 @@
38
40
  "prompts": "^2.4.2"
39
41
  },
40
42
  "devDependencies": {
41
- "@types/node": "^20.11.0",
43
+ "@types/node": "^22.20.0",
42
44
  "@types/prompts": "^2.4.9",
45
+ "esbuild": "^0.25.0",
43
46
  "tsx": "^4.7.0",
44
- "typescript": "^5.5.3"
47
+ "typescript": "^7.0.2"
45
48
  }
46
49
  }
@@ -1,92 +0,0 @@
1
- import { spawn } from 'node:child_process';
2
- import { readFile } from 'node:fs/promises';
3
- import { red, green, yellow, dim, bold } from 'kleur/colors';
4
- export async function runApplyPatch(opts) {
5
- const patchContent = await loadPatch(opts.patchFile);
6
- if (!patchContent.trim()) {
7
- process.stderr.write(red('Empty patch — nothing to apply.\n'));
8
- process.exit(1);
9
- }
10
- const stats = parseDiffStats(patchContent);
11
- process.stderr.write(dim(`Patch: ${stats.files} ${stats.files === 1 ? 'file' : 'files'}, `) +
12
- green(`+${stats.added}`) + ' ' + red(`-${stats.removed}`) + '\n');
13
- // git apply --check first — never make a partial mess
14
- const check = await runGit(['apply', '--check'], patchContent);
15
- if (check.code !== 0) {
16
- if (opts.threeWay) {
17
- const three = await runGit(['apply', '--3way'], patchContent);
18
- if (three.code !== 0) {
19
- process.stderr.write(red('3-way apply failed:\n') + three.stderr + '\n');
20
- process.exit(1);
21
- }
22
- process.stderr.write(yellow('✓ Applied via 3-way merge — review conflicts before committing.\n'));
23
- return;
24
- }
25
- process.stderr.write(red('Patch does not apply cleanly:\n') + check.stderr + '\n');
26
- process.stderr.write(dim('Try ') + bold('--3way') + dim(' to attempt a 3-way merge against the index.\n'));
27
- process.exit(1);
28
- }
29
- if (opts.dryRun) {
30
- process.stderr.write(green('✓ Patch would apply cleanly.\n'));
31
- return;
32
- }
33
- const apply = await runGit(['apply'], patchContent);
34
- if (apply.code !== 0) {
35
- process.stderr.write(red('git apply failed:\n') + apply.stderr + '\n');
36
- process.exit(1);
37
- }
38
- process.stderr.write(green('✓ Applied.\n'));
39
- if (stats.files > 0) {
40
- process.stderr.write(dim(' Run ') + bold('git diff --stat') + dim(' to review.\n'));
41
- }
42
- }
43
- async function loadPatch(filePath) {
44
- if (filePath) {
45
- return await readFile(filePath, 'utf8');
46
- }
47
- // Read stdin to a single string
48
- const chunks = [];
49
- for await (const chunk of process.stdin) {
50
- chunks.push(chunk);
51
- }
52
- return Buffer.concat(chunks).toString('utf8');
53
- }
54
- /** Spawn `git <args>`, pipe `stdinData` to its stdin, return code +
55
- * captured streams. No shell — args are passed as a list to spawn. */
56
- function runGit(args, stdinData) {
57
- return new Promise((resolve, reject) => {
58
- const child = spawn('git', args, { stdio: ['pipe', 'pipe', 'pipe'] });
59
- let stdout = '';
60
- let stderr = '';
61
- child.stdout.on('data', d => { stdout += d.toString(); });
62
- child.stderr.on('data', d => { stderr += d.toString(); });
63
- child.on('error', reject);
64
- child.on('close', code => resolve({ code: code ?? 0, stdout, stderr }));
65
- child.stdin.write(stdinData);
66
- child.stdin.end();
67
- });
68
- }
69
- /** Quick stats parse off a unified diff — counts file headers and
70
- * +/- lines. Skips diff metadata lines (+++, ---) so the report
71
- * matches `git apply --stat` shape close enough for an inline note. */
72
- function parseDiffStats(patch) {
73
- let files = 0;
74
- let added = 0;
75
- let removed = 0;
76
- for (const line of patch.split('\n')) {
77
- if (line.startsWith('diff --git'))
78
- files += 1;
79
- else if (line.startsWith('+++') || line.startsWith('---'))
80
- continue;
81
- else if (line.startsWith('+'))
82
- added += 1;
83
- else if (line.startsWith('-'))
84
- removed += 1;
85
- }
86
- if (files === 0 && (added > 0 || removed > 0)) {
87
- // Patch had no `diff --git` headers (a hand-rolled fragment);
88
- // count filename headers instead.
89
- files = (patch.match(/^\+\+\+ /gm) ?? []).length;
90
- }
91
- return { files, added, removed };
92
- }
@@ -1,296 +0,0 @@
1
- import { execFileSync } from 'child_process';
2
- import open from 'open';
3
- import prompts from 'prompts';
4
- import { green, dim, bold, yellow, cyan } from 'kleur/colors';
5
- import { saveCredentials } from '../lib/credentials.js';
6
- import { detectAndPatchConfigs, detectClaudeSubscription, patchClaudeTelemetry, } from '../lib/configPatch.js';
7
- const POLL_INTERVAL_MS = 2_000;
8
- const POLL_TIMEOUT_MS = 15 * 60 * 1000;
9
- export async function runInit(opts) {
10
- const { endpoint } = opts;
11
- // eslint-disable-next-line no-console
12
- console.log(bold('AnyForge Control — device authorisation'));
13
- // eslint-disable-next-line no-console
14
- console.log(dim(` endpoint: ${endpoint}`));
15
- // 1. Issue a one-time code. We detect the git remote up-front when
16
- // running in a repo so it can be attached to the API key later.
17
- // Detection happens before scope-pick so the server has it
18
- // regardless of which scope the operator chooses.
19
- const cwd0 = process.cwd();
20
- const gitRoot0 = findGitRoot(cwd0);
21
- const earlyRepo = gitRoot0 ? detectGitRemoteRepo(gitRoot0) : null;
22
- const initRes = await fetch(`${endpoint}/api/cli/init`, {
23
- method: 'POST',
24
- headers: { 'content-type': 'application/json' },
25
- body: JSON.stringify({ repo: earlyRepo }),
26
- });
27
- const init = (await initRes.json());
28
- if (!init.ok || !init.data) {
29
- throw new Error(`Could not start the authorisation flow: ${init.error ?? 'unknown error'}`);
30
- }
31
- const { code, authUrl, pollUrl } = init.data;
32
- // 2. Open the browser. Some shells (CI, headless) can't open one;
33
- // the URL also prints so the user can copy it manually.
34
- // eslint-disable-next-line no-console
35
- console.log();
36
- // eslint-disable-next-line no-console
37
- console.log(`Open this URL in your browser to authorise:`);
38
- // eslint-disable-next-line no-console
39
- console.log(` ${cyan(authUrl)}`);
40
- // eslint-disable-next-line no-console
41
- console.log(dim(` Code: ${code}`));
42
- // eslint-disable-next-line no-console
43
- console.log();
44
- try {
45
- await open(authUrl);
46
- }
47
- catch {
48
- // Headless / CI — the printed URL is fine.
49
- }
50
- // 3. Poll. Keep the message live so the user sees we're alive.
51
- const credentials = await pollForToken({ pollUrl, endpoint });
52
- // 4. Save credentials with restrictive perms.
53
- await saveCredentials(credentials);
54
- // eslint-disable-next-line no-console
55
- console.log(green('✔') + ' API key saved to ' + dim('~/.anyforge/credentials'));
56
- // 4b. A Claude Pro/Max subscription login is mutually exclusive with
57
- // the BYOK inference proxy: the ANTHROPIC_AUTH_TOKEN we'd patch
58
- // out-ranks (and thereby disables) the subscription login, and the
59
- // proxy has no upstream Anthropic key for a subscription user. So
60
- // we skip patching and steer them to the MCP server instead — which
61
- // keeps their subscription for inference and adds AnyForge control
62
- // on top. `--force` overrides for users who really do want a
63
- // BYOK-keyed metered proxy.
64
- const sub = await detectClaudeSubscription();
65
- if (sub.isSubscription && !opts.force) {
66
- await guideSubscriptionUser(sub, endpoint, credentials.apiKey);
67
- return;
68
- }
69
- // 5. Pick a scope. The repo option only shows when we're sitting
70
- // in a git working tree root — that matches the documented
71
- // contract and avoids accidentally writing settings into a
72
- // subdirectory the operator didn't mean to target.
73
- const cwd = process.cwd();
74
- const repoRoot = findGitRoot(cwd);
75
- const scope = await pickScope(repoRoot);
76
- // 6. Patch the chosen target. apiKey is inlined into the env map
77
- // so Claude Code can read it without a shell restart.
78
- const patches = await detectAndPatchConfigs({
79
- endpoint,
80
- apiKey: credentials.apiKey,
81
- scope,
82
- repoRoot,
83
- });
84
- if (patches.length === 0) {
85
- // eslint-disable-next-line no-console
86
- console.log(yellow('No agent configs were patched.'));
87
- // eslint-disable-next-line no-console
88
- console.log(dim(' Set ANTHROPIC_BASE_URL=' + endpoint + '/v1/anthropic and'));
89
- // eslint-disable-next-line no-console
90
- console.log(dim(' ANTHROPIC_AUTH_TOKEN=<your AnyForge key> manually if needed.'));
91
- }
92
- else {
93
- summarisePatches(patches);
94
- }
95
- // eslint-disable-next-line no-console
96
- console.log();
97
- if (sub.isSubscription && opts.force) {
98
- // They overrode the subscription guard. Be explicit about the
99
- // trade-off and the one-command way back.
100
- // eslint-disable-next-line no-console
101
- console.log(yellow(` ⚠ --force: your Claude ${sub.plan ?? ''} subscription login is now shadowed`));
102
- // eslint-disable-next-line no-console
103
- console.log(yellow(' by the AnyForge token — Claude Code will use the BYOK proxy, which'));
104
- // eslint-disable-next-line no-console
105
- console.log(yellow(' needs an Anthropic API key on your company. Run ') + bold('anyforge logout'));
106
- // eslint-disable-next-line no-console
107
- console.log(yellow(' to revert and restore your subscription.'));
108
- }
109
- if (scope === 'repo') {
110
- // eslint-disable-next-line no-console
111
- console.log(dim(' Re-run this command in another repo to patch it too,'));
112
- // eslint-disable-next-line no-console
113
- console.log(dim(' or run from any directory and choose "global" to install system-wide.'));
114
- }
115
- // eslint-disable-next-line no-console
116
- console.log(bold('Done.') + ' Visit ' + cyan(`${endpoint}/usage`) + ' to see your traffic.');
117
- }
118
- async function pollForToken(args) {
119
- const start = Date.now();
120
- // eslint-disable-next-line no-console
121
- process.stdout.write('Waiting for browser authorisation… ');
122
- let dotCount = 0;
123
- while (Date.now() - start < POLL_TIMEOUT_MS) {
124
- await new Promise(r => setTimeout(r, POLL_INTERVAL_MS));
125
- process.stdout.write('.');
126
- dotCount += 1;
127
- if (dotCount > 40) {
128
- process.stdout.write('\n');
129
- dotCount = 0;
130
- }
131
- const res = await fetch(args.pollUrl);
132
- const body = (await res.json());
133
- if (body.status === 'authorized' && body.data) {
134
- process.stdout.write(' ' + green('done') + '\n');
135
- return {
136
- apiKey: body.data.apiKey,
137
- companyId: body.data.companyId,
138
- endpoint: args.endpoint,
139
- savedAt: new Date().toISOString(),
140
- };
141
- }
142
- if (body.status === 'expired' || body.status === 'not_found') {
143
- process.stdout.write('\n');
144
- throw new Error(`Authorisation ${body.status}. Re-run \`anyforge init\`.`);
145
- }
146
- }
147
- throw new Error('Authorisation timed out (15 min). Re-run `anyforge init`.');
148
- }
149
- /**
150
- * Steer a subscription user to the MCP server instead of patching the
151
- * BYOK proxy. The MCP server keeps their Claude Pro/Max login for
152
- * inference and adds AnyForge control (HIL approvals, work items, crews)
153
- * on top, with no auth conflict. Offers to run `claude mcp add` for them,
154
- * and to enable OpenTelemetry usage metrics so their spend is still
155
- * visible on the AnyForge dashboard even though the proxy isn't in path.
156
- */
157
- async function guideSubscriptionUser(sub, endpoint, apiKey) {
158
- const who = sub.email ? ` (${sub.email})` : '';
159
- /* eslint-disable no-console */
160
- console.log();
161
- console.log(yellow(` ⚠ Claude ${sub.plan ?? ''} subscription login detected${who}.`));
162
- console.log();
163
- console.log(' AnyForge Control’s inference proxy is ' + bold('BYOK-only') + ' — it routes');
164
- console.log(' Claude Code through your own Anthropic API key for metering and');
165
- console.log(' governance. Pointing Claude Code at the proxy would ' + bold('override and'));
166
- console.log(' ' + bold('disable your subscription login') + ', so we did NOT patch your config.');
167
- console.log();
168
- console.log(' Instead, use your subscription for inference and add the ' + bold('AnyForge'));
169
- console.log(' ' + bold('MCP server') + ' for control (HIL approvals, work items, crews):');
170
- console.log();
171
- const mcpCmd = `claude mcp add anyforge --env ANYFORGE_API_KEY=${apiKey} -- npx -y @anyforge/mcp-server`;
172
- const add = await prompts({
173
- type: 'confirm',
174
- name: 'go',
175
- message: 'Add the AnyForge MCP server to Claude Code now?',
176
- initial: true,
177
- });
178
- if (add.go) {
179
- try {
180
- execFileSync('claude', ['mcp', 'add', 'anyforge', '--env', `ANYFORGE_API_KEY=${apiKey}`, '--', 'npx', '-y', '@anyforge/mcp-server'], { stdio: 'inherit' });
181
- console.log(green(' ✔') + ' AnyForge MCP server added to Claude Code.');
182
- }
183
- catch {
184
- console.log(yellow(' Could not run `claude mcp add` automatically (is the `claude` CLI on PATH?).'));
185
- console.log(dim(' Add it manually:'));
186
- console.log(' ' + mcpCmd);
187
- }
188
- }
189
- else {
190
- console.log(dim(' Add it later with:'));
191
- console.log(' ' + mcpCmd);
192
- }
193
- // Spend visibility without the proxy: Claude Code exports OpenTelemetry
194
- // usage metrics (token counts + estimated cost — no prompt content) that
195
- // AnyForge can ingest. Safe alongside the subscription login (no auth
196
- // env touched).
197
- console.log();
198
- const tel = await prompts({
199
- type: 'confirm',
200
- name: 'go',
201
- message: 'Send Claude Code usage to your AnyForge dashboard? (token counts only — no prompt content)',
202
- initial: true,
203
- });
204
- if (tel.go) {
205
- const sum = await patchClaudeTelemetry({ endpoint, apiKey });
206
- if (sum) {
207
- console.log(green(' ✔') + ' Usage telemetry enabled — view it at ' + cyan(`${endpoint}/usage`));
208
- console.log(dim(' (written to ' + sum.path + '; `anyforge logout` removes it)'));
209
- }
210
- }
211
- else {
212
- console.log(dim(' Skipped — your subscription usage stays local to Claude Code.'));
213
- }
214
- console.log();
215
- console.log(dim(' Want the metered BYOK proxy instead? Re-run with ') + bold('--force') + dim(' using an'));
216
- console.log(dim(' Anthropic API key on your company (this disables your subscription in'));
217
- console.log(dim(' Claude Code; ') + bold('anyforge logout') + dim(' reverts it).'));
218
- console.log();
219
- console.log(bold('Done.') + ' Visit ' + cyan(`${endpoint}/usage`) + ' to see your traffic.');
220
- /* eslint-enable no-console */
221
- }
222
- function findGitRoot(cwd) {
223
- // Works from any subdirectory — git walks up the tree to find the root.
224
- try {
225
- return execFileSync('git', ['rev-parse', '--show-toplevel'], {
226
- cwd,
227
- encoding: 'utf8',
228
- stdio: ['pipe', 'pipe', 'pipe'],
229
- }).trim();
230
- }
231
- catch {
232
- return null;
233
- }
234
- }
235
- async function pickScope(repoRoot) {
236
- if (!repoRoot) {
237
- // eslint-disable-next-line no-console
238
- console.log(dim(' Not a git repo root — installing globally.'));
239
- // eslint-disable-next-line no-console
240
- console.log(dim(' Run this from a repo root if you want a per-repo install instead.'));
241
- return 'global';
242
- }
243
- const choice = await prompts({
244
- type: 'select',
245
- name: 'scope',
246
- message: 'Where should AnyForge patch Claude Code?',
247
- choices: [
248
- { title: `This repo only (${repoRoot}/.claude/settings.local.json + .env)`, value: 'repo' },
249
- { title: 'Globally (~/.claude/settings.json)', value: 'global' },
250
- ],
251
- initial: 0,
252
- });
253
- // prompts returns {} on cancel — default to repo since they were given the choice.
254
- return choice.scope ?? 'repo';
255
- }
256
- function summarisePatches(patches) {
257
- // eslint-disable-next-line no-console
258
- console.log(green('✔') + ' Patched ' + patches.length + ' agent config' + (patches.length === 1 ? '' : 's') + ':');
259
- for (const p of patches) {
260
- // eslint-disable-next-line no-console
261
- console.log(` ${dim('•')} ${p.label} ${dim('(' + p.path + ')')}`);
262
- }
263
- }
264
- /**
265
- * Read the `origin` remote URL and normalise it to `owner/repo`.
266
- * Handles both SSH (`git@github.com:owner/repo.git`) and HTTPS forms.
267
- * Returns null when there is no remote, git is unavailable, or the
268
- * URL can't be parsed into an owner/repo pair.
269
- *
270
- * Uses execFileSync (not exec/execSync) so no shell interpolation
271
- * occurs — the args array is passed directly to the git binary.
272
- */
273
- function detectGitRemoteRepo(repoRoot) {
274
- try {
275
- const raw = execFileSync('git', ['remote', 'get-url', 'origin'], {
276
- cwd: repoRoot,
277
- encoding: 'utf8',
278
- stdio: ['pipe', 'pipe', 'pipe'],
279
- }).trim();
280
- // SSH: git@github.com:owner/repo.git
281
- const sshMatch = raw.match(/^git@[\w.-]+:([\w.-]+\/[\w.-]+?)(?:\.git)?$/);
282
- if (sshMatch)
283
- return sshMatch[1];
284
- // HTTPS: https://github.com/owner/repo[.git]
285
- const httpsMatch = raw.match(/^https?:\/\/[\w.-]+\/([\w.-]+\/[\w.-]+?)(?:\.git)?$/);
286
- if (httpsMatch)
287
- return httpsMatch[1];
288
- return null;
289
- }
290
- catch {
291
- return null;
292
- }
293
- }
294
- // Re-exported so future commands (e.g. status) can reuse the prompt
295
- // shape without duplicating the question text.
296
- export { prompts };
@@ -1,34 +0,0 @@
1
- import { green, dim, yellow } from 'kleur/colors';
2
- import { clearCredentials, loadCredentials } from '../lib/credentials.js';
3
- import { revertClaudeCodeConfigs } from '../lib/configPatch.js';
4
- /**
5
- * `npx @anyforge/control logout` — wipe the local credential file AND
6
- * revert the config patches `init` wrote (so a previously-shadowed
7
- * Claude subscription login is restored). Does NOT revoke the key on
8
- * the server side — that's a separate action on /settings → API keys.
9
- */
10
- export async function runLogout() {
11
- // Load before clearing so we know which proxy host we patched in
12
- // (matters when the user ran `init --endpoint <staging>`).
13
- const creds = await loadCredentials();
14
- const removed = await clearCredentials();
15
- const { reverted } = await revertClaudeCodeConfigs({ endpoint: creds?.endpoint ?? null });
16
- /* eslint-disable no-console */
17
- if (removed) {
18
- console.log(green('✔') + ' Local credentials cleared.');
19
- }
20
- else {
21
- console.log(yellow('No credentials were saved on this machine.'));
22
- }
23
- if (reverted.length) {
24
- console.log(green('✔') + ' Reverted patched configs:');
25
- for (const p of reverted)
26
- console.log(` ${dim('•')} ${p}`);
27
- console.log(dim(' Your previous Claude Code login (if any) is restored.'));
28
- }
29
- else {
30
- console.log(dim(' No AnyForge config patches found to revert.'));
31
- }
32
- console.log(dim(' Revoke the key server-side at /settings → API keys.'));
33
- /* eslint-enable no-console */
34
- }
@@ -1,58 +0,0 @@
1
- import { green, red, yellow, dim } from 'kleur/colors';
2
- import { loadCredentials } from '../lib/credentials.js';
3
- export async function runStatus(opts) {
4
- const c = await loadCredentials();
5
- if (!c) {
6
- // eslint-disable-next-line no-console
7
- console.log(yellow('Not logged in.') + ' Run ' + dim('npx @anyforge/control init') + ' first.');
8
- return;
9
- }
10
- const endpoint = opts.endpoint || c.endpoint;
11
- // eslint-disable-next-line no-console
12
- console.log(`Endpoint: ${endpoint}`);
13
- // eslint-disable-next-line no-console
14
- console.log(`Company: ${c.companyId}`);
15
- // We don't have a dedicated /v1/health endpoint yet — the cheapest
16
- // probe is a 1-token Anthropic Messages call. Costs ~$0.0001, fine.
17
- const probeBody = {
18
- model: 'claude-3-5-haiku-20241022',
19
- max_tokens: 1,
20
- messages: [{ role: 'user', content: 'Hi' }],
21
- };
22
- let res;
23
- try {
24
- res = await fetch(`${endpoint}/v1/anthropic/messages`, {
25
- method: 'POST',
26
- headers: {
27
- 'Content-Type': 'application/json',
28
- 'anthropic-version': '2023-06-01',
29
- Authorization: `Bearer ${c.apiKey}`,
30
- },
31
- body: JSON.stringify(probeBody),
32
- });
33
- }
34
- catch (err) {
35
- // eslint-disable-next-line no-console
36
- console.log(red('✗ Proxy unreachable: ') + err.message);
37
- process.exit(1);
38
- }
39
- if (res.status === 401) {
40
- // eslint-disable-next-line no-console
41
- console.log(red('✗ API key rejected.') + ' Re-run ' + dim('npx @anyforge/control init') + '.');
42
- process.exit(1);
43
- }
44
- if (res.status === 502) {
45
- // eslint-disable-next-line no-console
46
- console.log(red('✗ No BYOK Anthropic key configured for your company.'));
47
- // eslint-disable-next-line no-console
48
- console.log(dim(' Add one at ') + endpoint + dim('/settings → API keys.'));
49
- process.exit(1);
50
- }
51
- if (!res.ok) {
52
- // eslint-disable-next-line no-console
53
- console.log(red(`✗ Proxy returned ${res.status}: `) + (await res.text()).slice(0, 200));
54
- process.exit(1);
55
- }
56
- // eslint-disable-next-line no-console
57
- console.log(green('✔ Proxy reachable, key valid, BYOK configured.'));
58
- }
@@ -1,23 +0,0 @@
1
- import { dim, yellow } from 'kleur/colors';
2
- import { loadCredentials } from '../lib/credentials.js';
3
- /**
4
- * `npx @anyforge/control whoami` — show the company id tied to the
5
- * currently saved credential. Useful for verifying the right device
6
- * is configured before running long agent sessions.
7
- */
8
- export async function runWhoami() {
9
- const c = await loadCredentials();
10
- if (!c) {
11
- // eslint-disable-next-line no-console
12
- console.log(yellow('Not logged in.') + ' Run ' + dim('npx @anyforge/control init') + ' first.');
13
- return;
14
- }
15
- // eslint-disable-next-line no-console
16
- console.log(`Company: ${c.companyId}`);
17
- // eslint-disable-next-line no-console
18
- console.log(`Endpoint: ${c.endpoint}`);
19
- // eslint-disable-next-line no-console
20
- console.log(`Saved: ${new Date(c.savedAt).toLocaleString()}`);
21
- // eslint-disable-next-line no-console
22
- console.log(`Key tail: ${dim('…' + c.apiKey.slice(-4))}`);
23
- }