lsd-pi 1.1.4 → 1.1.6

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.
Files changed (175) hide show
  1. package/README.md +2 -1
  2. package/dist/headless-ui.js +2 -0
  3. package/dist/onboarding.js +11 -8
  4. package/dist/resources/extensions/async-jobs/async-bash-tool.js +14 -0
  5. package/dist/resources/extensions/async-jobs/await-tool.js +14 -0
  6. package/dist/resources/extensions/async-jobs/cancel-job-tool.js +7 -0
  7. package/dist/resources/extensions/cache-timer/index.js +5 -0
  8. package/dist/resources/extensions/codex-rotate/IMPLEMENTATION.md +18 -13
  9. package/dist/resources/extensions/codex-rotate/README.md +9 -3
  10. package/dist/resources/extensions/codex-rotate/commands.js +15 -8
  11. package/dist/resources/extensions/codex-rotate/index.js +17 -8
  12. package/dist/resources/extensions/memory/auto-extract.js +196 -80
  13. package/dist/resources/extensions/memory/dream.js +86 -19
  14. package/dist/resources/extensions/shared/rtk.js +89 -87
  15. package/dist/resources/extensions/subagent/index.js +33 -7
  16. package/dist/startup-model-validation.js +12 -2
  17. package/dist/update-check.js +2 -2
  18. package/dist/update-cmd.js +3 -3
  19. package/dist/welcome-screen.js +43 -14
  20. package/package.json +3 -2
  21. package/packages/pi-coding-agent/dist/core/agent-session.clear-queue.test.d.ts +2 -0
  22. package/packages/pi-coding-agent/dist/core/agent-session.clear-queue.test.d.ts.map +1 -0
  23. package/packages/pi-coding-agent/dist/core/agent-session.clear-queue.test.js +46 -0
  24. package/packages/pi-coding-agent/dist/core/agent-session.clear-queue.test.js.map +1 -0
  25. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +8 -0
  26. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  27. package/packages/pi-coding-agent/dist/core/agent-session.js +43 -4
  28. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  29. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +3 -1
  30. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  31. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  32. package/packages/pi-coding-agent/dist/core/keybindings.d.ts +1 -1
  33. package/packages/pi-coding-agent/dist/core/keybindings.d.ts.map +1 -1
  34. package/packages/pi-coding-agent/dist/core/keybindings.js +2 -0
  35. package/packages/pi-coding-agent/dist/core/keybindings.js.map +1 -1
  36. package/packages/pi-coding-agent/dist/core/pty-executor.d.ts +48 -0
  37. package/packages/pi-coding-agent/dist/core/pty-executor.d.ts.map +1 -0
  38. package/packages/pi-coding-agent/dist/core/pty-executor.js +173 -0
  39. package/packages/pi-coding-agent/dist/core/pty-executor.js.map +1 -0
  40. package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
  41. package/packages/pi-coding-agent/dist/core/sdk.js +16 -3
  42. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  43. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +9 -0
  44. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  45. package/packages/pi-coding-agent/dist/core/settings-manager.js +18 -0
  46. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  47. package/packages/pi-coding-agent/dist/core/tool-approval.d.ts.map +1 -1
  48. package/packages/pi-coding-agent/dist/core/tool-approval.js +2 -2
  49. package/packages/pi-coding-agent/dist/core/tool-approval.js.map +1 -1
  50. package/packages/pi-coding-agent/dist/core/tools/index.d.ts +7 -0
  51. package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  52. package/packages/pi-coding-agent/dist/core/tools/index.js +23 -2
  53. package/packages/pi-coding-agent/dist/core/tools/index.js.map +1 -1
  54. package/packages/pi-coding-agent/dist/core/tools/pty.d.ts +50 -0
  55. package/packages/pi-coding-agent/dist/core/tools/pty.d.ts.map +1 -0
  56. package/packages/pi-coding-agent/dist/core/tools/pty.js +289 -0
  57. package/packages/pi-coding-agent/dist/core/tools/pty.js.map +1 -0
  58. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts +3 -1
  59. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  60. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js +36 -22
  61. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
  62. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.d.ts +3 -5
  63. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  64. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.js +23 -62
  65. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.js.map +1 -1
  66. package/packages/pi-coding-agent/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
  67. package/packages/pi-coding-agent/dist/modes/interactive/components/branch-summary-message.js +1 -4
  68. package/packages/pi-coding-agent/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
  69. package/packages/pi-coding-agent/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
  70. package/packages/pi-coding-agent/dist/modes/interactive/components/compaction-summary-message.js +1 -4
  71. package/packages/pi-coding-agent/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
  72. package/packages/pi-coding-agent/dist/modes/interactive/components/embedded-terminal.d.ts +39 -0
  73. package/packages/pi-coding-agent/dist/modes/interactive/components/embedded-terminal.d.ts.map +1 -0
  74. package/packages/pi-coding-agent/dist/modes/interactive/components/embedded-terminal.js +182 -0
  75. package/packages/pi-coding-agent/dist/modes/interactive/components/embedded-terminal.js.map +1 -0
  76. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +6 -0
  77. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  78. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +36 -0
  79. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
  80. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
  81. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js +2 -4
  82. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
  83. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +1 -2
  84. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  85. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +106 -77
  86. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  87. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.d.ts +2 -5
  88. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  89. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.js +4 -13
  90. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.js.map +1 -1
  91. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts +11 -0
  92. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  93. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +49 -13
  94. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  95. package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.d.ts.map +1 -1
  96. package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.js +1 -0
  97. package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.js.map +1 -1
  98. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts.map +1 -1
  99. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js +2 -0
  100. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js.map +1 -1
  101. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts +3 -0
  102. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts.map +1 -1
  103. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.js.map +1 -1
  104. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +27 -0
  105. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  106. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +251 -39
  107. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  108. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js +2 -2
  109. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js.map +1 -1
  110. package/packages/pi-coding-agent/dist/utils/terminal-screen.d.ts +10 -0
  111. package/packages/pi-coding-agent/dist/utils/terminal-screen.d.ts.map +1 -0
  112. package/packages/pi-coding-agent/dist/utils/terminal-screen.js +67 -0
  113. package/packages/pi-coding-agent/dist/utils/terminal-screen.js.map +1 -0
  114. package/packages/pi-coding-agent/dist/utils/terminal-serializer.d.ts +7 -0
  115. package/packages/pi-coding-agent/dist/utils/terminal-serializer.d.ts.map +1 -0
  116. package/packages/pi-coding-agent/dist/utils/terminal-serializer.js +67 -0
  117. package/packages/pi-coding-agent/dist/utils/terminal-serializer.js.map +1 -0
  118. package/packages/pi-coding-agent/package.json +9 -4
  119. package/packages/pi-coding-agent/src/core/agent-session.clear-queue.test.ts +50 -0
  120. package/packages/pi-coding-agent/src/core/agent-session.ts +50 -4
  121. package/packages/pi-coding-agent/src/core/extensions/types.ts +1 -1
  122. package/packages/pi-coding-agent/src/core/keybindings.ts +4 -1
  123. package/packages/pi-coding-agent/src/core/pty-executor.ts +229 -0
  124. package/packages/pi-coding-agent/src/core/sdk.ts +16 -3
  125. package/packages/pi-coding-agent/src/core/settings-manager.ts +27 -0
  126. package/packages/pi-coding-agent/src/core/tool-approval.ts +2 -2
  127. package/packages/pi-coding-agent/src/core/tools/index.ts +35 -2
  128. package/packages/pi-coding-agent/src/core/tools/pty.ts +354 -0
  129. package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +37 -24
  130. package/packages/pi-coding-agent/src/modes/interactive/components/bash-execution.ts +22 -70
  131. package/packages/pi-coding-agent/src/modes/interactive/components/branch-summary-message.ts +1 -3
  132. package/packages/pi-coding-agent/src/modes/interactive/components/compaction-summary-message.ts +1 -3
  133. package/packages/pi-coding-agent/src/modes/interactive/components/embedded-terminal.ts +224 -0
  134. package/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +45 -0
  135. package/packages/pi-coding-agent/src/modes/interactive/components/skill-invocation-message.ts +2 -3
  136. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +104 -81
  137. package/packages/pi-coding-agent/src/modes/interactive/components/user-message.ts +5 -19
  138. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +55 -13
  139. package/packages/pi-coding-agent/src/modes/interactive/controllers/extension-ui-controller.ts +1 -0
  140. package/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.ts +2 -0
  141. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode-state.ts +3 -0
  142. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +296 -48
  143. package/packages/pi-coding-agent/src/modes/interactive/theme/themes.ts +2 -2
  144. package/packages/pi-coding-agent/src/utils/terminal-screen.ts +77 -0
  145. package/packages/pi-coding-agent/src/utils/terminal-serializer.ts +72 -0
  146. package/packages/pi-tui/dist/components/__tests__/editor-dropped-image.test.d.ts +2 -0
  147. package/packages/pi-tui/dist/components/__tests__/editor-dropped-image.test.d.ts.map +1 -0
  148. package/packages/pi-tui/dist/components/__tests__/editor-dropped-image.test.js +105 -0
  149. package/packages/pi-tui/dist/components/__tests__/editor-dropped-image.test.js.map +1 -0
  150. package/packages/pi-tui/dist/components/editor.d.ts +4 -0
  151. package/packages/pi-tui/dist/components/editor.d.ts.map +1 -1
  152. package/packages/pi-tui/dist/components/editor.js +57 -3
  153. package/packages/pi-tui/dist/components/editor.js.map +1 -1
  154. package/packages/pi-tui/dist/components/loader.d.ts +26 -6
  155. package/packages/pi-tui/dist/components/loader.d.ts.map +1 -1
  156. package/packages/pi-tui/dist/components/loader.js +178 -18
  157. package/packages/pi-tui/dist/components/loader.js.map +1 -1
  158. package/packages/pi-tui/src/components/editor.ts +65 -3
  159. package/packages/pi-tui/src/components/loader.ts +196 -19
  160. package/pkg/dist/modes/interactive/theme/themes.js +2 -2
  161. package/pkg/dist/modes/interactive/theme/themes.js.map +1 -1
  162. package/pkg/package.json +1 -1
  163. package/src/resources/extensions/async-jobs/async-bash-tool.ts +13 -0
  164. package/src/resources/extensions/async-jobs/await-tool.ts +13 -0
  165. package/src/resources/extensions/async-jobs/cancel-job-tool.ts +8 -0
  166. package/src/resources/extensions/cache-timer/index.ts +102 -96
  167. package/src/resources/extensions/codex-rotate/IMPLEMENTATION.md +18 -13
  168. package/src/resources/extensions/codex-rotate/README.md +9 -3
  169. package/src/resources/extensions/codex-rotate/commands.ts +335 -329
  170. package/src/resources/extensions/codex-rotate/index.ts +85 -75
  171. package/src/resources/extensions/memory/auto-extract.ts +330 -204
  172. package/src/resources/extensions/memory/dream.ts +88 -21
  173. package/src/resources/extensions/memory/tests/auto-extract.test.ts +200 -144
  174. package/src/resources/extensions/shared/rtk.js +112 -0
  175. package/src/resources/extensions/subagent/index.ts +35 -6
