@shanesaravia/hive 0.1.0 → 0.2.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/CHANGELOG.md +28 -0
- package/README.md +67 -10
- package/dist/bin/hive.js +157 -0
- package/node_modules/@hive/shared/dist/directStudio.d.ts +7 -0
- package/node_modules/@hive/shared/dist/directStudio.js +17 -0
- package/node_modules/@hive/shared/dist/index.d.ts +1 -0
- package/node_modules/@hive/shared/dist/index.js +1 -0
- package/node_modules/@hive/shared/dist/status.d.ts +1 -1
- package/node_modules/@hive/shared/dist/status.js +19 -7
- package/node_modules/@hive/shared/dist/types.d.ts +5 -0
- package/node_modules/@hive/shared/package.json +3 -0
- package/package.json +1 -1
- package/packages/server/dist/api/rest.js +165 -14
- package/packages/server/dist/control/codexRuntime.js +24 -4
- package/packages/server/dist/hooks/hookIngest.js +44 -1
- package/packages/server/dist/missions/missionsStore.js +2 -0
- package/packages/server/dist/plans/plansStore.js +35 -4
- package/packages/server/dist/roster/rosterBuilder.js +183 -19
- package/packages/server/dist/skills/skillDiscovery.js +10 -8
- package/packages/server/dist/skills/skillInvocation.js +11 -0
- package/packages/web/dist/assets/index-BrkIk6ny.js +11 -0
- package/packages/web/dist/assets/index-DJFn_ZsI.css +2 -0
- package/packages/web/dist/index.html +2 -2
- package/templates/agents/hive-orchestrator.md +6 -0
- package/packages/web/dist/assets/index-CrKMFCkZ.js +0 -11
- package/packages/web/dist/assets/index-gEGU_lr3.css +0 -2
|
@@ -19,6 +19,7 @@ function resultText(job) {
|
|
|
19
19
|
}
|
|
20
20
|
/** Joins Claude's short-lived jobs into durable, Hive-managed missions. */
|
|
21
21
|
export function buildFleetSnapshot(sessions, jobs, events, missions, messageStore, planStore, now = Date.now()) {
|
|
22
|
+
const workerCorrelationGraceMs = 8_000;
|
|
22
23
|
const orchestrators = [];
|
|
23
24
|
const other = [];
|
|
24
25
|
const sessionsByJob = new Map();
|
|
@@ -73,28 +74,163 @@ export function buildFleetSnapshot(sessions, jobs, events, missions, messageStor
|
|
|
73
74
|
eventIds.add(jobId);
|
|
74
75
|
if (session)
|
|
75
76
|
eventIds.add(session.sessionId);
|
|
76
|
-
const
|
|
77
|
-
.flatMap((id) => events.recentFor(id))
|
|
77
|
+
const allEvents = [...new Map([...eventIds]
|
|
78
|
+
.flatMap((id) => events.recentFor(id, 200))
|
|
78
79
|
.map((event) => [`${event.sessionId}:${event.jobId ?? ""}:${event.ts}:${event.phase}:${event.decisionId ?? ""}:${event.targetWorker ?? ""}:${event.detail}`, event])).values()]
|
|
79
|
-
.sort((a, b) => a.ts - b.ts)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const workers = group.flatMap(({ jobId, job }) => (job.fan ?? []).map((worker) => ({
|
|
80
|
+
.sort((a, b) => a.ts - b.ts);
|
|
81
|
+
let recentEvents = allEvents.slice(-100);
|
|
82
|
+
let workers = group.flatMap(({ jobId, job }) => (job.fan ?? []).filter((worker) => worker.kind === "agent" || worker.kind === "subagent").map((worker) => ({
|
|
83
83
|
...worker,
|
|
84
84
|
jobId,
|
|
85
85
|
jobState: job.state,
|
|
86
86
|
jobUpdatedAt: dateMs(job.updatedAt) || undefined,
|
|
87
87
|
})));
|
|
88
|
+
const jobIdBySession = new Map(group.flatMap(({ jobId, job }) => {
|
|
89
|
+
const linkedSession = sessionsByJob.get(jobId);
|
|
90
|
+
return [...(job.sessionId ? [[job.sessionId, jobId]] : []), ...(linkedSession ? [[linkedSession.sessionId, jobId]] : [])];
|
|
91
|
+
}));
|
|
92
|
+
const jobStateById = new Map(group.map(({ jobId, job }) => [jobId, job.state]));
|
|
93
|
+
const parentTurnFinished = (jobId) => ["done", "completed", "failed", "error", "cancelled", "canceled", "stopped"].includes((jobStateById.get(jobId) ?? "").toLowerCase());
|
|
94
|
+
const eventJobId = (event) => event.jobId ?? jobIdBySession.get(event.sessionId);
|
|
95
|
+
// A job's fan array is only a current view and can drop an earlier worker
|
|
96
|
+
// as soon as a later worker starts. Rebuild the durable native roster from
|
|
97
|
+
// hook history, keyed by Claude's real agent id, and merge fan metadata.
|
|
98
|
+
const nativeEvidence = new Map();
|
|
99
|
+
for (const event of allEvents) {
|
|
100
|
+
const resolvedJobId = eventJobId(event);
|
|
101
|
+
const raw = event.rawPayload && typeof event.rawPayload === "object" ? event.rawPayload : {};
|
|
102
|
+
const response = raw.tool_response && typeof raw.tool_response === "object" ? raw.tool_response : {};
|
|
103
|
+
const input = raw.tool_input && typeof raw.tool_input === "object" ? raw.tool_input : {};
|
|
104
|
+
const launchId = typeof response.agentId === "string" ? response.agentId : typeof response.agent_id === "string" ? response.agent_id : undefined;
|
|
105
|
+
const nativeId = launchId ?? event.targetWorker;
|
|
106
|
+
if (!resolvedJobId || !nativeId || nativeId === "hive-orchestrator")
|
|
107
|
+
continue;
|
|
108
|
+
const isStart = event.hookEventName === "SubagentStart" || (event.hookEventName === "PostToolUse" && event.toolName === "Agent" && Boolean(launchId));
|
|
109
|
+
const isStop = event.hookEventName === "SubagentStop";
|
|
110
|
+
if (!isStart && !isStop)
|
|
111
|
+
continue;
|
|
112
|
+
const label = typeof response.description === "string" ? response.description : typeof input.description === "string" ? input.description : nativeId;
|
|
113
|
+
const key = `${resolvedJobId}:${nativeId}`;
|
|
114
|
+
const prior = nativeEvidence.get(key);
|
|
115
|
+
// A stop without any observed launch is often a nested/background task
|
|
116
|
+
// notification. It is evidence, but not enough to invent a person.
|
|
117
|
+
if (isStop && !prior)
|
|
118
|
+
continue;
|
|
119
|
+
nativeEvidence.set(key, {
|
|
120
|
+
id: nativeId,
|
|
121
|
+
jobId: resolvedJobId,
|
|
122
|
+
label: prior?.label !== prior?.id ? prior.label : label,
|
|
123
|
+
startedAt: Math.min(prior?.startedAt ?? event.ts, event.ts),
|
|
124
|
+
doneAt: isStop ? event.ts : isStart ? undefined : prior?.doneAt,
|
|
125
|
+
updatedAt: event.ts,
|
|
126
|
+
running: isStart,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
const workerByNativeKey = new Map(workers.map((worker) => [`${worker.jobId}:${worker.id}`, worker]));
|
|
130
|
+
for (const native of nativeEvidence.values()) {
|
|
131
|
+
const key = `${native.jobId}:${native.id}`;
|
|
132
|
+
const fanWorker = workerByNativeKey.get(key);
|
|
133
|
+
const turnFinished = parentTurnFinished(native.jobId);
|
|
134
|
+
// SubagentStop is the worker's own terminal lifecycle boundary. The
|
|
135
|
+
// manager may remain idle/open for follow-up, but that must not keep a
|
|
136
|
+
// finished worker seated indefinitely. A later SubagentStart for the
|
|
137
|
+
// same native id clears native.doneAt above and resumes that person.
|
|
138
|
+
const workerFinished = Boolean(native.doneAt) || turnFinished;
|
|
139
|
+
const merged = fanWorker ? {
|
|
140
|
+
...fanWorker,
|
|
141
|
+
label: fanWorker.label || native.label,
|
|
142
|
+
startedAt: Math.min(fanWorker.startedAt, native.startedAt),
|
|
143
|
+
doneAt: native.doneAt ?? (turnFinished ? fanWorker.doneAt : undefined),
|
|
144
|
+
updatedAt: Math.max(fanWorker.updatedAt ?? 0, native.updatedAt),
|
|
145
|
+
jobState: workerFinished ? "done" : native.running ? "working" : "idle",
|
|
146
|
+
} : {
|
|
147
|
+
id: native.id,
|
|
148
|
+
kind: "agent",
|
|
149
|
+
label: native.label,
|
|
150
|
+
startedAt: native.startedAt,
|
|
151
|
+
doneAt: native.doneAt,
|
|
152
|
+
updatedAt: native.updatedAt,
|
|
153
|
+
jobId: native.jobId,
|
|
154
|
+
jobState: workerFinished ? "done" : native.running ? "working" : "idle",
|
|
155
|
+
};
|
|
156
|
+
workerByNativeKey.set(key, merged);
|
|
157
|
+
}
|
|
158
|
+
workers = [...workerByNativeKey.values()];
|
|
159
|
+
// A resumed/follow-up job may address the same provider-native agent id.
|
|
160
|
+
// The mission roster is person-oriented, so expose that id once using its
|
|
161
|
+
// latest lifecycle state instead of creating duplicate React/desk actors
|
|
162
|
+
// for each short-lived parent job.
|
|
163
|
+
const workersByIdentity = new Map();
|
|
164
|
+
for (const worker of workers.sort((a, b) => a.startedAt - b.startedAt || a.id.localeCompare(b.id))) {
|
|
165
|
+
const prior = workersByIdentity.get(worker.id);
|
|
166
|
+
if (!prior) {
|
|
167
|
+
workersByIdentity.set(worker.id, worker);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const priorFreshness = prior.updatedAt ?? prior.doneAt ?? prior.startedAt;
|
|
171
|
+
const workerFreshness = worker.updatedAt ?? worker.doneAt ?? worker.startedAt;
|
|
172
|
+
const latest = workerFreshness >= priorFreshness ? worker : prior;
|
|
173
|
+
workersByIdentity.set(worker.id, {
|
|
174
|
+
...latest,
|
|
175
|
+
label: latest.label || prior.label,
|
|
176
|
+
startedAt: Math.min(prior.startedAt, worker.startedAt),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
workers = [...workersByIdentity.values()];
|
|
180
|
+
// Friendly/custom worker names are presentation aliases only. Native
|
|
181
|
+
// Claude ids remain canonical so close or repeated delegations can never
|
|
182
|
+
// swap React identities or manufacture an extra avatar.
|
|
183
|
+
const workerAliases = new Map();
|
|
184
|
+
const claimedNativeWorkers = new Set();
|
|
185
|
+
for (const event of allEvents.filter((candidate) => candidate.source === "custom" && candidate.phase === "delegating" && candidate.targetWorker)) {
|
|
186
|
+
const resolvedJobId = eventJobId(event);
|
|
187
|
+
if (!resolvedJobId)
|
|
188
|
+
continue;
|
|
189
|
+
const aliasKey = `${resolvedJobId}:${event.targetWorker}`;
|
|
190
|
+
const candidate = workers
|
|
191
|
+
.filter((worker) => worker.jobId === resolvedJobId && !claimedNativeWorkers.has(worker.id) && Math.abs(worker.startedAt - event.ts) <= 30_000)
|
|
192
|
+
.sort((a, b) => Math.abs(a.startedAt - event.ts) - Math.abs(b.startedAt - event.ts))[0];
|
|
193
|
+
workerAliases.set(aliasKey, candidate?.id ?? aliasKey);
|
|
194
|
+
if (!candidate)
|
|
195
|
+
continue;
|
|
196
|
+
claimedNativeWorkers.add(candidate.id);
|
|
197
|
+
}
|
|
198
|
+
if (workerAliases.size)
|
|
199
|
+
recentEvents = recentEvents.map((event) => {
|
|
200
|
+
if (!event.targetWorker)
|
|
201
|
+
return event;
|
|
202
|
+
const resolvedJobId = eventJobId(event);
|
|
203
|
+
if (!resolvedJobId)
|
|
204
|
+
return event;
|
|
205
|
+
const key = `${resolvedJobId}:${event.targetWorker}`;
|
|
206
|
+
const targetWorker = workerAliases.get(key);
|
|
207
|
+
return targetWorker ? { ...event, targetWorker } : event;
|
|
208
|
+
});
|
|
209
|
+
// Claude's fan file can arrive just before its delegation hook. Publishing
|
|
210
|
+
// that unmatched native id for a single poll makes the office animate a
|
|
211
|
+
// second person. Hold it briefly for correlation; canonical/event-backed
|
|
212
|
+
// workers remain immediate, and hookless workers appear after the grace.
|
|
213
|
+
const correlatedWorkerIds = new Set(recentEvents.filter((event) => event.source === "custom" && event.phase === "delegating").map((event) => event.targetWorker).filter((id) => Boolean(id)));
|
|
214
|
+
workers = workers.filter((worker) => nativeEvidence.has(`${worker.jobId}:${worker.id}`) || correlatedWorkerIds.has(worker.id) || now - worker.startedAt >= workerCorrelationGraceMs);
|
|
215
|
+
const derived = deriveStatus(session, latest.job, recentEvents, now);
|
|
88
216
|
const workerIds = new Set(workers.map((worker) => worker.id));
|
|
89
217
|
for (const event of recentEvents) {
|
|
90
218
|
if (!event.targetWorker || workerIds.has(event.targetWorker))
|
|
91
219
|
continue;
|
|
92
220
|
const related = recentEvents.filter((candidate) => candidate.targetWorker === event.targetWorker);
|
|
93
|
-
const start = related.find((candidate) => candidate.phase === "delegating" || candidate.phase?.toLowerCase().includes("subagentstart"));
|
|
221
|
+
const start = related.find((candidate) => candidate.phase === "delegating" || candidate.phase?.toLowerCase().includes("subagentstart") || candidate.hookEventName?.toLowerCase() === "subagentstart");
|
|
94
222
|
if (!start)
|
|
95
223
|
continue;
|
|
96
|
-
|
|
97
|
-
|
|
224
|
+
// A friendly delegation target is an alias, not a person. Hold every
|
|
225
|
+
// unmatched identity through the correlation window; a native Agent id
|
|
226
|
+
// normally arrives within milliseconds and becomes the sole actor.
|
|
227
|
+
if (now - start.ts < workerCorrelationGraceMs)
|
|
228
|
+
continue;
|
|
229
|
+
const done = [...related].reverse().find((candidate) => candidate.phase === "worker_reported" || candidate.phase?.toLowerCase().includes("subagentstop") || candidate.hookEventName?.toLowerCase() === "subagentstop");
|
|
230
|
+
const resolvedJobId = eventJobId(event);
|
|
231
|
+
if (!resolvedJobId)
|
|
232
|
+
continue;
|
|
233
|
+
workers.push({ id: event.targetWorker, label: event.targetWorker, kind: "subagent", startedAt: start.ts, doneAt: done?.ts, updatedAt: related.at(-1)?.ts, jobId: resolvedJobId, jobState: done ? "done" : latest.job.state });
|
|
98
234
|
workerIds.add(event.targetWorker);
|
|
99
235
|
}
|
|
100
236
|
const derivedMessages = group.flatMap(({ jobId, job }) => {
|
|
@@ -141,7 +277,15 @@ export function buildFleetSnapshot(sessions, jobs, events, missions, messageStor
|
|
|
141
277
|
const missionPlan = planStore?.get(missionId);
|
|
142
278
|
const inactiveForMs = Math.max(0, now - updatedAt);
|
|
143
279
|
let activityStatus = derived.status;
|
|
144
|
-
if (mission.lifecycleStatus
|
|
280
|
+
if (mission.lifecycleStatus === "completed" || mission.lifecycleStatus === "archived") {
|
|
281
|
+
// Durable terminal lifecycle is authoritative over a stale provider ask,
|
|
282
|
+
// parked permission, or process record left behind by the final turn.
|
|
283
|
+
activityStatus = "done";
|
|
284
|
+
}
|
|
285
|
+
else if (mission.lifecycleStatus === "failed") {
|
|
286
|
+
activityStatus = "error";
|
|
287
|
+
}
|
|
288
|
+
else if (mission.lifecycleStatus !== "active" && !session) {
|
|
145
289
|
activityStatus = "offline";
|
|
146
290
|
}
|
|
147
291
|
else if (mission.lifecycleStatus === "active") {
|
|
@@ -177,7 +321,7 @@ export function buildFleetSnapshot(sessions, jobs, events, missions, messageStor
|
|
|
177
321
|
activityStatus,
|
|
178
322
|
turnCompleted: latest.job.state === "done",
|
|
179
323
|
inactiveForMs,
|
|
180
|
-
waitingFor: session?.waitingFor ?? latest.job.needs,
|
|
324
|
+
waitingFor: mission.lifecycleStatus === "completed" || mission.lifecycleStatus === "archived" ? undefined : session?.waitingFor ?? latest.job.needs,
|
|
181
325
|
worktreePath: latest.job.worktreePath ?? oldest.job.worktreePath,
|
|
182
326
|
worktreeBranch: latest.job.worktreeBranch ?? oldest.job.worktreeBranch,
|
|
183
327
|
tokens: group.reduce((total, { job }) => total + (job.tokens ?? 0), 0),
|
|
@@ -203,7 +347,7 @@ export function buildFleetSnapshot(sessions, jobs, events, missions, messageStor
|
|
|
203
347
|
continue;
|
|
204
348
|
const jobEntry = session.jobId ? jobs.get(session.jobId) : undefined;
|
|
205
349
|
const recentEvents = events.recentFor(session.sessionId);
|
|
206
|
-
const { status } = deriveStatus(session, jobEntry, recentEvents);
|
|
350
|
+
const { status } = deriveStatus(session, jobEntry, recentEvents, now);
|
|
207
351
|
other.push({
|
|
208
352
|
jobId: session.jobId,
|
|
209
353
|
sessionId: session.sessionId,
|
|
@@ -219,25 +363,45 @@ export function buildFleetSnapshot(sessions, jobs, events, missions, messageStor
|
|
|
219
363
|
}
|
|
220
364
|
orchestrators.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
221
365
|
const decisions = orchestrators.flatMap((node) => {
|
|
366
|
+
if (node.lifecycleStatus === "completed" || node.lifecycleStatus === "archived")
|
|
367
|
+
return [];
|
|
222
368
|
const resolved = new Set(node.recentEvents.filter((event) => event.phase === "decision_resolved" && event.decisionId).map((event) => event.decisionId));
|
|
223
369
|
const missionContext = { missionObjective: node.mission.objective || undefined, repository: node.mission.repository };
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
370
|
+
// Only the current, mid-turn blocker belongs in the decision inbox. An
|
|
371
|
+
// unresolved event from an older/completed turn is history: the mission
|
|
372
|
+
// may remain open for optional follow-up, but it is not waiting on the
|
|
373
|
+
// user. Requiring the blocker to be the latest observed event also clears
|
|
374
|
+
// it as soon as provider activity resumes.
|
|
375
|
+
const latestEvent = node.recentEvents.at(-1);
|
|
376
|
+
const fromEvents = !node.turnCompleted && latestEvent?.phase === "blocked_on_user"
|
|
377
|
+
&& latestEvent.decisionId && !resolved.has(latestEvent.decisionId)
|
|
378
|
+
&& (!latestEvent.jobId || latestEvent.jobId === node.jobId)
|
|
379
|
+
? [{
|
|
380
|
+
id: latestEvent.decisionId, missionId: node.missionId, missionName: node.name,
|
|
381
|
+
kind: latestEvent.decisionKind ?? "question", question: latestEvent.detail, context: latestEvent.context,
|
|
382
|
+
choices: latestEvent.choices ?? [], recommendation: latestEvent.recommendation, impact: latestEvent.impact, createdAt: latestEvent.ts, ...missionContext,
|
|
383
|
+
}]
|
|
384
|
+
: [];
|
|
229
385
|
// A permission prompt parks the session mid-turn (tempo blocked, the ask
|
|
230
386
|
// in needs) with no TTY to answer it — surface it as an answerable
|
|
231
387
|
// decision. It self-clears once the job is no longer parked.
|
|
232
388
|
const latestJob = jobs.get(node.jobId);
|
|
233
|
-
const parked = latestJob?.tempo === "blocked" && latestJob.needs && /^approve\s/i.test(latestJob.needs) && !resolved.has(`perm:${node.jobId}`)
|
|
389
|
+
const parked = !node.turnCompleted && latestJob?.tempo === "blocked" && latestJob.needs && /^approve\s/i.test(latestJob.needs) && !resolved.has(`perm:${node.jobId}`)
|
|
234
390
|
? [{
|
|
235
391
|
id: `perm:${node.jobId}`, missionId: node.missionId, missionName: node.name,
|
|
236
392
|
kind: "permission", question: latestJob.needs, context: latestJob.detail,
|
|
237
393
|
choices: ["Approve & continue", "Deny"], impact: "The session is parked at this prompt until you answer.", createdAt: node.updatedAt, ...missionContext,
|
|
238
394
|
}]
|
|
239
395
|
: [];
|
|
240
|
-
|
|
396
|
+
const hasStructuredPrompt = node.recentEvents.some((event) => event.jobId === node.jobId && event.phase === "blocked_on_user" && event.decisionId?.startsWith("skill-prompt:"));
|
|
397
|
+
const providerPrompt = !node.turnCompleted && latestJob?.tempo === "blocked" && latestJob.needs && (latestJob.promptChoices?.length ?? 0) > 0 && !/^approve\s/i.test(latestJob.needs) && !/rate limit|spend limit/i.test(latestJob.needs) && !hasStructuredPrompt && !resolved.has(`prompt:${node.jobId}`)
|
|
398
|
+
? [{
|
|
399
|
+
id: `prompt:${node.jobId}`, missionId: node.missionId, missionName: node.name,
|
|
400
|
+
kind: "question", question: latestJob.needs, context: latestJob.detail,
|
|
401
|
+
choices: latestJob.promptChoices ?? [], impact: "The provider is waiting for this answer before the skill can continue.", createdAt: node.updatedAt, ...missionContext,
|
|
402
|
+
}]
|
|
403
|
+
: [];
|
|
404
|
+
return [...fromEvents, ...parked, ...providerPrompt];
|
|
241
405
|
}).sort((a, b) => b.createdAt - a.createdAt);
|
|
242
406
|
return { orchestrators, decisions, other, generatedAt: Date.now() };
|
|
243
407
|
}
|
|
@@ -50,17 +50,19 @@ function collectPluginSkills(root) {
|
|
|
50
50
|
}
|
|
51
51
|
return output;
|
|
52
52
|
}
|
|
53
|
-
export function discoverSkills(cwd) {
|
|
53
|
+
export function discoverSkills(cwd, provider = "claude") {
|
|
54
54
|
const project = requireWorkingDirectory(cwd);
|
|
55
|
-
const
|
|
55
|
+
const claudeConfig = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
|
|
56
|
+
const codexHome = process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
|
|
57
|
+
const all = provider === "codex" ? [
|
|
58
|
+
...collect(path.join(project, ".codex", "skills"), "project"),
|
|
59
|
+
...collect(path.join(codexHome, "skills"), "user"),
|
|
60
|
+
...collectPluginSkills(path.join(codexHome, "plugins", "cache")),
|
|
61
|
+
] : [
|
|
56
62
|
...collect(path.join(project, ".claude", "skills"), "project"),
|
|
57
63
|
...collect(path.join(project, ".claude", "commands"), "project"),
|
|
58
|
-
...collect(path.join(
|
|
59
|
-
...collect(path.join(
|
|
60
|
-
...collectPluginSkills(path.join(os.homedir(), ".claude", "plugins")),
|
|
61
|
-
...collect(path.join(project, ".codex", "skills"), "project"),
|
|
62
|
-
...collect(path.join(os.homedir(), ".codex", "skills"), "user"),
|
|
63
|
-
...collectPluginSkills(path.join(os.homedir(), ".codex", "plugins", "cache")),
|
|
64
|
+
...collect(path.join(claudeConfig, "skills"), "user"),
|
|
65
|
+
...collect(path.join(claudeConfig, "commands"), "user"),
|
|
64
66
|
];
|
|
65
67
|
const unique = new Map();
|
|
66
68
|
for (const skill of all.reverse())
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hive exposes one consistent `/skill` syntax in its UI. Codex invokes skills
|
|
3
|
+
* with `$skill`, so translate only names that were discovered for the active
|
|
4
|
+
* provider, machine, and working directory. Unknown slash-prefixed text is
|
|
5
|
+
* left alone (it may be a path or ordinary prose).
|
|
6
|
+
*/
|
|
7
|
+
export function translateSkillInvocations(text, provider, availableSkillNames) {
|
|
8
|
+
if (provider !== "codex" || availableSkillNames.size === 0)
|
|
9
|
+
return text;
|
|
10
|
+
return text.replace(/(^|\s)\/([a-zA-Z0-9][a-zA-Z0-9:_-]*)(?=\s|$)/g, (match, prefix, name) => availableSkillNames.has(name) ? `${prefix}$${name}` : match);
|
|
11
|
+
}
|