@akanjs/cli 2.4.0 → 2.4.1-rc.1
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/agent.command-h4afc69n.js +26 -0
- package/application.command-47mj9qsy.js +165 -0
- package/applicationBuildRunner-yz144508.js +284 -0
- package/applicationReleasePackager-brvth6rs.js +245 -0
- package/capacitorApp-y0h6cgft.js +58 -0
- package/cloud.command-qgjxja3n.js +94 -0
- package/commandManifest.json +1 -0
- package/context.command-nqbtak4f.js +82 -0
- package/dependencyScanner-m4x5maek.js +9 -0
- package/guideline.command-ya0dh44f.js +356 -0
- package/incrementalBuilder.proc.js +89 -17394
- package/index-1xdrsbry.js +1447 -0
- package/index-45aj5ry0.js +990 -0
- package/index-5vvwc0cz.js +559 -0
- package/index-61keag0s.js +40 -0
- package/index-6pz1j0zj.js +62 -0
- package/index-73pr2cmy.js +534 -0
- package/index-76rn3g2c.js +76 -0
- package/index-77crfweb.js +1884 -0
- package/index-85msc0wg.js +161 -0
- package/index-8pkbzj26.js +840 -0
- package/index-8rc0bm04.js +514 -0
- package/index-a5rmdgy4.js +4359 -0
- package/index-a6sbyy0b.js +2769 -0
- package/index-fgc8r6dj.js +33 -0
- package/index-h6ca6qg0.js +2777 -0
- package/index-hdqztm58.js +758 -0
- package/index-hwzpw9c1.js +202 -0
- package/index-pmm9e2jf.js +120 -0
- package/index-qaq13qk3.js +80 -0
- package/index-qhtr07v8.js +1072 -0
- package/index-r24hmh0q.js +4 -0
- package/index-sgmas1fc.js +462 -0
- package/index-ss469dec.js +11 -0
- package/index-swf4bmbg.js +25 -0
- package/index-wnp7hwq7.js +193 -0
- package/index-wq8jwx8z.js +224 -0
- package/index-x53a5nya.js +301 -0
- package/index-y3hdhy4p.js +229 -0
- package/index-z9gvz7b0.js +83 -0
- package/index.js +54 -23340
- package/library.command-r15zdqvp.js +33 -0
- package/localRegistry.command-p1pgxw78.js +178 -0
- package/module.command-qrj3kmyz.js +54 -0
- package/package.command-r8sq5kzp.js +43 -0
- package/package.json +2 -2
- package/page.command-c6xdx0xm.js +24 -0
- package/primitive.command-pv9ssmtf.js +62 -0
- package/quality.command-es67wvdp.js +809 -0
- package/repair.command-677675vw.js +67 -0
- package/routeSourceValidator-wbhmbwpj.js +132 -0
- package/scalar.command-kabkd6wd.js +35 -0
- package/templates/facetIndex/index.ts +1 -1
- package/typeChecker-kravn7ns.js +8 -0
- package/typecheck.proc.js +4 -194
- package/workflow.command-64r6cw0w.js +108 -0
- package/workspace.command-xk68sd6c.js +435 -0
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
FileSys
|
|
4
|
+
} from "./index-61keag0s.js";
|
|
5
|
+
import {
|
|
6
|
+
Spinner
|
|
7
|
+
} from "./index-6pz1j0zj.js";
|
|
8
|
+
|
|
9
|
+
// pkgs/@akanjs/devkit/cloud/globalConfig.ts
|
|
10
|
+
import { mkdir } from "fs/promises";
|
|
11
|
+
import dayjs from "dayjs";
|
|
12
|
+
|
|
13
|
+
// pkgs/@akanjs/devkit/cloud/constants.ts
|
|
14
|
+
var basePath = `${Bun.env.HOME ?? Bun.env.USERPROFILE}/.akan`;
|
|
15
|
+
var configPath = `${basePath}/config.json`;
|
|
16
|
+
var getDefaultHostConfig = (host = GlobalConfig.akanCloudHost) => ({ host });
|
|
17
|
+
var defaultAkanGlobalConfig = {
|
|
18
|
+
cloudHost: {},
|
|
19
|
+
remoteEnvServers: {},
|
|
20
|
+
llm: null
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// pkgs/@akanjs/devkit/cloud/globalConfig.ts
|
|
24
|
+
class GlobalConfig {
|
|
25
|
+
static akanCloudHost = process.env.USE_AKANJS_PKGS === "true" ? `http://localhost:${process.env.CLOUD_HOST_PORT ?? 8283}` : "https://cloud.akanjs.com";
|
|
26
|
+
static async#getAkanGlobalConfig() {
|
|
27
|
+
const exists = await FileSys.fileExists(configPath);
|
|
28
|
+
const akanConfig = exists ? await FileSys.readJson(configPath) : {};
|
|
29
|
+
return {
|
|
30
|
+
...defaultAkanGlobalConfig,
|
|
31
|
+
...akanConfig,
|
|
32
|
+
cloudHost: akanConfig.cloudHost ?? defaultAkanGlobalConfig.cloudHost,
|
|
33
|
+
remoteEnvServers: akanConfig.remoteEnvServers ?? defaultAkanGlobalConfig.remoteEnvServers
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
static async#setAkanGlobalConfig(akanConfig) {
|
|
37
|
+
await mkdir(basePath, { recursive: true });
|
|
38
|
+
await Bun.write(configPath, JSON.stringify(akanConfig, null, 2));
|
|
39
|
+
}
|
|
40
|
+
static async getHostConfig(host = GlobalConfig.akanCloudHost) {
|
|
41
|
+
const akanConfig = await GlobalConfig.#getAkanGlobalConfig();
|
|
42
|
+
return GlobalConfig.toHostConfig(akanConfig.cloudHost[host] ?? getDefaultHostConfig(host));
|
|
43
|
+
}
|
|
44
|
+
static async setHostConfig(config = getDefaultHostConfig()) {
|
|
45
|
+
const akanConfig = await GlobalConfig.#getAkanGlobalConfig();
|
|
46
|
+
akanConfig.cloudHost[config.host] = GlobalConfig.toHostConfigDto(config);
|
|
47
|
+
await GlobalConfig.#setAkanGlobalConfig(akanConfig);
|
|
48
|
+
}
|
|
49
|
+
static async getLlmConfig() {
|
|
50
|
+
const akanConfig = await GlobalConfig.#getAkanGlobalConfig();
|
|
51
|
+
return akanConfig.llm ?? null;
|
|
52
|
+
}
|
|
53
|
+
static async setLlmConfig(llmConfig) {
|
|
54
|
+
const akanConfig = await GlobalConfig.#getAkanGlobalConfig();
|
|
55
|
+
await GlobalConfig.#setAkanGlobalConfig({ ...akanConfig, llm: llmConfig });
|
|
56
|
+
}
|
|
57
|
+
static async getRemoteEnvServers() {
|
|
58
|
+
const akanConfig = await GlobalConfig.#getAkanGlobalConfig();
|
|
59
|
+
return akanConfig.remoteEnvServers;
|
|
60
|
+
}
|
|
61
|
+
static async setRemoteEnvServer(name, config) {
|
|
62
|
+
const akanConfig = await GlobalConfig.#getAkanGlobalConfig();
|
|
63
|
+
await GlobalConfig.#setAkanGlobalConfig({
|
|
64
|
+
...akanConfig,
|
|
65
|
+
remoteEnvServers: {
|
|
66
|
+
...akanConfig.remoteEnvServers,
|
|
67
|
+
[name]: config
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
static async removeRemoteEnvServer(name) {
|
|
72
|
+
const akanConfig = await GlobalConfig.#getAkanGlobalConfig();
|
|
73
|
+
const { [name]: _, ...remoteEnvServers } = akanConfig.remoteEnvServers;
|
|
74
|
+
await GlobalConfig.#setAkanGlobalConfig({
|
|
75
|
+
...akanConfig,
|
|
76
|
+
remoteEnvServers
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
static needRefreshToken(accessToken) {
|
|
80
|
+
return !!accessToken?.expiresAt?.isBefore(dayjs().add(1, "hour"));
|
|
81
|
+
}
|
|
82
|
+
static toAccessToken(accessToken) {
|
|
83
|
+
return {
|
|
84
|
+
jwt: accessToken.jwt,
|
|
85
|
+
refreshToken: accessToken.refreshToken ?? null,
|
|
86
|
+
expiresAt: accessToken.expiresAt ? dayjs(accessToken.expiresAt) : null
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
static toAccessTokenDto(accessToken) {
|
|
90
|
+
return {
|
|
91
|
+
jwt: accessToken.jwt,
|
|
92
|
+
refreshToken: accessToken.refreshToken ?? null,
|
|
93
|
+
expiresAt: accessToken.expiresAt?.toString() ?? null
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
static toHostConfigDto(hostConfig) {
|
|
97
|
+
return {
|
|
98
|
+
host: hostConfig.host,
|
|
99
|
+
auth: {
|
|
100
|
+
accessToken: hostConfig.auth?.accessToken ? GlobalConfig.toAccessTokenDto(hostConfig.auth.accessToken) : undefined,
|
|
101
|
+
self: hostConfig.auth?.self
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
static toHostConfig(hostConfigDto) {
|
|
106
|
+
return {
|
|
107
|
+
host: hostConfigDto.host,
|
|
108
|
+
auth: {
|
|
109
|
+
accessToken: hostConfigDto.auth?.accessToken ? GlobalConfig.toAccessToken(hostConfigDto.auth.accessToken) : undefined,
|
|
110
|
+
self: hostConfigDto.auth?.self
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// pkgs/@akanjs/devkit/cloud/cloudApi.ts
|
|
117
|
+
class HttpClient {
|
|
118
|
+
baseUrl;
|
|
119
|
+
headers = {};
|
|
120
|
+
constructor(baseUrl, headers = {}) {
|
|
121
|
+
this.baseUrl = baseUrl;
|
|
122
|
+
this.headers = headers;
|
|
123
|
+
}
|
|
124
|
+
async get(url, { headers } = {}) {
|
|
125
|
+
const response = await fetch(`${this.baseUrl}${url}`, {
|
|
126
|
+
headers: {
|
|
127
|
+
"Content-Type": "application/json",
|
|
128
|
+
...this.headers,
|
|
129
|
+
...headers
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
return await response.json();
|
|
133
|
+
}
|
|
134
|
+
async getFile(url, localPath, headers) {
|
|
135
|
+
const response = await fetch(`${this.baseUrl}${url}`, {
|
|
136
|
+
headers: { ...this.headers, ...headers }
|
|
137
|
+
});
|
|
138
|
+
if (!response.ok)
|
|
139
|
+
throw new Error(`Failed to download file: ${response.status} ${response.statusText}`);
|
|
140
|
+
await Bun.write(localPath, response);
|
|
141
|
+
}
|
|
142
|
+
async post(url, data, { headers } = {}) {
|
|
143
|
+
const isFormData = data instanceof FormData;
|
|
144
|
+
const response = await fetch(`${this.baseUrl}${url}`, {
|
|
145
|
+
method: "POST",
|
|
146
|
+
body: isFormData ? data : JSON.stringify(data),
|
|
147
|
+
headers: isFormData ? { ...this.headers, ...headers } : { "Content-Type": "application/json", ...this.headers, ...headers }
|
|
148
|
+
});
|
|
149
|
+
return await response.json();
|
|
150
|
+
}
|
|
151
|
+
setHeaders(headers) {
|
|
152
|
+
Object.assign(this.headers, headers);
|
|
153
|
+
return this;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
class CloudApi {
|
|
158
|
+
#api;
|
|
159
|
+
#accessToken = null;
|
|
160
|
+
#workspace;
|
|
161
|
+
host;
|
|
162
|
+
url;
|
|
163
|
+
static async fromHost(workspace, host) {
|
|
164
|
+
const hostConfig = await GlobalConfig.getHostConfig(host);
|
|
165
|
+
return new CloudApi(workspace, hostConfig);
|
|
166
|
+
}
|
|
167
|
+
constructor(workspace, hostConfig) {
|
|
168
|
+
this.#workspace = workspace;
|
|
169
|
+
this.#accessToken = hostConfig.auth?.accessToken ?? null;
|
|
170
|
+
this.host = hostConfig.host;
|
|
171
|
+
this.url = `${this.host}/api`;
|
|
172
|
+
this.#api = new HttpClient(this.url);
|
|
173
|
+
if (this.#accessToken && !GlobalConfig.needRefreshToken(this.#accessToken))
|
|
174
|
+
this.#api.setHeaders({
|
|
175
|
+
Authorization: `Bearer ${this.#accessToken.jwt}`
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
async uploadEnv(devProjectId, file) {
|
|
179
|
+
const formData = new FormData;
|
|
180
|
+
formData.append("devProjectId", devProjectId);
|
|
181
|
+
formData.append("file", file);
|
|
182
|
+
const data = await this.#api.post(`/uploadEnv/${devProjectId}`, formData);
|
|
183
|
+
return data;
|
|
184
|
+
}
|
|
185
|
+
async downloadEnv(devProjectId) {
|
|
186
|
+
const localPath = `${this.#workspace.workspaceRoot}/local/env.tar`;
|
|
187
|
+
await this.#api.getFile(`/downloadEnv/${devProjectId}`, localPath);
|
|
188
|
+
return localPath;
|
|
189
|
+
}
|
|
190
|
+
async getRemoteAuthToken(remoteId) {
|
|
191
|
+
try {
|
|
192
|
+
const accessToken = await this.#api.get(`/getRemoteAuthToken/${remoteId}`);
|
|
193
|
+
this.#accessToken = GlobalConfig.toAccessToken(accessToken);
|
|
194
|
+
this.#api.setHeaders({
|
|
195
|
+
Authorization: `Bearer ${this.#accessToken.jwt}`
|
|
196
|
+
});
|
|
197
|
+
return this.#accessToken;
|
|
198
|
+
} catch (_) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async#ensureAccessTokenLive({
|
|
203
|
+
allowUnauthorized = false
|
|
204
|
+
} = {}) {
|
|
205
|
+
if (!this.#accessToken)
|
|
206
|
+
throw new Error("No access token");
|
|
207
|
+
const needRefresh = GlobalConfig.needRefreshToken(this.#accessToken);
|
|
208
|
+
if (!needRefresh)
|
|
209
|
+
return this.#accessToken;
|
|
210
|
+
const refreshToken = this.#accessToken?.refreshToken;
|
|
211
|
+
if (!refreshToken)
|
|
212
|
+
throw new Error("No refresh token");
|
|
213
|
+
return await this.refreshAuthToken(refreshToken);
|
|
214
|
+
}
|
|
215
|
+
async refreshAuthToken(refreshToken) {
|
|
216
|
+
const response = await this.#api.post(`/refreshAuthToken`, { refreshToken });
|
|
217
|
+
this.#accessToken = GlobalConfig.toAccessToken(response);
|
|
218
|
+
this.#api.setHeaders({ Authorization: `Bearer ${this.#accessToken.jwt}` });
|
|
219
|
+
return this.#accessToken;
|
|
220
|
+
}
|
|
221
|
+
async getRemoteSelf() {
|
|
222
|
+
try {
|
|
223
|
+
const data = await this.#api.get(`/getRemoteSelf`);
|
|
224
|
+
return data;
|
|
225
|
+
} catch {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// pkgs/@akanjs/devkit/aiEditor.ts
|
|
231
|
+
import { input, select } from "@inquirer/prompts";
|
|
232
|
+
import {
|
|
233
|
+
AIMessage,
|
|
234
|
+
HumanMessage,
|
|
235
|
+
mapChatMessagesToStoredMessages,
|
|
236
|
+
mapStoredMessagesToChatMessages
|
|
237
|
+
} from "@langchain/core/messages";
|
|
238
|
+
import { ChatDeepSeek } from "@langchain/deepseek";
|
|
239
|
+
import { ChatOpenAI } from "@langchain/openai";
|
|
240
|
+
import { Logger } from "akanjs/common";
|
|
241
|
+
import chalk from "chalk";
|
|
242
|
+
var MAX_ASK_TRY = 300;
|
|
243
|
+
var deepSeekLlmModels = ["deepseek-chat", "deepseek-reasoner"];
|
|
244
|
+
var openAiLlmModels = ["gpt-5.5"];
|
|
245
|
+
var supportedLlmModels = [...deepSeekLlmModels, ...openAiLlmModels];
|
|
246
|
+
var isOpenAiLlmModel = (model) => openAiLlmModels.includes(model);
|
|
247
|
+
var parseTypescriptFileBlocks = (text) => {
|
|
248
|
+
const fileBlocks = [];
|
|
249
|
+
const codeBlockRegex = /```(?:typescript|ts|tsx)\s*\n([\s\S]*?)```/gi;
|
|
250
|
+
const filePathRegex = /^\s*\/\/\s*File:\s*(.+?)\s*$/im;
|
|
251
|
+
for (const codeBlock of text.matchAll(codeBlockRegex)) {
|
|
252
|
+
const content = codeBlock[1]?.trim();
|
|
253
|
+
if (!content)
|
|
254
|
+
continue;
|
|
255
|
+
const filePath = filePathRegex.exec(content)?.[1]?.trim();
|
|
256
|
+
if (!filePath)
|
|
257
|
+
continue;
|
|
258
|
+
fileBlocks.push({
|
|
259
|
+
filePath,
|
|
260
|
+
content: content.replace(filePathRegex, "").trim()
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
return fileBlocks;
|
|
264
|
+
};
|
|
265
|
+
var preserveTypescriptResponseContent = (previousContent, nextContent) => {
|
|
266
|
+
const previousWrites = parseTypescriptFileBlocks(previousContent);
|
|
267
|
+
const nextWrites = parseTypescriptFileBlocks(nextContent);
|
|
268
|
+
if (previousWrites.length > 0 && nextWrites.length === 0)
|
|
269
|
+
return previousContent;
|
|
270
|
+
return nextContent;
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
class AiSession {
|
|
274
|
+
static #cacheDir = "node_modules/.cache/akan/aiSession";
|
|
275
|
+
static #chat = null;
|
|
276
|
+
static async init({ temperature = 0, useExisting = true } = {}) {
|
|
277
|
+
if (useExisting) {
|
|
278
|
+
const llmConfig2 = await AiSession.getLlmConfig();
|
|
279
|
+
if (llmConfig2) {
|
|
280
|
+
AiSession.#setChatModel(llmConfig2.model, llmConfig2.apiKey);
|
|
281
|
+
Logger.rawLog(chalk.dim(`\uD83E\uDD16akan editor uses existing LLM config (${llmConfig2.model})`));
|
|
282
|
+
return AiSession;
|
|
283
|
+
}
|
|
284
|
+
} else
|
|
285
|
+
Logger.rawLog(chalk.yellow("\uD83E\uDD16akan-editor is not initialized. LLM configuration should be set first."));
|
|
286
|
+
const llmConfig = await AiSession.#requestLlmConfig();
|
|
287
|
+
const { model, apiKey } = llmConfig;
|
|
288
|
+
await AiSession.#validateApiKey(model, apiKey);
|
|
289
|
+
const session = AiSession.#setChatModel(model, apiKey, { temperature });
|
|
290
|
+
await session.setLlmConfig({ model, apiKey });
|
|
291
|
+
return session;
|
|
292
|
+
}
|
|
293
|
+
static #setChatModel(model, apiKey, { temperature = 0 } = {}) {
|
|
294
|
+
AiSession.#chat = AiSession.#createChatModel(model, apiKey, {
|
|
295
|
+
temperature,
|
|
296
|
+
streaming: true
|
|
297
|
+
});
|
|
298
|
+
return AiSession;
|
|
299
|
+
}
|
|
300
|
+
static #createChatModel(model, apiKey, { temperature = 0, streaming = false } = {}) {
|
|
301
|
+
if (isOpenAiLlmModel(model))
|
|
302
|
+
return new ChatOpenAI({
|
|
303
|
+
modelName: model,
|
|
304
|
+
temperature,
|
|
305
|
+
streaming,
|
|
306
|
+
openAIApiKey: apiKey
|
|
307
|
+
});
|
|
308
|
+
return new ChatDeepSeek({
|
|
309
|
+
modelName: model,
|
|
310
|
+
temperature,
|
|
311
|
+
streaming,
|
|
312
|
+
apiKey
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
static async getLlmConfig() {
|
|
316
|
+
return await GlobalConfig.getLlmConfig();
|
|
317
|
+
}
|
|
318
|
+
static async setLlmConfig(llmConfig) {
|
|
319
|
+
await GlobalConfig.setLlmConfig(llmConfig);
|
|
320
|
+
return AiSession;
|
|
321
|
+
}
|
|
322
|
+
static async#requestLlmConfig() {
|
|
323
|
+
const model = await select({
|
|
324
|
+
message: "Select a LLM model",
|
|
325
|
+
choices: supportedLlmModels
|
|
326
|
+
});
|
|
327
|
+
const apiKey = await input({ message: "Enter your API key" });
|
|
328
|
+
return { model, apiKey };
|
|
329
|
+
}
|
|
330
|
+
static async#validateApiKey(modelName, apiKey) {
|
|
331
|
+
const spinner = new Spinner("Validating LLM API key...", {
|
|
332
|
+
prefix: `\uD83E\uDD16akan-editor`
|
|
333
|
+
}).start();
|
|
334
|
+
const chat = AiSession.#createChatModel(modelName, apiKey);
|
|
335
|
+
try {
|
|
336
|
+
await chat.invoke("Hi, and just say 'ok'");
|
|
337
|
+
spinner.succeed("LLM API key is valid");
|
|
338
|
+
return true;
|
|
339
|
+
} catch (error) {
|
|
340
|
+
spinner.fail(chalk.red(`LLM API key is invalid. Please check your API key and try again. You can set it again by running "akan set-llm" or reset by running "akan reset-llm"`));
|
|
341
|
+
throw error;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
static async clearCache(workspaceRoot) {
|
|
345
|
+
const cacheDir = `${workspaceRoot}/${AiSession.#cacheDir}`;
|
|
346
|
+
await Bun.$`rm -rf ${cacheDir}`;
|
|
347
|
+
}
|
|
348
|
+
messageHistory = [];
|
|
349
|
+
sessionKey;
|
|
350
|
+
isCacheLoaded = false;
|
|
351
|
+
workspace;
|
|
352
|
+
constructor(type, {
|
|
353
|
+
workspace,
|
|
354
|
+
cacheKey,
|
|
355
|
+
isContinued
|
|
356
|
+
}) {
|
|
357
|
+
this.workspace = workspace;
|
|
358
|
+
this.sessionKey = `${type}${cacheKey ? `-${cacheKey}` : ""}`;
|
|
359
|
+
if (isContinued)
|
|
360
|
+
this.#cacheLoadPromise = this.#loadCache();
|
|
361
|
+
}
|
|
362
|
+
#cacheLoadPromise = null;
|
|
363
|
+
async#loadCache() {
|
|
364
|
+
const cacheFile = `${AiSession.#cacheDir}/${this.sessionKey}.json`;
|
|
365
|
+
const isCacheExists = await this.workspace.exists(cacheFile);
|
|
366
|
+
if (isCacheExists)
|
|
367
|
+
this.messageHistory = mapStoredMessagesToChatMessages(await this.workspace.readJson(cacheFile));
|
|
368
|
+
else
|
|
369
|
+
this.messageHistory = [];
|
|
370
|
+
this.isCacheLoaded = isCacheExists;
|
|
371
|
+
}
|
|
372
|
+
async#saveCache() {
|
|
373
|
+
const cacheFilePath = `${AiSession.#cacheDir}/${this.sessionKey}.json`;
|
|
374
|
+
await this.workspace.writeJson(cacheFilePath, mapChatMessagesToStoredMessages(this.messageHistory));
|
|
375
|
+
}
|
|
376
|
+
async ask(question, {
|
|
377
|
+
onReasoning = (reasoning) => {
|
|
378
|
+
Logger.raw(chalk.dim(reasoning));
|
|
379
|
+
},
|
|
380
|
+
onChunk = (chunk) => {
|
|
381
|
+
Logger.raw(chunk);
|
|
382
|
+
}
|
|
383
|
+
} = {}) {
|
|
384
|
+
if (!AiSession.#chat)
|
|
385
|
+
await AiSession.init();
|
|
386
|
+
if (this.#cacheLoadPromise)
|
|
387
|
+
await this.#cacheLoadPromise;
|
|
388
|
+
if (!AiSession.#chat)
|
|
389
|
+
throw new Error("Failed to initialize the AI session");
|
|
390
|
+
const loader = new Spinner(`${AiSession.#chat.model} is thinking...`, {
|
|
391
|
+
prefix: `\uD83E\uDD16akan-editor`
|
|
392
|
+
}).start();
|
|
393
|
+
try {
|
|
394
|
+
const humanMessage = new HumanMessage(question);
|
|
395
|
+
this.messageHistory.push(humanMessage);
|
|
396
|
+
const stream = await AiSession.#chat.stream(this.messageHistory);
|
|
397
|
+
let reasoningResponse = "", fullResponse = "";
|
|
398
|
+
for await (const chunk of stream) {
|
|
399
|
+
if (loader.isSpinning())
|
|
400
|
+
loader.succeed(`${AiSession.#chat.model} responded`);
|
|
401
|
+
if (!fullResponse.length) {
|
|
402
|
+
const reasoningContent = chunk.additional_kwargs.reasoning_content ?? "";
|
|
403
|
+
if (reasoningContent.length) {
|
|
404
|
+
reasoningResponse += reasoningContent;
|
|
405
|
+
onReasoning(reasoningContent);
|
|
406
|
+
continue;
|
|
407
|
+
} else if (chunk.content.length) {
|
|
408
|
+
reasoningResponse += `
|
|
409
|
+
`;
|
|
410
|
+
onReasoning(reasoningResponse);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
const content = chunk.content;
|
|
414
|
+
if (typeof content === "string") {
|
|
415
|
+
fullResponse += content;
|
|
416
|
+
onChunk(content);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
fullResponse += `
|
|
420
|
+
`;
|
|
421
|
+
onChunk(`
|
|
422
|
+
`);
|
|
423
|
+
this.messageHistory.push(new AIMessage(fullResponse));
|
|
424
|
+
return { content: fullResponse, messageHistory: this.messageHistory };
|
|
425
|
+
} catch {
|
|
426
|
+
loader.fail(`${AiSession.#chat.model} failed to respond`);
|
|
427
|
+
throw new Error("Failed to stream response");
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
async edit(question, { onChunk, onReasoning, maxTry = MAX_ASK_TRY, validate, approve, fallbackToPreviousTypescript } = {}) {
|
|
431
|
+
for (let tryCount = 0;tryCount < maxTry; tryCount++) {
|
|
432
|
+
let response = await this.ask(question, { onChunk, onReasoning });
|
|
433
|
+
if (validate?.length && tryCount === 0) {
|
|
434
|
+
const validateQuestion = `Double check if the response meets the requirements and conditions, and follow the instructions. If not, rewrite it.
|
|
435
|
+
${validate.map((v) => `- ${v}`).join(`
|
|
436
|
+
`)}`;
|
|
437
|
+
const validateResponse = await this.ask(validateQuestion, {
|
|
438
|
+
onChunk,
|
|
439
|
+
onReasoning
|
|
440
|
+
});
|
|
441
|
+
response = {
|
|
442
|
+
...validateResponse,
|
|
443
|
+
content: fallbackToPreviousTypescript ? preserveTypescriptResponseContent(response.content, validateResponse.content) : validateResponse.content
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
const isConfirmed = approve ? true : await select({
|
|
447
|
+
message: "Do you want to edit the response?",
|
|
448
|
+
choices: [
|
|
449
|
+
{ name: "\u2705 Yes, confirm and apply this result", value: true },
|
|
450
|
+
{ name: "\uD83D\uDD04 No, I want to edit it more", value: false }
|
|
451
|
+
]
|
|
452
|
+
});
|
|
453
|
+
if (isConfirmed) {
|
|
454
|
+
await this.#saveCache();
|
|
455
|
+
return response.content;
|
|
456
|
+
}
|
|
457
|
+
question = await input({ message: "What do you want to change?" });
|
|
458
|
+
tryCount++;
|
|
459
|
+
}
|
|
460
|
+
throw new Error("Failed to edit");
|
|
461
|
+
}
|
|
462
|
+
async editTypescript(question, options = {}) {
|
|
463
|
+
const content = await this.edit(question, options);
|
|
464
|
+
return this.#getTypescriptCode(content);
|
|
465
|
+
}
|
|
466
|
+
#getTypescriptCode(content) {
|
|
467
|
+
//! will be deprecated
|
|
468
|
+
const code = /```(typescript|tsx)([\s\S]*?)```/.exec(content);
|
|
469
|
+
return code?.[2] ?? content;
|
|
470
|
+
}
|
|
471
|
+
addToolMessgaes(messages) {
|
|
472
|
+
const toolMessages = messages.map((message) => new HumanMessage(message.content));
|
|
473
|
+
this.messageHistory.push(...toolMessages);
|
|
474
|
+
return this;
|
|
475
|
+
}
|
|
476
|
+
async writeTypescripts(question, executor, options = {}) {
|
|
477
|
+
const content = await this.edit(question, {
|
|
478
|
+
...options,
|
|
479
|
+
fallbackToPreviousTypescript: true
|
|
480
|
+
});
|
|
481
|
+
const writes = this.#getTypescriptCodes(content);
|
|
482
|
+
if (!writes.length)
|
|
483
|
+
throw new Error("No parseable TypeScript file blocks were found in the AI response. Include `// File: <path>` in each code block.");
|
|
484
|
+
for (const write of writes)
|
|
485
|
+
await executor.writeFile(write.filePath, write.content);
|
|
486
|
+
return await this.#tryFixTypescripts(writes, executor, options);
|
|
487
|
+
}
|
|
488
|
+
async#editTypescripts(question, options = {}, fallbackWrites) {
|
|
489
|
+
const content = await this.edit(question, {
|
|
490
|
+
...options,
|
|
491
|
+
fallbackToPreviousTypescript: true
|
|
492
|
+
});
|
|
493
|
+
const writes = this.#getTypescriptCodes(content);
|
|
494
|
+
if (!writes.length && fallbackWrites?.length)
|
|
495
|
+
return fallbackWrites;
|
|
496
|
+
if (!writes.length)
|
|
497
|
+
throw new Error("No parseable TypeScript file blocks were found in the AI response. Include `// File: <path>` in each code block.");
|
|
498
|
+
return writes;
|
|
499
|
+
}
|
|
500
|
+
async#tryFixTypescripts(writes, executor, options = {}) {
|
|
501
|
+
const MAX_EDIT_TRY = 5;
|
|
502
|
+
for (let tryCount = 0;tryCount < MAX_EDIT_TRY; tryCount++) {
|
|
503
|
+
const loader = new Spinner(`Type checking and linting...`, {
|
|
504
|
+
prefix: `\uD83E\uDD16akan-editor`
|
|
505
|
+
}).start();
|
|
506
|
+
const fileChecks = await Promise.all(writes.map(async ({ filePath }) => {
|
|
507
|
+
const lintResult = await executor.lint(filePath, { fix: true });
|
|
508
|
+
const typeCheckResult = await executor.typeCheckAsync(filePath);
|
|
509
|
+
const hasTypeErrors = typeCheckResult.fileErrors.length > 0;
|
|
510
|
+
const hasLintErrors = lintResult.errors.length > 0;
|
|
511
|
+
const needFix = hasTypeErrors || hasLintErrors;
|
|
512
|
+
return { filePath, typeCheckResult, lintResult, needFix };
|
|
513
|
+
}));
|
|
514
|
+
const hasAnyFix = fileChecks.some((fileCheck) => fileCheck.needFix);
|
|
515
|
+
if (hasAnyFix) {
|
|
516
|
+
loader.fail("Type checking and linting has some errors, try to fix them");
|
|
517
|
+
fileChecks.forEach((fileCheck) => {
|
|
518
|
+
Logger.rawLog(`TypeCheck Result
|
|
519
|
+
${fileCheck.typeCheckResult.message}
|
|
520
|
+
Lint Result
|
|
521
|
+
${fileCheck.lintResult.message}`);
|
|
522
|
+
this.addToolMessgaes([
|
|
523
|
+
{ type: "typescript", content: fileCheck.typeCheckResult.message },
|
|
524
|
+
{ type: "eslint", content: fileCheck.lintResult.message }
|
|
525
|
+
]);
|
|
526
|
+
});
|
|
527
|
+
writes = await this.#editTypescripts("Fix the typescript and eslint errors", {
|
|
528
|
+
...options,
|
|
529
|
+
validate: undefined,
|
|
530
|
+
approve: true
|
|
531
|
+
}, writes);
|
|
532
|
+
for (const write of writes)
|
|
533
|
+
await executor.writeFile(write.filePath, write.content);
|
|
534
|
+
} else {
|
|
535
|
+
loader.succeed("Type checking and linting has no errors");
|
|
536
|
+
return writes;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
throw new Error("Failed to create scalar");
|
|
540
|
+
}
|
|
541
|
+
#getTypescriptCodes(text) {
|
|
542
|
+
return parseTypescriptFileBlocks(text);
|
|
543
|
+
}
|
|
544
|
+
async editMarkdown(request, options = {}) {
|
|
545
|
+
const content = await this.edit(request, options);
|
|
546
|
+
return this.#getMarkdownContent(content);
|
|
547
|
+
}
|
|
548
|
+
#getMarkdownContent(text) {
|
|
549
|
+
const searchText = "```markdown";
|
|
550
|
+
const firstIndex = text.indexOf("```markdown");
|
|
551
|
+
const lastIndex = text.lastIndexOf("```");
|
|
552
|
+
if (firstIndex === -1)
|
|
553
|
+
return text;
|
|
554
|
+
else
|
|
555
|
+
return text.slice(firstIndex + searchText.length, lastIndex).trim();
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
export { getDefaultHostConfig, GlobalConfig, CloudApi, AiSession };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// pkgs/@akanjs/devkit/fileSys.ts
|
|
3
|
+
import { stat } from "fs/promises";
|
|
4
|
+
import { Logger } from "akanjs/common";
|
|
5
|
+
|
|
6
|
+
class FileSys {
|
|
7
|
+
static logger = new Logger("FileSys");
|
|
8
|
+
static async fileExists(path) {
|
|
9
|
+
return await Bun.file(path).exists();
|
|
10
|
+
}
|
|
11
|
+
static async dirExists(path) {
|
|
12
|
+
return await stat(path).then((stat2) => stat2.isDirectory()).catch(() => false);
|
|
13
|
+
}
|
|
14
|
+
static async exists(path) {
|
|
15
|
+
return await stat(path).then(() => true).catch(() => false);
|
|
16
|
+
}
|
|
17
|
+
static async readText(path) {
|
|
18
|
+
return await Bun.file(path).text();
|
|
19
|
+
}
|
|
20
|
+
static async readJson(path) {
|
|
21
|
+
try {
|
|
22
|
+
return await Bun.file(path).json();
|
|
23
|
+
} catch (error) {
|
|
24
|
+
FileSys.logger.error(`Failed to read JSON file: ${path}`);
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
static async delete(path) {
|
|
29
|
+
return await Bun.file(path).delete();
|
|
30
|
+
}
|
|
31
|
+
static async writeText(path, content) {
|
|
32
|
+
return await Bun.file(path).write(content);
|
|
33
|
+
}
|
|
34
|
+
static async writeJson(path, content) {
|
|
35
|
+
return await Bun.file(path).write(`${JSON.stringify(content, null, 2)}
|
|
36
|
+
`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export { FileSys };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// pkgs/@akanjs/devkit/spinner.ts
|
|
3
|
+
import ora from "ora";
|
|
4
|
+
|
|
5
|
+
class Spinner {
|
|
6
|
+
static padding = 12;
|
|
7
|
+
spinner;
|
|
8
|
+
stopWatch = null;
|
|
9
|
+
startAt = new Date;
|
|
10
|
+
prefix;
|
|
11
|
+
message;
|
|
12
|
+
enableSpin;
|
|
13
|
+
constructor(message, { prefix = "", indent = 0, enableSpin = true } = {}) {
|
|
14
|
+
Spinner.padding = Math.max(Spinner.padding, prefix.length);
|
|
15
|
+
this.prefix = prefix;
|
|
16
|
+
this.message = message;
|
|
17
|
+
this.spinner = ora(message);
|
|
18
|
+
this.spinner.prefixText = prefix.padStart(Spinner.padding, " ");
|
|
19
|
+
this.spinner.indent = indent;
|
|
20
|
+
this.enableSpin = enableSpin;
|
|
21
|
+
}
|
|
22
|
+
start() {
|
|
23
|
+
this.startAt = new Date;
|
|
24
|
+
if (this.enableSpin) {
|
|
25
|
+
this.spinner.start();
|
|
26
|
+
this.stopWatch = setInterval(() => {
|
|
27
|
+
this.spinner.prefixText = this.prefix.padStart(Spinner.padding, " ");
|
|
28
|
+
this.spinner.text = `${this.message} (${this.#getElapsedTimeStr({ floor: true })})`;
|
|
29
|
+
}, 1000);
|
|
30
|
+
} else
|
|
31
|
+
this.spinner.info();
|
|
32
|
+
return this;
|
|
33
|
+
}
|
|
34
|
+
succeed(message) {
|
|
35
|
+
this.spinner.succeed(`${message} (${this.#getElapsedTimeStr()})`);
|
|
36
|
+
this.#reset();
|
|
37
|
+
}
|
|
38
|
+
fail(message) {
|
|
39
|
+
this.spinner.fail(`${message} (${this.#getElapsedTimeStr()})`);
|
|
40
|
+
this.#reset();
|
|
41
|
+
}
|
|
42
|
+
isSpinning() {
|
|
43
|
+
return this.spinner.isSpinning;
|
|
44
|
+
}
|
|
45
|
+
#reset() {
|
|
46
|
+
if (this.stopWatch)
|
|
47
|
+
clearInterval(this.stopWatch);
|
|
48
|
+
this.stopWatch = null;
|
|
49
|
+
}
|
|
50
|
+
#getElapsedTimeStr({ floor = false } = {}) {
|
|
51
|
+
const ms = Date.now() - this.startAt.getTime();
|
|
52
|
+
if (ms < 1000)
|
|
53
|
+
return `${ms}ms`;
|
|
54
|
+
const s = Math.floor(ms / 1000);
|
|
55
|
+
if (s < 60)
|
|
56
|
+
return `${floor ? Math.floor(ms / 1000) : Math.floor(ms / 100) / 10}s`;
|
|
57
|
+
const m = Math.floor(s / 60);
|
|
58
|
+
return `${m}m ${s % 60}s`;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { Spinner };
|