agent-relay-server 0.122.0 → 0.123.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/docs/openapi.json +1 -1
- package/package.json +7 -8
- package/public/assets/{MessageBubble-DRdUD-kW.js → MessageBubble-BTeFExjj.js} +2 -2
- package/public/assets/{MessageBubble-DRdUD-kW.js.map → MessageBubble-BTeFExjj.js.map} +1 -1
- package/public/assets/{PlanGraphPanel-siKMPCln.js → PlanGraphPanel-jU1DXg7w.js} +2 -2
- package/public/assets/{PlanGraphPanel-siKMPCln.js.map → PlanGraphPanel-jU1DXg7w.js.map} +1 -1
- package/public/assets/{activity-D3MNcSaV.js → activity-DUig2iYW.js} +2 -2
- package/public/assets/{activity-D3MNcSaV.js.map → activity-DUig2iYW.js.map} +1 -1
- package/public/assets/{agent-profiles-CKzttq6G.js → agent-profiles-D-oFmb6S.js} +2 -2
- package/public/assets/{agent-profiles-CKzttq6G.js.map → agent-profiles-D-oFmb6S.js.map} +1 -1
- package/public/assets/{agent-type-icon-0ZJRP1Vy.js → agent-type-icon-Dan2l3Tm.js} +2 -2
- package/public/assets/{agent-type-icon-0ZJRP1Vy.js.map → agent-type-icon-Dan2l3Tm.js.map} +1 -1
- package/public/assets/{agents-CuVj_rfG.js → agents-BkC6mcTh.js} +2 -2
- package/public/assets/{agents-CuVj_rfG.js.map → agents-BkC6mcTh.js.map} +1 -1
- package/public/assets/{analytics-iob6C27d.js → analytics-C3Dlmqe9.js} +2 -2
- package/public/assets/{analytics-iob6C27d.js.map → analytics-C3Dlmqe9.js.map} +1 -1
- package/public/assets/{automation-B2ixxs4q.js → automation-AeMZ70WQ.js} +2 -2
- package/public/assets/{automation-B2ixxs4q.js.map → automation-AeMZ70WQ.js.map} +1 -1
- package/public/assets/{branch-state-badge-CafBJWKo.js → branch-state-badge-CNW10MTC.js} +2 -2
- package/public/assets/{branch-state-badge-CafBJWKo.js.map → branch-state-badge-CNW10MTC.js.map} +1 -1
- package/public/assets/{channels-C-WsbL2H.js → channels-Bc_mEd56.js} +2 -2
- package/public/assets/{channels-C-WsbL2H.js.map → channels-Bc_mEd56.js.map} +1 -1
- package/public/assets/{chat-DeoVTVob.js → chat-BCNCuTgw.js} +2 -2
- package/public/assets/{chat-DeoVTVob.js.map → chat-BCNCuTgw.js.map} +1 -1
- package/public/assets/{connectors-C2Ac03LJ.js → connectors-h0d5Mc6e.js} +2 -2
- package/public/assets/{connectors-C2Ac03LJ.js.map → connectors-h0d5Mc6e.js.map} +1 -1
- package/public/assets/{display-CJzSoTMq.js → display-GFbGUG0O.js} +2 -2
- package/public/assets/{display-CJzSoTMq.js.map → display-GFbGUG0O.js.map} +1 -1
- package/public/assets/{formatted-body-35XBOY5L.js → formatted-body-CVr3Iev9.js} +3 -3
- package/public/assets/{formatted-body-35XBOY5L.js.map → formatted-body-CVr3Iev9.js.map} +1 -1
- package/public/assets/{formatted-body-impl-RmDxI5Ux.js → formatted-body-impl-Cq5SRGpm.js} +2 -2
- package/public/assets/{formatted-body-impl-RmDxI5Ux.js.map → formatted-body-impl-Cq5SRGpm.js.map} +1 -1
- package/public/assets/{index-D0xMQQb_.js → index--9UMzCFf.js} +6 -6
- package/public/assets/{index-D0xMQQb_.js.map → index--9UMzCFf.js.map} +1 -1
- package/public/assets/{insights-B643SFvm.js → insights-C8lIBXg3.js} +2 -2
- package/public/assets/{insights-B643SFvm.js.map → insights-C8lIBXg3.js.map} +1 -1
- package/public/assets/{integrations-hFIHeCF4.js → integrations-7OFsJtY7.js} +2 -2
- package/public/assets/{integrations-hFIHeCF4.js.map → integrations-7OFsJtY7.js.map} +1 -1
- package/public/assets/{maintenance-CURvdp_Z.js → maintenance-HDpg20OY.js} +2 -2
- package/public/assets/{maintenance-CURvdp_Z.js.map → maintenance-HDpg20OY.js.map} +1 -1
- package/public/assets/{managed-agents-DrZtdlSn.js → managed-agents-muCIMxng.js} +2 -2
- package/public/assets/{managed-agents-DrZtdlSn.js.map → managed-agents-muCIMxng.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-CVDO--ek.js → markdown-preview-impl-Cx7pCfA6.js} +2 -2
- package/public/assets/{markdown-preview-impl-CVDO--ek.js.map → markdown-preview-impl-Cx7pCfA6.js.map} +1 -1
- package/public/assets/{memory-Bm7dt_Qf.js → memory-CTO1FfJ-.js} +2 -2
- package/public/assets/{memory-Bm7dt_Qf.js.map → memory-CTO1FfJ-.js.map} +1 -1
- package/public/assets/{messages-DTq4i2KZ.js → messages-RWSXG2UM.js} +2 -2
- package/public/assets/{messages-DTq4i2KZ.js.map → messages-RWSXG2UM.js.map} +1 -1
- package/public/assets/{orchestrators-Dra318Ap.js → orchestrators-5aiYw3yx.js} +2 -2
- package/public/assets/{orchestrators-Dra318Ap.js.map → orchestrators-5aiYw3yx.js.map} +1 -1
- package/public/assets/{overview-yr1Vmk64.js → overview-Du6XLt5l.js} +2 -2
- package/public/assets/{overview-yr1Vmk64.js.map → overview-Du6XLt5l.js.map} +1 -1
- package/public/assets/{pairs-mnlKDBGp.js → pairs-BDRnlEta.js} +2 -2
- package/public/assets/{pairs-mnlKDBGp.js.map → pairs-BDRnlEta.js.map} +1 -1
- package/public/assets/{projects-Dzdf-gkK.js → projects-DwsWE9es.js} +2 -2
- package/public/assets/{projects-Dzdf-gkK.js.map → projects-DwsWE9es.js.map} +1 -1
- package/public/assets/{prompt-settings-ByL8SNCi.js → prompt-settings-CaJX_LbA.js} +2 -2
- package/public/assets/{prompt-settings-ByL8SNCi.js.map → prompt-settings-CaJX_LbA.js.map} +1 -1
- package/public/assets/{registry-CXWyOtpr.js → registry-C-uIL0Nl.js} +2 -2
- package/public/assets/{registry-CXWyOtpr.js.map → registry-C-uIL0Nl.js.map} +1 -1
- package/public/assets/{security-0SBsc_4W.js → security-BpVrdYhJ.js} +2 -2
- package/public/assets/{security-0SBsc_4W.js.map → security-BpVrdYhJ.js.map} +1 -1
- package/public/assets/{select-Bw5z2KJ_.js → select-DallbU93.js} +2 -2
- package/public/assets/{select-Bw5z2KJ_.js.map → select-DallbU93.js.map} +1 -1
- package/public/assets/{settings-78Cyq_tA.js → settings-BUQirdyI.js} +2 -2
- package/public/assets/{settings-78Cyq_tA.js.map → settings-BUQirdyI.js.map} +1 -1
- package/public/assets/{store-BPrVuKiv.js → store-DMZSTozC.js} +2 -2
- package/public/assets/{store-BPrVuKiv.js.map → store-DMZSTozC.js.map} +1 -1
- package/public/assets/{tasks-Cg2MMeri.js → tasks-k8mQDfKy.js} +2 -2
- package/public/assets/{tasks-Cg2MMeri.js.map → tasks-k8mQDfKy.js.map} +1 -1
- package/public/assets/{teams-DWv6UwFV.js → teams-DI-Lhhbl.js} +2 -2
- package/public/assets/{teams-DWv6UwFV.js.map → teams-DI-Lhhbl.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-DBLDjbIg.js → terminal-viewer-impl-BL853Fpi.js} +2 -2
- package/public/assets/{terminal-viewer-impl-DBLDjbIg.js.map → terminal-viewer-impl-BL853Fpi.js.map} +1 -1
- package/public/assets/types-CRptbSYh.js +2 -0
- package/public/assets/types-CRptbSYh.js.map +1 -0
- package/public/assets/{user-avatar-BsOLyVJj.js → user-avatar-D9xyGi3u.js} +2 -2
- package/public/assets/{user-avatar-BsOLyVJj.js.map → user-avatar-D9xyGi3u.js.map} +1 -1
- package/public/assets/{work-queue-DmwvnF6F.js → work-queue-DrLIdvxB.js} +2 -2
- package/public/assets/{work-queue-DmwvnF6F.js.map → work-queue-DrLIdvxB.js.map} +1 -1
- package/public/assets/{workspaces-dIBivJN8.js → workspaces-CnNL6T8Q.js} +2 -2
- package/public/assets/{workspaces-dIBivJN8.js.map → workspaces-CnNL6T8Q.js.map} +1 -1
- package/public/index.html +6 -6
- package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/runner/src/adapters/claude-delivery.ts +127 -0
- package/runner/src/adapters/claude-quota-harvest.ts +92 -0
- package/runner/src/adapters/claude-session-probe.ts +225 -0
- package/runner/src/adapters/claude-status-detectors.ts +147 -0
- package/runner/src/adapters/claude-transcript-tail.ts +128 -0
- package/runner/src/adapters/claude-transcript.ts +414 -0
- package/runner/src/adapters/claude.ts +675 -0
- package/runner/src/adapters/codex-agent-message-capture.ts +139 -0
- package/runner/src/adapters/codex-client.ts +279 -0
- package/runner/src/adapters/codex.ts +1345 -0
- package/runner/src/attachment-cache.ts +189 -0
- package/runner/src/busy-reconciler.ts +102 -0
- package/runner/src/claim-tracker.ts +140 -0
- package/runner/src/claude-prompt-gates.ts +268 -0
- package/runner/src/codex-version.ts +63 -0
- package/runner/src/config.ts +7 -0
- package/runner/src/context-probe-state.ts +6 -0
- package/runner/src/continuation-archive.ts +179 -0
- package/runner/src/control-server.ts +639 -0
- package/runner/src/index.ts +396 -0
- package/runner/src/launch-assembly.ts +674 -0
- package/runner/src/liveness.ts +50 -0
- package/runner/src/logger.ts +99 -0
- package/runner/src/mcp-outbox.ts +120 -0
- package/runner/src/message-body-config/config.ts +3 -0
- package/runner/src/message-body-config.ts +1 -0
- package/runner/src/native-self-resume.ts +202 -0
- package/runner/src/outbox.ts +342 -0
- package/runner/src/process-meta.ts +9 -0
- package/runner/src/profile-home.ts +260 -0
- package/runner/src/profile-projection.ts +324 -0
- package/runner/src/providers.ts +20 -0
- package/runner/src/provisioning.ts +368 -0
- package/runner/src/quota.ts +40 -0
- package/runner/src/rate-limit.ts +189 -0
- package/runner/src/relay-injection-events.ts +105 -0
- package/runner/src/relay-instructions.ts +60 -0
- package/runner/src/relay-mcp-proxy.ts +383 -0
- package/runner/src/relay-mcp.ts +156 -0
- package/runner/src/reply-obligation-cache.ts +136 -0
- package/runner/src/response-capture-report.ts +63 -0
- package/runner/src/runner-core.ts +2409 -0
- package/runner/src/runner-helpers.ts +435 -0
- package/runner/src/runner-insights.ts +105 -0
- package/runner/src/runner.ts +14 -0
- package/runner/src/session-destroy.ts +22 -0
- package/runner/src/session-insights.ts +118 -0
- package/runner/src/session-scratch.ts +271 -0
- package/runner/src/template-resolver.ts +29 -0
- package/runner/src/version.ts +44 -0
- package/src/config-store.ts +2 -0
- package/src/db/provisioning-registry.ts +40 -9
- package/src/db/provisioning-resolve.ts +66 -5
- package/src/gate-resolver.ts +7 -12
- package/src/routes/provisioning-registry.ts +1 -1
- package/src/services/spawn-agent.ts +1 -0
- package/public/assets/types-BeJUSfDj.js +0 -2
- package/public/assets/types-BeJUSfDj.js.map +0 -1
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Provider-agnostic core for the #184 context-gathering signal (epic #183).
|
|
2
|
+
//
|
|
3
|
+
// The transcript *format* is provider-specific (Claude JSONL, Codex app-server items,
|
|
4
|
+
// future providers), so each adapter normalizes its session into the same `SessionEvent`
|
|
5
|
+
// stream via `collectSessionEvents`. Everything downstream — the gathering/action
|
|
6
|
+
// classifier and the ratio math — lives here once and is shared, so a tool reclassified
|
|
7
|
+
// for one provider is reclassified for all, and a new provider only implements the
|
|
8
|
+
// normalization.
|
|
9
|
+
//
|
|
10
|
+
// The classifier is model-free and runs in the runner, so it costs zero agent tokens and
|
|
11
|
+
// the agent can't game it.
|
|
12
|
+
|
|
13
|
+
// A normalized, ordered session event. Order is significant: `leadingGather` counts the
|
|
14
|
+
// run of gathering tools before the first action.
|
|
15
|
+
export type SessionEvent =
|
|
16
|
+
// A tool invocation. Gathering-vs-action is decided here by `isGatheringTool(name)`.
|
|
17
|
+
| { type: "tool"; name: string }
|
|
18
|
+
// A failed tool result (paired outcome proxy — failures/workarounds the agent hit).
|
|
19
|
+
| { type: "tool_error" }
|
|
20
|
+
// A real user prompt (paired outcome proxy — more back-and-forth ~ clarification/correction).
|
|
21
|
+
| { type: "user_prompt" }
|
|
22
|
+
// A substantive assistant turn (one that produced text or a tool call).
|
|
23
|
+
| { type: "turn" };
|
|
24
|
+
|
|
25
|
+
// Tools that acquire context without changing anything. Anything not matched here is
|
|
26
|
+
// treated as an action (mutation, execution, or a delegation/direction decision) — Bash
|
|
27
|
+
// counts as an action because it executes (a conservative, documented choice for v0;
|
|
28
|
+
// `cat`/`ls` via Bash are misclassified, refine later if the data warrants it).
|
|
29
|
+
const GATHERING_TOOLS = new Set([
|
|
30
|
+
"Read", "Grep", "Glob", "LS", "NotebookRead", "WebFetch", "WebSearch",
|
|
31
|
+
]);
|
|
32
|
+
const GATHERING_NAME = /(?:^|[._-])(read|get|list|search|grep|glob|find|fetch|query|browse|view|show|cat|status|inspect|lookup|symbols|snippet)/i;
|
|
33
|
+
|
|
34
|
+
export function isGatheringTool(name: string): boolean {
|
|
35
|
+
if (GATHERING_TOOLS.has(name)) return true;
|
|
36
|
+
// MCP / custom tools: classify by name shape (e.g. mcp__callmux__searxng_web_search).
|
|
37
|
+
return GATHERING_NAME.test(name);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface ContextRatioMetric {
|
|
41
|
+
/** Session-wide gathering fraction: gatheringCalls / totalToolCalls. The headline metric. */
|
|
42
|
+
ratio: number;
|
|
43
|
+
gatheringCalls: number;
|
|
44
|
+
actionCalls: number;
|
|
45
|
+
totalToolCalls: number;
|
|
46
|
+
/** Consecutive gathering calls before the first action — the "read N files before moving" signal. */
|
|
47
|
+
leadingGather: number;
|
|
48
|
+
/** Substantive assistant turns (turns that produced text or a tool call). */
|
|
49
|
+
turns: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface SessionOutcomeProxy {
|
|
53
|
+
/** Real user prompts in the session — more back-and-forth ~ more clarification/correction. */
|
|
54
|
+
userPrompts: number;
|
|
55
|
+
/** tool_result blocks flagged is_error — failures/workarounds the agent hit. */
|
|
56
|
+
toolErrors: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface SessionAnalysis {
|
|
60
|
+
metric: ContextRatioMetric;
|
|
61
|
+
outcome: SessionOutcomeProxy;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Reduce a normalized event stream to the context-gathering ratio plus paired outcome
|
|
66
|
+
* proxies. Returns null when there's nothing substantive to measure (no tool calls) —
|
|
67
|
+
* trivial segments have nothing to learn from and shouldn't pollute the baselines.
|
|
68
|
+
*
|
|
69
|
+
* Per-segment by construction: callers pass only the events since the last capture
|
|
70
|
+
* boundary (compact/clear/restart/shutdown), so each result describes one work chunk.
|
|
71
|
+
*/
|
|
72
|
+
export function computeContextRatio(events: SessionEvent[]): SessionAnalysis | null {
|
|
73
|
+
let gatheringCalls = 0;
|
|
74
|
+
let actionCalls = 0;
|
|
75
|
+
let leadingGather = 0;
|
|
76
|
+
let sawAction = false;
|
|
77
|
+
let userPrompts = 0;
|
|
78
|
+
let toolErrors = 0;
|
|
79
|
+
let turns = 0;
|
|
80
|
+
|
|
81
|
+
for (const event of events) {
|
|
82
|
+
switch (event.type) {
|
|
83
|
+
case "user_prompt":
|
|
84
|
+
userPrompts++;
|
|
85
|
+
break;
|
|
86
|
+
case "tool_error":
|
|
87
|
+
toolErrors++;
|
|
88
|
+
break;
|
|
89
|
+
case "turn":
|
|
90
|
+
turns++;
|
|
91
|
+
break;
|
|
92
|
+
case "tool":
|
|
93
|
+
if (isGatheringTool(event.name)) {
|
|
94
|
+
gatheringCalls++;
|
|
95
|
+
if (!sawAction) leadingGather++;
|
|
96
|
+
} else {
|
|
97
|
+
actionCalls++;
|
|
98
|
+
sawAction = true;
|
|
99
|
+
}
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const totalToolCalls = gatheringCalls + actionCalls;
|
|
105
|
+
if (totalToolCalls === 0) return null;
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
metric: {
|
|
109
|
+
ratio: gatheringCalls / totalToolCalls,
|
|
110
|
+
gatheringCalls,
|
|
111
|
+
actionCalls,
|
|
112
|
+
totalToolCalls,
|
|
113
|
+
leadingGather,
|
|
114
|
+
turns,
|
|
115
|
+
},
|
|
116
|
+
outcome: { userPrompts, toolErrors },
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { accessSync, appendFileSync, constants, mkdirSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
|
+
import { sanitizeFsName } from "agent-relay-sdk/fs-name";
|
|
6
|
+
|
|
7
|
+
const SCRATCH_DIR_NAME = ".agent-relay";
|
|
8
|
+
// The local-ignore entry. Leading + trailing slash scopes it to the dir at the
|
|
9
|
+
// base, matching git's gitignore semantics.
|
|
10
|
+
const EXCLUDE_ENTRY = "/.agent-relay/";
|
|
11
|
+
const LAUNCHER_DIR = join(tmpdir(), `agent-relay-launchers-${typeof process.getuid === "function" ? process.getuid() : "user"}`);
|
|
12
|
+
const DEFAULT_LAUNCHER_SWEEP_MIN_AGE_MS = 24 * 60 * 60 * 1000;
|
|
13
|
+
|
|
14
|
+
export interface SessionScratchLayout {
|
|
15
|
+
baseDir: string; // dir that contains .agent-relay (cwd, or fallback base dir)
|
|
16
|
+
rootDir: string; // <base>/.agent-relay
|
|
17
|
+
sessionsDir: string; // <base>/.agent-relay/sessions
|
|
18
|
+
sessionDir: string; // <base>/.agent-relay/sessions/<id>
|
|
19
|
+
tmpDir: string; // <base>/.agent-relay/sessions/<id>/tmp
|
|
20
|
+
replyFile: string; // <tmp>/reply.md
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface SessionScratchTarget {
|
|
24
|
+
agentId: string;
|
|
25
|
+
cwd: string;
|
|
26
|
+
// Orchestrator base dir, used only when cwd is not writable. NEVER home — a
|
|
27
|
+
// home fallback would place scratch above the orchestrator base dir and break
|
|
28
|
+
// cwd containment.
|
|
29
|
+
fallbackBaseDir?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface SessionLauncherTarget {
|
|
33
|
+
launcherScript?: string;
|
|
34
|
+
launcherDir?: string;
|
|
35
|
+
tmuxSession?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isWritableDir(dir: string): boolean {
|
|
39
|
+
try {
|
|
40
|
+
if (!statSync(dir).isDirectory()) return false;
|
|
41
|
+
accessSync(dir, constants.W_OK);
|
|
42
|
+
return true;
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Prefer cwd; fall back to the orchestrator base dir only when cwd is not a
|
|
49
|
+
// writable dir. Returns cwd as a last resort so callers can still attempt mkdir
|
|
50
|
+
// (and surface the real error) rather than silently picking an unrelated path.
|
|
51
|
+
export function resolveScratchBase(cwd: string, fallbackBaseDir?: string): string {
|
|
52
|
+
if (isWritableDir(cwd)) return cwd;
|
|
53
|
+
if (fallbackBaseDir && isWritableDir(fallbackBaseDir)) return fallbackBaseDir;
|
|
54
|
+
return cwd;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function sessionScratchLayout(baseDir: string, agentId: string): SessionScratchLayout {
|
|
58
|
+
const rootDir = join(baseDir, SCRATCH_DIR_NAME);
|
|
59
|
+
const sessionsDir = join(rootDir, "sessions");
|
|
60
|
+
const sessionDir = join(sessionsDir, agentId);
|
|
61
|
+
const tmpDir = join(sessionDir, "tmp");
|
|
62
|
+
return { baseDir, rootDir, sessionsDir, sessionDir, tmpDir, replyFile: join(tmpDir, "reply.md") };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Resolve git's exclude file for baseDir, correctly handling plain repos,
|
|
66
|
+
// worktrees, and submodules (git rev-parse returns the shared commondir's
|
|
67
|
+
// info/exclude). Returns null when baseDir is not inside a git work tree.
|
|
68
|
+
function gitExcludePath(baseDir: string): string | null {
|
|
69
|
+
try {
|
|
70
|
+
const out = execFileSync("git", ["-C", baseDir, "rev-parse", "--git-path", "info/exclude"], {
|
|
71
|
+
encoding: "utf8",
|
|
72
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
73
|
+
}).trim();
|
|
74
|
+
if (!out) return null;
|
|
75
|
+
return isAbsolute(out) ? out : resolve(baseDir, out);
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Idempotently append EXCLUDE_ENTRY to an ignore file (grep-before-write).
|
|
82
|
+
function appendIgnoreEntry(file: string): void {
|
|
83
|
+
let existing = "";
|
|
84
|
+
try {
|
|
85
|
+
existing = readFileSync(file, "utf8");
|
|
86
|
+
} catch {}
|
|
87
|
+
const wanted = EXCLUDE_ENTRY.trim();
|
|
88
|
+
if (existing.split("\n").some((line) => line.trim() === wanted)) return;
|
|
89
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
90
|
+
const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
91
|
+
appendFileSync(file, `${prefix}${EXCLUDE_ENTRY}\n`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Ensure `.agent-relay/` is locally ignored. Prefers .git/info/exclude (git's
|
|
95
|
+
// local, uncommitted ignore list) so no tracked file is ever modified; falls
|
|
96
|
+
// back to .gitignore only when baseDir is not a git repo. Returns which file was
|
|
97
|
+
// used (for logging/tests).
|
|
98
|
+
export function ensureScratchIgnored(baseDir: string): "exclude" | "gitignore" {
|
|
99
|
+
const excludePath = gitExcludePath(baseDir);
|
|
100
|
+
if (excludePath) {
|
|
101
|
+
appendIgnoreEntry(excludePath);
|
|
102
|
+
return "exclude";
|
|
103
|
+
}
|
|
104
|
+
appendIgnoreEntry(join(baseDir, ".gitignore"));
|
|
105
|
+
return "gitignore";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// SessionStart: create the session tmp dir and ensure local ignore. Dir
|
|
109
|
+
// creation is the load-bearing part; ignore setup is best-effort.
|
|
110
|
+
export function ensureSessionScratch(target: SessionScratchTarget): SessionScratchLayout {
|
|
111
|
+
const base = resolveScratchBase(target.cwd, target.fallbackBaseDir);
|
|
112
|
+
const layout = sessionScratchLayout(base, target.agentId);
|
|
113
|
+
mkdirSync(layout.tmpDir, { recursive: true });
|
|
114
|
+
try {
|
|
115
|
+
ensureScratchIgnored(base);
|
|
116
|
+
} catch {
|
|
117
|
+
// best-effort: a missing ignore entry never pollutes git because the dir is
|
|
118
|
+
// already created and the entry is the only thing at risk.
|
|
119
|
+
}
|
|
120
|
+
return layout;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function resolveLauncherDir(launcherDir?: string): string {
|
|
124
|
+
return launcherDir ?? LAUNCHER_DIR;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function launcherScriptPathForSession(sessionName: string, launcherDir?: string): string {
|
|
128
|
+
const sanitized = sanitizeFsName(sessionName, { replacement: "-", collapse: false });
|
|
129
|
+
return join(resolveLauncherDir(launcherDir), `${sanitized}.sh`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function dedupeBases(bases: Array<string | undefined>): string[] {
|
|
133
|
+
const seen = new Set<string>();
|
|
134
|
+
const out: string[] = [];
|
|
135
|
+
for (const b of bases) {
|
|
136
|
+
if (!b || seen.has(b)) continue;
|
|
137
|
+
seen.add(b);
|
|
138
|
+
out.push(b);
|
|
139
|
+
}
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function dedupePaths(paths: Array<string | undefined>): string[] {
|
|
144
|
+
const seen = new Set<string>();
|
|
145
|
+
const out: string[] = [];
|
|
146
|
+
for (const path of paths) {
|
|
147
|
+
if (!path || seen.has(path)) continue;
|
|
148
|
+
seen.add(path);
|
|
149
|
+
out.push(path);
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// SessionEnd: remove this session's dir from every candidate base.
|
|
155
|
+
export function reapSessionScratch(target: SessionScratchTarget): void {
|
|
156
|
+
for (const base of dedupeBases([target.cwd, target.fallbackBaseDir])) {
|
|
157
|
+
const { sessionDir } = sessionScratchLayout(base, target.agentId);
|
|
158
|
+
try {
|
|
159
|
+
rmSync(sessionDir, { recursive: true, force: true });
|
|
160
|
+
} catch {}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Claude headless sessions externalize their provider launch command into a
|
|
165
|
+
// per-session script under /tmp. Remove it on runner shutdown alongside the
|
|
166
|
+
// scratch dir reap to avoid leaking one file per spawn.
|
|
167
|
+
export function reapSessionLauncher(target: SessionLauncherTarget): string[] {
|
|
168
|
+
const removed: string[] = [];
|
|
169
|
+
for (const path of dedupePaths([
|
|
170
|
+
target.launcherScript,
|
|
171
|
+
target.tmuxSession ? launcherScriptPathForSession(target.tmuxSession, target.launcherDir) : undefined,
|
|
172
|
+
])) {
|
|
173
|
+
try {
|
|
174
|
+
const exists = statSync(path).isFile();
|
|
175
|
+
rmSync(path, { force: true });
|
|
176
|
+
if (exists) removed.push(path);
|
|
177
|
+
} catch {}
|
|
178
|
+
}
|
|
179
|
+
return removed;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
interface SweepOptions {
|
|
183
|
+
cwd: string;
|
|
184
|
+
fallbackBaseDir?: string;
|
|
185
|
+
// Agent ids to keep (currently-known agents + self). Any session dir whose id
|
|
186
|
+
// is not in this set AND older than minAgeMs is removed.
|
|
187
|
+
keepAgentIds: Set<string>;
|
|
188
|
+
now: number;
|
|
189
|
+
minAgeMs?: number;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
interface LauncherSweepOptions {
|
|
193
|
+
// Agent ids to keep (currently-known agents + self). Launcher scripts embed
|
|
194
|
+
// AGENT_RELAY_ID, so a stale sweep can safely remove only scripts whose agent
|
|
195
|
+
// is no longer live.
|
|
196
|
+
keepAgentIds: Set<string>;
|
|
197
|
+
launcherDir?: string;
|
|
198
|
+
now: number;
|
|
199
|
+
minAgeMs?: number;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function maybeShellUnquote(value: string): string {
|
|
203
|
+
const trimmed = value.trim();
|
|
204
|
+
if (!trimmed.startsWith("'") || !trimmed.endsWith("'")) return trimmed;
|
|
205
|
+
return trimmed.slice(1, -1).replace(/'\\''/g, "'");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function launcherAgentId(scriptPath: string): string | null {
|
|
209
|
+
try {
|
|
210
|
+
const match = readFileSync(scriptPath, "utf8").match(/^export AGENT_RELAY_ID=(.+)$/m);
|
|
211
|
+
if (!match) return null;
|
|
212
|
+
const agentId = maybeShellUnquote(match[1] ?? "");
|
|
213
|
+
return agentId || null;
|
|
214
|
+
} catch {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Periodic sweep of leftover session dirs from agents no longer known to the
|
|
220
|
+
// relay. The minAgeMs grace avoids racing a peer that just created its dir but
|
|
221
|
+
// has not yet appeared in the agent list.
|
|
222
|
+
export function sweepStaleSessions(opts: SweepOptions): string[] {
|
|
223
|
+
const minAge = opts.minAgeMs ?? 60 * 60 * 1000;
|
|
224
|
+
const removed: string[] = [];
|
|
225
|
+
for (const base of dedupeBases([opts.cwd, opts.fallbackBaseDir])) {
|
|
226
|
+
const sessionsDir = join(base, SCRATCH_DIR_NAME, "sessions");
|
|
227
|
+
let ids: string[];
|
|
228
|
+
try {
|
|
229
|
+
ids = readdirSync(sessionsDir);
|
|
230
|
+
} catch {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
for (const id of ids) {
|
|
234
|
+
if (opts.keepAgentIds.has(id)) continue;
|
|
235
|
+
const dir = join(sessionsDir, id);
|
|
236
|
+
try {
|
|
237
|
+
if (opts.now - statSync(dir).mtimeMs < minAge) continue;
|
|
238
|
+
rmSync(dir, { recursive: true, force: true });
|
|
239
|
+
removed.push(dir);
|
|
240
|
+
} catch {}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return removed;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Periodic sweep of leaked per-spawn launcher scripts. Old scripts whose agent
|
|
247
|
+
// id is no longer live are safe to prune; old unparsable files are treated as
|
|
248
|
+
// stale too so the next run clears historical buildup without manual cleanup.
|
|
249
|
+
export function sweepStaleLaunchers(opts: LauncherSweepOptions): string[] {
|
|
250
|
+
const minAge = opts.minAgeMs ?? DEFAULT_LAUNCHER_SWEEP_MIN_AGE_MS;
|
|
251
|
+
const removed: string[] = [];
|
|
252
|
+
const launcherDir = resolveLauncherDir(opts.launcherDir);
|
|
253
|
+
let entries: string[];
|
|
254
|
+
try {
|
|
255
|
+
entries = readdirSync(launcherDir);
|
|
256
|
+
} catch {
|
|
257
|
+
return removed;
|
|
258
|
+
}
|
|
259
|
+
for (const entry of entries) {
|
|
260
|
+
const scriptPath = join(launcherDir, entry);
|
|
261
|
+
try {
|
|
262
|
+
if (!statSync(scriptPath).isFile()) continue;
|
|
263
|
+
if (opts.now - statSync(scriptPath).mtimeMs < minAge) continue;
|
|
264
|
+
const agentId = launcherAgentId(scriptPath);
|
|
265
|
+
if (agentId && opts.keepAgentIds.has(agentId)) continue;
|
|
266
|
+
rmSync(scriptPath, { force: true });
|
|
267
|
+
removed.push(scriptPath);
|
|
268
|
+
} catch {}
|
|
269
|
+
}
|
|
270
|
+
return removed;
|
|
271
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { RelayHttpClient } from "agent-relay-sdk";
|
|
2
|
+
import { RUNNER_PROMPT_TEMPLATE_DEFAULTS } from "agent-relay-sdk/prompt-template-defaults";
|
|
3
|
+
|
|
4
|
+
const RUNNER_DEFAULTS = new Map<string, string>(RUNNER_PROMPT_TEMPLATE_DEFAULTS.map((template) => [template.slug, template.defaultTemplate]));
|
|
5
|
+
const templateCache = new Map<string, string>();
|
|
6
|
+
|
|
7
|
+
function renderValue(value: string | undefined): string {
|
|
8
|
+
return value ?? "";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function applyVariables(template: string, vars: Record<string, string>): string {
|
|
12
|
+
return template.replace(/\{\{(\w+)\}\}/g, (_, key: string) => renderValue(vars[key]));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function initializeRunnerTemplates(http: RelayHttpClient): Promise<void> {
|
|
16
|
+
try {
|
|
17
|
+
const templates = await http.listPromptTemplates();
|
|
18
|
+
templateCache.clear();
|
|
19
|
+
for (const template of templates) templateCache.set(template.slug, template.template);
|
|
20
|
+
} catch {
|
|
21
|
+
templateCache.clear();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function renderRunnerTemplate(slug: string, vars: Record<string, string> = {}): string {
|
|
26
|
+
const template = templateCache.get(slug) ?? RUNNER_DEFAULTS.get(slug);
|
|
27
|
+
if (template === undefined) throw new Error(`unknown runner prompt template slug: ${slug}`);
|
|
28
|
+
return applyVariables(template, vars);
|
|
29
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { CONTRACT_VERSIONS } from "agent-relay-sdk";
|
|
5
|
+
import { getManifest } from "agent-relay-providers";
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf8")) as { name?: string; version?: string };
|
|
9
|
+
|
|
10
|
+
export const PACKAGE_NAME = pkg.name || "agent-relay-runner";
|
|
11
|
+
export const VERSION = pkg.version || "0.0.0";
|
|
12
|
+
// Protocol versions are owned by the SDK (CONTRACT_VERSIONS) — derive, never redeclare.
|
|
13
|
+
export const RUNNER_PROTOCOL_VERSION = CONTRACT_VERSIONS.runnerProtocol;
|
|
14
|
+
export const PROVIDER_PLUGIN_PROTOCOL_VERSION = CONTRACT_VERSIONS.providerPluginProtocol;
|
|
15
|
+
|
|
16
|
+
export const CONTRACTS = {
|
|
17
|
+
runnerProtocol: RUNNER_PROTOCOL_VERSION,
|
|
18
|
+
providerPluginProtocol: PROVIDER_PLUGIN_PROTOCOL_VERSION,
|
|
19
|
+
} as const;
|
|
20
|
+
|
|
21
|
+
export const RUNTIME_CAPABILITIES = {
|
|
22
|
+
relayCommandBus: true,
|
|
23
|
+
managedRegistration: true,
|
|
24
|
+
lifecycleCommands: true,
|
|
25
|
+
claimRenewal: true,
|
|
26
|
+
deliveryRetry: true,
|
|
27
|
+
} as const;
|
|
28
|
+
|
|
29
|
+
export function runtimeMetadata(provider?: string) {
|
|
30
|
+
const manifest = provider ? getManifest(provider) : undefined;
|
|
31
|
+
return {
|
|
32
|
+
package: {
|
|
33
|
+
name: PACKAGE_NAME,
|
|
34
|
+
version: VERSION,
|
|
35
|
+
},
|
|
36
|
+
contracts: CONTRACTS,
|
|
37
|
+
capabilities: {
|
|
38
|
+
...RUNTIME_CAPABILITIES,
|
|
39
|
+
...(manifest?.provisioning?.bundledPlugin ? { bundledPlugin: true } : {}),
|
|
40
|
+
...(manifest?.provisioning?.bundledSkills ? { bundledSkills: true } : {}),
|
|
41
|
+
...(manifest?.provisioning?.bundledHooks?.length ? { bundledHooks: true } : {}),
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
package/src/config-store.ts
CHANGED
|
@@ -335,6 +335,7 @@ function agentProfileDefaults(input: Pick<AgentProfile, "name" | "base"> & Omit<
|
|
|
335
335
|
},
|
|
336
336
|
hooks: {
|
|
337
337
|
mode: input.hooks?.mode ?? (isolated ? "none" : "host"),
|
|
338
|
+
...(input.hooks?.refs?.length ? { refs: input.hooks.refs } : {}),
|
|
338
339
|
},
|
|
339
340
|
permissions: {
|
|
340
341
|
mode: input.permissions?.mode,
|
|
@@ -419,6 +420,7 @@ function validateAgentProfile(key: string, value: unknown, opts: { allowBuiltIn?
|
|
|
419
420
|
},
|
|
420
421
|
hooks: {
|
|
421
422
|
mode: hooks.mode === undefined ? defaults.hooks.mode : cleanEnum(hooks.mode, "hooks.mode", VALID_PROFILE_CATEGORY_MODES),
|
|
423
|
+
refs: cleanAgentProfileAssetArray(hooks.refs, "hooks.refs"),
|
|
422
424
|
},
|
|
423
425
|
permissions: {
|
|
424
426
|
mode: permissions.mode === undefined || permissions.mode === null
|
|
@@ -9,6 +9,7 @@ import { parseJson } from "../utils";
|
|
|
9
9
|
import { errMessage, isRecord, SPAWN_PROVIDERS } from "agent-relay-sdk";
|
|
10
10
|
import type {
|
|
11
11
|
AgentProfileProvider,
|
|
12
|
+
HookAssetDefinition,
|
|
12
13
|
McpAssetDefinition,
|
|
13
14
|
PluginAssetDefinition,
|
|
14
15
|
ProvisioningCapability,
|
|
@@ -26,7 +27,7 @@ import type {
|
|
|
26
27
|
// the launch. #557 (per-profile assembly) is meant to be a thin unifier on top, so
|
|
27
28
|
// this stays the single home for asset definition + resolution — not three modules.
|
|
28
29
|
|
|
29
|
-
const PROVISIONING_ASSET_KINDS = ["skill", "mcp", "plugin"] as const;
|
|
30
|
+
const PROVISIONING_ASSET_KINDS = ["skill", "mcp", "plugin", "hook"] as const;
|
|
30
31
|
const PROVISIONING_PROVIDERS = [...SPAWN_PROVIDERS, "any"];
|
|
31
32
|
const GITHUB_API = "https://api.github.com";
|
|
32
33
|
const DEFAULT_MAX_FILE_BYTES = 256 * 1024;
|
|
@@ -213,6 +214,10 @@ function assertDefinition(kind: ProvisioningAssetKind, definition: ResolvedAsset
|
|
|
213
214
|
const metadata = isRecord(def.metadata) ? def.metadata : {};
|
|
214
215
|
if (isRecord(metadata.callmuxServer)) assertCallmuxServerPlaceholders(metadata.callmuxServer as ProvisioningMcpServer, def.name);
|
|
215
216
|
}
|
|
217
|
+
if (kind === "hook") {
|
|
218
|
+
const def = definition as HookAssetDefinition;
|
|
219
|
+
if (!isRecord(def.hooks) || !Object.keys(def.hooks).length) throw new ValidationError("hook definition requires a hooks object");
|
|
220
|
+
}
|
|
216
221
|
if ((kind === "skill" || kind === "plugin")) {
|
|
217
222
|
const def = definition as SkillAssetDefinition | PluginAssetDefinition;
|
|
218
223
|
if (!def.files?.length && !def.dir) throw new ValidationError(`${kind} definition requires files or dir`);
|
|
@@ -470,6 +475,24 @@ function mcpDefinitionFromFiles(name: string, files: VendoredFile[], mcpServerNa
|
|
|
470
475
|
return { kind: "mcp", name, server: coerceMcpServer(parsed) };
|
|
471
476
|
}
|
|
472
477
|
|
|
478
|
+
function hookDefinitionFromFiles(name: string, files: Array<{ path: string; content: string }>): HookAssetDefinition {
|
|
479
|
+
const preferred = files.find((file) => file.path === "hooks.json")
|
|
480
|
+
?? files.find((file) => file.path.endsWith("/hooks.json"))
|
|
481
|
+
?? files.find((file) => file.path === `${name}.json`);
|
|
482
|
+
if (!preferred) throw new ValidationError("hook imports require hooks.json");
|
|
483
|
+
let parsed: Record<string, unknown>;
|
|
484
|
+
try {
|
|
485
|
+
const value = JSON.parse(preferred.content);
|
|
486
|
+
if (!isRecord(value)) throw new Error("not an object");
|
|
487
|
+
parsed = value;
|
|
488
|
+
} catch {
|
|
489
|
+
throw new ValidationError(`${preferred.path} must be valid JSON object`);
|
|
490
|
+
}
|
|
491
|
+
const hooks = isRecord(parsed.hooks) ? parsed.hooks : parsed;
|
|
492
|
+
if (!isRecord(hooks) || !Object.keys(hooks).length) throw new ValidationError("hook import requires a non-empty hooks object");
|
|
493
|
+
return { kind: "hook", name, hooks: hooks as HookAssetDefinition["hooks"], files: files.map(({ path, content }) => ({ path, content })) };
|
|
494
|
+
}
|
|
495
|
+
|
|
473
496
|
function definitionFromVendoredFiles(input: ImportProvisioningAssetFromGitHubInput, files: VendoredFile[], provider: AgentProfileProvider): ResolvedAssetDefinition {
|
|
474
497
|
if (input.kind === "skill") {
|
|
475
498
|
assertRootSkill(files);
|
|
@@ -479,6 +502,7 @@ function definitionFromVendoredFiles(input: ImportProvisioningAssetFromGitHubInp
|
|
|
479
502
|
assertPlugin(files, provider);
|
|
480
503
|
return { kind: "plugin", name: input.name, files: files.map(({ path, content }) => ({ path, content })) };
|
|
481
504
|
}
|
|
505
|
+
if (input.kind === "hook") return hookDefinitionFromFiles(input.name, files);
|
|
482
506
|
return mcpDefinitionFromFiles(input.name, files, input.mcpServerName);
|
|
483
507
|
}
|
|
484
508
|
|
|
@@ -529,7 +553,7 @@ function updateBundledAssetMetadata(asset: ProvisioningAsset, root: string, file
|
|
|
529
553
|
}
|
|
530
554
|
|
|
531
555
|
function upsertBundledProvisioningAsset(input: {
|
|
532
|
-
kind: "skill" | "plugin";
|
|
556
|
+
kind: "skill" | "plugin" | "hook";
|
|
533
557
|
name: string;
|
|
534
558
|
provider: AgentProfileProvider;
|
|
535
559
|
files: BundledFile[];
|
|
@@ -538,7 +562,10 @@ function upsertBundledProvisioningAsset(input: {
|
|
|
538
562
|
description: string;
|
|
539
563
|
}): void {
|
|
540
564
|
if (!input.files.length) return;
|
|
541
|
-
const
|
|
565
|
+
const files = input.files.map(({ path, content }) => ({ path, content }));
|
|
566
|
+
const definition = input.kind === "hook"
|
|
567
|
+
? hookDefinitionFromFiles(input.name, input.files)
|
|
568
|
+
: { kind: input.kind, name: input.name, files } as SkillAssetDefinition | PluginAssetDefinition;
|
|
542
569
|
assertDefinition(input.kind, definition);
|
|
543
570
|
const asset = upsertProvisioningAsset({ kind: input.kind, name: input.name, provider: input.provider, definition });
|
|
544
571
|
updateBundledAssetMetadata(asset, input.root, input.files, input.title, input.description);
|
|
@@ -552,12 +579,16 @@ export function seedBundledProvisioningAssets(): void {
|
|
|
552
579
|
const plugin = provisioning.bundledPlugin;
|
|
553
580
|
upsertBundledProvisioningAsset({ kind: "plugin", name: plugin.ref, provider, root: plugin.dir, files: readBundledFiles(resolve(import.meta.dir, "../../", plugin.dir), { omitFiles: plugin.omitFiles, fileOverrides: plugin.fileOverrides }), title: plugin.title, description: plugin.description });
|
|
554
581
|
}
|
|
555
|
-
if (
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
582
|
+
if (provisioning.bundledSkills) {
|
|
583
|
+
const skills = provisioning.bundledSkills;
|
|
584
|
+
const skillsDir = resolve(import.meta.dir, "../../", skills.dir);
|
|
585
|
+
for (const ref of bundledSkillRefs(skills)) {
|
|
586
|
+
const skillName = ref.slice(skills.refPrefix.length);
|
|
587
|
+
upsertBundledProvisioningAsset({ kind: "skill", name: ref, provider, root: posix.join(skills.dir, skillName), files: readBundledFiles(join(skillsDir, skillName)), title: `${skills.titlePrefix}${skillName}`, description: skills.description });
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
for (const hook of provisioning.bundledHooks ?? []) {
|
|
591
|
+
upsertBundledProvisioningAsset({ kind: "hook", name: hook.ref, provider, root: hook.dir, files: readBundledFiles(resolve(import.meta.dir, "../../", hook.dir), { omitFiles: hook.omitFiles, fileOverrides: hook.fileOverrides }), title: hook.title, description: hook.description });
|
|
561
592
|
}
|
|
562
593
|
}
|
|
563
594
|
}
|