@shigureni/minion 0.1.0 → 0.3.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 (58) hide show
  1. package/README.md +79 -33
  2. package/cli/src/app.ts +2 -0
  3. package/cli/src/factory.ts +8 -1
  4. package/cli/src/index.ts +15 -2
  5. package/cli/src/lib/body-validator.ts +13 -0
  6. package/cli/src/lib/format.ts +36 -0
  7. package/cli/src/router.ts +2 -1
  8. package/cli/src/routes/config/get/[key]/route.ts +7 -7
  9. package/cli/src/routes/config/list/route.ts +4 -5
  10. package/cli/src/routes/config/set/[key]/[value]/route.ts +5 -7
  11. package/cli/src/routes/dev/route.ts +12 -7
  12. package/cli/src/routes/dex/route.ts +66 -0
  13. package/cli/src/routes/kill/route.ts +7 -5
  14. package/cli/src/routes/not-found.ts +1 -1
  15. package/cli/src/routes/reboot/route.ts +12 -7
  16. package/cli/src/routes/start/route.ts +7 -6
  17. package/cli/src/routes/status/route.ts +5 -5
  18. package/lib/engine/app/app-paths.ts +46 -0
  19. package/lib/engine/app/app-runner.ts +235 -0
  20. package/lib/engine/app/source-hash.ts +34 -0
  21. package/lib/engine/collection/achievements.ts +143 -0
  22. package/lib/engine/collection/collection-store.ts +43 -0
  23. package/lib/engine/collection/collection-tracker.ts +97 -0
  24. package/lib/engine/collection/moon.ts +26 -0
  25. package/lib/engine/collection/species.ts +275 -0
  26. package/lib/engine/config/config-store.ts +36 -0
  27. package/lib/engine/errors.ts +20 -0
  28. package/lib/engine/fs/file-system.ts +31 -0
  29. package/lib/engine/fs/json-file-store.ts +57 -0
  30. package/lib/engine/fs/memory-file-system.ts +106 -0
  31. package/lib/engine/fs/node-file-system.ts +87 -0
  32. package/lib/engine/gateway/gateway-daemon.ts +14 -0
  33. package/lib/engine/gateway/gateway-routes.ts +17 -0
  34. package/lib/engine/gateway/gateway-server.ts +126 -0
  35. package/lib/engine/gateway/pet-behavior.ts +164 -0
  36. package/lib/engine/gateway/resolve-daemon-script.ts +7 -0
  37. package/lib/engine/gateway/sessions.ts +127 -0
  38. package/lib/engine/process/memory-process-runner.ts +67 -0
  39. package/lib/engine/process/node-process-runner.ts +51 -0
  40. package/lib/engine/process/process-runner.ts +21 -0
  41. package/lib/engine/random/memory-random-source.ts +22 -0
  42. package/lib/engine/random/node-random-source.ts +7 -0
  43. package/lib/engine/random/random-source.ts +9 -0
  44. package/lib/engine/stats/date-window.ts +31 -0
  45. package/lib/engine/stats/session-stats-tracker.ts +125 -0
  46. package/lib/engine/stats/stats-collector.ts +100 -0
  47. package/lib/engine/stats/stats-snapshot.ts +31 -0
  48. package/lib/engine/stats/token-usage-tracker.ts +170 -0
  49. package/lib/engine/time/clock.ts +11 -0
  50. package/lib/engine/time/memory-clock.ts +26 -0
  51. package/lib/engine/time/node-clock.ts +7 -0
  52. package/lib/index.ts +65 -0
  53. package/lib/minion.ts +194 -0
  54. package/package.json +9 -1
  55. package/swift/Sources/open-minion/open_minion.swift +5 -1
  56. package/tsconfig.json +3 -2
  57. package/cli/src/gateway.ts +0 -204
  58. package/cli/src/lib/process.ts +0 -204
