codiedev 0.9.66 → 0.9.68

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.
@@ -0,0 +1,52 @@
1
+ export interface PackManifestFile {
2
+ name: string;
3
+ content: string;
4
+ /** Skills only: the artifact the content came from (identity marker). */
5
+ artifactId?: string;
6
+ }
7
+ export interface PackManifest {
8
+ slug: string;
9
+ name: string;
10
+ description: string;
11
+ version: number;
12
+ contentHash: string;
13
+ skills: PackManifestFile[];
14
+ agents: PackManifestFile[];
15
+ hooksJson: string | null;
16
+ /** Release channel the manifest resolved to (stable|beta|alpha|draft). */
17
+ channel?: string;
18
+ /** Display emoji/icon; never used in paths. */
19
+ icon?: string;
20
+ }
21
+ export interface PackSummary {
22
+ slug: string;
23
+ name: string;
24
+ description?: string;
25
+ version: number;
26
+ channel?: string;
27
+ icon?: string;
28
+ itemCount?: number;
29
+ agentCount?: number;
30
+ hasHooks?: boolean;
31
+ }
32
+ export declare function isSafeSegment(value: unknown): value is string;
33
+ export declare function isSafeSkillName(value: unknown): value is string;
34
+ export declare class ManifestError extends Error {
35
+ }
36
+ /** Validate a manifest from the server. Throws ManifestError on anything
37
+ * unsafe or malformed — a partial install of a tampered pack is worse than
38
+ * none. */
39
+ export declare function parseManifest(input: unknown): PackManifest;
40
+ /** Short display-only label from the server: printable, no control chars. */
41
+ export declare function cleanLabel(value: unknown, max: number): string | undefined;
42
+ /** "🧰 Backend starter" when an icon is set. */
43
+ export declare function displayName(pack: {
44
+ name?: string;
45
+ slug: string;
46
+ icon?: unknown;
47
+ }): string;
48
+ /** Parse leading `---` YAML-ish frontmatter (flat `key: value` lines only). */
49
+ export declare function parseFrontmatter(content: string): {
50
+ data: Record<string, string>;
51
+ body: string;
52
+ };
@@ -0,0 +1,140 @@
1
+ "use strict";
2
+ // Server manifest shape (GET /api/cli/packs/manifest) and its validation.
3
+ // Everything here is untrusted input that becomes file paths and shell hooks
4
+ // on the member's machine, so names are checked strictly before any write.
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ManifestError = void 0;
7
+ exports.isSafeSegment = isSafeSegment;
8
+ exports.isSafeSkillName = isSafeSkillName;
9
+ exports.parseManifest = parseManifest;
10
+ exports.cleanLabel = cleanLabel;
11
+ exports.displayName = displayName;
12
+ exports.parseFrontmatter = parseFrontmatter;
13
+ // One path segment: starts alphanumeric, then alphanumerics, dot, dash or
14
+ // underscore. Rejects "", ".", "..", slashes, backslashes, absolute paths,
15
+ // drive letters, NUL and whitespace.
16
+ const SAFE_SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
17
+ function isSafeSegment(value) {
18
+ return typeof value === "string" && SAFE_SEGMENT.test(value) && !value.includes("..");
19
+ }
20
+ // Skill dir names come from SKILL.md frontmatter and may contain inner spaces
21
+ // (the server allows `[A-Za-z0-9][A-Za-z0-9._ -]*`); still one safe segment.
22
+ const SAFE_SKILL_NAME = /^[A-Za-z0-9][A-Za-z0-9._ -]{0,127}$/;
23
+ function isSafeSkillName(value) {
24
+ return typeof value === "string" && SAFE_SKILL_NAME.test(value) && !value.includes("..") && value.trim() === value;
25
+ }
26
+ class ManifestError extends Error {
27
+ }
28
+ exports.ManifestError = ManifestError;
29
+ function fileList(value, field, safe) {
30
+ if (value === undefined || value === null)
31
+ return [];
32
+ if (!Array.isArray(value))
33
+ throw new ManifestError(`manifest.${field} must be an array`);
34
+ const seen = new Set();
35
+ return value.map((item, index) => {
36
+ const entry = item;
37
+ if (!entry || typeof entry !== "object")
38
+ throw new ManifestError(`manifest.${field}[${index}] is not an object`);
39
+ if (!safe(entry.name)) {
40
+ throw new ManifestError(`manifest.${field}[${index}] has an unsafe name: ${JSON.stringify(entry.name)}`);
41
+ }
42
+ if (typeof entry.content !== "string")
43
+ throw new ManifestError(`manifest.${field}[${index}].content must be a string`);
44
+ const key = entry.name.toLowerCase();
45
+ if (seen.has(key))
46
+ throw new ManifestError(`manifest.${field} has a duplicate name: ${entry.name}`);
47
+ seen.add(key);
48
+ const artifactId = typeof entry.artifactId === "string" && /^[a-z0-9]{16,64}$/i.test(entry.artifactId) ? entry.artifactId : undefined;
49
+ return { name: entry.name, content: entry.content, ...(artifactId ? { artifactId } : {}) };
50
+ });
51
+ }
52
+ /** Validate a manifest from the server. Throws ManifestError on anything
53
+ * unsafe or malformed — a partial install of a tampered pack is worse than
54
+ * none. */
55
+ function parseManifest(input) {
56
+ let raw = input;
57
+ if (typeof raw === "string") {
58
+ try {
59
+ raw = JSON.parse(raw);
60
+ }
61
+ catch {
62
+ throw new ManifestError("manifest is not valid JSON");
63
+ }
64
+ }
65
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
66
+ throw new ManifestError("manifest is not an object");
67
+ const m = raw;
68
+ if (!isSafeSegment(m.slug))
69
+ throw new ManifestError(`manifest has an unsafe slug: ${JSON.stringify(m.slug)}`);
70
+ const version = Number(m.version);
71
+ if (!Number.isInteger(version) || version < 0)
72
+ throw new ManifestError("manifest.version must be a non-negative integer");
73
+ if (typeof m.contentHash !== "string" || !m.contentHash)
74
+ throw new ManifestError("manifest.contentHash is missing");
75
+ let hooksJson = null;
76
+ if (m.hooksJson !== undefined && m.hooksJson !== null) {
77
+ if (typeof m.hooksJson !== "string")
78
+ throw new ManifestError("manifest.hooksJson must be a string or null");
79
+ if (m.hooksJson.trim()) {
80
+ let parsed;
81
+ try {
82
+ parsed = JSON.parse(m.hooksJson);
83
+ }
84
+ catch {
85
+ throw new ManifestError("manifest.hooksJson is not valid JSON");
86
+ }
87
+ const hooks = parsed?.hooks;
88
+ if (!hooks || typeof hooks !== "object" || Array.isArray(hooks)) {
89
+ throw new ManifestError("manifest.hooksJson must look like {\"hooks\":{...}}");
90
+ }
91
+ hooksJson = m.hooksJson;
92
+ }
93
+ }
94
+ return {
95
+ slug: m.slug,
96
+ name: typeof m.name === "string" && m.name ? m.name : m.slug,
97
+ description: typeof m.description === "string" ? m.description : "",
98
+ version,
99
+ contentHash: m.contentHash,
100
+ skills: fileList(m.skills, "skills", isSafeSkillName),
101
+ agents: fileList(m.agents, "agents", isSafeSegment),
102
+ hooksJson,
103
+ ...(cleanLabel(m.channel, 16) ? { channel: cleanLabel(m.channel, 16) } : {}),
104
+ ...(cleanLabel(m.icon, 16) ? { icon: cleanLabel(m.icon, 16) } : {}),
105
+ };
106
+ }
107
+ /** Short display-only label from the server: printable, no control chars. */
108
+ function cleanLabel(value, max) {
109
+ if (typeof value !== "string")
110
+ return undefined;
111
+ const clean = value
112
+ .replace(/\u001b\[[0-9;?]*[ -\/]*[@-~]/g, "") // ANSI CSI sequences
113
+ .replace(/[\u0000-\u001f\u007f-\u009f]/g, "")
114
+ .trim();
115
+ return clean ? [...clean].slice(0, max).join("") : undefined;
116
+ }
117
+ /** "🧰 Backend starter" when an icon is set. */
118
+ function displayName(pack) {
119
+ const icon = cleanLabel(pack.icon, 8);
120
+ const name = pack.name || pack.slug;
121
+ return icon ? `${icon} ${name}` : name;
122
+ }
123
+ /** Parse leading `---` YAML-ish frontmatter (flat `key: value` lines only). */
124
+ function parseFrontmatter(content) {
125
+ const match = content.match(/^?---\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/);
126
+ if (!match)
127
+ return { data: {}, body: content };
128
+ const data = {};
129
+ for (const line of match[1].split(/\r?\n/)) {
130
+ const kv = line.match(/^([A-Za-z0-9_-]+)\s*:\s*(.*)$/);
131
+ if (!kv)
132
+ continue;
133
+ let value = kv[2].trim();
134
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
135
+ value = value.slice(1, -1);
136
+ }
137
+ data[kv[1]] = value;
138
+ }
139
+ return { data, body: content.slice(match[0].length) };
140
+ }
@@ -0,0 +1,17 @@
1
+ export declare const MARKETPLACE_NAME = "codiedev-packs";
2
+ export declare function packsRoot(env?: NodeJS.ProcessEnv, home?: string): string;
3
+ export declare function pluginsDir(root: string): string;
4
+ export declare function pluginDir(root: string, slug: string): string;
5
+ /** Codex, Copilot and Cursor read a "portable" copy of each plugin: agent
6
+ * `model:` levels (opus/sonnet/haiku) are Claude Code aliases, so those tools
7
+ * get the line removed and use their own default. Same marketplace name, its
8
+ * own root. */
9
+ export declare function portableRoot(root: string): string;
10
+ export declare function marketplacePath(root: string): string;
11
+ export declare function registryPath(root: string): string;
12
+ export declare function shimPath(root: string): string;
13
+ /** Stamp of the last background `pack install --quiet` spawn. */
14
+ export declare function lastSyncPath(root: string): string;
15
+ /** Cached `pack status`, written by every sync/status run, read by the nag. */
16
+ export declare function statusCachePath(root: string): string;
17
+ export declare function lastNagPath(root: string): string;
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.MARKETPLACE_NAME = void 0;
37
+ exports.packsRoot = packsRoot;
38
+ exports.pluginsDir = pluginsDir;
39
+ exports.pluginDir = pluginDir;
40
+ exports.portableRoot = portableRoot;
41
+ exports.marketplacePath = marketplacePath;
42
+ exports.registryPath = registryPath;
43
+ exports.shimPath = shimPath;
44
+ exports.lastSyncPath = lastSyncPath;
45
+ exports.statusCachePath = statusCachePath;
46
+ exports.lastNagPath = lastNagPath;
47
+ // Where starter packs live on disk. Resolved per call (not at import) so tests
48
+ // and `CODIEDEV_CONFIG_DIR` overrides take effect without module reloads.
49
+ const os = __importStar(require("os"));
50
+ const path = __importStar(require("path"));
51
+ exports.MARKETPLACE_NAME = "codiedev-packs";
52
+ function packsRoot(env = process.env, home = os.homedir()) {
53
+ const override = env.CODIEDEV_CONFIG_DIR?.trim();
54
+ const stateDir = override ? path.resolve(override) : path.join(home, ".codiedev");
55
+ return path.join(stateDir, "packs");
56
+ }
57
+ function pluginsDir(root) {
58
+ return path.join(root, "plugins");
59
+ }
60
+ function pluginDir(root, slug) {
61
+ return path.join(pluginsDir(root), slug);
62
+ }
63
+ /** Codex, Copilot and Cursor read a "portable" copy of each plugin: agent
64
+ * `model:` levels (opus/sonnet/haiku) are Claude Code aliases, so those tools
65
+ * get the line removed and use their own default. Same marketplace name, its
66
+ * own root. */
67
+ function portableRoot(root) {
68
+ return path.join(root, "portable");
69
+ }
70
+ function marketplacePath(root) {
71
+ return path.join(root, ".claude-plugin", "marketplace.json");
72
+ }
73
+ function registryPath(root) {
74
+ return path.join(root, "installed.json");
75
+ }
76
+ function shimPath(root) {
77
+ return path.join(root, "codie-hook-shim.mjs");
78
+ }
79
+ /** Stamp of the last background `pack install --quiet` spawn. */
80
+ function lastSyncPath(root) {
81
+ return path.join(root, ".last-sync");
82
+ }
83
+ /** Cached `pack status`, written by every sync/status run, read by the nag. */
84
+ function statusCachePath(root) {
85
+ return path.join(root, "status-cache.json");
86
+ }
87
+ function lastNagPath(root) {
88
+ return path.join(root, ".last-nag");
89
+ }
@@ -0,0 +1,26 @@
1
+ export interface ShimPack {
2
+ slug: string;
3
+ root: string;
4
+ hooks: Record<string, unknown>;
5
+ }
6
+ export declare const SHIM_EVENTS: readonly ["SessionStart", "PreToolUse", "PostToolUse", "Stop"];
7
+ interface ShimHandler {
8
+ command: string;
9
+ timeout?: number;
10
+ }
11
+ interface ShimGroup {
12
+ matcher?: string;
13
+ hooks: ShimHandler[];
14
+ }
15
+ /** Per pack, the hook events in its hooks.json the shim cannot wire. */
16
+ export declare function unsupportedShimEvents(packs: ShimPack[]): Map<string, string[]>;
17
+ export declare function shimPacks(packs: ShimPack[]): Array<{
18
+ slug: string;
19
+ root: string;
20
+ hooks: Record<string, ShimGroup[]>;
21
+ }>;
22
+ export declare function generateShimSource(packs: ShimPack[]): string;
23
+ /** Write (or remove, when no pack has wireable hooks) the shim. Returns the
24
+ * path when a shim exists afterwards. */
25
+ export declare function writeShim(file: string, packs: ShimPack[]): string | null;
26
+ export {};
@@ -0,0 +1,222 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.SHIM_EVENTS = void 0;
37
+ exports.unsupportedShimEvents = unsupportedShimEvents;
38
+ exports.shimPacks = shimPacks;
39
+ exports.generateShimSource = generateShimSource;
40
+ exports.writeShim = writeShim;
41
+ // Generates the OpenCode plugin that runs starter-pack Claude hooks inside
42
+ // Codie. Stock OpenCode cannot load a Claude plugin's hooks.json, so this shim
43
+ // translates OpenCode plugin events into Claude hook invocations (Claude-style
44
+ // JSON on stdin, exit 2 on PreToolUse blocks the tool).
45
+ const fs = __importStar(require("fs"));
46
+ const store_1 = require("./store");
47
+ // Only events with an OpenCode equivalent are wired; others are dropped.
48
+ exports.SHIM_EVENTS = ["SessionStart", "PreToolUse", "PostToolUse", "Stop"];
49
+ function normalizeHooks(hooks) {
50
+ const out = {};
51
+ for (const event of exports.SHIM_EVENTS) {
52
+ const groups = hooks[event];
53
+ if (!Array.isArray(groups))
54
+ continue;
55
+ const normalized = [];
56
+ for (const groupValue of groups) {
57
+ const group = groupValue;
58
+ if (!group || !Array.isArray(group.hooks))
59
+ continue;
60
+ const handlers = [];
61
+ for (const handlerValue of group.hooks) {
62
+ const handler = handlerValue;
63
+ if (!handler || handler.type !== "command" || typeof handler.command !== "string")
64
+ continue;
65
+ handlers.push({
66
+ command: handler.command,
67
+ ...(typeof handler.timeout === "number" && handler.timeout > 0 ? { timeout: handler.timeout } : {}),
68
+ });
69
+ }
70
+ if (handlers.length === 0)
71
+ continue;
72
+ normalized.push({
73
+ ...(typeof group.matcher === "string" ? { matcher: group.matcher } : {}),
74
+ hooks: handlers,
75
+ });
76
+ }
77
+ if (normalized.length)
78
+ out[event] = normalized;
79
+ }
80
+ return out;
81
+ }
82
+ /** Per pack, the hook events in its hooks.json the shim cannot wire. */
83
+ function unsupportedShimEvents(packs) {
84
+ const out = new Map();
85
+ for (const pack of packs) {
86
+ const events = Object.keys(pack.hooks ?? {}).filter((e) => !exports.SHIM_EVENTS.includes(e)).sort();
87
+ if (events.length)
88
+ out.set(pack.slug, events);
89
+ }
90
+ return out;
91
+ }
92
+ function shimPacks(packs) {
93
+ return packs
94
+ .map((pack) => ({ slug: pack.slug, root: pack.root, hooks: normalizeHooks(pack.hooks) }))
95
+ .filter((pack) => Object.keys(pack.hooks).length > 0);
96
+ }
97
+ function generateShimSource(packs) {
98
+ const data = shimPacks(packs);
99
+ return `// Managed by \`codiedev pack install\`. Regenerated on every install; do not edit.
100
+ import { spawnSync } from "node:child_process";
101
+ import { existsSync } from "node:fs";
102
+
103
+ const PACKS = ${JSON.stringify(data, null, 2)};
104
+
105
+ // OpenCode tool ids are lowercase; Claude hook matchers use Claude casing.
106
+ const TOOL_NAMES = {
107
+ bash: "Bash", edit: "Edit", write: "Write", read: "Read", grep: "Grep",
108
+ glob: "Glob", list: "LS", ls: "LS", webfetch: "WebFetch", websearch: "WebSearch",
109
+ todowrite: "TodoWrite", todoread: "TodoRead", task: "Task", multiedit: "MultiEdit",
110
+ patch: "Edit", apply_patch: "Edit",
111
+ };
112
+
113
+ export function claudeToolName(tool) {
114
+ const name = String(tool ?? "");
115
+ if (!name) return "unknown";
116
+ return TOOL_NAMES[name.toLowerCase()] ?? name.charAt(0).toUpperCase() + name.slice(1);
117
+ }
118
+
119
+ // Claude semantics: empty or "*" matches everything; otherwise a regex that
120
+ // must match the whole tool name ("Edit|Write", "Notebook.*").
121
+ export function matches(matcher, value) {
122
+ if (matcher === undefined || matcher === null || matcher === "" || matcher === "*") return true;
123
+ try {
124
+ return new RegExp("^(?:" + matcher + ")$").test(value);
125
+ } catch {
126
+ return false;
127
+ }
128
+ }
129
+
130
+ function handlersFor(event, value) {
131
+ const out = [];
132
+ for (const pack of PACKS) {
133
+ for (const group of pack.hooks[event] ?? []) {
134
+ if (!matches(group.matcher, value)) continue;
135
+ for (const handler of group.hooks) out.push({ ...handler, root: pack.root });
136
+ }
137
+ }
138
+ return out;
139
+ }
140
+
141
+ function run(handler, payload) {
142
+ const cwd = typeof payload.cwd === "string" && existsSync(payload.cwd) ? payload.cwd : undefined;
143
+ return spawnSync(handler.command, [], {
144
+ input: JSON.stringify(payload),
145
+ encoding: "utf8",
146
+ shell: true,
147
+ cwd,
148
+ timeout: (handler.timeout ?? 60) * 1000,
149
+ env: { ...process.env, CLAUDE_PLUGIN_ROOT: handler.root },
150
+ });
151
+ }
152
+
153
+ function sessionId(value) {
154
+ return value?.sessionID ?? value?.sessionId ?? value?.properties?.sessionID ?? value?.properties?.info?.id ?? undefined;
155
+ }
156
+
157
+ export const CodiedevPackHooks = async (ctx = {}) => {
158
+ const cwd = ctx?.worktree || ctx?.directory || process.cwd();
159
+ const childSessions = new Set();
160
+ return {
161
+ "tool.execute.before": async (input, output) => {
162
+ const tool_name = claudeToolName(input?.tool);
163
+ for (const handler of handlersFor("PreToolUse", tool_name)) {
164
+ const result = run(handler, {
165
+ hook_event_name: "PreToolUse",
166
+ session_id: sessionId(input),
167
+ cwd,
168
+ tool_name,
169
+ tool_input: output?.args ?? {},
170
+ });
171
+ if (result.status === 2) throw new Error(String(result.stderr ?? "").trim() || "Blocked by hook");
172
+ }
173
+ },
174
+ "tool.execute.after": async (input, output) => {
175
+ const tool_name = claudeToolName(input?.tool);
176
+ for (const handler of handlersFor("PostToolUse", tool_name)) {
177
+ run(handler, {
178
+ hook_event_name: "PostToolUse",
179
+ session_id: sessionId(input),
180
+ cwd,
181
+ tool_name,
182
+ tool_input: input?.args ?? {},
183
+ tool_response: output ?? null,
184
+ });
185
+ }
186
+ },
187
+ event: async ({ event } = {}) => {
188
+ // OpenCode also emits session.created/idle for subagent (child)
189
+ // sessions. Claude's SessionStart/Stop are top-level only, so remember
190
+ // children (info.parentID) and skip them. session.idle carries only the
191
+ // sessionID, hence the set.
192
+ const info = event?.properties?.info;
193
+ if (info?.id && info?.parentID) childSessions.add(info.id);
194
+ if (event?.type === "session.created") {
195
+ if (info?.parentID) return;
196
+ for (const handler of handlersFor("SessionStart", "startup")) {
197
+ run(handler, { hook_event_name: "SessionStart", session_id: sessionId(event), cwd, source: "startup" });
198
+ }
199
+ return;
200
+ }
201
+ if (event?.type === "session.idle") {
202
+ const id = sessionId(event);
203
+ if (info?.parentID || (id && childSessions.has(id))) return;
204
+ for (const handler of handlersFor("Stop", "")) {
205
+ run(handler, { hook_event_name: "Stop", session_id: sessionId(event), cwd, stop_hook_active: false });
206
+ }
207
+ }
208
+ },
209
+ };
210
+ };
211
+ `;
212
+ }
213
+ /** Write (or remove, when no pack has wireable hooks) the shim. Returns the
214
+ * path when a shim exists afterwards. */
215
+ function writeShim(file, packs) {
216
+ if (shimPacks(packs).length === 0) {
217
+ fs.rmSync(file, { force: true });
218
+ return null;
219
+ }
220
+ (0, store_1.writeFileAtomic)(file, generateShimSource(packs), 0o600);
221
+ return file;
222
+ }
@@ -0,0 +1,42 @@
1
+ import { PackManifest } from "./manifest";
2
+ export interface RegistryEntry {
3
+ version: number;
4
+ contentHash: string;
5
+ dir: string;
6
+ installedAt: number;
7
+ harnesses: string[];
8
+ name?: string;
9
+ description?: string;
10
+ channel?: string;
11
+ icon?: string;
12
+ }
13
+ export interface Registry {
14
+ packs: Record<string, RegistryEntry>;
15
+ }
16
+ export declare function pluginVersion(version: number): string;
17
+ /** Write a file via temp + rename so readers never see a partial file. */
18
+ export declare function writeFileAtomic(file: string, content: string, mode?: number): void;
19
+ export declare const AGENT_LEVELS: Set<string>;
20
+ /** Agent file for tools other than Claude Code: drop a `model:` line unless it
21
+ * is a full `provider/model` id (levels and `inherit` are Claude aliases). */
22
+ export declare function portableAgentContent(content: string): string;
23
+ /** The full file set for a pack's plugin dir, relative path → content. */
24
+ export declare function pluginFiles(manifest: PackManifest, options?: {
25
+ portable?: boolean;
26
+ }): Record<string, string>;
27
+ /** Build the plugin dir in a sibling temp dir and swap it into place, so a
28
+ * failed write never leaves a half-updated plugin and stale files (a removed
29
+ * skill, hooks that were cleared) never linger. */
30
+ export declare function writePluginDir(root: string, manifest: PackManifest, options?: {
31
+ portable?: boolean;
32
+ }): string;
33
+ export declare function readRegistry(root: string): Registry;
34
+ export declare function writeRegistry(root: string, registry: Registry): void;
35
+ export interface MarketplaceEntry {
36
+ slug: string;
37
+ description?: string;
38
+ version: number;
39
+ }
40
+ export declare function marketplaceDocument(entries: MarketplaceEntry[]): Record<string, unknown>;
41
+ /** Rewrite marketplace.json atomically with one entry per pack. */
42
+ export declare function writeMarketplace(root: string, entries: MarketplaceEntry[]): void;