@hayasaka7/haya-pet 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 (131) hide show
  1. package/.gitattributes +34 -0
  2. package/.github/workflows/release.yml +61 -0
  3. package/LICENSE +21 -0
  4. package/README.md +247 -0
  5. package/apps/cli/src/haya-pet.js +395 -0
  6. package/apps/cli/test/haya-pet.test.mjs +339 -0
  7. package/apps/companion/README.md +83 -0
  8. package/apps/companion/package.json +17 -0
  9. package/apps/companion/src/main/display-manager.js +71 -0
  10. package/apps/companion/src/main/index.js +349 -0
  11. package/apps/companion/src/main/lock-file.js +52 -0
  12. package/apps/companion/src/main/panel-placement.js +45 -0
  13. package/apps/companion/src/main/pet-loader.js +2 -0
  14. package/apps/companion/src/main/position-store.js +3 -0
  15. package/apps/companion/src/main/preload.cjs +13 -0
  16. package/apps/companion/src/main/state-file.js +2 -0
  17. package/apps/companion/src/main/terminal-helper-client.js +79 -0
  18. package/apps/companion/src/main/terminal-locator.js +44 -0
  19. package/apps/companion/src/main/tray-menu.js +79 -0
  20. package/apps/companion/src/main/window-options.js +66 -0
  21. package/apps/companion/src/renderer/index.html +18 -0
  22. package/apps/companion/src/renderer/interaction-controller.js +114 -0
  23. package/apps/companion/src/renderer/pet-window.js +275 -0
  24. package/apps/companion/src/renderer/session-bubbles.js +138 -0
  25. package/apps/companion/src/renderer/styles.css +225 -0
  26. package/apps/companion/src/renderer/task-talk-window.js +141 -0
  27. package/apps/companion/test/display-manager.test.mjs +48 -0
  28. package/apps/companion/test/interaction-controller.test.mjs +107 -0
  29. package/apps/companion/test/panel-placement.test.mjs +60 -0
  30. package/apps/companion/test/position-store.test.mjs +54 -0
  31. package/apps/companion/test/state-file.test.mjs +52 -0
  32. package/apps/companion/test/terminal-helper-client.test.mjs +68 -0
  33. package/apps/companion/test/terminal-locator.test.mjs +35 -0
  34. package/apps/companion/test/tray-menu.test.mjs +45 -0
  35. package/apps/companion/test/window-options.test.mjs +62 -0
  36. package/apps/pet-preview/index.html +42 -0
  37. package/apps/pet-preview/src/preview-app.js +123 -0
  38. package/apps/pet-preview/src/preview-state.js +70 -0
  39. package/apps/pet-preview/src/preview.css +125 -0
  40. package/apps/pet-preview/test/preview-state.test.mjs +62 -0
  41. package/assets/fallback-pet/README.md +16 -0
  42. package/assets/fallback-pet/pet.json +13 -0
  43. package/docs/architecture.md +144 -0
  44. package/docs/known-issues.md +49 -0
  45. package/docs/publishing.md +48 -0
  46. package/docs/screenshots/README.md +7 -0
  47. package/docs/screenshots/folder-collapsed.png +0 -0
  48. package/docs/screenshots/hero.png +0 -0
  49. package/docs/screenshots/pet-overlay.png +0 -0
  50. package/docs/screenshots/session-bubbles.png +0 -0
  51. package/docs/screenshots/tray-menu.png +0 -0
  52. package/docs/troubleshooting.md +36 -0
  53. package/native/README.md +80 -0
  54. package/native/linux-window-helper/README.md +29 -0
  55. package/native/mac-window-helper/README.md +30 -0
  56. package/native/win-window-helper/Program.cs +312 -0
  57. package/native/win-window-helper/README.md +53 -0
  58. package/native/win-window-helper/win-window-helper.csproj +12 -0
  59. package/package.json +35 -0
  60. package/packages/adapters/src/adapter-info.js +61 -0
  61. package/packages/adapters/src/capabilities.js +39 -0
  62. package/packages/adapters/src/heuristics.js +114 -0
  63. package/packages/adapters/src/output-observer.js +164 -0
  64. package/packages/adapters/src/routing.js +86 -0
  65. package/packages/adapters/test/adapter-info.test.mjs +35 -0
  66. package/packages/adapters/test/capabilities.test.mjs +44 -0
  67. package/packages/adapters/test/heuristics.test.mjs +42 -0
  68. package/packages/adapters/test/output-observer.test.mjs +142 -0
  69. package/packages/adapters/test/routing.test.mjs +93 -0
  70. package/packages/app-state/src/state-file.js +53 -0
  71. package/packages/app-state/src/state.js +80 -0
  72. package/packages/app-state/test/state.test.mjs +36 -0
  73. package/packages/cli-core/src/companion-launcher.js +69 -0
  74. package/packages/cli-core/src/pty-runner.js +96 -0
  75. package/packages/cli-core/src/run-command.js +353 -0
  76. package/packages/cli-core/src/strip-ansi.js +16 -0
  77. package/packages/cli-core/test/companion-launcher.test.mjs +98 -0
  78. package/packages/cli-core/test/run-command.test.mjs +177 -0
  79. package/packages/cli-core/test/strip-ansi.test.mjs +27 -0
  80. package/packages/daemon-core/src/daemon-runtime.js +49 -0
  81. package/packages/daemon-core/src/ipc-server.js +180 -0
  82. package/packages/daemon-core/src/ipc-transport.js +70 -0
  83. package/packages/daemon-core/src/singleton.js +46 -0
  84. package/packages/daemon-core/test/daemon-runtime.test.mjs +65 -0
  85. package/packages/daemon-core/test/ipc-server.test.mjs +70 -0
  86. package/packages/daemon-core/test/ipc-transport.test.mjs +72 -0
  87. package/packages/daemon-core/test/singleton.test.mjs +32 -0
  88. package/packages/pet-core/src/animation-state.js +84 -0
  89. package/packages/pet-core/src/animator.js +26 -0
  90. package/packages/pet-core/src/atlas.js +81 -0
  91. package/packages/pet-core/src/discovery.js +90 -0
  92. package/packages/pet-core/src/manifest.js +112 -0
  93. package/packages/pet-core/src/validation.js +43 -0
  94. package/packages/pet-core/test/animation-state.test.mjs +47 -0
  95. package/packages/pet-core/test/animator.test.mjs +31 -0
  96. package/packages/pet-core/test/atlas.test.mjs +81 -0
  97. package/packages/pet-core/test/discovery.test.mjs +93 -0
  98. package/packages/pet-core/test/manifest.test.mjs +93 -0
  99. package/packages/pet-core/test/validation.test.mjs +69 -0
  100. package/packages/platform-core/src/capabilities.js +49 -0
  101. package/packages/platform-core/src/paths.js +75 -0
  102. package/packages/platform-core/src/platform.js +15 -0
  103. package/packages/platform-core/test/platform.test.mjs +84 -0
  104. package/packages/protocol/src/messages.js +156 -0
  105. package/packages/protocol/test/messages.test.mjs +112 -0
  106. package/packages/session-core/src/bubble-linger.js +47 -0
  107. package/packages/session-core/src/bubble-view.js +79 -0
  108. package/packages/session-core/src/pet-state.js +56 -0
  109. package/packages/session-core/src/priority.js +56 -0
  110. package/packages/session-core/src/registry.js +144 -0
  111. package/packages/session-core/src/summaries.js +54 -0
  112. package/packages/session-core/test/bubble-linger.test.mjs +96 -0
  113. package/packages/session-core/test/bubble-view.test.mjs +79 -0
  114. package/packages/session-core/test/pet-state.test.mjs +118 -0
  115. package/packages/session-core/test/priority.test.mjs +53 -0
  116. package/packages/session-core/test/registry.test.mjs +161 -0
  117. package/packages/session-core/test/summaries.test.mjs +38 -0
  118. package/packages/task-core/src/approvals.js +91 -0
  119. package/packages/task-core/src/controls.js +61 -0
  120. package/packages/task-core/src/replies.js +80 -0
  121. package/packages/task-core/src/task-events.js +101 -0
  122. package/packages/task-core/src/task-status.js +93 -0
  123. package/packages/task-core/src/task-store.js +74 -0
  124. package/packages/task-core/test/approvals.test.mjs +61 -0
  125. package/packages/task-core/test/controls.test.mjs +61 -0
  126. package/packages/task-core/test/replies.test.mjs +51 -0
  127. package/packages/task-core/test/task-events.test.mjs +67 -0
  128. package/packages/task-core/test/task-status.test.mjs +49 -0
  129. package/packages/task-core/test/task-store.test.mjs +65 -0
  130. package/test/harness.mjs +22 -0
  131. package/test/run-tests.mjs +47 -0