@@ -2,361 +2,367 @@
2
2
  * /codex slash command handlers
3
3
  */
4
4
 
5
- import type { ExtensionAPI } from "@gsd/pi-coding-agent";
5
+ import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent";
6
6
  import type { CodexAccount } from "./types.js";
7
7
  import {
8
- addAccount,
9
- getAccountByEmail,
10
- getAccountById,
11
- getAllAccounts,
12
- removeAccount,
13
- updateAccount,
14
- markAccountUsed,
8
+ addAccount,
9
+ getAccountByEmail,
10
+ getAccountById,
11
+ getAllAccounts,
12
+ removeAccount,
13
+ updateAccount,
15
14
  } from "./accounts.js";
16
15
  import { performOAuthLogin, refreshAccountToken, importFromExistingCodexAuth, importFromCockpit } from "./oauth.js";
17
- import { syncAccountsToAuth, removeCodexFromAuth } from "./sync.js";
18
- import { PROVIDER_NAME } from "./config.js";
16
+ import { syncAccountsToAuth } from "./sync.js";
19
17
  import { logCodexRotateError } from "./logger.js";
20
18
 
21
19
  /**
22
20
  * Format a timestamp for display
23
21
  */
24
22
  function formatTimestamp(timestamp?: number): string {
25
- if (!timestamp) return "never";
26
- const date = new Date(timestamp);
27
- const now = new Date();
28
- const diffMs = now.getTime() - date.getTime();
29
- const diffMins = Math.floor(diffMs / 60000);
30
-
31
- if (diffMins < 1) return "just now";
32
- if (diffMins < 60) return `${diffMins}m ago`;
33
- const diffHours = Math.floor(diffMins / 60);
34
- if (diffHours < 24) return `${diffHours}h ago`;
35
- return date.toLocaleDateString();
23
+ if (!timestamp) return "never";
24
+ const date = new Date(timestamp);
25
+ const now = new Date();
26
+ const diffMs = now.getTime() - date.getTime();
27
+ const diffMins = Math.floor(diffMs / 60000);
28
+
29
+ if (diffMins < 1) return "just now";
30
+ if (diffMins < 60) return `${diffMins}m ago`;
31
+ const diffHours = Math.floor(diffMins / 60);
32
+ if (diffHours < 24) return `${diffHours}h ago`;
33
+ return date.toLocaleDateString();
36
34
  }
