@divergeai/cli 0.1.25 → 0.1.27

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 +13 -3
  2. package/dist/diverge.mjs +132 -48
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -157,8 +157,13 @@ The set is derived from the login, never chosen on the machine: who is
157
157
  signed in, where they may act, and what they can run there. A skill's
158
158
  identity is its organization and its name, so every organization's skills
159
159
  sit side by side in one folder, each under the organization's prefix — its
160
- short code, lowercased, or a slug of its name: `dvx-consultar-inventario`,
161
- `red-team-triage-cloud-run`. Each SKILL.md is rendered from the skill's
160
+ handle, the same lowercase slug that prefixes its case numbers, or a slug of
161
+ its name when it chose none: `acme-consultar-inventario`,
162
+ `red-team-triage-cloud-run`. A folder longer than the 64 characters a
163
+ harness accepts as a skill name is cut and ended with a short stable hash,
164
+ and its SKILL.md names the skill; a served folder that is not a skill name is
165
+ skipped with a warning naming it, and the folder that skill was installed
166
+ under before stays. Each SKILL.md is rendered from the skill's
162
167
  record on every install, named after its folder, and opens with the
163
168
  organization its tools run in, so an agent passes `--organization-id` on
164
169
  every call instead of depending on the machine's default organization.
@@ -249,12 +254,17 @@ reaches it through your bridge binding. See
249
254
 
250
255
  ```bash
251
256
  diverge plugins validate manifest.json # dry run: every problem the platform would refuse, at once
252
- diverge plugins publish manifest.json # forge_plugins:create in the login's organization
257
+ diverge plugins publish manifest.json # forge_plugins:create in the login's organization; exits 3 when it waits on a decision
253
258
  diverge plugins list
254
259
  diverge plugins retire <pluginId> # the descriptor and its tools go; installations stay
255
260
  ```
256
261
 
257
262
  Tools are pinned to the manifest: a change is a new version published again.
263
+ A version that only reshapes or removes the tools already served is stored at
264
+ once. Anything else changes authority and is stored only once a person decides
265
+ it: `publish` raises it as `forge_publish_plugin`, prints the invocation and
266
+ its Compass link, and exits 3; read the outcome with
267
+ `diverge tools invocation <id>`.
258
268
  After publishing, enable the plugin with `forge_set_plugin_state` (or the
259
269
  Forge Routing page) and bind the runtime with a bridge binding whose
260
270
  `runtimeType` equals the manifest's `runtime.type`; declare `networkPolicy`
package/dist/diverge.mjs CHANGED
@@ -7556,7 +7556,7 @@ function tokenSource(env, baseUrl) {
7556
7556
  }
7557
7557
 
7558
7558
  // src/version.ts
7559
- var VERSION = true ? "0.1.25" : "dev";
7559
+ var VERSION = true ? "0.1.27" : "dev";
7560
7560
  var USER_AGENT = `diverge/${VERSION}`;
7561
7561
 
7562
7562
  // src/http.ts
@@ -8361,8 +8361,16 @@ function describeToolExecution(env = process.env) {
8361
8361
  }
8362
8362
 
8363
8363
  // src/tools/client.ts
8364
+ var AUTHORITY_CHANGE_CODE = "REMOTE_PLUGIN_AUTHORITY_CHANGE";
8364
8365
  var INVOCATION_SET_TOOL = "functions_invocation_set";
8365
8366
  var PENDING_INVOCATION_STATUSES = ["pending", "approved", "executing"];
8367
+ function parseJson(text2) {
8368
+ try {
8369
+ return JSON.parse(text2);
8370
+ } catch {
8371
+ return void 0;
8372
+ }
8373
+ }
8366
8374
  function isPending(result) {
8367
8375
  return !!result && typeof result === "object" && result.status === "pending_approval";
8368
8376
  }
