@shanesaravia/hive 0.1.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/LICENSE +21 -0
  3. package/README.md +417 -0
  4. package/dist/bin/hive-emit.js +75 -0
  5. package/dist/bin/hive.js +506 -0
  6. package/node_modules/@hive/shared/dist/index.d.ts +2 -0
  7. package/node_modules/@hive/shared/dist/index.js +2 -0
  8. package/node_modules/@hive/shared/dist/status.d.ts +12 -0
  9. package/node_modules/@hive/shared/dist/status.js +52 -0
  10. package/node_modules/@hive/shared/dist/types.d.ts +384 -0
  11. package/node_modules/@hive/shared/dist/types.js +14 -0
  12. package/node_modules/@hive/shared/package.json +18 -0
  13. package/package.json +72 -0
  14. package/packages/server/dist/api/rest.js +793 -0
  15. package/packages/server/dist/api/ws.js +37 -0
  16. package/packages/server/dist/config.js +24 -0
  17. package/packages/server/dist/control/codexRuntime.js +169 -0
  18. package/packages/server/dist/control/killer.js +25 -0
  19. package/packages/server/dist/control/launcher.js +114 -0
  20. package/packages/server/dist/control/messaging.js +75 -0
  21. package/packages/server/dist/control/nativeCommands.js +29 -0
  22. package/packages/server/dist/control/permissionPark.js +23 -0
  23. package/packages/server/dist/control/providerModels.js +53 -0
  24. package/packages/server/dist/events/eventsStore.js +55 -0
  25. package/packages/server/dist/health/deriveAlerts.js +55 -0
  26. package/packages/server/dist/hooks/hookIngest.js +90 -0
  27. package/packages/server/dist/hooks/hookSpool.js +33 -0
  28. package/packages/server/dist/hooks/setupHooks.js +102 -0
  29. package/packages/server/dist/index.js +88 -0
  30. package/packages/server/dist/messages/messagesStore.js +211 -0
  31. package/packages/server/dist/missions/missionsStore.js +283 -0
  32. package/packages/server/dist/paths/pathResolver.js +167 -0
  33. package/packages/server/dist/plans/plansStore.js +212 -0
  34. package/packages/server/dist/policies/policiesStore.js +61 -0
  35. package/packages/server/dist/reports/githubPublisher.js +21 -0
  36. package/packages/server/dist/reports/missionReport.js +16 -0
  37. package/packages/server/dist/roster/rosterBuilder.js +243 -0
  38. package/packages/server/dist/security/originPolicy.js +31 -0
  39. package/packages/server/dist/skills/skillDiscovery.js +69 -0
  40. package/packages/server/dist/templates/templateDiscovery.js +97 -0
  41. package/packages/server/dist/watch/jobsWatcher.js +224 -0
  42. package/packages/server/dist/watch/sessionsWatcher.js +65 -0
  43. package/packages/web/dist/assets/index-CrKMFCkZ.js +11 -0
  44. package/packages/web/dist/assets/index-gEGU_lr3.css +2 -0
  45. package/packages/web/dist/favicon.svg +12 -0
  46. package/packages/web/dist/index.html +14 -0
  47. package/templates/agents/hive-orchestrator.md +42 -0
