@akanjs/devkit 2.2.1 → 2.2.2

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/aiEditor.ts CHANGED
@@ -19,11 +19,16 @@ import type { FileContent } from "./types";
19
19
 
20
20
  const MAX_ASK_TRY = 300;
21
21
 
22
- export const supportedLlmModels = [
23
- "deepseek-chat",
24
- "deepseek-reasoner",
25
- ] as const;
22
+ const deepSeekLlmModels = ["deepseek-chat", "deepseek-reasoner"] as const;
23
+
24
+ const openAiLlmModels = ["gpt-5.5"] as const;
25
+
26
+ export const supportedLlmModels = [...deepSeekLlmModels, ...openAiLlmModels] as const;
26
27
  export type SupportedLlmModel = (typeof supportedLlmModels)[number];
28
+ type OpenAiLlmModel = (typeof openAiLlmModels)[number];
29
+
30
+ const isOpenAiLlmModel = (model: SupportedLlmModel): model is OpenAiLlmModel =>
31
+ openAiLlmModels.includes(model as OpenAiLlmModel);
27
32
 
28
33
  interface EditOptions {
29
34
  onReasoning?: (reasoning: string) => void;
@@ -55,44 +60,25 @@ export const parseTypescriptFileBlocks = (text: string): FileContent[] => {
55
60
  return fileBlocks;
56
61
  };
57
62
 
58
- export const preserveTypescriptResponseContent = (
59
- previousContent: string,
60
- nextContent: string,
61
- ) => {
63
+ export const preserveTypescriptResponseContent = (previousContent: string, nextContent: string) => {
62
64
  const previousWrites = parseTypescriptFileBlocks(previousContent);
63
65
  const nextWrites = parseTypescriptFileBlocks(nextContent);
64
- if (previousWrites.length > 0 && nextWrites.length === 0)
65
- return previousContent;
66
+ if (previousWrites.length > 0 && nextWrites.length === 0) return previousContent;
66
67
  return nextContent;
67
68
  };
68
69
 
69
70
  export class AiSession {
70
71
  static #cacheDir = "node_modules/.cache/akan/aiSession";
71
72
  static #chat: ChatDeepSeek | ChatOpenAI | null = null;
72
- static async init({
73
- temperature = 0,
74
- useExisting = true,
75
- }: {
76
- temperature?: number;
77
- useExisting?: boolean;
78
- } = {}) {
73
+ static async init({ temperature = 0, useExisting = true }: { temperature?: number; useExisting?: boolean } = {}) {
79
74
  if (useExisting) {
80
75
  const llmConfig = await AiSession.getLlmConfig();
81
76
  if (llmConfig) {
82
77
  AiSession.#setChatModel(llmConfig.model, llmConfig.apiKey);
83
- Logger.rawLog(
84
- chalk.dim(
85
- `🤖akan editor uses existing LLM config (${llmConfig.model})`,
86
- ),
87
- );
78
+ Logger.rawLog(chalk.dim(`🤖akan editor uses existing LLM config (${llmConfig.model})`));
88
79
  return AiSession;
89
80
  }
90
- } else
91
- Logger.rawLog(
92
- chalk.yellow(
93
- "🤖akan-editor is not initialized. LLM configuration should be set first.",
94
- ),
95
- );
81
+ } else Logger.rawLog(chalk.yellow("🤖akan-editor is not initialized. LLM configuration should be set first."));
96
82
 
97
83
  const llmConfig = await AiSession.#requestLlmConfig();
98
84
  const { model, apiKey } = llmConfig;
@@ -102,26 +88,36 @@ export class AiSession {
102
88
  await session.setLlmConfig({ model, apiKey });
103
89
  return session;
104
90
  }
105
- static #setChatModel(
91
+ static #setChatModel(model: SupportedLlmModel, apiKey: string, { temperature = 0 }: { temperature?: number } = {}) {
92
+ AiSession.#chat = AiSession.#createChatModel(model, apiKey, {
93
+ temperature,
94
+ streaming: true,
95
+ });
96
+ return AiSession;
97
+ }
98
+ static #createChatModel(
106
99
  model: SupportedLlmModel,
107
100
  apiKey: string,
108
- { temperature = 0 }: { temperature?: number } = {},
101
+ { temperature = 0, streaming = false }: { temperature?: number; streaming?: boolean } = {},
109
102
  ) {
110
- AiSession.#chat = new ChatDeepSeek({
103
+ if (isOpenAiLlmModel(model))
104
+ return new ChatOpenAI({
105
+ modelName: model,
106
+ temperature,
107
+ streaming,
108
+ openAIApiKey: apiKey,
109
+ });
110
+ return new ChatDeepSeek({
111
111
  modelName: model,
112
112
  temperature,
113
- streaming: true,
113
+ streaming,
114
114
  apiKey,
115
- // configuration: { baseURL: "https://api.deepseek.com/v1", apiKey },
116
115
  });
117
- return AiSession;
118
116
  }
