lunel-cli 0.1.119 → 0.1.121

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.
@@ -1,4 +1,4 @@
1
- import type { AIProvider, AiEventEmitter, CodexPromptOptions, AiSyncState, FileAttachment, ModelSelector, MessageInfo, ProviderInfo, SessionInfo, ShareInfo } from "./interface.js";
1
+ import type { AIProvider, AiEventEmitter, CodexPromptOptions, FileAttachment, ModelSelector, MessageInfo, ProviderInfo, SessionInfo, ShareInfo } from "./interface.js";
2
2
  export declare class CodexProvider implements AIProvider {
3
3
  private proc;
4
4
  private shuttingDown;
@@ -36,7 +36,6 @@ export declare class CodexProvider implements AIProvider {
36
36
  getMessages(sessionId: string): Promise<{
37
37
  messages: MessageInfo[];
38
38
  }>;
39
- syncState(sessionIds?: string[]): Promise<AiSyncState>;
40
39
  prompt(sessionId: string, text: string, model?: ModelSelector, agent?: string, files?: FileAttachment[], codexOptions?: CodexPromptOptions): Promise<{
41
40
  ack: true;
42
41
  }>;
@@ -77,6 +76,7 @@ export declare class CodexProvider implements AIProvider {
77
76
  private refreshSessionMetadata;
78
77
  private fetchServerThreadsByArchiveState;
79
78
  private fetchModels;
79
+ private fetchModelById;
80
80
  private resolveModelId;
81
81
  private buildCollaborationMode;
82
82
  private parseThreadListEntry;
package/dist/ai/codex.js CHANGED
@@ -268,80 +268,25 @@ export class CodexProvider {
268
268
  }
269
269
  return { messages: session.messages };
270
270
  }
271
- async syncState(sessionIds = []) {
272
- await this.listSessions().catch(() => ({ sessions: [] }));
273
- const messageSessionIds = new Set(sessionIds);
274
- for (const session of this.sessions.values()) {
275
- if (session.activeTurnId)
276
- messageSessionIds.add(session.id);
277
- }
278
- for (const pending of this.pendingPermissionRequestIds.values()) {
279
- if (pending.sessionId)
280
- messageSessionIds.add(pending.sessionId);
281
- }
282
- for (const pending of this.pendingQuestionRequestIds.values()) {
283
- if (pending.sessionId)
284
- messageSessionIds.add(pending.sessionId);
285
- }
286
- const messages = {};
287
- const statuses = [];
288
- await Promise.allSettled(Array.from(messageSessionIds).map(async (sessionId) => {
289
- const response = await this.getMessages(sessionId);
290
- messages[sessionId] = response.messages;
291
- const session = this.sessions.get(sessionId);
292
- if (!session)
293
- return;
294
- const activeTurnId = await this.resolveInFlightTurnId(sessionId).catch(() => undefined);
295
- session.activeTurnId = activeTurnId;
296
- if (activeTurnId) {
297
- statuses.push({
298
- sessionID: sessionId,
299
- status: { type: "running", activeTurnId },
300
- });
301
- }
302
- }));
303
- return {
304
- sessions: Array.from(this.sessions.values()).map((session) => this.toSessionInfo(session)),
305
- statuses,
306
- messages,
307
- pendingPermissions: Array.from(this.pendingPermissionRequestIds.entries()).map(([id, pending]) => ({
308
- id,
309
- sessionID: pending.sessionId,
310
- messageID: pending.messageId,
311
- callID: pending.callId,
312
- type: pending.method,
313
- title: pending.method,
314
- metadata: { method: pending.method },
315
- })),
316
- pendingQuestions: Array.from(this.pendingQuestionRequestIds.entries()).map(([id, pending]) => ({
317
- id,
318
- sessionID: pending.sessionId,
319
- questions: [],
320
- tool: {
321
- ...(pending.messageId ? { messageID: pending.messageId } : {}),
322
- ...(pending.callId ? { callID: pending.callId } : {}),
323
- },
324
- })),
325
- statusAuthoritative: true,
326
- generatedAt: Date.now(),
327
- };
328
- }
329
271
  async prompt(sessionId, text, model, agent, files = [], codexOptions) {
330
272
  const session = this.ensureLocalSession(sessionId);
331
273
  session.updatedAt = Date.now();
332
274
  (async () => {
333
275
  try {
334
- const effortLevels = ["low", "medium", "high"];
335
- const speedDelta = {
336
- fast: -1,
337
- balanced: 0,
338
- quality: 1,
339
- };
340
- const baseEffort = codexOptions?.reasoningEffort ?? "medium";
341
- const baseIndex = effortLevels.indexOf(baseEffort);
342
- const adjustedIndex = Math.max(0, Math.min(effortLevels.length - 1, baseIndex + (codexOptions?.speed ? speedDelta[codexOptions.speed] : 0)));
343
- const reasoningEffort = effortLevels[adjustedIndex];
344
276
  const modelId = await this.resolveModelId(model);
277
+ const modelInfo = await this.fetchModelById(modelId);
278
+ const effortLevels = modelInfo?.supportedReasoningEfforts.map((effort) => effort.reasoningEffort) ?? [];
279
+ const defaultEffort = modelInfo?.defaultReasoningEffort ?? effortLevels[0] ?? "medium";
280
+ const requestedEffort = codexOptions?.reasoningEffort;
281
+ const reasoningEffort = requestedEffort && effortLevels.includes(requestedEffort)
282
+ ? requestedEffort
283
+ : defaultEffort;
284
+ const requestedSpeedTier = codexOptions?.speed && codexOptions.speed !== "default"
285
+ ? codexOptions.speed
286
+ : undefined;
287
+ const serviceTier = requestedSpeedTier && modelInfo?.additionalSpeedTiers.includes(requestedSpeedTier)
288
+ ? requestedSpeedTier
289
+ : undefined;
345
290
  const collaborationMode = this.buildCollaborationMode(agent, modelId, reasoningEffort);
346
291
  // Freshly created sessions already have a live backend thread from
347
292
  // thread/start. Forcing thread/resume here can attach stale state to a
@@ -354,7 +299,8 @@ export class CodexProvider {
354
299
  threadId: session.id,
355
300
  input: this.makeTurnInputPayload(text, files, imageUrlKey),
356
301
  ...(modelId ? { model: modelId } : {}),
357
- ...(reasoningEffort ? { reasoningEffort } : {}),
302
+ ...(reasoningEffort ? { effort: reasoningEffort } : {}),
303
+ ...(serviceTier ? { serviceTier } : {}),
358
304
  ...(collaborationMode ? { collaborationMode } : {}),
359
305
  });
360
306
  break;
@@ -400,6 +346,9 @@ export class CodexProvider {
400
346
  name: item.displayName || item.model,
401
347
  provider: "codex",
402
348
  description: item.description,
349
+ defaultReasoningEffort: item.defaultReasoningEffort,
350
+ supportedReasoningEfforts: item.supportedReasoningEfforts,
351
+ additionalSpeedTiers: item.additionalSpeedTiers,
403
352
  },
404
353
  ]));
405
354
  const defaultModel = items.find((item) => item.isDefault)?.model;
@@ -1044,16 +993,49 @@ export class CodexProvider {
1044
993
  const displayName = this.readString(obj.displayName)
1045
994
  ?? this.readString(obj.display_name)
1046
995
  ?? model;
996
+ const supportedReasoningEfforts = Array.isArray(obj.supportedReasoningEfforts)
997
+ ? obj.supportedReasoningEfforts
998
+ .map((effort) => {
999
+ const effortObj = this.asRecord(effort);
1000
+ const reasoningEffort = this.readString(effortObj.reasoningEffort)
1001
+ ?? this.readString(effortObj.reasoning_effort);
1002
+ if (!reasoningEffort)
1003
+ return undefined;
1004
+ const description = this.readString(effortObj.description);
1005
+ return {
1006
+ reasoningEffort,
1007
+ ...(description ? { description } : {}),
1008
+ };
1009
+ })
1010
+ .filter((effort) => Boolean(effort))
1011
+ : [];
1012
+ const defaultReasoningEffort = this.readString(obj.defaultReasoningEffort)
1013
+ ?? this.readString(obj.default_reasoning_effort);
1014
+ const additionalSpeedTiers = Array.isArray(obj.additionalSpeedTiers)
1015
+ ? obj.additionalSpeedTiers.filter((tier) => typeof tier === "string" && tier.length > 0)
1016
+ : [];
1047
1017
  return {
1048
1018
  id: this.readString(obj.id) ?? model,
1049
1019
  model,
1050
1020
  displayName,
1051
1021
  description: this.readString(obj.description) ?? "",
1052
1022
  isDefault: Boolean(obj.isDefault ?? obj.is_default),
1023
+ ...(defaultReasoningEffort ? { defaultReasoningEffort } : {}),
1024
+ supportedReasoningEfforts,
1025
+ additionalSpeedTiers,
1053
1026
  };
1054
1027
  })
1055
1028
  .filter((value) => Boolean(value));
1056
1029
  }
