@alisio/core 0.1.0-alpha.3

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 (71) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +25 -0
  3. package/dist/application.d.ts +192 -0
  4. package/dist/application.js +764 -0
  5. package/dist/config.d.ts +420 -0
  6. package/dist/config.js +416 -0
  7. package/dist/core/compaction.d.ts +48 -0
  8. package/dist/core/compaction.js +206 -0
  9. package/dist/core/contracts.d.ts +141 -0
  10. package/dist/core/contracts.js +1 -0
  11. package/dist/core/registry.d.ts +8 -0
  12. package/dist/core/registry.js +54 -0
  13. package/dist/core/runner.d.ts +121 -0
  14. package/dist/core/runner.js +543 -0
  15. package/dist/extensions/registry.d.ts +28 -0
  16. package/dist/extensions/registry.js +58 -0
  17. package/dist/index.d.ts +28 -0
  18. package/dist/index.js +27 -0
  19. package/dist/integrations/herdr.d.ts +34 -0
  20. package/dist/integrations/herdr.js +174 -0
  21. package/dist/mcp/connector.d.ts +63 -0
  22. package/dist/mcp/connector.js +384 -0
  23. package/dist/plugins/host.d.ts +164 -0
  24. package/dist/plugins/host.js +572 -0
  25. package/dist/providers/openai-compatible.d.ts +14 -0
  26. package/dist/providers/openai-compatible.js +228 -0
  27. package/dist/providers/registry.d.ts +35 -0
  28. package/dist/providers/registry.js +74 -0
  29. package/dist/providers/routing.d.ts +11 -0
  30. package/dist/providers/routing.js +41 -0
  31. package/dist/providers/settings.d.ts +30 -0
  32. package/dist/providers/settings.js +80 -0
  33. package/dist/resources/context.d.ts +39 -0
  34. package/dist/resources/context.js +164 -0
  35. package/dist/resources/prompts.d.ts +77 -0
  36. package/dist/resources/prompts.js +182 -0
  37. package/dist/resources/skills.d.ts +82 -0
  38. package/dist/resources/skills.js +214 -0
  39. package/dist/runtime/fs.d.ts +9 -0
  40. package/dist/runtime/fs.js +73 -0
  41. package/dist/runtime/modules.d.ts +14 -0
  42. package/dist/runtime/modules.js +76 -0
  43. package/dist/runtime/paths.d.ts +4 -0
  44. package/dist/runtime/paths.js +42 -0
  45. package/dist/runtime/process.d.ts +16 -0
  46. package/dist/runtime/process.js +104 -0
  47. package/dist/runtime/sqlite.d.ts +3 -0
  48. package/dist/runtime/sqlite.js +85 -0
  49. package/dist/runtime/store.d.ts +37 -0
  50. package/dist/runtime/store.js +250 -0
  51. package/dist/sessions/children.d.ts +55 -0
  52. package/dist/sessions/children.js +231 -0
  53. package/dist/startup/index.d.ts +41 -0
  54. package/dist/startup/index.js +105 -0
  55. package/dist/startup/mascot.d.ts +6 -0
  56. package/dist/startup/mascot.js +30 -0
  57. package/dist/startup/screen.d.ts +18 -0
  58. package/dist/startup/screen.js +116 -0
  59. package/dist/startup/text.d.ts +19 -0
  60. package/dist/startup/text.js +68 -0
  61. package/dist/tools/execute.d.ts +16 -0
  62. package/dist/tools/execute.js +96 -0
  63. package/dist/tools/search.d.ts +69 -0
  64. package/dist/tools/search.js +232 -0
  65. package/dist/tools/standard.d.ts +22 -0
  66. package/dist/tools/standard.js +408 -0
  67. package/dist/tools/webfetch.d.ts +26 -0
  68. package/dist/tools/webfetch.js +119 -0
  69. package/dist/trust.d.ts +34 -0
  70. package/dist/trust.js +113 -0
  71. package/package.json +62 -0