119
117
  static async getLlmConfig() {
120
118
  return await GlobalConfig.getLlmConfig();
121
119
  }
122
- static async setLlmConfig(
123
- llmConfig: { model: SupportedLlmModel; apiKey: string } | null,
124
- ) {
120
+ static async setLlmConfig(llmConfig: { model: SupportedLlmModel; apiKey: string } | null) {
125
121
  await GlobalConfig.setLlmConfig(llmConfig);
126
122
  return AiSession;
127
123
  }
@@ -137,11 +133,7 @@ export class AiSession {
137
133
  const spinner = new Spinner("Validating LLM API key...", {
138
134
  prefix: `🤖akan-editor`,
139
135
  }).start();
140
- const chat = new ChatOpenAI({
141
- modelName,
142
- temperature: 0,
143
- configuration: { baseURL: "https://api.deepseek.com/v1", apiKey },
144
- });
136
+ const chat = AiSession.#createChatModel(modelName, apiKey);
145
137
  try {
146
138
  await chat.invoke("Hi, and just say 'ok'");
147
139
  spinner.succeed("LLM API key is valid");
@@ -193,10 +185,7 @@ export class AiSession {
193
185
  }
194
186
  async #saveCache() {
195
187
  const cacheFilePath = `${AiSession.#cacheDir}/${this.sessionKey}.json`;
196
- await this.workspace.writeJson(
197
- cacheFilePath,
198
- mapChatMessagesToStoredMessages(this.messageHistory),
199
- );
188
+ await this.workspace.writeJson(cacheFilePath, mapChatMessagesToStoredMessages(this.messageHistory));
200
189
  }
201
190
  async ask(
202
191
  question: string,
@@ -212,8 +201,7 @@ export class AiSession {
212
201
  if (!AiSession.#chat) await AiSession.init();
213
202
  if (this.#cacheLoadPromise) await this.#cacheLoadPromise;
214
203
 
215
- if (!AiSession.#chat)
216
- throw new Error("Failed to initialize the AI session");
204
+ if (!AiSession.#chat) throw new Error("Failed to initialize the AI session");
217
205
  const loader = new Spinner(`${AiSession.#chat.model} is thinking...`, {
218
206
  prefix: `🤖akan-editor`,
219
207
  }).start();
@@ -224,13 +212,10 @@ export class AiSession {
224
212
  let reasoningResponse = "",
225
213
  fullResponse = "";
226
214
  for await (const chunk of stream) {
227
- if (loader.isSpinning())
228
- loader.succeed(`${AiSession.#chat.model} responded`);
215
+ if (loader.isSpinning()) loader.succeed(`${AiSession.#chat.model} responded`);
229
216
 
230
217
  if (!fullResponse.length) {
231
- const reasoningContent =
232
- (chunk.additional_kwargs as { reasoning_content?: string })
233
- .reasoning_content ?? "";
218
+ const reasoningContent = (chunk.additional_kwargs as { reasoning_content?: string }).reasoning_content ?? "";
234
219
  if (reasoningContent.length) {
235
220
  reasoningResponse += reasoningContent;
236
221
  onReasoning(reasoningContent);
@@ -258,14 +243,7 @@ export class AiSession {
258
243
  }
259
244
  async edit(
260
245
  question: string,
261
- {
262
- onChunk,
263
- onReasoning,
264
- maxTry = MAX_ASK_TRY,
265
- validate,
266
- approve,
267
- fallbackToPreviousTypescript,
268
- }: EditOptions = {},
246
+ { onChunk, onReasoning, maxTry = MAX_ASK_TRY, validate, approve, fallbackToPreviousTypescript }: EditOptions = {},
269
247
  ) {
270
248
  for (let tryCount = 0; tryCount < maxTry; tryCount++) {
271
249
  let response = await this.ask(question, { onChunk, onReasoning });
@@ -279,10 +257,7 @@ ${validate.map((v) => `- ${v}`).join("\n")}`;
279
257
  response = {
280
258
  ...validateResponse,
281
259
  content: fallbackToPreviousTypescript
282
- ? preserveTypescriptResponseContent(
283
- response.content,
284
- validateResponse.content,
285
- )
260
+ ? preserveTypescriptResponseContent(response.content, validateResponse.content)
286
261
  : validateResponse.content,
287
262
  };
288
263
  }
@@ -320,17 +295,11 @@ ${validate.map((v) => `- ${v}`).join("\n")}`;
320
295
  // const toolMessages = messages.map(
321
296
  // (message) => new ToolMessage({ content: message.content, tool_call_id: message.type })
322
297
  // );
323
- const toolMessages = messages.map(
324
- (message) => new HumanMessage(message.content),
325
- );
298
+ const toolMessages = messages.map((message) => new HumanMessage(message.content));
326
299
  this.messageHistory.push(...toolMessages);
327
300
  return this;
328
301
  }
329
- async writeTypescripts(
330
- question: string,
331
- executor: Executor,
332
- options: EditOptions = {},
333
- ) {
302
+ async writeTypescripts(question: string, executor: Executor, options: EditOptions = {}) {
334
303
  const content = await this.edit(question, {
335
304
  ...options,
336
305
  fallbackToPreviousTypescript: true,
@@ -340,15 +309,10 @@ ${validate.map((v) => `- ${v}`).join("\n")}`;
340
309
  throw new Error(
341
310
  "No parseable TypeScript file blocks were found in the AI response. Include `// File: <path>` in each code block.",
342
311
  );
343
- for (const write of writes)
344
- await executor.writeFile(write.filePath, write.content);
312
+ for (const write of writes) await executor.writeFile(write.filePath, write.content);
345
313
  return await this.#tryFixTypescripts(writes, executor, options);
346
314
  }
347
- async #editTypescripts(
348
- question: string,
349
- options: EditOptions = {},
350
- fallbackWrites?: FileContent[],
351
- ) {
315
+ async #editTypescripts(question: string, options: EditOptions = {}, fallbackWrites?: FileContent[]) {
352
316
  const content = await this.edit(question, {
353
317
  ...options,
354
318
  fallbackToPreviousTypescript: true,
@@ -361,11 +325,7 @@ ${validate.map((v) => `- ${v}`).join("\n")}`;
361
325
  );
362
326
  return writes;
363
327
  }
364
- async #tryFixTypescripts(
365
- writes: FileContent[],
366
- executor: Executor,
367
- options: EditOptions = {},
368
- ) {
328
+ async #tryFixTypescripts(writes: FileContent[], executor: Executor, options: EditOptions = {}) {
369
329
  const MAX_EDIT_TRY = 5;
370
330
  for (let tryCount = 0; tryCount < MAX_EDIT_TRY; tryCount++) {
371
331
  const loader = new Spinner(`Type checking and linting...`, {
@@ -383,9 +343,7 @@ ${validate.map((v) => `- ${v}`).join("\n")}`;
383
343
  );
384
344
  const hasAnyFix = fileChecks.some((fileCheck) => fileCheck.needFix);
385
345
  if (hasAnyFix) {
386
- loader.fail(
387
- "Type checking and linting has some errors, try to fix them",
388
- );
346
+ loader.fail("Type checking and linting has some errors, try to fix them");
389
347
  fileChecks.forEach((fileCheck) => {
390
348
  Logger.rawLog(
391
349
  `TypeCheck Result \n${fileCheck.typeCheckResult.message}\nLint Result \n${fileCheck.lintResult.message}`,
@@ -404,8 +362,7 @@ ${validate.map((v) => `- ${v}`).join("\n")}`;
404
362
  },
405
363
  writes,
406
364
  );
407
- for (const write of writes)
408
- await executor.writeFile(write.filePath, write.content);
365
+ for (const write of writes) await executor.writeFile(write.filePath, write.content);
409
366
  } else {
410
367
  loader.succeed("Type checking and linting has no errors");
411
368
  return writes;
package/cloud/cloudApi.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Workspace } from "../commandDecorators";
2
- import { type AccessToken, type AccessTokenDto, akanCloudHost, type HostConfig } from "./constants";
2
+ import type { AccessToken, AccessTokenDto, HostConfig } from "./constants";
3
3
  import { GlobalConfig } from "./globalConfig";
4
4
 
5
5
  class HttpClient {
@@ -17,7 +17,7 @@ class HttpClient {
17
17
  ...headers,
18
18
  },
19
19
  });
20
- return response.json();
20
+ return await response.json();
21
21
  }
22
22
  async getFile(url: string, localPath: string, headers?: Record<string, string>): Promise<void> {
23
23
  const response = await fetch(`${this.baseUrl}${url}`, {
@@ -35,7 +35,7 @@ class HttpClient {
35
35
  ? { ...this.headers, ...headers }
36
36
  : { "Content-Type": "application/json", ...this.headers, ...headers },
37
37
  });
38
- return response.json();
38
+ return await response.json();
39
39
  }
40
40
  setHeaders(headers: Record<string, string>) {
41
41
  Object.assign(this.headers, headers);
@@ -47,6 +47,8 @@ export class CloudApi {
47
47
  readonly #api: HttpClient;
48
48
  #accessToken: AccessToken | null = null;
49
49
  #workspace: Workspace;
50
+ host: string;
51
+ url: string;
50
52
 
51
53
  static async fromHost(workspace: Workspace, host?: string) {
52
54
  const hostConfig = await GlobalConfig.getHostConfig(host);
@@ -54,9 +56,10 @@ export class CloudApi {
54
56
  }
55
57
  constructor(workspace: Workspace, hostConfig: HostConfig) {
56
58
  this.#workspace = workspace;
57
- const host = akanCloudHost;
58
- this.#api = new HttpClient(`${host}/api`);
59
59
  this.#accessToken = hostConfig.auth?.accessToken ?? null;
60
+ this.host = hostConfig.host;
61
+ this.url = `${this.host}/api`;
62
+ this.#api = new HttpClient(this.url);
60
63
  if (this.#accessToken && !GlobalConfig.needRefreshToken(this.#accessToken))
61
64
  this.#api.setHeaders({
62
65
  Authorization: `Bearer ${this.#accessToken.jwt}`,
@@ -64,6 +67,7 @@ export class CloudApi {
64
67
  }
65
68
 
66
69
  async uploadEnv(devProjectId: string, file: File): Promise<boolean> {
70
+ await this.#ensureAccessTokenLive();
67
71
  const formData = new FormData();
68
72
  formData.append("devProjectId", devProjectId);
69
73
  formData.append("file", file);
@@ -71,16 +75,13 @@ export class CloudApi {
71
75
  return data;
72
76
  }
73
77
  async downloadEnv(devProjectId: string): Promise<unknown> {
78
+ await this.#ensureAccessTokenLive();
74
79
  const localPath = `${this.#workspace.workspaceRoot}/local/env.tar`;
75
80
  await this.#api.getFile(`/downloadEnv/${devProjectId}`, localPath);
76
81
  return localPath;
77
82
  }
78
83
  async getRemoteAuthToken(remoteId: string): Promise<AccessToken | null> {
79
84
  try {
80
- if (this.#accessToken) {
81
- if (GlobalConfig.needRefreshToken(this.#accessToken)) return await this.#refreshAuthToken();
82
- else return await this.#refreshAuthToken();
83
- }
84
85
  const accessToken = await this.#api.get<AccessTokenDto>(`/getRemoteAuthToken/${remoteId}`);
85
86
  this.#accessToken = GlobalConfig.toAccessToken(accessToken);
86
87
  this.#api.setHeaders({
@@ -91,13 +92,20 @@ export class CloudApi {
91
92
  return null;
92
93
  }
93
94
  }
94
- async #refreshAuthToken(): Promise<AccessToken> {
95
+ async #ensureAccessTokenLive({
96
+ allowUnauthorized = false,
97
+ }: {
98
+ allowUnauthorized?: boolean;
99
+ } = {}): Promise<AccessToken> {
100
+ if (!this.#accessToken) throw new Error("No access token");
101
+ const needRefresh = GlobalConfig.needRefreshToken(this.#accessToken);
102
+ if (!needRefresh) return this.#accessToken;
95
103
  const refreshToken = this.#accessToken?.refreshToken;
96
104
  if (!refreshToken) throw new Error("No refresh token");
97
105
  return await this.refreshAuthToken(refreshToken);
98
106
  }
99
107
  async refreshAuthToken(refreshToken: string): Promise<AccessToken> {
100
- const response = await this.#api.post<AccessTokenDto>(`/refreshRemoteAuthToken`, { refreshToken });
108
+ const response = await this.#api.post<AccessTokenDto>(`/refreshAuthToken`, { refreshToken });
101
109
  this.#accessToken = GlobalConfig.toAccessToken(response);
102
110
  this.#api.setHeaders({ Authorization: `Bearer ${this.#accessToken.jwt}` });
103
111
  return this.#accessToken;
@@ -1,24 +1,25 @@
1
1
  import type { Dayjs } from "dayjs";
2
+ import { GlobalConfig } from "..";
2
3
  import type { SupportedLlmModel } from "../aiEditor";
3
4
 
4
5
  export const basePath = `${Bun.env.HOME ?? Bun.env.USERPROFILE}/.akan`;
5
6
  export const configPath = `${basePath}/config.json`;
6
- export const akanCloudHost = process.env.USE_AKANJS_PKGS === "true" ? "http://localhost" : "https://cloud.akanjs.com";
7
- export const akanCloudUrl = `${akanCloudHost}${process.env.USE_AKANJS_PKGS === "true" ? ":8282" : ""}/api`;
8
7
 
9
8
  export interface HostConfig {
9
+ host: string;
10
10
  auth?: {
11
11
  accessToken?: AccessToken;
12
12
  self?: { id: string; nickname: string };
13
13
  };
14
14
  }
15
15
  export interface HostConfigDto {
16
+ host: string;
16
17
  auth?: {
17
18
  accessToken?: AccessTokenDto;
18
19
  self?: { id: string; nickname: string };
19
20
  };
20
21
  }
21
- export const defaultHostConfig: HostConfig = {};
22
+ export const getDefaultHostConfig = (host = GlobalConfig.akanCloudHost): HostConfig => ({ host });
22
23
  export interface RemoteEnvServerConfig {
23
24
  host: string;
24
25
  username?: string;
@@ -5,17 +5,20 @@ import {
5
5
  type AccessToken,
6
6
  type AccessTokenDto,
7
7
  type AkanGlobalConfig,
8
- akanCloudHost,
9
8
  basePath,
10
9
  configPath,
11
10
  defaultAkanGlobalConfig,
12
- defaultHostConfig,
11
+ getDefaultHostConfig,
13
12
  type HostConfig,
14
13
  type HostConfigDto,
15
14
  type RemoteEnvServerConfig,
16
15
  } from "./constants";
17
16
 
18
17
  export class GlobalConfig {
18
+ static akanCloudHost =
19
+ process.env.USE_AKANJS_PKGS === "true"
20
+ ? `http://localhost:${process.env.CLOUD_HOST_PORT ?? 8283}`
21
+ : "https://cloud.akanjs.com";
19
22
  static async #getAkanGlobalConfig(): Promise<AkanGlobalConfig> {
20
23
  const exists = await FileSys.fileExists(configPath);
21
24
  const akanConfig = exists ? await FileSys.readJson<Partial<AkanGlobalConfig>>(configPath) : {};
@@ -30,13 +33,13 @@ export class GlobalConfig {
30
33
  await mkdir(basePath, { recursive: true });
31
34
  await Bun.write(configPath, JSON.stringify(akanConfig, null, 2));
32
35
  }
33
- static async getHostConfig(host = akanCloudHost): Promise<HostConfig> {
36
+ static async getHostConfig(host = GlobalConfig.akanCloudHost): Promise<HostConfig> {
34
37
  const akanConfig = await GlobalConfig.#getAkanGlobalConfig();
35
- return GlobalConfig.toHostConfig(akanConfig.cloudHost[host] ?? defaultHostConfig);
38
+ return GlobalConfig.toHostConfig(akanConfig.cloudHost[host] ?? getDefaultHostConfig(host));
36
39
  }
37
- static async setHostConfig(host = akanCloudHost, config: HostConfig = {}) {
40
+ static async setHostConfig(config: HostConfig = getDefaultHostConfig()) {
38
41
  const akanConfig = await GlobalConfig.#getAkanGlobalConfig();
39
- akanConfig.cloudHost[host] = GlobalConfig.toHostConfigDto(config);
42
+ akanConfig.cloudHost[config.host] = GlobalConfig.toHostConfigDto(config);
40
43
  await GlobalConfig.#setAkanGlobalConfig(akanConfig);
41
44
  }
42
45
  static async getLlmConfig(): Promise<AkanGlobalConfig["llm"]> {
@@ -88,6 +91,7 @@ export class GlobalConfig {
88
91
  }
89
92
  static toHostConfigDto(hostConfig: HostConfig): HostConfigDto {
90
93
  return {
94
+ host: hostConfig.host,
91
95
  auth: {
92
96
  accessToken: hostConfig.auth?.accessToken
93
97
  ? GlobalConfig.toAccessTokenDto(hostConfig.auth.accessToken)
@@ -98,6 +102,7 @@ export class GlobalConfig {
98
102
  }
99
103
  static toHostConfig(hostConfigDto: HostConfigDto): HostConfig {
100
104
  return {
105
+ host: hostConfigDto.host,
101
106
  auth: {
102
107
  accessToken: hostConfigDto.auth?.accessToken
103
108
  ? GlobalConfig.toAccessToken(hostConfigDto.auth.accessToken)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akanjs/devkit",
3
- "version": "2.2.1",
3
+ "version": "2.2.2",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -32,7 +32,7 @@
32
32
  "@langchain/openai": "^1.4.6",
33
33
  "@tailwindcss/node": "^4.3.0",
34
34
  "@trapezedev/project": "^7.1.4",
35
- "akanjs": "2.2.1",
35
+ "akanjs": "2.2.2",
36
36
  "chalk": "^5.6.2",
37
37
  "commander": "^14.0.3",
38
38
  "daisyui": "^5.5.20",