@@ -0,0 +1,224 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { config } from "../config.js";
5
+ /**
6
+ * Watches ~/.claude/jobs/<jobId>/state.json for every job directory (fs.watch
7
+ * with recursive: true on macOS) and tails each job's timeline.jsonl by byte
8
+ * offset rather than re-reading the whole file, since these logs grow large.
9
+ */
10
+ export class JobsWatcher {
11
+ jobs = new Map();
12
+ timelineOffsets = new Map();
13
+ jobsListeners = new Set();
14
+ timelineListeners = new Set();
15
+ debounceTimer;
16
+ watchers = [];
17
+ start() {
18
+ const directories = [config.jobsDir, config.codexJobsDir];
19
+ for (const directory of directories)
20
+ fs.mkdirSync(directory, { recursive: true });
21
+ this.reloadAll();
22
+ this.watchers = directories.map((directory) => fs.watch(directory, { persistent: true, recursive: true }, () => {
23
+ this.scheduleReload();
24
+ }));
25
+ }
26
+ stop() {
27
+ for (const watcher of this.watchers)
28
+ watcher.close();
29
+ if (this.debounceTimer)
30
+ clearTimeout(this.debounceTimer);
31
+ }
32
+ onJobsChange(listener) {
33
+ this.jobsListeners.add(listener);
34
+ }
35
+ onTimelineAppend(listener) {
36
+ this.timelineListeners.add(listener);
37
+ }
38
+ getAll() {
39
+ return this.jobs;
40
+ }
41
+ scheduleReload() {
42
+ if (this.debounceTimer)
43
+ clearTimeout(this.debounceTimer);
44
+ this.debounceTimer = setTimeout(() => this.reloadAll(), 50);
45
+ }
46
+ reloadAll() {
47
+ const next = new Map();
48
+ for (const directory of [config.jobsDir, config.codexJobsDir])
49
+ for (const jobId of listDirectories(directory)) {
50
+ const statePath = path.join(directory, jobId, "state.json");
51
+ try {
52
+ const raw = fs.readFileSync(statePath, "utf-8");
53
+ const job = JSON.parse(raw);
54
+ // The daemon compresses the model's final message: blocked turns keep
55
+ // only a terse question in needs/detail, and done turns store a
56
+ // one-line summary in output.result. The full reply lives in the
57
+ // session transcript (timeline text is a 4KB tail as fallback).
58
+ if (job.state === "done" || job.state === "blocked") {
59
+ job.lastText = this.resolveLastText(jobId, job, directory);
60
+ }
61
+ next.set(jobId, job);
62
+ }
63
+ catch {
64
+ continue;
65
+ }
66
+ this.tailTimeline(jobId, directory);
67
+ }
68
+ this.jobs = next;
69
+ for (const listener of this.jobsListeners)
70
+ listener(this.jobs);
71
+ }
72
+ lastTextCache = new Map();
73
+ /** Cached per job/updatedAt so reloads don't re-read transcripts. */
74
+ resolveLastText(jobId, job, directory) {
75
+ const cached = this.lastTextCache.get(jobId);
76
+ if (cached && cached.updatedAt === job.updatedAt)
77
+ return cached.text;
78
+ const text = readTranscriptFinalText(job.sessionId)
79
+ ?? readLatestTimelineText(path.join(directory, jobId, "timeline.jsonl"));
80
+ this.lastTextCache.set(jobId, { updatedAt: job.updatedAt, text });
81
+ return text;
82
+ }
83
+ tailTimeline(jobId, directory) {
84
+ const timelinePath = path.join(directory, jobId, "timeline.jsonl");
85
+ let size;
86
+ try {
87
+ size = fs.statSync(timelinePath).size;
88
+ }
89
+ catch {
90
+ return;
91
+ }
92
+ const prevOffset = this.timelineOffsets.get(jobId) ?? 0;
93
+ if (size <= prevOffset) {
94
+ if (size < prevOffset)
95
+ this.timelineOffsets.set(jobId, 0); // file truncated/rotated
96
+ return;
97
+ }
98
+ const fd = fs.openSync(timelinePath, "r");
99
+ try {
100
+ const buf = Buffer.alloc(size - prevOffset);
101
+ fs.readSync(fd, buf, 0, buf.length, prevOffset);
102
+ this.timelineOffsets.set(jobId, size);
103
+ const lines = buf
104
+ .toString("utf-8")
105
+ .split("\n")
106
+ .map((l) => l.trim())
107
+ .filter(Boolean);
108
+ if (lines.length) {
109
+ for (const listener of this.timelineListeners)
110
+ listener(jobId, lines);
111
+ }
112
+ }
113
+ finally {
114
+ fs.closeSync(fd);
115
+ }
116
+ }
117
+ }
118
+ const TRANSCRIPT_TAIL_BYTES = 256 * 1024;
119
+ /** Locates the session's transcript under ~/.claude/projects and returns the
120
+ * last assistant text message — the model's actual final reply. */
121
+ function readTranscriptFinalText(sessionId) {
122
+ if (!sessionId)
123
+ return undefined;
124
+ const root = path.join(os.homedir(), ".claude", "projects");
125
+ let dirs;
126
+ try {
127
+ dirs = fs.readdirSync(root);
128
+ }
129
+ catch {
130
+ return undefined;
131
+ }
132
+ for (const dir of dirs) {
133
+ const file = path.join(root, dir, `${sessionId}.jsonl`);
134
+ if (!fs.existsSync(file))
135
+ continue;
136
+ return finalAssistantText(file);
137
+ }
138
+ return undefined;
139
+ }
140
+ function finalAssistantText(file) {
141
+ let fd;
142
+ try {
143
+ fd = fs.openSync(file, "r");
144
+ }
145
+ catch {
146
+ return undefined;
147
+ }
148
+ try {
149
+ const size = fs.fstatSync(fd).size;
150
+ const offset = Math.max(0, size - TRANSCRIPT_TAIL_BYTES);
151
+ const buf = Buffer.alloc(size - offset);
152
+ fs.readSync(fd, buf, 0, buf.length, offset);
153
+ const lines = buf.toString("utf-8").split("\n");
154
+ // Skip a line truncated by the tail window (index 0 when offset > 0).
155
+ for (let i = lines.length - 1; i >= (offset > 0 ? 1 : 0); i--) {
156
+ const line = lines[i].trim();
157
+ if (!line)
158
+ continue;
159
+ try {
160
+ const entry = JSON.parse(line);
161
+ if (entry.type !== "assistant" || !Array.isArray(entry.message?.content))
162
+ continue;
163
+ const text = entry.message.content
164
+ .filter((item) => item?.type === "text" && typeof item.text === "string")
165
+ .map((item) => item.text)
166
+ .join("\n")
167
+ .trim();
168
+ if (text)
169
+ return text;
170
+ }
171
+ catch {
172
+ continue;
173
+ }
174
+ }
175
+ return undefined;
176
+ }
177
+ finally {
178
+ fs.closeSync(fd);
179
+ }
180
+ }
181
+ const TIMELINE_TAIL_BYTES = 64 * 1024;
182
+ /** Latest non-empty assistant `text` from the tail of a timeline.jsonl. */
183
+ function readLatestTimelineText(timelinePath) {
184
+ let fd;
185
+ try {
186
+ fd = fs.openSync(timelinePath, "r");
187
+ }
188
+ catch {
189
+ return undefined;
190
+ }
191
+ try {
192
+ const size = fs.fstatSync(fd).size;
193
+ const offset = Math.max(0, size - TIMELINE_TAIL_BYTES);
194
+ const buf = Buffer.alloc(size - offset);
195
+ fs.readSync(fd, buf, 0, buf.length, offset);
196
+ const lines = buf.toString("utf-8").split("\n");
197
+ // Skip a line truncated by the tail window (index 0 when offset > 0).
198
+ for (let i = lines.length - 1; i >= (offset > 0 ? 1 : 0); i--) {
199
+ const line = lines[i].trim();
200
+ if (!line)
201
+ continue;
202
+ try {
203
+ const entry = JSON.parse(line);
204
+ if (typeof entry.text === "string" && entry.text.trim())
205
+ return entry.text;
206
+ }
207
+ catch {
208
+ continue;
209
+ }
210
+ }
211
+ return undefined;
212
+ }
213
+ finally {
214
+ fs.closeSync(fd);
215
+ }
216
+ }
217
+ function listDirectories(directory) {
218
+ try {
219
+ return fs.readdirSync(directory, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
220
+ }
221
+ catch {
222
+ return [];
223
+ }
224
+ }
@@ -0,0 +1,65 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { config } from "../config.js";
4
+ /**
5
+ * Watches ~/.claude/sessions/*.json with fs.watch (no polling) and keeps an
6
+ * in-memory map of live sessions keyed by pid. Debounced to coalesce bursts
7
+ * of writes into a single roster rebuild.
8
+ */
9
+ export class SessionsWatcher {
10
+ sessions = new Map();
11
+ listeners = new Set();
12
+ debounceTimer;
13
+ watchers = [];
14
+ start() {
15
+ const directories = [config.sessionsDir, config.codexSessionsDir];
16
+ for (const directory of directories)
17
+ fs.mkdirSync(directory, { recursive: true });
18
+ this.reloadAll();
19
+ this.watchers = directories.map((directory) => fs.watch(directory, { persistent: true }, () => {
20
+ this.scheduleReload();
21
+ }));
22
+ }
23
+ stop() {
24
+ for (const watcher of this.watchers)
25
+ watcher.close();
26
+ if (this.debounceTimer)
27
+ clearTimeout(this.debounceTimer);
28
+ }
29
+ onChange(listener) {
30
+ this.listeners.add(listener);
31
+ }
32
+ getAll() {
33
+ return this.sessions;
34
+ }
35
+ scheduleReload() {
36
+ if (this.debounceTimer)
37
+ clearTimeout(this.debounceTimer);
38
+ this.debounceTimer = setTimeout(() => this.reloadAll(), 50);
39
+ }
40
+ reloadAll() {
41
+ const next = new Map();
42
+ for (const directory of [config.sessionsDir, config.codexSessionsDir])
43
+ for (const entry of safeEntries(directory)) {
44
+ if (!entry.endsWith(".json"))
45
+ continue;
46
+ try {
47
+ const raw = fs.readFileSync(path.join(directory, entry), "utf-8");
48
+ const parsed = JSON.parse(raw);
49
+ next.set(parsed.pid, parsed);
50
+ }
51
+ catch {
52
+ // file mid-write or malformed; skip this cycle, next watch event will retry
53
+ }
54
+ }
55
+ this.sessions = next;
56
+ for (const listener of this.listeners)
57
+ listener(this.sessions);
58
+ }
59
+ }
60
+ function safeEntries(directory) { try {
61
+ return fs.readdirSync(directory);
62
+ }
63
+ catch {
64
+ return [];
65
+ } }