37
35
 
38
36
  /**
39
37
  * Format expiry time for display
40
38
  */
41
39
  function formatExpiry(expiresAt: number): string {
42
- const date = new Date(expiresAt);
43
- const now = new Date();
44
- const diffMs = date.getTime() - now.getTime();
45
- const diffMins = Math.floor(diffMs / 60000);
46
-
47
- if (diffMins < 0) return "expired";
48
- if (diffMins < 5) return "expires soon";
49
- if (diffMins < 60) return `${diffMins}m`;
50
- const diffHours = Math.floor(diffMins / 60);
51
- return `${diffHours}h`;
40
+ const date = new Date(expiresAt);
41
+ const now = new Date();
42
+ const diffMs = date.getTime() - now.getTime();
43
+ const diffMins = Math.floor(diffMs / 60000);
44
+
45
+ if (diffMins < 0) return "expired";
46
+ if (diffMins < 5) return "expires soon";
47
+ if (diffMins < 60) return `${diffMins}m`;
48
+ const diffHours = Math.floor(diffMins / 60);
49
+ return `${diffHours}h`;
52
50
  }
53
51
 
54
52
  /**
55
53
  * Display list of accounts
56
54
  */
57
55
  function displayAccounts(ctx: any, accounts: CodexAccount[]): void {
58
- if (accounts.length === 0) {
59
- ctx.ui.notify("No Codex accounts configured.", "info");
60
- return;
61
- }
62
-
63
- const lines: string[] = [];
64
- accounts.forEach((acc, index) => {
65
- const status = acc.disabled ? "✗" : "✓";
66
- const email = acc.email || acc.accountId;
67
- const lastUsed = acc.lastUsed ? formatTimestamp(acc.lastUsed) : "never";
68
- const expiry = formatExpiry(acc.expiresAt);
69
-
70
- lines.push(`${index + 1}. ${status} ${email}`);
71
- lines.push(` Last used: ${lastUsed}, Token: ${expiry}`);
72
- if (acc.disabled && acc.disabledReason) {
73
- lines.push(` Reason: ${acc.disabledReason}`);
74
- }
75
- });
76
-
77
- ctx.ui.notify(lines.join("\n"), "info");
56
+ if (accounts.length === 0) {
57
+ ctx.ui.notify("No Codex accounts configured.", "info");
58
+ return;
59
+ }
60
+
61
+ const lines: string[] = [];
62
+ accounts.forEach((acc, index) => {
63
+ const status = acc.disabled ? "✗" : "✓";
64
+ const email = acc.email || acc.accountId;
65
+ const lastUsed = acc.lastUsed ? formatTimestamp(acc.lastUsed) : "never";
66
+ const expiry = formatExpiry(acc.expiresAt);
67
+
68
+ lines.push(`${index + 1}. ${status} ${email}`);
69
+ lines.push(` Last used: ${lastUsed}, Token: ${expiry}`);
70
+ if (acc.disabled && acc.disabledReason) {
71
+ lines.push(` Reason: ${acc.disabledReason}`);
72
+ }
73
+ });
74
+
75
+ ctx.ui.notify(lines.join("\n"), "info");
76
+ }
77
+
78
+ async function syncAccountsToAuthAndReload(ctx: ExtensionCommandContext): Promise<boolean> {
79
+ const synced = await syncAccountsToAuth(getAllAccounts());
80
+ if (synced) {
81
+ ctx.modelRegistry.authStorage.reload();
82
+ }
83
+ return synced;
78
84
  }
