aimux-cli 0.1.0

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 (207) hide show
  1. package/README.md +743 -0
  2. package/bin/aimux +2 -0
  3. package/dist/agent-events.d.ts +20 -0
  4. package/dist/agent-events.js +2 -0
  5. package/dist/agent-events.js.map +1 -0
  6. package/dist/agent-message-parts.d.ts +17 -0
  7. package/dist/agent-message-parts.js +31 -0
  8. package/dist/agent-message-parts.js.map +1 -0
  9. package/dist/agent-output-parser.d.ts +16 -0
  10. package/dist/agent-output-parser.js +229 -0
  11. package/dist/agent-output-parser.js.map +1 -0
  12. package/dist/agent-tracker.d.ts +9 -0
  13. package/dist/agent-tracker.js +144 -0
  14. package/dist/agent-tracker.js.map +1 -0
  15. package/dist/agent-watcher.d.ts +15 -0
  16. package/dist/agent-watcher.js +2 -0
  17. package/dist/agent-watcher.js.map +1 -0
  18. package/dist/attachment-store.d.ts +35 -0
  19. package/dist/attachment-store.js +129 -0
  20. package/dist/attachment-store.js.map +1 -0
  21. package/dist/builtin-metadata-watchers.d.ts +2 -0
  22. package/dist/builtin-metadata-watchers.js +275 -0
  23. package/dist/builtin-metadata-watchers.js.map +1 -0
  24. package/dist/claude-hooks.d.ts +29 -0
  25. package/dist/claude-hooks.js +106 -0
  26. package/dist/claude-hooks.js.map +1 -0
  27. package/dist/config.d.ts +78 -0
  28. package/dist/config.js +172 -0
  29. package/dist/config.js.map +1 -0
  30. package/dist/context/compactor.d.ts +20 -0
  31. package/dist/context/compactor.js +212 -0
  32. package/dist/context/compactor.js.map +1 -0
  33. package/dist/context/context-bridge.d.ts +67 -0
  34. package/dist/context/context-bridge.js +471 -0
  35. package/dist/context/context-bridge.js.map +1 -0
  36. package/dist/context/context-file.d.ts +11 -0
  37. package/dist/context/context-file.js +93 -0
  38. package/dist/context/context-file.js.map +1 -0
  39. package/dist/context/history.d.ts +40 -0
  40. package/dist/context/history.js +108 -0
  41. package/dist/context/history.js.map +1 -0
  42. package/dist/daemon.d.ts +39 -0
  43. package/dist/daemon.js +344 -0
  44. package/dist/daemon.js.map +1 -0
  45. package/dist/dashboard-session-registry.d.ts +47 -0
  46. package/dist/dashboard-session-registry.js +161 -0
  47. package/dist/dashboard-session-registry.js.map +1 -0
  48. package/dist/dashboard-state.d.ts +18 -0
  49. package/dist/dashboard-state.js +26 -0
  50. package/dist/dashboard-state.js.map +1 -0
  51. package/dist/dashboard.d.ts +118 -0
  52. package/dist/dashboard.js +91 -0
  53. package/dist/dashboard.js.map +1 -0
  54. package/dist/debug.d.ts +7 -0
  55. package/dist/debug.js +41 -0
  56. package/dist/debug.js.map +1 -0
  57. package/dist/fast-control.d.ts +45 -0
  58. package/dist/fast-control.js +174 -0
  59. package/dist/fast-control.js.map +1 -0
  60. package/dist/hotkeys.d.ts +44 -0
  61. package/dist/hotkeys.js +118 -0
  62. package/dist/hotkeys.js.map +1 -0
  63. package/dist/http-client.d.ts +10 -0
  64. package/dist/http-client.js +54 -0
  65. package/dist/http-client.js.map +1 -0
  66. package/dist/instance-directory.d.ts +32 -0
  67. package/dist/instance-directory.js +82 -0
  68. package/dist/instance-directory.js.map +1 -0
  69. package/dist/instance-registry.d.ts +38 -0
  70. package/dist/instance-registry.js +208 -0
  71. package/dist/instance-registry.js.map +1 -0
  72. package/dist/key-parser.d.ts +30 -0
  73. package/dist/key-parser.js +272 -0
  74. package/dist/key-parser.js.map +1 -0
  75. package/dist/last-used.d.ts +31 -0
  76. package/dist/last-used.js +93 -0
  77. package/dist/last-used.js.map +1 -0
  78. package/dist/main.d.ts +1 -0
  79. package/dist/main.js +2483 -0
  80. package/dist/main.js.map +1 -0
  81. package/dist/metadata-server.d.ts +268 -0
  82. package/dist/metadata-server.js +1379 -0
  83. package/dist/metadata-server.js.map +1 -0
  84. package/dist/metadata-store.d.ts +80 -0
  85. package/dist/metadata-store.js +87 -0
  86. package/dist/metadata-store.js.map +1 -0
  87. package/dist/multiplexer.d.ts +471 -0
  88. package/dist/multiplexer.js +5714 -0
  89. package/dist/multiplexer.js.map +1 -0
  90. package/dist/notification-context.d.ts +18 -0
  91. package/dist/notification-context.js +68 -0
  92. package/dist/notification-context.js.map +1 -0
  93. package/dist/notifications.d.ts +38 -0
  94. package/dist/notifications.js +111 -0
  95. package/dist/notifications.js.map +1 -0
  96. package/dist/notify.d.ts +10 -0
  97. package/dist/notify.js +62 -0
  98. package/dist/notify.js.map +1 -0
  99. package/dist/orchestration-actions.d.ts +76 -0
  100. package/dist/orchestration-actions.js +310 -0
  101. package/dist/orchestration-actions.js.map +1 -0
  102. package/dist/orchestration-dispatcher.d.ts +22 -0
  103. package/dist/orchestration-dispatcher.js +49 -0
  104. package/dist/orchestration-dispatcher.js.map +1 -0
  105. package/dist/orchestration-routing.d.ts +20 -0
  106. package/dist/orchestration-routing.js +78 -0
  107. package/dist/orchestration-routing.js.map +1 -0
  108. package/dist/orchestration.d.ts +26 -0
  109. package/dist/orchestration.js +110 -0
  110. package/dist/orchestration.js.map +1 -0
  111. package/dist/osc-notifications.d.ts +15 -0
  112. package/dist/osc-notifications.js +180 -0
  113. package/dist/osc-notifications.js.map +1 -0
  114. package/dist/paths.d.ts +55 -0
  115. package/dist/paths.js +259 -0
  116. package/dist/paths.js.map +1 -0
  117. package/dist/plugin-runtime.d.ts +46 -0
  118. package/dist/plugin-runtime.js +180 -0
  119. package/dist/plugin-runtime.js.map +1 -0
  120. package/dist/project-events.d.ts +36 -0
  121. package/dist/project-events.js +63 -0
  122. package/dist/project-events.js.map +1 -0
  123. package/dist/project-scanner.d.ts +38 -0
  124. package/dist/project-scanner.js +243 -0
  125. package/dist/project-scanner.js.map +1 -0
  126. package/dist/project-service-manifest.d.ts +18 -0
  127. package/dist/project-service-manifest.js +56 -0
  128. package/dist/project-service-manifest.js.map +1 -0
  129. package/dist/recency.d.ts +2 -0
  130. package/dist/recency.js +34 -0
  131. package/dist/recency.js.map +1 -0
  132. package/dist/recorder.d.ts +14 -0
  133. package/dist/recorder.js +130 -0
  134. package/dist/recorder.js.map +1 -0
  135. package/dist/session-bootstrap.d.ts +45 -0
  136. package/dist/session-bootstrap.js +436 -0
  137. package/dist/session-bootstrap.js.map +1 -0
  138. package/dist/session-message-history.d.ts +27 -0
  139. package/dist/session-message-history.js +105 -0
  140. package/dist/session-message-history.js.map +1 -0
  141. package/dist/session-runtime.d.ts +44 -0
  142. package/dist/session-runtime.js +56 -0
  143. package/dist/session-runtime.js.map +1 -0
  144. package/dist/session-semantics.d.ts +35 -0
  145. package/dist/session-semantics.js +110 -0
  146. package/dist/session-semantics.js.map +1 -0
  147. package/dist/status-detector.d.ts +17 -0
  148. package/dist/status-detector.js +67 -0
  149. package/dist/status-detector.js.map +1 -0
  150. package/dist/statusline-model.d.ts +103 -0
  151. package/dist/statusline-model.js +177 -0
  152. package/dist/statusline-model.js.map +1 -0
  153. package/dist/task-dispatcher.d.ts +63 -0
  154. package/dist/task-dispatcher.js +210 -0
  155. package/dist/task-dispatcher.js.map +1 -0
  156. package/dist/task-workflow.d.ts +13 -0
  157. package/dist/task-workflow.js +153 -0
  158. package/dist/task-workflow.js.map +1 -0
  159. package/dist/tasks.d.ts +60 -0
  160. package/dist/tasks.js +120 -0
  161. package/dist/tasks.js.map +1 -0
  162. package/dist/team.d.ts +28 -0
  163. package/dist/team.js +91 -0
  164. package/dist/team.js.map +1 -0
  165. package/dist/terminal-host.d.ts +10 -0
  166. package/dist/terminal-host.js +52 -0
  167. package/dist/terminal-host.js.map +1 -0
  168. package/dist/threads.d.ts +61 -0
  169. package/dist/threads.js +200 -0
  170. package/dist/threads.js.map +1 -0
  171. package/dist/tmux-doctor.d.ts +47 -0
  172. package/dist/tmux-doctor.js +112 -0
  173. package/dist/tmux-doctor.js.map +1 -0
  174. package/dist/tmux-runtime-manager.d.ts +164 -0
  175. package/dist/tmux-runtime-manager.js +794 -0
  176. package/dist/tmux-runtime-manager.js.map +1 -0
  177. package/dist/tmux-session-transport.d.ts +31 -0
  178. package/dist/tmux-session-transport.js +115 -0
  179. package/dist/tmux-session-transport.js.map +1 -0
  180. package/dist/tmux-statusline.d.ts +17 -0
  181. package/dist/tmux-statusline.js +166 -0
  182. package/dist/tmux-statusline.js.map +1 -0
  183. package/dist/tool-output-watchers.d.ts +10 -0
  184. package/dist/tool-output-watchers.js +190 -0
  185. package/dist/tool-output-watchers.js.map +1 -0
  186. package/dist/tui/render/box.d.ts +1 -0
  187. package/dist/tui/render/box.js +20 -0
  188. package/dist/tui/render/box.js.map +1 -0
  189. package/dist/tui/render/text.d.ts +8 -0
  190. package/dist/tui/render/text.js +92 -0
  191. package/dist/tui/render/text.js.map +1 -0
  192. package/dist/tui/screens/dashboard-renderers.d.ts +23 -0
  193. package/dist/tui/screens/dashboard-renderers.js +411 -0
  194. package/dist/tui/screens/dashboard-renderers.js.map +1 -0
  195. package/dist/tui/screens/overlay-renderers.d.ts +10 -0
  196. package/dist/tui/screens/overlay-renderers.js +274 -0
  197. package/dist/tui/screens/overlay-renderers.js.map +1 -0
  198. package/dist/tui/screens/subscreen-renderers.d.ts +9 -0
  199. package/dist/tui/screens/subscreen-renderers.js +327 -0
  200. package/dist/tui/screens/subscreen-renderers.js.map +1 -0
  201. package/dist/workflow.d.ts +19 -0
  202. package/dist/workflow.js +111 -0
  203. package/dist/workflow.js.map +1 -0
  204. package/dist/worktree.d.ts +23 -0
  205. package/dist/worktree.js +101 -0
  206. package/dist/worktree.js.map +1 -0
  207. package/package.json +70 -0