@@ -0,0 +1,125 @@
1
+ import { z } from "zod"
2
+ import type { MinionFileSystem } from "@lib/engine/fs/file-system"
3
+ import { JsonFileStore } from "@lib/engine/fs/json-file-store"
4
+ import type { SessionInfo } from "@lib/engine/gateway/sessions"
5
+ import { isNextDay } from "@lib/engine/stats/date-window"
6
+
7
+ /**
8
+ * Per-field `.catch()` defaults double as the schema-evolution backfill: a
9
+ * file written by an older version of this tracker (missing a field, e.g.
10
+ * `seenProjects` was added later) reads back with that field defaulted
11
+ * instead of the whole file being discarded.
12
+ */
13
+ const schema = z.object({
14
+ totalSessionsSeen: z.number().catch(0),
15
+ maxConcurrentSessions: z.number().catch(0),
16
+ /** Bounded, most-recent-first log of distinct session ids, used only to dedup lifetime counting across restarts. */
17
+ seenSessionIds: z.array(z.string()).catch([]),
18
+ /** Bounded log of distinct working directories seen, used only to dedup `uniqueProjectsSeen` across restarts. */
19
+ seenProjects: z.array(z.string()).catch([]),
20
+ currentStreakDays: z.number().catch(0),
21
+ longestStreakDays: z.number().catch(0),
22
+ /** "YYYY-MM-DD" (UTC) of the last `record()` call that saw at least one active session. */
23
+ lastActiveDate: z.string().nullable().catch(null),
24
+ })
25
+
26
+ type SessionStatsData = z.infer<typeof schema>
27
+
28
+ const EMPTY: SessionStatsData = {
29
+ totalSessionsSeen: 0,
30
+ maxConcurrentSessions: 0,
31
+ seenSessionIds: [],
32
+ seenProjects: [],
33
+ currentStreakDays: 0,
34
+ longestStreakDays: 0,
35
+ lastActiveDate: null,
36
+ }
37
+
38
+ // Bounds the dedup logs' size; an id/project aged out here can be recounted
39
+ // as "new" if it somehow reappears, which only over-counts figures that are
40
+ // already just fun numbers, not something that needs to be exact.
41
+ const MAX_SEEN_IDS = 5000
42
+ const MAX_SEEN_PROJECTS = 2000
43
+
44
+ export type SessionStatsSummary = {
45
+ totalSessionsSeen: number
46
+ currentConcurrentSessions: number
47
+ maxConcurrentSessions: number
48
+ uniqueProjectsSeen: number
49
+ currentStreakDays: number
50
+ longestStreakDays: number
51
+ }
52
+
53
+ /**
54
+ * Tracks lifetime distinct session/project counts, the concurrency
55
+ * high-water-mark, and a day-streak of "was any session active that day",
56
+ * all persisted across gateway restarts.
57
+ */
58
+ export class SessionStatsTracker {
59
+ private readonly store: JsonFileStore<SessionStatsData>
60
+
61
+ constructor(props: { fs: MinionFileSystem; path: string }) {
62
+ this.store = new JsonFileStore({ fs: props.fs, path: props.path, schema, defaultValue: EMPTY })
63
+ }
64
+
65
+ /** Records the latest active-session snapshot (at `now`) and returns the updated summary, or the write failure. */
66
+ record(activeSessions: Map<string, SessionInfo>, now: Date): SessionStatsSummary | Error {
67
+ const data = this.store.read()
68
+ const seenIds = new Set(data.seenSessionIds)
69
+ const seenProjects = new Set(data.seenProjects)
70
+
71
+ for (const info of activeSessions.values()) {
72
+ if (info.cwd) seenProjects.add(info.cwd)
73
+ }
74
+
75
+ for (const id of activeSessions.keys()) {
76
+ if (seenIds.has(id)) continue
77
+ seenIds.add(id)
78
+ data.totalSessionsSeen += 1
79
+ }
80
+
81
+ data.seenSessionIds = Array.from(seenIds).slice(-MAX_SEEN_IDS)
82
+ data.seenProjects = Array.from(seenProjects).slice(-MAX_SEEN_PROJECTS)
83
+ data.maxConcurrentSessions = Math.max(data.maxConcurrentSessions, activeSessions.size)
84
+
85
+ if (activeSessions.size > 0) {
86
+ this.recordActiveDay(data, now.toISOString().slice(0, 10))
87
+ }
88
+
89
+ const writeError = this.store.write(data)
90
+ if (writeError) return writeError
91
+
92
+ return {
93
+ totalSessionsSeen: data.totalSessionsSeen,
94
+ currentConcurrentSessions: activeSessions.size,
95
+ maxConcurrentSessions: data.maxConcurrentSessions,
96
+ uniqueProjectsSeen: data.seenProjects.length,
97
+ currentStreakDays: data.currentStreakDays,
98
+ longestStreakDays: data.longestStreakDays,
99
+ }
100
+ }
101
+
102
+ /** Persisted totals without recording a new sample; `currentConcurrentSessions` is always 0 here since no live sessions were read. */
103
+ summary(): SessionStatsSummary {
104
+ const data = this.store.read()
105
+ return {
106
+ totalSessionsSeen: data.totalSessionsSeen,
107
+ currentConcurrentSessions: 0,
108
+ maxConcurrentSessions: data.maxConcurrentSessions,
109
+ uniqueProjectsSeen: data.seenProjects.length,
110
+ currentStreakDays: data.currentStreakDays,
111
+ longestStreakDays: data.longestStreakDays,
112
+ }
113
+ }
114
+
115
+ private recordActiveDay(data: SessionStatsData, today: string): void {
116
+ if (data.lastActiveDate === today) return // already recorded today
117
+
118
+ data.currentStreakDays =
119
+ data.lastActiveDate !== null && isNextDay(data.lastActiveDate, today)
120
+ ? data.currentStreakDays + 1
121
+ : 1
122
+ data.longestStreakDays = Math.max(data.longestStreakDays, data.currentStreakDays)
123
+ data.lastActiveDate = today
124
+ }
125
+ }
@@ -0,0 +1,100 @@
1
+ import type { MinionFileSystem } from "@lib/engine/fs/file-system"
2
+ import type { MinionProcessRunner } from "@lib/engine/process/process-runner"
3
+ import type { MinionClock } from "@lib/engine/time/clock"
4
+ import { readActiveSessions } from "@lib/engine/gateway/sessions"
5
+ import type { SessionStatsSummary } from "@lib/engine/stats/session-stats-tracker"
6
+ import { SessionStatsTracker } from "@lib/engine/stats/session-stats-tracker"
7
+ import type { TokenUsageSummary } from "@lib/engine/stats/token-usage-tracker"
8
+ import { TokenUsageTracker } from "@lib/engine/stats/token-usage-tracker"
9
+ import { type StatsSnapshot, timeBucketForHour } from "@lib/engine/stats/stats-snapshot"
10
+ import { sumRecentDays } from "@lib/engine/stats/date-window"
11
+
12
+ const WEEK_DAYS = 7
13
+
14
+ type Props = {
15
+ fs: MinionFileSystem
16
+ process: MinionProcessRunner
17
+ clock: MinionClock
18
+ sessionsDir: string
19
+ projectsDir: string
20
+ sessionStats: SessionStatsTracker
21
+ tokenUsage: TokenUsageTracker
22
+ }
23
+
24
+ /**
25
+ * Combines session tracking and token-usage scanning into the single
26
+ * `StatsSnapshot` that achievement and minion-species conditions evaluate
27
+ * against.
28
+ */
29
+ export class MinionStatsCollector {
30
+ private readonly fs: MinionFileSystem
31
+ private readonly process: MinionProcessRunner
32
+ private readonly clock: MinionClock
33
+ private readonly sessionsDir: string
34
+ private readonly projectsDir: string
35
+ private readonly sessionStats: SessionStatsTracker
36
+ private readonly tokenUsage: TokenUsageTracker
37
+
38
+ constructor(props: Props) {
39
+ this.fs = props.fs
40
+ this.process = props.process
41
+ this.clock = props.clock
42
+ this.sessionsDir = props.sessionsDir
43
+ this.projectsDir = props.projectsDir
44
+ this.sessionStats = props.sessionStats
45
+ this.tokenUsage = props.tokenUsage
46
+ }
47
+
48
+ /**
49
+ * Reads the current sessions and re-scans token usage, recording both.
50
+ * Touches disk (transcripts can be large) — call this on a slow cadence,
51
+ * not on every animation tick. Returns an Error when persisting either
52
+ * tracker's state fails.
53
+ */
54
+ collect(): StatsSnapshot | Error {
55
+ const activeSessions = readActiveSessions({
56
+ fs: this.fs,
57
+ process: this.process,
58
+ clock: this.clock,
59
+ sessionsDir: this.sessionsDir,
60
+ projectsDir: this.projectsDir,
61
+ })
62
+ const sessionSummary = this.sessionStats.record(activeSessions, this.clock.now())
63
+ if (sessionSummary instanceof Error) return sessionSummary
64
+ const usage = this.tokenUsage.scan()
65
+ if (usage instanceof Error) return usage
66
+ return this.buildSnapshot(sessionSummary, usage)
67
+ }
68
+
69
+ /** Cheap read of the last persisted totals, without touching sessions or transcripts. */
70
+ peek(): StatsSnapshot {
71
+ const sessionSummary = this.sessionStats.summary()
72
+ const usage = this.tokenUsage.summary()
73
+ return this.buildSnapshot(sessionSummary, usage)
74
+ }
75
+
76
+ private buildSnapshot(
77
+ sessionSummary: SessionStatsSummary,
78
+ usage: TokenUsageSummary,
79
+ ): StatsSnapshot {
80
+ const now = this.clock.now()
81
+ const hour = now.getHours()
82
+ // UTC date, matching TokenUsageTracker's transcript-timestamp bucketing.
83
+ const today = now.toISOString().slice(0, 10)
84
+
85
+ return {
86
+ now,
87
+ hour,
88
+ timeBucket: timeBucketForHour(hour),
89
+ currentConcurrentSessions: sessionSummary.currentConcurrentSessions,
90
+ maxConcurrentSessions: sessionSummary.maxConcurrentSessions,
91
+ totalSessionsSeen: sessionSummary.totalSessionsSeen,
92
+ uniqueProjectsSeen: sessionSummary.uniqueProjectsSeen,
93
+ currentStreakDays: sessionSummary.currentStreakDays,
94
+ longestStreakDays: sessionSummary.longestStreakDays,
95
+ tokensTotal: usage.tokensTotal,
96
+ tokensToday: usage.tokensByDate[today] ?? 0,
97
+ tokensThisWeek: sumRecentDays(usage.tokensByDate, today, WEEK_DAYS),
98
+ }
99
+ }
100
+ }
@@ -0,0 +1,31 @@
1
+ export type TimeBucket = "lateNight" | "morning" | "day" | "evening" | "night"
2
+
3
+ /**
4
+ * Buckets an hour-of-day (0-23, local time) into one of five time-of-day
5
+ * windows. Covers every hour exactly once, so `resolveSpecies` always has a
6
+ * fallback time-of-day species to match against.
7
+ */
8
+ export function timeBucketForHour(hour: number): TimeBucket {
9
+ if (hour < 5) return "lateNight" // 0-4
10
+ if (hour < 10) return "morning" // 5-9
11
+ if (hour < 17) return "day" // 10-16
12
+ if (hour < 20) return "evening" // 17-19
13
+ return "night" // 20-23
14
+ }
15
+
16
+ export type StatsSnapshot = {
17
+ now: Date
18
+ hour: number
19
+ timeBucket: TimeBucket
20
+ currentConcurrentSessions: number
21
+ maxConcurrentSessions: number
22
+ totalSessionsSeen: number
23
+ uniqueProjectsSeen: number
24
+ currentStreakDays: number
25
+ longestStreakDays: number
26
+ tokensTotal: number
27
+ /** Tokens consumed on `now`'s (UTC) calendar date. */
28
+ tokensToday: number
29
+ /** Rolling 7-day sum ending on `now`'s (UTC) calendar date. */
30
+ tokensThisWeek: number
31
+ }
@@ -0,0 +1,170 @@
1
+ import { join } from "node:path"
2
+ import { z } from "zod"
3
+ import { safeJsonParse } from "@lib/engine/errors"
4
+ import type { MinionFileSystem } from "@lib/engine/fs/file-system"
5
+ import { JsonFileStore } from "@lib/engine/fs/json-file-store"
6
+ import type { MinionClock } from "@lib/engine/time/clock"
7
+
8
+ const scanDataSchema = z.object({
9
+ tokensTotal: z.number().catch(0),
10
+ tokensByDate: z.record(z.string(), z.number()).catch({}),
11
+ files: z
12
+ .record(z.string(), z.object({ mtimeMs: z.number(), linesConsumed: z.number() }))
13
+ .catch({}),
14
+ })
15
+
16
+ type UsageScanData = z.infer<typeof scanDataSchema>
17
+
18
+ const EMPTY: UsageScanData = { tokensTotal: 0, tokensByDate: {}, files: {} }
19
+
20
+ // Bounds tokensByDate's growth; only the running total needs to survive
21
+ // forever, per-day figures beyond ~4 months aren't queried by anything here.
22
+ const MAX_TRACKED_DATES = 120
23
+
24
+ // Backfill cutoff: an unseen transcript whose mtime is older than this is
25
+ // skipped without being read. The rolling figures (today / this week) only
26
+ // need the trailing 7 days, so the first-ever scan parses the active week
27
+ // instead of the full history — lifetime totals just accrue from that point
28
+ // on. Must stay >= the WEEK_DAYS window in stats-collector.ts. A dormant
29
+ // file that gets appended later turns recent again and is then counted whole,
30
+ // history included — a harmless one-time bump.
31
+ const BACKFILL_WINDOW_MS = 7 * 24 * 60 * 60 * 1000
32
+
33
+ export type TokenUsageSummary = {
34
+ tokensTotal: number
35
+ tokensByDate: Record<string, number>
36
+ }
37
+
38
+ type Props = {
39
+ fs: MinionFileSystem
40
+ clock: MinionClock
41
+ path: string
42
+ /** Claude Code's transcript root, e.g. `~/.claude/projects`. */
43
+ projectsDir: string
44
+ }
45
+
46
+ /**
47
+ * Incrementally sums token usage out of Claude Code's transcript files
48
+ * (`<projectsDir>/**\/*.jsonl`, one JSON object per line). Tracks each
49
+ * file's mtime and how many lines were already counted, so a scan only
50
+ * re-reads files that changed since the last scan, and only parses newly
51
+ * appended lines within those — this keeps repeated scans cheap even though
52
+ * the underlying transcripts only grow over time.
53
+ */
54
+ export class TokenUsageTracker {
55
+ private readonly fs: MinionFileSystem
56
+ private readonly clock: MinionClock
57
+ private readonly projectsDir: string
58
+ private readonly store: JsonFileStore<UsageScanData>
59
+
60
+ constructor(props: Props) {
61
+ this.fs = props.fs
62
+ this.clock = props.clock
63
+ this.projectsDir = props.projectsDir
64
+ this.store = new JsonFileStore({
65
+ fs: props.fs,
66
+ path: props.path,
67
+ schema: scanDataSchema,
68
+ defaultValue: EMPTY,
69
+ })
70
+ }
71
+
72
+ /**
73
+ * Re-scans changed transcript files and persists the updated totals.
74
+ * Touches disk — call on a slow cadence. An unreadable transcript dir/file
75
+ * is skipped (transcripts come and go); only a failure to persist the
76
+ * scan state itself returns an Error.
77
+ */
78
+ scan(): TokenUsageSummary | Error {
79
+ const data = this.store.read()
80
+
81
+ const entries = this.fs.readdirRecursiveSync(this.projectsDir)
82
+ if (entries instanceof Error) return toSummary(data)
83
+
84
+ for (const relativeFile of entries.filter((f) => f.endsWith(".jsonl"))) {
85
+ this.scanFile(join(this.projectsDir, relativeFile), data)
86
+ }
87
+
88
+ pruneOldDates(data.tokensByDate)
89
+ const writeError = this.store.write(data)
90
+ if (writeError) return writeError
91
+ return toSummary(data)
92
+ }
93
+
94
+ /** Persisted totals without touching the transcript files. */
95
+ summary(): TokenUsageSummary {
96
+ return toSummary(this.store.read())
97
+ }
98
+
99
+ private scanFile(path: string, data: UsageScanData): void {
100
+ const stat = this.fs.statSync(path)
101
+ if (stat instanceof Error) return
102
+
103
+ const known = data.files[path]
104
+ if (known && known.mtimeMs === stat.mtimeMs) return // unchanged since the last scan
105
+ if (!known && this.clock.millis() - stat.mtimeMs > BACKFILL_WINDOW_MS) return // old history — not worth reading
106
+
107
+ const content = this.fs.readFileSync(path)
108
+ if (content instanceof Error) return
109
+
110
+ // Filter blank lines (notably the trailing "" from a file that ends in
111
+ // "\n", which every appended JSONL record does) before indexing — a raw
112
+ // split() index would double-count that trailing blank as "consumed" and
113
+ // then permanently skip the next real line once the file grows again.
114
+ const lines = content.split("\n").filter((line) => line.length > 0)
115
+ const startIndex = known?.linesConsumed ?? 0
116
+
117
+ for (let i = startIndex; i < lines.length; i++) {
118
+ addLineUsage(lines[i] as string, data)
119
+ }
120
+
121
+ data.files[path] = { mtimeMs: stat.mtimeMs, linesConsumed: lines.length }
122
+ }
123
+ }
124
+
125
+ // Hand-rolled shape checks, not zod: this runs per transcript line, and a
126
+ // first-ever scan chews through hundreds of thousands of lines — zod here
127
+ // roughly doubles the post-JSON.parse cost for no safety gain over these
128
+ // typeof guards (a wrong-shaped line just contributes 0 tokens either way).
129
+ function addLineUsage(line: string, data: UsageScanData): void {
130
+ const parsed = safeJsonParse(line)
131
+ if (parsed instanceof Error) return
132
+ if (typeof parsed !== "object" || parsed === null) return
133
+
134
+ const usage = (parsed as { message?: { usage?: Record<string, unknown> } }).message?.usage
135
+ if (!usage || typeof usage !== "object") return
136
+
137
+ const tokens =
138
+ numberOr0(usage.input_tokens) +
139
+ numberOr0(usage.output_tokens) +
140
+ numberOr0(usage.cache_creation_input_tokens) +
141
+ numberOr0(usage.cache_read_input_tokens)
142
+ if (tokens <= 0) return
143
+
144
+ data.tokensTotal += tokens
145
+
146
+ // Bucketed by the transcript line's own (UTC) timestamp date, matching the
147
+ // UTC day boundary MinionStatsCollector uses for "today" — the two must
148
+ // agree, even though it means the day rolls over at UTC midnight rather
149
+ // than the user's local midnight.
150
+ const timestamp = (parsed as { timestamp?: unknown }).timestamp
151
+ const date = typeof timestamp === "string" ? timestamp.slice(0, 10) : undefined
152
+ if (date) data.tokensByDate[date] = (data.tokensByDate[date] ?? 0) + tokens
153
+ }
154
+
155
+ function numberOr0(value: unknown): number {
156
+ return typeof value === "number" ? value : 0
157
+ }
158
+
159
+ function pruneOldDates(tokensByDate: Record<string, number>): void {
160
+ const dates = Object.keys(tokensByDate).sort()
161
+ const excess = dates.length - MAX_TRACKED_DATES
162
+ for (let i = 0; i < excess; i++) {
163
+ const date = dates[i]
164
+ if (date) delete tokensByDate[date]
165
+ }
166
+ }
167
+
168
+ function toSummary(data: UsageScanData): TokenUsageSummary {
169
+ return { tokensTotal: data.tokensTotal, tokensByDate: { ...data.tokensByDate } }
170
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Time boundary. Default NodeMinionClock returns `new Date()`; MemoryMinionClock
3
+ * is settable and `advance(ms)`-able for deterministic session-staleness / tick tests.
4
+ */
5
+ export abstract class MinionClock {
6
+ abstract now(): Date
7
+
8
+ millis(): number {
9
+ return this.now().getTime()
10
+ }
11
+ }
@@ -0,0 +1,26 @@
1
+ import { MinionClock } from "@lib/engine/time/clock"
2
+
3
+ type Props = {
4
+ start?: Date
5
+ }
6
+
7
+ export class MemoryMinionClock extends MinionClock {
8
+ private current: Date
9
+
10
+ constructor(props: Props = {}) {
11
+ super()
12
+ this.current = props.start ?? new Date(0)
13
+ }
14
+
15
+ now(): Date {
16
+ return new Date(this.current.getTime())
17
+ }
18
+
19
+ set(date: Date): void {
20
+ this.current = date
21
+ }
22
+
23
+ advance(ms: number): void {
24
+ this.current = new Date(this.current.getTime() + ms)
25
+ }
26
+ }
@@ -0,0 +1,7 @@
1
+ import { MinionClock } from "@lib/engine/time/clock"
2
+
3
+ export class NodeMinionClock extends MinionClock {
4
+ now(): Date {
5
+ return new Date()
6
+ }
7
+ }
package/lib/index.ts ADDED
@@ -0,0 +1,65 @@
1
+ // Public API surface for the @shigureni/minion package.
2
+ //
3
+ // This is the programmable counterpart to the `minion` CLI — the CLI (cli/)
4
+ // is a thin Hono-routed consumer of the same `Minion` facade exported here.
5
+ //
6
+ // import { Minion } from "@shigureni/minion"
7
+ //
8
+ // const minion = new Minion()
9
+ // const result = await minion.app.start() // { kind: "started", pid: 123 } など
10
+ // console.log(minion.app.status().app.running)
11
+ //
12
+ // Pass `Minion.inMemory()` to get a fully sandboxed instance (no real disk,
13
+ // processes, or wall-clock time) for tests or ad-hoc experiments.
14
+
15
+ // Facade
16
+ export * from "@lib/minion"
17
+
18
+ // Error-as-value helpers — fallible operations return `T | Error`, never throw
19
+ export * from "@lib/engine/errors"
20
+
21
+ // App — build/start/kill/status for the Swift app + gateway daemon
22
+ export * from "@lib/engine/app/app-paths"
23
+ export * from "@lib/engine/app/app-runner"
24
+ export * from "@lib/engine/app/source-hash"
25
+
26
+ // Config — flat string-keyed `config.json` store
27
+ export * from "@lib/engine/config/config-store"
28
+
29
+ // Gateway — session watching + pet behavior + the in-process HTTP/WS server
30
+ export * from "@lib/engine/gateway/sessions"
31
+ export * from "@lib/engine/gateway/pet-behavior"
32
+ export * from "@lib/engine/gateway/gateway-routes"
33
+ export * from "@lib/engine/gateway/gateway-server"
34
+ export * from "@lib/engine/gateway/resolve-daemon-script"
35
+
36
+ // Stats — session/token tracking feeding achievement + minion-species conditions
37
+ export * from "@lib/engine/stats/stats-snapshot"
38
+ export * from "@lib/engine/stats/session-stats-tracker"
39
+ export * from "@lib/engine/stats/token-usage-tracker"
40
+ export * from "@lib/engine/stats/stats-collector"
41
+
42
+ // Collection — the minion dex: rarity-tiered species + achievements, unlocked over time
43
+ export * from "@lib/engine/collection/moon"
44
+ export * from "@lib/engine/collection/species"
45
+ export * from "@lib/engine/collection/achievements"
46
+ export * from "@lib/engine/collection/collection-store"
47
+ export * from "@lib/engine/collection/collection-tracker"
48
+
49
+ // IO boundaries (abstract + Node / Memory implementations)
50
+ export * from "@lib/engine/fs/file-system"
51
+ export * from "@lib/engine/fs/node-file-system"
52
+ export * from "@lib/engine/fs/memory-file-system"
53
+ export * from "@lib/engine/fs/json-file-store"
54
+
55
+ export * from "@lib/engine/process/process-runner"
56
+ export * from "@lib/engine/process/node-process-runner"
57
+ export * from "@lib/engine/process/memory-process-runner"
58
+
59
+ export * from "@lib/engine/time/clock"
60
+ export * from "@lib/engine/time/node-clock"
61
+ export * from "@lib/engine/time/memory-clock"
62
+
63
+ export * from "@lib/engine/random/random-source"
64
+ export * from "@lib/engine/random/node-random-source"
65
+ export * from "@lib/engine/random/memory-random-source"