@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 +16 -0
- package/agent-forge.json +11 -0
- package/dist/entry.d.ts +23 -0
- package/dist/entry.d.ts.map +1 -0
- package/dist/entry.js +68 -0
- package/dist/entry.js.map +1 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +176 -0
- package/dist/index.js.map +1 -0
- package/dist/share-viewer.d.ts +36 -0
- package/dist/share-viewer.d.ts.map +1 -0
- package/dist/share-viewer.js +43 -0
- package/dist/share-viewer.js.map +1 -0
- package/package.json +53 -0
- package/plugin.json +9 -0
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`.
|
package/agent-forge.json
ADDED
package/dist/entry.d.ts
ADDED
|
@@ -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"]}
|
package/dist/index.d.ts
ADDED
|
@@ -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(\"&\", \"&\")\n\t\t.replaceAll(\"<\", \"<\")\n\t\t.replaceAll(\">\", \">\")\n\t\t.replaceAll('\"', \""\")\n\t\t.replaceAll(\"'\", \"'\");\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("&", "&")
|
|
4
|
+
.replaceAll("<", "<")
|
|
5
|
+
.replaceAll(">", ">")
|
|
6
|
+
.replaceAll('"', """)
|
|
7
|
+
.replaceAll("'", "'");
|
|
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(\"&\", \"&\")\n\t\t.replaceAll(\"<\", \"<\")\n\t\t.replaceAll(\">\", \">\")\n\t\t.replaceAll('\"', \""\")\n\t\t.replaceAll(\"'\", \"'\");\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
|
+
}
|