@@ -0,0 +1,208 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import * as lockfile from "proper-lockfile";
4
+ import { getInstancesPath, getAimuxDirFor } from "./paths.js";
5
+ import { findMainRepo } from "./worktree.js";
6
+ import { debug } from "./debug.js";
7
+ const HEARTBEAT_STALE_MS = 15_000;
8
+ const LOCK_RETRIES = { retries: 5, minTimeout: 50 };
9
+ /**
10
+ * Check if a PID is alive.
11
+ */
12
+ function isPidAlive(pid) {
13
+ try {
14
+ process.kill(pid, 0);
15
+ return true;
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
21
+ /**
22
+ * Filter out dead instances (dead PID or stale heartbeat).
23
+ */
24
+ function pruneDeadEntries(instances) {
25
+ const now = Date.now();
26
+ return instances.filter((inst) => {
27
+ if (!isPidAlive(inst.pid)) {
28
+ debug(`pruning dead instance ${inst.instanceId} (PID ${inst.pid} dead)`, "instance");
29
+ return false;
30
+ }
31
+ const heartbeatAge = now - new Date(inst.heartbeat).getTime();
32
+ if (heartbeatAge > HEARTBEAT_STALE_MS) {
33
+ debug(`pruning stale instance ${inst.instanceId} (heartbeat ${Math.round(heartbeatAge / 1000)}s old)`, "instance");
34
+ return false;
35
+ }
36
+ return true;
37
+ });
38
+ }
39
+ /**
40
+ * Read instances.json, returning empty array if missing or corrupt.
41
+ */
42
+ function readInstancesFile(filePath) {
43
+ if (!existsSync(filePath))
44
+ return [];
45
+ try {
46
+ return JSON.parse(readFileSync(filePath, "utf-8"));
47
+ }
48
+ catch {
49
+ return [];
50
+ }
51
+ }
52
+ /**
53
+ * Ensure the directory for the instances file exists and the file is created.
54
+ */
55
+ function ensureInstancesFile(filePath) {
56
+ const dir = filePath.replace(/\/[^/]+$/, "");
57
+ if (!existsSync(dir)) {
58
+ mkdirSync(dir, { recursive: true });
59
+ }
60
+ if (!existsSync(filePath)) {
61
+ writeFileSync(filePath, "[]");
62
+ }
63
+ }
64
+ /**
65
+ * Run an operation against instances.json with locking.
66
+ * Operates on both the global project state dir and the main repo (for cross-worktree discovery).
67
+ */
68
+ async function withLockedInstances(cwd, fn) {
69
+ const paths = getInstancesPaths(cwd);
70
+ for (const filePath of paths) {
71
+ ensureInstancesFile(filePath);
72
+ let release;
73
+ try {
74
+ release = await lockfile.lock(filePath, { retries: LOCK_RETRIES });
75
+ const instances = readInstancesFile(filePath);
76
+ const updated = fn(instances, filePath);
77
+ writeFileSync(filePath, JSON.stringify(updated, null, 2) + "\n");
78
+ }
79
+ finally {
80
+ if (release)
81
+ await release();
82
+ }
83
+ }
84
+ }
85
+ /**
86
+ * Get all instances.json paths to update (global project state + main repo .aimux/ if different).
87
+ * The global path is primary; the in-repo path is for cross-worktree discovery by older instances.
88
+ */
89
+ function getInstancesPaths(cwd) {
90
+ const globalPath = getInstancesPath();
91
+ const paths = [globalPath];
92
+ // Also write to in-repo .aimux/ for backward compat with older instances
93
+ try {
94
+ const mainRepo = findMainRepo(cwd);
95
+ const inRepoPath = join(getAimuxDirFor(mainRepo), "instances.json");
96
+ if (inRepoPath !== globalPath) {
97
+ paths.push(inRepoPath);
98
+ }
99
+ }
100
+ catch {
101
+ // Not in a git repo or worktree detection failed — global only
102
+ }
103
+ return paths;
104
+ }
105
+ /**
106
+ * Register this instance in instances.json.
107
+ */
108
+ export async function registerInstance(instanceId, cwd) {
109
+ let remoteInstances = [];
110
+ await withLockedInstances(cwd, (instances) => {
111
+ const pruned = pruneDeadEntries(instances);
112
+ const entry = {
113
+ instanceId,
114
+ pid: process.pid,
115
+ startedAt: new Date().toISOString(),
116
+ heartbeat: new Date().toISOString(),
117
+ cwd,
118
+ sessions: [],
119
+ };
120
+ const updated = [...pruned, entry];
121
+ remoteInstances = pruned.filter((i) => i.instanceId !== instanceId);
122
+ return updated;
123
+ });
124
+ if (remoteInstances.length > 0) {
125
+ debug(`registered instance ${instanceId}, found ${remoteInstances.length} other instance(s)`, "instance");
126
+ }
127
+ else {
128
+ debug(`registered instance ${instanceId} (sole instance)`, "instance");
129
+ }
130
+ return remoteInstances;
131
+ }
132
+ /**
133
+ * Unregister this instance from instances.json.
134
+ */
135
+ export async function unregisterInstance(instanceId, cwd) {
136
+ await withLockedInstances(cwd, (instances) => {
137
+ return instances.filter((i) => i.instanceId !== instanceId);
138
+ });
139
+ debug(`unregistered instance ${instanceId}`, "instance");
140
+ }
141
+ /**
142
+ * Update heartbeat timestamp and sessions list. Also prunes dead instances.
143
+ * Returns the session IDs that were in the registry BEFORE this update
144
+ * (so the caller can detect which were claimed by comparing against expectations).
145
+ */
146
+ export async function updateHeartbeat(instanceId, sessions, cwd) {
147
+ // Collect previous session IDs across ALL instances.json files (global + in-repo).
148
+ // withLockedInstances calls the callback once per file, so we merge results.
149
+ const allPreviousIds = new Set();
150
+ await withLockedInstances(cwd, (instances) => {
151
+ // Prune dead instances, but never prune ourselves — we know we're alive
152
+ const pruned = pruneDeadEntries(instances.filter((i) => i.instanceId !== instanceId));
153
+ const self = instances.find((i) => i.instanceId === instanceId);
154
+ const result = pruned.map((inst) => inst);
155
+ if (self) {
156
+ for (const s of self.sessions)
157
+ allPreviousIds.add(s.id);
158
+ result.push({ ...self, heartbeat: new Date().toISOString(), sessions });
159
+ }
160
+ return result;
161
+ });
162
+ return [...allPreviousIds];
163
+ }
164
+ /**
165
+ * Get instances belonging to other aimux processes.
166
+ * Reads from all known instances.json files for cross-worktree visibility.
167
+ */
168
+ export function getRemoteInstances(ownInstanceId, cwd) {
169
+ const paths = getInstancesPaths(cwd);
170
+ const seen = new Set();
171
+ const result = [];
172
+ for (const filePath of paths) {
173
+ const instances = readInstancesFile(filePath);
174
+ const alive = pruneDeadEntries(instances);
175
+ for (const inst of alive) {
176
+ if (inst.instanceId !== ownInstanceId && !seen.has(inst.instanceId)) {
177
+ seen.add(inst.instanceId);
178
+ result.push(inst);
179
+ }
180
+ }
181
+ }
182
+ return result;
183
+ }
184
+ /**
185
+ * Remove a session from another instance's entry (takeover step).
186
+ * Returns the claimed session ref, or undefined if not found.
187
+ */
188
+ export async function claimSession(sessionId, fromInstanceId, cwd) {
189
+ let claimed;
190
+ await withLockedInstances(cwd, (instances) => {
191
+ return instances.map((inst) => {
192
+ if (inst.instanceId === fromInstanceId) {
193
+ const session = inst.sessions.find((s) => s.id === sessionId);
194
+ if (session) {
195
+ claimed = session;
196
+ debug(`claimed session ${sessionId} from instance ${fromInstanceId}`, "instance");
197
+ return {
198
+ ...inst,
199
+ sessions: inst.sessions.filter((s) => s.id !== sessionId),
200
+ };
201
+ }
202
+ }
203
+ return inst;
204
+ });
205
+ });
206
+ return claimed;
207
+ }
208
+ //# sourceMappingURL=instance-registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"instance-registry.js","sourceRoot":"","sources":["../src/instance-registry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Terminal CSI escape sequence parser.
3
+ *
4
+ * Normalizes raw terminal key sequences into a clean KeyEvent format.
5
+ * Handles xterm modifyOtherKeys, kitty keyboard protocol, and standard CSI.
6
+ *
7
+ * This is the single choke point for all terminal key handling —
8
+ * swap this module to change the parsing strategy.
9
+ */
10
+ export interface KeyEvent {
11
+ /** The printable character, or empty for special keys */
12
+ char: string;
13
+ /** Named key: "enter", "tab", "backspace", "escape", "up", "down", "left", "right", "home", "end", "delete", "pageup", "pagedown", "f1"-"f12", "focusin", "focusout" */
14
+ name: string;
15
+ shift: boolean;
16
+ ctrl: boolean;
17
+ alt: boolean;
18
+ /** The raw input string (for debugging) */
19
+ raw: string;
20
+ }
21
+ /**
22
+ * Parse raw stdin data into KeyEvents.
23
+ * Returns an array because a single stdin chunk can contain multiple key sequences.
24
+ */
25
+ export declare function parseKeys(data: Buffer | string): KeyEvent[];
26
+ /**
27
+ * Check if a KeyEvent matches a key descriptor.
28
+ * Examples: "ctrl+o", "shift+enter", "ctrl+shift+left", "alt+b"
29
+ */
30
+ export declare function matchKey(event: KeyEvent, descriptor: string): boolean;
@@ -0,0 +1,272 @@
1
+ /**
2
+ * Terminal CSI escape sequence parser.
3
+ *
4
+ * Normalizes raw terminal key sequences into a clean KeyEvent format.
5
+ * Handles xterm modifyOtherKeys, kitty keyboard protocol, and standard CSI.
6
+ *
7
+ * This is the single choke point for all terminal key handling —
8
+ * swap this module to change the parsing strategy.
9
+ */
10
+ // Modifier bitmask (shared by xterm and standard CSI):
11
+ // 1=none, 2=shift, 3=alt, 4=shift+alt, 5=ctrl, 6=ctrl+shift, 7=ctrl+alt, 8=ctrl+shift+alt
12
+ function parseModifier(mod) {
13
+ const m = mod - 1; // convert to 0-based bitmask
14
+ return {
15
+ shift: !!(m & 1),
16
+ alt: !!(m & 2),
17
+ ctrl: !!(m & 4),
18
+ };
19
+ }
20
+ const KEYCODE_NAMES = {
21
+ 9: "tab",
22
+ 13: "enter",
23
+ 27: "escape",
24
+ 127: "backspace",
25
+ };
26
+ const CSI_FINAL_NAMES = {
27
+ A: "up",
28
+ B: "down",
29
+ C: "right",
30
+ D: "left",
31
+ H: "home",
32
+ F: "end",
33
+ };
34
+ const CSI_TILDE_NAMES = {
35
+ 1: "home",
36
+ 2: "insert",
37
+ 3: "delete",
38
+ 4: "end",
39
+ 5: "pageup",
40
+ 6: "pagedown",
41
+ 11: "f1",
42
+ 12: "f2",
43
+ 13: "f3",
44
+ 14: "f4",
45
+ 15: "f5",
46
+ 17: "f6",
47
+ 18: "f7",
48
+ 19: "f8",
49
+ 20: "f9",
50
+ 21: "f10",
51
+ 23: "f11",
52
+ 24: "f12",
53
+ };
54
+ /**
55
+ * Parse raw stdin data into KeyEvents.
56
+ * Returns an array because a single stdin chunk can contain multiple key sequences.
57
+ */
58
+ export function parseKeys(data) {
59
+ const str = typeof data === "string" ? data : data.toString("utf-8");
60
+ const events = [];
61
+ let i = 0;
62
+ while (i < str.length) {
63
+ // ESC sequence
64
+ if (str[i] === "\x1b") {
65
+ // Bracketed paste: ESC [200~ ... ESC [201~
66
+ // Terminals wrap pasted text in these markers
67
+ if (str.startsWith("\x1b[200~", i)) {
68
+ const pasteStart = i + 6; // skip ESC [200~
69
+ const pasteEnd = str.indexOf("\x1b[201~", pasteStart);
70
+ const content = pasteEnd >= 0 ? str.slice(pasteStart, pasteEnd) : str.slice(pasteStart); // no end marker yet — take everything
71
+ if (content) {
72
+ events.push({ char: content, name: "paste", shift: false, ctrl: false, alt: false, raw: content });
73
+ }
74
+ i = pasteEnd >= 0 ? pasteEnd + 6 : str.length;
75
+ continue;
76
+ }
77
+ // CSI: ESC [
78
+ if (i + 1 < str.length && str[i + 1] === "[") {
79
+ const parsed = parseCSI(str, i + 2);
80
+ if (parsed) {
81
+ parsed.event.raw = str.slice(i, i + 2 + parsed.consumed);
82
+ events.push(parsed.event);
83
+ i += 2 + parsed.consumed;
84
+ continue;
85
+ }
86
+ }
87
+ // SS3: ESC O (used by some terminals for arrow keys, function keys)
88
+ if (i + 1 < str.length && str[i + 1] === "O") {
89
+ if (i + 2 < str.length) {
90
+ const ch = str[i + 2];
91
+ const name = CSI_FINAL_NAMES[ch];
92
+ if (name) {
93
+ events.push({ char: "", name, shift: false, ctrl: false, alt: false, raw: str.slice(i, i + 3) });
94
+ i += 3;
95
+ continue;
96
+ }
97
+ // SS3 P-S = F1-F4
98
+ if (ch >= "P" && ch <= "S") {
99
+ const fNum = ch.charCodeAt(0) - "P".charCodeAt(0) + 1;
100
+ events.push({
101
+ char: "",
102
+ name: `f${fNum}`,
103
+ shift: false,
104
+ ctrl: false,
105
+ alt: false,
106
+ raw: str.slice(i, i + 3),
107
+ });
108
+ i += 3;
109
+ continue;
110
+ }
111
+ }
112
+ }
113
+ // ESC + single char = Alt+char (meta key)
114
+ if (i + 1 < str.length && str[i + 1] !== "\x1b") {
115
+ const ch = str[i + 1];
116
+ const code = ch.charCodeAt(0);
117
+ // Map control characters to their names (e.g., 0x7F = backspace, 0x0D = enter)
118
+ const ctrlName = code === 127 || code === 8
119
+ ? "backspace"
120
+ : code === 13
121
+ ? "enter"
122
+ : code === 9
123
+ ? "tab"
124
+ : code < 32
125
+ ? String.fromCharCode(code + 96) // Ctrl+letter
126
+ : "";
127
+ events.push({
128
+ char: ctrlName ? "" : ch,
129
+ name: ctrlName || ch,
130
+ shift: false,
131
+ ctrl: code < 32 && code !== 13 && code !== 9 && code !== 8,
132
+ alt: true,
133
+ raw: str.slice(i, i + 2),
134
+ });
135
+ i += 2;
136
+ continue;
137
+ }
138
+ // Lone ESC
139
+ events.push({ char: "", name: "escape", shift: false, ctrl: false, alt: false, raw: "\x1b" });
140
+ i++;
141
+ continue;
142
+ }
143
+ // Control characters
144
+ const code = str.charCodeAt(i);
145
+ if (code < 32 || code === 127) {
146
+ if (code === 13) {
147
+ events.push({ char: "", name: "enter", shift: false, ctrl: false, alt: false, raw: str[i] });
148
+ }
149
+ else if (code === 9) {
150
+ events.push({ char: "", name: "tab", shift: false, ctrl: false, alt: false, raw: str[i] });
151
+ }
152
+ else if (code === 127 || code === 8) {
153
+ events.push({ char: "", name: "backspace", shift: false, ctrl: false, alt: false, raw: str[i] });
154
+ }
155
+ else {
156
+ // Ctrl+letter: Ctrl+A = 1, Ctrl+B = 2, ..., Ctrl+Z = 26
157
+ const letter = String.fromCharCode(code + 96); // 1 -> 'a', etc.
158
+ events.push({ char: "", name: letter, shift: false, ctrl: true, alt: false, raw: str[i] });
159
+ }
160
+ i++;
161
+ continue;
162
+ }
163
+ // Regular printable character(s) — batch consecutive printable chars
164
+ let end = i + 1;
165
+ while (end < str.length && str.charCodeAt(end) >= 32 && str[end] !== "\x1b") {
166
+ end++;
167
+ }
168
+ const chars = str.slice(i, end);
169
+ events.push({ char: chars, name: "", shift: false, ctrl: false, alt: false, raw: chars });
170
+ i = end;
171
+ }
172
+ return events;
173
+ }
174
+ /**
175
+ * Parse CSI sequence parameters (after ESC [).
176
+ * Returns the parsed event and number of characters consumed after "ESC [".
177
+ */
178
+ function parseCSI(str, start) {
179
+ // Collect parameter bytes (digits and semicolons) and the final byte
180
+ let i = start;
181
+ let params = "";
182
+ // Collect parameter bytes: 0x30-0x3F (digits, semicolons, etc.)
183
+ while (i < str.length && str.charCodeAt(i) >= 0x30 && str.charCodeAt(i) <= 0x3f) {
184
+ params += str[i];
185
+ i++;
186
+ }
187
+ // Final byte: 0x40-0x7E
188
+ if (i >= str.length)
189
+ return null;
190
+ const finalByte = str[i];
191
+ const finalCode = finalByte.charCodeAt(0);
192
+ if (finalCode < 0x40 || finalCode > 0x7e)
193
+ return null;
194
+ const consumed = i - start + 1;
195
+ const parts = params.split(";").map(Number);
196
+ // Kitty keyboard protocol: ESC [ keycode ; modifier u
197
+ if (finalByte === "u") {
198
+ const keycode = parts[0] ?? 0;
199
+ const mods = parseModifier(parts[1] ?? 1);
200
+ const name = KEYCODE_NAMES[keycode] ?? "";
201
+ const char = name ? "" : String.fromCharCode(keycode);
202
+ return {
203
+ event: { char, name: name || char, ...mods, raw: "" },
204
+ consumed,
205
+ };
206
+ }
207
+ // xterm modifyOtherKeys: ESC [ 27 ; modifier ; keycode ~
208
+ if (finalByte === "~" && parts[0] === 27 && parts.length >= 3) {
209
+ const mods = parseModifier(parts[1] ?? 1);
210
+ const keycode = parts[2] ?? 0;
211
+ const name = KEYCODE_NAMES[keycode] ?? "";
212
+ const char = name ? "" : String.fromCharCode(keycode);
213
+ return {
214
+ event: { char, name: name || char, ...mods, raw: "" },
215
+ consumed,
216
+ };
217
+ }
218
+ // Standard CSI with tilde: ESC [ number ~ (with optional modifier)
219
+ if (finalByte === "~") {
220
+ const keyNum = parts[0] ?? 0;
221
+ const mods = parseModifier(parts[1] ?? 1);
222
+ const name = CSI_TILDE_NAMES[keyNum] ?? "";
223
+ if (name) {
224
+ return {
225
+ event: { char: "", name, ...mods, raw: "" },
226
+ consumed,
227
+ };
228
+ }
229
+ return null;
230
+ }
231
+ // Standard CSI arrow/nav: ESC [ (modifier ;)? final
232
+ // e.g., ESC [ 1 ; 2 A = Shift+Up
233
+ if (finalByte === "I") {
234
+ return {
235
+ event: { char: "", name: "focusin", shift: false, ctrl: false, alt: false, raw: "" },
236
+ consumed,
237
+ };
238
+ }
239
+ if (finalByte === "O") {
240
+ return {
241
+ event: { char: "", name: "focusout", shift: false, ctrl: false, alt: false, raw: "" },
242
+ consumed,
243
+ };
244
+ }
245
+ const name = CSI_FINAL_NAMES[finalByte];
246
+ if (name) {
247
+ // Modifier is in the last param if there are two parts
248
+ const mod = parts.length >= 2 ? (parts[1] ?? 1) : 1;
249
+ const mods = parseModifier(mod);
250
+ return {
251
+ event: { char: "", name, ...mods, raw: "" },
252
+ consumed,
253
+ };
254
+ }
255
+ return null;
256
+ }
257
+ /**
258
+ * Check if a KeyEvent matches a key descriptor.
259
+ * Examples: "ctrl+o", "shift+enter", "ctrl+shift+left", "alt+b"
260
+ */
261
+ export function matchKey(event, descriptor) {
262
+ const parts = descriptor.toLowerCase().split("+");
263
+ const key = parts.pop();
264
+ const wantCtrl = parts.includes("ctrl");
265
+ const wantShift = parts.includes("shift");
266
+ const wantAlt = parts.includes("alt");
267
+ return (event.ctrl === wantCtrl &&
268
+ event.shift === wantShift &&
269
+ event.alt === wantAlt &&
270
+ (event.name === key || event.char === key));
271
+ }
272
+ //# sourceMappingURL=key-parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"key-parser.js","sourceRoot":"","sources":["../src/key-parser.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ export interface LastUsedEntry {
2
+ lastUsedAt: string;
3
+ }
4
+ export interface LastUsedClientState {
5
+ recentIds: string[];
6
+ updatedAt: string;
7
+ }
8
+ export interface LastUsedState {
9
+ version: number;
10
+ items: Record<string, LastUsedEntry>;
11
+ clients: Record<string, LastUsedClientState>;
12
+ projectRecentIds: string[];
13
+ updatedAt?: string;
14
+ }
15
+ export interface MarkLastUsedOptions {
16
+ itemId: string;
17
+ clientSession?: string;
18
+ usedAt?: string;
19
+ }
20
+ export declare function getLastUsedPath(projectRoot: string): string;
21
+ export declare function loadLastUsedState(projectRoot: string): LastUsedState;
22
+ export declare function markLastUsed(projectRoot: string, options: MarkLastUsedOptions): LastUsedState;
23
+ export declare function getLastUsedAt(projectRoot: string, itemId: string): string | undefined;
24
+ export declare function getRecentRankMap(projectRoot: string, clientSession?: string): Map<string, number>;
25
+ export declare function compareLastUsed(left: {
26
+ id: string;
27
+ lastUsedAt?: string;
28
+ }, right: {
29
+ id: string;
30
+ lastUsedAt?: string;
31
+ }, rankMap: Map<string, number>): number;
@@ -0,0 +1,93 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { getProjectStateDirFor } from "./paths.js";
3
+ import { join } from "node:path";
4
+ import { parseRecencyTimestamp } from "./recency.js";
5
+ const LAST_USED_VERSION = 1;
6
+ const MAX_RECENT_IDS = 64;
7
+ const EMPTY_STATE = {
8
+ version: LAST_USED_VERSION,
9
+ items: {},
10
+ clients: {},
11
+ projectRecentIds: [],
12
+ };
13
+ export function getLastUsedPath(projectRoot) {
14
+ return join(getProjectStateDirFor(projectRoot), "last-used.json");
15
+ }
16
+ export function loadLastUsedState(projectRoot) {
17
+ const path = getLastUsedPath(projectRoot);
18
+ if (!existsSync(path))
19
+ return { ...EMPTY_STATE };
20
+ try {
21
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
22
+ return normalizeLastUsedState(parsed);
23
+ }
24
+ catch {
25
+ return { ...EMPTY_STATE };
26
+ }
27
+ }
28
+ export function markLastUsed(projectRoot, options) {
29
+ const itemId = options.itemId?.trim();
30
+ if (!itemId)
31
+ return loadLastUsedState(projectRoot);
32
+ const usedAt = options.usedAt?.trim() || new Date().toISOString();
33
+ const state = loadLastUsedState(projectRoot);
34
+ state.items[itemId] = { lastUsedAt: usedAt };
35
+ state.projectRecentIds = pushRecentId(state.projectRecentIds, itemId);
36
+ const clientSession = options.clientSession?.trim();
37
+ if (clientSession) {
38
+ const existing = state.clients[clientSession] ?? { recentIds: [], updatedAt: usedAt };
39
+ state.clients[clientSession] = {
40
+ recentIds: pushRecentId(existing.recentIds, itemId),
41
+ updatedAt: usedAt,
42
+ };
43
+ }
44
+ state.updatedAt = usedAt;
45
+ persistLastUsedState(projectRoot, state);
46
+ return state;
47
+ }
48
+ export function getLastUsedAt(projectRoot, itemId) {
49
+ return loadLastUsedState(projectRoot).items[itemId]?.lastUsedAt;
50
+ }
51
+ export function getRecentRankMap(projectRoot, clientSession) {
52
+ const state = loadLastUsedState(projectRoot);
53
+ const clientIds = clientSession ? (state.clients[clientSession]?.recentIds ?? []) : [];
54
+ const ordered = [...clientIds, ...state.projectRecentIds.filter((id) => !clientIds.includes(id))];
55
+ return new Map(ordered.map((id, index) => [id, index]));
56
+ }
57
+ export function compareLastUsed(left, right, rankMap) {
58
+ const leftRank = rankMap.get(left.id) ?? Number.MAX_SAFE_INTEGER;
59
+ const rightRank = rankMap.get(right.id) ?? Number.MAX_SAFE_INTEGER;
60
+ if (leftRank !== rightRank)
61
+ return leftRank - rightRank;
62
+ const leftUsedAt = parseRecencyTimestamp(left.lastUsedAt) ?? 0;
63
+ const rightUsedAt = parseRecencyTimestamp(right.lastUsedAt) ?? 0;
64
+ return rightUsedAt - leftUsedAt;
65
+ }
66
+ function persistLastUsedState(projectRoot, state) {
67
+ const dir = getProjectStateDirFor(projectRoot);
68
+ mkdirSync(dir, { recursive: true });
69
+ writeFileSync(getLastUsedPath(projectRoot), JSON.stringify(state, null, 2));
70
+ }
71
+ function normalizeLastUsedState(state) {
72
+ const items = Object.fromEntries(Object.entries(state.items ?? {}).flatMap(([itemId, value]) => typeof value?.lastUsedAt === "string" ? [[itemId, { lastUsedAt: value.lastUsedAt }]] : []));
73
+ const clients = Object.fromEntries(Object.entries(state.clients ?? {}).map(([clientSession, value]) => [
74
+ clientSession,
75
+ {
76
+ recentIds: Array.isArray(value?.recentIds) ? value.recentIds.filter(Boolean).slice(0, MAX_RECENT_IDS) : [],
77
+ updatedAt: typeof value?.updatedAt === "string" ? value.updatedAt : "",
78
+ },
79
+ ]));
80
+ return {
81
+ version: LAST_USED_VERSION,
82
+ items,
83
+ clients,
84
+ projectRecentIds: Array.isArray(state.projectRecentIds)
85
+ ? state.projectRecentIds.filter(Boolean).slice(0, MAX_RECENT_IDS)
86
+ : [],
87
+ updatedAt: typeof state.updatedAt === "string" ? state.updatedAt : undefined,
88
+ };
89
+ }
90
+ function pushRecentId(ids, itemId) {
91
+ return [itemId, ...ids.filter((entry) => entry !== itemId)].slice(0, MAX_RECENT_IDS);
92
+ }
93
+ //# sourceMappingURL=last-used.js.map