79
85
 
80
86
  /**
81
87
  * Register the /codex command
82
88
  */
83
89
  export function registerCodexCommand(pi: ExtensionAPI): void {
84
- pi.registerCommand("codex", {
85
- description: "Manage Codex OAuth accounts: /codex [add|list|status|remove|enable|disable|import|import-cockpit|sync]",
86
-
87
- getArgumentCompletions: (prefix: string) => {
88
- const subcommands = [
89
- "add",
90
- "list",
91
- "status",
92
- "remove",
93
- "enable",
94
- "disable",
95
- "import",
96
- "import-cockpit",
97
- "sync",
98
- ];
99
- const parts = prefix.trim().split(/\s+/);
100
-
101
- if (parts.length <= 1) {
102
- return subcommands
103
- .filter((cmd) => cmd.startsWith(parts[0] ?? ""))
104
- .map((cmd) => ({ value: cmd, label: cmd }));
105
- }
106
-
107
- // For remove/enable/disable, suggest account indices
108
- if (["remove", "enable", "disable"].includes(parts[0])) {
109
- const accounts = getAllAccounts();
110
- return accounts.map((acc, idx) => ({
111
- value: `${parts[0]} ${idx + 1}`,
112
- label: `${idx + 1} — ${acc.email || acc.accountId}`,
113
- }));
114
- }
115
-
116
- return [];
117
- },
118
-
119
- handler: async (args, ctx) => {
120
- const parts = args.trim().split(/\s+/);
121
- const sub = parts[0] || "list";
122
-
123
- try {
124
- switch (sub) {
125
- case "add": {
126
- ctx.ui.notify("Starting OAuth login flow...", "info");
127
- const accountData = await performOAuthLogin(undefined, {
128
- onStatus: (msg: string) => ctx.ui.notify(msg, "info"),
129
- onManualCodeInput: async () =>
130
- (await ctx.ui.input(
131
- "Paste the redirect URL from your browser:",
132
- "http://localhost:...",
133
- )) ?? "",
134
- });
135
-
136
- // Prompt for email (optional)
137
- const emailInput = await ctx.ui.input("Email for this account (optional, press Enter to skip)", "");
138
- const email = emailInput || undefined;
139
-
140
- const account = addAccount({
141
- ...accountData,
142
- email,
143
- lastUsed: undefined,
144
- disabled: false,
145
- });
146
-
147
- // Sync to auth.json
148
- const success = await syncAccountsToAuth(getAllAccounts());
149
- if (success) {
150
- ctx.ui.notify(`Added account: ${email || account.accountId}. Synced to auth.json.`, "success");
151
- } else {
152
- ctx.ui.notify(`Added account: ${email || account.accountId}. Failed to sync to auth.json.`, "warning");
153
- }
154
- return;
155
- }
156
-
157
- case "list": {
158
- const accounts = getAllAccounts();
159
- displayAccounts(ctx, accounts);
160
- return;
161
- }
162
-
163
- case "status": {
164
- const accounts = getAllAccounts();
165
- const activeCount = accounts.filter((a) => !a.disabled).length;
166
- const disabledCount = accounts.length - activeCount;
167
- const expiringSoon = accounts.filter((a) => !a.disabled && a.expiresAt - Date.now() < 5 * 60 * 1000).length;
168
-
169
- const lines: string[] = [];
170
- lines.push(`Codex OAuth Rotation Status`);
171
- lines.push(`===========================`);
172
- lines.push(`Total accounts: ${accounts.length}`);
173
- lines.push(`Active: ${activeCount}, Disabled: ${disabledCount}`);
174
- lines.push(`Expiring soon: ${expiringSoon}`);
175
-
176
- if (accounts.length > 0) {
177
- lines.push(`\nAccounts:`);
178
- accounts.forEach((acc, index) => {
179
- const status = acc.disabled ? "✗" : "✓";
180
- const email = acc.email || acc.accountId;
181
- const expiry = formatExpiry(acc.expiresAt);
182
- lines.push(` ${index + 1}. ${status} ${email} (${expiry})`);
183
- });
184
- }
185
-
186
- ctx.ui.notify(lines.join("\n"), "info");
187
- return;
188
- }
189
-
190
- case "remove": {
191
- const indexStr = parts[1];
192
- if (!indexStr) {
193
- ctx.ui.notify("Usage: /codex remove <index|email>", "error");
194
- return;
195
- }
196
-
197
- let account: CodexAccount | undefined;
198
- const index = parseInt(indexStr, 10);
199
-
200
- if (!isNaN(index) && index > 0 && index <= getAllAccounts().length) {
201
- account = getAllAccounts()[index - 1];
202
- } else {
203
- account = getAccountByEmail(indexStr) || getAccountById(indexStr);
204
- }
205
-
206
- if (!account) {
207
- ctx.ui.notify(`Account not found: ${indexStr}`, "error");
208
- return;
209
- }
210
-
211
- const confirmed = await ctx.ui.select(
212
- `Remove account: ${account.email || account.accountId}?`,
213
- ["Yes, remove", "Cancel"],
214
- { signal: AbortSignal.timeout(30000) },
215
- );
216
-
217
- if (confirmed === "Yes, remove") {
218
- removeAccount(account.id);
219
- await syncAccountsToAuth(getAllAccounts());
220
- ctx.ui.notify(`Removed account: ${account.email || account.accountId}`, "success");
221
- }
222
- return;
223
- }
224
-
225
- case "enable": {
226
- const indexStr = parts[1];
227
- if (!indexStr) {
228
- ctx.ui.notify("Usage: /codex enable <index|email>", "error");
229
- return;
230
- }
231
-
232
- let account: CodexAccount | undefined;
233
- const index = parseInt(indexStr, 10);
234
-
235
- if (!isNaN(index) && index > 0 && index <= getAllAccounts().length) {
236
- account = getAllAccounts()[index - 1];
237
- } else {
238
- account = getAccountByEmail(indexStr) || getAccountById(indexStr);
239
- }
240
-
241
- if (!account) {
242
- ctx.ui.notify(`Account not found: ${indexStr}`, "error");
243
- return;
244
- }
245
-
246
- updateAccount(account.id, { disabled: false, disabledReason: undefined });
247
- await syncAccountsToAuth(getAllAccounts());
248
- ctx.ui.notify(`Enabled account: ${account.email || account.accountId}`, "success");
249
- return;
250
- }
251
-
252
- case "disable": {
253
- const indexStr = parts[1];
254
- if (!indexStr) {
255
- ctx.ui.notify("Usage: /codex disable <index|email>", "error");
256
- return;
257
- }
258
-
259
- let account: CodexAccount | undefined;
260
- const index = parseInt(indexStr, 10);
261
-
262
- if (!isNaN(index) && index > 0 && index <= getAllAccounts().length) {
263
- account = getAllAccounts()[index - 1];
264
- } else {
265
- account = getAccountByEmail(indexStr) || getAccountById(indexStr);
266
- }
267
-
268
- if (!account) {
269
- ctx.ui.notify(`Account not found: ${indexStr}`, "error");
270
- return;
271
- }
272
-
273
- updateAccount(account.id, { disabled: true, disabledReason: "manually disabled" });
274
- await syncAccountsToAuth(getAllAccounts());
275
- ctx.ui.notify(`Disabled account: ${account.email || account.accountId}`, "success");
276
- return;
277
- }
278
-
279
- case "import": {
280
- ctx.ui.notify("Importing from ~/.codex/auth.json...", "info");
281
- const imported = await importFromExistingCodexAuth();
282
-
283
- if (!imported) {
284
- ctx.ui.notify("No account found to import.", "warning");
285
- return;
286
- }
287
-
288
- const account = addAccount({
289
- email: imported.email,
290
- accountId: imported.accountId,
291
- refreshToken: imported.refreshToken,
292
- accessToken: imported.accessToken,
293
- expiresAt: imported.expiresAt,
294
- lastUsed: undefined,
295
- disabled: false,
296
- });
297
-
298
- await syncAccountsToAuth(getAllAccounts());
299
- ctx.ui.notify(`Imported account: ${imported.email || imported.accountId}`, "success");
300
- return;
301
- }
302
-
303
- case "import-cockpit": {
304
- ctx.ui.notify("Importing from Cockpit Tools...", "info");
305
- const imported = await importFromCockpit();
306
-
307
- if (imported.length === 0) {
308
- ctx.ui.notify("No accounts found to import.", "warning");
309
- return;
310
- }
311
-
312
- for (const acc of imported) {
313
- addAccount(acc);
314
- }
315
-
316
- await syncAccountsToAuth(getAllAccounts());
317
- ctx.ui.notify(`Imported ${imported.length} account(s) from Cockpit Tools`, "success");
318
- return;
319
- }
320
-
321
- case "sync": {
322
- ctx.ui.notify("Refreshing all tokens and syncing to auth.json...", "info");
323
-
324
- const accounts = getAllAccounts();
325
- const results: { success: number; failed: number } = { success: 0, failed: 0 };
326
-
327
- for (const acc of accounts) {
328
- if (acc.disabled) continue;
329
-
330
- try {
331
- const refreshed = await refreshAccountToken(acc);
332
- updateAccount(acc.id, refreshed);
333
- results.success++;
334
- } catch (error) {
335
- logCodexRotateError(`Failed to refresh ${acc.id}:`, error);
336
- results.failed++;
337
- }
338
- }
339
-
340
- await syncAccountsToAuth(getAllAccounts());
341
-
342
- if (results.failed === 0) {
343
- ctx.ui.notify(`Synced ${results.success} account(s) to auth.json`, "success");
344
- } else {
345
- ctx.ui.notify(`Synced ${results.success} account(s), ${results.failed} failed`, "warning");
346
- }
347
- return;
348
- }
349
-
350
- default:
351
- ctx.ui.notify(
352
- `Usage: /codex [add|list|status|remove|enable|disable|import|import-cockpit|sync]`,
353
- "info",
354
- );
355
- }
356
- } catch (error) {
357
- const message = error instanceof Error ? error.message : String(error);
358
- ctx.ui.notify(`Error: ${message}`, "error");
359
- }
360
- },
361
- });
90
+ pi.registerCommand("codex", {
91
+ description: "Manage Codex OAuth accounts: /codex [add|list|status|remove|enable|disable|import|import-cockpit|sync]",
92
+
93
+ getArgumentCompletions: (prefix: string) => {
94
+ const subcommands = [
95
+ "add",
96
+ "list",
97
+ "status",
98
+ "remove",
99
+ "enable",
100
+ "disable",
101
+ "import",
102
+ "import-cockpit",
103
+ "sync",
104
+ ];
105
+ const parts = prefix.trim().split(/\s+/);
106
+
107
+ if (parts.length <= 1) {
108
+ return subcommands
109
+ .filter((cmd) => cmd.startsWith(parts[0] ?? ""))
110
+ .map((cmd) => ({ value: cmd, label: cmd }));
111
+ }
112
+
113
+ // For remove/enable/disable, suggest account indices
114
+ if (["remove", "enable", "disable"].includes(parts[0])) {
115
+ const accounts = getAllAccounts();
116
+ return accounts.map((acc, idx) => ({
117
+ value: `${parts[0]} ${idx + 1}`,
118
+ label: `${idx + 1} — ${acc.email || acc.accountId}`,
119
+ }));
120
+ }
121
+
122
+ return [];
123
+ },
124
+
125
+ handler: async (args, ctx) => {
126
+ const parts = args.trim().split(/\s+/);
127
+ const sub = parts[0] || "list";
128
+
129
+ try {
130
+ switch (sub) {
131
+ case "add": {
132
+ ctx.ui.notify("Starting OAuth login flow...", "info");
133
+ const accountData = await performOAuthLogin(undefined, {
134
+ onStatus: (msg: string) => ctx.ui.notify(msg, "info"),
135
+ onManualCodeInput: async () =>
136
+ (await ctx.ui.input(
137
+ "Paste the redirect URL from your browser:",
138
+ "http://localhost:...",
139
+ )) ?? "",
140
+ });
141
+
142
+ // Prompt for email (optional)
143
+ const emailInput = await ctx.ui.input("Email for this account (optional, press Enter to skip)", "");
144
+ const email = emailInput || undefined;
145
+
146
+ const account = addAccount({
147
+ ...accountData,
148
+ email,
149
+ lastUsed: undefined,
150
+ disabled: false,
151
+ });
152
+
153
+ // Sync to auth.json
154
+ const success = await syncAccountsToAuthAndReload(ctx);
155
+ if (success) {
156
+ ctx.ui.notify(`Added account: ${email || account.accountId}. Synced to auth.json.`, "success");
157
+ } else {
158
+ ctx.ui.notify(`Added account: ${email || account.accountId}. Failed to sync to auth.json.`, "warning");
159
+ }
160
+ return;
161
+ }
162
+
163
+ case "list": {
164
+ const accounts = getAllAccounts();
165
+ displayAccounts(ctx, accounts);
166
+ return;
167
+ }
168
+
169
+ case "status": {
170
+ const accounts = getAllAccounts();
171
+ const activeCount = accounts.filter((a) => !a.disabled).length;
172
+ const disabledCount = accounts.length - activeCount;
173
+ const expiringSoon = accounts.filter((a) => !a.disabled && a.expiresAt - Date.now() < 5 * 60 * 1000).length;
174
+
175
+ const lines: string[] = [];
176
+ lines.push(`Codex OAuth Rotation Status`);
177
+ lines.push(`===========================`);
178
+ lines.push(`Total accounts: ${accounts.length}`);
179
+ lines.push(`Active: ${activeCount}, Disabled: ${disabledCount}`);
180
+ lines.push(`Expiring soon: ${expiringSoon}`);
181
+
182
+ if (accounts.length > 0) {
183
+ lines.push(`\nAccounts:`);
184
+ accounts.forEach((acc, index) => {
185
+ const status = acc.disabled ? "✗" : "✓";
186
+ const email = acc.email || acc.accountId;
187
+ const expiry = formatExpiry(acc.expiresAt);
188
+ lines.push(` ${index + 1}. ${status} ${email} (${expiry})`);
189
+ });
190
+ }
191
+
192
+ ctx.ui.notify(lines.join("\n"), "info");
193
+ return;
194
+ }
195
+
196
+ case "remove": {
197
+ const indexStr = parts[1];
198
+ if (!indexStr) {
199
+ ctx.ui.notify("Usage: /codex remove <index|email>", "error");
200
+ return;
201
+ }
202
+
203
+ let account: CodexAccount | undefined;
204
+ const index = parseInt(indexStr, 10);
205
+
206
+ if (!isNaN(index) && index > 0 && index <= getAllAccounts().length) {
207
+ account = getAllAccounts()[index - 1];
208
+ } else {
209
+ account = getAccountByEmail(indexStr) || getAccountById(indexStr);
210
+ }
211
+
212
+ if (!account) {
213
+ ctx.ui.notify(`Account not found: ${indexStr}`, "error");
214
+ return;
215
+ }
216
+
217
+ const confirmed = await ctx.ui.select(
218
+ `Remove account: ${account.email || account.accountId}?`,
219
+ ["Yes, remove", "Cancel"],
220
+ { signal: AbortSignal.timeout(30000) },
221
+ );
222
+
223
+ if (confirmed === "Yes, remove") {
224
+ removeAccount(account.id);
225
+ await syncAccountsToAuthAndReload(ctx);
226
+ ctx.ui.notify(`Removed account: ${account.email || account.accountId}`, "success");
227
+ }
228
+ return;
229
+ }
230
+
231
+ case "enable": {
232
+ const indexStr = parts[1];
233
+ if (!indexStr) {
234
+ ctx.ui.notify("Usage: /codex enable <index|email>", "error");
235
+ return;
236
+ }
237
+
238
+ let account: CodexAccount | undefined;
239
+ const index = parseInt(indexStr, 10);
240
+
241
+ if (!isNaN(index) && index > 0 && index <= getAllAccounts().length) {
242
+ account = getAllAccounts()[index - 1];
243
+ } else {
244
+ account = getAccountByEmail(indexStr) || getAccountById(indexStr);
245
+ }
246
+
247
+ if (!account) {
248
+ ctx.ui.notify(`Account not found: ${indexStr}`, "error");
249
+ return;
250
+ }
251
+
252
+ updateAccount(account.id, { disabled: false, disabledReason: undefined });
253
+ await syncAccountsToAuthAndReload(ctx);
254
+ ctx.ui.notify(`Enabled account: ${account.email || account.accountId}`, "success");
255
+ return;
256
+ }
257
+
258
+ case "disable": {
259
+ const indexStr = parts[1];
260
+ if (!indexStr) {
261
+ ctx.ui.notify("Usage: /codex disable <index|email>", "error");
262
+ return;
263
+ }
264
+
265
+ let account: CodexAccount | undefined;
266
+ const index = parseInt(indexStr, 10);
267
+
268
+ if (!isNaN(index) && index > 0 && index <= getAllAccounts().length) {
269
+ account = getAllAccounts()[index - 1];
270
+ } else {
271
+ account = getAccountByEmail(indexStr) || getAccountById(indexStr);
272
+ }
273
+
274
+ if (!account) {
275
+ ctx.ui.notify(`Account not found: ${indexStr}`, "error");
276
+ return;
277
+ }
278
+
279
+ updateAccount(account.id, { disabled: true, disabledReason: "manually disabled" });
280
+ await syncAccountsToAuthAndReload(ctx);
281
+ ctx.ui.notify(`Disabled account: ${account.email || account.accountId}`, "success");
282
+ return;
283
+ }
284
+
285
+ case "import": {
286
+ ctx.ui.notify("Importing from ~/.codex/auth.json...", "info");
287
+ const imported = await importFromExistingCodexAuth();
288
+
289
+ if (!imported) {
290
+ ctx.ui.notify("No account found to import.", "warning");
291
+ return;
292
+ }
293
+
294
+ addAccount({
295
+ email: imported.email,
296
+ accountId: imported.accountId,
297
+ refreshToken: imported.refreshToken,
298
+ accessToken: imported.accessToken,
299
+ expiresAt: imported.expiresAt,
300
+ lastUsed: undefined,
301
+ disabled: false,
302
+ });
303
+
304
+ await syncAccountsToAuthAndReload(ctx);
305
+ ctx.ui.notify(`Imported account: ${imported.email || imported.accountId}`, "success");
306
+ return;
307
+ }
308
+
309
+ case "import-cockpit": {
310
+ ctx.ui.notify("Importing from Cockpit Tools...", "info");
311
+ const imported = await importFromCockpit();
312
+
313
+ if (imported.length === 0) {
314
+ ctx.ui.notify("No accounts found to import.", "warning");
315
+ return;
316
+ }
317
+
318
+ for (const acc of imported) {
319
+ addAccount(acc);
320
+ }
321
+
322
+ await syncAccountsToAuthAndReload(ctx);
323
+ ctx.ui.notify(`Imported ${imported.length} account(s) from Cockpit Tools`, "success");
324
+ return;
325
+ }
326
+
327
+ case "sync": {
328
+ ctx.ui.notify("Refreshing all tokens and syncing to auth.json...", "info");
329
+
330
+ const accounts = getAllAccounts();
331
+ const results: { success: number; failed: number } = { success: 0, failed: 0 };
332
+
333
+ for (const acc of accounts) {
334
+ if (acc.disabled) continue;
335
+
336
+ try {
337
+ const refreshed = await refreshAccountToken(acc);
338
+ updateAccount(acc.id, refreshed);
339
+ results.success++;
340
+ } catch (error) {
341
+ logCodexRotateError(`Failed to refresh ${acc.id}:`, error);
342
+ results.failed++;
343
+ }
344
+ }
345
+
346
+ await syncAccountsToAuthAndReload(ctx);
347
+
348
+ if (results.failed === 0) {
349
+ ctx.ui.notify(`Synced ${results.success} account(s) to auth.json`, "success");
350
+ } else {
351
+ ctx.ui.notify(`Synced ${results.success} account(s), ${results.failed} failed`, "warning");
352
+ }
353
+ return;
354
+ }
355
+
356
+ default:
357
+ ctx.ui.notify(
358
+ `Usage: /codex [add|list|status|remove|enable|disable|import|import-cockpit|sync]`,
359
+ "info",
360
+ );
361
+ }
362
+ } catch (error) {
363
+ const message = error instanceof Error ? error.message : String(error);
364
+ ctx.ui.notify(`Error: ${message}`, "error");
365
+ }
366
+ },
367
+ });
362
368
  }