@alisio/core 0.1.0-alpha.10
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/LICENSE +21 -0
- package/README.md +25 -0
- package/dist/application.d.ts +225 -0
- package/dist/application.js +950 -0
- package/dist/config.d.ts +481 -0
- package/dist/config.js +592 -0
- package/dist/core/compaction.d.ts +136 -0
- package/dist/core/compaction.js +404 -0
- package/dist/core/contracts.d.ts +147 -0
- package/dist/core/contracts.js +1 -0
- package/dist/core/registry.d.ts +8 -0
- package/dist/core/registry.js +54 -0
- package/dist/core/runner.d.ts +160 -0
- package/dist/core/runner.js +625 -0
- package/dist/extensions/registry.d.ts +28 -0
- package/dist/extensions/registry.js +58 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +28 -0
- package/dist/integrations/herdr.d.ts +34 -0
- package/dist/integrations/herdr.js +174 -0
- package/dist/mcp/connector.d.ts +65 -0
- package/dist/mcp/connector.js +423 -0
- package/dist/plugins/host.d.ts +169 -0
- package/dist/plugins/host.js +585 -0
- package/dist/plugins/install.d.ts +100 -0
- package/dist/plugins/install.js +322 -0
- package/dist/providers/openai-compatible.d.ts +14 -0
- package/dist/providers/openai-compatible.js +228 -0
- package/dist/providers/registry.d.ts +35 -0
- package/dist/providers/registry.js +74 -0
- package/dist/providers/routing.d.ts +11 -0
- package/dist/providers/routing.js +41 -0
- package/dist/providers/settings.d.ts +30 -0
- package/dist/providers/settings.js +80 -0
- package/dist/resources/context.d.ts +44 -0
- package/dist/resources/context.js +171 -0
- package/dist/resources/prompts.d.ts +77 -0
- package/dist/resources/prompts.js +182 -0
- package/dist/resources/skills.d.ts +82 -0
- package/dist/resources/skills.js +214 -0
- package/dist/runtime/fs.d.ts +9 -0
- package/dist/runtime/fs.js +73 -0
- package/dist/runtime/modules.d.ts +16 -0
- package/dist/runtime/modules.js +83 -0
- package/dist/runtime/paths.d.ts +4 -0
- package/dist/runtime/paths.js +42 -0
- package/dist/runtime/process.d.ts +25 -0
- package/dist/runtime/process.js +122 -0
- package/dist/runtime/sqlite.d.ts +3 -0
- package/dist/runtime/sqlite.js +85 -0
- package/dist/runtime/store.d.ts +38 -0
- package/dist/runtime/store.js +259 -0
- package/dist/sessions/children.d.ts +55 -0
- package/dist/sessions/children.js +233 -0
- package/dist/startup/index.d.ts +41 -0
- package/dist/startup/index.js +105 -0
- package/dist/startup/mascot.d.ts +6 -0
- package/dist/startup/mascot.js +30 -0
- package/dist/startup/screen.d.ts +18 -0
- package/dist/startup/screen.js +117 -0
- package/dist/startup/text.d.ts +19 -0
- package/dist/startup/text.js +68 -0
- package/dist/tools/execute.d.ts +16 -0
- package/dist/tools/execute.js +96 -0
- package/dist/tools/search.d.ts +69 -0
- package/dist/tools/search.js +232 -0
- package/dist/tools/standard.d.ts +22 -0
- package/dist/tools/standard.js +419 -0
- package/dist/tools/webfetch.d.ts +26 -0
- package/dist/tools/webfetch.js +119 -0
- package/dist/trust.d.ts +34 -0
- package/dist/trust.js +113 -0
- package/dist/version.d.ts +7 -0
- package/dist/version.js +26 -0
- package/package.json +62 -0
package/dist/config.js
ADDED
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
import { chmod, mkdir, open, readFile, rename, unlink } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { exists, readJson } from "./runtime/fs.js";
|
|
6
|
+
import { isPathSpec } from "./runtime/modules.js";
|
|
7
|
+
const providerSchema = z
|
|
8
|
+
.object({
|
|
9
|
+
baseURL: z.url().default("https://api.openai.com/v1"),
|
|
10
|
+
apiKeyEnv: z.string().default("OPENAI_API_KEY"),
|
|
11
|
+
model: z.string().default(""),
|
|
12
|
+
apiMode: z.enum(["chat", "responses"]).default("chat"),
|
|
13
|
+
auth: z.enum(["bearer", "none"]).default("bearer"),
|
|
14
|
+
tokenParameter: z.enum(["max_tokens", "max_completion_tokens", "omit"]).default("max_tokens"),
|
|
15
|
+
streamUsage: z.boolean().default(false),
|
|
16
|
+
/** Context window in tokens; overrides the value reported by GET /models. */
|
|
17
|
+
contextWindow: z.number().int().positive().optional(),
|
|
18
|
+
})
|
|
19
|
+
.strict();
|
|
20
|
+
const safeUrl = z
|
|
21
|
+
.string()
|
|
22
|
+
.url()
|
|
23
|
+
.refine((value) => {
|
|
24
|
+
const url = new URL(value);
|
|
25
|
+
return ["http:", "https:"].includes(url.protocol) && !url.username && !url.password;
|
|
26
|
+
}, { message: "must be an HTTP(S) URL without credentials" });
|
|
27
|
+
const environment = z.record(z.string(), z.string());
|
|
28
|
+
const stdioServerSchema = z
|
|
29
|
+
.object({
|
|
30
|
+
transport: z.literal("stdio"),
|
|
31
|
+
enabled: z.boolean().default(true),
|
|
32
|
+
command: z.string().trim().min(1),
|
|
33
|
+
args: z.array(z.string()).default([]),
|
|
34
|
+
envAllow: z.array(z.string()).default([]),
|
|
35
|
+
env: environment.default({}),
|
|
36
|
+
})
|
|
37
|
+
.strict();
|
|
38
|
+
const httpServerSchema = z
|
|
39
|
+
.object({
|
|
40
|
+
transport: z.literal("http"),
|
|
41
|
+
enabled: z.boolean().default(true),
|
|
42
|
+
url: safeUrl,
|
|
43
|
+
bearerTokenEnv: z.string().trim().min(1).optional(),
|
|
44
|
+
})
|
|
45
|
+
.strict();
|
|
46
|
+
const serverSchema = z.discriminatedUnion("transport", [stdioServerSchema, httpServerSchema]);
|
|
47
|
+
const compatibleServerSchema = z.union([
|
|
48
|
+
serverSchema,
|
|
49
|
+
z
|
|
50
|
+
.object({
|
|
51
|
+
command: z.string().trim().min(1),
|
|
52
|
+
enabled: z.boolean().default(true),
|
|
53
|
+
args: z.array(z.string()).default([]),
|
|
54
|
+
envAllow: z.array(z.string()).default([]),
|
|
55
|
+
env: environment.default({}),
|
|
56
|
+
})
|
|
57
|
+
.strict()
|
|
58
|
+
.transform((server) => ({ ...server, transport: "stdio" })),
|
|
59
|
+
z
|
|
60
|
+
.object({
|
|
61
|
+
url: safeUrl,
|
|
62
|
+
enabled: z.boolean().default(true),
|
|
63
|
+
bearerTokenEnv: z.string().trim().min(1).optional(),
|
|
64
|
+
})
|
|
65
|
+
.strict()
|
|
66
|
+
.transform((server) => ({ ...server, transport: "http" })),
|
|
67
|
+
]);
|
|
68
|
+
const serverName = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
69
|
+
const serversSchema = (value) => z.record(z.string(), value).superRefine((servers, context) => {
|
|
70
|
+
for (const name of Object.keys(servers))
|
|
71
|
+
if (!serverName.test(name))
|
|
72
|
+
context.addIssue({
|
|
73
|
+
code: "custom",
|
|
74
|
+
path: [name],
|
|
75
|
+
message: "MCP server names may contain only letters, numbers, dot, underscore and dash",
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
const configObjectSchema = z
|
|
79
|
+
.object({
|
|
80
|
+
schemaVersion: z.literal(1).default(1),
|
|
81
|
+
provider: providerSchema.default(() => providerSchema.parse({})),
|
|
82
|
+
plugins: z.array(z.string()).default([]),
|
|
83
|
+
skills: z.array(z.string()).default([]),
|
|
84
|
+
/** Project-local enable/disable overrides for effective non-plugin skills. */
|
|
85
|
+
skillOverrides: z.record(z.string(), z.object({ enabled: z.boolean() }).strict()).default({}),
|
|
86
|
+
mcp: z
|
|
87
|
+
.object({
|
|
88
|
+
servers: serversSchema(compatibleServerSchema).default({}),
|
|
89
|
+
/**
|
|
90
|
+
* Global-only user preference: grants MCP process/network consent across sessions.
|
|
91
|
+
* Only the user (global) configuration layer is consulted; a project value is ignored.
|
|
92
|
+
*/
|
|
93
|
+
allow: z.boolean().optional(),
|
|
94
|
+
})
|
|
95
|
+
.strict()
|
|
96
|
+
.default({ servers: {} }),
|
|
97
|
+
/** Compatibility with the common MCP client configuration shape. */
|
|
98
|
+
mcpServers: serversSchema(compatibleServerSchema).optional(),
|
|
99
|
+
/** Built-in plugin options keyed by id; each plugin validates its own section. */
|
|
100
|
+
builtinPlugins: z
|
|
101
|
+
.record(z.string(), z.object({ enabled: z.boolean().optional() }).passthrough())
|
|
102
|
+
.default({}),
|
|
103
|
+
/** Project-local enable/disable overrides for configured external plugins, keyed by plugin id. */
|
|
104
|
+
pluginOverrides: z.record(z.string(), z.object({ enabled: z.boolean() }).strict()).default({}),
|
|
105
|
+
/** Host-enforced timeouts for plugin hooks. */
|
|
106
|
+
pluginHooks: z
|
|
107
|
+
.object({
|
|
108
|
+
timeoutMs: z.number().int().min(100).max(120_000).default(15_000),
|
|
109
|
+
sessionEndTimeoutMs: z.number().int().min(100).max(120_000).default(10_000),
|
|
110
|
+
})
|
|
111
|
+
.strict()
|
|
112
|
+
.default(() => ({ timeoutMs: 15_000, sessionEndTimeoutMs: 10_000 })),
|
|
113
|
+
context: z
|
|
114
|
+
.object({
|
|
115
|
+
/** Use CLAUDE.md where a directory has no AGENTS.md (off by default). */
|
|
116
|
+
claudeMdFallback: z.boolean().default(false),
|
|
117
|
+
/** Total bytes of AGENTS.md content injected (closest files kept). */
|
|
118
|
+
maxBytes: z
|
|
119
|
+
.number()
|
|
120
|
+
.int()
|
|
121
|
+
.min(1024)
|
|
122
|
+
.max(1_048_576)
|
|
123
|
+
.default(32 * 1024),
|
|
124
|
+
})
|
|
125
|
+
.strict()
|
|
126
|
+
.default(() => ({ claudeMdFallback: false, maxBytes: 32 * 1024 })),
|
|
127
|
+
compaction: z
|
|
128
|
+
.object({
|
|
129
|
+
auto: z.boolean().default(true),
|
|
130
|
+
threshold: z.number().min(0.1).max(0.99).default(0.85),
|
|
131
|
+
keepTurns: z.number().int().min(0).max(20).default(2),
|
|
132
|
+
/**
|
|
133
|
+
* Output token budget for the summarizer call. Larger than the agent-loop budget on
|
|
134
|
+
* purpose: a truncated summary is kept as partial, never re-run.
|
|
135
|
+
*/
|
|
136
|
+
maxOutputTokens: z.number().int().positive().default(16_000),
|
|
137
|
+
})
|
|
138
|
+
.strict()
|
|
139
|
+
.default(() => ({ auto: true, threshold: 0.85, keepTurns: 2, maxOutputTokens: 16_000 })),
|
|
140
|
+
websearch: z
|
|
141
|
+
.object({
|
|
142
|
+
provider: z
|
|
143
|
+
.enum(["searxng", "duckduckgo-instant", "tavily", "brave", "serpapi", "native"])
|
|
144
|
+
.optional(),
|
|
145
|
+
searxngUrl: z.string().optional(),
|
|
146
|
+
apiKeyEnv: z.string().optional(),
|
|
147
|
+
/** Only for provider "native": the provider-native tool type sent to the model. */
|
|
148
|
+
nativeToolType: z.string().default("web_search"),
|
|
149
|
+
})
|
|
150
|
+
.strict()
|
|
151
|
+
.default(() => ({ nativeToolType: "web_search" })),
|
|
152
|
+
limits: z
|
|
153
|
+
.object({
|
|
154
|
+
maxTurns: z.number().int().min(1).max(100).default(20),
|
|
155
|
+
timeoutMs: z.number().int().min(100).default(300000),
|
|
156
|
+
maxContextChars: z.number().int().positive().default(800000),
|
|
157
|
+
/**
|
|
158
|
+
* Per-call output token budget for one agent turn. 4096 starves reasoning-heavy models,
|
|
159
|
+
* which can spend the whole budget on `reasoning_content` before any usable text arrives.
|
|
160
|
+
*/
|
|
161
|
+
maxOutputTokens: z.number().int().positive().default(16384),
|
|
162
|
+
/** Cumulative tokens per run; default is proportional to the context window. */
|
|
163
|
+
maxTokens: z.number().int().positive().optional(),
|
|
164
|
+
})
|
|
165
|
+
.default(() => ({
|
|
166
|
+
maxTurns: 20,
|
|
167
|
+
timeoutMs: 300000,
|
|
168
|
+
maxContextChars: 800000,
|
|
169
|
+
maxOutputTokens: 16384,
|
|
170
|
+
})),
|
|
171
|
+
tui: z
|
|
172
|
+
.object({
|
|
173
|
+
/** Horizontal padding (columns) around the editor input box. */
|
|
174
|
+
paddingX: z.number().int().min(0).max(4).default(1),
|
|
175
|
+
/**
|
|
176
|
+
* Offer effective skills as first-class `skill:<id>` editor slash-autocomplete entries.
|
|
177
|
+
* Off hides those entries; the `/skills` manager and its argument completion stay available.
|
|
178
|
+
*/
|
|
179
|
+
skillSlashCommands: z.boolean().default(true),
|
|
180
|
+
})
|
|
181
|
+
.strict()
|
|
182
|
+
.default(() => ({ paddingX: 1, skillSlashCommands: true })),
|
|
183
|
+
})
|
|
184
|
+
.strict()
|
|
185
|
+
.superRefine((config, context) => {
|
|
186
|
+
if (!config.mcpServers)
|
|
187
|
+
return;
|
|
188
|
+
for (const name of Object.keys(config.mcpServers))
|
|
189
|
+
if (name in config.mcp.servers)
|
|
190
|
+
context.addIssue({
|
|
191
|
+
code: "custom",
|
|
192
|
+
path: ["mcpServers", name],
|
|
193
|
+
message: `MCP server "${name}" is defined in both mcp.servers and mcpServers`,
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
export const configSchema = configObjectSchema.transform(({ mcpServers, ...config }) => ({
|
|
197
|
+
...config,
|
|
198
|
+
mcp: { ...config.mcp, servers: { ...config.mcp.servers, ...mcpServers } },
|
|
199
|
+
}));
|
|
200
|
+
/** The `alisio setup` scaffold writes this placeholder; a model matching it is not usable. */
|
|
201
|
+
const LEGACY_PROVIDER_MODEL_PLACEHOLDER = "YOUR_MODEL_ID";
|
|
202
|
+
/**
|
|
203
|
+
* A legacy root `provider` is usable only when its parsed model is non-empty and not the
|
|
204
|
+
* `alisio setup` placeholder, so a scaffolded or empty project config never defeats a saved
|
|
205
|
+
* `/connect` profile.
|
|
206
|
+
*/
|
|
207
|
+
function isUsableLegacyModel(model) {
|
|
208
|
+
const normalized = model.trim();
|
|
209
|
+
return normalized !== "" && normalized !== LEGACY_PROVIDER_MODEL_PLACEHOLDER;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Whether legacy provider selection should take priority over a saved plugin profile for this
|
|
213
|
+
* run: an explicit endpoint override, or a trusted project/explicit layer whose parsed root
|
|
214
|
+
* `provider.model` is actually usable (non-empty and not the `alisio setup` placeholder).
|
|
215
|
+
*/
|
|
216
|
+
export function overridesSavedProviderProfile(provenance, hasEndpointOverride) {
|
|
217
|
+
return hasEndpointOverride || !!provenance.selectedLayer?.hasLegacyProvider;
|
|
218
|
+
}
|
|
219
|
+
export const configHome = () => process.env.ALISIO_CONFIG_HOME ??
|
|
220
|
+
join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "alisio");
|
|
221
|
+
export const stateHome = () => process.env.ALISIO_STATE_HOME ??
|
|
222
|
+
join(process.env.XDG_STATE_HOME ?? join(homedir(), ".local", "state"), "alisio");
|
|
223
|
+
/** Highest-priority configuration file selected for a workspace (it may not exist). */
|
|
224
|
+
export function configFile(workspace, options = {}) {
|
|
225
|
+
// Only explicit/trusted project configuration may redirect credentials to another endpoint.
|
|
226
|
+
return options.file
|
|
227
|
+
? resolve(options.file)
|
|
228
|
+
: options.trustProject
|
|
229
|
+
? join(workspace, ".alisio", "config.json")
|
|
230
|
+
: join(configHome(), "config.json");
|
|
231
|
+
}
|
|
232
|
+
export async function loadConfigWithProvenance(workspace, options = {}) {
|
|
233
|
+
const globalFile = join(configHome(), "config.json");
|
|
234
|
+
const selectedFile = options.file
|
|
235
|
+
? resolve(options.file)
|
|
236
|
+
: options.trustProject
|
|
237
|
+
? join(workspace, ".alisio", "config.json")
|
|
238
|
+
: undefined;
|
|
239
|
+
const parseLayer = async (file, kind) => {
|
|
240
|
+
const raw = (await readJson(file)) ?? {};
|
|
241
|
+
const config = configSchema.parse(raw);
|
|
242
|
+
config.skills = config.skills.map((p) => resolve(file, "..", p));
|
|
243
|
+
// Plugin entries are paths (relative to the config file) or npm package names.
|
|
244
|
+
config.plugins = config.plugins.map((p) => (isPathSpec(p) ? resolve(file, "..", p) : p));
|
|
245
|
+
for (const server of Object.values(config.mcp.servers))
|
|
246
|
+
if (server.transport === "stdio") {
|
|
247
|
+
if (server.command.startsWith("./") || server.command.startsWith("../"))
|
|
248
|
+
server.command = resolve(file, "..", server.command);
|
|
249
|
+
server.args = server.args.map((argument) => argument.startsWith("./") || argument.startsWith("../")
|
|
250
|
+
? resolve(file, "..", argument)
|
|
251
|
+
: argument);
|
|
252
|
+
}
|
|
253
|
+
const object = raw;
|
|
254
|
+
const canonical = object.mcp && typeof object.mcp === "object" && !Array.isArray(object.mcp)
|
|
255
|
+
? object.mcp.servers
|
|
256
|
+
: undefined;
|
|
257
|
+
const alias = object.mcpServers;
|
|
258
|
+
const sources = Object.fromEntries(Object.keys(config.mcp.servers).map((name) => [
|
|
259
|
+
name,
|
|
260
|
+
{
|
|
261
|
+
kind,
|
|
262
|
+
file,
|
|
263
|
+
form: canonical && name in canonical ? "canonical" : "alias",
|
|
264
|
+
},
|
|
265
|
+
]));
|
|
266
|
+
return { config, keys: new Set(Object.keys(object)), sources };
|
|
267
|
+
};
|
|
268
|
+
const global = await parseLayer(globalFile, "global");
|
|
269
|
+
// Skill activation is deliberately project-local; never carry a similarly named global field
|
|
270
|
+
// into another workspace.
|
|
271
|
+
global.config.skillOverrides = {};
|
|
272
|
+
let config = global.config;
|
|
273
|
+
let mcpSources = global.sources;
|
|
274
|
+
// Parsed `provider.model` of the selected layer itself (before env/CLI overrides), used to
|
|
275
|
+
// decide whether its legacy root `provider` is usable against a saved `/connect` profile.
|
|
276
|
+
let selectedLegacyModel = "";
|
|
277
|
+
if (selectedFile && selectedFile !== globalFile) {
|
|
278
|
+
const selected = await parseLayer(selectedFile, options.file ? "explicit" : "project");
|
|
279
|
+
selectedLegacyModel = selected.config.provider.model;
|
|
280
|
+
const overlaid = { ...config };
|
|
281
|
+
for (const key of selected.keys) {
|
|
282
|
+
if (key === "mcp" || key === "mcpServers")
|
|
283
|
+
continue;
|
|
284
|
+
if (key in selected.config)
|
|
285
|
+
overlaid[key] = selected.config[key];
|
|
286
|
+
}
|
|
287
|
+
if (selected.keys.has("mcp") || selected.keys.has("mcpServers"))
|
|
288
|
+
// Only servers merge upward; `mcp.allow` is a global/user preference and is deliberately
|
|
289
|
+
// dropped from the selected layer so a project can never grant itself network consent.
|
|
290
|
+
overlaid.mcp = {
|
|
291
|
+
...config.mcp,
|
|
292
|
+
servers: { ...config.mcp.servers, ...selected.config.mcp.servers },
|
|
293
|
+
};
|
|
294
|
+
mcpSources = { ...mcpSources, ...selected.sources };
|
|
295
|
+
config = overlaid;
|
|
296
|
+
}
|
|
297
|
+
else if (selectedFile) {
|
|
298
|
+
selectedLegacyModel = global.config.provider.model;
|
|
299
|
+
}
|
|
300
|
+
config.provider = providerSchema.parse({
|
|
301
|
+
...config.provider,
|
|
302
|
+
...(process.env.OPENAI_BASE_URL ? { baseURL: process.env.OPENAI_BASE_URL } : {}),
|
|
303
|
+
...(process.env.ALISIO_MODEL ? { model: process.env.ALISIO_MODEL } : {}),
|
|
304
|
+
...(process.env.ALISIO_API_MODE ? { apiMode: process.env.ALISIO_API_MODE } : {}),
|
|
305
|
+
...(options.baseURL ? { baseURL: options.baseURL } : {}),
|
|
306
|
+
...(options.model ? { model: options.model } : {}),
|
|
307
|
+
...(options.apiMode ? { apiMode: options.apiMode } : {}),
|
|
308
|
+
});
|
|
309
|
+
const url = new URL(config.provider.baseURL);
|
|
310
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password)
|
|
311
|
+
throw new Error("baseURL must be an HTTP(S) URL without credentials");
|
|
312
|
+
return {
|
|
313
|
+
config: { ...config, mcpSources },
|
|
314
|
+
provenance: {
|
|
315
|
+
...(selectedFile
|
|
316
|
+
? {
|
|
317
|
+
selectedLayer: {
|
|
318
|
+
kind: options.file ? "explicit" : "project",
|
|
319
|
+
hasLegacyProvider: isUsableLegacyModel(selectedLegacyModel),
|
|
320
|
+
},
|
|
321
|
+
}
|
|
322
|
+
: {}),
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
export async function loadConfig(workspace, options = {}) {
|
|
327
|
+
return (await loadConfigWithProvenance(workspace, options)).config;
|
|
328
|
+
}
|
|
329
|
+
/** Atomically toggles one MCP server in the form and file that defined it. */
|
|
330
|
+
export async function setMcpServerEnabled(input) {
|
|
331
|
+
if (!input.source.file)
|
|
332
|
+
throw new Error("This MCP server is managed by its registering plugin");
|
|
333
|
+
const file = input.source.file;
|
|
334
|
+
const parsed = JSON.parse(await readFile(file, "utf8"));
|
|
335
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
336
|
+
throw new Error("Alisio configuration must be a JSON object");
|
|
337
|
+
const raw = parsed;
|
|
338
|
+
const container = input.source.form === "alias"
|
|
339
|
+
? raw.mcpServers
|
|
340
|
+
: raw.mcp && typeof raw.mcp === "object" && !Array.isArray(raw.mcp)
|
|
341
|
+
? raw.mcp.servers
|
|
342
|
+
: undefined;
|
|
343
|
+
if (!container || typeof container !== "object" || Array.isArray(container))
|
|
344
|
+
throw new Error(`MCP server "${input.name}" is no longer present in its defining configuration`);
|
|
345
|
+
const current = container[input.name];
|
|
346
|
+
if (!current || typeof current !== "object" || Array.isArray(current))
|
|
347
|
+
throw new Error(`MCP server "${input.name}" is no longer present in its defining configuration`);
|
|
348
|
+
container[input.name] = {
|
|
349
|
+
...current,
|
|
350
|
+
enabled: input.enabled,
|
|
351
|
+
};
|
|
352
|
+
await mkdir(dirname(file), { recursive: true, mode: 0o700 });
|
|
353
|
+
const temporary = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
354
|
+
const handle = await open(temporary, "wx", 0o600);
|
|
355
|
+
try {
|
|
356
|
+
await handle.writeFile(`${JSON.stringify(raw, null, 2)}\n`, "utf8");
|
|
357
|
+
await handle.sync();
|
|
358
|
+
}
|
|
359
|
+
finally {
|
|
360
|
+
await handle.close();
|
|
361
|
+
}
|
|
362
|
+
try {
|
|
363
|
+
await rename(temporary, file);
|
|
364
|
+
await chmod(file, 0o600);
|
|
365
|
+
}
|
|
366
|
+
catch (error) {
|
|
367
|
+
await unlink(temporary).catch(() => { });
|
|
368
|
+
throw error;
|
|
369
|
+
}
|
|
370
|
+
return file;
|
|
371
|
+
}
|
|
372
|
+
/** Atomically sets or clears the global `mcp.allow` consent preference in the user config file. */
|
|
373
|
+
export async function setGlobalMcpAllow(input) {
|
|
374
|
+
const file = join(configHome(), "config.json");
|
|
375
|
+
let raw = {};
|
|
376
|
+
if (await exists(file)) {
|
|
377
|
+
const parsed = JSON.parse(await readFile(file, "utf8"));
|
|
378
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
379
|
+
throw new Error("Global Alisio configuration must be a JSON object");
|
|
380
|
+
raw = parsed;
|
|
381
|
+
}
|
|
382
|
+
if (input.allow) {
|
|
383
|
+
const current = raw.mcp && typeof raw.mcp === "object" && !Array.isArray(raw.mcp)
|
|
384
|
+
? raw.mcp
|
|
385
|
+
: {};
|
|
386
|
+
raw.mcp = { ...current, allow: true };
|
|
387
|
+
}
|
|
388
|
+
else if (raw.mcp && typeof raw.mcp === "object" && !Array.isArray(raw.mcp)) {
|
|
389
|
+
const mcp = { ...raw.mcp };
|
|
390
|
+
delete mcp.allow;
|
|
391
|
+
if (Object.keys(mcp).length)
|
|
392
|
+
raw.mcp = mcp;
|
|
393
|
+
else
|
|
394
|
+
delete raw.mcp;
|
|
395
|
+
}
|
|
396
|
+
raw.schemaVersion ??= 1;
|
|
397
|
+
await mkdir(dirname(file), { recursive: true, mode: 0o700 });
|
|
398
|
+
const temporary = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
399
|
+
const handle = await open(temporary, "wx", 0o600);
|
|
400
|
+
try {
|
|
401
|
+
await handle.writeFile(`${JSON.stringify(raw, null, 2)}\n`, "utf8");
|
|
402
|
+
await handle.sync();
|
|
403
|
+
}
|
|
404
|
+
finally {
|
|
405
|
+
await handle.close();
|
|
406
|
+
}
|
|
407
|
+
try {
|
|
408
|
+
await rename(temporary, file);
|
|
409
|
+
await chmod(file, 0o600);
|
|
410
|
+
}
|
|
411
|
+
catch (error) {
|
|
412
|
+
await unlink(temporary).catch(() => { });
|
|
413
|
+
throw error;
|
|
414
|
+
}
|
|
415
|
+
return file;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* The user-facing settings writable through the atomic `setConfigValue` writer, with their zod
|
|
419
|
+
* leaf validators derived from the config schema. Only these keys are accepted; every other
|
|
420
|
+
* config path stays out of reach of the settings menu (unknown keys are rejected, never created).
|
|
421
|
+
*/
|
|
422
|
+
const SETTABLE_SECTIONS = {
|
|
423
|
+
compaction: configObjectSchema.shape.compaction.removeDefault(),
|
|
424
|
+
context: configObjectSchema.shape.context.removeDefault(),
|
|
425
|
+
limits: configObjectSchema.shape.limits.removeDefault(),
|
|
426
|
+
pluginHooks: configObjectSchema.shape.pluginHooks.removeDefault(),
|
|
427
|
+
tui: configObjectSchema.shape.tui.removeDefault(),
|
|
428
|
+
websearch: configObjectSchema.shape.websearch.removeDefault(),
|
|
429
|
+
};
|
|
430
|
+
const SETTABLE_KEYS = {
|
|
431
|
+
"compaction.auto": SETTABLE_SECTIONS.compaction.shape.auto,
|
|
432
|
+
"compaction.threshold": SETTABLE_SECTIONS.compaction.shape.threshold,
|
|
433
|
+
"compaction.keepTurns": SETTABLE_SECTIONS.compaction.shape.keepTurns,
|
|
434
|
+
"compaction.maxOutputTokens": SETTABLE_SECTIONS.compaction.shape.maxOutputTokens,
|
|
435
|
+
"context.claudeMdFallback": SETTABLE_SECTIONS.context.shape.claudeMdFallback,
|
|
436
|
+
"context.maxBytes": SETTABLE_SECTIONS.context.shape.maxBytes,
|
|
437
|
+
"limits.maxTurns": SETTABLE_SECTIONS.limits.shape.maxTurns,
|
|
438
|
+
"limits.maxOutputTokens": SETTABLE_SECTIONS.limits.shape.maxOutputTokens,
|
|
439
|
+
"limits.maxContextChars": SETTABLE_SECTIONS.limits.shape.maxContextChars,
|
|
440
|
+
"limits.timeoutMs": SETTABLE_SECTIONS.limits.shape.timeoutMs,
|
|
441
|
+
"pluginHooks.timeoutMs": SETTABLE_SECTIONS.pluginHooks.shape.timeoutMs,
|
|
442
|
+
"tui.paddingX": SETTABLE_SECTIONS.tui.shape.paddingX,
|
|
443
|
+
"tui.skillSlashCommands": SETTABLE_SECTIONS.tui.shape.skillSlashCommands,
|
|
444
|
+
"websearch.provider": SETTABLE_SECTIONS.websearch.shape.provider,
|
|
445
|
+
};
|
|
446
|
+
export function isSettableSettingKey(key) {
|
|
447
|
+
return Object.prototype.hasOwnProperty.call(SETTABLE_KEYS, key);
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Atomically sets one user-facing setting in the global config file (`<config home>/config.json`)
|
|
451
|
+
* while preserving every unrelated JSON field. The value is validated against the same zod leaf
|
|
452
|
+
* used by `configSchema`, so a write can never produce a config the loader would reject. The
|
|
453
|
+
* caller is responsible for applying the change to the running process (see `application.ts`).
|
|
454
|
+
*/
|
|
455
|
+
export async function setConfigValue(input) {
|
|
456
|
+
const validator = SETTABLE_KEYS[input.key];
|
|
457
|
+
if (!validator)
|
|
458
|
+
throw new Error(`Unknown setting key: ${input.key}`);
|
|
459
|
+
const parsed = validator.safeParse(input.value);
|
|
460
|
+
if (!parsed.success) {
|
|
461
|
+
const detail = parsed.error.issues[0];
|
|
462
|
+
throw new Error(detail
|
|
463
|
+
? `Invalid value for ${input.key}: ${detail.path.join(".") || "value"} ${detail.message}`
|
|
464
|
+
: `Invalid value for ${input.key}`);
|
|
465
|
+
}
|
|
466
|
+
const file = join(configHome(), "config.json");
|
|
467
|
+
let raw = {};
|
|
468
|
+
if (await exists(file)) {
|
|
469
|
+
const existing = JSON.parse(await readFile(file, "utf8"));
|
|
470
|
+
if (!existing || typeof existing !== "object" || Array.isArray(existing))
|
|
471
|
+
throw new Error("Global Alisio configuration must be a JSON object");
|
|
472
|
+
raw = existing;
|
|
473
|
+
}
|
|
474
|
+
const [section, leaf] = input.key.split(".");
|
|
475
|
+
const container = raw[section] && typeof raw[section] === "object" && !Array.isArray(raw[section])
|
|
476
|
+
? raw[section]
|
|
477
|
+
: {};
|
|
478
|
+
raw[section] = { ...container, [leaf]: parsed.data };
|
|
479
|
+
raw.schemaVersion ??= 1;
|
|
480
|
+
await mkdir(dirname(file), { recursive: true, mode: 0o700 });
|
|
481
|
+
const temporary = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
482
|
+
const handle = await open(temporary, "wx", 0o600);
|
|
483
|
+
try {
|
|
484
|
+
await handle.writeFile(`${JSON.stringify(raw, null, 2)}\n`, "utf8");
|
|
485
|
+
await handle.sync();
|
|
486
|
+
}
|
|
487
|
+
finally {
|
|
488
|
+
await handle.close();
|
|
489
|
+
}
|
|
490
|
+
try {
|
|
491
|
+
await rename(temporary, file);
|
|
492
|
+
await chmod(file, 0o600);
|
|
493
|
+
}
|
|
494
|
+
catch (error) {
|
|
495
|
+
await unlink(temporary).catch(() => { });
|
|
496
|
+
throw error;
|
|
497
|
+
}
|
|
498
|
+
return file;
|
|
499
|
+
}
|
|
500
|
+
/** Atomically updates one project plugin override while preserving every unrelated JSON field. */
|
|
501
|
+
export async function setProjectPluginEnabled(input) {
|
|
502
|
+
const file = join(input.workspace, ".alisio", "config.json");
|
|
503
|
+
let raw = {};
|
|
504
|
+
if (await exists(file)) {
|
|
505
|
+
if (!input.trusted)
|
|
506
|
+
throw new Error("Trust this project before changing its existing Alisio configuration");
|
|
507
|
+
const parsed = JSON.parse(await readFile(file, "utf8"));
|
|
508
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
509
|
+
throw new Error("Project Alisio configuration must be a JSON object");
|
|
510
|
+
raw = parsed;
|
|
511
|
+
}
|
|
512
|
+
if (input.builtin) {
|
|
513
|
+
const current = raw.builtinPlugins &&
|
|
514
|
+
typeof raw.builtinPlugins === "object" &&
|
|
515
|
+
!Array.isArray(raw.builtinPlugins)
|
|
516
|
+
? raw.builtinPlugins
|
|
517
|
+
: {};
|
|
518
|
+
const entry = current[input.id] &&
|
|
519
|
+
typeof current[input.id] === "object" &&
|
|
520
|
+
!Array.isArray(current[input.id])
|
|
521
|
+
? current[input.id]
|
|
522
|
+
: {};
|
|
523
|
+
raw.builtinPlugins = { ...current, [input.id]: { ...entry, enabled: input.enabled } };
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
const current = raw.pluginOverrides &&
|
|
527
|
+
typeof raw.pluginOverrides === "object" &&
|
|
528
|
+
!Array.isArray(raw.pluginOverrides)
|
|
529
|
+
? raw.pluginOverrides
|
|
530
|
+
: {};
|
|
531
|
+
raw.pluginOverrides = { ...current, [input.id]: { enabled: input.enabled } };
|
|
532
|
+
}
|
|
533
|
+
raw.schemaVersion ??= 1;
|
|
534
|
+
await mkdir(dirname(file), { recursive: true, mode: 0o700 });
|
|
535
|
+
const temporary = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
536
|
+
const handle = await open(temporary, "wx", 0o600);
|
|
537
|
+
try {
|
|
538
|
+
await handle.writeFile(`${JSON.stringify(raw, null, 2)}\n`, "utf8");
|
|
539
|
+
await handle.sync();
|
|
540
|
+
}
|
|
541
|
+
finally {
|
|
542
|
+
await handle.close();
|
|
543
|
+
}
|
|
544
|
+
try {
|
|
545
|
+
await rename(temporary, file);
|
|
546
|
+
await chmod(file, 0o600);
|
|
547
|
+
}
|
|
548
|
+
catch (error) {
|
|
549
|
+
await unlink(temporary).catch(() => { });
|
|
550
|
+
throw error;
|
|
551
|
+
}
|
|
552
|
+
return file;
|
|
553
|
+
}
|
|
554
|
+
/** Atomically updates one project skill override while preserving every unrelated JSON field. */
|
|
555
|
+
export async function setProjectSkillEnabled(input) {
|
|
556
|
+
const file = join(input.workspace, ".alisio", "config.json");
|
|
557
|
+
let raw = {};
|
|
558
|
+
if (await exists(file)) {
|
|
559
|
+
if (!input.trusted)
|
|
560
|
+
throw new Error("Trust this project before changing its existing Alisio configuration");
|
|
561
|
+
const parsed = JSON.parse(await readFile(file, "utf8"));
|
|
562
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
563
|
+
throw new Error("Project Alisio configuration must be a JSON object");
|
|
564
|
+
raw = parsed;
|
|
565
|
+
}
|
|
566
|
+
const current = raw.skillOverrides &&
|
|
567
|
+
typeof raw.skillOverrides === "object" &&
|
|
568
|
+
!Array.isArray(raw.skillOverrides)
|
|
569
|
+
? raw.skillOverrides
|
|
570
|
+
: {};
|
|
571
|
+
raw.skillOverrides = { ...current, [input.id]: { enabled: input.enabled } };
|
|
572
|
+
raw.schemaVersion ??= 1;
|
|
573
|
+
await mkdir(dirname(file), { recursive: true, mode: 0o700 });
|
|
574
|
+
const temporary = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
575
|
+
const handle = await open(temporary, "wx", 0o600);
|
|
576
|
+
try {
|
|
577
|
+
await handle.writeFile(`${JSON.stringify(raw, null, 2)}\n`, "utf8");
|
|
578
|
+
await handle.sync();
|
|
579
|
+
}
|
|
580
|
+
finally {
|
|
581
|
+
await handle.close();
|
|
582
|
+
}
|
|
583
|
+
try {
|
|
584
|
+
await rename(temporary, file);
|
|
585
|
+
await chmod(file, 0o600);
|
|
586
|
+
}
|
|
587
|
+
catch (error) {
|
|
588
|
+
await unlink(temporary).catch(() => { });
|
|
589
|
+
throw error;
|
|
590
|
+
}
|
|
591
|
+
return file;
|
|
592
|
+
}
|