1030
+ async fetchModelById(modelId) {
1031
+ const items = await this.fetchModels();
1032
+ if (!modelId) {
1033
+ return items.find((item) => item.isDefault) ?? items[0];
1034
+ }
1035
+ return items.find((item) => item.model === modelId || item.id === modelId)
1036
+ ?? items.find((item) => item.isDefault)
1037
+ ?? items[0];
1038
+ }
1057
1039
  async resolveModelId(model) {
1058
1040
  if (model) {
1059
1041
  return model.providerID === "codex" ? model.modelID : `${model.providerID}/${model.modelID}`;
@@ -14,24 +14,6 @@ export declare class AiManager {
14
14
  backend: AiBackend;
15
15
  }>;
16
16
  }>;
17
- syncState(sessionIds?: Partial<Record<AiBackend, string[]>>): Promise<{
18
- sessions: Array<Record<string, unknown> & {
19
- backend: AiBackend;
20
- }>;
21
- statuses: Array<Record<string, unknown> & {
22
- backend: AiBackend;
23
- }>;
24
- messages: Record<string, unknown>;
25
- pendingPermissions: Array<Record<string, unknown> & {
26
- backend: AiBackend;
27
- }>;
28
- pendingQuestions: Array<Record<string, unknown> & {
29
- backend: AiBackend;
30
- }>;
31
- statusAuthoritativeByBackend: Partial<Record<AiBackend, boolean>>;
32
- syncedBackends: AiBackend[];
33
- generatedAt: number;
34
- }>;
35
17
  createSession(backend: AiBackend, title?: string): Promise<{
36
18
  session: import("./interface.js").SessionInfo;
37
19
  }>;
package/dist/ai/index.js CHANGED
@@ -68,51 +68,6 @@ export class AiManager {
68
68
  const sessions = results.flatMap((r) => (r.status === "fulfilled" ? r.value : []));
69
69
  return { sessions };
70
70
  }
71
- async syncState(sessionIds = {}) {
72
- const results = await Promise.allSettled(this._available.map(async (backend) => {
73
- const provider = this._providers[backend];
74
- const state = provider.syncState
75
- ? await provider.syncState(sessionIds[backend])
76
- : {
77
- sessions: (await provider.listSessions()).sessions,
78
- statuses: [],
79
- messages: {},
80
- generatedAt: Date.now(),
81
- };
82
- return { backend, state };
83
- }));
84
- const sessions = [];
85
- const statuses = [];
86
- const messages = {};
87
- const pendingPermissions = [];
88
- const pendingQuestions = [];
89
- const statusAuthoritativeByBackend = {};
90
- const syncedBackends = [];
91
- for (const result of results) {
92
- if (result.status !== "fulfilled")
93
- continue;
94
- const { backend, state } = result.value;
95
- syncedBackends.push(backend);
96
- statusAuthoritativeByBackend[backend] = state.statusAuthoritative !== false;
97
- sessions.push(...(state.sessions ?? []).map((session) => ({ ...session, backend })));
98
- statuses.push(...(state.statuses ?? []).map((status) => ({ ...status, backend })));
99
- for (const [sessionId, value] of Object.entries(state.messages ?? {})) {
100
- messages[`${backend}:${sessionId}`] = value;
101
- }
102
- pendingPermissions.push(...(state.pendingPermissions ?? []).map((permission) => ({ ...permission, backend })));
103
- pendingQuestions.push(...(state.pendingQuestions ?? []).map((question) => ({ ...question, backend })));
104
- }
105
- return {
106
- sessions,
107
- statuses,
108
- messages,
109
- pendingPermissions,
110
- pendingQuestions,
111
- statusAuthoritativeByBackend,
112
- syncedBackends,
113
- generatedAt: Date.now(),
114
- };
115
- }
116
71
  // Session management — all require explicit backend
117
72
  createSession(backend, title) { return this.get(backend).createSession(title); }
118
73
  getSession(backend, id) { return this.get(backend).getSession(id); }
@@ -8,8 +8,8 @@ export interface ModelSelector {
8
8
  modelID: string;
9
9
  }
10
10
  export interface CodexPromptOptions {
11
- reasoningEffort?: "low" | "medium" | "high";
12
- speed?: "fast" | "balanced" | "quality";
11
+ reasoningEffort?: string;
12
+ speed?: string;
13
13
  permissionMode?: "default" | "full-access";
14
14
  }
15
15
  export interface FileAttachment {
@@ -35,19 +35,6 @@ export interface ProviderInfo {
35
35
  default: Record<string, string>;
36
36
  [key: string]: unknown;
37
37
  }
38
- export interface AiSessionStatus {
39
- sessionID: string;
40
- status: Record<string, unknown> | string;
41
- }
42
- export interface AiSyncState {
43
- sessions: unknown[];
44
- statuses: AiSessionStatus[];
45
- messages: Record<string, MessageInfo[]>;
46
- pendingPermissions?: Record<string, unknown>[];
47
- pendingQuestions?: Record<string, unknown>[];
48
- statusAuthoritative?: boolean;
49
- generatedAt: number;
50
- }
51
38
  /**
52
39
  * Every AI backend (OpenCode, Codex, …) implements this interface.
53
40
  * Method names map 1-to-1 with the "ai" namespace actions in index.ts.
@@ -80,7 +67,6 @@ export interface AIProvider {
80
67
  getMessages(sessionId: string): Promise<{
81
68
  messages: MessageInfo[];
82
69
  }>;
83
- syncState?(sessionIds?: string[]): Promise<AiSyncState>;
84
70
  prompt(sessionId: string, text: string, model?: ModelSelector, agent?: string, files?: FileAttachment[], codexOptions?: CodexPromptOptions): Promise<{
85
71
  ack: true;
86
72
  }>;
@@ -1,4 +1,4 @@
1
- import type { AIProvider, AiEventEmitter, CodexPromptOptions, AiSyncState, FileAttachment, ModelSelector, MessageInfo, ProviderInfo, SessionInfo, ShareInfo } from "./interface.js";
1
+ import type { AIProvider, AiEventEmitter, CodexPromptOptions, FileAttachment, ModelSelector, MessageInfo, ProviderInfo, SessionInfo, ShareInfo } from "./interface.js";
2
2
  export declare class OpenCodeProvider implements AIProvider {
3
3
  private client;
4
4
  private server;
@@ -33,7 +33,6 @@ export declare class OpenCodeProvider implements AIProvider {
33
33
  getMessages(sessionId: string): Promise<{
34
34
  messages: MessageInfo[];
35
35
  }>;
36
- syncState(sessionIds?: string[]): Promise<AiSyncState>;
37
36
  prompt(sessionId: string, text: string, model?: ModelSelector, agent?: string, files?: FileAttachment[], codexOptions?: CodexPromptOptions): Promise<{
38
37
  ack: true;
39
38
  }>;
@@ -58,9 +57,6 @@ export declare class OpenCodeProvider implements AIProvider {
58
57
  private sendPromptAsync;
59
58
  private reconcileOpenCodeState;
60
59
  private refreshBusySessionMessages;
61
- private fetchSessionStatuses;
62
- private listPendingPermissions;
63
- private listPendingQuestions;
64
60
  private refreshSessionsMetadata;
65
61
  private refreshPendingPermissions;
66
62
  private refreshPendingQuestions;
@@ -102,10 +102,19 @@ function normalizeOpenCodePart(part) {
102
102
  });
103
103
  }
104
104
  else if (status === "error") {
105
- normalized.error = readString(state.error) ?? "Tool failed";
106
- const errorMessage = readString(state.error);
107
- if (errorMessage)
108
- normalized.output = errorMessage;
105
+ if (metadata.interrupted === true) {
106
+ normalized.state = "completed";
107
+ normalized.interrupted = true;
108
+ const interruptedOutput = readString(metadata.output);
109
+ if (interruptedOutput)
110
+ normalized.output = interruptedOutput;
111
+ }
112
+ else {
113
+ normalized.error = readString(state.error) ?? "Tool failed";
114
+ const errorMessage = readString(state.error);
115
+ if (errorMessage)
116
+ normalized.output = errorMessage;
117
+ }
109
118
  }
110
119
  const attachments = Array.isArray(state.attachments) ? state.attachments : [];
111
120
  if (attachments.length > 0) {
@@ -393,57 +402,10 @@ export class OpenCodeProvider {
393
402
  throw err;
394
403
  }
395
404
  }
396
- async syncState(sessionIds = []) {
397
- const [sessionsResult, statusesResult, permissionsResult, questionsResult] = await Promise.allSettled([
398
- this.listSessions(),
399
- this.fetchSessionStatuses(),
400
- this.listPendingPermissions(),
401
- this.listPendingQuestions(),
402
- ]);
403
- const sessions = sessionsResult.status === "fulfilled"
404
- ? (sessionsResult.value.sessions ?? [])
405
- : [];
406
- const statuses = statusesResult.status === "fulfilled" ? statusesResult.value : [];
407
- const pendingPermissions = permissionsResult.status === "fulfilled" ? permissionsResult.value : [];
408
- const pendingQuestions = questionsResult.status === "fulfilled" ? questionsResult.value : [];
409
- const messageSessionIds = new Set(sessionIds);
410
- for (const entry of statuses) {
411
- const statusObj = typeof entry.status === "object" && entry.status !== null ? entry.status : {};
412
- const statusType = typeof statusObj.type === "string" ? statusObj.type.toLowerCase() : String(entry.status ?? "").toLowerCase();
413
- if (statusType === "busy" || statusType === "running" || statusType === "working" || statusType === "retry") {
414
- messageSessionIds.add(entry.sessionID);
415
- }
416
- }
417
- for (const permission of pendingPermissions) {
418
- const sessionID = this.readString(permission.sessionID) ?? this.readString(permission.sessionId);
419
- if (sessionID)
420
- messageSessionIds.add(sessionID);
421
- }
422
- for (const question of pendingQuestions) {
423
- const sessionID = this.readString(question.sessionID) ?? this.readString(question.sessionId);
424
- if (sessionID)
425
- messageSessionIds.add(sessionID);
426
- }
427
- const messages = {};
428
- await Promise.allSettled(Array.from(messageSessionIds).map(async (sessionId) => {
429
- const response = await this.getMessages(sessionId);
430
- messages[sessionId] = response.messages;
431
- }));
432
- return {
433
- sessions,
434
- statuses,
435
- messages,
436
- pendingPermissions,
437
- pendingQuestions,
438
- statusAuthoritative: statusesResult.status === "fulfilled",
439
- generatedAt: Date.now(),
440
- };
441
- }
442
405
  // -------------------------------------------------------------------------
443
406
  // Interaction
444
407
  // -------------------------------------------------------------------------
445
408
  async prompt(sessionId, text, model, agent, files = [], codexOptions) {
446
- void codexOptions;
447
409
  if (sessionId)
448
410
  this.lastActiveSessionId = sessionId;
449
411
  if (VERBOSE_AI_LOGS) {
@@ -457,7 +419,7 @@ export class OpenCodeProvider {
457
419
  // Fire-and-forget — results come back through the SSE event stream.
458
420
  // Prefer the async prompt endpoint so long-running turns do not get tied
459
421
  // to the request lifecycle the way the basic prompt route can be.
460
- this.sendPromptAsync(sessionId, text, model, agent, files).catch((err) => {
422
+ this.sendPromptAsync(sessionId, text, model, agent, files, codexOptions).catch((err) => {
461
423
  console.error("[ai] prompt error:", err.message);
462
424
  this.emitter?.({
463
425
  type: "prompt_error",
@@ -644,7 +606,7 @@ export class OpenCodeProvider {
644
606
  }
645
607
  }
646
608
  }
647
- async sendPromptAsync(sessionId, text, model, agent, files = []) {
609
+ async sendPromptAsync(sessionId, text, model, agent, files = [], promptOptions) {
648
610
  const server = this.server;
649
611
  const authHeader = this.authHeader;
650
612
  if (!server || !authHeader) {
@@ -665,6 +627,7 @@ export class OpenCodeProvider {
665
627
  ],
666
628
  ...(model ? { model } : {}),
667
629
  ...(agent ? { agent } : {}),
630
+ ...(promptOptions?.reasoningEffort ? { variant: promptOptions.reasoningEffort } : {}),
668
631
  }),
669
632
  });
670
633
  if (!response.ok) {
@@ -736,43 +699,6 @@ export class OpenCodeProvider {
736
699
  }
737
700
  }
738
701
  }
739
- async fetchSessionStatuses() {
740
- const payload = await this.fetchOpenCodeJson("/session/status", { method: "GET" });
741
- if (!payload || typeof payload !== "object")
742
- return [];
743
- return Object.entries(payload).map(([sessionID, status]) => ({
744
- sessionID,
745
- status: status,
746
- }));
747
- }
748
- async listPendingPermissions() {
749
- const permissionApi = this.client?.permission;
750
- if (!permissionApi?.list)
751
- return [];
752
- const response = await permissionApi.list();
753
- const data = Array.isArray(response.data) ? response.data : [];
754
- return data
755
- .map((entry) => normalizePermissionProperties(this.asRecord(entry)))
756
- .filter((entry) => !!this.readString(entry.id));
757
- }
758
- async listPendingQuestions() {
759
- const data = await this.fetchOpenCodeJson("/question", { method: "GET" });
760
- const questions = Array.isArray(data) ? data : [];
761
- return questions
762
- .flatMap((entry) => {
763
- const question = this.asRecord(entry);
764
- const id = this.readString(question.id);
765
- const sessionID = this.readString(question.sessionID) ?? this.readString(question.sessionId);
766
- if (!id || !sessionID)
767
- return [];
768
- return [{
769
- id,
770
- sessionID,
771
- questions: Array.isArray(question.questions) ? question.questions : [],
772
- tool: typeof question.tool === "object" && question.tool !== null ? question.tool : undefined,
773
- }];
774
- });
775
- }
776
702
  async refreshSessionsMetadata() {
777
703
  const response = await this.client.session.list();
778
704
  const sessions = Array.isArray(response.data) ? response.data : [];
package/dist/index.js CHANGED
@@ -90,13 +90,10 @@ let aiManagerInitPromise = null;
90
90
  // Proxy tunnel management
91
91
  let currentSessionCode = null;
92
92
  let currentSessionPassword = null;
93
- let currentManagerSessionId = null;
94
93
  let currentPrimaryGateway = DEFAULT_PROXY_URL;
95
94
  let activeGatewayUrl = DEFAULT_PROXY_URL;
96
- let appPeerConnected = false;
97
95
  let shuttingDown = false;
98
96
  let activeV2Transport = null;
99
- const runningAiSessions = new Set();
100
97
  const trackedEditorFiles = new Map();
101
98
  const trackedEditorDirectories = new Map();
102
99
  const pendingTrackedFileChecks = new Set();
@@ -305,21 +302,7 @@ async function readCliConfig() {
305
302
  rootDir: entry.rootDir,
306
303
  sessionCode: typeof entry.sessionCode === "string" ? entry.sessionCode : null,
307
304
  sessionPassword: entry.sessionPassword,
308
- managerSessionId: typeof entry.managerSessionId === "string" ? entry.managerSessionId : null,
309
305
  savedAt: entry.savedAt,
310
- pushDevices: Array.isArray(entry.pushDevices)
311
- ? entry.pushDevices.filter((device) => (!!device
312
- && typeof device.phoneId === "string"
313
- && typeof device.notificationsEnabled === "boolean"
314
- && typeof device.updatedAt === "number"
315
- && (typeof device.expoPushToken === "string" || device.expoPushToken === null))).map((device) => ({
316
- phoneId: device.phoneId,
317
- expoPushToken: device.expoPushToken,
318
- notificationsEnabled: device.notificationsEnabled,
319
- platform: typeof device.platform === "string" ? device.platform : null,
320
- updatedAt: device.updatedAt,
321
- }))
322
- : [],
323
306
  }))
324
307
  : [],
325
308
  };
@@ -334,9 +317,7 @@ async function readCliConfig() {
334
317
  }
335
318
  async function writeCliConfig(config) {
336
319
  await fs.mkdir(path.dirname(CLI_CONFIG_PATH), { recursive: true });
337
- const tmpPath = `${CLI_CONFIG_PATH}.${process.pid}.${Date.now()}.tmp`;
338
- await fs.writeFile(tmpPath, JSON.stringify(config, null, 2), "utf-8");
339
- await fs.rename(tmpPath, CLI_CONFIG_PATH);
320
+ await fs.writeFile(CLI_CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
340
321
  cliConfigPromise = Promise.resolve(config);
341
322
  }
342
323
  let cliConfigPromise = null;
@@ -350,17 +331,14 @@ function getSavedSessionForRoot(config, rootDir) {
350
331
  const sessions = Array.isArray(config.sessions) ? config.sessions : [];
351
332
  return sessions.find((entry) => entry.rootDir === rootDir) || null;
352
333
  }
353
- async function saveSessionForRoot(sessionCode, sessionPassword, managerSessionId) {
334
+ async function saveSessionForRoot(sessionCode, sessionPassword) {
354
335
  const config = await getCliConfig();
355
336
  const sessions = Array.isArray(config.sessions) ? [...config.sessions] : [];
356
- const previous = sessions.find((entry) => entry.rootDir === ROOT_DIR);
357
337
  const nextEntry = {
358
338
  rootDir: ROOT_DIR,
359
339
  sessionCode,
360
340
  sessionPassword,
361
- managerSessionId,
362
341
  savedAt: Date.now(),
363
- pushDevices: previous?.pushDevices ?? [],
364
342
  };
365
343
  const deduped = sessions.filter((entry) => entry.rootDir !== ROOT_DIR);
366
344
  deduped.unshift(nextEntry);
@@ -369,192 +347,6 @@ async function saveSessionForRoot(sessionCode, sessionPassword, managerSessionId
369
347
  sessions: deduped.slice(0, 100),
370
348
  });
371
349
  }
372
- async function savePushDeviceForCurrentSession(payload) {
373
- if (!currentSessionPassword) {
374
- throw Object.assign(new Error("No active session"), { code: "EUNAVAILABLE" });
375
- }
376
- const phoneId = typeof payload.phoneId === "string" ? payload.phoneId.trim() : "";
377
- if (!phoneId) {
378
- throw Object.assign(new Error("phoneId is required"), { code: "EINVAL" });
379
- }
380
- const expoPushToken = typeof payload.expoPushToken === "string" && payload.expoPushToken.trim()
381
- ? payload.expoPushToken.trim()
382
- : null;
383
- const notificationsEnabled = payload.notificationsEnabled === true && Boolean(expoPushToken);
384
- const platform = typeof payload.platform === "string" && payload.platform.trim()
385
- ? payload.platform.trim()
386
- : null;
387
- const config = await getCliConfig();
388
- const sessions = Array.isArray(config.sessions) ? [...config.sessions] : [];
389
- const now = Date.now();
390
- const currentEntry = sessions.find((entry) => entry.rootDir === ROOT_DIR) ?? {
391
- rootDir: ROOT_DIR,
392
- sessionCode: currentSessionCode,
393
- sessionPassword: currentSessionPassword,
394
- managerSessionId: currentManagerSessionId,
395
- savedAt: now,
396
- pushDevices: [],
397
- };
398
- const nextDevices = (currentEntry.pushDevices ?? []).filter((device) => device.phoneId !== phoneId);
399
- nextDevices.unshift({
400
- phoneId,
401
- expoPushToken,
402
- notificationsEnabled,
403
- platform,
404
- updatedAt: now,
405
- });
406
- const nextEntry = {
407
- ...currentEntry,
408
- sessionCode: currentSessionCode,
409
- sessionPassword: currentSessionPassword,
410
- managerSessionId: currentManagerSessionId,
411
- savedAt: now,
412
- pushDevices: nextDevices.slice(0, 10),
413
- };
414
- const deduped = sessions.filter((entry) => entry.rootDir !== ROOT_DIR);
415
- deduped.unshift(nextEntry);
416
- await writeCliConfig({
417
- ...config,
418
- sessions: deduped.slice(0, 100),
419
- });
420
- return { ok: true, notificationsEnabled, tokenStored: Boolean(expoPushToken) };
421
- }
422
- function readAiEventSessionId(event) {
423
- const properties = event.properties || {};
424
- const info = properties.info && typeof properties.info === "object" ? properties.info : {};
425
- return ((typeof properties.sessionID === "string" && properties.sessionID)
426
- || (typeof properties.sessionId === "string" && properties.sessionId)
427
- || (typeof info.sessionID === "string" && info.sessionID)
428
- || (typeof info.sessionId === "string" && info.sessionId)
429
- || (typeof info.id === "string" && info.id)
430
- || null);
431
- }
432
- function readAiStatusType(status) {
433
- if (typeof status === "string")
434
- return status.toLowerCase();
435
- if (status && typeof status === "object") {
436
- const value = status.type;
437
- if (typeof value === "string")
438
- return value.toLowerCase();
439
- }
440
- return "";
441
- }
442
- function isRunningAiStatus(status) {
443
- const value = readAiStatusType(status);
444
- return value === "running" || value === "busy" || value === "working" || value === "processing" || value === "retry";
445
- }
446
- function isTerminalAiStatus(status) {
447
- const value = readAiStatusType(status);
448
- return (value === "idle"
449
- || value === "complete"
450
- || value === "completed"
451
- || value === "done"
452
- || value === "finished"
453
- || value === "error"
454
- || value === "failed"
455
- || value === "cancelled"
456
- || value === "canceled");
457
- }
458
- async function getCurrentNotificationDevices() {
459
- const config = await getCliConfig();
460
- const saved = getSavedSessionForRoot(config, ROOT_DIR);
461
- if (!saved || saved.sessionPassword !== currentSessionPassword)
462
- return [];
463
- return (saved.pushDevices ?? []).filter((device) => (device.notificationsEnabled
464
- && typeof device.expoPushToken === "string"
465
- && device.expoPushToken.length > 0));
466
- }
467
- async function clearInvalidNotificationDevices(phoneIds) {
468
- if (phoneIds.length === 0)
469
- return;
470
- const invalidPhoneIds = new Set(phoneIds);
471
- const config = await getCliConfig();
472
- const sessions = Array.isArray(config.sessions) ? [...config.sessions] : [];
473
- const saved = getSavedSessionForRoot(config, ROOT_DIR);
474
- if (!saved?.pushDevices?.length)
475
- return;
476
- const nextEntry = {
477
- ...saved,
478
- pushDevices: saved.pushDevices.map((device) => (invalidPhoneIds.has(device.phoneId)
479
- ? { ...device, expoPushToken: null, notificationsEnabled: false, updatedAt: Date.now() }
480
- : device)),
481
- };
482
- const deduped = sessions.filter((entry) => entry.rootDir !== ROOT_DIR);
483
- deduped.unshift(nextEntry);
484
- await writeCliConfig({
485
- ...config,
486
- sessions: deduped.slice(0, 100),
487
- });
488
- }
489
- async function notifyManagerAiCompletion(backend, aiSessionId) {
490
- if (appPeerConnected)
491
- return;
492
- if (!currentManagerSessionId || !currentSessionPassword)
493
- return;
494
- const devices = await getCurrentNotificationDevices();
495
- if (devices.length === 0)
496
- return;
497
- const response = await fetch(new URL("/v1/notifications/ai-complete", MANAGER_URL), {
498
- method: "POST",
499
- headers: { "Content-Type": "application/json" },
500
- signal: AbortSignal.timeout(10_000),
501
- body: JSON.stringify({
502
- sessionId: currentManagerSessionId,
503
- resumeToken: currentSessionPassword,
504
- backend,
505
- aiSessionId,
506
- devices: devices.map((device) => ({
507
- phoneId: device.phoneId,
508
- expoPushToken: device.expoPushToken,
509
- platform: device.platform,
510
- })),
511
- }),
512
- });
513
- if (!response.ok) {
514
- if (DEBUG_MODE) {
515
- console.warn(`[notifications] manager notify failed: ${response.status}`);
516
- }
517
- return;
518
- }
519
- const body = await response.json().catch(() => ({}));
520
- const invalidPhoneIds = Array.isArray(body.invalidPhoneIds)
521
- ? body.invalidPhoneIds.filter((value) => typeof value === "string")
522
- : [];
523
- if (invalidPhoneIds.length > 0) {
524
- await clearInvalidNotificationDevices(invalidPhoneIds);
525
- }
526
- }
527
- function handleAiNotificationEvent(backend, event) {
528
- const sessionId = readAiEventSessionId(event);
529
- if (!sessionId)
530
- return;
531
- const key = `${backend}:${sessionId}`;
532
- if (event.type === "session.status") {
533
- if (isRunningAiStatus(event.properties.status)) {
534
- runningAiSessions.add(key);
535
- return;
536
- }
537
- if (!isTerminalAiStatus(event.properties.status))
538
- return;
539
- if (!runningAiSessions.delete(key))
540
- return;
541
- void notifyManagerAiCompletion(backend, sessionId).catch((error) => {
542
- if (DEBUG_MODE) {
543
- console.warn("[notifications] failed to notify manager:", error instanceof Error ? error.message : String(error));
544
- }
545
- });
546
- return;
547
- }
548
- if (event.type !== "session.idle")
549
- return;
550
- if (!runningAiSessions.delete(key))
551
- return;
552
- void notifyManagerAiCompletion(backend, sessionId).catch((error) => {
553
- if (DEBUG_MODE) {
554
- console.warn("[notifications] failed to notify manager:", error instanceof Error ? error.message : String(error));
555
- }
556
- });
557
- }
558
350
  async function clearSavedSessionForRoot() {
559
351
  const config = await getCliConfig();
560
352
  const sessions = Array.isArray(config.sessions) ? config.sessions : [];
@@ -1741,7 +1533,6 @@ function handleSystemCapabilities() {
1741
1533
  platform: os.platform(),
1742
1534
  rootDir: ROOT_DIR,
1743
1535
  hostname: os.hostname(),
1744
- managerSessionId: currentManagerSessionId,
1745
1536
  };
1746
1537
  }
1747
1538
  function handleSystemPing() {
@@ -2715,9 +2506,6 @@ async function processMessage(message) {
2715
2506
  case "ping":
2716
2507
  result = handleSystemPing();
2717
2508
  break;
2718
- case "setPushToken":
2719
- result = await savePushDeviceForCurrentSession(payload);
2720
- break;
2721
2509
  case "pairDevice": {
2722
2510
  throw Object.assign(new Error("pairDevice is no longer supported"), { code: "EINVAL" });
2723
2511
  }
@@ -2928,14 +2716,6 @@ async function processMessage(message) {
2928
2716
  case "listSessions":
2929
2717
  result = await aiManager.listAllSessions();
2930
2718
  break;
2931
- case "syncState": {
2932
- const rawSessionIds = payload.sessionIds;
2933
- result = await aiManager.syncState({
2934
- opencode: Array.isArray(rawSessionIds?.opencode) ? rawSessionIds.opencode.filter((id) => typeof id === "string") : undefined,
2935
- codex: Array.isArray(rawSessionIds?.codex) ? rawSessionIds.codex.filter((id) => typeof id === "string") : undefined,
2936
- });
2937
- break;
2938
- }
2939
2719
  case "getSession":
2940
2720
  result = await aiManager.getSession(backend, payload.id);
2941
2721
  break;
@@ -3107,11 +2887,7 @@ async function assembleWithCode(code) {
3107
2887
  return;
3108
2888
  settled = true;
3109
2889
  ws.send(JSON.stringify({ type: "ack" }));
3110
- resolve({
3111
- code: parsed.code,
3112
- password: parsed.password,
3113
- sessionId: typeof parsed.sessionId === "string" ? parsed.sessionId : null,
3114
- });
2890
+ resolve({ code: parsed.code, password: parsed.password });
3115
2891
  }
3116
2892
  catch (error) {
3117
2893
  fail(error instanceof Error ? error : new Error(String(error)));
@@ -3373,7 +3149,6 @@ function startAiManagerInBackground() {
3373
3149
  }
3374
3150
  aiManager = manager;
3375
3151
  aiManager.subscribe((backend, event) => {
3376
- handleAiNotificationEvent(backend, event);
3377
3152
  emitAppEvent({
3378
3153
  v: 1,
3379
3154
  id: `evt-${Date.now()}`,
@@ -3413,19 +3188,16 @@ async function connectWebSocketV2() {
3413
3188
  if (message.type === "connected")
3414
3189
  return;
3415
3190
  if (message.type === "peer_connected") {
3416
- appPeerConnected = true;
3417
3191
  console.log("App connected!\n");
3418
3192
  void publishDiscoveredPorts(true);
3419
3193
  return;
3420
3194
  }
3421
3195
  if (message.type === "peer_disconnected") {
3422
- appPeerConnected = false;
3423
3196
  console.log("App disconnected. Waiting for reconnect window.\n");
3424
3197
  stopPortSync();
3425
3198
  return;
3426
3199
  }
3427
3200
  if (message.type === "app_disconnected") {
3428
- appPeerConnected = false;
3429
3201
  if (message.reconnectDeadline) {
3430
3202
  console.log(`[session] app disconnected, waiting until ${new Date(message.reconnectDeadline).toISOString()}`);
3431
3203
  }
@@ -3530,7 +3302,6 @@ async function main() {
3530
3302
  displaySavedSessionNotice();
3531
3303
  sessionCodeToUse = savedSession.sessionCode;
3532
3304
  sessionPasswordToUse = savedSession.sessionPassword;
3533
- currentManagerSessionId = savedSession.managerSessionId ?? null;
3534
3305
  usedSavedSession = true;
3535
3306
  }
3536
3307
  else {
@@ -3544,8 +3315,7 @@ async function main() {
3544
3315
  const assembled = await assembleWithCode(qr.code);
3545
3316
  sessionCodeToUse = assembled.code;
3546
3317
  sessionPasswordToUse = assembled.password;
3547
- currentManagerSessionId = assembled.sessionId;
3548
- await saveSessionForRoot(sessionCodeToUse, sessionPasswordToUse, currentManagerSessionId);
3318
+ await saveSessionForRoot(sessionCodeToUse, sessionPasswordToUse);
3549
3319
  }
3550
3320
  currentSessionCode = sessionCodeToUse;
3551
3321
  currentSessionPassword = sessionPasswordToUse;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lunel-cli",
3
- "version": "0.1.119",
3
+ "version": "0.1.121",
4
4
  "author": [
5
5
  {
6
6
  "name": "Soham Bharambe",