mirror of
https://github.com/block/buzz.git
synced 2026-11-05 08:51:10 +00:00
Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@sprout-oss.stage.blox.sqprod.co>
173 lines
6.4 KiB
TypeScript
173 lines
6.4 KiB
TypeScript
/**
|
|
* Utilities for extracting and parsing the `buzz:config-nudge` sentinel that
|
|
* `buzz-acp`'s setup-listener appends to its kind:9 nudge body.
|
|
*
|
|
* Wire format (appended by `setup_mode.rs::nudge_body()`):
|
|
*
|
|
* ```
|
|
* ```buzz:config-nudge
|
|
* {"agent_name":"…","agent_pubkey":"…","requirements":[…]}
|
|
* ```
|
|
* ```
|
|
*
|
|
* The prose above the fence is left untouched and used as a plaintext
|
|
* fallback for non-card clients. The desktop detects the sentinel here,
|
|
* strips it from the displayed markdown, and renders a `ConfigNudgeCard`.
|
|
*/
|
|
|
|
// ── Types ─────────────────────────────────────────────────────────────────────
|
|
|
|
import type { AcpAvailabilityStatus } from "@/shared/api/types";
|
|
|
|
/** A single missing requirement — mirrors the Rust `RequirementPayload` enum. */
|
|
export type ConfigNudgeRequirement =
|
|
| { surface: "normalized_field"; field: string }
|
|
| { surface: "env_key"; key: string }
|
|
| {
|
|
surface: "cli_login";
|
|
probe_args: string[];
|
|
setup_copy: string;
|
|
/**
|
|
* Granular install/auth state — mirrors `AcpAvailabilityStatus` from Rust.
|
|
* Determines which message and CTA the nudge card shows:
|
|
* - "available" → tooling installed, needs login
|
|
* - "adapter_missing" → CLI installed but ACP adapter missing
|
|
* - "adapter_outdated" → ACP adapter present but from deprecated package; reinstall required
|
|
* - "cli_missing" → ACP adapter installed but CLI missing
|
|
* - "not_installed" → neither adapter nor CLI found
|
|
*/
|
|
availability: AcpAvailabilityStatus;
|
|
}
|
|
| {
|
|
/**
|
|
* The CLI is installed but its config file could not be parsed.
|
|
* Informational only — no in-app action can repair an external config
|
|
* file. Rendered without a Doctor or Edit Agent CTA.
|
|
*/
|
|
surface: "cli_config_invalid";
|
|
probe_args: string[];
|
|
setup_copy: string;
|
|
/** One-line stderr excerpt from the CLI's parse error. */
|
|
diagnostic: string;
|
|
}
|
|
| { surface: "git_bash" };
|
|
|
|
/**
|
|
* The structured payload embedded in the `buzz:config-nudge` sentinel block.
|
|
* Mirrors the Rust `SetupPayload` struct.
|
|
*/
|
|
export type ConfigNudgePayload = {
|
|
agent_name: string;
|
|
/** Hex-encoded agent pubkey. Used by the desktop card action to open Edit Agent. */
|
|
agent_pubkey: string;
|
|
requirements: ConfigNudgeRequirement[];
|
|
};
|
|
|
|
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
|
|
const FENCE_OPEN = "```buzz:config-nudge";
|
|
const FENCE_CLOSE = "```";
|
|
|
|
// ── Extractor ─────────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Extract the `ConfigNudgePayload` from a message body, if present.
|
|
*
|
|
* Returns `null` when:
|
|
* - the sentinel fence is absent
|
|
* - the JSON inside is malformed
|
|
* - the parsed value doesn't match the expected shape
|
|
*
|
|
* Never throws — all errors are swallowed so this is safe to call in the
|
|
* render path.
|
|
*/
|
|
export function extractConfigNudge(content: string): ConfigNudgePayload | null {
|
|
const openIdx = content.indexOf(FENCE_OPEN);
|
|
if (openIdx === -1) return null;
|
|
|
|
// The JSON starts on the line after the opening fence.
|
|
const jsonStart = content.indexOf("\n", openIdx);
|
|
if (jsonStart === -1) return null;
|
|
|
|
// The JSON ends at the next closing ``` that appears on its own line.
|
|
const closeIdx = content.indexOf(`\n${FENCE_CLOSE}`, jsonStart);
|
|
if (closeIdx === -1) return null;
|
|
|
|
const json = content.slice(jsonStart + 1, closeIdx).trim();
|
|
if (!json) return null;
|
|
|
|
try {
|
|
const parsed: unknown = JSON.parse(json);
|
|
return isConfigNudgePayload(parsed) ? parsed : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Strip the `buzz:config-nudge` sentinel block (and any preceding blank line)
|
|
* from a message body. Returns the original string unchanged when no sentinel
|
|
* is present.
|
|
*
|
|
* Used so the prose fallback is rendered without the raw code block.
|
|
*/
|
|
export function stripConfigNudgeSentinel(content: string): string {
|
|
const openIdx = content.indexOf(FENCE_OPEN);
|
|
if (openIdx === -1) return content;
|
|
|
|
const closeIdx = content.indexOf(`\n${FENCE_CLOSE}`, openIdx);
|
|
if (closeIdx === -1) return content;
|
|
|
|
const afterFence = closeIdx + `\n${FENCE_CLOSE}`.length;
|
|
// Trim a preceding blank line so the prose doesn't gain a trailing gap.
|
|
const prose = content.slice(0, openIdx).replace(/\n{2,}$/, "\n");
|
|
return prose + content.slice(afterFence);
|
|
}
|
|
|
|
// ── Type-guard ─────────────────────────────────────────────────────────────────
|
|
|
|
function isConfigNudgeRequirement(v: unknown): v is ConfigNudgeRequirement {
|
|
if (typeof v !== "object" || v === null) return false;
|
|
const r = v as Record<string, unknown>;
|
|
if (typeof r.surface !== "string") return false;
|
|
switch (r.surface) {
|
|
case "normalized_field":
|
|
return typeof r.field === "string";
|
|
case "env_key":
|
|
return typeof r.key === "string";
|
|
case "cli_login":
|
|
return (
|
|
Array.isArray(r.probe_args) &&
|
|
r.probe_args.every((a) => typeof a === "string") &&
|
|
typeof r.setup_copy === "string" &&
|
|
(r.availability === "available" ||
|
|
r.availability === "adapter_missing" ||
|
|
r.availability === "adapter_outdated" ||
|
|
r.availability === "cli_missing" ||
|
|
r.availability === "not_installed")
|
|
);
|
|
case "git_bash":
|
|
return true;
|
|
case "cli_config_invalid":
|
|
return (
|
|
Array.isArray(r.probe_args) &&
|
|
r.probe_args.every((a) => typeof a === "string") &&
|
|
typeof r.setup_copy === "string" &&
|
|
typeof r.diagnostic === "string"
|
|
);
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function isConfigNudgePayload(v: unknown): v is ConfigNudgePayload {
|
|
if (typeof v !== "object" || v === null) return false;
|
|
const p = v as Record<string, unknown>;
|
|
return (
|
|
typeof p.agent_name === "string" &&
|
|
typeof p.agent_pubkey === "string" &&
|
|
Array.isArray(p.requirements) &&
|
|
p.requirements.every(isConfigNudgeRequirement)
|
|
);
|
|
}
|