@ashulab/agent-forge 0.2.0 → 0.3.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/README.md CHANGED
@@ -87,7 +87,7 @@ By default the installation token carries the full installation scope. Narrow it
87
87
 
88
88
  Omitting the `permissions` key entirely is the safe default — the token then carries whatever the App grants. `repositories` works the same way: list it and the token can only touch those repos.
89
89
 
90
- On launch, `agent-forge` prints the scope GitHub actually granted (`token` row in the summary); `agent-forge token --agent <name>` prints it to stderr. If a call fails with `403 Resource not accessible by integration`, check that row first.
90
+ On launch, `agent-forge` prints the scope GitHub actually granted (`scope` row in the summary, alongside the `account` the token is for); `agent-forge token --agent <name>` prints it to stderr. If a call fails with `403 Resource not accessible by integration`, check that row first.
91
91
 
92
92
  ## Add an agent
93
93
 
@@ -124,6 +124,7 @@ Other commands:
124
124
 
125
125
  ```bash
126
126
  agent-forge --list
127
+ agent-forge --version
127
128
  agent-forge --help
128
129
  ```
129
130
 
package/dist/launcher.cjs CHANGED
@@ -4017,7 +4017,7 @@ var require_jsonwebtoken = __commonJS({
4017
4017
  // src/launcher.js
4018
4018
  var import_node_child_process3 = require("node:child_process");
4019
4019
  var import_node_path4 = require("node:path");
4020
- var import_node_util3 = require("node:util");
4020
+ var import_node_util4 = require("node:util");
4021
4021
 
4022
4022
  // node_modules/.pnpm/@clack+core@1.4.3/node_modules/@clack/core/dist/index.mjs
4023
4023
  var import_node_util = require("node:util");
@@ -5409,19 +5409,21 @@ function currentRepoSlug() {
5409
5409
  async function installationForRepo(slug, jwtToken) {
5410
5410
  try {
5411
5411
  const data = await githubApi(`/repos/${slug.owner}/${slug.repo}/installation`, { jwtToken });
5412
- return String(data.id);
5412
+ return { id: String(data.id), account: data.account?.login || data.account?.slug || null };
5413
5413
  } catch {
5414
5414
  return null;
5415
5415
  }
5416
5416
  }
5417
- async function resolveInstallationId(agent, jwtToken) {
5418
- if (agent.installationId) return String(agent.installationId);
5417
+ async function resolveInstallation(agent, jwtToken) {
5418
+ if (agent.installationId) return { id: String(agent.installationId), account: null };
5419
5419
  const slug = currentRepoSlug();
5420
5420
  if (slug) {
5421
- const id = await installationForRepo(slug, jwtToken);
5422
- if (id) return id;
5421
+ const hit = await installationForRepo(slug, jwtToken);
5422
+ if (hit) return hit;
5423
5423
  }
5424
- return pickInstallation(await listInstallations(jwtToken), agent);
5424
+ const installations = await listInstallations(jwtToken);
5425
+ const id = pickInstallation(installations, agent);
5426
+ return { id, account: installations.find((item) => item.id === id)?.account ?? null };
5425
5427
  }
5426
5428
  function buildTokenScope(agent) {
5427
5429
  const scope = {};
@@ -5432,13 +5434,15 @@ function buildTokenScope(agent) {
5432
5434
  async function generateGithubAppToken(agent) {
5433
5435
  const privateKey = readPrivateKey(agent.privateKeyPath, agent.name || agent.appId);
5434
5436
  const jwtToken = signAppJwt(agent.appId, privateKey);
5435
- const installationId = await resolveInstallationId(agent, jwtToken);
5437
+ const { id: installationId, account } = await resolveInstallation(agent, jwtToken);
5436
5438
  const data = await githubApi(`/app/installations/${installationId}/access_tokens`, {
5437
5439
  jwtToken,
5438
5440
  body: buildTokenScope(agent)
5439
5441
  });
5440
5442
  return {
5441
5443
  token: data.token,
5444
+ account,
5445
+ expiresAt: data.expires_at || null,
5442
5446
  // What GitHub actually granted, after both the App's config and our scoping.
5443
5447
  permissions: data.permissions || {},
5444
5448
  repositorySelection: data.repository_selection || "all"
@@ -5515,10 +5519,97 @@ function syncIdentityFile(providerName, agent, cwd = process.cwd()) {
5515
5519
  return file;
5516
5520
  }
5517
5521
 
5522
+ // src/format.js
5523
+ function formatScope(permissions, full = false) {
5524
+ const entries = Object.entries(permissions);
5525
+ if (entries.length === 0) return "full installation scope";
5526
+ if (full) return entries.map(([name, level]) => `${name}:${level}`).join(" ");
5527
+ const writes = entries.filter(([, level]) => level !== "read").map(([name]) => name);
5528
+ if (writes.length === 0) return `read-only (${entries.length})`;
5529
+ const reads = entries.length - writes.length;
5530
+ return `write: ${writes.join(", ")}${reads ? ` \xB7 +${reads} read` : ""}`;
5531
+ }
5532
+ function formatExpiry(iso) {
5533
+ const ms = new Date(iso).getTime() - Date.now();
5534
+ if (!Number.isFinite(ms)) return String(iso);
5535
+ const minutes = Math.round(ms / 6e4);
5536
+ return minutes > 0 ? `~${minutes}m` : "expired";
5537
+ }
5538
+
5518
5539
  // src/cli.js
5540
+ var import_node_util3 = require("node:util");
5541
+
5542
+ // package.json
5543
+ var package_default = {
5544
+ name: "@ashulab/agent-forge",
5545
+ version: "0.3.0",
5546
+ description: "Give claude/codex/antigravity their own GitHub identity: a bot account, a scoped token minted per run, git attribution to match",
5547
+ license: "MIT",
5548
+ author: "Diego Ghersi <ghersidl@gmail.com>",
5549
+ type: "module",
5550
+ bin: {
5551
+ "agent-forge": "dist/launcher.cjs"
5552
+ },
5553
+ main: "dist/launcher.cjs",
5554
+ files: [
5555
+ "dist",
5556
+ "schema",
5557
+ "LICENSE",
5558
+ "README.md"
5559
+ ],
5560
+ engines: {
5561
+ node: ">=24"
5562
+ },
5563
+ packageManager: "pnpm@11.20.0",
5564
+ repository: {
5565
+ type: "git",
5566
+ url: "git+https://github.com/AshuLab/agent-forge.git"
5567
+ },
5568
+ bugs: {
5569
+ url: "https://github.com/AshuLab/agent-forge/issues"
5570
+ },
5571
+ homepage: "https://github.com/AshuLab/agent-forge#readme",
5572
+ keywords: [
5573
+ "github-app",
5574
+ "cli",
5575
+ "coding-agent",
5576
+ "claude",
5577
+ "codex",
5578
+ "antigravity",
5579
+ "installation-token",
5580
+ "git-identity"
5581
+ ],
5582
+ publishConfig: {
5583
+ access: "public"
5584
+ },
5585
+ scripts: {
5586
+ dev: "node src/launcher.js",
5587
+ test: "node --test",
5588
+ start: "node dist/launcher.cjs",
5589
+ build: "esbuild src/launcher.js --bundle --platform=node --format=cjs --target=node24 --outfile=dist/launcher.cjs --banner:js='#!/usr/bin/env node' && chmod +x dist/launcher.cjs",
5590
+ prepublishOnly: "pnpm build && pnpm test"
5591
+ },
5592
+ dependencies: {
5593
+ "@clack/prompts": "^1.7.0",
5594
+ jsonwebtoken: "^9.0.3"
5595
+ },
5596
+ devDependencies: {
5597
+ esbuild: "^0.28.2"
5598
+ }
5599
+ };
5600
+
5601
+ // src/cli.js
5602
+ var LOGO = `
5603
+ \u2584\u2584 \u2584\u2584\u2584 \u2584\u2584\u2584 \u2584\u2584 \u2584\u2584\u2584 \u2584\u2584\u2584 \u2584\u2584 \u2584\u2584 \u2584\u2584\u2584 \u2584\u2584\u2584
5604
+ \u2588\u2584\u2588 \u2588 \u2584 \u2588\u2584 \u2588 \u2588 \u2588 \u2588\u2584 \u2588 \u2588 \u2588\u2584\u2580 \u2588 \u2584 \u2588\u2584
5605
+ \u2588 \u2588 \u2580\u2584\u2588 \u2588\u2584\u2584 \u2588 \u2588 \u2588 \u2588 \u2580\u2584\u2580 \u2588 \u2588 \u2580\u2584\u2588 \u2588\u2584\u2584
5606
+ `;
5607
+ function printVersion() {
5608
+ console.log(package_default.version);
5609
+ }
5519
5610
  function printHelp() {
5520
5611
  console.log(`
5521
- Agent Forge
5612
+ Agent Forge v${package_default.version}
5522
5613
 
5523
5614
  Usage:
5524
5615
  agent-forge
@@ -5526,6 +5617,7 @@ Usage:
5526
5617
  agent-forge add guided setup for a new agent
5527
5618
  agent-forge token --agent <name> print a fresh GitHub App token (for refresh)
5528
5619
  agent-forge --list
5620
+ agent-forge --version
5529
5621
  agent-forge --help
5530
5622
 
5531
5623
  Requirements:
@@ -5539,10 +5631,13 @@ Config resolution (first match wins):
5539
5631
  - ./agents.json (only if it already exists)
5540
5632
  `);
5541
5633
  }
5634
+ function bail(message = "Cancelled") {
5635
+ cancel(`\u2716 ${message}`);
5636
+ process.exit(0);
5637
+ }
5542
5638
  function keep(value) {
5543
5639
  if (isCancel(value)) {
5544
- cancel("Cancelled.");
5545
- process.exit(0);
5640
+ bail();
5546
5641
  }
5547
5642
  return value;
5548
5643
  }
@@ -5614,7 +5709,7 @@ async function interactiveSelection(defaults = {}) {
5614
5709
  if (agents.length === 0 && !defaults.agent) {
5615
5710
  note("No agents configured yet. Each agent is a GitHub App identity the launcher runs under.", "First run");
5616
5711
  if (!keep(await confirm({ message: "Add one now?" }))) {
5617
- cancel(`Run "agent-forge add" when you're ready.`);
5712
+ outro(`Run ${(0, import_node_util3.styleText)("cyan", "agent-forge add")} when you're ready.`);
5618
5713
  process.exit(0);
5619
5714
  }
5620
5715
  await addAgentWizard({ embedded: true });
@@ -5644,9 +5739,9 @@ async function interactiveSelection(defaults = {}) {
5644
5739
  })
5645
5740
  );
5646
5741
  }
5647
- if (!keep(await confirm({ message: `Launch ${agentChoice} with ${providerChoice}?` }))) {
5648
- cancel("Cancelled.");
5649
- process.exit(0);
5742
+ const launchPrompt = `Launch ${(0, import_node_util3.styleText)("green", agentChoice)} with ${(0, import_node_util3.styleText)(["cyan", "bold"], providerChoice)}?`;
5743
+ if (!keep(await confirm({ message: launchPrompt }))) {
5744
+ bail("Cancelled \u2014 nothing launched");
5650
5745
  }
5651
5746
  return { agentName: agentChoice, providerName: providerChoice };
5652
5747
  }
@@ -5655,6 +5750,7 @@ function parseArgs() {
5655
5750
  const result = {
5656
5751
  command: args[0] && !args[0].startsWith("-") ? args[0] : void 0,
5657
5752
  help: false,
5753
+ version: false,
5658
5754
  list: false,
5659
5755
  agent: void 0,
5660
5756
  provider: void 0
@@ -5664,6 +5760,7 @@ function parseArgs() {
5664
5760
  const arg = args[i2];
5665
5761
  const next = args[i2 + 1];
5666
5762
  if (arg === "--help" || arg === "-h") result.help = true;
5763
+ if (arg === "--version" || arg === "-v") result.version = true;
5667
5764
  if (arg === "--list" || arg === "-l") result.list = true;
5668
5765
  if (arg === "--agent" || arg === "-a") {
5669
5766
  result.agent = value(next);
@@ -5677,7 +5774,8 @@ function parseArgs() {
5677
5774
  return result;
5678
5775
  }
5679
5776
  function showIntro() {
5680
- intro("Agent Forge");
5777
+ console.log((0, import_node_util3.styleText)("cyan", LOGO));
5778
+ intro(`Agent Forge ${(0, import_node_util3.styleText)("dim", `v${package_default.version}`)}`);
5681
5779
  }
5682
5780
 
5683
5781
  // src/launcher.js
@@ -5703,10 +5801,10 @@ async function launchAgent(agentName, providerName) {
5703
5801
  const providerInfo = getProviderInfo(providerName, agent);
5704
5802
  const s = spinner();
5705
5803
  s.start("Minting installation token");
5706
- const { token: githubToken, permissions, repositorySelection } = await generateGithubAppToken(agent);
5804
+ const { token: githubToken, account, expiresAt, permissions, repositorySelection } = await generateGithubAppToken(agent);
5707
5805
  const botId = await resolveBotId(agent);
5708
5806
  const gitIdentity = buildGitIdentity(agent, botId);
5709
- s.stop(`Token ready for ${(0, import_node_util3.styleText)("green", gitIdentity.GIT_AUTHOR_NAME)}`);
5807
+ s.stop(`Token ready for ${(0, import_node_util4.styleText)("green", gitIdentity.GIT_AUTHOR_NAME)}`);
5710
5808
  const identityFile = syncIdentityFile(providerName, agent);
5711
5809
  const runtimeEnv = {
5712
5810
  ...process.env,
@@ -5714,18 +5812,24 @@ async function launchAgent(agentName, providerName) {
5714
5812
  GH_TOKEN: githubToken,
5715
5813
  ...gitIdentity
5716
5814
  };
5717
- const scopeEntries = Object.entries(permissions);
5718
- const scopeText = scopeEntries.length ? scopeEntries.map(([name, level]) => `${name}:${level}`).join(" ") : "full installation scope";
5719
- const row = (label, value) => `${(0, import_node_util3.styleText)("dim", label.padEnd(10))}${value}`;
5815
+ const narrowed = Boolean(agent.permissions) || Boolean(agent.repositories);
5816
+ const scopeText = formatScope(permissions, narrowed);
5817
+ const repoSlug = currentRepoSlug();
5818
+ const row = (label, value) => `${(0, import_node_util4.styleText)("dim", label.padEnd(10))}${value}`;
5720
5819
  const summary = [
5721
- row("provider", (0, import_node_util3.styleText)(["cyan", "bold"], providerName)),
5722
- row("identity", (0, import_node_util3.styleText)("green", gitIdentity.GIT_AUTHOR_NAME)),
5723
- row("token", (0, import_node_util3.styleText)("dim", repositorySelection === "selected" ? `${scopeText} \xB7 selected repos` : scopeText))
5820
+ row("provider", (0, import_node_util4.styleText)(["cyan", "bold"], providerName)),
5821
+ row("identity", (0, import_node_util4.styleText)("green", gitIdentity.GIT_AUTHOR_NAME))
5724
5822
  ];
5823
+ if (account) summary.push(row("account", (0, import_node_util4.styleText)("yellow", account)));
5824
+ if (repoSlug) summary.push(row("repo", (0, import_node_util4.styleText)("dim", `${repoSlug.owner}/${repoSlug.repo}`)));
5825
+ summary.push(
5826
+ row("scope", (0, import_node_util4.styleText)("dim", repositorySelection === "selected" ? `${scopeText} \xB7 selected repos` : scopeText))
5827
+ );
5828
+ if (expiresAt) summary.push(row("expires", (0, import_node_util4.styleText)("dim", formatExpiry(expiresAt))));
5725
5829
  if (identityFile) {
5726
- summary.push(row("memory", (0, import_node_util3.styleText)("dim", (0, import_node_path4.relative)(process.cwd(), identityFile) || identityFile)));
5830
+ summary.push(row("memory", (0, import_node_util4.styleText)("dim", (0, import_node_path4.relative)(process.cwd(), identityFile) || identityFile)));
5727
5831
  }
5728
- note(summary.join("\n"), (0, import_node_util3.styleText)("bold", agent.label || agent.name));
5832
+ note(summary.join("\n"), (0, import_node_util4.styleText)("bold", agent.label || agent.name));
5729
5833
  const providerArgs = getProviderPromptArgs(providerName, agent);
5730
5834
  const child = (0, import_node_child_process3.spawn)(providerInfo.command, providerArgs, {
5731
5835
  stdio: "inherit",
@@ -5739,7 +5843,7 @@ async function launchAgent(agentName, providerName) {
5739
5843
  cancel(`Could not execute ${providerInfo.command}: ${error.message}`);
5740
5844
  process.exit(1);
5741
5845
  });
5742
- outro(`Running ${(0, import_node_util3.styleText)("cyan", providerName)}\u2026`);
5846
+ outro(`Running ${(0, import_node_util4.styleText)("cyan", providerName)}\u2026`);
5743
5847
  }
5744
5848
  async function main() {
5745
5849
  const flags = parseArgs();
@@ -5747,6 +5851,10 @@ async function main() {
5747
5851
  printHelp();
5748
5852
  return;
5749
5853
  }
5854
+ if (flags.version) {
5855
+ printVersion();
5856
+ return;
5857
+ }
5750
5858
  if (flags.list) {
5751
5859
  listAgents();
5752
5860
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ashulab/agent-forge",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Give claude/codex/antigravity their own GitHub identity: a bot account, a scoped token minted per run, git attribution to match",
5
5
  "license": "MIT",
6
6
  "author": "Diego Ghersi <ghersidl@gmail.com>",