@@ -0,0 +1,312 @@
1
+ // Haya Pet — Windows window helper.
2
+ //
3
+ // Implements the line-delimited JSON helper protocol documented in
4
+ // ../README.md. Reads one JSON request per line on stdin and writes one JSON
5
+ // response per line on stdout. Diagnostics (if any) go to stderr only.
6
+ //
7
+ // Supported ops:
8
+ // { "id": "...", "op": "capabilities" }
9
+ // { "id": "...", "op": "locate", "pid": <int>, "terminalPid": <int?> }
10
+
11
+ using System.Runtime.InteropServices;
12
+ using System.Runtime.Versioning;
13
+ using System.Text;
14
+ using System.Text.Json;
15
+
16
+ [assembly: SupportedOSPlatform("windows")]
17
+
18
+ var jsonOptions = new JsonSerializerOptions
19
+ {
20
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
21
+ };
22
+
23
+ // Per-monitor DPI awareness so GetWindowRect returns physical pixels the
24
+ // runtime can convert via its display manager. Best-effort; ignore failure.
25
+ try { Native.SetProcessDpiAwarenessContext(Native.DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); }
26
+ catch { /* older Windows: ignore */ }
27
+
28
+ string? line;
29
+ while ((line = Console.ReadLine()) != null)
30
+ {
31
+ if (string.IsNullOrWhiteSpace(line))
32
+ {
33
+ continue;
34
+ }
35
+
36
+ object response;
37
+ try
38
+ {
39
+ response = Handle(line);
40
+ }
41
+ catch (Exception ex)
42
+ {
43
+ response = new { id = (string?)null, ok = false, error = "invalid_request", detail = ex.Message };
44
+ }
45
+
46
+ Console.WriteLine(JsonSerializer.Serialize(response, jsonOptions));
47
+ Console.Out.Flush();
48
+ }
49
+
50
+ object Handle(string requestLine)
51
+ {
52
+ using var doc = JsonDocument.Parse(requestLine);
53
+ var root = doc.RootElement;
54
+
55
+ string? id = root.TryGetProperty("id", out var idEl) ? idEl.GetString() : null;
56
+ string op = root.TryGetProperty("op", out var opEl) ? opEl.GetString() ?? "" : "";
57
+
58
+ switch (op)
59
+ {
60
+ case "capabilities":
61
+ return new
62
+ {
63
+ id,
64
+ ok = true,
65
+ capabilities = new { locate = true, follow = false, permission = "granted" }
66
+ };
67
+
68
+ case "locate":
69
+ if (!root.TryGetProperty("pid", out var pidEl) || !pidEl.TryGetInt32(out int pid))
70
+ {
71
+ return new { id, ok = false, error = "invalid_request" };
72
+ }
73
+
74
+ int? terminalPid = root.TryGetProperty("terminalPid", out var tEl) && tEl.TryGetInt32(out int t) ? t : null;
75
+ var window = WindowLocator.Locate(pid, terminalPid);
76
+ return window is null
77
+ ? new { id, ok = false, error = "not_found" }
78
+ : new { id, ok = true, window };
79
+
80
+ default:
81
+ return new { id, ok = false, error = "invalid_request" };
82
+ }
83
+ }
84
+
85
+ /// <summary>Resolves the terminal window for an AI client process tree.</summary>
86
+ static class WindowLocator
87
+ {
88
+ // Process names that own a real terminal window, ranked first when matched.
89
+ private static readonly HashSet<string> KnownTerminals = new(StringComparer.OrdinalIgnoreCase)
90
+ {
91
+ "WindowsTerminal.exe", "powershell.exe", "pwsh.exe", "cmd.exe",
92
+ "Code.exe", "wezterm-gui.exe", "alacritty.exe", "conemu64.exe", "conemu.exe"
93
+ };
94
+
95
+ public static object? Locate(int pid, int? terminalPid)
96
+ {
97
+ var processNames = ProcessTree.SnapshotNames();
98
+ var parents = ProcessTree.SnapshotParents();
99
+
100
+ // Candidate pids: the client, an explicit terminal hint, and all ancestors.
101
+ var candidates = new HashSet<int>();
102
+ AddAncestors(pid, parents, candidates);
103
+ if (terminalPid is int hint)
104
+ {
105
+ AddAncestors(hint, parents, candidates);
106
+ }
107
+
108
+ (IntPtr hWnd, int pidOwner, Native.RECT rect, string title)? best = null;
109
+ int bestScore = int.MinValue;
110
+
111
+ Native.EnumWindows((hWnd, _) =>
112
+ {
113
+ if (!Native.IsWindowVisible(hWnd))
114
+ {
115
+ return true;
116
+ }
117
+
118
+ Native.GetWindowThreadProcessId(hWnd, out uint wpid);
119
+ if (!candidates.Contains((int)wpid))
120
+ {
121
+ return true;
122
+ }
123
+
124
+ if (!Native.GetWindowRect(hWnd, out var rect))
125
+ {
126
+ return true;
127
+ }
128
+
129
+ int width = rect.Right - rect.Left;
130
+ int height = rect.Bottom - rect.Top;
131
+ if (width <= 0 || height <= 0)
132
+ {
133
+ return true;
134
+ }
135
+
136
+ string title = GetTitle(hWnd);
137
+ if (title.Length == 0)
138
+ {
139
+ return true;
140
+ }
141
+
142
+ int score = Score((int)wpid, width, height, processNames);
143
+ if (score > bestScore)
144
+ {
145
+ bestScore = score;
146
+ best = (hWnd, (int)wpid, rect, title);
147
+ }
148
+
149
+ return true;
150
+ }, IntPtr.Zero);
151
+
152
+ if (best is null)
153
+ {
154
+ return null;
155
+ }
156
+
157
+ var b = best.Value;
158
+ bool knownTerminal = processNames.TryGetValue(b.pidOwner, out var name) && KnownTerminals.Contains(name);
159
+
160
+ return new
161
+ {
162
+ x = b.rect.Left,
163
+ y = b.rect.Top,
164
+ width = b.rect.Right - b.rect.Left,
165
+ height = b.rect.Bottom - b.rect.Top,
166
+ displayId = "primary",
167
+ title = b.title,
168
+ confidence = knownTerminal ? 0.8 : 0.5
169
+ };
170
+ }
171
+
172
+ private static int Score(int pid, int width, int height, IReadOnlyDictionary<int, string> names)
173
+ {
174
+ // Prefer known terminal processes, then larger windows.
175
+ int terminalBonus = names.TryGetValue(pid, out var name) && KnownTerminals.Contains(name) ? 1_000_000_000 : 0;
176
+ return terminalBonus + Math.Min(width * height, 900_000_000);
177
+ }
178
+
179
+ private static void AddAncestors(int pid, IReadOnlyDictionary<int, int> parents, HashSet<int> into)
180
+ {
181
+ int current = pid;
182
+ int guard = 0;
183
+ while (current > 0 && into.Add(current) && guard++ < 64)
184
+ {
185
+ if (!parents.TryGetValue(current, out int parent) || parent == current)
186
+ {
187
+ break;
188
+ }
189
+ current = parent;
190
+ }
191
+ }
192
+
193
+ private static string GetTitle(IntPtr hWnd)
194
+ {
195
+ int length = Native.GetWindowTextLengthW(hWnd);
196
+ if (length <= 0)
197
+ {
198
+ return "";
199
+ }
200
+
201
+ var sb = new StringBuilder(length + 1);
202
+ Native.GetWindowTextW(hWnd, sb, sb.Capacity);
203
+ return sb.ToString();
204
+ }
205
+ }
206
+
207
+ /// <summary>Reads the live process table once per request via Toolhelp.</summary>
208
+ static class ProcessTree
209
+ {
210
+ public static Dictionary<int, int> SnapshotParents() => Snapshot().parents;
211
+
212
+ public static Dictionary<int, string> SnapshotNames() => Snapshot().names;
213
+
214
+ private static (Dictionary<int, int> parents, Dictionary<int, string> names) Snapshot()
215
+ {
216
+ var parents = new Dictionary<int, int>();
217
+ var names = new Dictionary<int, string>();
218
+
219
+ IntPtr snapshot = Native.CreateToolhelp32Snapshot(Native.TH32CS_SNAPPROCESS, 0);
220
+ if (snapshot == Native.INVALID_HANDLE_VALUE)
221
+ {
222
+ return (parents, names);
223
+ }
224
+
225
+ try
226
+ {
227
+ var entry = new Native.PROCESSENTRY32W { dwSize = (uint)Marshal.SizeOf<Native.PROCESSENTRY32W>() };
228
+ if (Native.Process32FirstW(snapshot, ref entry))
229
+ {
230
+ do
231
+ {
232
+ parents[(int)entry.th32ProcessID] = (int)entry.th32ParentProcessID;
233
+ names[(int)entry.th32ProcessID] = entry.szExeFile ?? "";
234
+ }
235
+ while (Native.Process32NextW(snapshot, ref entry));
236
+ }
237
+ }
238
+ finally
239
+ {
240
+ Native.CloseHandle(snapshot);
241
+ }
242
+
243
+ return (parents, names);
244
+ }
245
+ }
246
+
247
+ static class Native
248
+ {
249
+ public const uint TH32CS_SNAPPROCESS = 0x00000002;
250
+ public static readonly IntPtr INVALID_HANDLE_VALUE = new(-1);
251
+ public static readonly IntPtr DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = new(-4);
252
+
253
+ public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
254
+
255
+ [StructLayout(LayoutKind.Sequential)]
256
+ public struct RECT
257
+ {
258
+ public int Left;
259
+ public int Top;
260
+ public int Right;
261
+ public int Bottom;
262
+ }
263
+
264
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
265
+ public struct PROCESSENTRY32W
266
+ {
267
+ public uint dwSize;
268
+ public uint cntUsage;
269
+ public uint th32ProcessID;
270
+ public IntPtr th32DefaultHeapID;
271
+ public uint th32ModuleID;
272
+ public uint cntThreads;
273
+ public uint th32ParentProcessID;
274
+ public int pcPriClassBase;
275
+ public uint dwFlags;
276
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
277
+ public string szExeFile;
278
+ }
279
+
280
+ [DllImport("user32.dll")]
281
+ public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
282
+
283
+ [DllImport("user32.dll")]
284
+ public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
285
+
286
+ [DllImport("user32.dll")]
287
+ public static extern bool IsWindowVisible(IntPtr hWnd);
288
+
289
+ [DllImport("user32.dll")]
290
+ public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
291
+
292
+ [DllImport("user32.dll", CharSet = CharSet.Unicode)]
293
+ public static extern int GetWindowTextW(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
294
+
295
+ [DllImport("user32.dll", CharSet = CharSet.Unicode)]
296
+ public static extern int GetWindowTextLengthW(IntPtr hWnd);
297
+
298
+ [DllImport("user32.dll")]
299
+ public static extern bool SetProcessDpiAwarenessContext(IntPtr value);
300
+
301
+ [DllImport("kernel32.dll", SetLastError = true)]
302
+ public static extern IntPtr CreateToolhelp32Snapshot(uint dwFlags, uint th32ProcessID);
303
+
304
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
305
+ public static extern bool Process32FirstW(IntPtr hSnapshot, ref PROCESSENTRY32W lppe);
306
+
307
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
308
+ public static extern bool Process32NextW(IntPtr hSnapshot, ref PROCESSENTRY32W lppe);
309
+
310
+ [DllImport("kernel32.dll", SetLastError = true)]
311
+ public static extern bool CloseHandle(IntPtr hObject);
312
+ }
@@ -0,0 +1,53 @@
1
+ # Windows Window Helper
2
+
3
+ Strategy id: `win32-window-helper` (best-effort).
4
+
5
+ Implements the shared helper protocol in [`../README.md`](../README.md).
6
+
7
+ ## Responsibility
8
+
9
+ - Walk from the AI client PID up the parent process tree to the hosting terminal
10
+ process (`WindowsTerminal.exe`, `powershell.exe`, `pwsh.exe`, `cmd.exe`, `Code.exe`).
11
+ - Enumerate top-level windows with `EnumWindows`, match the owning PID with
12
+ `GetWindowThreadProcessId`, and read bounds with `GetWindowRect`.
13
+ - Return window bounds for bubble attachment.
14
+
15
+ ## Implementation notes
16
+
17
+ - Suggested language: C# (.NET) or C++ with the Win32 API.
18
+ - Use per-monitor DPI awareness (`SetProcessDpiAwarenessContext`) so reported
19
+ bounds are physical pixels the runtime can convert.
20
+ - Windows Terminal tab/pane precision is not reliable; return the window rect and
21
+ let the runtime attach to a corner. Report `confidence` accordingly.
22
+ - VS Code integrated terminal pane precision requires a VS Code extension; the
23
+ helper should return the editor window rect with lower `confidence`.
24
+
25
+ ## Protocol mapping
26
+
27
+ - `op: "capabilities"` → `{ "locate": true, "follow": false, "permission": "granted" }`.
28
+ - `op: "locate"` → `window` rect on success, or `{ "ok": false, "error": "not_found" }`.
29
+
30
+ ## Status: implemented
31
+
32
+ This helper is implemented in C# (.NET, `Program.cs`) and builds with the .NET
33
+ SDK. It walks the process tree with Toolhelp (`CreateToolhelp32Snapshot`),
34
+ enumerates top-level windows (`EnumWindows` + `GetWindowThreadProcessId` +
35
+ `GetWindowRect`), prefers known terminal processes, and is per-monitor DPI aware.
36
+
37
+ ### Build
38
+
39
+ ```bash
40
+ cd native/win-window-helper
41
+ dotnet build -c Release
42
+ # -> bin/Release/net10.0-windows/haya-pet-win-window-helper.exe
43
+ ```
44
+
45
+ ### Try it
46
+
47
+ ```bash
48
+ echo {"id":"a","op":"capabilities"} | bin/Release/net10.0-windows/haya-pet-win-window-helper.exe
49
+ ```
50
+
51
+ The `apps/companion/src/main/terminal-helper-client.js` client spawns this exe
52
+ and speaks the protocol; the companion converts the returned window bounds to the
53
+ pet's display/DPI space via `display-manager.js`.
@@ -0,0 +1,12 @@
1
+ <Project Sdk="Microsoft.NET.Sdk">
2
+
3
+ <PropertyGroup>
4
+ <OutputType>Exe</OutputType>
5
+ <TargetFramework>net10.0-windows</TargetFramework>
6
+ <Nullable>enable</Nullable>
7
+ <ImplicitUsings>enable</ImplicitUsings>
8
+ <AssemblyName>haya-pet-win-window-helper</AssemblyName>
9
+ <InvariantGlobalization>true</InvariantGlobalization>
10
+ </PropertyGroup>
11
+
12
+ </Project>
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@hayasaka7/haya-pet",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Generic AI CLI pet runtime foundation.",
6
+ "license": "MIT",
7
+ "author": "Ai Hayasaka",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/HAYASAKA7/HAYA-PET.git"
11
+ },
12
+ "homepage": "https://github.com/HAYASAKA7/HAYA-PET#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/HAYASAKA7/HAYA-PET/issues"
15
+ },
16
+ "bin": {
17
+ "haya-pet": "apps/cli/src/haya-pet.js"
18
+ },
19
+ "scripts": {
20
+ "test": "node test/run-tests.mjs"
21
+ },
22
+ "workspaces": [
23
+ "apps/*",
24
+ "packages/*"
25
+ ],
26
+ "dependencies": {
27
+ "electron": "42.3.3"
28
+ },
29
+ "optionalDependencies": {
30
+ "node-pty": "^1.1.0"
31
+ },
32
+ "engines": {
33
+ "node": ">=16.20.0"
34
+ }
35
+ }
@@ -0,0 +1,61 @@
1
+ const ADAPTERS = Object.freeze({
2
+ codex: Object.freeze({
3
+ id: "codex",
4
+ displayName: "Codex",
5
+ detection: "manual",
6
+ supportLevel: 2,
7
+ defaultCommand: "codex",
8
+ knownProcessNames: Object.freeze(["codex"]),
9
+ stateHeuristics: Object.freeze(["pty_output"])
10
+ }),
11
+ "claude-code": Object.freeze({
12
+ id: "claude-code",
13
+ displayName: "Claude Code",
14
+ detection: "manual",
15
+ supportLevel: 2,
16
+ defaultCommand: "claude",
17
+ knownProcessNames: Object.freeze(["claude"]),
18
+ stateHeuristics: Object.freeze(["pty_output", "official_plugin"])
19
+ }),
20
+ antigravity: Object.freeze({
21
+ id: "antigravity",
22
+ displayName: "Antigravity",
23
+ detection: "manual",
24
+ supportLevel: 1,
25
+ defaultCommand: "antigravity",
26
+ knownProcessNames: Object.freeze(["antigravity"]),
27
+ stateHeuristics: Object.freeze(["wrapper"])
28
+ }),
29
+ generic: Object.freeze({
30
+ id: "generic",
31
+ displayName: "Generic",
32
+ detection: "manual",
33
+ supportLevel: 1,
34
+ defaultCommand: undefined,
35
+ knownProcessNames: Object.freeze([]),
36
+ stateHeuristics: Object.freeze(["wrapper"])
37
+ })
38
+ });
39
+
40
+ export const KNOWN_CLIENT_IDS = Object.freeze(["codex", "claude-code", "antigravity", "generic"]);
41
+
42
+ export function listAdapters() {
43
+ return KNOWN_CLIENT_IDS.map((id) => ADAPTERS[id]);
44
+ }
45
+
46
+ export function getAdapterInfo(clientId) {
47
+ return ADAPTERS[clientId];
48
+ }
49
+
50
+ export function resolveAdapterInfo(clientId) {
51
+ const known = ADAPTERS[clientId];
52
+ if (known) {
53
+ return known;
54
+ }
55
+
56
+ return {
57
+ ...ADAPTERS.generic,
58
+ id: clientId,
59
+ displayName: clientId
60
+ };
61
+ }
@@ -0,0 +1,39 @@
1
+ // Adapter capability declarations (product plan section 23). The talk window
2
+ // must hide or disable any control an adapter cannot safely support.
3
+
4
+ const WRAPPER_ONLY = Object.freeze({
5
+ canReply: "unsupported",
6
+ canApprove: "unsupported",
7
+ canPause: false,
8
+ canResume: false,
9
+ canStop: true,
10
+ canFocusTerminal: true,
11
+ canOpenTranscript: false,
12
+ canShowDiffs: false,
13
+ canShowFiles: false,
14
+ canShowTests: false
15
+ });
16
+
17
+ const PTY_OBSERVER = Object.freeze({
18
+ canReply: "best_effort",
19
+ canApprove: "best_effort",
20
+ canPause: false,
21
+ canResume: false,
22
+ canStop: true,
23
+ canFocusTerminal: true,
24
+ canOpenTranscript: false,
25
+ canShowDiffs: false,
26
+ canShowFiles: false,
27
+ canShowTests: false
28
+ });
29
+
30
+ const ADAPTER_CAPABILITIES = Object.freeze({
31
+ generic: WRAPPER_ONLY,
32
+ antigravity: WRAPPER_ONLY,
33
+ codex: PTY_OBSERVER,
34
+ "claude-code": PTY_OBSERVER
35
+ });
36
+
37
+ export function getAdapterCapabilities(clientId) {
38
+ return ADAPTER_CAPABILITIES[clientId] ?? WRAPPER_ONLY;
39
+ }
@@ -0,0 +1,114 @@
1
+ import { isAiClientState } from "../../protocol/src/messages.js";
2
+
3
+ // Generic regex rules from the product plan (section 35). Patterns are matched
4
+ // case-insensitively against a single line of client output.
5
+ export const DEFAULT_GENERIC_RULES = Object.freeze({
6
+ waiting_approval: ["approve", "permission", "continue\\?", "allow"],
7
+ waiting_user: ["\\? for", "press enter", "type your", "your input"],
8
+ running_tool: ["running", "executing", "\\$ "],
9
+ failed: ["error", "failed", "exception", "traceback"],
10
+ reviewing: ["review", "diff", "summary"]
11
+ });
12
+
13
+ const CLIENT_RULES = Object.freeze({
14
+ codex: Object.freeze({
15
+ ...DEFAULT_GENERIC_RULES,
16
+ editing_files: ["applying patch", "editing", "writing file", "apply_patch"],
17
+ waiting_approval: ["approve", "permission", "allow command", "run command\\?"]
18
+ }),
19
+ "claude-code": Object.freeze({
20
+ ...DEFAULT_GENERIC_RULES,
21
+ editing_files: ["edit ", "writing to", "applying edit"],
22
+ running_tool: ["running", "executing", "bash", "\\$ "],
23
+ waiting_approval: ["permission", "do you want to proceed", "allow"],
24
+ waiting_user: ["press enter", "your input", "human:"]
25
+ }),
26
+ antigravity: Object.freeze({
27
+ ...DEFAULT_GENERIC_RULES,
28
+ thinking: ["generating", "thinking"]
29
+ })
30
+ });
31
+
32
+ // Most-urgent-first precedence so a line matching several categories resolves
33
+ // to the state the user most needs to see (mirrors the session priority model).
34
+ const MATCH_PRECEDENCE = Object.freeze([
35
+ "waiting_approval",
36
+ "waiting_user",
37
+ "failed",
38
+ "editing_files",
39
+ "running_tool",
40
+ "reviewing",
41
+ "compacting",
42
+ "thinking",
43
+ "success",
44
+ "idle"
45
+ ]);
46
+
47
+ export function getClientRules(clientId) {
48
+ return CLIENT_RULES[clientId] ?? DEFAULT_GENERIC_RULES;
49
+ }
50
+
51
+ export function matchAiState(text, rules = DEFAULT_GENERIC_RULES) {
52
+ if (typeof text !== "string" || text === "") {
53
+ return undefined;
54
+ }
55
+
56
+ const compiled = compileRules(rules);
57
+
58
+ for (const state of MATCH_PRECEDENCE) {
59
+ const regexes = compiled[state];
60
+ if (!regexes) {
61
+ continue;
62
+ }
63
+
64
+ if (regexes.some((regex) => regex.test(text))) {
65
+ return state;
66
+ }
67
+ }
68
+
69
+ return undefined;
70
+ }
71
+
72
+ export function compileRules(rules) {
73
+ const compiled = {};
74
+
75
+ if (!isPlainObject(rules)) {
76
+ return compiled;
77
+ }
78
+
79
+ for (const [state, patterns] of Object.entries(rules)) {
80
+ if (!isAiClientState(state) || !Array.isArray(patterns)) {
81
+ continue;
82
+ }
83
+
84
+ const regexes = [];
85
+ for (const pattern of patterns) {
86
+ const regex = safeCompile(pattern);
87
+ if (regex) {
88
+ regexes.push(regex);
89
+ }
90
+ }
91
+
92
+ if (regexes.length > 0) {
93
+ compiled[state] = regexes;
94
+ }
95
+ }
96
+
97
+ return compiled;
98
+ }
99
+
100
+ function safeCompile(pattern) {
101
+ if (typeof pattern !== "string") {
102
+ return undefined;
103
+ }
104
+
105
+ try {
106
+ return new RegExp(pattern, "i");
107
+ } catch {
108
+ return undefined;
109
+ }
110
+ }
111
+
112
+ function isPlainObject(value) {
113
+ return typeof value === "object" && value !== null && !Array.isArray(value);
114
+ }