@geminixiang/mikan 0.5.2 → 1.0.0-beta.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 (208) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +3 -3
  3. package/dist/adapter.d.ts +1 -1
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/adapter.js.map +1 -1
  6. package/dist/adapters/shared.js +2 -2
  7. package/dist/adapters/shared.js.map +1 -1
  8. package/dist/agent.d.ts +11 -2
  9. package/dist/agent.d.ts.map +1 -1
  10. package/dist/agent.js +211 -119
  11. package/dist/agent.js.map +1 -1
  12. package/dist/commands/admin.d.ts.map +1 -1
  13. package/dist/commands/admin.js +1 -5
  14. package/dist/commands/admin.js.map +1 -1
  15. package/dist/commands/auto-reply.d.ts +3 -0
  16. package/dist/commands/auto-reply.d.ts.map +1 -1
  17. package/dist/commands/auto-reply.js +3 -0
  18. package/dist/commands/auto-reply.js.map +1 -1
  19. package/dist/commands/model.d.ts +2 -3
  20. package/dist/commands/model.d.ts.map +1 -1
  21. package/dist/commands/model.js +1 -4
  22. package/dist/commands/model.js.map +1 -1
  23. package/dist/commands/new.d.ts.map +1 -1
  24. package/dist/commands/new.js +1 -5
  25. package/dist/commands/new.js.map +1 -1
  26. package/dist/commands/registry.d.ts.map +1 -1
  27. package/dist/commands/registry.js +2 -5
  28. package/dist/commands/registry.js.map +1 -1
  29. package/dist/commands/types.d.ts +6 -2
  30. package/dist/commands/types.d.ts.map +1 -1
  31. package/dist/commands/types.js.map +1 -1
  32. package/dist/config.d.ts +42 -1
  33. package/dist/config.d.ts.map +1 -1
  34. package/dist/config.js +93 -7
  35. package/dist/config.js.map +1 -1
  36. package/dist/events.d.ts +0 -1
  37. package/dist/events.d.ts.map +1 -1
  38. package/dist/events.js +1 -6
  39. package/dist/events.js.map +1 -1
  40. package/dist/execution-resolver.d.ts.map +1 -1
  41. package/dist/execution-resolver.js +9 -6
  42. package/dist/execution-resolver.js.map +1 -1
  43. package/dist/harness/auth.d.ts +16 -0
  44. package/dist/harness/auth.d.ts.map +1 -0
  45. package/dist/harness/auth.js +86 -0
  46. package/dist/harness/auth.js.map +1 -0
  47. package/dist/harness/extensions/loader.d.ts +43 -0
  48. package/dist/harness/extensions/loader.d.ts.map +1 -0
  49. package/dist/harness/extensions/loader.js +325 -0
  50. package/dist/harness/extensions/loader.js.map +1 -0
  51. package/dist/harness/extensions/registry.d.ts +23 -0
  52. package/dist/harness/extensions/registry.d.ts.map +1 -0
  53. package/dist/harness/extensions/registry.js +45 -0
  54. package/dist/harness/extensions/registry.js.map +1 -0
  55. package/dist/harness/extensions/types.d.ts +225 -0
  56. package/dist/harness/extensions/types.d.ts.map +1 -0
  57. package/dist/harness/extensions/types.js +2 -0
  58. package/dist/harness/extensions/types.js.map +1 -0
  59. package/dist/harness/http.d.ts +16 -0
  60. package/dist/harness/http.d.ts.map +1 -0
  61. package/dist/harness/http.js +71 -0
  62. package/dist/harness/http.js.map +1 -0
  63. package/dist/harness/index.d.ts +19 -0
  64. package/dist/harness/index.d.ts.map +1 -0
  65. package/dist/harness/index.js +18 -0
  66. package/dist/harness/index.js.map +1 -0
  67. package/dist/harness/models.d.ts +34 -0
  68. package/dist/harness/models.d.ts.map +1 -0
  69. package/dist/harness/models.js +248 -0
  70. package/dist/harness/models.js.map +1 -0
  71. package/dist/harness/runner.d.ts +124 -0
  72. package/dist/harness/runner.d.ts.map +1 -0
  73. package/dist/harness/runner.js +436 -0
  74. package/dist/harness/runner.js.map +1 -0
  75. package/dist/harness/session-store.d.ts +77 -0
  76. package/dist/harness/session-store.d.ts.map +1 -0
  77. package/dist/harness/session-store.js +248 -0
  78. package/dist/harness/session-store.js.map +1 -0
  79. package/dist/harness/settings.d.ts +47 -0
  80. package/dist/harness/settings.d.ts.map +1 -0
  81. package/dist/harness/settings.js +29 -0
  82. package/dist/harness/settings.js.map +1 -0
  83. package/dist/harness/skills.d.ts +54 -0
  84. package/dist/harness/skills.d.ts.map +1 -0
  85. package/dist/harness/skills.js +203 -0
  86. package/dist/harness/skills.js.map +1 -0
  87. package/dist/harness/types.d.ts +32 -0
  88. package/dist/harness/types.d.ts.map +1 -0
  89. package/dist/harness/types.js +2 -0
  90. package/dist/harness/types.js.map +1 -0
  91. package/dist/index.d.ts +1 -0
  92. package/dist/index.d.ts.map +1 -1
  93. package/dist/index.js +1 -0
  94. package/dist/index.js.map +1 -1
  95. package/dist/log.d.ts.map +1 -1
  96. package/dist/log.js +11 -14
  97. package/dist/log.js.map +1 -1
  98. package/dist/main.js +67 -8
  99. package/dist/main.js.map +1 -1
  100. package/dist/model-registry.d.ts +2 -2
  101. package/dist/model-registry.d.ts.map +1 -1
  102. package/dist/model-registry.js.map +1 -1
  103. package/dist/provisioner.d.ts +1 -4
  104. package/dist/provisioner.d.ts.map +1 -1
  105. package/dist/provisioner.js +3 -8
  106. package/dist/provisioner.js.map +1 -1
  107. package/dist/runtime/agent-run-controller.d.ts.map +1 -1
  108. package/dist/runtime/agent-run-controller.js +6 -3
  109. package/dist/runtime/agent-run-controller.js.map +1 -1
  110. package/dist/runtime/conversation-runtime.js +1 -1
  111. package/dist/runtime/conversation-runtime.js.map +1 -1
  112. package/dist/runtime/types.d.ts +4 -3
  113. package/dist/runtime/types.d.ts.map +1 -1
  114. package/dist/runtime/types.js.map +1 -1
  115. package/dist/sandbox/container.d.ts +1 -0
  116. package/dist/sandbox/container.d.ts.map +1 -1
  117. package/dist/sandbox/container.js +2 -2
  118. package/dist/sandbox/container.js.map +1 -1
  119. package/dist/sandbox/host.d.ts.map +1 -1
  120. package/dist/sandbox/host.js +0 -4
  121. package/dist/sandbox/host.js.map +1 -1
  122. package/dist/sandbox/image.d.ts.map +1 -1
  123. package/dist/sandbox/image.js +0 -3
  124. package/dist/sandbox/image.js.map +1 -1
  125. package/dist/sandbox/index.d.ts.map +1 -1
  126. package/dist/sandbox/index.js +4 -1
  127. package/dist/sandbox/index.js.map +1 -1
  128. package/dist/sandbox/types.d.ts +2 -2
  129. package/dist/sandbox/types.d.ts.map +1 -1
  130. package/dist/sandbox/types.js.map +1 -1
  131. package/dist/sessions/agent-memory-file-manager.d.ts.map +1 -1
  132. package/dist/sessions/agent-memory-file-manager.js +8 -8
  133. package/dist/sessions/agent-memory-file-manager.js.map +1 -1
  134. package/dist/sessions/metadata.js +2 -2
  135. package/dist/sessions/metadata.js.map +1 -1
  136. package/dist/sessions/store.d.ts +3 -3
  137. package/dist/sessions/store.d.ts.map +1 -1
  138. package/dist/sessions/store.js +17 -43
  139. package/dist/sessions/store.js.map +1 -1
  140. package/dist/sessions/types.d.ts +1 -1
  141. package/dist/sessions/types.d.ts.map +1 -1
  142. package/dist/sessions/types.js.map +1 -1
  143. package/dist/store.d.ts +1 -0
  144. package/dist/store.d.ts.map +1 -1
  145. package/dist/store.js +18 -11
  146. package/dist/store.js.map +1 -1
  147. package/dist/tools/edit.d.ts.map +1 -1
  148. package/dist/tools/edit.js +1 -3
  149. package/dist/tools/edit.js.map +1 -1
  150. package/dist/tools/event.d.ts.map +1 -1
  151. package/dist/tools/event.js +7 -74
  152. package/dist/tools/event.js.map +1 -1
  153. package/dist/tools/read.d.ts.map +1 -1
  154. package/dist/tools/read.js +1 -3
  155. package/dist/tools/read.js.map +1 -1
  156. package/dist/tools/write.d.ts.map +1 -1
  157. package/dist/tools/write.js +3 -7
  158. package/dist/tools/write.js.map +1 -1
  159. package/dist/trigger.d.ts +2 -0
  160. package/dist/trigger.d.ts.map +1 -1
  161. package/dist/trigger.js +7 -13
  162. package/dist/trigger.js.map +1 -1
  163. package/dist/types.d.ts +21 -0
  164. package/dist/types.d.ts.map +1 -1
  165. package/dist/types.js.map +1 -1
  166. package/dist/vault/index.d.ts.map +1 -1
  167. package/dist/vault/index.js +7 -1
  168. package/dist/vault/index.js.map +1 -1
  169. package/dist/web/admin/portal.d.ts +1 -1
  170. package/dist/web/admin/portal.d.ts.map +1 -1
  171. package/dist/web/admin/portal.js +394 -73
  172. package/dist/web/admin/portal.js.map +1 -1
  173. package/dist/web/admin/provider-models.d.ts +2 -2
  174. package/dist/web/admin/provider-models.d.ts.map +1 -1
  175. package/dist/web/admin/provider-models.js +4 -2
  176. package/dist/web/admin/provider-models.js.map +1 -1
  177. package/dist/web/admin/store.d.ts.map +1 -1
  178. package/dist/web/admin/store.js +3 -12
  179. package/dist/web/admin/store.js.map +1 -1
  180. package/dist/web/admin/types.d.ts +7 -5
  181. package/dist/web/admin/types.d.ts.map +1 -1
  182. package/dist/web/admin/types.js.map +1 -1
  183. package/dist/web/login/oauth.d.ts.map +1 -1
  184. package/dist/web/login/oauth.js +41 -23
  185. package/dist/web/login/oauth.js.map +1 -1
  186. package/dist/web/login/store.d.ts +0 -4
  187. package/dist/web/login/store.d.ts.map +1 -1
  188. package/dist/web/login/store.js +3 -16
  189. package/dist/web/login/store.js.map +1 -1
  190. package/dist/web/server.js +2 -2
  191. package/dist/web/server.js.map +1 -1
  192. package/dist/web/session-view/portal.d.ts.map +1 -1
  193. package/dist/web/session-view/portal.js +5 -16
  194. package/dist/web/session-view/portal.js.map +1 -1
  195. package/dist/web/session-view/service.js +7 -13
  196. package/dist/web/session-view/service.js.map +1 -1
  197. package/dist/web/session-view/store.d.ts.map +1 -1
  198. package/dist/web/session-view/store.js +2 -7
  199. package/dist/web/session-view/store.js.map +1 -1
  200. package/dist/web/token-store.d.ts +2 -8
  201. package/dist/web/token-store.d.ts.map +1 -1
  202. package/dist/web/token-store.js +11 -12
  203. package/dist/web/token-store.js.map +1 -1
  204. package/package.json +6 -3
  205. package/dist/context.d.ts +0 -2
  206. package/dist/context.d.ts.map +0 -1
  207. package/dist/context.js +0 -2
  208. package/dist/context.js.map +0 -1
