lunel-cli 0.1.80 → 0.1.82

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.
@@ -6,6 +6,7 @@ export declare class CodexProvider implements AIProvider {
6
6
  private nextId;
7
7
  private pending;
8
8
  private sessions;
9
+ private deletedThreadIds;
9
10
  private resumedThreadIds;
10
11
  private pendingPermissionRequestIds;
11
12
  private assistantMessageIdByTurnId;
@@ -22,7 +23,9 @@ export declare class CodexProvider implements AIProvider {
22
23
  getSession(id: string): Promise<{
23
24
  session: SessionInfo;
24
25
  }>;
25
- deleteSession(id: string): Promise<Record<string, never>>;
26
+ deleteSession(id: string): Promise<{
27
+ deleted: boolean;
28
+ }>;
26
29
  getMessages(sessionId: string): Promise<{
27
30
  messages: MessageInfo[];
28
31
  }>;
package/dist/ai/codex.js CHANGED
@@ -13,6 +13,7 @@ export class CodexProvider {
13
13
  nextId = 1;
14
14
  pending = new Map();
15
15
  sessions = new Map();
16
+ deletedThreadIds = new Set();
16
17
  resumedThreadIds = new Set();
17
18
  pendingPermissionRequestIds = new Map();
18
19
  assistantMessageIdByTurnId = new Map();
@@ -81,6 +82,8 @@ export class CodexProvider {
81
82
  }
82
83
  this.reconcileSessionsWithServer(activeThreads, archivedThreads);
83
84
  const sessions = Array.from(this.sessions.values())
85
+ .filter((session) => !session.archived)
86
+ .filter((session) => !this.deletedThreadIds.has(session.id))
84
87
  .filter((session) => this.belongsToCurrentRoot(session))
85
88
  .sort((a, b) => a.updatedAt - b.updatedAt)
86
89
  .map((session) => this.toSessionInfo(session));
@@ -99,8 +102,21 @@ export class CodexProvider {
99
102
  return { session: this.toSessionInfo(next) };
100
103
  }
101
104
  async deleteSession(id) {
105
+ const session = this.sessions.get(id);
106
+ this.deletedThreadIds.add(id);
102
107
  this.sessions.delete(id);
103
- return {};
108
+ this.resumedThreadIds.delete(id);
109
+ try {
110
+ const params = { threadId: id };
111
+ if (session?.cwd) {
112
+ params.cwd = session.cwd;
113
+ }
114
+ await this.call("thread/archive", params);
115
+ }
116
+ catch {
117
+ // Match Remodex behavior: delete is optimistic locally, archive is best effort.
118
+ }
119
+ return { deleted: true };
104
120
  }
105
121
  async getMessages(sessionId) {
106
122
  const session = this.ensureLocalSession(sessionId);
@@ -783,10 +799,14 @@ export class CodexProvider {
783
799
  const localSessions = this.sessions;
784
800
  const merged = new Map();
785
801
  for (const thread of activeThreads) {
802
+ if (this.deletedThreadIds.has(thread.id))
803
+ continue;
786
804
  const session = this.mergeSession(localSessions.get(thread.id), { ...thread, archived: false });
787
805
  merged.set(thread.id, session);
788
806
  }
789
807
  for (const thread of archivedThreads) {
808
+ if (this.deletedThreadIds.has(thread.id))
809
+ continue;
790
810
  if (merged.has(thread.id))
791
811
  continue;
792
812
  const session = this.mergeSession(localSessions.get(thread.id), { ...thread, archived: true });
@@ -20,7 +20,9 @@ export declare class AiManager {
20
20
  getSession(backend: AiBackend, id: string): Promise<{
21
21
  session: import("./interface.js").SessionInfo;
22
22
  }>;
23
- deleteSession(backend: AiBackend, id: string): Promise<Record<string, never>>;
23
+ deleteSession(backend: AiBackend, id: string): Promise<{
24
+ deleted: boolean;
25
+ }>;
24
26
  getMessages(backend: AiBackend, sessionId: string): Promise<{
25
27
  messages: import("./interface.js").MessageInfo[];
26
28
  }>;
@@ -53,7 +53,9 @@ export interface AIProvider {
53
53
  getSession(id: string): Promise<{
54
54
  session: SessionInfo;
55
55
  }>;
56
- deleteSession(id: string): Promise<Record<string, never>>;
56
+ deleteSession(id: string): Promise<{
57
+ deleted: boolean;
58
+ }>;
57
59
  getMessages(sessionId: string): Promise<{
58
60
  messages: MessageInfo[];
59
61
  }>;
@@ -21,7 +21,9 @@ export declare class OpenCodeProvider implements AIProvider {
21
21
  getSession(id: string): Promise<{
22
22
  session: SessionInfo;
23
23
  }>;
24
- deleteSession(id: string): Promise<Record<string, never>>;
24
+ deleteSession(id: string): Promise<{
25
+ deleted: boolean;
26
+ }>;
25
27
  getMessages(sessionId: string): Promise<{
26
28
  messages: MessageInfo[];
27
29
  }>;
@@ -108,9 +108,7 @@ export class OpenCodeProvider {
108
108
  }
109
109
  async deleteSession(id) {
110
110
  const response = await this.client.session.delete({ path: { id } });
111
- if (response.error)
112
- throw new Error(JSON.stringify(response.error));
113
- return {};
111
+ return { deleted: Boolean(requireData(response, "session.delete")) };
114
112
  }
115
113
  // -------------------------------------------------------------------------
116
114
  // Messages
package/dist/index.js CHANGED
@@ -216,6 +216,7 @@ function hasFlag(args, flag) {
216
216
  }
217
217
  const EXTRA_PORTS = parseExtraPortsFromArgs(CLI_ARGS);
218
218
  const USE_APPLE_REVIEW_CODE = hasFlag(CLI_ARGS, "--abcd-code");
219
+ const FORCE_NEW_CODE = hasFlag(CLI_ARGS, "--new-code");
219
220
  const SCAN_PORTS = Array.from(new Set([...DEV_PORTS, ...EXTRA_PORTS])).sort((a, b) => a - b);
220
221
  function samePortSet(a, b) {
221
222
  if (a.length !== b.length)
@@ -303,18 +304,31 @@ async function readCliConfig() {
303
304
  return {
304
305
  version: 1,
305
306
  deviceId: typeof parsed.deviceId === "string" && parsed.deviceId ? parsed.deviceId : generatePersistentSecret(32),
307
+ sessions: Array.isArray(parsed.sessions)
308
+ ? parsed.sessions.filter((entry) => (!!entry
309
+ && typeof entry.rootDir === "string"
310
+ && typeof entry.sessionPassword === "string"
311
+ && typeof entry.savedAt === "number")).map((entry) => ({
312
+ rootDir: entry.rootDir,
313
+ sessionCode: typeof entry.sessionCode === "string" ? entry.sessionCode : null,
314
+ sessionPassword: entry.sessionPassword,
315
+ savedAt: entry.savedAt,
316
+ }))
317
+ : [],
306
318
  };
307
319
  }
308
320
  catch {
309
321
  return {
310
322
  version: 1,
311
323
  deviceId: generatePersistentSecret(32),
324
+ sessions: [],
312
325
  };
313
326
  }
314
327
  }
315
328
  async function writeCliConfig(config) {
316
329
  await fs.mkdir(path.dirname(CLI_CONFIG_PATH), { recursive: true });
317
330
  await fs.writeFile(CLI_CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
331
+ cliConfigPromise = Promise.resolve(config);
318
332
  }
319
333
  let cliConfigPromise = null;
320
334
  async function getCliConfig() {
@@ -323,6 +337,34 @@ async function getCliConfig() {
323
337
  }
324
338
  return await cliConfigPromise;
325
339
  }
340
+ function getSavedSessionForRoot(config, rootDir) {
341
+ const sessions = Array.isArray(config.sessions) ? config.sessions : [];
342
+ return sessions.find((entry) => entry.rootDir === rootDir) || null;
343
+ }
344
+ async function saveSessionForRoot(sessionCode, sessionPassword) {
345
+ const config = await getCliConfig();
346
+ const sessions = Array.isArray(config.sessions) ? [...config.sessions] : [];
347
+ const nextEntry = {
348
+ rootDir: ROOT_DIR,
349
+ sessionCode,
350
+ sessionPassword,
351
+ savedAt: Date.now(),
352
+ };
353
+ const deduped = sessions.filter((entry) => entry.rootDir !== ROOT_DIR);
354
+ deduped.unshift(nextEntry);
355
+ await writeCliConfig({
356
+ ...config,
357
+ sessions: deduped.slice(0, 100),
358
+ });
359
+ }
360
+ async function clearSavedSessionForRoot() {
361
+ const config = await getCliConfig();
362
+ const sessions = Array.isArray(config.sessions) ? config.sessions : [];
363
+ await writeCliConfig({
364
+ ...config,
365
+ sessions: sessions.filter((entry) => entry.rootDir !== ROOT_DIR),
366
+ });
367
+ }
326
368
  // ============================================================================
327
369
  // File System Handlers
328
370
  // ============================================================================
@@ -2596,7 +2638,20 @@ async function getAssignedProxyUrl(password) {
2596
2638
  url.searchParams.set("password", password);
2597
2639
  const response = await fetch(url);
2598
2640
  if (!response.ok) {
2599
- throw new Error(`Failed to get proxy from manager: ${response.status}`);
2641
+ let message = `Failed to get proxy from manager: ${response.status}`;
2642
+ try {
2643
+ const payload = await response.json();
2644
+ if (payload.error) {
2645
+ message = payload.error;
2646
+ }
2647
+ else if (payload.reason) {
2648
+ message = payload.reason;
2649
+ }
2650
+ }
2651
+ catch {
2652
+ // ignore parse failures and use the fallback message
2653
+ }
2654
+ throw new Error(message);
2600
2655
  }
2601
2656
  const payload = await response.json();
2602
2657
  if (typeof payload.proxyUrl !== "string" || !payload.proxyUrl) {
@@ -2604,6 +2659,30 @@ async function getAssignedProxyUrl(password) {
2604
2659
  }
2605
2660
  return normalizeGatewayUrl(payload.proxyUrl);
2606
2661
  }
2662
+ async function revokePassword(password, reason = "revoked by cli --new-code") {
2663
+ const response = await fetch(new URL("/v2/revoke", MANAGER_URL), {
2664
+ method: "POST",
2665
+ headers: { "Content-Type": "application/json" },
2666
+ body: JSON.stringify({ password, reason }),
2667
+ });
2668
+ if (response.ok || response.status === 404) {
2669
+ return;
2670
+ }
2671
+ let message = `Failed to revoke previous session: ${response.status}`;
2672
+ try {
2673
+ const payload = await response.json();
2674
+ if (payload.error) {
2675
+ message = payload.error;
2676
+ }
2677
+ else if (payload.reason) {
2678
+ message = payload.reason;
2679
+ }
2680
+ }
2681
+ catch {
2682
+ // ignore parse failures and use the fallback message
2683
+ }
2684
+ throw new Error(message);
2685
+ }
2607
2686
  function displayQR(code) {
2608
2687
  console.log("\n");
2609
2688
  qrcode.generate(code, { small: true }, (qr) => {
@@ -2875,8 +2954,10 @@ async function main() {
2875
2954
  if (EXTRA_PORTS.length > 0) {
2876
2955
  console.log(`Extra ports enabled: ${EXTRA_PORTS.join(", ")}`);
2877
2956
  }
2957
+ let usedSavedSession = false;
2878
2958
  try {
2879
- await getCliConfig();
2959
+ const cliConfig = await getCliConfig();
2960
+ const savedSession = getSavedSessionForRoot(cliConfig, ROOT_DIR);
2880
2961
  console.log("Checking PTY runtime...");
2881
2962
  await ensurePtyBinaryReady();
2882
2963
  console.log("PTY runtime ready.\n");
@@ -2895,27 +2976,48 @@ async function main() {
2895
2976
  checkDataChannelBackpressure();
2896
2977
  }
2897
2978
  });
2898
- let codeToAssemble;
2979
+ let sessionCodeToUse = null;
2980
+ let sessionPasswordToUse;
2899
2981
  if (USE_APPLE_REVIEW_CODE) {
2900
- codeToAssemble = APPLE_REVIEW_CODE;
2901
- currentSessionCode = codeToAssemble;
2902
2982
  console.log(`Using fixed review code: ${APPLE_REVIEW_CODE}`);
2983
+ const assembled = await assembleWithCode(APPLE_REVIEW_CODE);
2984
+ sessionCodeToUse = assembled.code;
2985
+ sessionPasswordToUse = assembled.password;
2986
+ await saveSessionForRoot(sessionCodeToUse, sessionPasswordToUse);
2987
+ }
2988
+ else if (!FORCE_NEW_CODE && savedSession) {
2989
+ console.log(`Using saved session for ${ROOT_DIR}`);
2990
+ sessionCodeToUse = savedSession.sessionCode;
2991
+ sessionPasswordToUse = savedSession.sessionPassword;
2992
+ usedSavedSession = true;
2903
2993
  }
2904
2994
  else {
2995
+ if (FORCE_NEW_CODE && savedSession?.sessionPassword) {
2996
+ await revokePassword(savedSession.sessionPassword);
2997
+ await clearSavedSessionForRoot();
2998
+ }
2905
2999
  const qr = await createQrCode();
2906
- codeToAssemble = qr.code;
2907
3000
  currentSessionCode = qr.code;
2908
3001
  displayQR(qr.code);
3002
+ const assembled = await assembleWithCode(qr.code);
3003
+ sessionCodeToUse = assembled.code;
3004
+ sessionPasswordToUse = assembled.password;
3005
+ await saveSessionForRoot(sessionCodeToUse, sessionPasswordToUse);
2909
3006
  }
2910
- const assembled = await assembleWithCode(codeToAssemble);
2911
3007
  resetReplayBuffer();
2912
- currentSessionCode = assembled.code;
2913
- currentSessionPassword = assembled.password;
2914
- currentPrimaryGateway = await getAssignedProxyUrl(assembled.password);
3008
+ currentSessionCode = sessionCodeToUse;
3009
+ currentSessionPassword = sessionPasswordToUse;
3010
+ currentPrimaryGateway = await getAssignedProxyUrl(sessionPasswordToUse);
2915
3011
  activeGatewayUrl = currentPrimaryGateway;
2916
3012
  await connectWebSocket();
2917
3013
  }
2918
3014
  catch (error) {
3015
+ const message = error instanceof Error ? error.message : String(error);
3016
+ if (!USE_APPLE_REVIEW_CODE &&
3017
+ usedSavedSession &&
3018
+ /invalid|revoked|not found|expired|password invalid|password revoked/i.test(message)) {
3019
+ await clearSavedSessionForRoot().catch(() => { });
3020
+ }
2919
3021
  if (error instanceof Error) {
2920
3022
  console.error(`Error: ${error.message}`);
2921
3023
  if (error.stack)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lunel-cli",
3
- "version": "0.1.80",
3
+ "version": "0.1.82",
4
4
  "author": [
5
5
  {
6
6
  "name": "Soham Bharambe",