min-agent 0.2.1 → 0.4.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 (137) hide show
  1. package/README.md +242 -31
  2. package/dist/agent.js +1233 -485
  3. package/dist/assistant-stream.js +11 -7
  4. package/dist/cli/commands/chat.js +10 -0
  5. package/dist/cli/commands/exec.js +32 -0
  6. package/dist/cli/commands/history.js +58 -0
  7. package/dist/cli/commands/index.js +224 -0
  8. package/dist/cli/commands/init.js +18 -0
  9. package/dist/cli/commands/mcp.js +173 -0
  10. package/dist/cli/commands/memory.js +69 -0
  11. package/dist/cli/commands/models.js +21 -0
  12. package/dist/cli/commands/permission.js +12 -0
  13. package/dist/cli/commands/rules.js +33 -0
  14. package/dist/cli/commands/sandbox.js +13 -0
  15. package/dist/cli/commands/serve.js +9 -0
  16. package/dist/cli/commands/setup.js +4 -0
  17. package/dist/cli/commands/shared.js +16 -0
  18. package/dist/cli/commands/skills.js +119 -0
  19. package/dist/cli/commands/update.js +7 -0
  20. package/dist/cli/commands/write-config.js +30 -0
  21. package/dist/cli/errors.js +36 -0
  22. package/dist/cli/exec-prompt.js +26 -0
  23. package/dist/cli/option-helpers.js +53 -0
  24. package/dist/cli/program.js +180 -0
  25. package/dist/cli.js +7 -632
  26. package/dist/clipboard.js +59 -23
  27. package/dist/code-mode.js +35 -17
  28. package/dist/compaction.js +457 -169
  29. package/dist/config.js +298 -38
  30. package/dist/confirm.js +105 -9
  31. package/dist/context-window.js +156 -75
  32. package/dist/doom-loop.js +268 -26
  33. package/dist/fetch-timeout.js +152 -0
  34. package/dist/http-approvals.js +60 -0
  35. package/dist/http.js +119 -0
  36. package/dist/instructions.js +72 -33
  37. package/dist/logger.js +95 -0
  38. package/dist/markdown.js +35 -50
  39. package/dist/mcp.js +847 -102
  40. package/dist/memory.js +128 -45
  41. package/dist/output.js +42 -31
  42. package/dist/paste-handler.js +3 -3
  43. package/dist/permission-cli.js +43 -0
  44. package/dist/plugins.js +76 -11
  45. package/dist/pricing.js +119 -0
  46. package/dist/provider.js +34 -15
  47. package/dist/question-format.js +60 -0
  48. package/dist/sandbox-cli.js +82 -0
  49. package/dist/sandbox.js +403 -0
  50. package/dist/save-throttle.js +45 -0
  51. package/dist/serve/common.js +404 -0
  52. package/dist/serve/routes-chat.js +347 -0
  53. package/dist/serve/routes-mcp.js +212 -0
  54. package/dist/serve/routes-memory.js +66 -0
  55. package/dist/serve/routes-meta.js +205 -0
  56. package/dist/serve/routes-sessions.js +61 -0
  57. package/dist/serve/routes-skills.js +70 -0
  58. package/dist/serve.js +74 -635
  59. package/dist/sessions.js +197 -15
  60. package/dist/skills.js +531 -77
  61. package/dist/synthetic.js +7 -0
  62. package/dist/title-gen.js +9 -2
  63. package/dist/token-display.js +36 -0
  64. package/dist/tool-display.js +178 -0
  65. package/dist/tool-output.js +53 -46
  66. package/dist/tools/apply_patch.js +265 -0
  67. package/dist/tools/atomic-file.js +35 -0
  68. package/dist/tools/backend.js +61 -0
  69. package/dist/tools/bash.js +186 -71
  70. package/dist/tools/code_search.js +13 -6
  71. package/dist/tools/edit.js +26 -9
  72. package/dist/tools/explore.js +144 -16
  73. package/dist/tools/glob.js +7 -3
  74. package/dist/tools/grep.js +153 -14
  75. package/dist/tools/index.js +9 -24
  76. package/dist/tools/question.js +31 -30
  77. package/dist/tools/read.js +77 -15
  78. package/dist/tools/search-searxng.js +223 -0
  79. package/dist/tools/search-serper.js +189 -0
  80. package/dist/tools/task.js +100 -33
  81. package/dist/tools/todo.js +178 -67
  82. package/dist/tools/web_fetch.js +158 -46
  83. package/dist/tools/web_search.js +217 -29
  84. package/dist/tools/write.js +34 -11
  85. package/dist/tui/App.js +89 -6
  86. package/dist/tui/ConfirmBar.js +57 -4
  87. package/dist/tui/InputBar.js +504 -44
  88. package/dist/tui/MessageList.js +674 -20
  89. package/dist/tui/ModelPicker.js +113 -0
  90. package/dist/tui/QuestionBar.js +136 -0
  91. package/dist/tui/SessionPicker.js +79 -0
  92. package/dist/tui/StatusBar.js +14 -12
  93. package/dist/tui/agent-runner.js +223 -0
  94. package/dist/tui/caret-pos.js +177 -0
  95. package/dist/tui/caret.js +69 -0
  96. package/dist/tui/click-count.js +13 -0
  97. package/dist/tui/diff-view.js +61 -0
  98. package/dist/tui/drag-state.js +49 -0
  99. package/dist/tui/hydrate.js +129 -0
  100. package/dist/tui/index.js +189 -31
  101. package/dist/tui/input-history.js +125 -0
  102. package/dist/tui/layout.js +88 -0
  103. package/dist/tui/mouse.js +46 -0
  104. package/dist/tui/prompt-queue.js +24 -0
  105. package/dist/tui/selection.js +226 -0
  106. package/dist/tui/session-switch.js +28 -0
  107. package/dist/tui/slash-commands.js +106 -0
  108. package/dist/tui/slash-handler.js +545 -0
  109. package/dist/tui/text-width.js +113 -0
  110. package/dist/tui/theme.js +12 -0
  111. package/dist/tui/token-info.js +7 -0
  112. package/dist/tui/tool-children.js +19 -0
  113. package/dist/tui/undo-stack.js +14 -0
  114. package/dist/tui/use-sgr-mouse.js +29 -0
  115. package/dist/tui-chat.js +346 -330
  116. package/dist/updater.js +116 -0
  117. package/dist/xml-search.js +194 -0
  118. package/docs/API.md +410 -32
  119. package/docs/superpowers/plans/2026-08-16-batch1-tui-improvements.md +1510 -0
  120. package/docs/superpowers/plans/2026-08-16-batch2-cli-tools-api.md +2105 -0
  121. package/docs/superpowers/plans/2026-08-16-batch3-config-engineering.md +1595 -0
  122. package/docs/superpowers/plans/2026-08-16-input-caret.md +782 -0
  123. package/docs/superpowers/plans/2026-08-20-tui-completeness.md +873 -0
  124. package/docs/superpowers/plans/2026-08-20-unified-tui-default.md +631 -0
  125. package/docs/superpowers/specs/2026-08-16-batch1-tui-improvements-design.md +183 -0
  126. package/docs/superpowers/specs/2026-08-16-batch2-cli-tools-api-design.md +220 -0
  127. package/docs/superpowers/specs/2026-08-16-batch3-config-engineering-design.md +196 -0
  128. package/docs/superpowers/specs/2026-08-16-input-caret-design.md +63 -0
  129. package/docs/superpowers/specs/2026-08-17-mouse-selection-design.md +116 -0
  130. package/docs/superpowers/specs/2026-08-20-config-http-alignment-design.md +47 -0
  131. package/docs/superpowers/specs/2026-08-20-mcp-plugins-alignment-design.md +37 -0
  132. package/docs/superpowers/specs/2026-08-20-sandbox-permissions-design.md +68 -0
  133. package/docs/superpowers/specs/2026-08-20-tui-completeness-design.md +273 -0
  134. package/docs/superpowers/specs/2026-08-20-unified-tui-default-design.md +165 -0
  135. package/package.json +12 -8
  136. package/skills/self-config/SKILL.md +90 -0
  137. package/skills/self-config/reference.md +149 -0