@@ -0,0 +1,248 @@
1
+ /**
2
+ * mikan-owned session storage.
3
+ *
4
+ * Sessions are append-only trees stored as JSONL files: a `session` header
5
+ * line followed by entries that carry `id`/`parentId` links. The format is
6
+ * the v3 layout mikan has always written, and entry shapes are structurally
7
+ * identical to pi-agent-core's `SessionTreeEntry`, so pi-agent-core's
8
+ * context-building and compaction helpers operate on these entries directly.
9
+ *
10
+ * The store is synchronous by design: every mikan call site (session scope
11
+ * resolution, chat-history sync, session view, admin portal) reads and
12
+ * writes sessions inside synchronous flows.
13
+ */
14
+ import { randomUUID } from "crypto";
15
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from "fs";
16
+ import { dirname, resolve } from "path";
17
+ import { buildSessionContext as buildContextFromEntries } from "@earendil-works/pi-agent-core";
18
+ import { atomicWritePrivateFile } from "../utils/fs-atomic.js";
19
+ import { CURRENT_SESSION_VERSION, } from "./types.js";
20
+ /** Generate a unique short entry id (8 hex chars, collision-checked). */
21
+ function generateEntryId(byId) {
22
+ for (let i = 0; i < 100; i++) {
23
+ const id = randomUUID().slice(0, 8);
24
+ if (!byId.has(id))
25
+ return id;
26
+ }
27
+ return randomUUID();
28
+ }
29
+ /** Parse JSONL content tolerantly: malformed lines are skipped. */
30
+ export function parseSessionFileEntries(content) {
31
+ const entries = [];
32
+ for (const line of content.split("\n")) {
33
+ const trimmed = line.trim();
34
+ if (!trimmed)
35
+ continue;
36
+ try {
37
+ const parsed = JSON.parse(trimmed);
38
+ if (parsed &&
39
+ typeof parsed === "object" &&
40
+ typeof parsed.type === "string") {
41
+ entries.push(parsed);
42
+ }
43
+ }
44
+ catch {
45
+ // Skip malformed lines
46
+ }
47
+ }
48
+ return entries;
49
+ }
50
+ function isSessionHeader(entry) {
51
+ return entry.type === "session" && typeof entry.id === "string";
52
+ }
53
+ /**
54
+ * Load a session file. Returns no entries when the file is missing or its
55
+ * first parseable line is not a valid session header (mirrors the tolerant
56
+ * read behavior mikan has relied on). Throws only on I/O errors.
57
+ */
58
+ export function loadSessionFileEntries(filePath) {
59
+ if (!existsSync(filePath))
60
+ return [];
61
+ const entries = parseSessionFileEntries(readFileSync(filePath, "utf-8"));
62
+ if (entries.length === 0)
63
+ return entries;
64
+ if (!isSessionHeader(entries[0]))
65
+ return [];
66
+ return entries;
67
+ }
68
+ /** True when the file exists and holds any non-whitespace content on disk. */
69
+ function hasSessionFileContent(filePath) {
70
+ if (!existsSync(filePath))
71
+ return false;
72
+ return readFileSync(filePath, "utf-8").trim().length > 0;
73
+ }
74
+ /**
75
+ * Append-oriented session store over one v3 JSONL file.
76
+ *
77
+ * Reading is tolerant (missing files and malformed lines yield an empty
78
+ * session); appends persist immediately. A session opened from a file
79
+ * without a valid header gets its header materialized on the first append.
80
+ */
81
+ export class SessionStore {
82
+ constructor(sessionFile, cwd, fileEntries) {
83
+ this.entries = [];
84
+ this.byId = new Map();
85
+ this.leafId = null;
86
+ this.sessionFile = resolve(sessionFile);
87
+ this.header = fileEntries.length > 0 && isSessionHeader(fileEntries[0]) ? fileEntries[0] : null;
88
+ this.headerOnDisk = this.header !== null;
89
+ this.sessionId = this.header?.id ?? randomUUID();
90
+ this.cwd = cwd;
91
+ for (const entry of fileEntries) {
92
+ if (isSessionHeader(entry))
93
+ continue;
94
+ if (entry.type === "leaf") {
95
+ // Written by pi-agent-core JSONL sessions; mikan tracks the leaf implicitly.
96
+ this.leafId = entry.targetId;
97
+ continue;
98
+ }
99
+ this.entries.push(entry);
100
+ this.byId.set(entry.id, entry);
101
+ this.leafId = entry.id;
102
+ }
103
+ }
104
+ /**
105
+ * Open a session file. A missing or empty file starts an empty session
106
+ * whose header is written on the first append. A file that has content but
107
+ * no leading session header is treated as corrupted and throws, rather than
108
+ * being silently overwritten on the next append (which would erase the
109
+ * existing history). Callers on read paths already wrap this in try/catch.
110
+ * @param cwdOverride Working directory override; defaults to the header cwd.
111
+ */
112
+ static open(path, cwdOverride) {
113
+ const fileEntries = loadSessionFileEntries(path);
114
+ if (fileEntries.length === 0 && hasSessionFileContent(path)) {
115
+ throw new Error(`Session file is corrupted (no valid session header): ${path}. ` +
116
+ `Refusing to open to avoid overwriting existing content.`);
117
+ }
118
+ const header = fileEntries.find(isSessionHeader);
119
+ const cwd = cwdOverride ?? header?.cwd ?? process.cwd();
120
+ return new SessionStore(path, cwd, fileEntries);
121
+ }
122
+ /** Create a new session file with a fresh header, replacing any existing file. */
123
+ static create(path, cwd, options) {
124
+ const header = {
125
+ type: "session",
126
+ version: CURRENT_SESSION_VERSION,
127
+ id: options?.id ?? randomUUID(),
128
+ timestamp: new Date().toISOString(),
129
+ cwd,
130
+ };
131
+ atomicWritePrivateFile(path, `${JSON.stringify(header)}\n`);
132
+ return new SessionStore(path, cwd, [header]);
133
+ }
134
+ getSessionFile() {
135
+ return this.sessionFile;
136
+ }
137
+ getCwd() {
138
+ return this.cwd;
139
+ }
140
+ /** Session header as read from disk, or null when the file had none. */
141
+ getHeader() {
142
+ return this.header;
143
+ }
144
+ /** Stable session id: the header id, or a generated id for fresh sessions. */
145
+ getSessionId() {
146
+ return this.sessionId;
147
+ }
148
+ getLeafId() {
149
+ return this.leafId;
150
+ }
151
+ getEntry(id) {
152
+ return this.byId.get(id);
153
+ }
154
+ /** All session entries (header excluded). Returns a shallow copy. */
155
+ getEntries() {
156
+ return [...this.entries];
157
+ }
158
+ /** Latest user-assigned session name, if any. */
159
+ getSessionName() {
160
+ for (let i = this.entries.length - 1; i >= 0; i--) {
161
+ const entry = this.entries[i];
162
+ if (entry.type === "session_info")
163
+ return entry.name?.trim() || undefined;
164
+ }
165
+ return undefined;
166
+ }
167
+ /**
168
+ * Walk from an entry to the root, returning entries in path order
169
+ * (root first). Defaults to the current leaf.
170
+ */
171
+ getBranch(fromId) {
172
+ const path = [];
173
+ let currentId = fromId ?? this.leafId;
174
+ while (currentId) {
175
+ const entry = this.byId.get(currentId);
176
+ if (!entry)
177
+ break;
178
+ path.push(entry);
179
+ currentId = entry.parentId;
180
+ }
181
+ return path.toReversed();
182
+ }
183
+ /**
184
+ * Build the LLM context (messages, thinking level, model) from the current
185
+ * branch, resolving compaction and branch summaries along the path.
186
+ */
187
+ buildSessionContext() {
188
+ return buildContextFromEntries(this.getBranch());
189
+ }
190
+ /** Append a chat message as a child of the current leaf. Returns the entry id. */
191
+ appendMessage(message) {
192
+ return this.appendEntry({ type: "message", message });
193
+ }
194
+ /** Append an extension/application data entry (never sent to the LLM). */
195
+ appendCustomEntry(customType, data) {
196
+ return this.appendEntry({ type: "custom", customType, data });
197
+ }
198
+ /** Append a custom message entry that participates in LLM context. */
199
+ appendCustomMessageEntry(customType, content, display, details) {
200
+ return this.appendEntry({ type: "custom_message", customType, content, display, details });
201
+ }
202
+ /** Record a user-visible session name. */
203
+ appendSessionInfo(name) {
204
+ return this.appendEntry({ type: "session_info", name });
205
+ }
206
+ /** Persist a compaction summary produced for this session. */
207
+ appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromHook) {
208
+ return this.appendEntry({
209
+ type: "compaction",
210
+ summary,
211
+ firstKeptEntryId,
212
+ tokensBefore,
213
+ ...(details !== undefined ? { details } : {}),
214
+ ...(fromHook !== undefined ? { fromHook } : {}),
215
+ });
216
+ }
217
+ appendEntry(partial) {
218
+ const entry = {
219
+ ...partial,
220
+ id: generateEntryId(this.byId),
221
+ parentId: this.leafId,
222
+ timestamp: new Date().toISOString(),
223
+ };
224
+ this.entries.push(entry);
225
+ this.byId.set(entry.id, entry);
226
+ this.leafId = entry.id;
227
+ this.persist(entry);
228
+ return entry.id;
229
+ }
230
+ persist(entry) {
231
+ if (!this.headerOnDisk) {
232
+ this.header ??= {
233
+ type: "session",
234
+ version: CURRENT_SESSION_VERSION,
235
+ id: this.sessionId,
236
+ timestamp: new Date().toISOString(),
237
+ cwd: this.cwd,
238
+ };
239
+ const lines = [this.header, ...this.entries].map((line) => JSON.stringify(line));
240
+ mkdirSync(dirname(this.sessionFile), { recursive: true });
241
+ atomicWritePrivateFile(this.sessionFile, `${lines.join("\n")}\n`);
242
+ this.headerOnDisk = true;
243
+ return;
244
+ }
245
+ appendFileSync(this.sessionFile, `${JSON.stringify(entry)}\n`);
246
+ }
247
+ }
248
+ //# sourceMappingURL=session-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-store.js","sourceRoot":"","sources":["../../src/harness/session-store.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * mikan-owned session storage.\n *\n * Sessions are append-only trees stored as JSONL files: a `session` header\n * line followed by entries that carry `id`/`parentId` links. The format is\n * the v3 layout mikan has always written, and entry shapes are structurally\n * identical to pi-agent-core's `SessionTreeEntry`, so pi-agent-core's\n * context-building and compaction helpers operate on these entries directly.\n *\n * The store is synchronous by design: every mikan call site (session scope\n * resolution, chat-history sync, session view, admin portal) reads and\n * writes sessions inside synchronous flows.\n */\nimport { randomUUID } from \"crypto\";\nimport { appendFileSync, existsSync, mkdirSync, readFileSync } from \"fs\";\nimport { dirname, resolve } from \"path\";\nimport { buildSessionContext as buildContextFromEntries } from \"@earendil-works/pi-agent-core\";\nimport type { AgentMessage } from \"@earendil-works/pi-agent-core\";\nimport type { ImageContent, TextContent } from \"@earendil-works/pi-ai\";\nimport { atomicWritePrivateFile } from \"../utils/fs-atomic.js\";\nimport {\n CURRENT_SESSION_VERSION,\n type SessionContext,\n type SessionEntry,\n type SessionFileEntry,\n type SessionHeader,\n} from \"./types.js\";\n\n/** Distributive omit so each entry variant keeps its own fields. */\ntype PendingSessionEntry = SessionEntry extends infer TEntry\n ? TEntry extends SessionEntry\n ? Omit<TEntry, \"id\" | \"parentId\" | \"timestamp\">\n : never\n : never;\n\n/** Generate a unique short entry id (8 hex chars, collision-checked). */\nfunction generateEntryId(byId: Map<string, SessionEntry>): string {\n for (let i = 0; i < 100; i++) {\n const id = randomUUID().slice(0, 8);\n if (!byId.has(id)) return id;\n }\n return randomUUID();\n}\n\n/** Parse JSONL content tolerantly: malformed lines are skipped. */\nexport function parseSessionFileEntries(content: string): SessionFileEntry[] {\n const entries: SessionFileEntry[] = [];\n for (const line of content.split(\"\\n\")) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n try {\n const parsed: unknown = JSON.parse(trimmed);\n if (\n parsed &&\n typeof parsed === \"object\" &&\n typeof (parsed as { type?: unknown }).type === \"string\"\n ) {\n entries.push(parsed as SessionFileEntry);\n }\n } catch {\n // Skip malformed lines\n }\n }\n return entries;\n}\n\nfunction isSessionHeader(entry: SessionFileEntry): entry is SessionHeader {\n return entry.type === \"session\" && typeof (entry as SessionHeader).id === \"string\";\n}\n\n/**\n * Load a session file. Returns no entries when the file is missing or its\n * first parseable line is not a valid session header (mirrors the tolerant\n * read behavior mikan has relied on). Throws only on I/O errors.\n */\nexport function loadSessionFileEntries(filePath: string): SessionFileEntry[] {\n if (!existsSync(filePath)) return [];\n const entries = parseSessionFileEntries(readFileSync(filePath, \"utf-8\"));\n if (entries.length === 0) return entries;\n if (!isSessionHeader(entries[0])) return [];\n return entries;\n}\n\n/** True when the file exists and holds any non-whitespace content on disk. */\nfunction hasSessionFileContent(filePath: string): boolean {\n if (!existsSync(filePath)) return false;\n return readFileSync(filePath, \"utf-8\").trim().length > 0;\n}\n\n/**\n * Append-oriented session store over one v3 JSONL file.\n *\n * Reading is tolerant (missing files and malformed lines yield an empty\n * session); appends persist immediately. A session opened from a file\n * without a valid header gets its header materialized on the first append.\n */\nexport class SessionStore {\n private readonly sessionFile: string;\n private readonly cwd: string;\n private readonly sessionId: string;\n private header: SessionHeader | null;\n private headerOnDisk: boolean;\n private entries: SessionEntry[] = [];\n private byId = new Map<string, SessionEntry>();\n private leafId: string | null = null;\n\n private constructor(sessionFile: string, cwd: string, fileEntries: SessionFileEntry[]) {\n this.sessionFile = resolve(sessionFile);\n this.header = fileEntries.length > 0 && isSessionHeader(fileEntries[0]) ? fileEntries[0] : null;\n this.headerOnDisk = this.header !== null;\n this.sessionId = this.header?.id ?? randomUUID();\n this.cwd = cwd;\n for (const entry of fileEntries) {\n if (isSessionHeader(entry)) continue;\n if (entry.type === \"leaf\") {\n // Written by pi-agent-core JSONL sessions; mikan tracks the leaf implicitly.\n this.leafId = entry.targetId;\n continue;\n }\n this.entries.push(entry);\n this.byId.set(entry.id, entry);\n this.leafId = entry.id;\n }\n }\n\n /**\n * Open a session file. A missing or empty file starts an empty session\n * whose header is written on the first append. A file that has content but\n * no leading session header is treated as corrupted and throws, rather than\n * being silently overwritten on the next append (which would erase the\n * existing history). Callers on read paths already wrap this in try/catch.\n * @param cwdOverride Working directory override; defaults to the header cwd.\n */\n static open(path: string, cwdOverride?: string): SessionStore {\n const fileEntries = loadSessionFileEntries(path);\n if (fileEntries.length === 0 && hasSessionFileContent(path)) {\n throw new Error(\n `Session file is corrupted (no valid session header): ${path}. ` +\n `Refusing to open to avoid overwriting existing content.`,\n );\n }\n const header = fileEntries.find(isSessionHeader);\n const cwd = cwdOverride ?? header?.cwd ?? process.cwd();\n return new SessionStore(path, cwd, fileEntries);\n }\n\n /** Create a new session file with a fresh header, replacing any existing file. */\n static create(path: string, cwd: string, options?: { id?: string }): SessionStore {\n const header: SessionHeader = {\n type: \"session\",\n version: CURRENT_SESSION_VERSION,\n id: options?.id ?? randomUUID(),\n timestamp: new Date().toISOString(),\n cwd,\n };\n atomicWritePrivateFile(path, `${JSON.stringify(header)}\\n`);\n return new SessionStore(path, cwd, [header]);\n }\n\n getSessionFile(): string {\n return this.sessionFile;\n }\n\n getCwd(): string {\n return this.cwd;\n }\n\n /** Session header as read from disk, or null when the file had none. */\n getHeader(): SessionHeader | null {\n return this.header;\n }\n\n /** Stable session id: the header id, or a generated id for fresh sessions. */\n getSessionId(): string {\n return this.sessionId;\n }\n\n getLeafId(): string | null {\n return this.leafId;\n }\n\n getEntry(id: string): SessionEntry | undefined {\n return this.byId.get(id);\n }\n\n /** All session entries (header excluded). Returns a shallow copy. */\n getEntries(): SessionEntry[] {\n return [...this.entries];\n }\n\n /** Latest user-assigned session name, if any. */\n getSessionName(): string | undefined {\n for (let i = this.entries.length - 1; i >= 0; i--) {\n const entry = this.entries[i];\n if (entry.type === \"session_info\") return entry.name?.trim() || undefined;\n }\n return undefined;\n }\n\n /**\n * Walk from an entry to the root, returning entries in path order\n * (root first). Defaults to the current leaf.\n */\n getBranch(fromId?: string): SessionEntry[] {\n const path: SessionEntry[] = [];\n let currentId = fromId ?? this.leafId;\n while (currentId) {\n const entry = this.byId.get(currentId);\n if (!entry) break;\n path.push(entry);\n currentId = entry.parentId;\n }\n return path.toReversed();\n }\n\n /**\n * Build the LLM context (messages, thinking level, model) from the current\n * branch, resolving compaction and branch summaries along the path.\n */\n buildSessionContext(): SessionContext {\n return buildContextFromEntries(this.getBranch());\n }\n\n /** Append a chat message as a child of the current leaf. Returns the entry id. */\n appendMessage(message: AgentMessage): string {\n return this.appendEntry({ type: \"message\", message });\n }\n\n /** Append an extension/application data entry (never sent to the LLM). */\n appendCustomEntry(customType: string, data?: unknown): string {\n return this.appendEntry({ type: \"custom\", customType, data });\n }\n\n /** Append a custom message entry that participates in LLM context. */\n appendCustomMessageEntry(\n customType: string,\n content: string | (TextContent | ImageContent)[],\n display: boolean,\n details?: unknown,\n ): string {\n return this.appendEntry({ type: \"custom_message\", customType, content, display, details });\n }\n\n /** Record a user-visible session name. */\n appendSessionInfo(name: string): string {\n return this.appendEntry({ type: \"session_info\", name });\n }\n\n /** Persist a compaction summary produced for this session. */\n appendCompaction(\n summary: string,\n firstKeptEntryId: string,\n tokensBefore: number,\n details?: unknown,\n fromHook?: boolean,\n ): string {\n return this.appendEntry({\n type: \"compaction\",\n summary,\n firstKeptEntryId,\n tokensBefore,\n ...(details !== undefined ? { details } : {}),\n ...(fromHook !== undefined ? { fromHook } : {}),\n });\n }\n\n private appendEntry(partial: PendingSessionEntry): string {\n const entry = {\n ...partial,\n id: generateEntryId(this.byId),\n parentId: this.leafId,\n timestamp: new Date().toISOString(),\n } as SessionEntry;\n this.entries.push(entry);\n this.byId.set(entry.id, entry);\n this.leafId = entry.id;\n this.persist(entry);\n return entry.id;\n }\n\n private persist(entry: SessionEntry): void {\n if (!this.headerOnDisk) {\n this.header ??= {\n type: \"session\",\n version: CURRENT_SESSION_VERSION,\n id: this.sessionId,\n timestamp: new Date().toISOString(),\n cwd: this.cwd,\n };\n const lines = [this.header, ...this.entries].map((line) => JSON.stringify(line));\n mkdirSync(dirname(this.sessionFile), { recursive: true });\n atomicWritePrivateFile(this.sessionFile, `${lines.join(\"\\n\")}\\n`);\n this.headerOnDisk = true;\n return;\n }\n appendFileSync(this.sessionFile, `${JSON.stringify(entry)}\\n`);\n }\n}\n"]}
@@ -0,0 +1,47 @@
1
+ import type { CompactionSettings } from "@earendil-works/pi-agent-core";
2
+ export type { CompactionSettings };
3
+ export interface RetrySettings {
4
+ enabled: boolean;
5
+ maxRetries: number;
6
+ baseDelayMs: number;
7
+ }
8
+ export declare const DEFAULT_RETRY_SETTINGS: RetrySettings;
9
+ /**
10
+ * Per-run resource ceilings — the harness's external circuit breakers.
11
+ *
12
+ * An LLM cannot reliably decide on its own when to stop (the halting problem),
13
+ * so a runaway run has to be stopped from the outside. When any populated cap
14
+ * is exceeded, the run is aborted and a `budget_exceeded` event is emitted.
15
+ * Every field is a cap on a single `prompt()` call; an undefined field means
16
+ * "no cap on that axis". Interactive runs are gated turn-by-turn by a human,
17
+ * so they default to uncapped; autonomous runs (scheduled events, triggers)
18
+ * pass {@link DEFAULT_EVENT_BUDGET}, where nobody is watching the loop.
19
+ */
20
+ export interface BudgetSettings {
21
+ /** Max cumulative tokens processed this run (input + output + cache read/write). */
22
+ maxTokens?: number;
23
+ /** Max cumulative provider cost (USD) this run. Requires a populated model cost table. */
24
+ maxCostUsd?: number;
25
+ /** Max wall-clock duration for the run, in milliseconds. */
26
+ maxDurationMs?: number;
27
+ /** Max number of LLM calls (assistant turns) this run. */
28
+ maxLlmCalls?: number;
29
+ }
30
+ /** Interactive runs are human-gated per turn — no automatic ceiling by default. */
31
+ export declare const DEFAULT_BUDGET_SETTINGS: BudgetSettings;
32
+ /**
33
+ * Ceiling for autonomous (event / trigger) runs, where no human is watching
34
+ * the loop. Deliberately generous — it is a stop-loss, not a target.
35
+ */
36
+ export declare const DEFAULT_EVENT_BUDGET: BudgetSettings;
37
+ export interface HarnessSettings {
38
+ compaction: CompactionSettings;
39
+ retry: RetrySettings;
40
+ budget: BudgetSettings;
41
+ }
42
+ export declare function resolveHarnessSettings(overrides?: {
43
+ compaction?: Partial<CompactionSettings>;
44
+ retry?: Partial<RetrySettings>;
45
+ budget?: Partial<BudgetSettings>;
46
+ }): HarnessSettings;
47
+ //# sourceMappingURL=settings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../../src/harness/settings.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AAExE,YAAY,EAAE,kBAAkB,EAAE,CAAC;AAEnC,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,eAAO,MAAM,sBAAsB,EAAE,aAIpC,CAAC;AAEF;;;;;;;;;;GAUG;AACH,MAAM,WAAW,cAAc;IAC7B,oFAAoF;IACpF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,0FAA0F;IAC1F,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4DAA4D;IAC5D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,0DAA0D;IAC1D,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,mFAAmF;AACnF,eAAO,MAAM,uBAAuB,EAAE,cAAmB,CAAC;AAE1D;;;GAGG;AACH,eAAO,MAAM,oBAAoB,EAAE,cAIlC,CAAC;AAEF,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,kBAAkB,CAAC;IAC/B,KAAK,EAAE,aAAa,CAAC;IACrB,MAAM,EAAE,cAAc,CAAC;CACxB;AAED,wBAAgB,sBAAsB,CAAC,SAAS,CAAC,EAAE;IACjD,UAAU,CAAC,EAAE,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACzC,KAAK,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC;IAC/B,MAAM,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,CAAC;CAClC,GAAG,eAAe,CAMlB"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Runtime settings for the mikan harness. mikan runs headless, so these are
3
+ * plain values with defaults matching the behavior mikan shipped with.
4
+ */
5
+ import { DEFAULT_COMPACTION_SETTINGS } from "@earendil-works/pi-agent-core";
6
+ export const DEFAULT_RETRY_SETTINGS = {
7
+ enabled: true,
8
+ maxRetries: 3,
9
+ baseDelayMs: 2000,
10
+ };
11
+ /** Interactive runs are human-gated per turn — no automatic ceiling by default. */
12
+ export const DEFAULT_BUDGET_SETTINGS = {};
13
+ /**
14
+ * Ceiling for autonomous (event / trigger) runs, where no human is watching
15
+ * the loop. Deliberately generous — it is a stop-loss, not a target.
16
+ */
17
+ export const DEFAULT_EVENT_BUDGET = {
18
+ maxDurationMs: 10 * 60 * 1000,
19
+ maxLlmCalls: 50,
20
+ maxCostUsd: 2,
21
+ };
22
+ export function resolveHarnessSettings(overrides) {
23
+ return {
24
+ compaction: { ...DEFAULT_COMPACTION_SETTINGS, ...overrides?.compaction },
25
+ retry: { ...DEFAULT_RETRY_SETTINGS, ...overrides?.retry },
26
+ budget: { ...DEFAULT_BUDGET_SETTINGS, ...overrides?.budget },
27
+ };
28
+ }
29
+ //# sourceMappingURL=settings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings.js","sourceRoot":"","sources":["../../src/harness/settings.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,2BAA2B,EAAE,MAAM,+BAA+B,CAAC;AAW5E,MAAM,CAAC,MAAM,sBAAsB,GAAkB;IACnD,OAAO,EAAE,IAAI;IACb,UAAU,EAAE,CAAC;IACb,WAAW,EAAE,IAAI;CAClB,CAAC;AAwBF,mFAAmF;AACnF,MAAM,CAAC,MAAM,uBAAuB,GAAmB,EAAE,CAAC;AAE1D;;;GAGG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAmB;IAClD,aAAa,EAAE,EAAE,GAAG,EAAE,GAAG,IAAI;IAC7B,WAAW,EAAE,EAAE;IACf,UAAU,EAAE,CAAC;CACd,CAAC;AAQF,MAAM,UAAU,sBAAsB,CAAC,SAItC;IACC,OAAO;QACL,UAAU,EAAE,EAAE,GAAG,2BAA2B,EAAE,GAAG,SAAS,EAAE,UAAU,EAAE;QACxE,KAAK,EAAE,EAAE,GAAG,sBAAsB,EAAE,GAAG,SAAS,EAAE,KAAK,EAAE;QACzD,MAAM,EAAE,EAAE,GAAG,uBAAuB,EAAE,GAAG,SAAS,EAAE,MAAM,EAAE;KAC7D,CAAC;AACJ,CAAC","sourcesContent":["/**\n * Runtime settings for the mikan harness. mikan runs headless, so these are\n * plain values with defaults matching the behavior mikan shipped with.\n */\nimport { DEFAULT_COMPACTION_SETTINGS } from \"@earendil-works/pi-agent-core\";\nimport type { CompactionSettings } from \"@earendil-works/pi-agent-core\";\n\nexport type { CompactionSettings };\n\nexport interface RetrySettings {\n enabled: boolean;\n maxRetries: number;\n baseDelayMs: number;\n}\n\nexport const DEFAULT_RETRY_SETTINGS: RetrySettings = {\n enabled: true,\n maxRetries: 3,\n baseDelayMs: 2000,\n};\n\n/**\n * Per-run resource ceilings — the harness's external circuit breakers.\n *\n * An LLM cannot reliably decide on its own when to stop (the halting problem),\n * so a runaway run has to be stopped from the outside. When any populated cap\n * is exceeded, the run is aborted and a `budget_exceeded` event is emitted.\n * Every field is a cap on a single `prompt()` call; an undefined field means\n * \"no cap on that axis\". Interactive runs are gated turn-by-turn by a human,\n * so they default to uncapped; autonomous runs (scheduled events, triggers)\n * pass {@link DEFAULT_EVENT_BUDGET}, where nobody is watching the loop.\n */\nexport interface BudgetSettings {\n /** Max cumulative tokens processed this run (input + output + cache read/write). */\n maxTokens?: number;\n /** Max cumulative provider cost (USD) this run. Requires a populated model cost table. */\n maxCostUsd?: number;\n /** Max wall-clock duration for the run, in milliseconds. */\n maxDurationMs?: number;\n /** Max number of LLM calls (assistant turns) this run. */\n maxLlmCalls?: number;\n}\n\n/** Interactive runs are human-gated per turn — no automatic ceiling by default. */\nexport const DEFAULT_BUDGET_SETTINGS: BudgetSettings = {};\n\n/**\n * Ceiling for autonomous (event / trigger) runs, where no human is watching\n * the loop. Deliberately generous — it is a stop-loss, not a target.\n */\nexport const DEFAULT_EVENT_BUDGET: BudgetSettings = {\n maxDurationMs: 10 * 60 * 1000,\n maxLlmCalls: 50,\n maxCostUsd: 2,\n};\n\nexport interface HarnessSettings {\n compaction: CompactionSettings;\n retry: RetrySettings;\n budget: BudgetSettings;\n}\n\nexport function resolveHarnessSettings(overrides?: {\n compaction?: Partial<CompactionSettings>;\n retry?: Partial<RetrySettings>;\n budget?: Partial<BudgetSettings>;\n}): HarnessSettings {\n return {\n compaction: { ...DEFAULT_COMPACTION_SETTINGS, ...overrides?.compaction },\n retry: { ...DEFAULT_RETRY_SETTINGS, ...overrides?.retry },\n budget: { ...DEFAULT_BUDGET_SETTINGS, ...overrides?.budget },\n };\n}\n"]}
@@ -0,0 +1,54 @@
1
+ /** A loaded skill. `baseDir` is the directory containing the skill file. */
2
+ export interface MikanSkill {
3
+ name: string;
4
+ description: string;
5
+ /** Full skill instructions (file content without frontmatter). */
6
+ content: string;
7
+ filePath: string;
8
+ baseDir: string;
9
+ /** Where the skill was loaded from (e.g. "workspace", "channel"). */
10
+ source: string;
11
+ /** Exclude from model-visible skill lists. */
12
+ disableModelInvocation?: boolean;
13
+ /**
14
+ * Embed the skill body in the prompt instead of referencing its file path.
15
+ * Used for skills whose files the agent cannot read at runtime (e.g.
16
+ * extension skills under the host-only state dir in sandbox modes).
17
+ */
18
+ inline?: boolean;
19
+ }
20
+ export interface SkillDiagnostic {
21
+ type: "warning";
22
+ message: string;
23
+ path: string;
24
+ }
25
+ export interface LoadSkillsResult {
26
+ skills: MikanSkill[];
27
+ diagnostics: SkillDiagnostic[];
28
+ }
29
+ interface Frontmatter {
30
+ values: Record<string, string>;
31
+ body: string;
32
+ }
33
+ /**
34
+ * Parse simple `key: value` YAML frontmatter delimited by `---` lines.
35
+ * Quoted values are unquoted; nested structures are not supported.
36
+ */
37
+ export declare function parseFrontmatter(content: string): Frontmatter;
38
+ /**
39
+ * Load skills from a directory tree.
40
+ * - A directory containing `SKILL.md` is a skill root; no further recursion.
41
+ * - Direct `.md` children of the top-level directory load as skills.
42
+ * - Dot-directories and `node_modules` are skipped.
43
+ */
44
+ export declare function loadSkillsFromDir(options: {
45
+ dir: string;
46
+ source: string;
47
+ }): LoadSkillsResult;
48
+ /**
49
+ * Format skills for a system prompt using the Agent Skills XML block.
50
+ * Skills with `disableModelInvocation` are excluded.
51
+ */
52
+ export declare function formatSkillsForPrompt(skills: MikanSkill[]): string;
53
+ export {};
54
+ //# sourceMappingURL=skills.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skills.d.ts","sourceRoot":"","sources":["../../src/harness/skills.ts"],"names":[],"mappings":"AAeA,4EAA4E;AAC5E,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,kEAAkE;IAClE,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,qEAAqE;IACrE,MAAM,EAAE,MAAM,CAAC;IACf,8CAA8C;IAC9C,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,WAAW,EAAE,eAAe,EAAE,CAAC;CAChC;AAED,UAAU,WAAW;IACnB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,CAoB7D;AAkED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,gBAAgB,CAE5F;AAgFD;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,UAAU,EAAE,GAAG,MAAM,CAyBlE"}
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Skill loading for the mikan harness.
3
+ *
4
+ * Skills follow the Agent Skills layout: a directory containing a `SKILL.md`
5
+ * with YAML frontmatter (`name`, `description`), or standalone `.md` files in
6
+ * the root of a skills directory. Discovery mirrors the behavior mikan's
7
+ * system prompt documents: a directory with a `SKILL.md` is a skill root
8
+ * (no further recursion), other directories are traversed.
9
+ */
10
+ import { existsSync, readdirSync, readFileSync, statSync } from "fs";
11
+ import { basename, dirname, join } from "path";
12
+ const MAX_NAME_LENGTH = 64;
13
+ const MAX_DESCRIPTION_LENGTH = 1024;
14
+ /**
15
+ * Parse simple `key: value` YAML frontmatter delimited by `---` lines.
16
+ * Quoted values are unquoted; nested structures are not supported.
17
+ */
18
+ export function parseFrontmatter(content) {
19
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
20
+ if (!match)
21
+ return { values: {}, body: content };
22
+ const values = {};
23
+ for (const line of match[1].split(/\r?\n/)) {
24
+ const separator = line.indexOf(":");
25
+ if (separator === -1)
26
+ continue;
27
+ const key = line.slice(0, separator).trim();
28
+ if (!key)
29
+ continue;
30
+ let value = line.slice(separator + 1).trim();
31
+ if ((value.startsWith('"') && value.endsWith('"')) ||
32
+ (value.startsWith("'") && value.endsWith("'"))) {
33
+ value = value.slice(1, -1);
34
+ }
35
+ values[key] = value;
36
+ }
37
+ return { values, body: content.slice(match[0].length) };
38
+ }
39
+ function validateSkill(name, description) {
40
+ const errors = [];
41
+ if (name.length > MAX_NAME_LENGTH) {
42
+ errors.push(`name exceeds ${MAX_NAME_LENGTH} characters (${name.length})`);
43
+ }
44
+ if (!/^[a-z0-9-]+$/.test(name) ||
45
+ name.startsWith("-") ||
46
+ name.endsWith("-") ||
47
+ name.includes("--")) {
48
+ errors.push("name must be lowercase a-z, 0-9 and single hyphens, not at the edges");
49
+ }
50
+ if (description.length > MAX_DESCRIPTION_LENGTH) {
51
+ errors.push(`description exceeds ${MAX_DESCRIPTION_LENGTH} characters (${description.length})`);
52
+ }
53
+ return errors;
54
+ }
55
+ function loadSkillFromFile(filePath, source) {
56
+ const diagnostics = [];
57
+ try {
58
+ const raw = readFileSync(filePath, "utf-8");
59
+ const { values, body } = parseFrontmatter(raw);
60
+ const skillDir = dirname(filePath);
61
+ const name = values.name || basename(skillDir);
62
+ const description = values.description ?? "";
63
+ if (!description.trim()) {
64
+ diagnostics.push({ type: "warning", message: "description is required", path: filePath });
65
+ return { skill: null, diagnostics };
66
+ }
67
+ for (const error of validateSkill(name, description)) {
68
+ diagnostics.push({ type: "warning", message: error, path: filePath });
69
+ }
70
+ return {
71
+ skill: {
72
+ name,
73
+ description,
74
+ content: body.trim(),
75
+ filePath,
76
+ baseDir: skillDir,
77
+ source,
78
+ disableModelInvocation: values["disable-model-invocation"] === "true",
79
+ },
80
+ diagnostics,
81
+ };
82
+ }
83
+ catch (error) {
84
+ diagnostics.push({
85
+ type: "warning",
86
+ message: error instanceof Error ? error.message : "failed to parse skill file",
87
+ path: filePath,
88
+ });
89
+ return { skill: null, diagnostics };
90
+ }
91
+ }
92
+ /**
93
+ * Load skills from a directory tree.
94
+ * - A directory containing `SKILL.md` is a skill root; no further recursion.
95
+ * - Direct `.md` children of the top-level directory load as skills.
96
+ * - Dot-directories and `node_modules` are skipped.
97
+ */
98
+ export function loadSkillsFromDir(options) {
99
+ return loadSkillsFromDirInternal(options.dir, options.source, true);
100
+ }
101
+ function loadSkillsFromDirInternal(dir, source, includeRootFiles) {
102
+ const skills = [];
103
+ const diagnostics = [];
104
+ if (!existsSync(dir))
105
+ return { skills, diagnostics };
106
+ let entries;
107
+ try {
108
+ entries = readdirSync(dir, { withFileTypes: true });
109
+ }
110
+ catch {
111
+ return { skills, diagnostics };
112
+ }
113
+ const skillFile = entries.find((entry) => entry.name === "SKILL.md" && isFileLike(dir, entry));
114
+ if (skillFile) {
115
+ const result = loadSkillFromFile(join(dir, skillFile.name), source);
116
+ if (result.skill)
117
+ skills.push(result.skill);
118
+ diagnostics.push(...result.diagnostics);
119
+ return { skills, diagnostics };
120
+ }
121
+ for (const entry of entries) {
122
+ if (entry.name.startsWith(".") || entry.name === "node_modules")
123
+ continue;
124
+ const fullPath = join(dir, entry.name);
125
+ if (isDirectoryLike(dir, entry)) {
126
+ const subResult = loadSkillsFromDirInternal(fullPath, source, false);
127
+ skills.push(...subResult.skills);
128
+ diagnostics.push(...subResult.diagnostics);
129
+ continue;
130
+ }
131
+ if (!includeRootFiles || !entry.name.endsWith(".md") || !isFileLike(dir, entry))
132
+ continue;
133
+ const result = loadSkillFromFile(fullPath, source);
134
+ if (result.skill)
135
+ skills.push(result.skill);
136
+ diagnostics.push(...result.diagnostics);
137
+ }
138
+ return { skills, diagnostics };
139
+ }
140
+ function isFileLike(dir, entry) {
141
+ if (entry.isFile())
142
+ return true;
143
+ if (!entry.isSymbolicLink())
144
+ return false;
145
+ try {
146
+ return statSync(join(dir, entry.name)).isFile();
147
+ }
148
+ catch {
149
+ return false;
150
+ }
151
+ }
152
+ function isDirectoryLike(dir, entry) {
153
+ if (entry.isDirectory())
154
+ return true;
155
+ if (!entry.isSymbolicLink())
156
+ return false;
157
+ try {
158
+ return statSync(join(dir, entry.name)).isDirectory();
159
+ }
160
+ catch {
161
+ return false;
162
+ }
163
+ }
164
+ function escapeXml(str) {
165
+ return str
166
+ .replace(/&/g, "&amp;")
167
+ .replace(/</g, "&lt;")
168
+ .replace(/>/g, "&gt;")
169
+ .replace(/"/g, "&quot;")
170
+ .replace(/'/g, "&apos;");
171
+ }
172
+ /**
173
+ * Format skills for a system prompt using the Agent Skills XML block.
174
+ * Skills with `disableModelInvocation` are excluded.
175
+ */
176
+ export function formatSkillsForPrompt(skills) {
177
+ const visible = skills.filter((skill) => !skill.disableModelInvocation);
178
+ if (visible.length === 0)
179
+ return "";
180
+ const lines = [
181
+ "\n\nThe following skills provide specialized instructions for specific tasks.",
182
+ "Use the read tool to load a skill's file when the task matches its description.",
183
+ "When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.",
184
+ "",
185
+ "<available_skills>",
186
+ ];
187
+ for (const skill of visible) {
188
+ lines.push(" <skill>");
189
+ lines.push(` <name>${escapeXml(skill.name)}</name>`);
190
+ lines.push(` <description>${escapeXml(skill.description)}</description>`);
191
+ if (skill.inline) {
192
+ // Inline skills carry their body: the agent cannot read their file.
193
+ lines.push(` <instructions>${escapeXml(skill.content)}</instructions>`);
194
+ }
195
+ else {
196
+ lines.push(` <location>${escapeXml(skill.filePath)}</location>`);
197
+ }
198
+ lines.push(" </skill>");
199
+ }
200
+ lines.push("</available_skills>");
201
+ return lines.join("\n");
202
+ }
203
+ //# sourceMappingURL=skills.js.map