@@ -0,0 +1,228 @@
1
+ import OpenAI from "openai";
2
+ const dataUrl = (a) => `data:${a.mimeType};base64,${a.data}`;
3
+ export class OpenAICompatibleProvider {
4
+ config;
5
+ id;
6
+ model;
7
+ client;
8
+ constructor(config, client) {
9
+ this.config = config;
10
+ this.model = config.model;
11
+ if (!this.model)
12
+ throw new Error("Set ALISIO_MODEL, --model, or provider.model");
13
+ const key = config.auth === "none" ? "unused" : process.env[config.apiKeyEnv];
14
+ if (!client && !key)
15
+ throw new Error(`Missing API key environment variable: ${config.apiKeyEnv}`);
16
+ this.id = `openai-compatible:${config.apiMode}:${config.baseURL.replace(/\/$/, "")}`;
17
+ this.client =
18
+ client ??
19
+ new OpenAI({
20
+ baseURL: config.baseURL,
21
+ apiKey: key,
22
+ maxRetries: 0,
23
+ timeout: 120_000,
24
+ ...(config.auth === "none" ? { defaultHeaders: { Authorization: null } } : {}),
25
+ });
26
+ }
27
+ /** Lists models from GET /models. Context window fields are provider extensions. */
28
+ async listModels(signal) {
29
+ const models = [];
30
+ for await (const m of this.client.models.list({ signal })) {
31
+ const extra = m;
32
+ const window = [extra.context_window, extra.context_length, extra.max_context_length].find((v) => typeof v === "number" && Number.isFinite(v) && v > 0);
33
+ models.push({ id: m.id, ...(window ? { contextWindow: window } : {}) });
34
+ if (models.length >= 1000)
35
+ break;
36
+ }
37
+ return models;
38
+ }
39
+ async *stream(request) {
40
+ if (this.config.apiMode === "responses") {
41
+ yield* this.responses(request);
42
+ return;
43
+ }
44
+ const messages = [
45
+ { role: "system", content: request.instructions },
46
+ ];
47
+ for (const m of request.messages) {
48
+ if (m.role === "user") {
49
+ if (m.attachments?.length) {
50
+ const parts = [];
51
+ if (m.text.trim())
52
+ parts.push({ type: "text", text: m.text });
53
+ for (const a of m.attachments)
54
+ parts.push({ type: "image_url", image_url: { url: dataUrl(a) } });
55
+ messages.push({ role: "user", content: parts });
56
+ }
57
+ else
58
+ messages.push({ role: "user", content: m.text });
59
+ }
60
+ else if (m.role === "tool")
61
+ messages.push({ role: "tool", tool_call_id: m.callId, content: JSON.stringify(m.result) });
62
+ else
63
+ messages.push({
64
+ role: "assistant",
65
+ content: m.text || null,
66
+ ...(m.calls.length
67
+ ? {
68
+ tool_calls: m.calls.map((c) => ({
69
+ id: c.id,
70
+ type: "function",
71
+ function: { name: c.name, arguments: c.arguments },
72
+ })),
73
+ }
74
+ : {}),
75
+ });
76
+ }
77
+ const limit = this.config.tokenParameter === "omit"
78
+ ? {}
79
+ : { [this.config.tokenParameter]: request.maxOutputTokens };
80
+ const stream = await this.client.chat.completions.create({
81
+ model: request.model || this.model,
82
+ messages,
83
+ stream: true,
84
+ ...limit,
85
+ ...(this.config.streamUsage ? { stream_options: { include_usage: true } } : {}),
86
+ ...(request.tools.length || request.nativeTools?.length
87
+ ? {
88
+ tools: [
89
+ ...request.tools.map((t) => ({
90
+ type: "function",
91
+ function: { name: t.name, description: t.description, parameters: t.inputSchema },
92
+ })),
93
+ ...(request.nativeTools ?? []),
94
+ ],
95
+ }
96
+ : {}),
97
+ }, { signal: request.signal });
98
+ let text = "", finish = null, usage;
99
+ const calls = new Map();
100
+ for await (const chunk of stream) {
101
+ if (chunk.usage) {
102
+ // DeepSeek reports prompt_cache_hit_tokens; OpenAI uses prompt_tokens_details.
103
+ const extra = chunk.usage;
104
+ const cached = chunk.usage.prompt_tokens_details?.cached_tokens ?? extra.prompt_cache_hit_tokens;
105
+ usage = {
106
+ input: chunk.usage.prompt_tokens,
107
+ output: chunk.usage.completion_tokens,
108
+ ...(typeof cached === "number" ? { cachedInput: cached } : {}),
109
+ };
110
+ }
111
+ const choice = chunk.choices[0];
112
+ if (!choice)
113
+ continue;
114
+ const reasoning = choice.delta.reasoning_content;
115
+ if (typeof reasoning === "string" && reasoning)
116
+ yield { type: "reasoning_delta", delta: reasoning };
117
+ if (choice.delta.content) {
118
+ text += choice.delta.content;
119
+ yield { type: "text_delta", delta: choice.delta.content };
120
+ }
121
+ if (choice.delta.refusal)
122
+ throw new Error(`Provider refusal: ${choice.delta.refusal}`);
123
+ for (const part of choice.delta.tool_calls ?? []) {
124
+ const c = calls.get(part.index) ?? { id: "", name: "", arguments: "" };
125
+ if (part.id)
126
+ c.id = part.id;
127
+ if (part.function?.name)
128
+ c.name += part.function.name;
129
+ if (part.function?.arguments)
130
+ c.arguments += part.function.arguments;
131
+ calls.set(part.index, c);
132
+ }
133
+ if (choice.finish_reason)
134
+ finish = choice.finish_reason;
135
+ }
136
+ if (finish !== "stop" && finish !== "tool_calls")
137
+ throw new Error(`Provider response incomplete: ${finish ?? "stream ended"}`);
138
+ const completed = [...calls.entries()].sort(([a], [b]) => a - b).map(([, c]) => c);
139
+ if (completed.some((c) => !c.id || !c.name))
140
+ throw new Error("Incomplete tool call");
141
+ yield { type: "completed", message: { role: "assistant", text, calls: completed }, usage };
142
+ }
143
+ async *responses(request) {
144
+ const input = [];
145
+ for (const m of request.messages) {
146
+ if (m.role === "user") {
147
+ if (m.attachments?.length) {
148
+ const content = [];
149
+ if (m.text.trim())
150
+ content.push({ type: "input_text", text: m.text });
151
+ for (const a of m.attachments)
152
+ content.push({ type: "input_image", image_url: dataUrl(a), detail: "auto" });
153
+ input.push({ role: "user", content });
154
+ }
155
+ else
156
+ input.push({ role: "user", content: m.text });
157
+ }
158
+ else if (m.role === "tool")
159
+ input.push({
160
+ type: "function_call_output",
161
+ call_id: m.callId,
162
+ output: JSON.stringify(m.result),
163
+ });
164
+ else if (m.providerData)
165
+ input.push(...m.providerData);
166
+ else
167
+ throw new Error("Missing provider continuation data for Responses session");
168
+ }
169
+ const stream = await this.client.responses.create({
170
+ model: request.model || this.model,
171
+ instructions: request.instructions,
172
+ input,
173
+ stream: true,
174
+ store: false,
175
+ include: ["reasoning.encrypted_content"],
176
+ max_output_tokens: request.maxOutputTokens,
177
+ tools: [
178
+ ...request.tools.map((t) => ({
179
+ type: "function",
180
+ name: t.name,
181
+ description: t.description,
182
+ parameters: t.inputSchema,
183
+ strict: false,
184
+ })),
185
+ ...(request.nativeTools ?? []),
186
+ ],
187
+ }, { signal: request.signal });
188
+ let complete = false;
189
+ for await (const event of stream) {
190
+ if (event.type === "response.output_text.delta")
191
+ yield { type: "text_delta", delta: event.delta };
192
+ if (event.type === "response.reasoning_summary_text.delta")
193
+ yield { type: "reasoning_delta", delta: event.delta };
194
+ if (event.type === "response.failed" ||
195
+ event.type === "response.incomplete" ||
196
+ event.type === "error")
197
+ throw new Error(`Provider response failed: ${event.type}`);
198
+ if (event.type === "response.completed") {
199
+ complete = true;
200
+ const r = event.response;
201
+ const calls = r.output
202
+ .filter((x) => x.type === "function_call")
203
+ .map((x) => ({ id: x.call_id, name: x.name, arguments: x.arguments }));
204
+ const text = r.output
205
+ .filter((x) => x.type === "message")
206
+ .flatMap((x) => x.content)
207
+ .filter((x) => x.type === "output_text")
208
+ .map((x) => x.text)
209
+ .join("");
210
+ yield {
211
+ type: "completed",
212
+ message: { role: "assistant", text, calls, providerData: r.output },
213
+ usage: r.usage
214
+ ? {
215
+ input: r.usage.input_tokens,
216
+ output: r.usage.output_tokens,
217
+ ...(typeof r.usage.input_tokens_details?.cached_tokens === "number"
218
+ ? { cachedInput: r.usage.input_tokens_details.cached_tokens }
219
+ : {}),
220
+ }
221
+ : undefined,
222
+ };
223
+ }
224
+ }
225
+ if (!complete)
226
+ throw new Error("Responses stream ended before completion");
227
+ }
228
+ }
@@ -0,0 +1,35 @@
1
+ import type { ModelInfo, ModelProvider, ProviderCreateRequest, ProviderRegistration } from "@alisio/sdk";
2
+ export interface RegisteredProvider extends ProviderRegistration {
3
+ plugin: string;
4
+ builtin: boolean;
5
+ }
6
+ /** Additive provider catalog. Registrations are unique by provider id and independently removable. */
7
+ export declare class ProviderRegistry {
8
+ private entries;
9
+ register(registration: ProviderRegistration, owner: {
10
+ plugin: string;
11
+ builtin: boolean;
12
+ }): () => void;
13
+ get(id: string): RegisteredProvider | undefined;
14
+ list(): RegisteredProvider[];
15
+ create(id: string, request: ProviderCreateRequest): Promise<ModelProvider>;
16
+ }
17
+ /** Mutable facade keeps runner/plugin completion references stable while the active provider changes. */
18
+ export declare class ActiveProvider implements ModelProvider {
19
+ private current;
20
+ constructor(current: ModelProvider);
21
+ /** Core-only lifecycle access; credentials remain encapsulated by the provider instance. */
22
+ get currentProvider(): ModelProvider;
23
+ get id(): string;
24
+ get model(): string;
25
+ stream(request: Parameters<ModelProvider["stream"]>[0]): AsyncIterable<import("@alisio/sdk").ProviderEvent>;
26
+ listModels(signal: AbortSignal): Promise<ModelInfo[]>;
27
+ /** Swap only after the candidate exists; a failed candidate creation leaves the old provider live. */
28
+ replace(next: ModelProvider, disposePrevious?: boolean): Promise<void>;
29
+ dispose(): void | Promise<void> | undefined;
30
+ }
31
+ export declare class UnconfiguredProvider implements ModelProvider {
32
+ readonly id = "unconfigured";
33
+ readonly model = "";
34
+ stream(): AsyncIterable<never>;
35
+ }
@@ -0,0 +1,74 @@
1
+ /** Additive provider catalog. Registrations are unique by provider id and independently removable. */
2
+ export class ProviderRegistry {
3
+ entries = new Map();
4
+ register(registration, owner) {
5
+ if (!/^[a-z0-9][a-z0-9.-]{0,63}$/.test(registration.id))
6
+ throw new Error(`Invalid provider id: ${registration.id}`);
7
+ if (this.entries.has(registration.id))
8
+ throw new Error(`Duplicate provider: ${registration.id}`);
9
+ const entry = { ...registration, ...owner };
10
+ this.entries.set(entry.id, entry);
11
+ return () => {
12
+ if (this.entries.get(entry.id) === entry)
13
+ this.entries.delete(entry.id);
14
+ };
15
+ }
16
+ get(id) {
17
+ return this.entries.get(id);
18
+ }
19
+ list() {
20
+ return [...this.entries.values()].sort((a, b) => a.name.localeCompare(b.name));
21
+ }
22
+ async create(id, request) {
23
+ const registration = this.entries.get(id);
24
+ if (!registration)
25
+ throw new Error(`Unknown provider: ${id}`);
26
+ return registration.create(request);
27
+ }
28
+ }
29
+ /** Mutable facade keeps runner/plugin completion references stable while the active provider changes. */
30
+ export class ActiveProvider {
31
+ current;
32
+ constructor(current) {
33
+ this.current = current;
34
+ }
35
+ /** Core-only lifecycle access; credentials remain encapsulated by the provider instance. */
36
+ get currentProvider() {
37
+ return this.current;
38
+ }
39
+ get id() {
40
+ return this.current.id;
41
+ }
42
+ get model() {
43
+ return this.current.model;
44
+ }
45
+ stream(request) {
46
+ return this.current.stream(request);
47
+ }
48
+ listModels(signal) {
49
+ return this.current.listModels?.(signal) ?? Promise.resolve([]);
50
+ }
51
+ /** Swap only after the candidate exists; a failed candidate creation leaves the old provider live. */
52
+ async replace(next, disposePrevious = true) {
53
+ const previous = this.current;
54
+ this.current = next;
55
+ if (!disposePrevious)
56
+ return;
57
+ try {
58
+ await previous.dispose?.();
59
+ }
60
+ catch {
61
+ // Activation already committed. A stale provider disposal failure must not roll it back.
62
+ }
63
+ }
64
+ dispose() {
65
+ return this.current.dispose?.();
66
+ }
67
+ }
68
+ export class UnconfiguredProvider {
69
+ id = "unconfigured";
70
+ model = "";
71
+ async *stream() {
72
+ throw new Error("No model provider is configured. Use /connect in the TUI or configure one before running headless.");
73
+ }
74
+ }
@@ -0,0 +1,11 @@
1
+ import type { AvailableProviderModel, ResolvedProviderModel } from "@alisio/sdk";
2
+ export interface ProviderModelCatalog {
3
+ profile: string;
4
+ provider: string;
5
+ title: string;
6
+ models: AvailableProviderModel["model"][];
7
+ unavailable: boolean;
8
+ }
9
+ export declare function availableProviderModels(catalogs: readonly ProviderModelCatalog[]): AvailableProviderModel[];
10
+ /** Resolves canonical provider/model selectors and safe, unique bare model ids. */
11
+ export declare function resolveProviderModel(catalogs: readonly ProviderModelCatalog[], input: string): ResolvedProviderModel;
@@ -0,0 +1,41 @@
1
+ export function availableProviderModels(catalogs) {
2
+ return catalogs
3
+ .flatMap((catalog) => catalog.unavailable
4
+ ? []
5
+ : catalog.models.map((model) => ({
6
+ reference: `${catalog.provider}/${model.id}`,
7
+ provider: catalog.provider,
8
+ profile: catalog.profile,
9
+ providerName: catalog.title,
10
+ model,
11
+ })))
12
+ .sort((a, b) => a.reference.localeCompare(b.reference));
13
+ }
14
+ const choices = (available) => [...new Set(available.map((entry) => entry.reference))].slice(0, 12).join(", ");
15
+ /** Resolves canonical provider/model selectors and safe, unique bare model ids. */
16
+ export function resolveProviderModel(catalogs, input) {
17
+ const reference = input.trim();
18
+ if (!reference)
19
+ throw new Error("Model selector must not be empty");
20
+ const available = availableProviderModels(catalogs);
21
+ const providers = [...new Set(catalogs.map((catalog) => catalog.provider))].sort((a, b) => b.length - a.length);
22
+ const provider = providers.find((id) => reference.startsWith(`${id}/`));
23
+ let matches;
24
+ if (provider) {
25
+ const model = reference.slice(provider.length + 1);
26
+ const matchingCatalogs = catalogs.filter((catalog) => catalog.provider === provider);
27
+ matches = available.filter((entry) => entry.provider === provider && entry.model.id === model);
28
+ if (!matches.length && matchingCatalogs.some((catalog) => catalog.unavailable))
29
+ throw new Error(`Configured provider "${provider}" is unavailable. Reconnect it with /connect and retry ${reference}.`);
30
+ }
31
+ else
32
+ matches = available.filter((entry) => entry.model.id === reference);
33
+ if (matches.length === 1)
34
+ return matches[0];
35
+ if (matches.length > 1)
36
+ throw new Error(`Model selector "${reference}" is ambiguous. Use one of: ${choices(matches)}.`);
37
+ const guidance = choices(available);
38
+ throw new Error(`Model selector "${reference}" is not available from configured /connect profiles.${guidance
39
+ ? ` Available provider/model choices: ${guidance}.`
40
+ : " Configure a provider with /connect first."}`);
41
+ }
@@ -0,0 +1,30 @@
1
+ import type { ProviderConfigurationValue } from "@alisio/sdk";
2
+ export interface ProviderProfile {
3
+ provider: string;
4
+ values: Record<string, ProviderConfigurationValue>;
5
+ model: string;
6
+ }
7
+ export interface ProviderSettings {
8
+ schemaVersion: 1;
9
+ active?: string;
10
+ profiles: Record<string, ProviderProfile>;
11
+ }
12
+ export declare class ProviderSettingsStore {
13
+ readonly profilesPath: string;
14
+ readonly credentialsPath: string;
15
+ constructor(root?: string);
16
+ load(): Promise<ProviderSettings>;
17
+ private credentials;
18
+ active(): Promise<{
19
+ name: string;
20
+ profile: ProviderProfile;
21
+ credentials: Record<string, string>;
22
+ } | undefined>;
23
+ /** Resolves one stored plugin profile for host use. Display layers must not call this. */
24
+ resolve(name: string): Promise<{
25
+ name: string;
26
+ profile: ProviderProfile;
27
+ credentials: Record<string, string>;
28
+ } | undefined>;
29
+ saveActive(name: string, profile: ProviderProfile, secrets: Record<string, string>): Promise<void>;
30
+ }
@@ -0,0 +1,80 @@
1
+ import { chmod, mkdir, open, readFile, rename } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { configHome } from "../config.js";
4
+ const emptySettings = () => ({ schemaVersion: 1, profiles: {} });
5
+ const emptyCredentials = () => ({ schemaVersion: 1, providers: {} });
6
+ async function read(path, fallback) {
7
+ try {
8
+ return JSON.parse(await readFile(path, "utf8"));
9
+ }
10
+ catch (error) {
11
+ if (error.code === "ENOENT")
12
+ return fallback;
13
+ throw error;
14
+ }
15
+ }
16
+ async function atomicJson(path, value, mode) {
17
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
18
+ await chmod(dirname(path), 0o700).catch(() => { });
19
+ const temporary = `${path}.${process.pid}.${crypto.randomUUID()}.tmp`;
20
+ const handle = await open(temporary, "wx", mode);
21
+ try {
22
+ await handle.writeFile(`${JSON.stringify(value, null, 2)}\n`, "utf8");
23
+ await handle.sync();
24
+ }
25
+ finally {
26
+ await handle.close();
27
+ }
28
+ await chmod(temporary, mode);
29
+ await rename(temporary, path);
30
+ await chmod(path, mode);
31
+ }
32
+ export class ProviderSettingsStore {
33
+ profilesPath;
34
+ credentialsPath;
35
+ constructor(root = configHome()) {
36
+ this.profilesPath = join(root, "providers.json");
37
+ this.credentialsPath = join(root, "credentials.json");
38
+ }
39
+ load() {
40
+ return read(this.profilesPath, emptySettings());
41
+ }
42
+ credentials() {
43
+ return read(this.credentialsPath, emptyCredentials()).then(async (value) => {
44
+ await chmod(this.credentialsPath, 0o600).catch(() => { });
45
+ return value;
46
+ });
47
+ }
48
+ async active() {
49
+ const settings = await this.load();
50
+ if (!settings.active)
51
+ return undefined;
52
+ const profile = settings.profiles[settings.active];
53
+ if (!profile)
54
+ return undefined;
55
+ const credentials = await this.credentials();
56
+ return {
57
+ name: settings.active,
58
+ profile,
59
+ credentials: credentials.providers[settings.active] ?? {},
60
+ };
61
+ }
62
+ /** Resolves one stored plugin profile for host use. Display layers must not call this. */
63
+ async resolve(name) {
64
+ const settings = await this.load();
65
+ const profile = settings.profiles[name];
66
+ if (!profile)
67
+ return undefined;
68
+ const credentials = await this.credentials();
69
+ return { name, profile, credentials: credentials.providers[name] ?? {} };
70
+ }
71
+ async saveActive(name, profile, secrets) {
72
+ const [settings, credentials] = await Promise.all([this.load(), this.credentials()]);
73
+ settings.active = name;
74
+ settings.profiles[name] = profile;
75
+ credentials.providers[name] = { ...(credentials.providers[name] ?? {}), ...secrets };
76
+ // Credentials first: an interrupted update can leave an unused secret, never an active profile without it.
77
+ await atomicJson(this.credentialsPath, credentials, 0o600);
78
+ await atomicJson(this.profilesPath, settings, 0o600);
79
+ }
80
+ }
@@ -0,0 +1,39 @@
1
+ import type { ContextSource } from "../core/contracts.ts";
2
+ export type InstructionKind = "override" | "agents" | "legacy" | "claude";
3
+ export interface InstructionFile {
4
+ path: string;
5
+ text: string;
6
+ kind: InstructionKind;
7
+ }
8
+ export interface ProjectContextOptions {
9
+ /** Directory holding the global AGENTS.md / AGENTS.override.md. */
10
+ globalDir?: string;
11
+ cwd?: string;
12
+ /** Use CLAUDE.md when a directory has no AGENTS file (default false). */
13
+ claudeMdFallback?: boolean;
14
+ /** Total bytes of instruction files (default 32 KiB). */
15
+ maxBytes?: number;
16
+ }
17
+ export declare class ProjectContext implements ContextSource {
18
+ readonly workspace: string;
19
+ extras: Array<() => Promise<string>>;
20
+ readonly cwd: string;
21
+ private options;
22
+ /** Per session: nested instruction file path → fingerprint already attached. */
23
+ private attached;
24
+ constructor(workspace: string, options?: ProjectContextOptions);
25
+ private names;
26
+ private readDir;
27
+ /** Directories from the workspace root down to the directory containing `path`. */
28
+ private chain;
29
+ private baseFiles;
30
+ explain(path: string): Promise<InstructionFile[]>;
31
+ /**
32
+ * Instruction files for paths a tool is about to touch that are not part of the base walk
33
+ * and were not attached to this session yet (or changed since). Returns their text.
34
+ */
35
+ beforePaths(paths: string[], sessionId?: string): Promise<string | undefined>;
36
+ instructions(sessionId?: string): Promise<string>;
37
+ /** Keeps the closest files within the byte budget; drops or truncates the farthest ones. */
38
+ private capped;
39
+ }