diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index d9f08a3da..a15aa5523 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -6,6 +6,7 @@ - Added support for the [Agent Plugins 1.0.0 standard](https://agent-plugins.org): plugin packages with a root `plugin.json` targeting the canonical schema are discovered from marketplace installs, `--plugin-dir`, and configured extension roots, with `skills/` and `mcp.json` loaded per the specification (closed-schema validation per skills-ref, `${PLUGIN_ROOT}`/`${PLUGIN_DATA}` expansion, reserved subprocess environment, instance-keyed persistent data directories, and per-component failure isolation). Package-boundary containment is enforced before every read — including `skill://` resource access from the read tool and bash, where plugin skill files must realpath-resolve inside the plugin root. - Remote MCP transports now enforce header precedence and origin policy: client-generated HTTP/MCP/authorization headers win over configured headers case-insensitively, and Agent Plugins servers never forward configured headers across a redirect to a different origin (method-changing redirects of JSON-RPC POSTs are refused). Agent Plugins stdio `env` values and remote `headers` are likewise exempt from config-value resolution (no ambient env-name lookup, no `!command` execution, empty values preserved). +- Added `omp share `: share a saved session by id prefix or `.jsonl` path without launching the agent — same encrypted upload, store selection, and `share.redactSecrets` handling as the `/share` slash command. ## [17.2.10] - 2026-08-06 diff --git a/packages/coding-agent/src/cli-commands.ts b/packages/coding-agent/src/cli-commands.ts index 6a56da8c6..2d0ae2924 100644 --- a/packages/coding-agent/src/cli-commands.ts +++ b/packages/coding-agent/src/cli-commands.ts @@ -120,6 +120,11 @@ export const commands: CommandEntry[] = [ load: () => import("./commands/say").then(m => m.default), help: commandHelp.sayHelp, }, + { + name: "share", + load: () => import("./commands/share").then(m => m.default), + help: commandHelp.shareHelp, + }, { name: "setup", load: () => import("./commands/setup").then(m => m.default), diff --git a/packages/coding-agent/src/cli/command-help.ts b/packages/coding-agent/src/cli/command-help.ts index 0375b57d1..17ee781f9 100644 --- a/packages/coding-agent/src/cli/command-help.ts +++ b/packages/coding-agent/src/cli/command-help.ts @@ -72,6 +72,10 @@ export const sayHelp = { export const searchHelp = { description: "Test web search providers" } satisfies CommandMetadata; +export const shareHelp = { + description: "Share a saved session via an encrypted link (same as /share)", +} satisfies CommandMetadata; + export const setupHelp = { description: "Run onboarding setup or install dependencies for optional features", } satisfies CommandMetadata; diff --git a/packages/coding-agent/src/commands/share.ts b/packages/coding-agent/src/commands/share.ts new file mode 100644 index 000000000..e4b5909e3 --- /dev/null +++ b/packages/coding-agent/src/commands/share.ts @@ -0,0 +1,71 @@ +/** + * Share a saved session as an encrypted link without launching the agent. + * + * `omp share ` accepts a session id (prefix) or a path to a session + * `.jsonl` and uploads the sealed snapshot exactly like the `/share` slash + * command, honoring `share.serverUrl`, `share.store`, and + * `share.redactSecrets`. + */ + +import { getAgentDir } from "@oh-my-pi/pi-utils"; +import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli"; +import { shareHelp as commandHelp } from "../cli/command-help"; +import { Settings } from "../config/settings"; +import { shareSession } from "../export/share"; +import { buildSecretObfuscator } from "../secrets"; +import { resolveResumableSession } from "../session/session-listing"; +import { SessionManager } from "../session/session-manager"; + +export default class Share extends Command { + static description = commandHelp.description; + static args = { + session: Args.string({ + description: "Session id (prefix) or path to a session .jsonl", + required: true, + }), + }; + static flags = { + gist: Flags.boolean({ + description: "Upload to a secret GitHub gist instead of the share server", + default: false, + }), + }; + + async run(): Promise { + const { args, flags } = await this.parse(Share); + + const sessionArg = args.session ?? ""; + let sessionPath = sessionArg; + if (!sessionArg.includes("/") && !sessionArg.includes("\\") && !sessionArg.endsWith(".jsonl")) { + const match = await resolveResumableSession(sessionArg, process.cwd()); + if (!match) { + process.stderr.write(`Session "${sessionArg}" not found.\n`); + process.exitCode = 1; + return; + } + sessionPath = match.session.path; + } + + const sm = await SessionManager.open(sessionPath); + // Settings resolve against the session's own project so its + // share.redactSecrets/secrets.enabled policy governs, not the invoking cwd's. + const settings = await Settings.loadReadOnly({ cwd: sm.getCwd() }); + // Same leak boundary as /share: a share blob leaves the machine, so honor + // share.redactSecrets with the full obfuscator built against the session's + // own project directory (its secrets.yml, not the invoking cwd's). + const obfuscator = + settings.get("share.redactSecrets") && settings.get("secrets.enabled") + ? await buildSecretObfuscator(sm.getCwd(), getAgentDir()) + : undefined; + + const result = await shareSession(sm, { + serverUrl: settings.get("share.serverUrl"), + store: flags.gist ? "gist" : settings.get("share.store"), + obfuscator, + }); + const lines = [`Share URL: ${result.url}`]; + if (result.gistUrl) lines.push(`Gist: ${result.gistUrl}`); + if (result.truncated) lines.push("Note: large content was trimmed to fit the share size limit."); + console.log(lines.join("\n")); + } +}