@yuandc/aica 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/dist/acp/agent.js +54 -0
- package/dist/acp/client/acp-client.js +102 -0
- package/dist/acp/client/acp-content.js +13 -0
- package/dist/acp/client/acp-events.js +106 -0
- package/dist/acp/client/acp-process.js +34 -0
- package/dist/acp/client/acp-runtime-pool.js +248 -0
- package/dist/acp/client/context-usage.js +29 -0
- package/dist/acp/client/json-rpc.js +128 -0
- package/dist/acp/provider-types.js +1 -0
- package/dist/acp/providers/codex/codex-process.js +51 -0
- package/dist/acp/providers/codex/events.js +1473 -0
- package/dist/acp/providers/codex/permissions.js +49 -0
- package/dist/acp/providers/codex/provider.js +376 -0
- package/dist/acp/providers/codex-acp/adapter.js +947 -0
- package/dist/acp/providers/codex-acp/context-maintenance.js +148 -0
- package/dist/acp/providers/codex-acp/launch.js +35 -0
- package/dist/acp/providers/codex-acp/provider.js +486 -0
- package/dist/acp/providers/mimo/provider.js +448 -0
- package/dist/acp/providers/opencode/provider.js +489 -0
- package/dist/acp/providers/registry.js +23 -0
- package/dist/acp/standard-events.js +167 -0
- package/dist/commands/start.js +137 -0
- package/dist/commands/worker-auth.js +100 -0
- package/dist/commands/worker-project.js +57 -0
- package/dist/core/aca-config.js +74 -0
- package/dist/core/aca-server-client.js +57 -0
- package/dist/core/acp-event-coalescer.js +108 -0
- package/dist/core/acp-event-upload-filter.js +16 -0
- package/dist/core/acp-orphan-cleanup.js +91 -0
- package/dist/core/affected-files.js +268 -0
- package/dist/core/auth.js +36 -0
- package/dist/core/file-transfer-worker.js +169 -0
- package/dist/core/fs.js +28 -0
- package/dist/core/heartbeat.js +578 -0
- package/dist/core/job-permission-policy.js +42 -0
- package/dist/core/job-worker.js +749 -0
- package/dist/core/logger.js +42 -0
- package/dist/core/long-poll-worker.js +26 -0
- package/dist/core/machine-filesystem-worker.js +352 -0
- package/dist/core/paths.js +26 -0
- package/dist/core/process-identity.js +34 -0
- package/dist/core/process.js +33 -0
- package/dist/core/provider-health.js +54 -0
- package/dist/core/runtime-options.js +38 -0
- package/dist/core/worktree.js +95 -0
- package/dist/worker-cli.js +27 -0
- package/dist/worker-single-cli.js +17 -0
- package/package.json +35 -0
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import { readJsonFile, writeJsonFile } from "./fs.js";
|
|
4
|
+
import { getRuntimeStatePath } from "./paths.js";
|
|
5
|
+
import { acaServerRequest } from "./aca-server-client.js";
|
|
6
|
+
import { loadAcaConfig } from "./aca-config.js";
|
|
7
|
+
import { providerHealthSnapshot } from "./provider-health.js";
|
|
8
|
+
export const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
9
|
+
const MIMO_MODELS_CACHE_TTL_MS = 6 * 60 * 60 * 1000;
|
|
10
|
+
const OPENCODE_MODELS_CACHE_TTL_MS = 6 * 60 * 60 * 1000;
|
|
11
|
+
let mimoCapabilitiesCache = null;
|
|
12
|
+
let openCodeCapabilitiesCache = null;
|
|
13
|
+
export async function sendHeartbeat(config = loadAcaConfig()) {
|
|
14
|
+
if (!config.token) {
|
|
15
|
+
return { ok: false, skipped: true, reason: "not_logged_in" };
|
|
16
|
+
}
|
|
17
|
+
const response = await acaServerRequest("POST", "/api/heartbeat", {
|
|
18
|
+
machineId: config.machineId,
|
|
19
|
+
machineName: config.machineName,
|
|
20
|
+
machineInfo: {
|
|
21
|
+
platform: os.platform(),
|
|
22
|
+
architecture: os.arch(),
|
|
23
|
+
osRelease: os.release(),
|
|
24
|
+
acaVersion: acaVersion(),
|
|
25
|
+
hostname: os.hostname()
|
|
26
|
+
},
|
|
27
|
+
workspaceId: config.defaultWorkspaceId,
|
|
28
|
+
agentConfigs: buildHeartbeatAgentConfigs(config),
|
|
29
|
+
projects: config.projects.map((project) => ({
|
|
30
|
+
projectId: project.projectId,
|
|
31
|
+
workspaceId: project.workspaceId,
|
|
32
|
+
name: project.name,
|
|
33
|
+
rootPath: project.rootPath
|
|
34
|
+
}))
|
|
35
|
+
});
|
|
36
|
+
return response;
|
|
37
|
+
}
|
|
38
|
+
function buildHeartbeatAgentConfigs(config) {
|
|
39
|
+
const health = providerHealthSnapshot();
|
|
40
|
+
return [
|
|
41
|
+
{
|
|
42
|
+
agentId: `${config.machineId}:builtin:codex`,
|
|
43
|
+
workspaceId: config.defaultWorkspaceId,
|
|
44
|
+
name: "Codex",
|
|
45
|
+
agentType: "codex",
|
|
46
|
+
cliType: "builtin",
|
|
47
|
+
machineId: config.machineId,
|
|
48
|
+
model: "gpt-5.5",
|
|
49
|
+
mode: "agent-full-access",
|
|
50
|
+
raw: {
|
|
51
|
+
providerStatus: health.codex,
|
|
52
|
+
capabilities: codexStaticCapabilities()
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
agentId: `${config.machineId}:builtin:mimo`,
|
|
57
|
+
workspaceId: config.defaultWorkspaceId,
|
|
58
|
+
name: "MimoCode",
|
|
59
|
+
agentType: "mimo",
|
|
60
|
+
cliType: "builtin",
|
|
61
|
+
machineId: config.machineId,
|
|
62
|
+
model: "mimo/mimo-auto",
|
|
63
|
+
mode: "agent-full-access",
|
|
64
|
+
raw: {
|
|
65
|
+
providerStatus: health.mimo,
|
|
66
|
+
capabilities: mimoCapabilities()
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
agentId: `${config.machineId}:builtin:opencode`,
|
|
71
|
+
workspaceId: config.defaultWorkspaceId,
|
|
72
|
+
name: "OpenCode",
|
|
73
|
+
agentType: "opencode",
|
|
74
|
+
cliType: "builtin",
|
|
75
|
+
machineId: config.machineId,
|
|
76
|
+
// The server schema accepts an omitted/null model, but intentionally
|
|
77
|
+
// rejects an empty string. A worker can run without OpenCode configured.
|
|
78
|
+
model: process.env.ACA_OPENCODE_DEFAULT_MODEL || null,
|
|
79
|
+
mode: "agent-full-access",
|
|
80
|
+
raw: {
|
|
81
|
+
providerStatus: health.opencode,
|
|
82
|
+
capabilities: openCodeCapabilities()
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
];
|
|
86
|
+
}
|
|
87
|
+
function acaVersion() {
|
|
88
|
+
return process.env.ACA_VERSION?.trim() || "0.1.0";
|
|
89
|
+
}
|
|
90
|
+
function codexStaticCapabilities() {
|
|
91
|
+
const modes = [
|
|
92
|
+
{ id: "read-only", name: "Read-only", description: "Requires approval to edit files and run commands." },
|
|
93
|
+
{ id: "agent", name: "Agent", description: "Read and edit files, and run commands." },
|
|
94
|
+
{ id: "agent-full-access", name: "Agent (full access)", description: "Edit files and run commands with full access." }
|
|
95
|
+
];
|
|
96
|
+
const models = [
|
|
97
|
+
{ modelId: "gpt-5.5", name: "gpt-5.5", description: "Latest frontier Codex model" },
|
|
98
|
+
{ modelId: "gpt-5.4", name: "gpt-5.4", description: "Frontier Codex model" },
|
|
99
|
+
{ modelId: "gpt-5.4-mini", name: "gpt-5.4-mini", description: "Smaller, faster Codex model" }
|
|
100
|
+
];
|
|
101
|
+
const reasoningOptions = [
|
|
102
|
+
{ value: "low", name: "Low", description: "Fastest responses" },
|
|
103
|
+
{ value: "medium", name: "Medium", description: "Balanced reasoning" },
|
|
104
|
+
{ value: "high", name: "High", description: "More reasoning for difficult tasks" },
|
|
105
|
+
{ value: "xhigh", name: "X High", description: "Extra reasoning for complex tasks" }
|
|
106
|
+
];
|
|
107
|
+
return {
|
|
108
|
+
source: "aca-static",
|
|
109
|
+
fetchedAt: Date.now(),
|
|
110
|
+
contextUsage: true,
|
|
111
|
+
contextCompaction: true,
|
|
112
|
+
modes,
|
|
113
|
+
models,
|
|
114
|
+
configOptions: [
|
|
115
|
+
{
|
|
116
|
+
id: "mode",
|
|
117
|
+
name: "Mode",
|
|
118
|
+
category: "mode",
|
|
119
|
+
type: "select",
|
|
120
|
+
currentValue: "agent-full-access",
|
|
121
|
+
options: modes.map((mode) => ({ value: mode.id, name: mode.name, description: mode.description }))
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
id: "model",
|
|
125
|
+
name: "Model",
|
|
126
|
+
category: "model",
|
|
127
|
+
type: "select",
|
|
128
|
+
currentValue: "gpt-5.5",
|
|
129
|
+
options: models.map((model) => ({ value: model.modelId, name: model.name, description: model.description }))
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: "reasoning_effort",
|
|
133
|
+
name: "Reasoning effort",
|
|
134
|
+
category: "thought_level",
|
|
135
|
+
type: "select",
|
|
136
|
+
currentValue: "medium",
|
|
137
|
+
options: reasoningOptions
|
|
138
|
+
}
|
|
139
|
+
]
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function mimoStaticCapabilities() {
|
|
143
|
+
const modes = [
|
|
144
|
+
{ id: "agent", name: "Agent", description: "Run MimoCode with normal workspace permissions." },
|
|
145
|
+
{ id: "agent-full-access", name: "Agent (full access)", description: "Run MimoCode and auto-approve supported local permissions." }
|
|
146
|
+
];
|
|
147
|
+
const models = [
|
|
148
|
+
{ modelId: "mimo/mimo-auto", name: "MiMo Auto (free)", description: "MiMo Auto free model" },
|
|
149
|
+
{ modelId: "xiaomi/mimo-v2.5", name: "mimo-v2.5", description: "MimoCode v2.5" },
|
|
150
|
+
{ modelId: "xiaomi/mimo-v2.5-pro", name: "mimo-v2.5-pro", description: "MimoCode v2.5 Pro" },
|
|
151
|
+
{ modelId: "xiaomi/mimo-v2.5-pro-ultraspeed", name: "mimo-v2.5-pro-ultraspeed", description: "MimoCode v2.5 Pro UltraSpeed" }
|
|
152
|
+
];
|
|
153
|
+
const variants = [
|
|
154
|
+
{ value: "minimal", name: "Minimal", description: "Fastest responses" },
|
|
155
|
+
{ value: "high", name: "High", description: "More reasoning for difficult tasks" },
|
|
156
|
+
{ value: "max", name: "Max", description: "Maximum reasoning when supported" }
|
|
157
|
+
];
|
|
158
|
+
return {
|
|
159
|
+
source: "aca-static",
|
|
160
|
+
fetchedAt: Date.now(),
|
|
161
|
+
contextUsage: true,
|
|
162
|
+
contextCompaction: false,
|
|
163
|
+
modes,
|
|
164
|
+
models,
|
|
165
|
+
configOptions: [
|
|
166
|
+
{
|
|
167
|
+
id: "mode",
|
|
168
|
+
name: "Mode",
|
|
169
|
+
category: "mode",
|
|
170
|
+
type: "select",
|
|
171
|
+
currentValue: "agent-full-access",
|
|
172
|
+
options: modes.map((mode) => ({ value: mode.id, name: mode.name, description: mode.description }))
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
id: "model",
|
|
176
|
+
name: "Model",
|
|
177
|
+
category: "model",
|
|
178
|
+
type: "select",
|
|
179
|
+
currentValue: "mimo/mimo-auto",
|
|
180
|
+
options: models.map((model) => ({ value: model.modelId, name: model.name, description: model.description }))
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
id: "variant",
|
|
184
|
+
name: "Variant",
|
|
185
|
+
category: "thought_level",
|
|
186
|
+
type: "select",
|
|
187
|
+
currentValue: "high",
|
|
188
|
+
options: variants
|
|
189
|
+
}
|
|
190
|
+
]
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function mimoCapabilities() {
|
|
194
|
+
const now = Date.now();
|
|
195
|
+
if (mimoCapabilitiesCache && mimoCapabilitiesCache.expiresAtMs > now) {
|
|
196
|
+
return mimoCapabilitiesCache.capabilities;
|
|
197
|
+
}
|
|
198
|
+
const capabilities = mimoCapabilitiesFromCli() ?? mimoStaticCapabilities();
|
|
199
|
+
mimoCapabilitiesCache = {
|
|
200
|
+
expiresAtMs: now + MIMO_MODELS_CACHE_TTL_MS,
|
|
201
|
+
capabilities
|
|
202
|
+
};
|
|
203
|
+
return capabilities;
|
|
204
|
+
}
|
|
205
|
+
function openCodeStaticCapabilities() {
|
|
206
|
+
const modes = [
|
|
207
|
+
{ id: "plan", name: "Plan", description: "Inspect the project and propose changes." },
|
|
208
|
+
{ id: "build", name: "Build", description: "Read and edit files, and run commands when approved." },
|
|
209
|
+
{ id: "agent-full-access", name: "Agent (full access)", description: "Run OpenCode with ACA full-access mode mapping." }
|
|
210
|
+
];
|
|
211
|
+
const models = [
|
|
212
|
+
{ modelId: "", name: "Default", description: "Use the model selected by OpenCode configuration." }
|
|
213
|
+
];
|
|
214
|
+
return {
|
|
215
|
+
source: "aca-static",
|
|
216
|
+
fetchedAt: Date.now(),
|
|
217
|
+
contextUsage: true,
|
|
218
|
+
contextCompaction: false,
|
|
219
|
+
modes,
|
|
220
|
+
models,
|
|
221
|
+
configOptions: [
|
|
222
|
+
{
|
|
223
|
+
id: "mode",
|
|
224
|
+
name: "Mode",
|
|
225
|
+
category: "mode",
|
|
226
|
+
type: "select",
|
|
227
|
+
currentValue: "agent-full-access",
|
|
228
|
+
options: modes.map((mode) => ({ value: mode.id, name: mode.name, description: mode.description }))
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
id: "model",
|
|
232
|
+
name: "Model",
|
|
233
|
+
category: "model",
|
|
234
|
+
type: "text",
|
|
235
|
+
currentValue: process.env.ACA_OPENCODE_DEFAULT_MODEL || "",
|
|
236
|
+
options: models.map((model) => ({ value: model.modelId, name: model.name, description: model.description }))
|
|
237
|
+
}
|
|
238
|
+
]
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function openCodeCapabilities() {
|
|
242
|
+
const now = Date.now();
|
|
243
|
+
if (openCodeCapabilitiesCache && openCodeCapabilitiesCache.expiresAtMs > now) {
|
|
244
|
+
return openCodeCapabilitiesCache.capabilities;
|
|
245
|
+
}
|
|
246
|
+
const capabilities = openCodeCapabilitiesFromCli() ?? openCodeStaticCapabilities();
|
|
247
|
+
openCodeCapabilitiesCache = {
|
|
248
|
+
expiresAtMs: now + OPENCODE_MODELS_CACHE_TTL_MS,
|
|
249
|
+
capabilities
|
|
250
|
+
};
|
|
251
|
+
return capabilities;
|
|
252
|
+
}
|
|
253
|
+
function openCodeCapabilitiesFromCli() {
|
|
254
|
+
try {
|
|
255
|
+
const command = process.env.ACA_OPENCODE_COMMAND || "opencode";
|
|
256
|
+
const output = execFileSync(command, ["models", "--verbose"], {
|
|
257
|
+
encoding: "utf8",
|
|
258
|
+
timeout: 15_000,
|
|
259
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
260
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
261
|
+
});
|
|
262
|
+
const models = parseOpenCodeVerboseModels(output);
|
|
263
|
+
if (!models.length)
|
|
264
|
+
return null;
|
|
265
|
+
const modes = [
|
|
266
|
+
{ id: "plan", name: "Plan", description: "Inspect the project and propose changes." },
|
|
267
|
+
{ id: "build", name: "Build", description: "Read and edit files, and run commands when approved." },
|
|
268
|
+
{ id: "agent-full-access", name: "Agent (full access)", description: "Run OpenCode with ACA full-access mode mapping." }
|
|
269
|
+
];
|
|
270
|
+
const defaultModel = openCodeDefaultModel(models);
|
|
271
|
+
return {
|
|
272
|
+
source: "opencode-cli",
|
|
273
|
+
fetchedAt: Date.now(),
|
|
274
|
+
contextUsage: true,
|
|
275
|
+
contextCompaction: false,
|
|
276
|
+
modes,
|
|
277
|
+
models: models.map((model) => ({
|
|
278
|
+
modelId: model.modelId,
|
|
279
|
+
name: model.displayName,
|
|
280
|
+
description: model.description,
|
|
281
|
+
metadata: model.metadata
|
|
282
|
+
})),
|
|
283
|
+
configOptions: [
|
|
284
|
+
{
|
|
285
|
+
id: "mode",
|
|
286
|
+
name: "Mode",
|
|
287
|
+
category: "mode",
|
|
288
|
+
type: "select",
|
|
289
|
+
currentValue: "agent-full-access",
|
|
290
|
+
options: modes.map((mode) => ({ value: mode.id, name: mode.name, description: mode.description }))
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
id: "model",
|
|
294
|
+
name: "Model",
|
|
295
|
+
category: "model",
|
|
296
|
+
type: "select",
|
|
297
|
+
currentValue: defaultModel,
|
|
298
|
+
options: models.map((model) => ({ value: model.modelId, name: model.displayName, description: model.description }))
|
|
299
|
+
}
|
|
300
|
+
]
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function parseOpenCodeVerboseModels(output) {
|
|
308
|
+
const lines = output.split(/\r?\n/);
|
|
309
|
+
const models = [];
|
|
310
|
+
let index = 0;
|
|
311
|
+
while (index < lines.length) {
|
|
312
|
+
const modelId = lines[index]?.trim() ?? "";
|
|
313
|
+
index += 1;
|
|
314
|
+
if (!/^[a-z0-9_.-]+\/[a-z0-9_.-]+$/i.test(modelId))
|
|
315
|
+
continue;
|
|
316
|
+
const jsonLines = [];
|
|
317
|
+
let depth = 0;
|
|
318
|
+
let started = false;
|
|
319
|
+
while (index < lines.length) {
|
|
320
|
+
const line = lines[index] ?? "";
|
|
321
|
+
index += 1;
|
|
322
|
+
if (!started && !line.trim().startsWith("{"))
|
|
323
|
+
continue;
|
|
324
|
+
started = true;
|
|
325
|
+
jsonLines.push(line);
|
|
326
|
+
depth += countChar(line, "{") - countChar(line, "}");
|
|
327
|
+
if (started && depth <= 0)
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
const metadata = parseJsonObject(jsonLines.join("\n"));
|
|
331
|
+
if (!metadata)
|
|
332
|
+
continue;
|
|
333
|
+
const rawName = firstString(metadata.name, metadata.id, modelId) ?? modelId;
|
|
334
|
+
models.push({
|
|
335
|
+
modelId,
|
|
336
|
+
displayName: formatOpenCodeModelName(rawName, metadata),
|
|
337
|
+
description: openCodeModelDescription(metadata),
|
|
338
|
+
metadata
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
return models;
|
|
342
|
+
}
|
|
343
|
+
function openCodeDefaultModel(models) {
|
|
344
|
+
const configured = process.env.ACA_OPENCODE_DEFAULT_MODEL?.trim();
|
|
345
|
+
if (configured && models.some((model) => model.modelId === configured))
|
|
346
|
+
return configured;
|
|
347
|
+
return models.find((model) => model.modelId === "opencode/big-pickle")?.modelId
|
|
348
|
+
|| models.find((model) => /free$/i.test(model.modelId))?.modelId
|
|
349
|
+
|| models[0]?.modelId
|
|
350
|
+
|| "";
|
|
351
|
+
}
|
|
352
|
+
function formatOpenCodeModelName(name, metadata) {
|
|
353
|
+
if (isFreeOpenCodeModel(metadata) && !/\bfree\b/i.test(name))
|
|
354
|
+
return `${name} (free)`;
|
|
355
|
+
return name;
|
|
356
|
+
}
|
|
357
|
+
function isFreeOpenCodeModel(metadata) {
|
|
358
|
+
const cost = metadata.cost;
|
|
359
|
+
if (!cost || typeof cost !== "object" || Array.isArray(cost))
|
|
360
|
+
return false;
|
|
361
|
+
const record = cost;
|
|
362
|
+
const cache = record.cache && typeof record.cache === "object" && !Array.isArray(record.cache) ? record.cache : {};
|
|
363
|
+
return [record.input, record.output, cache.read, cache.write].every((value) => Number(value ?? 0) === 0);
|
|
364
|
+
}
|
|
365
|
+
function openCodeModelDescription(metadata) {
|
|
366
|
+
const limit = metadata.limit && typeof metadata.limit === "object" && !Array.isArray(metadata.limit) ? metadata.limit : {};
|
|
367
|
+
const context = Number(limit.context ?? 0);
|
|
368
|
+
const output = Number(limit.output ?? 0);
|
|
369
|
+
if (context > 0 && output > 0)
|
|
370
|
+
return `Context ${context}, output ${output}`;
|
|
371
|
+
if (context > 0)
|
|
372
|
+
return `Context ${context}`;
|
|
373
|
+
return "OpenCode model";
|
|
374
|
+
}
|
|
375
|
+
function mimoCapabilitiesFromCli() {
|
|
376
|
+
try {
|
|
377
|
+
const command = process.env.ACA_MIMO_COMMAND || "mimo";
|
|
378
|
+
const output = execFileSync(command, ["models", "--verbose"], {
|
|
379
|
+
encoding: "utf8",
|
|
380
|
+
timeout: 15_000,
|
|
381
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
382
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
383
|
+
});
|
|
384
|
+
const models = parseMimoVerboseModels(output);
|
|
385
|
+
if (!models.length)
|
|
386
|
+
return null;
|
|
387
|
+
const modes = [
|
|
388
|
+
{ id: "agent", name: "Agent", description: "Run MimoCode with normal workspace permissions." },
|
|
389
|
+
{ id: "agent-full-access", name: "Agent (full access)", description: "Run MimoCode and auto-approve supported local permissions." }
|
|
390
|
+
];
|
|
391
|
+
const variants = collectMimoVariants(models);
|
|
392
|
+
return {
|
|
393
|
+
source: "mimo-cli",
|
|
394
|
+
fetchedAt: Date.now(),
|
|
395
|
+
contextUsage: true,
|
|
396
|
+
contextCompaction: false,
|
|
397
|
+
modes,
|
|
398
|
+
models: models.map((model) => ({
|
|
399
|
+
modelId: model.modelId,
|
|
400
|
+
name: model.displayName,
|
|
401
|
+
description: model.description,
|
|
402
|
+
metadata: model.metadata
|
|
403
|
+
})),
|
|
404
|
+
configOptions: [
|
|
405
|
+
{
|
|
406
|
+
id: "mode",
|
|
407
|
+
name: "Mode",
|
|
408
|
+
category: "mode",
|
|
409
|
+
type: "select",
|
|
410
|
+
currentValue: "agent-full-access",
|
|
411
|
+
options: modes.map((mode) => ({ value: mode.id, name: mode.name, description: mode.description }))
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
id: "model",
|
|
415
|
+
name: "Model",
|
|
416
|
+
category: "model",
|
|
417
|
+
type: "select",
|
|
418
|
+
currentValue: models.some((model) => model.modelId === "mimo/mimo-auto") ? "mimo/mimo-auto" : models[0]?.modelId,
|
|
419
|
+
options: models.map((model) => ({ value: model.modelId, name: model.displayName, description: model.description }))
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
id: "variant",
|
|
423
|
+
name: "Variant",
|
|
424
|
+
category: "thought_level",
|
|
425
|
+
type: "select",
|
|
426
|
+
currentValue: variants.some((variant) => variant.value === "high") ? "high" : variants[0]?.value,
|
|
427
|
+
options: variants
|
|
428
|
+
}
|
|
429
|
+
]
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
catch {
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
function parseMimoVerboseModels(output) {
|
|
437
|
+
const lines = output.split(/\r?\n/);
|
|
438
|
+
const models = [];
|
|
439
|
+
let index = 0;
|
|
440
|
+
while (index < lines.length) {
|
|
441
|
+
const modelId = lines[index]?.trim() ?? "";
|
|
442
|
+
index += 1;
|
|
443
|
+
if (!/^[a-z0-9_.-]+\/[a-z0-9_.-]+$/i.test(modelId))
|
|
444
|
+
continue;
|
|
445
|
+
const jsonLines = [];
|
|
446
|
+
let depth = 0;
|
|
447
|
+
let started = false;
|
|
448
|
+
while (index < lines.length) {
|
|
449
|
+
const line = lines[index] ?? "";
|
|
450
|
+
index += 1;
|
|
451
|
+
if (!started && !line.trim().startsWith("{"))
|
|
452
|
+
continue;
|
|
453
|
+
started = true;
|
|
454
|
+
jsonLines.push(line);
|
|
455
|
+
depth += countChar(line, "{") - countChar(line, "}");
|
|
456
|
+
if (started && depth <= 0)
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
const metadata = parseJsonObject(jsonLines.join("\n"));
|
|
460
|
+
if (!metadata)
|
|
461
|
+
continue;
|
|
462
|
+
const rawName = firstString(metadata.name, metadata.id, modelId) ?? modelId;
|
|
463
|
+
models.push({
|
|
464
|
+
modelId,
|
|
465
|
+
displayName: formatMimoModelName(rawName, metadata),
|
|
466
|
+
description: mimoModelDescription(metadata),
|
|
467
|
+
metadata
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
return models;
|
|
471
|
+
}
|
|
472
|
+
function collectMimoVariants(models) {
|
|
473
|
+
const values = new Set();
|
|
474
|
+
for (const model of models) {
|
|
475
|
+
const variants = model.metadata.variants;
|
|
476
|
+
if (!variants || typeof variants !== "object" || Array.isArray(variants))
|
|
477
|
+
continue;
|
|
478
|
+
for (const value of Object.keys(variants))
|
|
479
|
+
values.add(value);
|
|
480
|
+
}
|
|
481
|
+
const ordered = ["minimal", "low", "medium", "high", "max"].filter((value) => values.has(value));
|
|
482
|
+
const finalValues = ordered.length ? ordered : ["minimal", "high", "max"];
|
|
483
|
+
return finalValues.map((value) => ({
|
|
484
|
+
value,
|
|
485
|
+
name: titleCase(value),
|
|
486
|
+
description: value === "high" ? "More reasoning for difficult tasks" : value === "max" ? "Maximum reasoning when supported" : "Fastest responses"
|
|
487
|
+
}));
|
|
488
|
+
}
|
|
489
|
+
function formatMimoModelName(name, metadata) {
|
|
490
|
+
if (isFreeMimoModel(metadata) && !/\bfree\b/i.test(name))
|
|
491
|
+
return `${name} (free)`;
|
|
492
|
+
return name;
|
|
493
|
+
}
|
|
494
|
+
function isFreeMimoModel(metadata) {
|
|
495
|
+
const cost = metadata.cost;
|
|
496
|
+
if (!cost || typeof cost !== "object" || Array.isArray(cost))
|
|
497
|
+
return false;
|
|
498
|
+
const record = cost;
|
|
499
|
+
const cache = record.cache && typeof record.cache === "object" && !Array.isArray(record.cache) ? record.cache : {};
|
|
500
|
+
return [record.input, record.output, cache.read, cache.write].every((value) => Number(value ?? 0) === 0);
|
|
501
|
+
}
|
|
502
|
+
function mimoModelDescription(metadata) {
|
|
503
|
+
const limit = metadata.limit && typeof metadata.limit === "object" && !Array.isArray(metadata.limit) ? metadata.limit : {};
|
|
504
|
+
const context = Number(limit.context ?? 0);
|
|
505
|
+
return context > 0 ? `Context ${context}` : "MimoCode model";
|
|
506
|
+
}
|
|
507
|
+
function parseJsonObject(value) {
|
|
508
|
+
try {
|
|
509
|
+
const parsed = JSON.parse(value);
|
|
510
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
511
|
+
}
|
|
512
|
+
catch {
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
function firstString(...values) {
|
|
517
|
+
for (const value of values) {
|
|
518
|
+
if (typeof value === "string" && value.trim())
|
|
519
|
+
return value.trim();
|
|
520
|
+
}
|
|
521
|
+
return undefined;
|
|
522
|
+
}
|
|
523
|
+
function titleCase(value) {
|
|
524
|
+
return value.slice(0, 1).toUpperCase() + value.slice(1);
|
|
525
|
+
}
|
|
526
|
+
function countChar(value, char) {
|
|
527
|
+
let count = 0;
|
|
528
|
+
for (const current of value) {
|
|
529
|
+
if (current === char)
|
|
530
|
+
count += 1;
|
|
531
|
+
}
|
|
532
|
+
return count;
|
|
533
|
+
}
|
|
534
|
+
export function startHeartbeatLoop(logger) {
|
|
535
|
+
const tick = async () => {
|
|
536
|
+
const sentAt = new Date().toISOString();
|
|
537
|
+
try {
|
|
538
|
+
const result = await sendHeartbeat();
|
|
539
|
+
if (result.skipped) {
|
|
540
|
+
updateRuntimeHeartbeat({
|
|
541
|
+
lastHeartbeatAt: sentAt,
|
|
542
|
+
lastHeartbeatStatus: "skipped",
|
|
543
|
+
lastHeartbeatReason: result.reason ?? "unknown"
|
|
544
|
+
});
|
|
545
|
+
logger.debug(`heartbeat skipped: ${result.reason ?? "unknown"}`);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
updateRuntimeHeartbeat({
|
|
549
|
+
lastHeartbeatAt: sentAt,
|
|
550
|
+
lastHeartbeatStatus: "ok",
|
|
551
|
+
lastHeartbeatProjectCount: result.projectCount ?? null,
|
|
552
|
+
presenceExpiresAtMs: result.presenceExpiresAtMs ?? null
|
|
553
|
+
});
|
|
554
|
+
logger.debug(`heartbeat ok projects=${result.projectCount ?? 0}`);
|
|
555
|
+
}
|
|
556
|
+
catch (error) {
|
|
557
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
558
|
+
updateRuntimeHeartbeat({
|
|
559
|
+
lastHeartbeatAt: sentAt,
|
|
560
|
+
lastHeartbeatStatus: "error",
|
|
561
|
+
lastHeartbeatError: message
|
|
562
|
+
});
|
|
563
|
+
logger.warn(`heartbeat failed: ${message}`);
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
void tick();
|
|
567
|
+
return setInterval(() => {
|
|
568
|
+
void tick();
|
|
569
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
570
|
+
}
|
|
571
|
+
function updateRuntimeHeartbeat(patch) {
|
|
572
|
+
const current = readJsonFile(getRuntimeStatePath()) ?? {};
|
|
573
|
+
writeJsonFile(getRuntimeStatePath(), {
|
|
574
|
+
...current,
|
|
575
|
+
heartbeatIntervalMs: HEARTBEAT_INTERVAL_MS,
|
|
576
|
+
...patch
|
|
577
|
+
});
|
|
578
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export function resolveJobPermissionPolicy(input, request) {
|
|
2
|
+
const profile = asRecord(input.permissionProfile);
|
|
3
|
+
const mode = input.canExecuteTools === false
|
|
4
|
+
? "deny"
|
|
5
|
+
: normalizeMode(profile.mode ?? profile.permissionMode ?? profile.toolMode);
|
|
6
|
+
if (mode === "ask")
|
|
7
|
+
return null;
|
|
8
|
+
const explicitOptionId = mode === "allow"
|
|
9
|
+
? firstString(profile.allowOptionId, profile.allow_option_id)
|
|
10
|
+
: firstString(profile.denyOptionId, profile.deny_option_id);
|
|
11
|
+
const option = explicitOptionId
|
|
12
|
+
? request.options.find((item) => item.optionId === explicitOptionId)
|
|
13
|
+
: request.options.find((item) => optionMatchesMode(item, mode));
|
|
14
|
+
if (option)
|
|
15
|
+
return { outcome: { outcome: "selected", optionId: option.optionId } };
|
|
16
|
+
// Provider 没有提供可明确识别的选项时保持保守,避免误把拒绝项当成允许项。
|
|
17
|
+
return { outcome: { outcome: "cancelled" } };
|
|
18
|
+
}
|
|
19
|
+
function normalizeMode(value) {
|
|
20
|
+
const mode = String(value || "ask").trim().toLowerCase();
|
|
21
|
+
if (["allow", "approve", "auto_allow", "auto-allow"].includes(mode))
|
|
22
|
+
return "allow";
|
|
23
|
+
if (["deny", "reject", "disabled", "forbid"].includes(mode))
|
|
24
|
+
return "deny";
|
|
25
|
+
return "ask";
|
|
26
|
+
}
|
|
27
|
+
function optionMatchesMode(option, mode) {
|
|
28
|
+
const text = `${option.optionId} ${option.kind} ${option.label || ""}`.toLowerCase();
|
|
29
|
+
return mode === "allow"
|
|
30
|
+
? /(^|[^a-z])(allow|accept|approve|yes)([^a-z]|$)/.test(text)
|
|
31
|
+
: /(^|[^a-z])(deny|reject|decline|cancel|no)([^a-z]|$)/.test(text);
|
|
32
|
+
}
|
|
33
|
+
function asRecord(value) {
|
|
34
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
35
|
+
}
|
|
36
|
+
function firstString(...values) {
|
|
37
|
+
for (const value of values) {
|
|
38
|
+
if (typeof value === "string" && value.trim())
|
|
39
|
+
return value.trim();
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|