@@ -0,0 +1,404 @@
1
+ import { randomUUID } from "crypto";
2
+ import { getSandboxSnapshot, getPermissionSnapshot, permissionModeLabel } from "../config.js";
3
+ import { parseNetworkPolicy, parseSandboxMode, runWithSandboxPolicy } from "../sandbox.js";
4
+ import { runWithConfirmContext, getPermissionOverride } from "../confirm.js";
5
+ import { runOnceWithSystem } from "../agent.js";
6
+ import { saveSession, sanitizeForStorage } from "../sessions.js";
7
+ import { createSaveThrottle } from "../save-throttle.js";
8
+ import { scanProject, buildCodeSystemPrompt } from "../code-mode.js";
9
+ import { TokenTracker } from "../compaction.js";
10
+ import { emptyTaskState } from "../tools/todo.js";
11
+ export { localVersion as packageVersion } from "../updater.js";
12
+ const MAX_BODY_BYTES = 2 * 1024 * 1024;
13
+ const BODY_TIMEOUT_MS = 30_000;
14
+ const MAX_TOOL_RESULT_SSE_CHARS = 48_000;
15
+ export const MAX_CONCURRENT_CHATS = 8;
16
+ export function corsEnabled() {
17
+ const v = process.env.MIN_AGENT_SERVE_CORS;
18
+ return v === "1" || v === "true";
19
+ }
20
+ export function corsHeaders() {
21
+ if (corsEnabled()) {
22
+ return {
23
+ "Access-Control-Allow-Origin": "*",
24
+ "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
25
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
26
+ };
27
+ }
28
+ return {};
29
+ }
30
+ export function skillPayload(s) {
31
+ return {
32
+ name: s.name,
33
+ description: s.description,
34
+ location: s.location,
35
+ enabled: s.enabled,
36
+ ...(s.disabledScope ? { disabled_scope: s.disabledScope } : {}),
37
+ ...(s.version ? { version: s.version } : {}),
38
+ ...(s.allowedTools.length ? { allowed_tools: s.allowedTools } : {}),
39
+ ...(s.requiredBins.length ? { requires_bins: s.requiredBins } : {}),
40
+ ...(s.builtin ? { builtin: true } : {}),
41
+ ...(s.alwaysLoad ? { always_load: true } : {}),
42
+ };
43
+ }
44
+ export function sendJson(res, status, body) {
45
+ const headers = {
46
+ "Content-Type": "application/json; charset=utf-8",
47
+ ...corsHeaders(),
48
+ };
49
+ res.writeHead(status, headers);
50
+ res.end(JSON.stringify(body));
51
+ }
52
+ /** Decode a URL path segment without letting malformed escapes become a 500. */
53
+ export function decodePathSegment(raw) {
54
+ try {
55
+ return decodeURIComponent(raw);
56
+ }
57
+ catch {
58
+ return null;
59
+ }
60
+ }
61
+ function readBody(req) {
62
+ return new Promise((resolve, reject) => {
63
+ const chunks = [];
64
+ let total = 0;
65
+ const timer = setTimeout(() => {
66
+ req.destroy();
67
+ reject(new Error("body_timeout"));
68
+ }, BODY_TIMEOUT_MS);
69
+ req.on("data", (c) => {
70
+ total += c.length;
71
+ if (total > MAX_BODY_BYTES) {
72
+ clearTimeout(timer);
73
+ req.removeAllListeners("data");
74
+ req.pause();
75
+ reject(new Error("body_too_large"));
76
+ return;
77
+ }
78
+ chunks.push(c);
79
+ });
80
+ req.on("end", () => {
81
+ clearTimeout(timer);
82
+ resolve(Buffer.concat(chunks).toString("utf-8"));
83
+ });
84
+ req.on("error", (e) => {
85
+ clearTimeout(timer);
86
+ reject(e);
87
+ });
88
+ });
89
+ }
90
+ export async function readJsonBody(req) {
91
+ let raw;
92
+ try {
93
+ raw = await readBody(req);
94
+ }
95
+ catch (e) {
96
+ if (e?.message === "body_too_large") {
97
+ return { ok: false, status: 413, error: "payload_too_large" };
98
+ }
99
+ if (e?.message === "body_timeout") {
100
+ return { ok: false, status: 408, error: "payload_timeout" };
101
+ }
102
+ throw e;
103
+ }
104
+ try {
105
+ return { ok: true, value: JSON.parse(raw) };
106
+ }
107
+ catch {
108
+ return { ok: false, status: 400, error: "invalid_json" };
109
+ }
110
+ }
111
+ export function sendJsonError(res, e) {
112
+ if (e.status === 413) {
113
+ // The request body was abandoned mid-stream; drop keep-alive so the
114
+ // connection cannot be reused for a second request.
115
+ res.shouldKeepAlive = false;
116
+ }
117
+ sendJson(res, e.status, e.status === 413 ? { error: e.error, max_bytes: MAX_BODY_BYTES } : { error: e.error });
118
+ }
119
+ function sseWrite(res, obj) {
120
+ try {
121
+ if (res.writableEnded)
122
+ return;
123
+ res.write(`data: ${JSON.stringify(obj)}\n\n`);
124
+ }
125
+ catch {
126
+ /* client gone */
127
+ }
128
+ }
129
+ function truncateForJson(v, max) {
130
+ if (typeof v === "string" && v.length > max)
131
+ return v.slice(0, max) + `\n… [truncated ${v.length - max} chars]`;
132
+ return v;
133
+ }
134
+ function sseHeaders(cors) {
135
+ return {
136
+ "Content-Type": "text/event-stream; charset=utf-8",
137
+ "Cache-Control": "no-cache, no-transform",
138
+ Connection: "keep-alive",
139
+ "X-Accel-Buffering": "no",
140
+ ...cors,
141
+ };
142
+ }
143
+ function createSseCallbacks(res, messages, opts = {}) {
144
+ return {
145
+ onAssistantDisplayDelta(delta) {
146
+ sseWrite(res, { type: "assistant", text: delta });
147
+ },
148
+ onThinkingDelta(delta) {
149
+ sseWrite(res, { type: "thinking", text: delta });
150
+ },
151
+ onToolCall(name, input) {
152
+ sseWrite(res, { type: "tool_call", name, input: truncateForJson(input, MAX_TOOL_RESULT_SSE_CHARS) });
153
+ },
154
+ onToolResult(name, output) {
155
+ sseWrite(res, { type: "tool_result", name, output: truncateForJson(output, MAX_TOOL_RESULT_SSE_CHARS) });
156
+ },
157
+ onCompaction(line) {
158
+ sseWrite(res, { type: "compaction", line });
159
+ },
160
+ onRetryNotice(info) {
161
+ sseWrite(res, {
162
+ type: "notice",
163
+ kind: info.kind,
164
+ attempt: info.attempt,
165
+ max_attempts: info.maxAttempts,
166
+ delay_ms: info.delayMs,
167
+ });
168
+ },
169
+ onStreamError(message) {
170
+ sseWrite(res, { type: "error", message });
171
+ },
172
+ onRunFinish(info) {
173
+ let saved;
174
+ if (opts.sessionId && messages.length > 0) {
175
+ try {
176
+ saved = saveSession(messages, opts.sessionId, undefined, undefined, opts.taskState);
177
+ }
178
+ catch {
179
+ saved = undefined;
180
+ }
181
+ }
182
+ sseWrite(res, {
183
+ type: "done",
184
+ step_count: info.stepCount,
185
+ usage: info.usage,
186
+ context_tokens: info.contextTokens ?? 0,
187
+ has_error: info.hasError,
188
+ aborted: info.aborted,
189
+ max_steps_reached: info.maxStepsReached,
190
+ incomplete: info.incomplete ?? false,
191
+ budget_exceeded: info.budgetExceeded ?? false,
192
+ continues: info.continues ?? 0,
193
+ stopped: info.stopped ?? false,
194
+ empty_response: info.emptyResponse ?? false,
195
+ empty_attempts: info.emptyAttempts ?? 0,
196
+ wrap_up: info.wrapUp ?? false,
197
+ session_id: saved,
198
+ messages: sanitizeForStorage(messages),
199
+ task_state: opts.taskState,
200
+ project: opts.project,
201
+ });
202
+ if (!res.writableEnded)
203
+ res.end();
204
+ },
205
+ };
206
+ }
207
+ function createJsonCallbacks() {
208
+ const toolCalls = [];
209
+ const toolResults = [];
210
+ let finishInfo = null;
211
+ const callbacks = {
212
+ onToolCall(name, input) {
213
+ toolCalls.push({ name, input: truncateForJson(input, MAX_TOOL_RESULT_SSE_CHARS) });
214
+ },
215
+ onToolResult(name, output) {
216
+ toolResults.push({ name, output: truncateForJson(output, MAX_TOOL_RESULT_SSE_CHARS) });
217
+ },
218
+ onRunFinish(info) {
219
+ finishInfo = info;
220
+ },
221
+ };
222
+ return { callbacks, toolCalls, toolResults, finishInfo: () => finishInfo };
223
+ }
224
+ export function readSandboxTighten(body) {
225
+ const policy = {};
226
+ if (body.sandbox !== undefined) {
227
+ const mode = parseSandboxMode(body.sandbox);
228
+ if (!mode)
229
+ return { ok: false, error: "invalid_sandbox", detail: "sandbox must be off, workspace, or strict" };
230
+ policy.mode = mode;
231
+ }
232
+ if (body.network !== undefined) {
233
+ const network = parseNetworkPolicy(body.network);
234
+ if (!network)
235
+ return { ok: false, error: "invalid_network", detail: "network must be allow or deny" };
236
+ policy.network = network;
237
+ }
238
+ return { ok: true, policy };
239
+ }
240
+ export function readStringPathList(value) {
241
+ if (value === undefined)
242
+ return { ok: true };
243
+ if (!Array.isArray(value) || value.some((p) => typeof p !== "string" || p.length === 0))
244
+ return { ok: false };
245
+ return { ok: true, value };
246
+ }
247
+ export function sandboxPayload() {
248
+ const snap = getSandboxSnapshot();
249
+ return {
250
+ mode: snap.mode,
251
+ network: snap.network,
252
+ extraWriteRoots: snap.extraWriteRoots,
253
+ extraReadRoots: snap.extraReadRoots,
254
+ source: snap.source,
255
+ label: snap.label,
256
+ };
257
+ }
258
+ export function permissionPayload() {
259
+ const override = getPermissionOverride();
260
+ const snap = getPermissionSnapshot();
261
+ const permission = override ?? snap.permission;
262
+ const source = override ? "cli" : snap.source;
263
+ return {
264
+ permission,
265
+ source,
266
+ label: permissionModeLabel(permission),
267
+ };
268
+ }
269
+ export function normalizeMessages(body) {
270
+ if (body.messages && Array.isArray(body.messages)) {
271
+ if (body.messages.length === 0)
272
+ return { ok: false, error: "messages must be non-empty" };
273
+ return { ok: true, messages: [...body.messages] };
274
+ }
275
+ if (typeof body.message === "string" && body.message.length > 0) {
276
+ return { ok: true, messages: [{ role: "user", content: body.message }] };
277
+ }
278
+ return { ok: false, error: "Provide `message` (string) or non-empty `messages` array" };
279
+ }
280
+ export function chatRunOptions(body) {
281
+ return {
282
+ temperature: typeof body.temperature === "number" ? body.temperature : undefined,
283
+ maxTokens: typeof body.maxTokens === "number" ? body.maxTokens : undefined,
284
+ topP: typeof body.topP === "number" ? body.topP : undefined,
285
+ providerName: typeof body.provider === "string" ? body.provider : undefined,
286
+ ...(body.plan_mode === true ? { planMode: true } : {}),
287
+ };
288
+ }
289
+ function httpConfirmContext(ctx, res, autoApprove, pendingIds) {
290
+ if (autoApprove)
291
+ return { autoApprove: true };
292
+ return {
293
+ autoApprove: false,
294
+ onConfirm: async (message) => {
295
+ const id = randomUUID();
296
+ pendingIds.push(id);
297
+ sseWrite(res, { type: "confirm", id, message });
298
+ return ctx.approvalGate.waitConfirm(id);
299
+ },
300
+ onQuestion: async (prompt, options) => {
301
+ const id = randomUUID();
302
+ pendingIds.push(id);
303
+ sseWrite(res, { type: "question", id, prompt, ...(options && options.length > 0 ? { options } : {}) });
304
+ return ctx.approvalGate.waitQuestion(id);
305
+ },
306
+ };
307
+ }
308
+ export async function executeChat(ctx, opts) {
309
+ const { res, messages, modelId, stream, sessionId, runOptions, autoApprove } = opts;
310
+ const taskState = opts.taskState ?? emptyTaskState();
311
+ if (!autoApprove && !stream) {
312
+ sendJson(res, 400, {
313
+ error: "auto_approve_requires_stream",
314
+ detail: "Set stream: true when auto_approve is false, then answer via POST /v1/approvals and POST /v1/answers",
315
+ });
316
+ return;
317
+ }
318
+ const abort = new AbortController();
319
+ const pendingIds = [];
320
+ const onClose = () => {
321
+ abort.abort();
322
+ for (const id of pendingIds)
323
+ ctx.approvalGate.cancel(id);
324
+ };
325
+ res.on("close", onClose);
326
+ const tracker = new TokenTracker();
327
+ const project = scanProject();
328
+ const codeSystemPrompt = buildCodeSystemPrompt(project, ctx.instructions);
329
+ const confirmCtx = httpConfirmContext(ctx, res, autoApprove, pendingIds);
330
+ const tighten = opts.sandboxTighten;
331
+ const run = (callbacks) => {
332
+ // Persist mid-run so a dropped connection or crash keeps the work so far.
333
+ const saveThrottle = sessionId != null
334
+ ? createSaveThrottle(() => {
335
+ try {
336
+ saveSession(messages, sessionId, undefined, undefined, taskState);
337
+ }
338
+ catch { }
339
+ })
340
+ : null;
341
+ const exec = () => runWithConfirmContext(confirmCtx, () => runOnceWithSystem(messages, codeSystemPrompt, modelId, abort.signal, callbacks, tracker, {
342
+ ...runOptions,
343
+ taskState,
344
+ ...(saveThrottle ? { onStepPersist: () => saveThrottle.request() } : {}),
345
+ }));
346
+ const finish = (p) => p.finally(() => {
347
+ saveThrottle?.cancel();
348
+ });
349
+ if (tighten && (tighten.mode || tighten.network))
350
+ return finish(runWithSandboxPolicy(tighten, exec));
351
+ return finish(exec());
352
+ };
353
+ if (stream) {
354
+ res.writeHead(200, sseHeaders(corsHeaders()));
355
+ res.flushHeaders?.();
356
+ const callbacks = createSseCallbacks(res, messages, { sessionId, project, taskState });
357
+ try {
358
+ await run(callbacks);
359
+ }
360
+ catch (err) {
361
+ const message = err instanceof Error ? err.message : String(err);
362
+ if (!res.writableEnded) {
363
+ sseWrite(res, { type: "fatal", message });
364
+ res.end();
365
+ }
366
+ }
367
+ return;
368
+ }
369
+ const { callbacks, toolCalls, toolResults, finishInfo } = createJsonCallbacks();
370
+ await run(callbacks);
371
+ const lastAssistant = [...messages].reverse().find((m) => m.role === "assistant");
372
+ let savedSession;
373
+ if (sessionId && messages.length > 0) {
374
+ try {
375
+ savedSession = saveSession(messages, sessionId, undefined, undefined, taskState);
376
+ }
377
+ catch {
378
+ savedSession = undefined;
379
+ }
380
+ }
381
+ const fi = finishInfo();
382
+ sendJson(res, 200, {
383
+ messages: sanitizeForStorage(messages),
384
+ assistant: lastAssistant ?? null,
385
+ tool_calls: toolCalls,
386
+ tool_results: toolResults,
387
+ session_id: savedSession,
388
+ step_count: fi?.stepCount ?? 0,
389
+ usage: fi?.usage ?? null,
390
+ context_tokens: fi?.contextTokens ?? 0,
391
+ has_error: fi?.hasError ?? false,
392
+ aborted: fi?.aborted ?? false,
393
+ max_steps_reached: fi?.maxStepsReached ?? false,
394
+ incomplete: fi?.incomplete ?? false,
395
+ budget_exceeded: fi?.budgetExceeded ?? false,
396
+ continues: fi?.continues ?? 0,
397
+ stopped: fi?.stopped ?? false,
398
+ empty_response: fi?.emptyResponse ?? false,
399
+ empty_attempts: fi?.emptyAttempts ?? 0,
400
+ wrap_up: fi?.wrapUp ?? false,
401
+ task_state: taskState,
402
+ project,
403
+ });
404
+ }