@ai-agent-forge/plugin-session-share 0.85.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,16 @@
1
+ # @agent-forge/plugin-session-share
2
+
3
+ First-party Agent Forge capability plugin: shares the current session through
4
+ an explicitly configured target (`radius` or `gist`) via the `/share` command.
5
+
6
+ - No implicit fallback: a missing or unconfigured target is a structured
7
+ error naming the config file.
8
+ - Config channel: `<agentDir>/capabilities/builtin.session-share.json`
9
+ (read when the host declares `api.host.agentDir`), merged over the
10
+ manifest config.
11
+ - On hosts declaring the `output-artifacts` feature, the gist path also
12
+ persists an offline viewer HTML artifact through the public
13
+ OutputArtifactAPI.
14
+
15
+ Publishes as `@ai-agent-forge/plugin-session-share`; the workspace name is
16
+ `@agent-forge/plugin-session-share`.
@@ -0,0 +1,11 @@
1
+ {
2
+ "packageFormatVersion": 1,
3
+ "id": "agent-forge.plugin.session-share",
4
+ "pluginVersion": "0.84.4",
5
+ "apiVersion": "1",
6
+ "minHostVersion": "0.84.4",
7
+ "platform": {
8
+ "os": ["windows", "macos", "linux"],
9
+ "runtime": [{ "name": "node" }]
10
+ }
11
+ }
@@ -0,0 +1,23 @@
1
+ import type { CapabilityAPI } from "@agent-forge/plugin-sdk";
2
+ /**
3
+ * Default-export factory consumed by the host's `loadCapabilityPluginFromManifest`.
4
+ *
5
+ * Config channel (D-075 S4 second batch): when the host declares
6
+ * `api.host.agentDir`, the agent-dir config file
7
+ * `<agentDir>/capabilities/builtin.session-share.json` — the exact path the
8
+ * host's pre-split `builtinCapabilityConfigPath(agentDir, "builtin.session-share")`
9
+ * resolved — is read here and merged over the manifest config, restoring the
10
+ * embedded `withCapabilityConfig` semantics the host carried before the
11
+ * package split. A missing file is the normal unconfigured state; an
12
+ * unreadable file, invalid JSON, or a non-object payload fails the plugin
13
+ * load explicitly (D-028: this plugin only, never the session). A host
14
+ * without `agentDir` loads the plugin with the manifest config alone.
15
+ *
16
+ * The factory returns its registration as an array; the wrapper disposes it
17
+ * in reverse order on plugin teardown — the same disposal semantics as the
18
+ * host's embedded `syncRegistrationFactory`.
19
+ */
20
+ export default function createSessionSharePluginEntry(api: CapabilityAPI): {
21
+ dispose: () => void;
22
+ };
23
+ //# sourceMappingURL=entry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entry.d.ts","sourceRoot":"","sources":["../src/entry.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAgB,MAAM,yBAAyB,CAAC;AAG3E;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,OAAO,UAAU,6BAA6B,CAAC,GAAG,EAAE,aAAa,GAAG;IAAE,OAAO,EAAE,MAAM,IAAI,CAAA;CAAE,CAqBjG","sourcesContent":["import { readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { CapabilityAPI, PluginConfig } from \"@agent-forge/plugin-sdk\";\nimport { CAPABILITY_CONFIG_RELATIVE_PATH, createCapability } from \"./index.ts\";\n\n/**\n * Default-export factory consumed by the host's `loadCapabilityPluginFromManifest`.\n *\n * Config channel (D-075 S4 second batch): when the host declares\n * `api.host.agentDir`, the agent-dir config file\n * `<agentDir>/capabilities/builtin.session-share.json` — the exact path the\n * host's pre-split `builtinCapabilityConfigPath(agentDir, \"builtin.session-share\")`\n * resolved — is read here and merged over the manifest config, restoring the\n * embedded `withCapabilityConfig` semantics the host carried before the\n * package split. A missing file is the normal unconfigured state; an\n * unreadable file, invalid JSON, or a non-object payload fails the plugin\n * load explicitly (D-028: this plugin only, never the session). A host\n * without `agentDir` loads the plugin with the manifest config alone.\n *\n * The factory returns its registration as an array; the wrapper disposes it\n * in reverse order on plugin teardown — the same disposal semantics as the\n * host's embedded `syncRegistrationFactory`.\n */\nexport default function createSessionSharePluginEntry(api: CapabilityAPI): { dispose: () => void } {\n\tconst agentDir = api.host.agentDir;\n\tconst configPath = agentDir === undefined ? undefined : join(agentDir, CAPABILITY_CONFIG_RELATIVE_PATH);\n\tconst fileConfig = readAgentDirConfig(configPath);\n\tconst effectiveApi: CapabilityAPI =\n\t\tfileConfig === undefined\n\t\t\t? api\n\t\t\t: {\n\t\t\t\t\t...api,\n\t\t\t\t\tconfig: { ...api.config, ...fileConfig },\n\t\t\t\t};\n\tconst registrations = createCapability(effectiveApi, configPath === undefined ? {} : { configPath });\n\treturn {\n\t\tdispose: () => {\n\t\t\tfor (const registration of [...registrations].reverse()) {\n\t\t\t\t// Registration disposal in the capability runtime is synchronous;\n\t\t\t\t// void consumes the declared Promise possibility of the contract.\n\t\t\t\tvoid registration.dispose();\n\t\t\t}\n\t\t},\n\t};\n}\n\n/** Reads the agent-dir config channel; `undefined` means \"no file\" (normal unconfigured state). */\nfunction readAgentDirConfig(configPath: string | undefined): PluginConfig | undefined {\n\tif (configPath === undefined) return undefined;\n\tlet raw: string;\n\ttry {\n\t\traw = readFileSync(configPath, \"utf8\");\n\t} catch (error) {\n\t\tif ((error as NodeJS.ErrnoException).code === \"ENOENT\") return undefined;\n\t\tthrow new Error(`Failed to read ${configPath}: ${error instanceof Error ? error.message : String(error)}`);\n\t}\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(raw);\n\t} catch (error) {\n\t\tthrow new Error(`${configPath} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);\n\t}\n\tif (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n\t\tthrow new Error(`${configPath} must contain a JSON object`);\n\t}\n\treturn parsed as PluginConfig;\n}\n"]}
package/dist/entry.js ADDED
@@ -0,0 +1,68 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { CAPABILITY_CONFIG_RELATIVE_PATH, createCapability } from "./index.js";
4
+ /**
5
+ * Default-export factory consumed by the host's `loadCapabilityPluginFromManifest`.
6
+ *
7
+ * Config channel (D-075 S4 second batch): when the host declares
8
+ * `api.host.agentDir`, the agent-dir config file
9
+ * `<agentDir>/capabilities/builtin.session-share.json` — the exact path the
10
+ * host's pre-split `builtinCapabilityConfigPath(agentDir, "builtin.session-share")`
11
+ * resolved — is read here and merged over the manifest config, restoring the
12
+ * embedded `withCapabilityConfig` semantics the host carried before the
13
+ * package split. A missing file is the normal unconfigured state; an
14
+ * unreadable file, invalid JSON, or a non-object payload fails the plugin
15
+ * load explicitly (D-028: this plugin only, never the session). A host
16
+ * without `agentDir` loads the plugin with the manifest config alone.
17
+ *
18
+ * The factory returns its registration as an array; the wrapper disposes it
19
+ * in reverse order on plugin teardown — the same disposal semantics as the
20
+ * host's embedded `syncRegistrationFactory`.
21
+ */
22
+ export default function createSessionSharePluginEntry(api) {
23
+ const agentDir = api.host.agentDir;
24
+ const configPath = agentDir === undefined ? undefined : join(agentDir, CAPABILITY_CONFIG_RELATIVE_PATH);
25
+ const fileConfig = readAgentDirConfig(configPath);
26
+ const effectiveApi = fileConfig === undefined
27
+ ? api
28
+ : {
29
+ ...api,
30
+ config: { ...api.config, ...fileConfig },
31
+ };
32
+ const registrations = createCapability(effectiveApi, configPath === undefined ? {} : { configPath });
33
+ return {
34
+ dispose: () => {
35
+ for (const registration of [...registrations].reverse()) {
36
+ // Registration disposal in the capability runtime is synchronous;
37
+ // void consumes the declared Promise possibility of the contract.
38
+ void registration.dispose();
39
+ }
40
+ },
41
+ };
42
+ }
43
+ /** Reads the agent-dir config channel; `undefined` means "no file" (normal unconfigured state). */
44
+ function readAgentDirConfig(configPath) {
45
+ if (configPath === undefined)
46
+ return undefined;
47
+ let raw;
48
+ try {
49
+ raw = readFileSync(configPath, "utf8");
50
+ }
51
+ catch (error) {
52
+ if (error.code === "ENOENT")
53
+ return undefined;
54
+ throw new Error(`Failed to read ${configPath}: ${error instanceof Error ? error.message : String(error)}`);
55
+ }
56
+ let parsed;
57
+ try {
58
+ parsed = JSON.parse(raw);
59
+ }
60
+ catch (error) {
61
+ throw new Error(`${configPath} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
62
+ }
63
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
64
+ throw new Error(`${configPath} must contain a JSON object`);
65
+ }
66
+ return parsed;
67
+ }
68
+ //# sourceMappingURL=entry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entry.js","sourceRoot":"","sources":["../src/entry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,+BAA+B,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE/E;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,OAAO,UAAU,6BAA6B,CAAC,GAAkB,EAA2B;IAClG,MAAM,QAAQ,GAAG,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC;IACnC,MAAM,UAAU,GAAG,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,EAAE,+BAA+B,CAAC,CAAC;IACxG,MAAM,UAAU,GAAG,kBAAkB,CAAC,UAAU,CAAC,CAAC;IAClD,MAAM,YAAY,GACjB,UAAU,KAAK,SAAS;QACvB,CAAC,CAAC,GAAG;QACL,CAAC,CAAC;YACA,GAAG,GAAG;YACN,MAAM,EAAE,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,GAAG,UAAU,EAAE;SACxC,CAAC;IACL,MAAM,aAAa,GAAG,gBAAgB,CAAC,YAAY,EAAE,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC;IACrG,OAAO;QACN,OAAO,EAAE,GAAG,EAAE,CAAC;YACd,KAAK,MAAM,YAAY,IAAI,CAAC,GAAG,aAAa,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC;gBACzD,kEAAkE;gBAClE,kEAAkE;gBAClE,KAAK,YAAY,CAAC,OAAO,EAAE,CAAC;YAC7B,CAAC;QAAA,CACD;KACD,CAAC;AAAA,CACF;AAED,mGAAmG;AACnG,SAAS,kBAAkB,CAAC,UAA8B,EAA4B;IACrF,IAAI,UAAU,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC/C,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC;QACJ,GAAG,GAAG,YAAY,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;IACxC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAO,SAAS,CAAC;QACzE,MAAM,IAAI,KAAK,CAAC,kBAAkB,UAAU,KAAK,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAC5G,CAAC;IACD,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACJ,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC1B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,MAAM,IAAI,KAAK,CAAC,GAAG,UAAU,uBAAuB,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAC/G,CAAC;IACD,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAC5E,MAAM,IAAI,KAAK,CAAC,GAAG,UAAU,6BAA6B,CAAC,CAAC;IAC7D,CAAC;IACD,OAAO,MAAsB,CAAC;AAAA,CAC9B","sourcesContent":["import { readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { CapabilityAPI, PluginConfig } from \"@agent-forge/plugin-sdk\";\nimport { CAPABILITY_CONFIG_RELATIVE_PATH, createCapability } from \"./index.ts\";\n\n/**\n * Default-export factory consumed by the host's `loadCapabilityPluginFromManifest`.\n *\n * Config channel (D-075 S4 second batch): when the host declares\n * `api.host.agentDir`, the agent-dir config file\n * `<agentDir>/capabilities/builtin.session-share.json` — the exact path the\n * host's pre-split `builtinCapabilityConfigPath(agentDir, \"builtin.session-share\")`\n * resolved — is read here and merged over the manifest config, restoring the\n * embedded `withCapabilityConfig` semantics the host carried before the\n * package split. A missing file is the normal unconfigured state; an\n * unreadable file, invalid JSON, or a non-object payload fails the plugin\n * load explicitly (D-028: this plugin only, never the session). A host\n * without `agentDir` loads the plugin with the manifest config alone.\n *\n * The factory returns its registration as an array; the wrapper disposes it\n * in reverse order on plugin teardown — the same disposal semantics as the\n * host's embedded `syncRegistrationFactory`.\n */\nexport default function createSessionSharePluginEntry(api: CapabilityAPI): { dispose: () => void } {\n\tconst agentDir = api.host.agentDir;\n\tconst configPath = agentDir === undefined ? undefined : join(agentDir, CAPABILITY_CONFIG_RELATIVE_PATH);\n\tconst fileConfig = readAgentDirConfig(configPath);\n\tconst effectiveApi: CapabilityAPI =\n\t\tfileConfig === undefined\n\t\t\t? api\n\t\t\t: {\n\t\t\t\t\t...api,\n\t\t\t\t\tconfig: { ...api.config, ...fileConfig },\n\t\t\t\t};\n\tconst registrations = createCapability(effectiveApi, configPath === undefined ? {} : { configPath });\n\treturn {\n\t\tdispose: () => {\n\t\t\tfor (const registration of [...registrations].reverse()) {\n\t\t\t\t// Registration disposal in the capability runtime is synchronous;\n\t\t\t\t// void consumes the declared Promise possibility of the contract.\n\t\t\t\tvoid registration.dispose();\n\t\t\t}\n\t\t},\n\t};\n}\n\n/** Reads the agent-dir config channel; `undefined` means \"no file\" (normal unconfigured state). */\nfunction readAgentDirConfig(configPath: string | undefined): PluginConfig | undefined {\n\tif (configPath === undefined) return undefined;\n\tlet raw: string;\n\ttry {\n\t\traw = readFileSync(configPath, \"utf8\");\n\t} catch (error) {\n\t\tif ((error as NodeJS.ErrnoException).code === \"ENOENT\") return undefined;\n\t\tthrow new Error(`Failed to read ${configPath}: ${error instanceof Error ? error.message : String(error)}`);\n\t}\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(raw);\n\t} catch (error) {\n\t\tthrow new Error(`${configPath} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);\n\t}\n\tif (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n\t\tthrow new Error(`${configPath} must contain a JSON object`);\n\t}\n\treturn parsed as PluginConfig;\n}\n"]}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Session share capability plugin: shares the current session through an
3
+ * explicitly configured target (radius or gist), extracted from the host's
4
+ * `src/capabilities/session-share.ts` (D-075 S4, second B-class batch).
5
+ *
6
+ * Productized from the `session-share` example plugin (开发计划 §216): the
7
+ * sharing target must be configured explicitly and a missing target is a
8
+ * structured error — there is no implicit Radius/Gist fallback and no core
9
+ * `/share` special path. The plugin consumes the public `session`,
10
+ * `session-export`, `commands` and (optionally) `credentials` capabilities
11
+ * and carries its own transport. On hosts declaring `output-artifacts` the
12
+ * gist path additionally persists an offline viewer HTML artifact through
13
+ * the public OutputArtifactAPI (2.6d consumption side); the command result
14
+ * and its links stay untouched.
15
+ */
16
+ import type { CapabilityAPI, DisposableRegistration } from "@agent-forge/plugin-sdk";
17
+ import { renderSharedTranscriptView } from "./share-viewer.ts";
18
+ /** Viewer render re-exported for consumers of this module (2.6d consumption side). */
19
+ export { renderSharedTranscriptView };
20
+ export interface ShareProcessResult {
21
+ code: number | null;
22
+ stdout: string;
23
+ stderr: string;
24
+ }
25
+ export type ShareProcess = (argv: readonly string[], input: Uint8Array, signal: AbortSignal) => Promise<ShareProcessResult>;
26
+ /** Injection seam for deterministic tests; production uses fetch and a real child process. */
27
+ export interface SessionShareCapabilityOptions {
28
+ fetch?: typeof globalThis.fetch;
29
+ process?: ShareProcess;
30
+ /**
31
+ * Agent-dir config file path of this capability. When present, the
32
+ * unconfigured error names it so the user knows where to set the target;
33
+ * the standalone example passes nothing and keeps its exact message.
34
+ */
35
+ configPath?: string;
36
+ }
37
+ /**
38
+ * Config file channel (D-075 S4 second batch): the same agent-dir path the
39
+ * host's `builtinCapabilityConfigPath(agentDir, "builtin.session-share")`
40
+ * resolves to for the pre-split embedded capability. Hosts that declare
41
+ * `api.host.agentDir` have their `<agentDir>/capabilities/` config file read
42
+ * by the plugin entry and merged over the manifest config — the exact
43
+ * merge-over-defaults semantics the embedded `withCapabilityConfig` channel
44
+ * had. Keep in sync with the host constant.
45
+ */
46
+ export declare const CAPABILITY_CONFIG_RELATIVE_PATH: string;
47
+ export declare function createCapability(api: CapabilityAPI, options?: SessionShareCapabilityOptions): readonly DisposableRegistration[];
48
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,OAAO,KAAK,EACX,aAAa,EAGb,sBAAsB,EAEtB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,0BAA0B,EAAE,MAAM,mBAAmB,CAAC;AAE/D,sFAAsF;AACtF,OAAO,EAAE,0BAA0B,EAAE,CAAC;AAItC,MAAM,WAAW,kBAAkB;IAClC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,YAAY,GAAG,CAC1B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,KAAK,EAAE,UAAU,EACjB,MAAM,EAAE,WAAW,KACf,OAAO,CAAC,kBAAkB,CAAC,CAAC;AAEjC,8FAA8F;AAC9F,MAAM,WAAW,6BAA6B;IAC7C,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAChC,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,+BAA+B,QAAqD,CAAC;AA6IlG,wBAAgB,gBAAgB,CAC/B,GAAG,EAAE,aAAa,EAClB,OAAO,GAAE,6BAAkC,GACzC,SAAS,sBAAsB,EAAE,CAsBnC","sourcesContent":["/**\n * Session share capability plugin: shares the current session through an\n * explicitly configured target (radius or gist), extracted from the host's\n * `src/capabilities/session-share.ts` (D-075 S4, second B-class batch).\n *\n * Productized from the `session-share` example plugin (开发计划 §216): the\n * sharing target must be configured explicitly and a missing target is a\n * structured error — there is no implicit Radius/Gist fallback and no core\n * `/share` special path. The plugin consumes the public `session`,\n * `session-export`, `commands` and (optionally) `credentials` capabilities\n * and carries its own transport. On hosts declaring `output-artifacts` the\n * gist path additionally persists an offline viewer HTML artifact through\n * the public OutputArtifactAPI (2.6d consumption side); the command result\n * and its links stay untouched.\n */\n\nimport { spawn } from \"node:child_process\";\nimport { join } from \"node:path\";\nimport type {\n\tCapabilityAPI,\n\tCapabilityCommandContext,\n\tCapabilityCommandResult,\n\tDisposableRegistration,\n\tSessionExportArtifact,\n} from \"@agent-forge/plugin-sdk\";\nimport { renderSharedTranscriptView } from \"./share-viewer.ts\";\n\n/** Viewer render re-exported for consumers of this module (2.6d consumption side). */\nexport { renderSharedTranscriptView };\n\ntype ShareTarget = \"radius\" | \"gist\";\n\nexport interface ShareProcessResult {\n\tcode: number | null;\n\tstdout: string;\n\tstderr: string;\n}\n\nexport type ShareProcess = (\n\targv: readonly string[],\n\tinput: Uint8Array,\n\tsignal: AbortSignal,\n) => Promise<ShareProcessResult>;\n\n/** Injection seam for deterministic tests; production uses fetch and a real child process. */\nexport interface SessionShareCapabilityOptions {\n\tfetch?: typeof globalThis.fetch;\n\tprocess?: ShareProcess;\n\t/**\n\t * Agent-dir config file path of this capability. When present, the\n\t * unconfigured error names it so the user knows where to set the target;\n\t * the standalone example passes nothing and keeps its exact message.\n\t */\n\tconfigPath?: string;\n}\n\n/**\n * Config file channel (D-075 S4 second batch): the same agent-dir path the\n * host's `builtinCapabilityConfigPath(agentDir, \"builtin.session-share\")`\n * resolves to for the pre-split embedded capability. Hosts that declare\n * `api.host.agentDir` have their `<agentDir>/capabilities/` config file read\n * by the plugin entry and merged over the manifest config — the exact\n * merge-over-defaults semantics the embedded `withCapabilityConfig` channel\n * had. Keep in sync with the host constant.\n */\nexport const CAPABILITY_CONFIG_RELATIVE_PATH = join(\"capabilities\", \"builtin.session-share.json\");\n\nfunction configString(api: CapabilityAPI, key: string): string | undefined {\n\tconst value = api.config[key];\n\treturn typeof value === \"string\" && value.trim() ? value.trim() : undefined;\n}\n\nfunction targetOf(api: CapabilityAPI): ShareTarget | undefined {\n\tconst target = configString(api, \"target\");\n\treturn target === \"radius\" || target === \"gist\" ? target : undefined;\n}\n\nfunction defaultProcess(argv: readonly string[], input: Uint8Array, signal: AbortSignal): Promise<ShareProcessResult> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst child = spawn(argv[0] ?? \"\", argv.slice(1), { shell: false, stdio: [\"pipe\", \"pipe\", \"pipe\"] });\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\t\tlet settled = false;\n\t\tconst finish = (result: ShareProcessResult): void => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\tresolve(result);\n\t\t};\n\t\tconst onAbort = (): void => {\n\t\t\tif (!settled) child.kill(\"SIGTERM\");\n\t\t};\n\t\tchild.stdout.on(\"data\", (chunk) => {\n\t\t\tstdout += chunk.toString();\n\t\t});\n\t\tchild.stderr.on(\"data\", (chunk) => {\n\t\t\tstderr += chunk.toString();\n\t\t});\n\t\tchild.once(\"error\", reject);\n\t\tchild.once(\"close\", (code) => finish({ code, stdout, stderr }));\n\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t\tif (signal.aborted) onAbort();\n\t\tchild.stdin.end(input);\n\t});\n}\n\nfunction error(message: string): CapabilityCommandResult {\n\treturn { status: \"error\", message };\n}\n\nfunction success(message: string, url: string, label: string): CapabilityCommandResult {\n\treturn { status: \"success\", message, links: [{ label, url }] };\n}\n\nasync function shareViaRadius(\n\tapi: CapabilityAPI,\n\tartifact: SessionExportArtifact,\n\tcontext: CapabilityCommandContext,\n\tfetchImpl: typeof globalThis.fetch,\n): Promise<CapabilityCommandResult> {\n\tconst gateway = configString(api, \"gateway\");\n\tif (!gateway) return error(\"Session share target radius requires config.gateway\");\n\tif (!api.credentials) return error(\"Session share target radius requires the credentials capability\");\n\tconst credential = await api.credentials.resolve({\n\t\tproviderId: configString(api, \"credentialProvider\") ?? \"radius\",\n\t\tminValidityMs: 300_000,\n\t\tsignal: context.signal,\n\t});\n\tif (!credential?.apiKey) return error(\"No credential is available for the configured Radius provider\");\n\tconst url = new URL(\"/v1/artifacts\", gateway);\n\turl.searchParams.set(\"visibility\", \"organization\");\n\turl.searchParams.set(\"title\", \"Agent Forge session\");\n\tconst response = await fetchImpl(url, {\n\t\tmethod: \"POST\",\n\t\theaders: {\n\t\t\tAuthorization: `Bearer ${credential.apiKey}`,\n\t\t\t\"Content-Type\": artifact.mediaType,\n\t\t\t\"Content-Length\": String(artifact.bytes.byteLength),\n\t\t\t...credential.headers,\n\t\t},\n\t\tbody: artifact.bytes,\n\t\tsignal: context.signal,\n\t});\n\tconst body = (await response.json().catch(() => null)) as {\n\t\tartifact?: { canonical_url?: string };\n\t\terror?: string;\n\t} | null;\n\tif (!response.ok || !body?.artifact?.canonical_url) {\n\t\treturn error(`Failed to upload Radius artifact: ${body?.error ?? response.statusText ?? response.status}`);\n\t}\n\treturn success(\"Session shared through Radius\", body.artifact.canonical_url, \"Radius\");\n}\n\nasync function shareViaGist(\n\tapi: CapabilityAPI,\n\tartifact: SessionExportArtifact,\n\tcontext: CapabilityCommandContext,\n\tsessionId: string,\n\tprocessImpl: ShareProcess,\n): Promise<CapabilityCommandResult> {\n\tconst viewerUrl = configString(api, \"viewerUrl\");\n\tif (!viewerUrl) return error(\"Session share target gist requires config.viewerUrl\");\n\tconst auth = await processImpl([\"gh\", \"auth\", \"status\"], new Uint8Array(), context.signal);\n\tif (auth.code !== 0) return error(`GitHub CLI is not logged in: ${auth.stderr.trim() || \"run gh auth login first\"}`);\n\tconst result = await processImpl([\"gh\", \"gist\", \"create\", \"--public=false\", \"-\"], artifact.bytes, context.signal);\n\tif (result.code !== 0) return error(`Failed to create gist: ${result.stderr.trim() || \"unknown error\"}`);\n\tconst gistUrl = result.stdout.trim();\n\tconst gistId = gistUrl.split(\"/\").pop();\n\tif (!gistId) return error(\"Failed to parse gist ID from gh output\");\n\t// Offline viewer artifact through the public OutputArtifactAPI, mirroring\n\t// the observability precedent: hosts without `output-artifacts` leave\n\t// api.output undefined and the share alone is returned. Rejections\n\t// propagate — a failed artifact write is never silently swallowed.\n\tif (api.output) {\n\t\tconst viewer = renderSharedTranscriptView({\n\t\t\tshareId: gistId,\n\t\t\tsessionId,\n\t\t\ttranscriptLines: new TextDecoder()\n\t\t\t\t.decode(artifact.bytes)\n\t\t\t\t.split(\"\\n\")\n\t\t\t\t.filter((line) => line.length > 0),\n\t\t});\n\t\tawait api.output.put(\n\t\t\t{\n\t\t\t\tsessionId,\n\t\t\t\toperationId: `share-viewer-${gistId}`,\n\t\t\t\tkind: \"share-viewer\",\n\t\t\t\tmediaType: \"text/html\",\n\t\t\t\ttruncated: false,\n\t\t\t\tdata: new TextEncoder().encode(viewer.html),\n\t\t\t},\n\t\t\t{ signal: context.signal },\n\t\t);\n\t}\n\tconst preview = new URL(viewerUrl);\n\tpreview.hash = gistId;\n\treturn {\n\t\tstatus: \"success\",\n\t\tmessage: \"Session shared through GitHub gist\",\n\t\tlinks: [\n\t\t\t{ label: \"Viewer\", url: preview.toString() },\n\t\t\t{ label: \"Gist\", url: gistUrl },\n\t\t],\n\t};\n}\n\nexport function createCapability(\n\tapi: CapabilityAPI,\n\toptions: SessionShareCapabilityOptions = {},\n): readonly DisposableRegistration[] {\n\tconst unconfigured = (): CapabilityCommandResult =>\n\t\terror(\n\t\t\t`Configure plugin target as 'radius' or 'gist' before using /share${\n\t\t\t\toptions.configPath === undefined ? \"\" : ` (builtin config: ${options.configPath})`\n\t\t\t}`,\n\t\t);\n\tconst command = api.registerCommand({\n\t\tname: \"share\",\n\t\tdescription: \"Share the current session using the explicitly configured target\",\n\t\texecute: async (_args, context) => {\n\t\t\tconst target = targetOf(api);\n\t\t\tif (!target) return unconfigured();\n\t\t\tif (!api.session?.exportArtifact) return error(\"Session export capability is unavailable\");\n\t\t\tapi.session.appendEntry(\"agent-forge.share\", { target, version: 1 });\n\t\t\tconst artifact = api.session.exportArtifact({ format: \"jsonl\" });\n\t\t\treturn target === \"radius\"\n\t\t\t\t? shareViaRadius(api, artifact, context, options.fetch ?? globalThis.fetch)\n\t\t\t\t: shareViaGist(api, artifact, context, api.session.getSessionId(), options.process ?? defaultProcess);\n\t\t},\n\t});\n\treturn [command];\n}\n"]}
package/dist/index.js ADDED
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Session share capability plugin: shares the current session through an
3
+ * explicitly configured target (radius or gist), extracted from the host's
4
+ * `src/capabilities/session-share.ts` (D-075 S4, second B-class batch).
5
+ *
6
+ * Productized from the `session-share` example plugin (开发计划 §216): the
7
+ * sharing target must be configured explicitly and a missing target is a
8
+ * structured error — there is no implicit Radius/Gist fallback and no core
9
+ * `/share` special path. The plugin consumes the public `session`,
10
+ * `session-export`, `commands` and (optionally) `credentials` capabilities
11
+ * and carries its own transport. On hosts declaring `output-artifacts` the
12
+ * gist path additionally persists an offline viewer HTML artifact through
13
+ * the public OutputArtifactAPI (2.6d consumption side); the command result
14
+ * and its links stay untouched.
15
+ */
16
+ import { spawn } from "node:child_process";
17
+ import { join } from "node:path";
18
+ import { renderSharedTranscriptView } from "./share-viewer.js";
19
+ /** Viewer render re-exported for consumers of this module (2.6d consumption side). */
20
+ export { renderSharedTranscriptView };
21
+ /**
22
+ * Config file channel (D-075 S4 second batch): the same agent-dir path the
23
+ * host's `builtinCapabilityConfigPath(agentDir, "builtin.session-share")`
24
+ * resolves to for the pre-split embedded capability. Hosts that declare
25
+ * `api.host.agentDir` have their `<agentDir>/capabilities/` config file read
26
+ * by the plugin entry and merged over the manifest config — the exact
27
+ * merge-over-defaults semantics the embedded `withCapabilityConfig` channel
28
+ * had. Keep in sync with the host constant.
29
+ */
30
+ export const CAPABILITY_CONFIG_RELATIVE_PATH = join("capabilities", "builtin.session-share.json");
31
+ function configString(api, key) {
32
+ const value = api.config[key];
33
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
34
+ }
35
+ function targetOf(api) {
36
+ const target = configString(api, "target");
37
+ return target === "radius" || target === "gist" ? target : undefined;
38
+ }
39
+ function defaultProcess(argv, input, signal) {
40
+ return new Promise((resolve, reject) => {
41
+ const child = spawn(argv[0] ?? "", argv.slice(1), { shell: false, stdio: ["pipe", "pipe", "pipe"] });
42
+ let stdout = "";
43
+ let stderr = "";
44
+ let settled = false;
45
+ const finish = (result) => {
46
+ if (settled)
47
+ return;
48
+ settled = true;
49
+ signal.removeEventListener("abort", onAbort);
50
+ resolve(result);
51
+ };
52
+ const onAbort = () => {
53
+ if (!settled)
54
+ child.kill("SIGTERM");
55
+ };
56
+ child.stdout.on("data", (chunk) => {
57
+ stdout += chunk.toString();
58
+ });
59
+ child.stderr.on("data", (chunk) => {
60
+ stderr += chunk.toString();
61
+ });
62
+ child.once("error", reject);
63
+ child.once("close", (code) => finish({ code, stdout, stderr }));
64
+ signal.addEventListener("abort", onAbort, { once: true });
65
+ if (signal.aborted)
66
+ onAbort();
67
+ child.stdin.end(input);
68
+ });
69
+ }
70
+ function error(message) {
71
+ return { status: "error", message };
72
+ }
73
+ function success(message, url, label) {
74
+ return { status: "success", message, links: [{ label, url }] };
75
+ }
76
+ async function shareViaRadius(api, artifact, context, fetchImpl) {
77
+ const gateway = configString(api, "gateway");
78
+ if (!gateway)
79
+ return error("Session share target radius requires config.gateway");
80
+ if (!api.credentials)
81
+ return error("Session share target radius requires the credentials capability");
82
+ const credential = await api.credentials.resolve({
83
+ providerId: configString(api, "credentialProvider") ?? "radius",
84
+ minValidityMs: 300_000,
85
+ signal: context.signal,
86
+ });
87
+ if (!credential?.apiKey)
88
+ return error("No credential is available for the configured Radius provider");
89
+ const url = new URL("/v1/artifacts", gateway);
90
+ url.searchParams.set("visibility", "organization");
91
+ url.searchParams.set("title", "Agent Forge session");
92
+ const response = await fetchImpl(url, {
93
+ method: "POST",
94
+ headers: {
95
+ Authorization: `Bearer ${credential.apiKey}`,
96
+ "Content-Type": artifact.mediaType,
97
+ "Content-Length": String(artifact.bytes.byteLength),
98
+ ...credential.headers,
99
+ },
100
+ body: artifact.bytes,
101
+ signal: context.signal,
102
+ });
103
+ const body = (await response.json().catch(() => null));
104
+ if (!response.ok || !body?.artifact?.canonical_url) {
105
+ return error(`Failed to upload Radius artifact: ${body?.error ?? response.statusText ?? response.status}`);
106
+ }
107
+ return success("Session shared through Radius", body.artifact.canonical_url, "Radius");
108
+ }
109
+ async function shareViaGist(api, artifact, context, sessionId, processImpl) {
110
+ const viewerUrl = configString(api, "viewerUrl");
111
+ if (!viewerUrl)
112
+ return error("Session share target gist requires config.viewerUrl");
113
+ const auth = await processImpl(["gh", "auth", "status"], new Uint8Array(), context.signal);
114
+ if (auth.code !== 0)
115
+ return error(`GitHub CLI is not logged in: ${auth.stderr.trim() || "run gh auth login first"}`);
116
+ const result = await processImpl(["gh", "gist", "create", "--public=false", "-"], artifact.bytes, context.signal);
117
+ if (result.code !== 0)
118
+ return error(`Failed to create gist: ${result.stderr.trim() || "unknown error"}`);
119
+ const gistUrl = result.stdout.trim();
120
+ const gistId = gistUrl.split("/").pop();
121
+ if (!gistId)
122
+ return error("Failed to parse gist ID from gh output");
123
+ // Offline viewer artifact through the public OutputArtifactAPI, mirroring
124
+ // the observability precedent: hosts without `output-artifacts` leave
125
+ // api.output undefined and the share alone is returned. Rejections
126
+ // propagate — a failed artifact write is never silently swallowed.
127
+ if (api.output) {
128
+ const viewer = renderSharedTranscriptView({
129
+ shareId: gistId,
130
+ sessionId,
131
+ transcriptLines: new TextDecoder()
132
+ .decode(artifact.bytes)
133
+ .split("\n")
134
+ .filter((line) => line.length > 0),
135
+ });
136
+ await api.output.put({
137
+ sessionId,
138
+ operationId: `share-viewer-${gistId}`,
139
+ kind: "share-viewer",
140
+ mediaType: "text/html",
141
+ truncated: false,
142
+ data: new TextEncoder().encode(viewer.html),
143
+ }, { signal: context.signal });
144
+ }
145
+ const preview = new URL(viewerUrl);
146
+ preview.hash = gistId;
147
+ return {
148
+ status: "success",
149
+ message: "Session shared through GitHub gist",
150
+ links: [
151
+ { label: "Viewer", url: preview.toString() },
152
+ { label: "Gist", url: gistUrl },
153
+ ],
154
+ };
155
+ }
156
+ export function createCapability(api, options = {}) {
157
+ const unconfigured = () => error(`Configure plugin target as 'radius' or 'gist' before using /share${options.configPath === undefined ? "" : ` (builtin config: ${options.configPath})`}`);
158
+ const command = api.registerCommand({
159
+ name: "share",
160
+ description: "Share the current session using the explicitly configured target",
161
+ execute: async (_args, context) => {
162
+ const target = targetOf(api);
163
+ if (!target)
164
+ return unconfigured();
165
+ if (!api.session?.exportArtifact)
166
+ return error("Session export capability is unavailable");
167
+ api.session.appendEntry("agent-forge.share", { target, version: 1 });
168
+ const artifact = api.session.exportArtifact({ format: "jsonl" });
169
+ return target === "radius"
170
+ ? shareViaRadius(api, artifact, context, options.fetch ?? globalThis.fetch)
171
+ : shareViaGist(api, artifact, context, api.session.getSessionId(), options.process ?? defaultProcess);
172
+ },
173
+ });
174
+ return [command];
175
+ }
176
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Session share capability plugin: shares the current session through an\n * explicitly configured target (radius or gist), extracted from the host's\n * `src/capabilities/session-share.ts` (D-075 S4, second B-class batch).\n *\n * Productized from the `session-share` example plugin (开发计划 §216): the\n * sharing target must be configured explicitly and a missing target is a\n * structured error — there is no implicit Radius/Gist fallback and no core\n * `/share` special path. The plugin consumes the public `session`,\n * `session-export`, `commands` and (optionally) `credentials` capabilities\n * and carries its own transport. On hosts declaring `output-artifacts` the\n * gist path additionally persists an offline viewer HTML artifact through\n * the public OutputArtifactAPI (2.6d consumption side); the command result\n * and its links stay untouched.\n */\n\nimport { spawn } from \"node:child_process\";\nimport { join } from \"node:path\";\nimport type {\n\tCapabilityAPI,\n\tCapabilityCommandContext,\n\tCapabilityCommandResult,\n\tDisposableRegistration,\n\tSessionExportArtifact,\n} from \"@agent-forge/plugin-sdk\";\nimport { renderSharedTranscriptView } from \"./share-viewer.ts\";\n\n/** Viewer render re-exported for consumers of this module (2.6d consumption side). */\nexport { renderSharedTranscriptView };\n\ntype ShareTarget = \"radius\" | \"gist\";\n\nexport interface ShareProcessResult {\n\tcode: number | null;\n\tstdout: string;\n\tstderr: string;\n}\n\nexport type ShareProcess = (\n\targv: readonly string[],\n\tinput: Uint8Array,\n\tsignal: AbortSignal,\n) => Promise<ShareProcessResult>;\n\n/** Injection seam for deterministic tests; production uses fetch and a real child process. */\nexport interface SessionShareCapabilityOptions {\n\tfetch?: typeof globalThis.fetch;\n\tprocess?: ShareProcess;\n\t/**\n\t * Agent-dir config file path of this capability. When present, the\n\t * unconfigured error names it so the user knows where to set the target;\n\t * the standalone example passes nothing and keeps its exact message.\n\t */\n\tconfigPath?: string;\n}\n\n/**\n * Config file channel (D-075 S4 second batch): the same agent-dir path the\n * host's `builtinCapabilityConfigPath(agentDir, \"builtin.session-share\")`\n * resolves to for the pre-split embedded capability. Hosts that declare\n * `api.host.agentDir` have their `<agentDir>/capabilities/` config file read\n * by the plugin entry and merged over the manifest config — the exact\n * merge-over-defaults semantics the embedded `withCapabilityConfig` channel\n * had. Keep in sync with the host constant.\n */\nexport const CAPABILITY_CONFIG_RELATIVE_PATH = join(\"capabilities\", \"builtin.session-share.json\");\n\nfunction configString(api: CapabilityAPI, key: string): string | undefined {\n\tconst value = api.config[key];\n\treturn typeof value === \"string\" && value.trim() ? value.trim() : undefined;\n}\n\nfunction targetOf(api: CapabilityAPI): ShareTarget | undefined {\n\tconst target = configString(api, \"target\");\n\treturn target === \"radius\" || target === \"gist\" ? target : undefined;\n}\n\nfunction defaultProcess(argv: readonly string[], input: Uint8Array, signal: AbortSignal): Promise<ShareProcessResult> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst child = spawn(argv[0] ?? \"\", argv.slice(1), { shell: false, stdio: [\"pipe\", \"pipe\", \"pipe\"] });\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\t\tlet settled = false;\n\t\tconst finish = (result: ShareProcessResult): void => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\tresolve(result);\n\t\t};\n\t\tconst onAbort = (): void => {\n\t\t\tif (!settled) child.kill(\"SIGTERM\");\n\t\t};\n\t\tchild.stdout.on(\"data\", (chunk) => {\n\t\t\tstdout += chunk.toString();\n\t\t});\n\t\tchild.stderr.on(\"data\", (chunk) => {\n\t\t\tstderr += chunk.toString();\n\t\t});\n\t\tchild.once(\"error\", reject);\n\t\tchild.once(\"close\", (code) => finish({ code, stdout, stderr }));\n\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t\tif (signal.aborted) onAbort();\n\t\tchild.stdin.end(input);\n\t});\n}\n\nfunction error(message: string): CapabilityCommandResult {\n\treturn { status: \"error\", message };\n}\n\nfunction success(message: string, url: string, label: string): CapabilityCommandResult {\n\treturn { status: \"success\", message, links: [{ label, url }] };\n}\n\nasync function shareViaRadius(\n\tapi: CapabilityAPI,\n\tartifact: SessionExportArtifact,\n\tcontext: CapabilityCommandContext,\n\tfetchImpl: typeof globalThis.fetch,\n): Promise<CapabilityCommandResult> {\n\tconst gateway = configString(api, \"gateway\");\n\tif (!gateway) return error(\"Session share target radius requires config.gateway\");\n\tif (!api.credentials) return error(\"Session share target radius requires the credentials capability\");\n\tconst credential = await api.credentials.resolve({\n\t\tproviderId: configString(api, \"credentialProvider\") ?? \"radius\",\n\t\tminValidityMs: 300_000,\n\t\tsignal: context.signal,\n\t});\n\tif (!credential?.apiKey) return error(\"No credential is available for the configured Radius provider\");\n\tconst url = new URL(\"/v1/artifacts\", gateway);\n\turl.searchParams.set(\"visibility\", \"organization\");\n\turl.searchParams.set(\"title\", \"Agent Forge session\");\n\tconst response = await fetchImpl(url, {\n\t\tmethod: \"POST\",\n\t\theaders: {\n\t\t\tAuthorization: `Bearer ${credential.apiKey}`,\n\t\t\t\"Content-Type\": artifact.mediaType,\n\t\t\t\"Content-Length\": String(artifact.bytes.byteLength),\n\t\t\t...credential.headers,\n\t\t},\n\t\tbody: artifact.bytes,\n\t\tsignal: context.signal,\n\t});\n\tconst body = (await response.json().catch(() => null)) as {\n\t\tartifact?: { canonical_url?: string };\n\t\terror?: string;\n\t} | null;\n\tif (!response.ok || !body?.artifact?.canonical_url) {\n\t\treturn error(`Failed to upload Radius artifact: ${body?.error ?? response.statusText ?? response.status}`);\n\t}\n\treturn success(\"Session shared through Radius\", body.artifact.canonical_url, \"Radius\");\n}\n\nasync function shareViaGist(\n\tapi: CapabilityAPI,\n\tartifact: SessionExportArtifact,\n\tcontext: CapabilityCommandContext,\n\tsessionId: string,\n\tprocessImpl: ShareProcess,\n): Promise<CapabilityCommandResult> {\n\tconst viewerUrl = configString(api, \"viewerUrl\");\n\tif (!viewerUrl) return error(\"Session share target gist requires config.viewerUrl\");\n\tconst auth = await processImpl([\"gh\", \"auth\", \"status\"], new Uint8Array(), context.signal);\n\tif (auth.code !== 0) return error(`GitHub CLI is not logged in: ${auth.stderr.trim() || \"run gh auth login first\"}`);\n\tconst result = await processImpl([\"gh\", \"gist\", \"create\", \"--public=false\", \"-\"], artifact.bytes, context.signal);\n\tif (result.code !== 0) return error(`Failed to create gist: ${result.stderr.trim() || \"unknown error\"}`);\n\tconst gistUrl = result.stdout.trim();\n\tconst gistId = gistUrl.split(\"/\").pop();\n\tif (!gistId) return error(\"Failed to parse gist ID from gh output\");\n\t// Offline viewer artifact through the public OutputArtifactAPI, mirroring\n\t// the observability precedent: hosts without `output-artifacts` leave\n\t// api.output undefined and the share alone is returned. Rejections\n\t// propagate — a failed artifact write is never silently swallowed.\n\tif (api.output) {\n\t\tconst viewer = renderSharedTranscriptView({\n\t\t\tshareId: gistId,\n\t\t\tsessionId,\n\t\t\ttranscriptLines: new TextDecoder()\n\t\t\t\t.decode(artifact.bytes)\n\t\t\t\t.split(\"\\n\")\n\t\t\t\t.filter((line) => line.length > 0),\n\t\t});\n\t\tawait api.output.put(\n\t\t\t{\n\t\t\t\tsessionId,\n\t\t\t\toperationId: `share-viewer-${gistId}`,\n\t\t\t\tkind: \"share-viewer\",\n\t\t\t\tmediaType: \"text/html\",\n\t\t\t\ttruncated: false,\n\t\t\t\tdata: new TextEncoder().encode(viewer.html),\n\t\t\t},\n\t\t\t{ signal: context.signal },\n\t\t);\n\t}\n\tconst preview = new URL(viewerUrl);\n\tpreview.hash = gistId;\n\treturn {\n\t\tstatus: \"success\",\n\t\tmessage: \"Session shared through GitHub gist\",\n\t\tlinks: [\n\t\t\t{ label: \"Viewer\", url: preview.toString() },\n\t\t\t{ label: \"Gist\", url: gistUrl },\n\t\t],\n\t};\n}\n\nexport function createCapability(\n\tapi: CapabilityAPI,\n\toptions: SessionShareCapabilityOptions = {},\n): readonly DisposableRegistration[] {\n\tconst unconfigured = (): CapabilityCommandResult =>\n\t\terror(\n\t\t\t`Configure plugin target as 'radius' or 'gist' before using /share${\n\t\t\t\toptions.configPath === undefined ? \"\" : ` (builtin config: ${options.configPath})`\n\t\t\t}`,\n\t\t);\n\tconst command = api.registerCommand({\n\t\tname: \"share\",\n\t\tdescription: \"Share the current session using the explicitly configured target\",\n\t\texecute: async (_args, context) => {\n\t\t\tconst target = targetOf(api);\n\t\t\tif (!target) return unconfigured();\n\t\t\tif (!api.session?.exportArtifact) return error(\"Session export capability is unavailable\");\n\t\t\tapi.session.appendEntry(\"agent-forge.share\", { target, version: 1 });\n\t\t\tconst artifact = api.session.exportArtifact({ format: \"jsonl\" });\n\t\t\treturn target === \"radius\"\n\t\t\t\t? shareViaRadius(api, artifact, context, options.fetch ?? globalThis.fetch)\n\t\t\t\t: shareViaGist(api, artifact, context, api.session.getSessionId(), options.process ?? defaultProcess);\n\t\t},\n\t});\n\treturn [command];\n}\n"]}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Session share viewer render (inlined from the host's
3
+ * `src/share/share-viewer.ts`, D-075 S4 second batch): renders one
4
+ * already-shared transcript artifact into the deterministic, HTML-escaped
5
+ * viewer document the `/share` gist path persists through the public
6
+ * OutputArtifactAPI (2.6d consumption side).
7
+ *
8
+ * Sync obligations: keep byte-compatible with the host module (the host keeps
9
+ * the full 2.6c/2.6d contract + viewer face for its own public exports; this
10
+ * copy carries only the transcript-projection consumption path). The host
11
+ * `JsonValue` typing is narrowed to the SDK's `PluginConfigValue` — the
12
+ * payloads built here are JSON-safe plain objects, so the shape contract is
13
+ * unchanged.
14
+ */
15
+ import type { PluginConfigValue } from "@agent-forge/plugin-sdk";
16
+ export interface ShareViewV1 {
17
+ readonly shareId: string;
18
+ readonly state: "resolved" | "failed";
19
+ readonly kind?: string;
20
+ readonly html: string;
21
+ readonly payload: PluginConfigValue;
22
+ }
23
+ /**
24
+ * Renders one already-shared transcript artifact (e.g. the session JSONL a
25
+ * `/share` target published) into the viewer document. Callers hand over
26
+ * primitive shared-artifact facts and the synthesis of the render input stays
27
+ * inside the viewer module. The synthetic contract only drives the shareId
28
+ * into the rendered markup and payload; `createdAt` is never rendered, so
29
+ * callers pass no timestamp.
30
+ */
31
+ export declare function renderSharedTranscriptView(input: {
32
+ readonly shareId: string;
33
+ readonly sessionId: string;
34
+ readonly transcriptLines: readonly string[];
35
+ }): ShareViewV1;
36
+ //# sourceMappingURL=share-viewer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"share-viewer.d.ts","sourceRoot":"","sources":["../src/share-viewer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAcjE,MAAM,WAAW,WAAW;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,UAAU,GAAG,QAAQ,CAAC;IACtC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAC;CACpC;AAWD;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,EAAE;IACjD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;CAC5C,GAAG,WAAW,CA0Bd","sourcesContent":["/**\n * Session share viewer render (inlined from the host's\n * `src/share/share-viewer.ts`, D-075 S4 second batch): renders one\n * already-shared transcript artifact into the deterministic, HTML-escaped\n * viewer document the `/share` gist path persists through the public\n * OutputArtifactAPI (2.6d consumption side).\n *\n * Sync obligations: keep byte-compatible with the host module (the host keeps\n * the full 2.6c/2.6d contract + viewer face for its own public exports; this\n * copy carries only the transcript-projection consumption path). The host\n * `JsonValue` typing is narrowed to the SDK's `PluginConfigValue` — the\n * payloads built here are JSON-safe plain objects, so the shape contract is\n * unchanged.\n */\nimport type { PluginConfigValue } from \"@agent-forge/plugin-sdk\";\n\n/** Minimal synthetic share-contract shape driving the shareId into the markup. */\ninterface ShareContractView {\n\treadonly shareId: string;\n}\n\n/** Minimal resolved-result shape: the transcript projection is always a resolved messages result. */\ninterface ShareResolvedView {\n\treadonly status: \"resolved\";\n\treadonly kind: \"messages\";\n\treadonly content: PluginConfigValue;\n}\n\nexport interface ShareViewV1 {\n\treadonly shareId: string;\n\treadonly state: \"resolved\" | \"failed\";\n\treadonly kind?: string;\n\treadonly html: string;\n\treadonly payload: PluginConfigValue;\n}\n\nfunction escapeHtml(value: string): string {\n\treturn value\n\t\t.replaceAll(\"&\", \"&amp;\")\n\t\t.replaceAll(\"<\", \"&lt;\")\n\t\t.replaceAll(\">\", \"&gt;\")\n\t\t.replaceAll('\"', \"&quot;\")\n\t\t.replaceAll(\"'\", \"&#39;\");\n}\n\n/**\n * Renders one already-shared transcript artifact (e.g. the session JSONL a\n * `/share` target published) into the viewer document. Callers hand over\n * primitive shared-artifact facts and the synthesis of the render input stays\n * inside the viewer module. The synthetic contract only drives the shareId\n * into the rendered markup and payload; `createdAt` is never rendered, so\n * callers pass no timestamp.\n */\nexport function renderSharedTranscriptView(input: {\n\treadonly shareId: string;\n\treadonly sessionId: string;\n\treadonly transcriptLines: readonly string[];\n}): ShareViewV1 {\n\tconst contract: ShareContractView = { shareId: input.shareId };\n\t// The transcript projection is always a resolved messages result: denied\n\t// and not-found states only exist in the host's full 2.6d viewer face,\n\t// which the /share consumption path never produces.\n\tconst result: ShareResolvedView = {\n\t\tstatus: \"resolved\",\n\t\tkind: \"messages\",\n\t\tcontent: { sessionId: input.sessionId, turns: [...input.transcriptLines] },\n\t};\n\n\tconst content = result.content as { readonly turns?: readonly string[] };\n\tconst turns = Array.isArray(content.turns) ? content.turns : [];\n\tconst bodyHtml = [\n\t\t`<h1>Shared messages</h1>`,\n\t\t`<ol class=\"share-turns\">`,\n\t\t...turns.map((turn) => `<li>${escapeHtml(String(turn))}</li>`),\n\t\t`</ol>`,\n\t].join(\"\\n\");\n\treturn {\n\t\tshareId: contract.shareId,\n\t\tstate: \"resolved\",\n\t\tkind: result.kind,\n\t\thtml: `<article class=\"share-view\" data-share=\"${escapeHtml(contract.shareId)}\">\\n${bodyHtml}\\n</article>`,\n\t\tpayload: { shareId: contract.shareId, state: \"resolved\", kind: \"messages\", turns: [...turns] },\n\t};\n}\n"]}
@@ -0,0 +1,43 @@
1
+ function escapeHtml(value) {
2
+ return value
3
+ .replaceAll("&", "&amp;")
4
+ .replaceAll("<", "&lt;")
5
+ .replaceAll(">", "&gt;")
6
+ .replaceAll('"', "&quot;")
7
+ .replaceAll("'", "&#39;");
8
+ }
9
+ /**
10
+ * Renders one already-shared transcript artifact (e.g. the session JSONL a
11
+ * `/share` target published) into the viewer document. Callers hand over
12
+ * primitive shared-artifact facts and the synthesis of the render input stays
13
+ * inside the viewer module. The synthetic contract only drives the shareId
14
+ * into the rendered markup and payload; `createdAt` is never rendered, so
15
+ * callers pass no timestamp.
16
+ */
17
+ export function renderSharedTranscriptView(input) {
18
+ const contract = { shareId: input.shareId };
19
+ // The transcript projection is always a resolved messages result: denied
20
+ // and not-found states only exist in the host's full 2.6d viewer face,
21
+ // which the /share consumption path never produces.
22
+ const result = {
23
+ status: "resolved",
24
+ kind: "messages",
25
+ content: { sessionId: input.sessionId, turns: [...input.transcriptLines] },
26
+ };
27
+ const content = result.content;
28
+ const turns = Array.isArray(content.turns) ? content.turns : [];
29
+ const bodyHtml = [
30
+ `<h1>Shared messages</h1>`,
31
+ `<ol class="share-turns">`,
32
+ ...turns.map((turn) => `<li>${escapeHtml(String(turn))}</li>`),
33
+ `</ol>`,
34
+ ].join("\n");
35
+ return {
36
+ shareId: contract.shareId,
37
+ state: "resolved",
38
+ kind: result.kind,
39
+ html: `<article class="share-view" data-share="${escapeHtml(contract.shareId)}">\n${bodyHtml}\n</article>`,
40
+ payload: { shareId: contract.shareId, state: "resolved", kind: "messages", turns: [...turns] },
41
+ };
42
+ }
43
+ //# sourceMappingURL=share-viewer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"share-viewer.js","sourceRoot":"","sources":["../src/share-viewer.ts"],"names":[],"mappings":"AAoCA,SAAS,UAAU,CAAC,KAAa,EAAU;IAC1C,OAAO,KAAK;SACV,UAAU,CAAC,GAAG,EAAE,OAAO,CAAC;SACxB,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC;SACvB,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC;SACvB,UAAU,CAAC,GAAG,EAAE,QAAQ,CAAC;SACzB,UAAU,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;AAAA,CAC3B;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,0BAA0B,CAAC,KAI1C,EAAe;IACf,MAAM,QAAQ,GAAsB,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC;IAC/D,yEAAyE;IACzE,uEAAuE;IACvE,oDAAoD;IACpD,MAAM,MAAM,GAAsB;QACjC,MAAM,EAAE,UAAU;QAClB,IAAI,EAAE,UAAU;QAChB,OAAO,EAAE,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,KAAK,EAAE,CAAC,GAAG,KAAK,CAAC,eAAe,CAAC,EAAE;KAC1E,CAAC;IAEF,MAAM,OAAO,GAAG,MAAM,CAAC,OAAiD,CAAC;IACzE,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;IAChE,MAAM,QAAQ,GAAG;QAChB,0BAA0B;QAC1B,0BAA0B;QAC1B,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC;QAC9D,OAAO;KACP,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACb,OAAO;QACN,OAAO,EAAE,QAAQ,CAAC,OAAO;QACzB,KAAK,EAAE,UAAU;QACjB,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,IAAI,EAAE,2CAA2C,UAAU,CAAC,QAAQ,CAAC,OAAO,CAAC,OAAO,QAAQ,cAAc;QAC1G,OAAO,EAAE,EAAE,OAAO,EAAE,QAAQ,CAAC,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,GAAG,KAAK,CAAC,EAAE;KAC9F,CAAC;AAAA,CACF","sourcesContent":["/**\n * Session share viewer render (inlined from the host's\n * `src/share/share-viewer.ts`, D-075 S4 second batch): renders one\n * already-shared transcript artifact into the deterministic, HTML-escaped\n * viewer document the `/share` gist path persists through the public\n * OutputArtifactAPI (2.6d consumption side).\n *\n * Sync obligations: keep byte-compatible with the host module (the host keeps\n * the full 2.6c/2.6d contract + viewer face for its own public exports; this\n * copy carries only the transcript-projection consumption path). The host\n * `JsonValue` typing is narrowed to the SDK's `PluginConfigValue` — the\n * payloads built here are JSON-safe plain objects, so the shape contract is\n * unchanged.\n */\nimport type { PluginConfigValue } from \"@agent-forge/plugin-sdk\";\n\n/** Minimal synthetic share-contract shape driving the shareId into the markup. */\ninterface ShareContractView {\n\treadonly shareId: string;\n}\n\n/** Minimal resolved-result shape: the transcript projection is always a resolved messages result. */\ninterface ShareResolvedView {\n\treadonly status: \"resolved\";\n\treadonly kind: \"messages\";\n\treadonly content: PluginConfigValue;\n}\n\nexport interface ShareViewV1 {\n\treadonly shareId: string;\n\treadonly state: \"resolved\" | \"failed\";\n\treadonly kind?: string;\n\treadonly html: string;\n\treadonly payload: PluginConfigValue;\n}\n\nfunction escapeHtml(value: string): string {\n\treturn value\n\t\t.replaceAll(\"&\", \"&amp;\")\n\t\t.replaceAll(\"<\", \"&lt;\")\n\t\t.replaceAll(\">\", \"&gt;\")\n\t\t.replaceAll('\"', \"&quot;\")\n\t\t.replaceAll(\"'\", \"&#39;\");\n}\n\n/**\n * Renders one already-shared transcript artifact (e.g. the session JSONL a\n * `/share` target published) into the viewer document. Callers hand over\n * primitive shared-artifact facts and the synthesis of the render input stays\n * inside the viewer module. The synthetic contract only drives the shareId\n * into the rendered markup and payload; `createdAt` is never rendered, so\n * callers pass no timestamp.\n */\nexport function renderSharedTranscriptView(input: {\n\treadonly shareId: string;\n\treadonly sessionId: string;\n\treadonly transcriptLines: readonly string[];\n}): ShareViewV1 {\n\tconst contract: ShareContractView = { shareId: input.shareId };\n\t// The transcript projection is always a resolved messages result: denied\n\t// and not-found states only exist in the host's full 2.6d viewer face,\n\t// which the /share consumption path never produces.\n\tconst result: ShareResolvedView = {\n\t\tstatus: \"resolved\",\n\t\tkind: \"messages\",\n\t\tcontent: { sessionId: input.sessionId, turns: [...input.transcriptLines] },\n\t};\n\n\tconst content = result.content as { readonly turns?: readonly string[] };\n\tconst turns = Array.isArray(content.turns) ? content.turns : [];\n\tconst bodyHtml = [\n\t\t`<h1>Shared messages</h1>`,\n\t\t`<ol class=\"share-turns\">`,\n\t\t...turns.map((turn) => `<li>${escapeHtml(String(turn))}</li>`),\n\t\t`</ol>`,\n\t].join(\"\\n\");\n\treturn {\n\t\tshareId: contract.shareId,\n\t\tstate: \"resolved\",\n\t\tkind: result.kind,\n\t\thtml: `<article class=\"share-view\" data-share=\"${escapeHtml(contract.shareId)}\">\\n${bodyHtml}\\n</article>`,\n\t\tpayload: { shareId: contract.shareId, state: \"resolved\", kind: \"messages\", turns: [...turns] },\n\t};\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@ai-agent-forge/plugin-session-share",
3
+ "version": "0.85.0",
4
+ "description": "Session share capability plugin for Agent Forge (explicit radius/gist /share targets)",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "agent-forge.json",
17
+ "plugin.json",
18
+ "README.md"
19
+ ],
20
+ "scripts": {
21
+ "clean": "shx rm -rf dist",
22
+ "build": "tsgo -p tsconfig.build.json",
23
+ "test": "vitest --run",
24
+ "prepublishOnly": "npm run clean && npm run build"
25
+ },
26
+ "keywords": [
27
+ "agent-forge",
28
+ "plugin",
29
+ "session-share"
30
+ ],
31
+ "author": "Agent Forge contributors",
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/agent-forge/agent-forge.git",
36
+ "directory": "plugins/session-share"
37
+ },
38
+ "bugs": {
39
+ "url": "https://github.com/agent-forge/agent-forge/issues"
40
+ },
41
+ "homepage": "https://github.com/agent-forge/agent-forge#readme",
42
+ "engines": {
43
+ "node": ">=22.19.0"
44
+ },
45
+ "dependencies": {
46
+ "@agent-forge/plugin-sdk": "^0.85.0"
47
+ },
48
+ "devDependencies": {
49
+ "@agent-forge/agent-forge": "^0.85.0",
50
+ "shx": "0.4.0",
51
+ "vitest": "4.1.9"
52
+ }
53
+ }
package/plugin.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "id": "agent-forge.plugin.session-share",
3
+ "version": "0.84.4",
4
+ "apiVersion": "1-draft",
5
+ "entry": "./dist/entry.js",
6
+ "hostVersion": ">=0.84.4",
7
+ "requiredCapabilities": ["session", "session-export", "commands"],
8
+ "optionalCapabilities": ["credentials"]
9
+ }