@@ -8451,7 +8459,7 @@ var ToolsClient = class {
8451
8459
  if (typeof parsed.error === "string" && parsed.error) message = parsed.error;
8452
8460
  } catch {
8453
8461
  }
8454
- if (response.status === 403 && /scope/i.test(message)) {
8462
+ if (response.status === 403 && /^insufficient scope$/i.test(message.trim())) {
8455
8463
  throw new CliError(`${response.status}: ${message}
8456
8464
  The signed-in token is missing a required scope. Re-run: ${LOGIN_HINT}`);
8457
8465
  }
@@ -8523,14 +8531,14 @@ var ToolsClient = class {
8523
8531
  this.descriptorsUrl(),
8524
8532
  { targetOrganizationId: this.settings.organizationId, manifest, dryRun: options.dryRun },
8525
8533
  (status2, text2) => {
8526
- if (status2 !== 400) return void 0;
8527
- try {
8528
- const parsed = JSON.parse(text2);
8529
- if (Array.isArray(parsed.details)) return { ok: false, details: parsed.details.map(String) };
8530
- return { ok: false, details: [parsed.error ?? text2] };
8531
- } catch {
8532
- return { ok: false, details: [text2] };
8534
+ if (status2 === 403) {
8535
+ const parsed2 = parseJson(text2);
8536
+ return parsed2?.code === AUTHORITY_CHANGE_CODE && Array.isArray(parsed2.details) ? { ok: false, details: parsed2.details.map(String), authorityChange: true } : void 0;
8533
8537
  }
8538
+ if (status2 !== 400) return void 0;
8539
+ const parsed = parseJson(text2);
8540
+ if (Array.isArray(parsed?.details)) return { ok: false, details: parsed.details.map(String) };
8541
+ return { ok: false, details: [parsed?.error ?? text2] };
8534
8542
  }
8535
8543
  ).then((outcome) => "ok" in outcome ? outcome : { ok: true, result: outcome });
8536
8544
  }
@@ -8802,6 +8810,19 @@ function readSkillsManifest(root) {
8802
8810
  function few(names, limit = 6) {
8803
8811
  return names.length <= limit ? names.join(", ") : `${names.slice(0, limit).join(", ")} and ${names.length - limit} more`;
8804
8812
  }
8813
+ function isSkillFolder(folder) {
8814
+ return /^[a-z0-9][a-z0-9-]{0,63}$/.test(folder);
8815
+ }
8816
+ function retainedFolders(manifest, set2) {
8817
+ const retained = /* @__PURE__ */ new Map();
8818
+ for (const organization of set2.organizations) {
8819
+ const skipped = organization.skills.filter((skill) => !isSkillFolder(skill.folder));
8820
+ const held = Object.keys(manifest?.organizations[organization.id]?.skills ?? {});
8821
+ const kept = held.filter((folder) => skipped.some((skill) => folder.endsWith(`-${skill.name}`)));
8822
+ if (kept.length) retained.set(organization.id, kept);
8823
+ }
8824
+ return retained;
8825
+ }
8805
8826
  function heldFolders(manifest) {
8806
8827
  const held = /* @__PURE__ */ new Map();
8807
8828
  for (const [folder, version] of Object.entries(manifest?.flat ?? {})) held.set(folder, version);
@@ -8811,9 +8832,10 @@ function heldFolders(manifest) {
8811
8832
  }
8812
8833
  function retirable(manifest, set2) {
8813
8834
  const refreshed = new Set(set2.organizations.map((organization) => organization.id));
8835
+ const retained = new Set([...retainedFolders(manifest, set2).values()].flat());
8814
8836
  const folders = [...Object.keys(manifest?.flat ?? {}), ...Object.keys(manifest?.cli ?? {})];
8815
8837
  for (const [id, section] of Object.entries(manifest?.organizations ?? {})) {
8816
- if (set2.complete || refreshed.has(id)) folders.push(...Object.keys(section.skills));
8838
+ if (set2.complete || refreshed.has(id)) folders.push(...Object.keys(section.skills).filter((folder) => !retained.has(folder)));
8817
8839
  }
8818
8840
  return folders;
8819
8841
  }
@@ -8823,21 +8845,28 @@ function installSkills(root, set2, source, now = () => /* @__PURE__ */ new Date(
8823
8845
  const written = [];
8824
8846
  const added = [];
8825
8847
  const updated = [];
8848
+ const skipped = [];
8826
8849
  const counts = [];
8827
8850
  const write = (skill) => {
8828
- if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(skill.folder)) throw new Error(`skill folder ${JSON.stringify(skill.folder)} is not a folder name`);
8851
+ if (!isSkillFolder(skill.folder)) {
8852
+ skipped.push(skill.folder);
8853
+ return;
8854
+ }
8829
8855
  writeTextAtomic(path5.join(root, skill.folder, "SKILL.md"), skill.content, 420);
8830
8856
  written.push(skill.folder);
8831
- const before = held.get(skill.folder);
8832
- if (before === void 0) added.push(skill.folder);
8833
- else if (before !== skill.updatedAt) updated.push(skill.folder);
8857
+ const before2 = held.get(skill.folder);
8858
+ if (before2 === void 0) added.push(skill.folder);
8859
+ else if (before2 !== skill.updatedAt) updated.push(skill.folder);
8834
8860
  };
8835
8861
  for (const organization of set2.organizations) {
8862
+ const before2 = written.length;
8836
8863
  organization.skills.forEach(write);
8837
- counts.push({ label: organization.name, count: organization.skills.length });
8864
+ counts.push({ label: organization.name, count: written.length - before2 });
8838
8865
  }
8866
+ const before = written.length;
8839
8867
  set2.cli.forEach(write);
8840
- if (set2.cli.length) counts.push({ label: "CLI", count: set2.cli.length });
8868
+ if (set2.cli.length) counts.push({ label: "CLI", count: written.length - before });
8869
+ const retained = retainedFolders(previous, set2);
8841
8870
  const removed = [];
8842
8871
  const kept = new Set(written);
8843
8872
  for (const folder of retirable(previous, set2)) {
@@ -8855,7 +8884,10 @@ function installSkills(root, set2, source, now = () => /* @__PURE__ */ new Date(
8855
8884
  organizations[organization.id] = {
8856
8885
  name: organization.name,
8857
8886
  installedAt: stamp,
8858
- skills: Object.fromEntries(organization.skills.map((skill) => [skill.folder, skill.updatedAt]))
8887
+ skills: Object.fromEntries([
8888
+ ...(retained.get(organization.id) ?? []).map((folder) => [folder, previous.organizations[organization.id].skills[folder]]),
8889
+ ...organization.skills.filter((skill) => isSkillFolder(skill.folder)).map((skill) => [skill.folder, skill.updatedAt])
8890
+ ])
8859
8891
  };
8860
8892
  }
8861
8893
  const manifest = {
@@ -8865,7 +8897,7 @@ function installSkills(root, set2, source, now = () => /* @__PURE__ */ new Date(
8865
8897
  organizations
8866
8898
  };
8867
8899
  writeJsonAtomic(path5.join(root, SKILLS_MANIFEST), manifest, 420);
8868
- return { root, written, added, updated, removed, counts };
8900
+ return { root, written, added, updated, removed, skipped, counts };
8869
8901
  }
8870
8902
  function skillsStatus(root, set2) {
8871
8903
  const manifest = readSkillsManifest(root);
@@ -8873,7 +8905,9 @@ function skillsStatus(root, set2) {
8873
8905
  const current = [];
8874
8906
  const behind = [];
8875
8907
  const missing = [];
8876
- const served = [...set2.organizations.flatMap((organization) => organization.skills), ...set2.cli];
8908
+ const all = [...set2.organizations.flatMap((organization) => organization.skills), ...set2.cli];
8909
+ const served = all.filter((skill) => isSkillFolder(skill.folder));
8910
+ const skipped = all.filter((skill) => !isSkillFolder(skill.folder)).map((skill) => skill.folder);
8877
8911
  for (const skill of served) {
8878
8912
  const before = held.get(skill.folder);
8879
8913
  if (before === void 0 || !fs6.existsSync(path5.join(root, skill.folder, "SKILL.md"))) missing.push(skill.folder);
@@ -8882,7 +8916,7 @@ function skillsStatus(root, set2) {
8882
8916
  }
8883
8917
  const servedFolders = new Set(served.map((skill) => skill.folder));
8884
8918
  const retired = [...new Set(retirable(manifest, set2))].filter((folder) => !servedFolders.has(folder));
8885
- return { root, installedAt: manifest?.installedAt || null, current, behind, missing, retired };
8919
+ return { root, installedAt: manifest?.installedAt || null, current, behind, missing, retired, skipped };
8886
8920
  }
8887
8921
 
8888
8922
  // src/skills/installable.ts
@@ -8940,8 +8974,11 @@ var skillsUsage = `diverge skills <list|install|refresh|status> [--organization-
8940
8974
 
8941
8975
  A skill is a procedure over the tools it names; you get the ones whose every
8942
8976
  tool you may run, in every organization the login can act in. Each lands as
8943
- <organization prefix>-<name> \u2014 the organization's short code, or a slug of
8944
- its name \u2014 and its SKILL.md opens with the organization its tools run in.
8977
+ <organization prefix>-<name> \u2014 the organization's handle, or a slug of
8978
+ its name; the platform shortens a longer one to the 64 characters a harness
8979
+ accepts \u2014 and its SKILL.md opens with the organization its tools run in.
8980
+ A folder that is not a skill name is skipped with a warning naming it, and
8981
+ the folder that skill was installed under before stays.
8945
8982
  --organization-id refreshes one organization and leaves the others as they
8946
8983
  are; without it the whole login is refreshed, and an organization the login
8947
8984
  no longer lists is retired. Without --harness, the harnesses present on this
@@ -8980,6 +9017,11 @@ function harnessesOf(flag, env) {
8980
9017
  function installSet(harness, skills) {
8981
9018
  return { cli: bundledSkills(harness), organizations: skills.organizations, complete: skills.complete };
8982
9019
  }
9020
+ var SKIPPED = "not installed, the folder is not a skill name (at most 64 lowercase letters, digits and hyphens)";
9021
+ function skippedNote(results) {
9022
+ const skipped = [...new Set(results.flatMap((result) => result.skipped))];
9023
+ return skipped.length ? [`${skipped.length} ${SKIPPED}: ${few(skipped)}`] : [];
9024
+ }
8983
9025
  function installReport(harness, result) {
8984
9026
  const holdings = result.counts.map((entry) => `${entry.label} ${entry.count}`).join(", ");
8985
9027
  const changes = [
@@ -9007,11 +9049,13 @@ async function refreshSkills(io, apiUrl, options = {}) {
9007
9049
  io.stderr(`warning: skills not refreshed (${error instanceof Error ? error.message : String(error)}); run: diverge skills install`);
9008
9050
  return;
9009
9051
  }
9052
+ const results = [];
9010
9053
  for (const harness of harnesses) {
9011
9054
  const result = installSkills(skillsRoot(harness, io.env), installSet(harness, skills), apiUrl);
9012
9055
  if (!options.quiet || result.added.length || result.updated.length || result.removed.length) io.stdout(installReport(harness, result));
9056
+ results.push(result);
9013
9057
  }
9014
- for (const note of skills.notes) io.stderr(`warning: ${note}`);
9058
+ for (const note of [...skills.notes, ...skippedNote(results)]) io.stderr(`warning: ${note}`);
9015
9059
  }
9016
9060
  async function runSkills(args, io) {
9017
9061
  const [verb, ...rest] = args;
@@ -9033,8 +9077,8 @@ async function runSkills(args, io) {
9033
9077
  const skills = await loginSkillSet(apiUrl, io.env, organizationId);
9034
9078
  const harnesses = values.into ? [harnessesOf(values.harness ?? HARNESSES[0], io.env)[0]] : harnessesOf(values.harness, io.env);
9035
9079
  if (harnesses.length === 0) throw new CliError("No coding-agent harness on this machine yet (Claude Code, Codex or dsh); name one with --harness to write its folder anyway.");
9036
- for (const harness of harnesses) installForHarness(io, harness, skills, apiUrl, values.into);
9037
- for (const note of skills.notes) io.stderr(`warning: ${note}`);
9080
+ const results = harnesses.map((harness) => installForHarness(io, harness, skills, apiUrl, values.into));
9081
+ for (const note of [...skills.notes, ...skippedNote(results)]) io.stderr(`warning: ${note}`);
9038
9082
  return ExitCode.Ok;
9039
9083
  }
9040
9084
  case "refresh": {
@@ -9050,6 +9094,7 @@ async function runSkills(args, io) {
9050
9094
  if (status2.behind.length) parts.push(`${status2.behind.length} behind (${few(status2.behind)})`);
9051
9095
  if (status2.missing.length) parts.push(`${status2.missing.length} missing${status2.current.length || status2.behind.length ? ` (${few(status2.missing)})` : ""}`);
9052
9096
  if (status2.retired.length) parts.push(`${status2.retired.length} retired (${few(status2.retired)})`);
9097
+ if (status2.skipped.length) parts.push(`${status2.skipped.length} ${SKIPPED} (${few(status2.skipped)})`);
9053
9098
  behind ||= status2.behind.length + status2.missing.length + status2.retired.length > 0;
9054
9099
  io.stdout(`${harness}: ${parts.join(", ")} in ${status2.root}${status2.installedAt ? ` (installed ${status2.installedAt})` : " (never installed)"}`);
9055
9100
  }
@@ -9584,6 +9629,11 @@ var MAX_PENDING_EVENTS = 2e4;
9584
9629
  var DEFAULT_POLL_MS = 2e3;
9585
9630
  var LAUNCH_SLACK_MS = 5e3;
9586
9631
  var STALLED_TICKS = 30;
9632
+ var CAPTURE_NOTICES = {
9633
+ off: "This organization does not record agent sessions (Forge installation config agentSessions.capture is off); nothing is sent.",
9634
+ metadata: "Recorded: metadata only \u2014 which tools ran and how many failed, tokens, models and timing; no prompt, reply, tool argument or result.",
9635
+ conversation: "Recorded: the whole conversation \u2014 every prompt, reply, tool call and its result. Yours to read; anyone else in the organization needs the Agent Conversations role, and every such read is recorded on the session."
9636
+ };
9587
9637
  function eventBytes(event) {
9588
9638
  return Buffer.byteLength(JSON.stringify(event), "utf8");
9589
9639
  }
@@ -9612,6 +9662,8 @@ var DshSessionPublisher = class {
9612
9662
  loop = null;
9613
9663
  /** The organization does not record: nothing more is read or sent. */
9614
9664
  disabled = false;
9665
+ /** The level the person was last told. */
9666
+ announced = null;
9615
9667
  ticking = null;
9616
9668
  /** The directory dsh writes this launch's sessions under. */
9617
9669
  get projectDir() {
@@ -9781,9 +9833,12 @@ var DshSessionPublisher = class {
9781
9833
  return;
9782
9834
  }
9783
9835
  session.reportedFailure = null;
9784
- if (!result.recording) {
9836
+ if (result.capture !== this.announced) {
9837
+ this.announced = result.capture;
9838
+ this.deps.report(CAPTURE_NOTICES[result.capture]);
9839
+ }
9840
+ if (result.capture === "off") {
9785
9841
  this.disabled = true;
9786
- this.deps.report("This organization does not record agent sessions (Forge installation config agentSessions.recording is off); nothing is sent.");
9787
9842
  return;
9788
9843
  }
9789
9844
  session.opened = true;
@@ -9901,8 +9956,8 @@ function syncDivergentPreset(io, binary, home) {
9901
9956
  async function skillsFor(io, apiUrl, harness, organizationId) {
9902
9957
  try {
9903
9958
  const skills = await loginSkillSet(apiUrl, io.env, organizationId?.trim() || void 0);
9904
- installForHarness(io, harness, skills, apiUrl);
9905
- for (const note of skills.notes) io.stderr(`warning: ${note}`);
9959
+ const result = installForHarness(io, harness, skills, apiUrl);
9960
+ for (const note of [...skills.notes, ...skippedNote([result])]) io.stderr(`warning: ${note}`);
9906
9961
  } catch (error) {
9907
9962
  io.stderr(`warning: skills not installed (${error instanceof Error ? error.message : String(error)}); run: diverge skills install`);
9908
9963
  }
@@ -10033,7 +10088,7 @@ async function run(args, io) {
10033
10088
  const source = tokenSource(io.env, apiUrl);
10034
10089
  const organizationId = tokenClaims(token.access_token).organizationId ?? values["organization-id"];
10035
10090
  if (!organizationId) throw new CliError("the gateway token names no organization; pass --organization-id");
10036
- io.stderr(`This session is recorded as organization ${organizationId}'s own record \u2014 the conversation, every tool call and its result \u2014 unless the organization turned recording off; its administrators set whether and for how long. Yours to read: diverge tools call ${AGENT_SESSION_LIST_TOOL} (ADR 0121).`);
10091
+ io.stderr(`This session is organization ${organizationId}'s own record at the level its administrators set \u2014 off, metadata (tools, tokens, models; no content) or the whole conversation \u2014 named once its record opens. Yours to read: diverge tools call ${AGENT_SESSION_LIST_TOOL} (ADR 0121).`);
10037
10092
  const tools = new ToolsClient({ apiUrl, organizationId, mode: "operator", clientId: clientId(io.env) }, io.env);
10038
10093
  const publisher = new DshSessionPublisher({
10039
10094
  sessionsRoot: path14.join(home, "sessions"),
@@ -10076,8 +10131,10 @@ async function appendSession(tools, input) {
10076
10131
  if (response.success === false || response.error) throw new CliError(response.error ?? "the platform refused the append");
10077
10132
  if (isPending(response.result)) throw new CliError("the append was raised for a decision, which an inert record never needs");
10078
10133
  const result = response.result;
10079
- if (!result || typeof result.recording !== "boolean") throw new CliError("the platform answered the append without a result");
10080
- return { recording: result.recording, sessionId: result.sessionId ?? null, stored: result.stored ?? 0, skipped: result.skipped ?? 0, lastSeq: typeof result.lastSeq === "number" ? result.lastSeq : -1 };
10134
+ if (!result || !(result.capture === "off" || result.capture === "metadata" || result.capture === "conversation")) {
10135
+ throw new CliError("the platform answered the append without the level it records");
10136
+ }
10137
+ return { capture: result.capture, sessionId: result.sessionId ?? null, stored: result.stored ?? 0, skipped: result.skipped ?? 0, lastSeq: typeof result.lastSeq === "number" ? result.lastSeq : -1 };
10081
10138
  }
10082
10139
  async function status(args, io) {
10083
10140
  const { values } = parseArgs3({ args, options: { "organization-id": { type: "string" } }, strict: true });
@@ -10891,13 +10948,19 @@ import { parseArgs as parseArgs7 } from "node:util";
10891
10948
  var pluginsUsage = `diverge plugins <validate|publish|list|retire> [--organization-id <id>] ...
10892
10949
 
10893
10950
  validate <manifest.json> (dry run: the platform's full validation, nothing stored)
10894
- publish <manifest.json> [--json] (forge_plugins:create in the signed-in organization)
10951
+ publish <manifest.json> [--json] (forge_plugins:create in the signed-in organization;
10952
+ a version that changes authority waits on a decision)
10895
10953
  list [--json]
10896
10954
  retire <pluginId>
10897
10955
 
10898
10956
  Register a tenant plugin: a plugin whose code runs in your organization's own
10899
10957
  runtime and never inside the platform. The manifest lists the tools the
10900
- platform registers, pinned; a change is a new version published again. Enable
10958
+ platform registers, pinned; a change is a new version published again.
10959
+
10960
+ A version that only reshapes or removes the tools already served is stored at
10961
+ once. Anything else changes authority: publish raises it as
10962
+ forge_publish_plugin, prints the invocation, and exits 3; a person decides it
10963
+ in Compass, and diverge tools invocation <id> reads the outcome. Enable
10901
10964
  it with forge plugins (or forge_set_plugin_state) and bind the runtime with a
10902
10965
  Forge bridge binding whose runtimeType matches the manifest's runtime.type.`;
10903
10966
  var SCOPE_OPTIONS2 = { "organization-id": { type: "string" } };
@@ -10905,7 +10968,28 @@ async function client2(values, io) {
10905
10968
  const settings = loadToolsSettings({ organizationId: values["organization-id"] }, io.env);
10906
10969
  io.stderr(scopeLine(settings));
10907
10970
  io.stderr(await actingAsLine(settings, io.env));
10908
- return new ToolsClient(settings, io.env);
10971
+ return { settings, tools: new ToolsClient(settings, io.env) };
10972
+ }
10973
+ function printProblems(details, io) {
10974
+ io.stderr(`Invalid manifest (${details.length} problem${details.length === 1 ? "" : "s"}):`);
10975
+ for (const detail of details) io.stderr(` - ${detail}`);
10976
+ return ExitCode.Failure;
10977
+ }
10978
+ async function raisePublish(manifest, authorityChanges, context, io) {
10979
+ const { settings, tools, json } = context;
10980
+ if (!json) {
10981
+ io.stdout("This version changes authority, so it is stored only once it is decided:");
10982
+ for (const change of authorityChanges) io.stdout(` - ${change}`);
10983
+ }
10984
+ const outcome = await runOne({ tool: "forge_publish_plugin", args: { manifest } }, {
10985
+ callTool: (name, args) => tools.callTool(name, args),
10986
+ decideAt: (pending) => `${settings.apiUrl}${pending.approvalUrl ?? ""}`,
10987
+ ...json ? {} : { onPending: (pending) => formatPending(pending, settings.apiUrl, settings.organizationId).forEach((line) => io.stdout(line)) }
10988
+ });
10989
+ if (json) io.stdout(JSON.stringify({ authorityChanges, ...outcome }, null, 2));
10990
+ else if (outcome.status === "executed") io.stdout("Published.");
10991
+ else if (outcome.status === "failed") throw new CliError(`forge_publish_plugin failed: ${outcome.error}`);
10992
+ return worstExitCode([outcome]);
10909
10993
  }
10910
10994
  function readManifest(file) {
10911
10995
  if (!file) throw new CliError(`A manifest file is required.
@@ -10942,30 +11026,30 @@ async function runPlugins(args, io) {
10942
11026
  case "validate": {
10943
11027
  const { values, positionals } = parseArgs7({ args: rest, options: { ...SCOPE_OPTIONS2, json: { type: "boolean", default: false } }, allowPositionals: true, strict: true });
10944
11028
  const manifest = readManifest(positionals[0]);
10945
- const tools = await client2(values, io);
11029
+ const { tools } = await client2(values, io);
10946
11030
  const outcome = await tools.publishPlugin(manifest, { dryRun: true });
10947
- if (!outcome.ok) {
10948
- io.stderr(`Invalid manifest (${outcome.details.length} problem${outcome.details.length === 1 ? "" : "s"}):`);
10949
- for (const detail of outcome.details) io.stderr(` - ${detail}`);
10950
- return ExitCode.Failure;
10951
- }
11031
+ if (!outcome.ok) return printProblems(outcome.details, io);
10952
11032
  if (values.json) io.stdout(JSON.stringify(outcome.result, null, 2));
10953
11033
  else {
10954
11034
  io.stdout(`Valid. ${outcome.result.changed ? "Publishing would register" : "Already published at this digest:"}`);
10955
11035
  for (const line of formatPlugin(outcome.result.plugin)) io.stdout(line);
11036
+ const authority = outcome.result.authorityChanges ?? [];
11037
+ if (authority.length) {
11038
+ io.stdout("Publishing it changes authority, so it waits on a person's decision in Compass:");
11039
+ for (const change of authority) io.stdout(` - ${change}`);
11040
+ }
10956
11041
  }
10957
11042
  return ExitCode.Ok;
10958
11043
  }
10959
11044
  case "publish": {
10960
11045
  const { values, positionals } = parseArgs7({ args: rest, options: { ...SCOPE_OPTIONS2, json: { type: "boolean", default: false } }, allowPositionals: true, strict: true });
10961
11046
  const manifest = readManifest(positionals[0]);
10962
- const tools = await client2(values, io);
11047
+ const { settings, tools } = await client2(values, io);
10963
11048
  const outcome = await tools.publishPlugin(manifest, { dryRun: false });
10964
- if (!outcome.ok) {
10965
- io.stderr(`Invalid manifest (${outcome.details.length} problem${outcome.details.length === 1 ? "" : "s"}):`);
10966
- for (const detail of outcome.details) io.stderr(` - ${detail}`);
10967
- return ExitCode.Failure;
11049
+ if (!outcome.ok && outcome.authorityChange) {
11050
+ return raisePublish(manifest, outcome.details, { settings, tools, json: values.json }, io);
10968
11051
  }
11052
+ if (!outcome.ok) return printProblems(outcome.details, io);
10969
11053
  if (values.json) io.stdout(JSON.stringify(outcome.result, null, 2));
10970
11054
  else {
10971
11055
  io.stdout(outcome.result.changed ? "Published." : "Unchanged: this digest was already published.");
@@ -10976,7 +11060,7 @@ async function runPlugins(args, io) {
10976
11060
  }
10977
11061
  case "list": {
10978
11062
  const { values } = parseArgs7({ args: rest, options: { ...SCOPE_OPTIONS2, json: { type: "boolean", default: false } }, strict: true });
10979
- const tools = await client2(values, io);
11063
+ const { tools } = await client2(values, io);
10980
11064
  const plugins = await tools.listPlugins();
10981
11065
  if (values.json) io.stdout(JSON.stringify(plugins, null, 2));
10982
11066
  else if (!plugins.length) io.stdout("No tenant plugins published for this organization.");
@@ -10989,7 +11073,7 @@ async function runPlugins(args, io) {
10989
11073
  if (!pluginId) throw new CliError(`A plugin id is required.
10990
11074
 
10991
11075
  ${pluginsUsage}`);
10992
- const tools = await client2(values, io);
11076
+ const { tools } = await client2(values, io);
10993
11077
  const plugin = await tools.retirePlugin(pluginId);
10994
11078
  io.stdout(`Retired ${plugin.pluginId}@${plugin.version}; its ${plugin.toolNames.length} tool(s) are gone. Installation records stay for a re-publish.`);
10995
11079
  return ExitCode.Ok;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@divergeai/cli",
3
- "version": "0.1.25",
3
+ "version": "0.1.27",
4
4
  "description": "Divergent platform CLI: sign in, wire coding agents to the inference gateway, call platform tools",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",