@agentproto/runtime 2.8.0 → 2.10.1
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 +87 -1
- package/dist/catalog-models.d.ts +68 -2
- package/dist/catalog-models.mjs +53 -5
- package/dist/catalog-models.mjs.map +1 -1
- package/dist/config.d.ts +47 -2
- package/dist/config.mjs.map +1 -1
- package/dist/index.d.ts +399 -13
- package/dist/index.mjs +2733 -871
- package/dist/index.mjs.map +1 -1
- package/dist/pr-provenance.mjs +1 -1
- package/dist/pr-provenance.mjs.map +1 -1
- package/dist/release-check.d.ts +144 -0
- package/dist/release-check.mjs +149 -0
- package/dist/release-check.mjs.map +1 -0
- package/dist/resume-strategies.d.ts +35 -7
- package/dist/resume-strategies.mjs +12 -4
- package/dist/resume-strategies.mjs.map +1 -1
- package/dist/session-presence.d.ts +115 -0
- package/dist/session-presence.mjs +103 -0
- package/dist/session-presence.mjs.map +1 -0
- package/dist/session-story.mjs.map +1 -1
- package/dist/{spawn-defaults-DVgmfxWo.d.ts → spawn-defaults-DUnTfwVK.d.ts} +29 -10
- package/package.json +36 -24
package/dist/pr-provenance.mjs
CHANGED
|
@@ -46,7 +46,7 @@ var buildFooter = ({
|
|
|
46
46
|
if (prov?.host) parts.push(`host \`${prov.host}\``);
|
|
47
47
|
if (prov?.cwd) parts.push(`cwd \`${cwdLabel(prov.cwd, prov.workspaceSlug)}\``);
|
|
48
48
|
}
|
|
49
|
-
if (sha) parts.push(`sha \`${sha
|
|
49
|
+
if (sha) parts.push(`sha \`${sha}\``);
|
|
50
50
|
return `
|
|
51
51
|
|
|
52
52
|
---
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/pr-provenance.ts"],"names":[],"mappings":"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","file":"pr-provenance.mjs","sourcesContent":["/**\n * PR provenance footer — daemon/MCP lane.\n *\n * This is the SAME footer the runner-side agentflow scripts stamp on the\n * CI-PR, review, and local-`gh_open_pr` lanes; `scripts/lib/provenance-\n * footer.mjs` re-exports {@link buildFooter}/{@link MARKER}/{@link fmtTokens}\n * from HERE so there is exactly one format, one marker, one renderer. The\n * only reason this canonical copy lives in `@agentproto/runtime` rather than\n * in `scripts/` is direction of dependency: the daemon is a package and\n * cannot import a loose script, but a script can import a package's built\n * `dist/` (it already does for `computeProvenance`).\n *\n * What the DAEMON lane adds over the runner lanes: the footer is built from\n * a live {@link SessionDescriptor} (the executor agent-cli session that ran\n * `gh pr create`) plus its supervisor/parent session, rather than from a\n * `computeProvenance` join over the worktree. So this file also owns the\n * mapping from a session descriptor to the footer's provenance shape\n * ({@link sessionFooterProvenance}) and the small pure helpers the\n * command_execute stamp path needs ({@link parseGhPrCreate},\n * {@link pickExecutorSession}, {@link appendFooterOnce}).\n *\n * Pure by construction — only `node:path`, no daemon imports, no I/O — so it\n * bundles to a standalone `dist/pr-provenance.mjs` the scripts can import\n * without pulling the rest of the runtime in. Keep it that way. The\n * side-effecting orchestration (spawning `gh`, editing the PR body, recording\n * the opened PR) lives in `pr-provenance-stamp.ts`.\n */\n\nimport { basename } from \"node:path\"\n\n/** Deterministic marker the runner owns — a reliable native-vs-legacy\n * discriminator across every lane (CI-PR, review, local, daemon). */\nexport const MARKER = \"@agentproto-bot\"\n\nexport const fmtTokens = (n: unknown): string | null => {\n if (typeof n !== \"number\" || !Number.isFinite(n)) return null\n return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n)\n}\n\n/** The footer's provenance shape — field names are the renderer's, not the\n * session registry's (see {@link sessionFooterProvenance} for the mapping). */\nexport interface FooterProvenance {\n sessionId?: string\n label?: string\n /** Adapter / harness slug that ran (\"claude-code\", \"codex\", …). */\n adapter?: string\n model?: string\n /** Bound auth-profile IDENTITY — a label or ref, NEVER the credential. */\n authProfile?: string\n sandboxId?: string\n /** Supervisor / parent session that spawned the executor. */\n parentSessionId?: string\n tokensIn?: number\n tokensOut?: number\n costUsd?: number\n /** \"adapter\" | \"local\" | \"daemon\" — drives host/cwd vs run-link rendering. */\n source?: string\n host?: string\n cwd?: string\n /** The session's registered workspace slug (`SessionDescriptor.workspaceSlug`,\n * e.g. \"ts\", \"default\"). Rendered alongside `cwd`'s leaf directory name so\n * the footer identifies WHICH workspace ran the command, not just an\n * arbitrary trailing path segment — a bare `basename(cwd)` reads as\n * meaningless (or actively misleading) when the session's cwd is a\n * workspace root rather than a per-branch worktree. */\n workspaceSlug?: string\n}\n\n/**\n * Render the footer's `cwd` value. A bare `basename(cwd)` collapses to a\n * meaningless (or actively misleading) fragment whenever the session's cwd IS\n * the workspace root rather than a per-branch worktree dir — e.g. a checkout\n * literally named `ts` renders as the opaque `cwd \\`ts\\`` with no indication\n * that's a workspace name, not a worktree/branch leaf. Prefixing the\n * registered workspace slug disambiguates it, and is dropped when it's\n * identical to the leaf (the common per-worktree case) so the common case\n * stays exactly as compact as before.\n */\nfunction cwdLabel(cwd: string, workspaceSlug?: string): string {\n const leaf = basename(cwd)\n if (!workspaceSlug || workspaceSlug === leaf) return workspaceSlug ?? leaf\n return `${workspaceSlug}/${leaf}`\n}\n\nexport interface BuildFooterInput {\n prov?: FooterProvenance\n authMode?: string\n runId?: string\n runUrl?: string\n sha?: string\n kind?: string\n}\n\nexport const buildFooter = ({\n prov,\n authMode,\n runId,\n runUrl,\n sha,\n kind = \"review\",\n}: BuildFooterInput): string => {\n const parts = [`🤖 **${MARKER}** — ${kind}`]\n if (prov?.sessionId) {\n parts.push(`session \\`${prov.sessionId}\\`${prov.label ? ` (\\`${prov.label}\\`)` : \"\"}`)\n }\n // Engine label: \"adapter / authMode\" when an adapter ran; \"legacy fallback\n // (authMode)\" when no agent session exists at all (the API-key fallback path);\n // bare authMode only if a session ran without a resolved adapter slug.\n if (prov?.adapter) parts.push([prov.adapter, authMode].filter(Boolean).join(\" / \"))\n else if (!prov?.sessionId) parts.push(`legacy fallback${authMode ? ` (${authMode})` : \"\"}`)\n else if (authMode) parts.push(authMode)\n // Bound auth profile identity (the \"wallet\") — daemon lane; NEVER the secret.\n if (prov?.authProfile) parts.push(`auth-profile \\`${prov.authProfile}\\``)\n if (prov?.model) parts.push(`model \\`${prov.model}\\``)\n if (prov?.sandboxId) parts.push(`e2b \\`${prov.sandboxId}\\``)\n if (prov?.parentSessionId) parts.push(`supervisor \\`${prov.parentSessionId}\\``)\n const tin = fmtTokens(prov?.tokensIn)\n const tout = fmtTokens(prov?.tokensOut)\n if (tin || tout) parts.push(`${tin ?? \"?\"} in / ${tout ?? \"?\"} out`)\n if (typeof prov?.costUsd === \"number\") {\n parts.push(`$${prov.costUsd.toFixed(4)}${prov.source && prov.source !== \"adapter\" ? ` (${prov.source})` : \"\"}`)\n }\n const showLocalHostCwd = prov?.source === \"local\" || prov?.source === \"daemon\" || !runId\n if (runId && !showLocalHostCwd) parts.push(`run [${runId}](${runUrl})`)\n if (showLocalHostCwd) {\n if (prov?.host) parts.push(`host \\`${prov.host}\\``)\n if (prov?.cwd) parts.push(`cwd \\`${cwdLabel(prov.cwd, prov.workspaceSlug)}\\``)\n }\n if (sha) parts.push(`sha \\`${sha.slice(0, 7)}\\``)\n return `\\n\\n---\\n<sub>${parts.join(\" · \")}</sub>`\n}\n\n/** Structural subset of `SessionDescriptor` the footer needs. The runtime's\n * full descriptor satisfies this; keeping it structural is what lets this\n * module stay free of a `./sessions.js` import (and thus standalone). */\nexport interface FooterSession {\n id: string\n kind?: string\n status?: string\n startedAt?: string\n label?: string\n cwd?: string\n workspaceSlug?: string\n adapterSlug?: string\n harness?: string\n model?: string\n parentSessionId?: string\n costUsd?: number\n tokensIn?: number\n tokensOut?: number\n auth?: { mode?: string }\n accessProfile?: { profileRef: string; label?: string }\n}\n\nexport interface SessionFooterOptions {\n /** The supervisor/parent session descriptor, when resolvable. */\n supervisor?: { id: string } | null\n host?: string\n /** Provenance source tag; defaults to \"daemon\". */\n source?: string\n}\n\n/**\n * Map a live executor session (+ its supervisor) onto the footer's provenance\n * shape and the `authMode` the renderer takes separately. The adapter is the\n * canonical harness slug, falling back to `adapterSlug`. The bound auth\n * profile is the profile's human label (or its ref), never the credential.\n */\nexport function sessionFooterProvenance(\n session: FooterSession,\n options: SessionFooterOptions = {},\n): { prov: FooterProvenance; authMode: string | undefined } {\n const prov: FooterProvenance = {\n sessionId: session.id,\n label: session.label,\n adapter: session.harness ?? session.adapterSlug,\n model: session.model,\n authProfile: session.accessProfile?.label ?? session.accessProfile?.profileRef,\n parentSessionId: options.supervisor?.id,\n costUsd: session.costUsd,\n tokensIn: session.tokensIn,\n tokensOut: session.tokensOut,\n source: options.source ?? \"daemon\",\n host: options.host,\n cwd: session.cwd,\n workspaceSlug: session.workspaceSlug,\n }\n return { prov, authMode: session.auth?.mode }\n}\n\n/** Build the daemon-lane PR footer for one executor session directly. */\nexport function buildSessionPrFooter(\n session: FooterSession,\n options: SessionFooterOptions & { sha?: string } = {},\n): string {\n const { prov, authMode } = sessionFooterProvenance(session, options)\n return buildFooter({ prov, authMode, sha: options.sha, kind: \"PR\" })\n}\n\n/**\n * Append the footer to a PR body exactly once. Idempotent by the marker: a\n * body that already carries a `@agentproto-bot` footer is returned unchanged,\n * so a retried stamp (network reply lost after the edit landed) never stacks\n * a second footer.\n */\nexport function appendFooterOnce(body: string, footer: string): string {\n if (hasProvenanceFooter(body)) return body\n return `${body}${footer}`\n}\n\n/**\n * Whether a PR/review body already carries a RENDERED provenance footer — the\n * `<sub>…@agentproto-bot…</sub>` line {@link buildFooter} emits — as opposed\n * to merely MENTIONING the marker in prose. The distinction matters: a PR\n * whose description discusses the provenance machinery itself (they exist —\n * #999 explains a footer bug and quotes the marker) would otherwise read as\n * \"already stamped\" forever and never receive its real footer.\n */\nexport function hasProvenanceFooter(body: string): boolean {\n return new RegExp(`<sub>[^\\\\n]*${MARKER}`).test(body)\n}\n\n/**\n * Recognise a successful `gh pr create` from its argv + stdout and pull the\n * created PR's URL + number out. Returns null for anything that isn't a\n * PR-creating `gh` invocation, or a create whose stdout carried no PR URL.\n *\n * `gh pr create` prints the PR URL on its own line (often the last line,\n * after any advisory notices) — we take the LAST `…/pull/<n>` match so a\n * \"Warning: …/pull/…\" style notice can't shadow the real one.\n */\nexport function parseGhPrCreate(\n command: string,\n args: readonly string[],\n stdout: string,\n): { url: string; number: number } | null {\n if (basename(command) !== \"gh\") return null\n // Skip global flags; the first two non-flag tokens must be `pr` then `create`.\n const positionals = args.filter(a => !a.startsWith(\"-\"))\n if (positionals[0] !== \"pr\" || positionals[1] !== \"create\") return null\n const re = /https?:\\/\\/\\S+?\\/pull\\/(\\d+)/g\n let match: RegExpExecArray | null\n let last: { url: string; number: number } | null = null\n while ((match = re.exec(stdout)) !== null) {\n last = { url: match[0], number: Number(match[1]) }\n }\n return last\n}\n\n/**\n * String-form counterpart of {@link parseGhPrCreate} for the in-agent tool\n * lane: an ACP harness's Bash-style tool reports ONE shell string (possibly\n * compound — `git push && gh pr create … | tail -1`), not an argv, so the\n * argv parser above can't see it. Detects \"this call created a PR\" from the\n * command string plus the call's recorded result text, and returns the\n * created PR (LAST `…/pull/<n>` match in the result, same shadowing rule as\n * {@link parseGhPrCreate} — `gh pr create` prints its URL after any notices).\n *\n * Quoted spans are stripped before matching so a command that merely\n * MENTIONS the phrase — `grep \"gh pr create\" src/` over a result that quotes\n * a PR url — can never read as a create. Best-effort by design: the caller\n * additionally gates on the call's own `isError` (a failed create whose\n * stderr cites an existing PR must not attribute that PR here).\n */\nexport function detectShellPrCreate(\n command: string | undefined,\n resultText: string | undefined,\n): { url: string; number: number } | null {\n if (!command || !resultText) return null\n const unquoted = command.replace(/'[^']*'/g, \" \").replace(/\"(?:[^\"\\\\]|\\\\.)*\"/g, \" \")\n if (!/(^|[\\s;&|({])gh\\s+pr\\s+create(\\s|$)/.test(unquoted)) return null\n const re = /https?:\\/\\/\\S+?\\/pull\\/(\\d+)/g\n let match: RegExpExecArray | null\n let last: { url: string; number: number } | null = null\n while ((match = re.exec(resultText)) !== null) {\n last = { url: match[0], number: Number(match[1]) }\n }\n return last\n}\n\n/** `session.cwd == cwd || one contains the other` — the executor's cwd is\n * usually the worktree root while the command may run in a subdir (or vice\n * versa), so containment is checked in both directions. */\nfunction cwdRelated(sessionCwd: string, cwd: string): boolean {\n if (sessionCwd === cwd) return true\n const sep = \"/\"\n return cwd.startsWith(sessionCwd + sep) || sessionCwd.startsWith(cwd + sep)\n}\n\n/**\n * Best-effort attribution of a `command_execute` run to the executor agent\n * session that most likely issued it: the newest agent-cli session whose cwd\n * is related to the command's cwd, preferring a still-alive one. Returns\n * undefined when no agent-cli session matches (e.g. a bare shell opened the\n * PR) — the caller then skips stamping rather than emit a misleading \"legacy\n * fallback\" footer.\n */\nexport function pickExecutorSession<T extends FooterSession>(\n sessions: readonly T[],\n cwd: string,\n): T | undefined {\n const candidates = sessions.filter(\n s => s.kind === \"agent-cli\" && typeof s.cwd === \"string\" && cwdRelated(s.cwd, cwd),\n )\n if (candidates.length === 0) return undefined\n const alive = (s: T) => s.status === \"running\" || s.status === \"starting\"\n const byRecency = (a: T, b: T) => (b.startedAt ?? \"\").localeCompare(a.startedAt ?? \"\")\n const live = candidates.filter(alive).sort(byRecency)\n if (live.length > 0) return live[0]\n return [...candidates].sort(byRecency)[0]\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/pr-provenance.ts"],"names":[],"mappings":"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","file":"pr-provenance.mjs","sourcesContent":["/**\n * PR provenance footer — daemon/MCP lane.\n *\n * This is the SAME footer the runner-side agentflow scripts stamp on the\n * CI-PR, review, and local-`gh_open_pr` lanes; `scripts/lib/provenance-\n * footer.mjs` re-exports {@link buildFooter}/{@link MARKER}/{@link fmtTokens}\n * from HERE so there is exactly one format, one marker, one renderer. The\n * only reason this canonical copy lives in `@agentproto/runtime` rather than\n * in `scripts/` is direction of dependency: the daemon is a package and\n * cannot import a loose script, but a script can import a package's built\n * `dist/` (it already does for `computeProvenance`).\n *\n * What the DAEMON lane adds over the runner lanes: the footer is built from\n * a live {@link SessionDescriptor} (the executor agent-cli session that ran\n * `gh pr create`) plus its supervisor/parent session, rather than from a\n * `computeProvenance` join over the worktree. So this file also owns the\n * mapping from a session descriptor to the footer's provenance shape\n * ({@link sessionFooterProvenance}) and the small pure helpers the\n * command_execute stamp path needs ({@link parseGhPrCreate},\n * {@link pickExecutorSession}, {@link appendFooterOnce}).\n *\n * Pure by construction — only `node:path`, no daemon imports, no I/O — so it\n * bundles to a standalone `dist/pr-provenance.mjs` the scripts can import\n * without pulling the rest of the runtime in. Keep it that way. The\n * side-effecting orchestration (spawning `gh`, editing the PR body, recording\n * the opened PR) lives in `pr-provenance-stamp.ts`.\n */\n\nimport { basename } from \"node:path\"\n\n/** Deterministic marker the runner owns — a reliable native-vs-legacy\n * discriminator across every lane (CI-PR, review, local, daemon). */\nexport const MARKER = \"@agentproto-bot\"\n\nexport const fmtTokens = (n: unknown): string | null => {\n if (typeof n !== \"number\" || !Number.isFinite(n)) return null\n return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n)\n}\n\n/** The footer's provenance shape — field names are the renderer's, not the\n * session registry's (see {@link sessionFooterProvenance} for the mapping). */\nexport interface FooterProvenance {\n sessionId?: string\n label?: string\n /** Adapter / harness slug that ran (\"claude-code\", \"codex\", …). */\n adapter?: string\n model?: string\n /** Bound auth-profile IDENTITY — a label or ref, NEVER the credential. */\n authProfile?: string\n sandboxId?: string\n /** Supervisor / parent session that spawned the executor. */\n parentSessionId?: string\n tokensIn?: number\n tokensOut?: number\n costUsd?: number\n /** \"adapter\" | \"local\" | \"daemon\" — drives host/cwd vs run-link rendering. */\n source?: string\n host?: string\n cwd?: string\n /** The session's registered workspace slug (`SessionDescriptor.workspaceSlug`,\n * e.g. \"ts\", \"default\"). Rendered alongside `cwd`'s leaf directory name so\n * the footer identifies WHICH workspace ran the command, not just an\n * arbitrary trailing path segment — a bare `basename(cwd)` reads as\n * meaningless (or actively misleading) when the session's cwd is a\n * workspace root rather than a per-branch worktree. */\n workspaceSlug?: string\n}\n\n/**\n * Render the footer's `cwd` value. A bare `basename(cwd)` collapses to a\n * meaningless (or actively misleading) fragment whenever the session's cwd IS\n * the workspace root rather than a per-branch worktree dir — e.g. a checkout\n * literally named `ts` renders as the opaque `cwd \\`ts\\`` with no indication\n * that's a workspace name, not a worktree/branch leaf. Prefixing the\n * registered workspace slug disambiguates it, and is dropped when it's\n * identical to the leaf (the common per-worktree case) so the common case\n * stays exactly as compact as before.\n */\nfunction cwdLabel(cwd: string, workspaceSlug?: string): string {\n const leaf = basename(cwd)\n if (!workspaceSlug || workspaceSlug === leaf) return workspaceSlug ?? leaf\n return `${workspaceSlug}/${leaf}`\n}\n\nexport interface BuildFooterInput {\n prov?: FooterProvenance\n authMode?: string\n runId?: string\n runUrl?: string\n sha?: string\n kind?: string\n}\n\nexport const buildFooter = ({\n prov,\n authMode,\n runId,\n runUrl,\n sha,\n kind = \"review\",\n}: BuildFooterInput): string => {\n const parts = [`🤖 **${MARKER}** — ${kind}`]\n if (prov?.sessionId) {\n parts.push(`session \\`${prov.sessionId}\\`${prov.label ? ` (\\`${prov.label}\\`)` : \"\"}`)\n }\n // Engine label: \"adapter / authMode\" when an adapter ran; \"legacy fallback\n // (authMode)\" when no agent session exists at all (the API-key fallback path);\n // bare authMode only if a session ran without a resolved adapter slug.\n if (prov?.adapter) parts.push([prov.adapter, authMode].filter(Boolean).join(\" / \"))\n else if (!prov?.sessionId) parts.push(`legacy fallback${authMode ? ` (${authMode})` : \"\"}`)\n else if (authMode) parts.push(authMode)\n // Bound auth profile identity (the \"wallet\") — daemon lane; NEVER the secret.\n if (prov?.authProfile) parts.push(`auth-profile \\`${prov.authProfile}\\``)\n if (prov?.model) parts.push(`model \\`${prov.model}\\``)\n if (prov?.sandboxId) parts.push(`e2b \\`${prov.sandboxId}\\``)\n if (prov?.parentSessionId) parts.push(`supervisor \\`${prov.parentSessionId}\\``)\n const tin = fmtTokens(prov?.tokensIn)\n const tout = fmtTokens(prov?.tokensOut)\n if (tin || tout) parts.push(`${tin ?? \"?\"} in / ${tout ?? \"?\"} out`)\n if (typeof prov?.costUsd === \"number\") {\n parts.push(`$${prov.costUsd.toFixed(4)}${prov.source && prov.source !== \"adapter\" ? ` (${prov.source})` : \"\"}`)\n }\n const showLocalHostCwd = prov?.source === \"local\" || prov?.source === \"daemon\" || !runId\n if (runId && !showLocalHostCwd) parts.push(`run [${runId}](${runUrl})`)\n if (showLocalHostCwd) {\n if (prov?.host) parts.push(`host \\`${prov.host}\\``)\n if (prov?.cwd) parts.push(`cwd \\`${cwdLabel(prov.cwd, prov.workspaceSlug)}\\``)\n }\n if (sha) parts.push(`sha \\`${sha}\\``)\n return `\\n\\n---\\n<sub>${parts.join(\" · \")}</sub>`\n}\n\n/** Structural subset of `SessionDescriptor` the footer needs. The runtime's\n * full descriptor satisfies this; keeping it structural is what lets this\n * module stay free of a `./sessions.js` import (and thus standalone). */\nexport interface FooterSession {\n id: string\n kind?: string\n status?: string\n startedAt?: string\n label?: string\n cwd?: string\n workspaceSlug?: string\n adapterSlug?: string\n harness?: string\n model?: string\n parentSessionId?: string\n costUsd?: number\n tokensIn?: number\n tokensOut?: number\n auth?: { mode?: string }\n accessProfile?: { profileRef: string; label?: string }\n}\n\nexport interface SessionFooterOptions {\n /** The supervisor/parent session descriptor, when resolvable. */\n supervisor?: { id: string } | null\n host?: string\n /** Provenance source tag; defaults to \"daemon\". */\n source?: string\n}\n\n/**\n * Map a live executor session (+ its supervisor) onto the footer's provenance\n * shape and the `authMode` the renderer takes separately. The adapter is the\n * canonical harness slug, falling back to `adapterSlug`. The bound auth\n * profile is the profile's human label (or its ref), never the credential.\n */\nexport function sessionFooterProvenance(\n session: FooterSession,\n options: SessionFooterOptions = {},\n): { prov: FooterProvenance; authMode: string | undefined } {\n const prov: FooterProvenance = {\n sessionId: session.id,\n label: session.label,\n adapter: session.harness ?? session.adapterSlug,\n model: session.model,\n authProfile: session.accessProfile?.label ?? session.accessProfile?.profileRef,\n parentSessionId: options.supervisor?.id,\n costUsd: session.costUsd,\n tokensIn: session.tokensIn,\n tokensOut: session.tokensOut,\n source: options.source ?? \"daemon\",\n host: options.host,\n cwd: session.cwd,\n workspaceSlug: session.workspaceSlug,\n }\n return { prov, authMode: session.auth?.mode }\n}\n\n/** Build the daemon-lane PR footer for one executor session directly. */\nexport function buildSessionPrFooter(\n session: FooterSession,\n options: SessionFooterOptions & { sha?: string } = {},\n): string {\n const { prov, authMode } = sessionFooterProvenance(session, options)\n return buildFooter({ prov, authMode, sha: options.sha, kind: \"PR\" })\n}\n\n/**\n * Append the footer to a PR body exactly once. Idempotent by the marker: a\n * body that already carries a `@agentproto-bot` footer is returned unchanged,\n * so a retried stamp (network reply lost after the edit landed) never stacks\n * a second footer.\n */\nexport function appendFooterOnce(body: string, footer: string): string {\n if (hasProvenanceFooter(body)) return body\n return `${body}${footer}`\n}\n\n/**\n * Whether a PR/review body already carries a RENDERED provenance footer — the\n * `<sub>…@agentproto-bot…</sub>` line {@link buildFooter} emits — as opposed\n * to merely MENTIONING the marker in prose. The distinction matters: a PR\n * whose description discusses the provenance machinery itself (they exist —\n * #999 explains a footer bug and quotes the marker) would otherwise read as\n * \"already stamped\" forever and never receive its real footer.\n */\nexport function hasProvenanceFooter(body: string): boolean {\n return new RegExp(`<sub>[^\\\\n]*${MARKER}`).test(body)\n}\n\n/**\n * Recognise a successful `gh pr create` from its argv + stdout and pull the\n * created PR's URL + number out. Returns null for anything that isn't a\n * PR-creating `gh` invocation, or a create whose stdout carried no PR URL.\n *\n * `gh pr create` prints the PR URL on its own line (often the last line,\n * after any advisory notices) — we take the LAST `…/pull/<n>` match so a\n * \"Warning: …/pull/…\" style notice can't shadow the real one.\n */\nexport function parseGhPrCreate(\n command: string,\n args: readonly string[],\n stdout: string,\n): { url: string; number: number } | null {\n if (basename(command) !== \"gh\") return null\n // Skip global flags; the first two non-flag tokens must be `pr` then `create`.\n const positionals = args.filter(a => !a.startsWith(\"-\"))\n if (positionals[0] !== \"pr\" || positionals[1] !== \"create\") return null\n const re = /https?:\\/\\/\\S+?\\/pull\\/(\\d+)/g\n let match: RegExpExecArray | null\n let last: { url: string; number: number } | null = null\n while ((match = re.exec(stdout)) !== null) {\n last = { url: match[0], number: Number(match[1]) }\n }\n return last\n}\n\n/**\n * String-form counterpart of {@link parseGhPrCreate} for the in-agent tool\n * lane: an ACP harness's Bash-style tool reports ONE shell string (possibly\n * compound — `git push && gh pr create … | tail -1`), not an argv, so the\n * argv parser above can't see it. Detects \"this call created a PR\" from the\n * command string plus the call's recorded result text, and returns the\n * created PR (LAST `…/pull/<n>` match in the result, same shadowing rule as\n * {@link parseGhPrCreate} — `gh pr create` prints its URL after any notices).\n *\n * Quoted spans are stripped before matching so a command that merely\n * MENTIONS the phrase — `grep \"gh pr create\" src/` over a result that quotes\n * a PR url — can never read as a create. Best-effort by design: the caller\n * additionally gates on the call's own `isError` (a failed create whose\n * stderr cites an existing PR must not attribute that PR here).\n */\nexport function detectShellPrCreate(\n command: string | undefined,\n resultText: string | undefined,\n): { url: string; number: number } | null {\n if (!command || !resultText) return null\n const unquoted = command.replace(/'[^']*'/g, \" \").replace(/\"(?:[^\"\\\\]|\\\\.)*\"/g, \" \")\n if (!/(^|[\\s;&|({])gh\\s+pr\\s+create(\\s|$)/.test(unquoted)) return null\n const re = /https?:\\/\\/\\S+?\\/pull\\/(\\d+)/g\n let match: RegExpExecArray | null\n let last: { url: string; number: number } | null = null\n while ((match = re.exec(resultText)) !== null) {\n last = { url: match[0], number: Number(match[1]) }\n }\n return last\n}\n\n/** `session.cwd == cwd || one contains the other` — the executor's cwd is\n * usually the worktree root while the command may run in a subdir (or vice\n * versa), so containment is checked in both directions. */\nfunction cwdRelated(sessionCwd: string, cwd: string): boolean {\n if (sessionCwd === cwd) return true\n const sep = \"/\"\n return cwd.startsWith(sessionCwd + sep) || sessionCwd.startsWith(cwd + sep)\n}\n\n/**\n * Best-effort attribution of a `command_execute` run to the executor agent\n * session that most likely issued it: the newest agent-cli session whose cwd\n * is related to the command's cwd, preferring a still-alive one. Returns\n * undefined when no agent-cli session matches (e.g. a bare shell opened the\n * PR) — the caller then skips stamping rather than emit a misleading \"legacy\n * fallback\" footer.\n */\nexport function pickExecutorSession<T extends FooterSession>(\n sessions: readonly T[],\n cwd: string,\n): T | undefined {\n const candidates = sessions.filter(\n s => s.kind === \"agent-cli\" && typeof s.cwd === \"string\" && cwdRelated(s.cwd, cwd),\n )\n if (candidates.length === 0) return undefined\n const alive = (s: T) => s.status === \"running\" || s.status === \"starting\"\n const byRecency = (a: T, b: T) => (b.startedAt ?? \"\").localeCompare(a.startedAt ?? \"\")\n const live = candidates.filter(alive).sort(byRecency)\n if (live.length > 0) return live[0]\n return [...candidates].sort(byRecency)[0]\n}\n"]}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Release check for `@agentproto/cli` (and the daemon it carries) — shared
|
|
3
|
+
* between the CLI (`agentproto daemon status` fold) and the VS Code extension
|
|
4
|
+
* (the release status-bar indicator + update prompt). Both packages already
|
|
5
|
+
* depend on `@agentproto/runtime`, so it's the natural home for the ONE copy
|
|
6
|
+
* of the decision + IO, rather than a vscode-private module the CLI couldn't
|
|
7
|
+
* reach.
|
|
8
|
+
*
|
|
9
|
+
* Two halves:
|
|
10
|
+
* - the pure decision logic (`.`-version comparison, cache freshness, the
|
|
11
|
+
* offline-safe `resolveReleaseCheck`) — no import beyond the standard
|
|
12
|
+
* library, unit-testable without network/home/extension;
|
|
13
|
+
* - the IO half (npm registry fetch + the `~/.agentproto/release-check.json`
|
|
14
|
+
* versioned cache) — plain Node, no vscode import, runnable in tests.
|
|
15
|
+
*
|
|
16
|
+
* Contract (from .plans/release-update-indicator-PLAN.md, WP-A):
|
|
17
|
+
* - The signal "a new release exists" is the npm registry for
|
|
18
|
+
* `@agentproto/cli` (the CLI carries the daemon; `health.version` IS the
|
|
19
|
+
* CLI version, so the daemon inherits it).
|
|
20
|
+
* - Two comparison modes by `build.source` read on the daemon's /health:
|
|
21
|
+
* - `tarball` / absent → compare `health.version` against npm latest.
|
|
22
|
+
* - `workspace` → signal `workspace` distinctly: the real update for a
|
|
23
|
+
* workspace install is a local REBUILD, not an npm reinstall.
|
|
24
|
+
* - Offline-safe: never claim an update we didn't verify. When the npm fetch
|
|
25
|
+
* failed and the cache is stale, the answer is `unknown` — never a false
|
|
26
|
+
* "update dispo" off a stale number.
|
|
27
|
+
*/
|
|
28
|
+
/** Cache file version — bump when the on-disk shape changes. */
|
|
29
|
+
declare const RELEASE_CHECK_CACHE_VERSION = 1;
|
|
30
|
+
/** Default npm poll TTL (ms). ~1 h, never below the 10 min floor. */
|
|
31
|
+
declare const RELEASE_CHECK_DEFAULT_TTL_MS: number;
|
|
32
|
+
declare const RELEASE_CHECK_MIN_TTL_MS: number;
|
|
33
|
+
/** Build provenance, from the daemon's health.build.source. */
|
|
34
|
+
type ReleaseBuildSource = "workspace" | "tarball" | null;
|
|
35
|
+
/**
|
|
36
|
+
* What the release indicator reports. `workspace` is deliberately distinct
|
|
37
|
+
* from `current`: both mean "nothing to install via npm", but `workspace`
|
|
38
|
+
* additionally tells the display to frame the update as a rebuild.
|
|
39
|
+
*/
|
|
40
|
+
type ReleaseState = "current" | "behind" | "unknown" | "workspace";
|
|
41
|
+
/** Persistent cache — the offline fallback. Versioned like the other
|
|
42
|
+
* `~/.agentproto/*.json` stores (`{ version: N, ... }`). */
|
|
43
|
+
interface ReleaseCheckCache {
|
|
44
|
+
/** Cache-file shape version. Must equal RELEASE_CHECK_CACHE_VERSION to be
|
|
45
|
+
* trusted; a mismatch (older/newer on-disk shape) is treated as no cache. */
|
|
46
|
+
version: number;
|
|
47
|
+
/** Latest `@agentproto/cli` observed on npm. Null while unknown. */
|
|
48
|
+
latest: string | null;
|
|
49
|
+
/** Local CLI version at the time of the check that produced `latest`. */
|
|
50
|
+
localVersion: string | null;
|
|
51
|
+
/** Epoch ms of the last successful npm fetch. */
|
|
52
|
+
checkedAtMs: number;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Compare two dotted versions numerically. Returns a positive integer when
|
|
56
|
+
* `a > b`, negative when `a < b`, 0 when equal or unparseable. Accepts
|
|
57
|
+
* optional `v` prefix and trailing prerelease/build suffix.
|
|
58
|
+
*/
|
|
59
|
+
declare function compareVersions(a: string, b: string): number;
|
|
60
|
+
/**
|
|
61
|
+
* The WP-A decision. Returns:
|
|
62
|
+
* - `unknown` — no usable `latestVersion` (network error / no cache → the
|
|
63
|
+
* offline-safe answer; never a fabricated "behind").
|
|
64
|
+
* - `workspace` — the daemon is served from a workspace build: the real
|
|
65
|
+
* update is a rebuild, so this is its own state, not a
|
|
66
|
+
* plain `behind`.
|
|
67
|
+
* - `behind` — `buildSource` is a published tarball and npm is ahead.
|
|
68
|
+
* - `current` — tarball and npm is not ahead.
|
|
69
|
+
*/
|
|
70
|
+
declare function compareRelease(localVersion: string | null | undefined, latestVersion: string | null | undefined, buildSource: ReleaseBuildSource): ReleaseState;
|
|
71
|
+
/** TTL, clamped to the 10 min floor. */
|
|
72
|
+
declare function releaseTtlMs(intervalMin: number | undefined): number;
|
|
73
|
+
/** True when the cache is young enough to trust without a fresh fetch. */
|
|
74
|
+
declare function isCacheFresh(cache: ReleaseCheckCache | null, nowMs: number, ttlMs: number): boolean;
|
|
75
|
+
interface ResolveCheckInput {
|
|
76
|
+
localVersion: string | null;
|
|
77
|
+
buildSource: ReleaseBuildSource;
|
|
78
|
+
/** Npm result from a fresh fetch. Null when the network fetch failed or was
|
|
79
|
+
* skipped because the cache was already fresh. */
|
|
80
|
+
latestFromNpm: string | null;
|
|
81
|
+
cache: ReleaseCheckCache | null;
|
|
82
|
+
nowMs: number;
|
|
83
|
+
ttlMs: number;
|
|
84
|
+
}
|
|
85
|
+
interface ResolveCheckResult {
|
|
86
|
+
state: ReleaseState;
|
|
87
|
+
/** The `@agentproto/cli` latest we trust, if any (fresh npm or fresh cache). */
|
|
88
|
+
latest: string | null;
|
|
89
|
+
/** Whether `latest` came from the cache rather than a live fetch. */
|
|
90
|
+
fromCache: boolean;
|
|
91
|
+
/** Cached latest (may be stale) — informational only, never drives state. */
|
|
92
|
+
cachedLatest: string | null;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Resolve the full check from its inputs. This is the pure, offline-safe
|
|
96
|
+
* core: a fresh npm result wins; otherwise a fresh cache is trusted (this is
|
|
97
|
+
* the "return the cache if the TTL hasn't expired" fallback); otherwise the
|
|
98
|
+
* answer is `unknown` — a stale cache never fabricates a "behind".
|
|
99
|
+
*/
|
|
100
|
+
declare function resolveReleaseCheck(input: ResolveCheckInput): ResolveCheckResult;
|
|
101
|
+
/** `~/.agentproto/release-check.json`. */
|
|
102
|
+
declare function releaseCheckCachePath(home?: string): string;
|
|
103
|
+
interface FetchLatestCli {
|
|
104
|
+
(opts?: {
|
|
105
|
+
timeoutMs?: number;
|
|
106
|
+
}): Promise<string | null>;
|
|
107
|
+
}
|
|
108
|
+
/** Fetch the latest published `@agentproto/cli` version from the npm
|
|
109
|
+
* registry. Returns null on any network error (so the caller can fall back to
|
|
110
|
+
* the cache / report `unknown`), never throws. */
|
|
111
|
+
declare function fetchLatestCliVersion(opts?: {
|
|
112
|
+
timeoutMs?: number;
|
|
113
|
+
}): Promise<string | null>;
|
|
114
|
+
/** Read + parse the cache. Returns null when missing, malformed, or of a
|
|
115
|
+
* version we don't understand. */
|
|
116
|
+
declare function readReleaseCache(path?: string): Promise<ReleaseCheckCache | null>;
|
|
117
|
+
/** Atomically write the cache (tmp + rename), like the other versioned
|
|
118
|
+
* `~/.agentproto` stores. */
|
|
119
|
+
declare function writeReleaseCache(next: ReleaseCheckCache, path?: string): Promise<void>;
|
|
120
|
+
interface ReleaseCheckView {
|
|
121
|
+
/** Decided state. */
|
|
122
|
+
state: "current" | "behind" | "unknown" | "workspace";
|
|
123
|
+
/** Latest `@agentproto/cli` version to trust (fresh npm or fresh cache). */
|
|
124
|
+
latest: string | null;
|
|
125
|
+
/** Whether this run used the cache instead of npm. */
|
|
126
|
+
fromCache: boolean;
|
|
127
|
+
/** Local CLI version that was compared. */
|
|
128
|
+
localVersion: string | null;
|
|
129
|
+
}
|
|
130
|
+
interface RunReleaseCheckOptions {
|
|
131
|
+
localVersion: string | null;
|
|
132
|
+
buildSource: ReleaseBuildSource;
|
|
133
|
+
/** Poll TTL (ms). Pass releaseTtlMs(config) typically. */
|
|
134
|
+
ttlMs: number;
|
|
135
|
+
nowMs?: number;
|
|
136
|
+
/** Injectable fetcher for tests; defaults to the real npm fetch. */
|
|
137
|
+
fetchLatest?: FetchLatestCli;
|
|
138
|
+
cachePath?: string;
|
|
139
|
+
}
|
|
140
|
+
/** Run one release check: consult the cache, fetch npm only when stale, and
|
|
141
|
+
* persist a successful live result. Offline-safe by construction. */
|
|
142
|
+
declare function runReleaseCheck(opts: RunReleaseCheckOptions): Promise<ReleaseCheckView>;
|
|
143
|
+
|
|
144
|
+
export { type FetchLatestCli, RELEASE_CHECK_CACHE_VERSION, RELEASE_CHECK_DEFAULT_TTL_MS, RELEASE_CHECK_MIN_TTL_MS, type ReleaseBuildSource, type ReleaseCheckCache, type ReleaseCheckView, type ReleaseState, type ResolveCheckInput, type ResolveCheckResult, type RunReleaseCheckOptions, compareRelease, compareVersions, fetchLatestCliVersion, isCacheFresh, readReleaseCache, releaseCheckCachePath, releaseTtlMs, resolveReleaseCheck, runReleaseCheck, writeReleaseCache };
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { promises } from 'fs';
|
|
2
|
+
import { homedir } from 'os';
|
|
3
|
+
import { join, dirname } from 'path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @agentproto/runtime v0.1.0-alpha
|
|
7
|
+
* Long-running gateway: MCP server + HTTP transport + HEARTBEAT autonomy + conversation persistence over a workspace dir.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
var RELEASE_CHECK_CACHE_VERSION = 1;
|
|
11
|
+
var RELEASE_CHECK_DEFAULT_TTL_MS = 60 * 60 * 1e3;
|
|
12
|
+
var RELEASE_CHECK_MIN_TTL_MS = 10 * 60 * 1e3;
|
|
13
|
+
function compareVersions(a, b) {
|
|
14
|
+
const parse = (v) => {
|
|
15
|
+
const parts = v.replace(/^v/i, "").split("-")[0]?.split(".").map(Number).filter((n) => Number.isFinite(n)) ?? [];
|
|
16
|
+
return { parts, ok: parts.length > 0 };
|
|
17
|
+
};
|
|
18
|
+
const pa = parse(a);
|
|
19
|
+
const pb = parse(b);
|
|
20
|
+
if (!pa.ok || !pb.ok) return 0;
|
|
21
|
+
const len = Math.max(pa.parts.length, pb.parts.length);
|
|
22
|
+
for (let i = 0; i < len; i++) {
|
|
23
|
+
const da = pa.parts[i] ?? 0;
|
|
24
|
+
const db = pb.parts[i] ?? 0;
|
|
25
|
+
if (da !== db) return da - db;
|
|
26
|
+
}
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
function compareRelease(localVersion, latestVersion, buildSource) {
|
|
30
|
+
if (!localVersion || !latestVersion) return "unknown";
|
|
31
|
+
if (buildSource === "workspace") return "workspace";
|
|
32
|
+
if (compareVersions(latestVersion, localVersion) > 0) return "behind";
|
|
33
|
+
return "current";
|
|
34
|
+
}
|
|
35
|
+
function releaseTtlMs(intervalMin) {
|
|
36
|
+
const min = intervalMin && intervalMin > 0 ? intervalMin : RELEASE_CHECK_DEFAULT_TTL_MS / 6e4;
|
|
37
|
+
return Math.max(min * 6e4, RELEASE_CHECK_MIN_TTL_MS);
|
|
38
|
+
}
|
|
39
|
+
function isCacheFresh(cache, nowMs, ttlMs) {
|
|
40
|
+
if (!cache || cache.version !== RELEASE_CHECK_CACHE_VERSION) return false;
|
|
41
|
+
if (typeof cache.checkedAtMs !== "number" || Number.isNaN(cache.checkedAtMs)) return false;
|
|
42
|
+
return nowMs - cache.checkedAtMs <= ttlMs;
|
|
43
|
+
}
|
|
44
|
+
function resolveReleaseCheck(input) {
|
|
45
|
+
const freshCache = isCacheFresh(input.cache, input.nowMs, input.ttlMs);
|
|
46
|
+
if (input.latestFromNpm) {
|
|
47
|
+
return {
|
|
48
|
+
state: compareRelease(input.localVersion, input.latestFromNpm, input.buildSource),
|
|
49
|
+
latest: input.latestFromNpm,
|
|
50
|
+
fromCache: false,
|
|
51
|
+
cachedLatest: input.cache?.latest ?? null
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (freshCache && input.cache?.latest) {
|
|
55
|
+
return {
|
|
56
|
+
state: compareRelease(input.localVersion, input.cache.latest, input.buildSource),
|
|
57
|
+
latest: input.cache.latest,
|
|
58
|
+
fromCache: true,
|
|
59
|
+
cachedLatest: input.cache.latest
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
state: "unknown",
|
|
64
|
+
latest: null,
|
|
65
|
+
fromCache: false,
|
|
66
|
+
cachedLatest: input.cache?.latest ?? null
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
var CLI_PACKAGE = "@agentproto/cli";
|
|
70
|
+
var NPM_LATEST_URL = `https://registry.npmjs.org/${CLI_PACKAGE.replace("/", "%2F")}/latest`;
|
|
71
|
+
function releaseCheckCachePath(home = homedir()) {
|
|
72
|
+
return join(home, ".agentproto", "release-check.json");
|
|
73
|
+
}
|
|
74
|
+
async function fetchLatestCliVersion(opts = {}) {
|
|
75
|
+
const timeoutMs = opts.timeoutMs ?? 5e3;
|
|
76
|
+
const controller = new AbortController();
|
|
77
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
78
|
+
try {
|
|
79
|
+
const res = await fetch(NPM_LATEST_URL, { signal: controller.signal });
|
|
80
|
+
if (!res.ok) return null;
|
|
81
|
+
const body = await res.json();
|
|
82
|
+
if (typeof body.version === "string" && body.version.length > 0) return body.version;
|
|
83
|
+
return null;
|
|
84
|
+
} catch {
|
|
85
|
+
return null;
|
|
86
|
+
} finally {
|
|
87
|
+
clearTimeout(timer);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function readReleaseCache(path = releaseCheckCachePath()) {
|
|
91
|
+
try {
|
|
92
|
+
const raw = await promises.readFile(path, "utf8");
|
|
93
|
+
const parsed = JSON.parse(raw);
|
|
94
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
95
|
+
const c = parsed;
|
|
96
|
+
if (c.version !== RELEASE_CHECK_CACHE_VERSION) return null;
|
|
97
|
+
return {
|
|
98
|
+
version: RELEASE_CHECK_CACHE_VERSION,
|
|
99
|
+
latest: typeof c.latest === "string" ? c.latest : null,
|
|
100
|
+
localVersion: typeof c.localVersion === "string" ? c.localVersion : null,
|
|
101
|
+
checkedAtMs: typeof c.checkedAtMs === "number" ? c.checkedAtMs : Number.NaN
|
|
102
|
+
};
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async function writeReleaseCache(next, path = releaseCheckCachePath()) {
|
|
108
|
+
await promises.mkdir(dirname(path), { recursive: true });
|
|
109
|
+
const tmp = `${path}.tmp`;
|
|
110
|
+
await promises.writeFile(tmp, JSON.stringify(next, null, 2), "utf8");
|
|
111
|
+
await promises.rename(tmp, path);
|
|
112
|
+
}
|
|
113
|
+
async function runReleaseCheck(opts) {
|
|
114
|
+
const nowMs = opts.nowMs ?? Date.now();
|
|
115
|
+
const path = opts.cachePath ?? releaseCheckCachePath();
|
|
116
|
+
const fetchLatest = opts.fetchLatest ?? fetchLatestCliVersion;
|
|
117
|
+
const cache = await readReleaseCache(path);
|
|
118
|
+
const cacheFresh = cache && typeof cache.checkedAtMs === "number" && nowMs - cache.checkedAtMs <= opts.ttlMs;
|
|
119
|
+
const latestFromNpm = cacheFresh ? null : await fetchLatest();
|
|
120
|
+
const res = resolveReleaseCheck({
|
|
121
|
+
localVersion: opts.localVersion,
|
|
122
|
+
buildSource: opts.buildSource,
|
|
123
|
+
latestFromNpm,
|
|
124
|
+
cache,
|
|
125
|
+
nowMs,
|
|
126
|
+
ttlMs: opts.ttlMs
|
|
127
|
+
});
|
|
128
|
+
if (latestFromNpm) {
|
|
129
|
+
await writeReleaseCache(
|
|
130
|
+
{
|
|
131
|
+
version: RELEASE_CHECK_CACHE_VERSION,
|
|
132
|
+
latest: latestFromNpm,
|
|
133
|
+
localVersion: opts.localVersion,
|
|
134
|
+
checkedAtMs: nowMs
|
|
135
|
+
},
|
|
136
|
+
path
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
state: res.state,
|
|
141
|
+
latest: res.latest,
|
|
142
|
+
fromCache: res.fromCache,
|
|
143
|
+
localVersion: opts.localVersion
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export { RELEASE_CHECK_CACHE_VERSION, RELEASE_CHECK_DEFAULT_TTL_MS, RELEASE_CHECK_MIN_TTL_MS, compareRelease, compareVersions, fetchLatestCliVersion, isCacheFresh, readReleaseCache, releaseCheckCachePath, releaseTtlMs, resolveReleaseCheck, runReleaseCheck, writeReleaseCache };
|
|
148
|
+
//# sourceMappingURL=release-check.mjs.map
|
|
149
|
+
//# sourceMappingURL=release-check.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/release-check.ts"],"names":["fs"],"mappings":"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","file":"release-check.mjs","sourcesContent":["/**\n * Release check for `@agentproto/cli` (and the daemon it carries) — shared\n * between the CLI (`agentproto daemon status` fold) and the VS Code extension\n * (the release status-bar indicator + update prompt). Both packages already\n * depend on `@agentproto/runtime`, so it's the natural home for the ONE copy\n * of the decision + IO, rather than a vscode-private module the CLI couldn't\n * reach.\n *\n * Two halves:\n * - the pure decision logic (`.`-version comparison, cache freshness, the\n * offline-safe `resolveReleaseCheck`) — no import beyond the standard\n * library, unit-testable without network/home/extension;\n * - the IO half (npm registry fetch + the `~/.agentproto/release-check.json`\n * versioned cache) — plain Node, no vscode import, runnable in tests.\n *\n * Contract (from .plans/release-update-indicator-PLAN.md, WP-A):\n * - The signal \"a new release exists\" is the npm registry for\n * `@agentproto/cli` (the CLI carries the daemon; `health.version` IS the\n * CLI version, so the daemon inherits it).\n * - Two comparison modes by `build.source` read on the daemon's /health:\n * - `tarball` / absent → compare `health.version` against npm latest.\n * - `workspace` → signal `workspace` distinctly: the real update for a\n * workspace install is a local REBUILD, not an npm reinstall.\n * - Offline-safe: never claim an update we didn't verify. When the npm fetch\n * failed and the cache is stale, the answer is `unknown` — never a false\n * \"update dispo\" off a stale number.\n */\n\nimport { promises as fs } from \"node:fs\"\nimport { homedir } from \"node:os\"\nimport { dirname, join } from \"node:path\"\n\n/** Cache file version — bump when the on-disk shape changes. */\nexport const RELEASE_CHECK_CACHE_VERSION = 1\n\n/** Default npm poll TTL (ms). ~1 h, never below the 10 min floor. */\nexport const RELEASE_CHECK_DEFAULT_TTL_MS = 60 * 60 * 1000\nexport const RELEASE_CHECK_MIN_TTL_MS = 10 * 60 * 1000\n\n/** Build provenance, from the daemon's health.build.source. */\nexport type ReleaseBuildSource = \"workspace\" | \"tarball\" | null\n\n/**\n * What the release indicator reports. `workspace` is deliberately distinct\n * from `current`: both mean \"nothing to install via npm\", but `workspace`\n * additionally tells the display to frame the update as a rebuild.\n */\nexport type ReleaseState = \"current\" | \"behind\" | \"unknown\" | \"workspace\"\n\n/** Persistent cache — the offline fallback. Versioned like the other\n * `~/.agentproto/*.json` stores (`{ version: N, ... }`). */\nexport interface ReleaseCheckCache {\n /** Cache-file shape version. Must equal RELEASE_CHECK_CACHE_VERSION to be\n * trusted; a mismatch (older/newer on-disk shape) is treated as no cache. */\n version: number\n /** Latest `@agentproto/cli` observed on npm. Null while unknown. */\n latest: string | null\n /** Local CLI version at the time of the check that produced `latest`. */\n localVersion: string | null\n /** Epoch ms of the last successful npm fetch. */\n checkedAtMs: number\n}\n\n/**\n * Compare two dotted versions numerically. Returns a positive integer when\n * `a > b`, negative when `a < b`, 0 when equal or unparseable. Accepts\n * optional `v` prefix and trailing prerelease/build suffix.\n */\nexport function compareVersions(a: string, b: string): number {\n const parse = (v: string): { parts: number[]; ok: boolean } => {\n const parts =\n v\n .replace(/^v/i, \"\")\n .split(\"-\")[0]\n ?.split(\".\")\n .map(Number)\n .filter(n => Number.isFinite(n)) ?? []\n // A version that yields no numeric component (e.g. \"abc\") can't be\n // compared — treat the whole comparison as inconclusive rather than guess.\n return { parts, ok: parts.length > 0 }\n }\n const pa = parse(a)\n const pb = parse(b)\n if (!pa.ok || !pb.ok) return 0\n const len = Math.max(pa.parts.length, pb.parts.length)\n for (let i = 0; i < len; i++) {\n const da = pa.parts[i] ?? 0\n const db = pb.parts[i] ?? 0\n if (da !== db) return da - db\n }\n return 0\n}\n\n/**\n * The WP-A decision. Returns:\n * - `unknown` — no usable `latestVersion` (network error / no cache → the\n * offline-safe answer; never a fabricated \"behind\").\n * - `workspace` — the daemon is served from a workspace build: the real\n * update is a rebuild, so this is its own state, not a\n * plain `behind`.\n * - `behind` — `buildSource` is a published tarball and npm is ahead.\n * - `current` — tarball and npm is not ahead.\n */\nexport function compareRelease(\n localVersion: string | null | undefined,\n latestVersion: string | null | undefined,\n buildSource: ReleaseBuildSource,\n): ReleaseState {\n if (!localVersion || !latestVersion) return \"unknown\"\n if (buildSource === \"workspace\") return \"workspace\"\n if (compareVersions(latestVersion, localVersion) > 0) return \"behind\"\n return \"current\"\n}\n\n/** TTL, clamped to the 10 min floor. */\nexport function releaseTtlMs(intervalMin: number | undefined): number {\n const min = intervalMin && intervalMin > 0 ? intervalMin : RELEASE_CHECK_DEFAULT_TTL_MS / 60_000\n return Math.max(min * 60_000, RELEASE_CHECK_MIN_TTL_MS)\n}\n\n/** True when the cache is young enough to trust without a fresh fetch. */\nexport function isCacheFresh(\n cache: ReleaseCheckCache | null,\n nowMs: number,\n ttlMs: number,\n): boolean {\n if (!cache || cache.version !== RELEASE_CHECK_CACHE_VERSION) return false\n if (typeof cache.checkedAtMs !== \"number\" || Number.isNaN(cache.checkedAtMs)) return false\n return nowMs - cache.checkedAtMs <= ttlMs\n}\n\nexport interface ResolveCheckInput {\n localVersion: string | null\n buildSource: ReleaseBuildSource\n /** Npm result from a fresh fetch. Null when the network fetch failed or was\n * skipped because the cache was already fresh. */\n latestFromNpm: string | null\n cache: ReleaseCheckCache | null\n nowMs: number\n ttlMs: number\n}\n\nexport interface ResolveCheckResult {\n state: ReleaseState\n /** The `@agentproto/cli` latest we trust, if any (fresh npm or fresh cache). */\n latest: string | null\n /** Whether `latest` came from the cache rather than a live fetch. */\n fromCache: boolean\n /** Cached latest (may be stale) — informational only, never drives state. */\n cachedLatest: string | null\n}\n\n/**\n * Resolve the full check from its inputs. This is the pure, offline-safe\n * core: a fresh npm result wins; otherwise a fresh cache is trusted (this is\n * the \"return the cache if the TTL hasn't expired\" fallback); otherwise the\n * answer is `unknown` — a stale cache never fabricates a \"behind\".\n */\nexport function resolveReleaseCheck(input: ResolveCheckInput): ResolveCheckResult {\n const freshCache = isCacheFresh(input.cache, input.nowMs, input.ttlMs)\n if (input.latestFromNpm) {\n return {\n state: compareRelease(input.localVersion, input.latestFromNpm, input.buildSource),\n latest: input.latestFromNpm,\n fromCache: false,\n cachedLatest: input.cache?.latest ?? null,\n }\n }\n if (freshCache && input.cache?.latest) {\n return {\n state: compareRelease(input.localVersion, input.cache.latest, input.buildSource),\n latest: input.cache.latest,\n fromCache: true,\n cachedLatest: input.cache.latest,\n }\n }\n // Network failed AND the cache is stale or absent → unknown, no false claim.\n return {\n state: \"unknown\",\n latest: null,\n fromCache: false,\n cachedLatest: input.cache?.latest ?? null,\n }\n}\n\n// ── IO half ───────────────────────────────────────────────────────────────\n\n/** `@agentproto/cli` npm package (scoped → `%2F` in the registry URL). */\nconst CLI_PACKAGE = \"@agentproto/cli\"\nconst NPM_LATEST_URL = `https://registry.npmjs.org/${CLI_PACKAGE.replace(\"/\", \"%2F\")}/latest`\n\n/** `~/.agentproto/release-check.json`. */\nexport function releaseCheckCachePath(home: string = homedir()): string {\n return join(home, \".agentproto\", \"release-check.json\")\n}\n\nexport interface FetchLatestCli {\n (opts?: { timeoutMs?: number }): Promise<string | null>\n}\n\n/** Fetch the latest published `@agentproto/cli` version from the npm\n * registry. Returns null on any network error (so the caller can fall back to\n * the cache / report `unknown`), never throws. */\nexport async function fetchLatestCliVersion(opts: { timeoutMs?: number } = {}): Promise<string | null> {\n const timeoutMs = opts.timeoutMs ?? 5_000\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), timeoutMs)\n try {\n const res = await fetch(NPM_LATEST_URL, { signal: controller.signal })\n if (!res.ok) return null\n const body = (await res.json()) as { version?: unknown }\n if (typeof body.version === \"string\" && body.version.length > 0) return body.version\n return null\n } catch {\n return null\n } finally {\n clearTimeout(timer)\n }\n}\n\n/** Read + parse the cache. Returns null when missing, malformed, or of a\n * version we don't understand. */\nexport async function readReleaseCache(\n path: string = releaseCheckCachePath(),\n): Promise<ReleaseCheckCache | null> {\n try {\n const raw = await fs.readFile(path, \"utf8\")\n const parsed = JSON.parse(raw) as unknown\n if (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) return null\n const c = parsed as Partial<ReleaseCheckCache>\n if (c.version !== RELEASE_CHECK_CACHE_VERSION) return null\n return {\n version: RELEASE_CHECK_CACHE_VERSION,\n latest: typeof c.latest === \"string\" ? c.latest : null,\n localVersion: typeof c.localVersion === \"string\" ? c.localVersion : null,\n checkedAtMs: typeof c.checkedAtMs === \"number\" ? c.checkedAtMs : Number.NaN,\n }\n } catch {\n return null\n }\n}\n\n/** Atomically write the cache (tmp + rename), like the other versioned\n * `~/.agentproto` stores. */\nexport async function writeReleaseCache(\n next: ReleaseCheckCache,\n path: string = releaseCheckCachePath(),\n): Promise<void> {\n await fs.mkdir(dirname(path), { recursive: true })\n const tmp = `${path}.tmp`\n await fs.writeFile(tmp, JSON.stringify(next, null, 2), \"utf8\")\n await fs.rename(tmp, path)\n}\n\nexport interface ReleaseCheckView {\n /** Decided state. */\n state: \"current\" | \"behind\" | \"unknown\" | \"workspace\"\n /** Latest `@agentproto/cli` version to trust (fresh npm or fresh cache). */\n latest: string | null\n /** Whether this run used the cache instead of npm. */\n fromCache: boolean\n /** Local CLI version that was compared. */\n localVersion: string | null\n}\n\nexport interface RunReleaseCheckOptions {\n localVersion: string | null\n buildSource: ReleaseBuildSource\n /** Poll TTL (ms). Pass releaseTtlMs(config) typically. */\n ttlMs: number\n nowMs?: number\n /** Injectable fetcher for tests; defaults to the real npm fetch. */\n fetchLatest?: FetchLatestCli\n cachePath?: string\n}\n\n/** Run one release check: consult the cache, fetch npm only when stale, and\n * persist a successful live result. Offline-safe by construction. */\nexport async function runReleaseCheck(opts: RunReleaseCheckOptions): Promise<ReleaseCheckView> {\n const nowMs = opts.nowMs ?? Date.now()\n const path = opts.cachePath ?? releaseCheckCachePath()\n const fetchLatest = opts.fetchLatest ?? fetchLatestCliVersion\n const cache = await readReleaseCache(path)\n\n // Fetch only when the cache can't answer fresh (no cache, wrong version, or\n // TTL expired). A fresh cache is the offline fallback — do not hit npm.\n const cacheFresh = cache && typeof cache.checkedAtMs === \"number\" && nowMs - cache.checkedAtMs <= opts.ttlMs\n const latestFromNpm = cacheFresh ? null : await fetchLatest()\n\n const res = resolveReleaseCheck({\n localVersion: opts.localVersion,\n buildSource: opts.buildSource,\n latestFromNpm,\n cache,\n nowMs,\n ttlMs: opts.ttlMs,\n })\n\n // Persist a live successful result for the next offline window.\n if (latestFromNpm) {\n await writeReleaseCache(\n {\n version: RELEASE_CHECK_CACHE_VERSION,\n latest: latestFromNpm,\n localVersion: opts.localVersion,\n checkedAtMs: nowMs,\n },\n path,\n )\n }\n\n return {\n state: res.state,\n latest: res.latest,\n fromCache: res.fromCache,\n localVersion: opts.localVersion,\n }\n}"]}
|
|
@@ -72,6 +72,12 @@ interface ResumeStrategy {
|
|
|
72
72
|
* When omitted, the daemon falls back to ACP-level resume via
|
|
73
73
|
* the agent-cli protocol instead of the provider's native CLI. */
|
|
74
74
|
spawnArgs?(id: string): string[];
|
|
75
|
+
/** Env var the resumed PTY needs pointed at the session's isolated
|
|
76
|
+
* provider config dir to find ITS OWN conversation store — see
|
|
77
|
+
* `ConversationStore.configDirEnvVar`'s doc (conversation-store.ts) for
|
|
78
|
+
* why this exists and what silently omitting it breaks. Undefined for
|
|
79
|
+
* a provider with no config-dir-isolated store. */
|
|
80
|
+
configDirEnvVar?: string;
|
|
75
81
|
}
|
|
76
82
|
declare const RESUME_STRATEGIES: Record<string, ResumeStrategy>;
|
|
77
83
|
/**
|
|
@@ -116,12 +122,15 @@ interface FsProbeCandidate extends RestartCandidate {
|
|
|
116
122
|
*
|
|
117
123
|
* 1. pty-native — the adapter has a captured resume id AND declares
|
|
118
124
|
* `spawnArgs` (e.g. claude-code): respawn a PTY running the
|
|
119
|
-
* provider's own resume command. Most reliable
|
|
120
|
-
*
|
|
121
|
-
*
|
|
125
|
+
* provider's own resume command. Most reliable CONTINUITY mechanism
|
|
126
|
+
* when it applies — but only actually reachable by default when the
|
|
127
|
+
* prior session was ITSELF a real PTY (`prev.pty === true`), or the
|
|
128
|
+
* caller explicitly opts in (`preferNativeTerminal`) — see the
|
|
129
|
+
* `mayPreferNative` doc below for why.
|
|
122
130
|
* 2. pty-plain — the previous session was a real PTY with no
|
|
123
131
|
* native strategy match: re-run the same argv, no continuity.
|
|
124
|
-
* 3. agent — an agent-cli session with no native strategy
|
|
132
|
+
* 3. agent — an agent-cli session with no native strategy (or an
|
|
133
|
+
* ACP-origin session where pty-native doesn't apply — see above):
|
|
125
134
|
* resume at the ACP level via the adapter's own session id (may
|
|
126
135
|
* still 404 if the adapter never persisted a turn — callers
|
|
127
136
|
* should retry without `resumeSessionId` on a "not found" error).
|
|
@@ -141,7 +150,17 @@ type RestartStrategy = {
|
|
|
141
150
|
kind: "unsupported";
|
|
142
151
|
reason: string;
|
|
143
152
|
};
|
|
144
|
-
|
|
153
|
+
interface DecideRestartStrategyOptions {
|
|
154
|
+
/**
|
|
155
|
+
* Explicit opt-in to provider-native terminal resume for a session whose
|
|
156
|
+
* ORIGIN was agent-cli/ACP (not itself a PTY) — a human who wants the raw
|
|
157
|
+
* provider TUI instead of ACP-level resume, understanding the tradeoffs
|
|
158
|
+
* (see `mayPreferNative`'s doc below). Default false/omitted: an
|
|
159
|
+
* ACP-origin session always resumes via ACP.
|
|
160
|
+
*/
|
|
161
|
+
preferNativeTerminal?: boolean;
|
|
162
|
+
}
|
|
163
|
+
declare function decideRestartStrategy(prev: RestartCandidate, opts?: DecideRestartStrategyOptions): RestartStrategy;
|
|
145
164
|
/**
|
|
146
165
|
* Matches an adapter's rejection of a `resumeSessionId` it can't honour —
|
|
147
166
|
* the trigger for the "retry as a fresh spawn" fallback in
|
|
@@ -188,8 +207,17 @@ declare function augmentWithFsResume<T extends FsProbeCandidate>(prev: T): Promi
|
|
|
188
207
|
* declared capability backs that claim (`resumable !== false`). An adapter
|
|
189
208
|
* that declared `resumable: false` but still carries an `adapterSessionId`
|
|
190
209
|
* gets an honest degraded label instead of the lie — never silently "".
|
|
210
|
+
*
|
|
211
|
+
* Origin gate (mirrors `decideRestartStrategy`'s own — MUST stay in sync or
|
|
212
|
+
* this label lies about what actually happened): the native-resume phrasing
|
|
213
|
+
* is only reported when `decideRestartStrategy` itself would have picked
|
|
214
|
+
* pty-native for this `prev` — i.e. `prev.pty === true` or the caller passed
|
|
215
|
+
* the SAME `preferNativeTerminal` it decided the strategy with. Without this
|
|
216
|
+
* gate, a captured `resumeMetadata` alone used to make this function claim
|
|
217
|
+
* "resumed via claude --resume" even when the actual decision (now)
|
|
218
|
+
* downgrades an ACP-origin session to ACP-level resume instead.
|
|
191
219
|
*/
|
|
192
|
-
declare function describeResumePath(prev: RestartCandidate): string;
|
|
220
|
+
declare function describeResumePath(prev: RestartCandidate, opts?: DecideRestartStrategyOptions): string;
|
|
193
221
|
/**
|
|
194
222
|
* Shell-style argv tokenizer for descriptors that don't carry `argv`
|
|
195
223
|
* separately (legacy persisted rows predating that field). Same rules
|
|
@@ -198,4 +226,4 @@ declare function describeResumePath(prev: RestartCandidate): string;
|
|
|
198
226
|
*/
|
|
199
227
|
declare function tokenizeCommand(s: string): string[];
|
|
200
228
|
|
|
201
|
-
export { type FsProbeCandidate, RESUME_ID_REJECTED_RE, RESUME_STRATEGIES, type RestartCandidate, type RestartStrategy, type ResumeMetadataKey, type ResumeStrategy, augmentWithFsResume, decideRestartStrategy, describeResumePath, hasResumeStrategy, tokenizeCommand };
|
|
229
|
+
export { type DecideRestartStrategyOptions, type FsProbeCandidate, RESUME_ID_REJECTED_RE, RESUME_STRATEGIES, type RestartCandidate, type RestartStrategy, type ResumeMetadataKey, type ResumeStrategy, augmentWithFsResume, decideRestartStrategy, describeResumePath, hasResumeStrategy, tokenizeCommand };
|
|
@@ -618,6 +618,10 @@ Either the session never drove an agent-cli turn, or it predates this feature.`
|
|
|
618
618
|
flushAssistant();
|
|
619
619
|
messages.push({ role: "user", text: rec.text ?? "", ...tsOrUndefined !== void 0 ? { ts: tsOrUndefined } : {} });
|
|
620
620
|
break;
|
|
621
|
+
case "system-prompt":
|
|
622
|
+
flushAssistant();
|
|
623
|
+
messages.push({ role: "system", text: rec.text ?? "", ...tsOrUndefined !== void 0 ? { ts: tsOrUndefined } : {} });
|
|
624
|
+
break;
|
|
621
625
|
case "text-delta":
|
|
622
626
|
if (asmTs === void 0) asmTs = tsOrUndefined;
|
|
623
627
|
asmText += rec.text ?? "";
|
|
@@ -1598,6 +1602,7 @@ var init_conversation_store = __esm({
|
|
|
1598
1602
|
// (default). Example: `claude --resume 0e483f81-1a44-4bec-9667-b37158450296`
|
|
1599
1603
|
outputHint: /claude\s+--resume\s+([0-9a-f-]{8,})/i,
|
|
1600
1604
|
attachArgv: (conversationId) => ["claude", "--resume", conversationId],
|
|
1605
|
+
configDirEnvVar: "CLAUDE_CONFIG_DIR",
|
|
1601
1606
|
discover: discoverClaudeCode,
|
|
1602
1607
|
read: readClaudeCode
|
|
1603
1608
|
},
|
|
@@ -1648,6 +1653,7 @@ var RESUME_STRATEGIES = Object.fromEntries(
|
|
|
1648
1653
|
{
|
|
1649
1654
|
outputHint: store.outputHint,
|
|
1650
1655
|
storeAs: store.storeAs,
|
|
1656
|
+
...store.configDirEnvVar ? { configDirEnvVar: store.configDirEnvVar } : {},
|
|
1651
1657
|
fsProbe: async (cwd, prevStartedAt, expectedId, configDir) => {
|
|
1652
1658
|
const candidates = await s.discover({
|
|
1653
1659
|
cwd,
|
|
@@ -1667,8 +1673,9 @@ function hasResumeStrategy(adapterSlug) {
|
|
|
1667
1673
|
const s = RESUME_STRATEGIES[adapterSlug];
|
|
1668
1674
|
return !!(s && (s.outputHint || s.fsProbe || s.spawnArgs));
|
|
1669
1675
|
}
|
|
1670
|
-
function decideRestartStrategy(prev) {
|
|
1671
|
-
|
|
1676
|
+
function decideRestartStrategy(prev, opts = {}) {
|
|
1677
|
+
const mayPreferNative = prev.pty === true || opts.preferNativeTerminal === true;
|
|
1678
|
+
if (prev.adapterSlug && prev.nativeTerminalResume === true && mayPreferNative) {
|
|
1672
1679
|
const strategy = RESUME_STRATEGIES[prev.adapterSlug];
|
|
1673
1680
|
const id = strategy?.storeAs ? prev.resumeMetadata?.[strategy.storeAs] : void 0;
|
|
1674
1681
|
if (strategy?.spawnArgs && id) {
|
|
@@ -1723,8 +1730,9 @@ async function augmentWithFsResume(prev) {
|
|
|
1723
1730
|
}
|
|
1724
1731
|
};
|
|
1725
1732
|
}
|
|
1726
|
-
function describeResumePath(prev) {
|
|
1727
|
-
|
|
1733
|
+
function describeResumePath(prev, opts = {}) {
|
|
1734
|
+
const mayPreferNative = prev.pty === true || opts.preferNativeTerminal === true;
|
|
1735
|
+
if (prev.adapterSlug && mayPreferNative) {
|
|
1728
1736
|
const s = RESUME_STRATEGIES[prev.adapterSlug];
|
|
1729
1737
|
if (s?.spawnArgs && s.storeAs && prev.resumeMetadata?.[s.storeAs]) {
|
|
1730
1738
|
const sample = s.spawnArgs("\u2026")[0] ?? prev.adapterSlug;
|