@apolloyh/apollo-agent 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/.apollo/config.example.json +37 -0
- package/.env.example +17 -0
- package/README.md +62 -0
- package/dist/brand.d.ts +17 -0
- package/dist/brand.d.ts.map +1 -0
- package/dist/brand.js +23 -0
- package/dist/cli-args.d.ts +13 -0
- package/dist/cli-args.d.ts.map +1 -0
- package/dist/cli-args.js +50 -0
- package/dist/config.d.ts +13 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +190 -0
- package/dist/constants/spinner-verbs.d.ts +7 -0
- package/dist/constants/spinner-verbs.d.ts.map +1 -0
- package/dist/constants/spinner-verbs.js +21 -0
- package/dist/coordinator/workflow.d.ts +20 -0
- package/dist/coordinator/workflow.d.ts.map +1 -0
- package/dist/coordinator/workflow.js +120 -0
- package/dist/goal/goal-mode.d.ts +26 -0
- package/dist/goal/goal-mode.d.ts.map +1 -0
- package/dist/goal/goal-mode.js +141 -0
- package/dist/hooks/runner.d.ts +43 -0
- package/dist/hooks/runner.d.ts.map +1 -0
- package/dist/hooks/runner.js +94 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1716 -0
- package/dist/llm/anthropic.d.ts +46 -0
- package/dist/llm/anthropic.d.ts.map +1 -0
- package/dist/llm/anthropic.js +278 -0
- package/dist/mcp/manager.d.ts +14 -0
- package/dist/mcp/manager.d.ts.map +1 -0
- package/dist/mcp/manager.js +93 -0
- package/dist/memory/memdir.d.ts +19 -0
- package/dist/memory/memdir.d.ts.map +1 -0
- package/dist/memory/memdir.js +96 -0
- package/dist/plan/plan-mode.d.ts +14 -0
- package/dist/plan/plan-mode.d.ts.map +1 -0
- package/dist/plan/plan-mode.js +90 -0
- package/dist/project-instructions.d.ts +8 -0
- package/dist/project-instructions.d.ts.map +1 -0
- package/dist/project-instructions.js +33 -0
- package/dist/runtime/compaction.d.ts +5 -0
- package/dist/runtime/compaction.d.ts.map +1 -0
- package/dist/runtime/compaction.js +37 -0
- package/dist/runtime/context.d.ts +25 -0
- package/dist/runtime/context.d.ts.map +1 -0
- package/dist/runtime/context.js +75 -0
- package/dist/runtime/permissions.d.ts +11 -0
- package/dist/runtime/permissions.d.ts.map +1 -0
- package/dist/runtime/permissions.js +119 -0
- package/dist/runtime/prompt-builder.d.ts +27 -0
- package/dist/runtime/prompt-builder.d.ts.map +1 -0
- package/dist/runtime/prompt-builder.js +112 -0
- package/dist/runtime/query-engine.d.ts +136 -0
- package/dist/runtime/query-engine.d.ts.map +1 -0
- package/dist/runtime/query-engine.js +1182 -0
- package/dist/runtime/tool-policy.d.ts +13 -0
- package/dist/runtime/tool-policy.d.ts.map +1 -0
- package/dist/runtime/tool-policy.js +106 -0
- package/dist/sdk.d.ts +33 -0
- package/dist/sdk.d.ts.map +1 -0
- package/dist/sdk.js +41 -0
- package/dist/session/resume-ui.d.ts +17 -0
- package/dist/session/resume-ui.d.ts.map +1 -0
- package/dist/session/resume-ui.js +64 -0
- package/dist/session/store.d.ts +66 -0
- package/dist/session/store.d.ts.map +1 -0
- package/dist/session/store.js +120 -0
- package/dist/skillify/skillify.d.ts +12 -0
- package/dist/skillify/skillify.d.ts.map +1 -0
- package/dist/skillify/skillify.js +52 -0
- package/dist/skills/skills.d.ts +26 -0
- package/dist/skills/skills.d.ts.map +1 -0
- package/dist/skills/skills.js +195 -0
- package/dist/status-ui.d.ts +37 -0
- package/dist/status-ui.d.ts.map +1 -0
- package/dist/status-ui.js +130 -0
- package/dist/thought-fold.d.ts +48 -0
- package/dist/thought-fold.d.ts.map +1 -0
- package/dist/thought-fold.js +156 -0
- package/dist/tools/builtin.d.ts +27 -0
- package/dist/tools/builtin.d.ts.map +1 -0
- package/dist/tools/builtin.js +898 -0
- package/dist/tools/registry.d.ts +17 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +86 -0
- package/dist/trace.d.ts +68 -0
- package/dist/trace.d.ts.map +1 -0
- package/dist/trace.js +600 -0
- package/dist/types.d.ts +202 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +8 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +78 -0
- package/docs/sdk.md +259 -0
- package/package.json +53 -0
- package/src/skills/tech-research-skill/SKILL.md +200 -0
- package/src/skills/tech-research-skill/agents/openai.yaml +4 -0
- package/src/skills/tech-research-skill/repo-analyze +435 -0
- package/src/skills/tech-research-skill/repo-fetch +310 -0
- package/src/skills/tech-research-skill/report-generate +254 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { AnthropicTool, ContentBlock, LlmConfig, Message } from "../types.js";
|
|
2
|
+
export type AnthropicResponse = {
|
|
3
|
+
id: string;
|
|
4
|
+
type: string;
|
|
5
|
+
role: "assistant";
|
|
6
|
+
content: ContentBlock[];
|
|
7
|
+
stop_reason?: string;
|
|
8
|
+
usage?: AnthropicUsage;
|
|
9
|
+
};
|
|
10
|
+
export type AnthropicUsage = {
|
|
11
|
+
input_tokens: number;
|
|
12
|
+
output_tokens: number;
|
|
13
|
+
cache_creation_input_tokens: number;
|
|
14
|
+
cache_read_input_tokens: number;
|
|
15
|
+
};
|
|
16
|
+
export type ToolInputDelta = {
|
|
17
|
+
bytes: number;
|
|
18
|
+
tool?: string;
|
|
19
|
+
detail?: string;
|
|
20
|
+
};
|
|
21
|
+
export declare class MalformedStreamError extends Error {
|
|
22
|
+
constructor(message: string, cause?: unknown);
|
|
23
|
+
}
|
|
24
|
+
export declare class MalformedToolInputError extends MalformedStreamError {
|
|
25
|
+
constructor(tool: string, bytes: number, cause: unknown);
|
|
26
|
+
}
|
|
27
|
+
export declare class AnthropicApiError extends Error {
|
|
28
|
+
readonly status: number;
|
|
29
|
+
readonly responseBody: string;
|
|
30
|
+
readonly responseHeaders: Headers;
|
|
31
|
+
constructor(status: number, responseBody: string, responseHeaders: Headers);
|
|
32
|
+
}
|
|
33
|
+
export declare class AnthropicClient {
|
|
34
|
+
private readonly config;
|
|
35
|
+
constructor(config: LlmConfig);
|
|
36
|
+
createMessage(input: {
|
|
37
|
+
system: string;
|
|
38
|
+
messages: Message[];
|
|
39
|
+
tools: AnthropicTool[];
|
|
40
|
+
stream?: boolean;
|
|
41
|
+
signal?: AbortSignal;
|
|
42
|
+
onTextDelta?: (text: string) => void;
|
|
43
|
+
onToolInputDelta?: (event: ToolInputDelta) => void;
|
|
44
|
+
}): Promise<AnthropicResponse>;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=anthropic.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anthropic.d.ts","sourceRoot":"","sources":["../../src/llm/anthropic.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAEnF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,WAAW,CAAC;IAClB,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,cAAc,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,2BAA2B,EAAE,MAAM,CAAC;IACpC,uBAAuB,EAAE,MAAM,CAAC;CACjC,CAAC;AAMF,MAAM,MAAM,cAAc,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/E,qBAAa,oBAAqB,SAAQ,KAAK;gBACjC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAI7C;AAED,qBAAa,uBAAwB,SAAQ,oBAAoB;gBACnD,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO;CAIxD;AAED,qBAAa,iBAAkB,SAAQ,KAAK;aAExB,MAAM,EAAE,MAAM;aACd,YAAY,EAAE,MAAM;aACpB,eAAe,EAAE,OAAO;gBAFxB,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,EACpB,eAAe,EAAE,OAAO;CAK3C;AAED,qBAAa,eAAe;IACd,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,SAAS;IAExC,aAAa,CAAC,KAAK,EAAE;QACzB,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,OAAO,EAAE,CAAC;QACpB,KAAK,EAAE,aAAa,EAAE,CAAC;QACvB,MAAM,CAAC,EAAE,OAAO,CAAC;QACjB,MAAM,CAAC,EAAE,WAAW,CAAC;QACrB,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;QACrC,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;KACpD,GAAG,OAAO,CAAC,iBAAiB,CAAC;CAqC/B"}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
export class MalformedStreamError extends Error {
|
|
2
|
+
constructor(message, cause) {
|
|
3
|
+
super(message, cause === undefined ? undefined : { cause });
|
|
4
|
+
this.name = "MalformedStreamError";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export class MalformedToolInputError extends MalformedStreamError {
|
|
8
|
+
constructor(tool, bytes, cause) {
|
|
9
|
+
super(`Model returned incomplete JSON for tool ${tool || "unknown"} (${bytes} characters).`, cause);
|
|
10
|
+
this.name = "MalformedToolInputError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export class AnthropicApiError extends Error {
|
|
14
|
+
status;
|
|
15
|
+
responseBody;
|
|
16
|
+
responseHeaders;
|
|
17
|
+
constructor(status, responseBody, responseHeaders) {
|
|
18
|
+
super(`Anthropic API error ${status}: ${responseBody}`);
|
|
19
|
+
this.status = status;
|
|
20
|
+
this.responseBody = responseBody;
|
|
21
|
+
this.responseHeaders = responseHeaders;
|
|
22
|
+
this.name = "AnthropicApiError";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export class AnthropicClient {
|
|
26
|
+
config;
|
|
27
|
+
constructor(config) {
|
|
28
|
+
this.config = config;
|
|
29
|
+
}
|
|
30
|
+
async createMessage(input) {
|
|
31
|
+
const url = `${this.config.baseUrl.replace(/\/$/, "")}/v1/messages`;
|
|
32
|
+
const response = await fetch(url, {
|
|
33
|
+
method: "POST",
|
|
34
|
+
headers: {
|
|
35
|
+
"content-type": "application/json",
|
|
36
|
+
"anthropic-version": "2023-06-01",
|
|
37
|
+
"x-api-key": this.config.authToken,
|
|
38
|
+
authorization: `Bearer ${this.config.authToken}`,
|
|
39
|
+
},
|
|
40
|
+
body: JSON.stringify({
|
|
41
|
+
model: this.config.model,
|
|
42
|
+
max_tokens: this.config.maxTokens,
|
|
43
|
+
system: input.system,
|
|
44
|
+
messages: input.messages,
|
|
45
|
+
...(input.tools.length > 0
|
|
46
|
+
? { tools: input.tools, tool_choice: { type: "auto" } }
|
|
47
|
+
: {}),
|
|
48
|
+
cache_control: this.config.cacheTtl === "1h"
|
|
49
|
+
? { type: "ephemeral", ttl: "1h" }
|
|
50
|
+
: { type: "ephemeral" },
|
|
51
|
+
stream: input.stream ?? false,
|
|
52
|
+
}),
|
|
53
|
+
signal: input.signal
|
|
54
|
+
? AbortSignal.any([input.signal, AbortSignal.timeout(normalizeTimeout(this.config.timeoutMs))])
|
|
55
|
+
: AbortSignal.timeout(normalizeTimeout(this.config.timeoutMs)),
|
|
56
|
+
});
|
|
57
|
+
if (!response.ok) {
|
|
58
|
+
const errorText = await response.text();
|
|
59
|
+
throw new AnthropicApiError(response.status, errorText, response.headers);
|
|
60
|
+
}
|
|
61
|
+
if (input.stream) {
|
|
62
|
+
return readStreamingResponse(response, input.onTextDelta, input.onToolInputDelta);
|
|
63
|
+
}
|
|
64
|
+
return (await response.json());
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function readStreamingResponse(response, onTextDelta, onToolInputDelta) {
|
|
68
|
+
if (!response.body)
|
|
69
|
+
throw new Error("Anthropic API returned an empty stream body.");
|
|
70
|
+
const reader = response.body.getReader();
|
|
71
|
+
const decoder = new TextDecoder();
|
|
72
|
+
const blocks = new Map();
|
|
73
|
+
let buffer = "";
|
|
74
|
+
let responseId = "";
|
|
75
|
+
let stopReason;
|
|
76
|
+
let usage;
|
|
77
|
+
let streamEnded = false;
|
|
78
|
+
try {
|
|
79
|
+
for (;;) {
|
|
80
|
+
const { done, value } = await reader.read();
|
|
81
|
+
if (done) {
|
|
82
|
+
streamEnded = true;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
buffer += decoder.decode(value, { stream: true });
|
|
86
|
+
let separator = buffer.match(/\r?\n\r?\n/);
|
|
87
|
+
while (separator?.index !== undefined) {
|
|
88
|
+
const separatorIndex = separator.index;
|
|
89
|
+
const rawEvent = buffer.slice(0, separatorIndex);
|
|
90
|
+
buffer = buffer.slice(separatorIndex + separator[0].length);
|
|
91
|
+
const data = parseSseData(rawEvent);
|
|
92
|
+
if (data && data !== "[DONE]") {
|
|
93
|
+
const event = parseStreamEvent(data);
|
|
94
|
+
if (event.type === "error") {
|
|
95
|
+
throw new Error(`Anthropic stream error: ${JSON.stringify(event.error ?? event)}`);
|
|
96
|
+
}
|
|
97
|
+
applyStreamEvent(event, blocks, (id) => {
|
|
98
|
+
responseId = id;
|
|
99
|
+
}, (reason) => {
|
|
100
|
+
stopReason = reason;
|
|
101
|
+
}, (part) => {
|
|
102
|
+
usage = mergeUsage(usage, part);
|
|
103
|
+
}, onTextDelta, onToolInputDelta);
|
|
104
|
+
}
|
|
105
|
+
separator = buffer.match(/\r?\n\r?\n/);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
buffer += decoder.decode();
|
|
109
|
+
if (buffer.trim()) {
|
|
110
|
+
const data = parseSseData(buffer);
|
|
111
|
+
if (data && data !== "[DONE]") {
|
|
112
|
+
const event = parseStreamEvent(data);
|
|
113
|
+
if (event.type === "error") {
|
|
114
|
+
throw new Error(`Anthropic stream error: ${JSON.stringify(event.error ?? event)}`);
|
|
115
|
+
}
|
|
116
|
+
applyStreamEvent(event, blocks, (id) => {
|
|
117
|
+
responseId = id;
|
|
118
|
+
}, (reason) => {
|
|
119
|
+
stopReason = reason;
|
|
120
|
+
}, (part) => {
|
|
121
|
+
usage = mergeUsage(usage, part);
|
|
122
|
+
}, onTextDelta, onToolInputDelta);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (blocks.size === 0) {
|
|
126
|
+
throw new MalformedStreamError("Model returned an empty or incomplete event stream.");
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
id: responseId,
|
|
130
|
+
type: "message",
|
|
131
|
+
role: "assistant",
|
|
132
|
+
content: Array.from(blocks.entries())
|
|
133
|
+
.sort(([left], [right]) => left - right)
|
|
134
|
+
.map(([, block]) => finalizeStreamBlock(block)),
|
|
135
|
+
stop_reason: stopReason,
|
|
136
|
+
usage,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
if (!streamEnded)
|
|
141
|
+
await reader.cancel().catch(() => undefined);
|
|
142
|
+
reader.releaseLock();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function normalizeTimeout(value) {
|
|
146
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 300000;
|
|
147
|
+
}
|
|
148
|
+
function parseSseData(rawEvent) {
|
|
149
|
+
return rawEvent
|
|
150
|
+
.split(/\r?\n/)
|
|
151
|
+
.filter((line) => line.startsWith("data:"))
|
|
152
|
+
.map((line) => line.slice("data:".length).trimStart())
|
|
153
|
+
.join("\n")
|
|
154
|
+
.trim();
|
|
155
|
+
}
|
|
156
|
+
function parseStreamEvent(data) {
|
|
157
|
+
try {
|
|
158
|
+
return JSON.parse(data);
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
throw new MalformedStreamError("Model returned malformed JSON in the event stream.", error);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function applyStreamEvent(event, blocks, setResponseId, setStopReason, setUsage, onTextDelta, onToolInputDelta) {
|
|
165
|
+
if (event.type === "message_start") {
|
|
166
|
+
const message = event.message;
|
|
167
|
+
if (message?.id)
|
|
168
|
+
setResponseId(message.id);
|
|
169
|
+
if (message?.usage)
|
|
170
|
+
setUsage(message.usage);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (event.type === "message_delta") {
|
|
174
|
+
const delta = event.delta;
|
|
175
|
+
if (delta?.stop_reason)
|
|
176
|
+
setStopReason(delta.stop_reason);
|
|
177
|
+
if (event.usage)
|
|
178
|
+
setUsage(event.usage);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (event.type === "content_block_start") {
|
|
182
|
+
const index = Number(event.index);
|
|
183
|
+
const contentBlock = event.content_block;
|
|
184
|
+
if (!Number.isFinite(index) || !contentBlock)
|
|
185
|
+
return;
|
|
186
|
+
if (contentBlock.type === "text") {
|
|
187
|
+
blocks.set(index, { type: "text", text: contentBlock.text ?? "" });
|
|
188
|
+
}
|
|
189
|
+
else if (contentBlock.type === "tool_use") {
|
|
190
|
+
const block = {
|
|
191
|
+
type: "tool_use",
|
|
192
|
+
id: contentBlock.id ?? "",
|
|
193
|
+
name: contentBlock.name ?? "",
|
|
194
|
+
inputJson: "",
|
|
195
|
+
};
|
|
196
|
+
blocks.set(index, block);
|
|
197
|
+
onToolInputDelta?.({ bytes: 0, tool: block.name || undefined });
|
|
198
|
+
}
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (event.type !== "content_block_delta")
|
|
202
|
+
return;
|
|
203
|
+
const index = Number(event.index);
|
|
204
|
+
const block = blocks.get(index);
|
|
205
|
+
const delta = event.delta;
|
|
206
|
+
if (!block || !delta)
|
|
207
|
+
return;
|
|
208
|
+
if (block.type === "text" && delta.type === "text_delta" && delta.text) {
|
|
209
|
+
block.text += delta.text;
|
|
210
|
+
onTextDelta?.(delta.text);
|
|
211
|
+
}
|
|
212
|
+
else if (block.type === "tool_use" && delta.type === "input_json_delta" && delta.partial_json) {
|
|
213
|
+
block.inputJson += delta.partial_json;
|
|
214
|
+
block.detail ??= partialToolDetail(block.inputJson);
|
|
215
|
+
onToolInputDelta?.({
|
|
216
|
+
bytes: block.inputJson.length,
|
|
217
|
+
tool: block.name || undefined,
|
|
218
|
+
detail: block.detail,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function partialToolDetail(inputJson) {
|
|
223
|
+
const match = inputJson
|
|
224
|
+
.slice(0, 8192)
|
|
225
|
+
.match(/"(?:path|file_path|command|query|url|description)"\s*:\s*"((?:\\.|[^"\\])*)"/);
|
|
226
|
+
if (!match?.[1])
|
|
227
|
+
return undefined;
|
|
228
|
+
try {
|
|
229
|
+
return JSON.parse(`"${match[1]}"`);
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function mergeUsage(current, part) {
|
|
236
|
+
if (!part || typeof part !== "object")
|
|
237
|
+
return current;
|
|
238
|
+
const source = part;
|
|
239
|
+
const fields = [
|
|
240
|
+
"input_tokens",
|
|
241
|
+
"output_tokens",
|
|
242
|
+
"cache_creation_input_tokens",
|
|
243
|
+
"cache_read_input_tokens",
|
|
244
|
+
];
|
|
245
|
+
if (!fields.some((field) => typeof source[field] === "number"))
|
|
246
|
+
return current;
|
|
247
|
+
const next = current ?? {
|
|
248
|
+
input_tokens: 0,
|
|
249
|
+
output_tokens: 0,
|
|
250
|
+
cache_creation_input_tokens: 0,
|
|
251
|
+
cache_read_input_tokens: 0,
|
|
252
|
+
};
|
|
253
|
+
return fields.reduce((result, field) => {
|
|
254
|
+
const value = source[field];
|
|
255
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
256
|
+
result[field] = value;
|
|
257
|
+
return result;
|
|
258
|
+
}, { ...next });
|
|
259
|
+
}
|
|
260
|
+
function finalizeStreamBlock(block) {
|
|
261
|
+
if (block.type === "text")
|
|
262
|
+
return block;
|
|
263
|
+
let input = {};
|
|
264
|
+
if (block.inputJson.trim()) {
|
|
265
|
+
try {
|
|
266
|
+
input = JSON.parse(block.inputJson);
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
throw new MalformedToolInputError(block.name, block.inputJson.length, error);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
type: "tool_use",
|
|
274
|
+
id: block.id,
|
|
275
|
+
name: block.name,
|
|
276
|
+
input,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AgentConfig, JsonObject } from "../types.js";
|
|
2
|
+
export declare class McpManager {
|
|
3
|
+
private readonly config;
|
|
4
|
+
private readonly connections;
|
|
5
|
+
private readonly pending;
|
|
6
|
+
constructor(config: AgentConfig);
|
|
7
|
+
serverNames(): string[];
|
|
8
|
+
listTools(serverName?: string): Promise<unknown>;
|
|
9
|
+
callTool(serverName: string, toolName: string, args: JsonObject): Promise<string>;
|
|
10
|
+
closeAll(): Promise<void>;
|
|
11
|
+
private client;
|
|
12
|
+
private toTransportConfig;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../src/mcp/manager.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAmB,MAAM,aAAa,CAAC;AAQ5E,qBAAa,UAAU;IAIT,OAAO,CAAC,QAAQ,CAAC,MAAM;IAHnC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAoC;IAChE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsC;gBAEjC,MAAM,EAAE,WAAW;IAEhD,WAAW,IAAI,MAAM,EAAE;IAIjB,SAAS,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAahD,QAAQ,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC;IAMjF,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;YAgBjB,MAAM;IA+BpB,OAAO,CAAC,iBAAiB;CAS1B"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
3
|
+
import { stringify } from "../utils.js";
|
|
4
|
+
export class McpManager {
|
|
5
|
+
config;
|
|
6
|
+
connections = new Map();
|
|
7
|
+
pending = new Map();
|
|
8
|
+
constructor(config) {
|
|
9
|
+
this.config = config;
|
|
10
|
+
}
|
|
11
|
+
serverNames() {
|
|
12
|
+
return Object.keys(this.config.mcpServers);
|
|
13
|
+
}
|
|
14
|
+
async listTools(serverName) {
|
|
15
|
+
if (serverName) {
|
|
16
|
+
const client = await this.client(serverName);
|
|
17
|
+
return { server: serverName, ...(await client.listTools()) };
|
|
18
|
+
}
|
|
19
|
+
const all = [];
|
|
20
|
+
for (const name of this.serverNames()) {
|
|
21
|
+
const client = await this.client(name);
|
|
22
|
+
all.push({ server: name, ...(await client.listTools()) });
|
|
23
|
+
}
|
|
24
|
+
return all;
|
|
25
|
+
}
|
|
26
|
+
async callTool(serverName, toolName, args) {
|
|
27
|
+
const client = await this.client(serverName);
|
|
28
|
+
const result = await client.callTool({ name: toolName, arguments: args });
|
|
29
|
+
return stringify(result, 12000);
|
|
30
|
+
}
|
|
31
|
+
async closeAll() {
|
|
32
|
+
// Wait for any in-flight connects to settle first, so a connection that finishes
|
|
33
|
+
// mid-close still lands in `connections` and gets reaped instead of leaking its child.
|
|
34
|
+
if (this.pending.size)
|
|
35
|
+
await Promise.allSettled(this.pending.values());
|
|
36
|
+
const failures = [];
|
|
37
|
+
for (const [name, { transport }] of this.connections) {
|
|
38
|
+
try {
|
|
39
|
+
await transport.close();
|
|
40
|
+
this.connections.delete(name);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
failures.push(error);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (failures.length)
|
|
47
|
+
throw new AggregateError(failures, "Failed to close one or more MCP transports.");
|
|
48
|
+
}
|
|
49
|
+
async client(name) {
|
|
50
|
+
const existing = this.connections.get(name);
|
|
51
|
+
if (existing)
|
|
52
|
+
return existing.client;
|
|
53
|
+
// Cache the in-flight connect so concurrent callers share one child process
|
|
54
|
+
// instead of each spawning (and leaking) their own.
|
|
55
|
+
const inFlight = this.pending.get(name);
|
|
56
|
+
if (inFlight)
|
|
57
|
+
return inFlight;
|
|
58
|
+
const server = this.config.mcpServers[name];
|
|
59
|
+
if (!server)
|
|
60
|
+
throw new Error(`Unknown MCP server: ${name}`);
|
|
61
|
+
const connect = (async () => {
|
|
62
|
+
const transport = new StdioClientTransport(this.toTransportConfig(server));
|
|
63
|
+
const client = new Client({ name: "langcore-agent", version: "0.1.0" }, { capabilities: {} });
|
|
64
|
+
try {
|
|
65
|
+
await client.connect(transport);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
await transport.close().catch(() => undefined);
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
// Drain stderr so a chatty server can't fill the pipe buffer and hang on write.
|
|
72
|
+
transport.stderr?.resume();
|
|
73
|
+
this.connections.set(name, { client, transport });
|
|
74
|
+
return client;
|
|
75
|
+
})();
|
|
76
|
+
this.pending.set(name, connect);
|
|
77
|
+
try {
|
|
78
|
+
return await connect;
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
this.pending.delete(name);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
toTransportConfig(server) {
|
|
85
|
+
return {
|
|
86
|
+
command: server.command,
|
|
87
|
+
args: server.args,
|
|
88
|
+
env: server.env,
|
|
89
|
+
cwd: server.cwd,
|
|
90
|
+
stderr: "pipe",
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type MemoryNote = {
|
|
2
|
+
id: string;
|
|
3
|
+
title: string;
|
|
4
|
+
content: string;
|
|
5
|
+
tags: string[];
|
|
6
|
+
createdAt: string;
|
|
7
|
+
updatedAt: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function ensureMemoryDir(workspaceRoot: string): Promise<string>;
|
|
10
|
+
export declare function listMemories(workspaceRoot: string): Promise<MemoryNote[]>;
|
|
11
|
+
export declare function saveMemory(workspaceRoot: string, input: {
|
|
12
|
+
title: string;
|
|
13
|
+
content: string;
|
|
14
|
+
tags?: string[];
|
|
15
|
+
id?: string;
|
|
16
|
+
}): Promise<MemoryNote>;
|
|
17
|
+
export declare function searchMemories(workspaceRoot: string, query: string, limit?: number): Promise<MemoryNote[]>;
|
|
18
|
+
export declare function memoryContextBlock(workspaceRoot: string, query: string, limit?: number): Promise<string>;
|
|
19
|
+
//# sourceMappingURL=memdir.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memdir.d.ts","sourceRoot":"","sources":["../../src/memory/memdir.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,UAAU,GAAG;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAMF,wBAAsB,eAAe,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAI5E;AAED,wBAAsB,YAAY,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAmB/E;AAED,wBAAsB,UAAU,CAC9B,aAAa,EAAE,MAAM,EACrB,KAAK,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,GACtE,OAAO,CAAC,UAAU,CAAC,CAuBrB;AAED,wBAAsB,cAAc,CAAC,aAAa,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,SAAI,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAiB3G;AAED,wBAAsB,kBAAkB,CAAC,aAAa,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,SAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAOzG"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory directory (CC memdir-inspired).
|
|
3
|
+
* Stores short durable notes under .apollo/memory/
|
|
4
|
+
*/
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import fs from "node:fs/promises";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { safeRealResolve } from "../utils.js";
|
|
9
|
+
import { writeFileAtomic } from "../session/store.js";
|
|
10
|
+
function memoryDir(workspaceRoot) {
|
|
11
|
+
return path.join(workspaceRoot, ".apollo", "memory");
|
|
12
|
+
}
|
|
13
|
+
export async function ensureMemoryDir(workspaceRoot) {
|
|
14
|
+
const dir = await safeRealResolve(workspaceRoot, memoryDir(workspaceRoot));
|
|
15
|
+
await fs.mkdir(dir, { recursive: true });
|
|
16
|
+
return dir;
|
|
17
|
+
}
|
|
18
|
+
export async function listMemories(workspaceRoot) {
|
|
19
|
+
const dir = await ensureMemoryDir(workspaceRoot);
|
|
20
|
+
let names = [];
|
|
21
|
+
try {
|
|
22
|
+
names = await fs.readdir(dir);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
const notes = [];
|
|
28
|
+
for (const name of names) {
|
|
29
|
+
if (!name.endsWith(".json"))
|
|
30
|
+
continue;
|
|
31
|
+
try {
|
|
32
|
+
const raw = await fs.readFile(await safeRealResolve(dir, path.join(dir, name)), "utf8");
|
|
33
|
+
notes.push(JSON.parse(raw));
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
/* skip */
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return notes.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
40
|
+
}
|
|
41
|
+
export async function saveMemory(workspaceRoot, input) {
|
|
42
|
+
const dir = await ensureMemoryDir(workspaceRoot);
|
|
43
|
+
const now = new Date().toISOString();
|
|
44
|
+
const id = input.id ?? `m-${randomUUID()}`;
|
|
45
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(id))
|
|
46
|
+
throw new Error(`Invalid memory id: ${id}`);
|
|
47
|
+
const file = await safeRealResolve(dir, path.join(dir, `${id}.json`));
|
|
48
|
+
const note = {
|
|
49
|
+
id,
|
|
50
|
+
title: input.title.slice(0, 120),
|
|
51
|
+
content: input.content.slice(0, 20000),
|
|
52
|
+
tags: input.tags ?? [],
|
|
53
|
+
createdAt: now,
|
|
54
|
+
updatedAt: now,
|
|
55
|
+
};
|
|
56
|
+
// preserve createdAt if updating
|
|
57
|
+
try {
|
|
58
|
+
const existing = JSON.parse(await fs.readFile(file, "utf8"));
|
|
59
|
+
note.createdAt = existing.createdAt;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
/* new */
|
|
63
|
+
}
|
|
64
|
+
await writeFileAtomic(file, JSON.stringify(note, null, 2));
|
|
65
|
+
return note;
|
|
66
|
+
}
|
|
67
|
+
export async function searchMemories(workspaceRoot, query, limit = 8) {
|
|
68
|
+
const q = query.toLowerCase().trim();
|
|
69
|
+
const all = await listMemories(workspaceRoot);
|
|
70
|
+
if (!q)
|
|
71
|
+
return all.slice(0, limit);
|
|
72
|
+
const scored = all
|
|
73
|
+
.map((n) => {
|
|
74
|
+
const hay = `${n.title}\n${n.content}\n${n.tags.join(" ")}`.toLowerCase();
|
|
75
|
+
let score = 0;
|
|
76
|
+
for (const term of q.split(/\s+/)) {
|
|
77
|
+
if (hay.includes(term))
|
|
78
|
+
score += 1;
|
|
79
|
+
if (n.title.toLowerCase().includes(term))
|
|
80
|
+
score += 2;
|
|
81
|
+
}
|
|
82
|
+
return { n, score };
|
|
83
|
+
})
|
|
84
|
+
.filter((x) => x.score > 0)
|
|
85
|
+
.sort((a, b) => b.score - a.score);
|
|
86
|
+
return scored.slice(0, limit).map((x) => x.n);
|
|
87
|
+
}
|
|
88
|
+
export async function memoryContextBlock(workspaceRoot, query, limit = 5) {
|
|
89
|
+
const hits = await searchMemories(workspaceRoot, query, limit);
|
|
90
|
+
if (!hits.length)
|
|
91
|
+
return "";
|
|
92
|
+
const body = hits
|
|
93
|
+
.map((n) => `### ${n.title}\n${n.content.slice(0, 1500)}`)
|
|
94
|
+
.join("\n\n");
|
|
95
|
+
return `# Relevant Memories\nThe following notes were retrieved from the project memory store. Use when relevant.\n\n${body}`;
|
|
96
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type PlanState = {
|
|
2
|
+
active: boolean;
|
|
3
|
+
path: string;
|
|
4
|
+
content: string;
|
|
5
|
+
approved: boolean;
|
|
6
|
+
createdAt: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function planFilePath(workspaceRoot: string): string;
|
|
9
|
+
export declare function writePlan(workspaceRoot: string, content: string): Promise<PlanState>;
|
|
10
|
+
export declare function readPlan(workspaceRoot: string): Promise<PlanState | null>;
|
|
11
|
+
export declare function planModeSystemExtra(plan: PlanState | null, mode: "plan" | "normal"): string;
|
|
12
|
+
/** Read-only tools permitted in plan mode. Everything else is denied by default. */
|
|
13
|
+
export declare function isPlanModeToolAllowed(toolName: string, input: Record<string, unknown>, workspaceRoot: string): boolean;
|
|
14
|
+
//# sourceMappingURL=plan-mode.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plan-mode.d.ts","sourceRoot":"","sources":["../../src/plan/plan-mode.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,SAAS,GAAG;IACtB,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,wBAAgB,YAAY,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,wBAAsB,SAAS,CAAC,aAAa,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAY1F;AAED,wBAAsB,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAc/E;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI,EAAE,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,MAAM,CA4B3F;AAED,oFAAoF;AACpF,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAsBtH"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan mode — plan → user confirm → execute (CC EnterPlanMode / ExitPlanMode inspired).
|
|
3
|
+
*/
|
|
4
|
+
import fs from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { safeRealResolve } from "../utils.js";
|
|
7
|
+
export function planFilePath(workspaceRoot) {
|
|
8
|
+
return path.join(workspaceRoot, ".apollo", "plan.md");
|
|
9
|
+
}
|
|
10
|
+
export async function writePlan(workspaceRoot, content) {
|
|
11
|
+
const file = await safeRealResolve(workspaceRoot, planFilePath(workspaceRoot));
|
|
12
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
13
|
+
const body = content.trim() || "# Plan\n\n(empty)\n";
|
|
14
|
+
await fs.writeFile(file, body, "utf8");
|
|
15
|
+
return {
|
|
16
|
+
active: true,
|
|
17
|
+
path: file,
|
|
18
|
+
content: body,
|
|
19
|
+
approved: false,
|
|
20
|
+
createdAt: new Date().toISOString(),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export async function readPlan(workspaceRoot) {
|
|
24
|
+
try {
|
|
25
|
+
const file = await safeRealResolve(workspaceRoot, planFilePath(workspaceRoot));
|
|
26
|
+
const content = await fs.readFile(file, "utf8");
|
|
27
|
+
return {
|
|
28
|
+
active: true,
|
|
29
|
+
path: file,
|
|
30
|
+
content,
|
|
31
|
+
approved: false,
|
|
32
|
+
createdAt: "",
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function planModeSystemExtra(plan, mode) {
|
|
40
|
+
if (mode !== "plan") {
|
|
41
|
+
if (plan?.approved) {
|
|
42
|
+
return `# Plan Execution
|
|
43
|
+
A plan was approved. Implement it carefully. Plan file: ${plan.path}
|
|
44
|
+
|
|
45
|
+
## Approved Plan
|
|
46
|
+
${plan.content}
|
|
47
|
+
|
|
48
|
+
Do not re-enter plan mode unless the user asks. Prefer executing the plan.`;
|
|
49
|
+
}
|
|
50
|
+
return "";
|
|
51
|
+
}
|
|
52
|
+
return `# Plan Mode (ACTIVE)
|
|
53
|
+
You are in **plan mode**. Rules:
|
|
54
|
+
1. Do NOT modify project source files (except the plan file at \`.apollo/plan.md\` via write_file).
|
|
55
|
+
2. Prefer read-only tools: list_files, read_file, git_status, git_diff, web_search, web_fetch, skill_search, skill_read, memory_search, memory_list.
|
|
56
|
+
3. Research thoroughly, then write a clear plan to \`.apollo/plan.md\` using write_file.
|
|
57
|
+
4. Plan structure:
|
|
58
|
+
- Goal
|
|
59
|
+
- Approach
|
|
60
|
+
- Ordered steps
|
|
61
|
+
- Critical files
|
|
62
|
+
- Risks / open questions
|
|
63
|
+
- Verification
|
|
64
|
+
5. When the plan is ready, tell the user to run \`/approve\` to exit plan mode and execute.
|
|
65
|
+
6. Do not claim implementation is done while still in plan mode.`;
|
|
66
|
+
}
|
|
67
|
+
/** Read-only tools permitted in plan mode. Everything else is denied by default. */
|
|
68
|
+
export function isPlanModeToolAllowed(toolName, input, workspaceRoot) {
|
|
69
|
+
if (toolName === "write_file") {
|
|
70
|
+
const p = typeof input.path === "string" ? input.path : "";
|
|
71
|
+
const planRel = ".apollo/plan.md";
|
|
72
|
+
const planAbs = path.join(workspaceRoot, planRel);
|
|
73
|
+
const resolved = path.isAbsolute(p) ? path.normalize(p) : path.normalize(path.join(workspaceRoot, p));
|
|
74
|
+
return resolved === path.normalize(planAbs);
|
|
75
|
+
}
|
|
76
|
+
return new Set([
|
|
77
|
+
"list_files",
|
|
78
|
+
"read_file",
|
|
79
|
+
"web_search",
|
|
80
|
+
"web_fetch",
|
|
81
|
+
"git_status",
|
|
82
|
+
"git_diff",
|
|
83
|
+
"request_human_approval",
|
|
84
|
+
"skill_search",
|
|
85
|
+
"skill_read",
|
|
86
|
+
"memory_search",
|
|
87
|
+
"memory_list",
|
|
88
|
+
"ask_user_question",
|
|
89
|
+
]).has(toolName);
|
|
90
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Same spirit as CLAUDE.md / AGENTS.md discovery. */
|
|
2
|
+
export declare const PROJECT_INSTRUCTION_FILES: readonly ["APOLLO.md", "CLAUDE.md", "AGENTS.md", ".apollo/instructions.md", ".claude/CLAUDE.md"];
|
|
3
|
+
export type ProjectInstructions = {
|
|
4
|
+
path: string;
|
|
5
|
+
content: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function loadProjectInstructions(workspaceRoot: string, maxChars?: number): Promise<ProjectInstructions | null>;
|
|
8
|
+
//# sourceMappingURL=project-instructions.d.ts.map
|