@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,395 @@
1
+ #!/usr/bin/env node
2
+ import { realpathSync } from "node:fs";
3
+ import { spawn } from "node:child_process";
4
+ import { fileURLToPath } from "node:url";
5
+ import { runGenericCommand as defaultRunGenericCommand } from "../../../packages/cli-core/src/run-command.js";
6
+ import { ensureCompanionConnection } from "../../../packages/cli-core/src/companion-launcher.js";
7
+ import { createIpcClient as defaultCreateIpcClient } from "../../../packages/daemon-core/src/ipc-server.js";
8
+ import { getDefaultPaths } from "../../../packages/platform-core/src/paths.js";
9
+ import { discoverPets as defaultDiscoverPets } from "../../../packages/pet-core/src/discovery.js";
10
+ import { createStateFile as defaultCreateStateFile } from "../../../packages/app-state/src/state-file.js";
11
+ import { getSelectedPetId, setSelectedPet } from "../../../packages/app-state/src/state.js";
12
+ import { getAdapterInfo } from "../../../packages/adapters/src/adapter-info.js";
13
+
14
+ const CLIENT_DISPLAY_NAMES = Object.freeze({
15
+ generic: "Generic",
16
+ codex: "Codex",
17
+ "claude-code": "Claude Code",
18
+ antigravity: "Antigravity"
19
+ });
20
+
21
+ export function parseAiPetArgs(argv) {
22
+ if (!Array.isArray(argv) || argv.length === 0) {
23
+ throw new Error("haya-pet requires a command");
24
+ }
25
+
26
+ const [command, ...rest] = argv;
27
+
28
+ if (command === "run") {
29
+ return parseRunArgs(rest);
30
+ }
31
+
32
+ if (command === "pets") {
33
+ return parsePetsArgs(rest);
34
+ }
35
+
36
+ if (command === "start") {
37
+ return { command: "start" };
38
+ }
39
+
40
+ if (command === "stop") {
41
+ return { command: "stop" };
42
+ }
43
+
44
+ throw new Error(`Unsupported haya-pet command: ${command}`);
45
+ }
46
+
47
+ export async function runAiPet(argv, dependencies = {}) {
48
+ const parsed = parseAiPetArgs(argv);
49
+
50
+ if (parsed.command === "pets") {
51
+ return runPetsCommand(parsed, dependencies);
52
+ }
53
+
54
+ if (parsed.command === "start") {
55
+ return runStartCommand(parsed, dependencies);
56
+ }
57
+
58
+ if (parsed.command === "stop") {
59
+ return runStopCommand(parsed, dependencies);
60
+ }
61
+
62
+ return runRunCommand(parsed, dependencies);
63
+ }
64
+
65
+ // Ask a running companion to quit (the CLI counterpart to the tray's Quit).
66
+ export async function runStopCommand(_parsed, dependencies = {}) {
67
+ const print = dependencies.print ?? defaultPrint;
68
+ // Never auto-start just to stop; only connect if one is already running.
69
+ const { client } = await connectCompanion(dependencies, false);
70
+
71
+ if (!client) {
72
+ print("haya-pet: companion is not running.");
73
+ return { command: "stop", ok: true, wasRunning: false };
74
+ }
75
+
76
+ await client.send({ type: "shutdown" });
77
+ await client.close();
78
+ print("haya-pet: companion stopped.");
79
+ return { command: "stop", ok: true, wasRunning: true };
80
+ }
81
+
82
+ // Explicitly start the companion overlay (so users never need `npm start`).
83
+ export async function runStartCommand(_parsed, dependencies = {}) {
84
+ const print = dependencies.print ?? defaultPrint;
85
+ const { client, started, error, timedOut } = await connectCompanion(dependencies, true);
86
+
87
+ if (client) {
88
+ await client.close();
89
+ print(started ? "haya-pet: companion started." : "haya-pet: companion is already running.");
90
+ return { command: "start", ok: true, started };
91
+ }
92
+
93
+ if (error) {
94
+ print(`haya-pet: could not start the companion (${error.message}).`);
95
+ } else if (timedOut) {
96
+ print("haya-pet: started the companion but it did not come up in time.");
97
+ } else {
98
+ print("haya-pet: companion is not running and auto-start is disabled.");
99
+ }
100
+ return { command: "start", ok: false, started: false };
101
+ }
102
+
103
+ async function runRunCommand(parsed, dependencies) {
104
+ const runGenericCommand = dependencies.runGenericCommand ?? defaultRunGenericCommand;
105
+ const messageSender = await createMessageSender(dependencies);
106
+
107
+ try {
108
+ return await runGenericCommand({
109
+ command: parsed.childCommand,
110
+ args: parsed.childArgs,
111
+ cwd: dependencies.cwd ?? process.cwd(),
112
+ clientId: parsed.clientId,
113
+ clientDisplayName: CLIENT_DISPLAY_NAMES[parsed.clientId] ?? parsed.clientId,
114
+ observe: parsed.observe,
115
+ heartbeatIntervalMs: dependencies.heartbeatIntervalMs,
116
+ now: dependencies.now,
117
+ stdio: dependencies.stdio,
118
+ send: messageSender.send
119
+ });
120
+ } finally {
121
+ await messageSender.close();
122
+ }
123
+ }
124
+
125
+ export async function runPetsCommand(parsed, dependencies = {}) {
126
+ const paths = getDefaultPaths({
127
+ platform: dependencies.platform,
128
+ env: dependencies.env,
129
+ homeDir: dependencies.homeDir
130
+ });
131
+ const discoverPets = dependencies.discoverPets ?? defaultDiscoverPets;
132
+ const createStateFile = dependencies.createStateFile ?? defaultCreateStateFile;
133
+ const print = dependencies.print ?? defaultPrint;
134
+
135
+ const stateFile = createStateFile({ statePath: paths.statePath });
136
+ const pets = await discoverPets(paths.petSearchPaths);
137
+ const state = await stateFile.load();
138
+
139
+ if (parsed.action === "use") {
140
+ return usePet({ parsed, pets, state, stateFile, setSelectedPet, print });
141
+ }
142
+
143
+ return listPets({ pets, selectedId: getSelectedPetId(state), print });
144
+ }
145
+
146
+ function listPets({ pets, selectedId, print }) {
147
+ if (pets.length === 0) {
148
+ print("No pets found. Add a pet folder (pet.json + spritesheet) to ~/.codex/pets or ~/.haya-pet/pets.");
149
+ return { command: "pets", action: "list", pets: [], selectedId };
150
+ }
151
+
152
+ print("Installed pets (* = selected):");
153
+ for (const pet of pets) {
154
+ const marker = pet.manifest.id === selectedId ? "*" : " ";
155
+ print(`${marker} ${pet.manifest.id}\t${pet.manifest.name}`);
156
+ }
157
+
158
+ return { command: "pets", action: "list", pets: pets.map((pet) => pet.manifest.id), selectedId };
159
+ }
160
+
161
+ async function usePet({ parsed, pets, state, stateFile, setSelectedPet: applySelection, print }) {
162
+ const target = pets.find((pet) => pet.manifest.id === parsed.petId);
163
+
164
+ await stateFile.save(applySelection(state, parsed.petId));
165
+
166
+ if (!target) {
167
+ print(`Warning: "${parsed.petId}" is not currently installed; it will be used when available.`);
168
+ }
169
+ print(`Selected pet: ${parsed.petId}`);
170
+
171
+ return { command: "pets", action: "use", ok: true, petId: parsed.petId, installed: Boolean(target) };
172
+ }
173
+
174
+ function defaultPrint(line) {
175
+ process.stdout.write(`${line}\n`);
176
+ }
177
+
178
+ export async function main(argv = process.argv.slice(2), dependencies = {}) {
179
+ const result = await runAiPet(argv, dependencies);
180
+ process.exitCode = result.exitCode ?? 0;
181
+ return result;
182
+ }
183
+
184
+ function parsePetsArgs(args) {
185
+ if (args.length === 0) {
186
+ return { command: "pets", action: "list" };
187
+ }
188
+
189
+ const [action, ...rest] = args;
190
+
191
+ if (action === "list") {
192
+ return { command: "pets", action: "list" };
193
+ }
194
+
195
+ if (action === "use") {
196
+ const petId = rest[0];
197
+ if (!petId) {
198
+ throw new Error("pets use requires a pet id");
199
+ }
200
+ return { command: "pets", action: "use", petId };
201
+ }
202
+
203
+ throw new Error(`Unknown pets action: ${action}`);
204
+ }
205
+
206
+ function parseRunArgs(args) {
207
+ let clientId = "generic";
208
+ let observe = true; // live PTY observation is on by default; --no-observe opts out
209
+ let childStart = -1;
210
+
211
+ for (let index = 0; index < args.length; index += 1) {
212
+ const arg = args[index];
213
+
214
+ // Explicit separator: everything after it is the child command. `--` is
215
+ // optional — some shells (PowerShell's npm .ps1 shim) strip a lone `--`
216
+ // before it reaches the CLI, so we also accept a bare command word below.
217
+ if (arg === "--") {
218
+ childStart = index + 1;
219
+ break;
220
+ }
221
+
222
+ if (arg === "--client") {
223
+ const value = args[index + 1];
224
+ if (!value || value === "--") {
225
+ throw new Error("--client requires a value");
226
+ }
227
+
228
+ clientId = value;
229
+ index += 1;
230
+ continue;
231
+ }
232
+
233
+ if (arg === "--observe") {
234
+ observe = true;
235
+ continue;
236
+ }
237
+
238
+ if (arg === "--no-observe") {
239
+ observe = false;
240
+ continue;
241
+ }
242
+
243
+ if (arg.startsWith("-")) {
244
+ throw new Error(`Unknown run option: ${arg}`);
245
+ }
246
+
247
+ // First bare positional starts the child command (no `--` required).
248
+ childStart = index;
249
+ break;
250
+ }
251
+
252
+ if (childStart === -1) {
253
+ // No command given: fall back to the client's declared default command
254
+ // (e.g. `haya-pet run --client codex` launches `codex`).
255
+ const defaultCommand = getAdapterInfo(clientId)?.defaultCommand;
256
+ if (!defaultCommand) {
257
+ throw new Error(
258
+ `run requires a command (client "${clientId}" has no default command; pass one after the client)`
259
+ );
260
+ }
261
+
262
+ return { command: "run", clientId, observe, childCommand: defaultCommand, childArgs: [] };
263
+ }
264
+
265
+ const childArgs = args.slice(childStart);
266
+ const childCommand = childArgs.shift();
267
+
268
+ if (!childCommand) {
269
+ // `--` was the last token; fall back to the default command if any.
270
+ const defaultCommand = getAdapterInfo(clientId)?.defaultCommand;
271
+ if (defaultCommand) {
272
+ return { command: "run", clientId, observe, childCommand: defaultCommand, childArgs: [] };
273
+ }
274
+ throw new Error("run requires a child command");
275
+ }
276
+
277
+ return {
278
+ command: "run",
279
+ clientId,
280
+ observe,
281
+ childCommand,
282
+ childArgs
283
+ };
284
+ }
285
+
286
+ async function createMessageSender(dependencies) {
287
+ if (typeof dependencies.send === "function") {
288
+ return {
289
+ send: dependencies.send,
290
+ close: async () => {}
291
+ };
292
+ }
293
+
294
+ const { client, started } = await connectCompanion(dependencies, shouldAutoStart(dependencies));
295
+
296
+ if (!client) {
297
+ // No daemon and could not auto-start: still run the wrapped command and
298
+ // preserve its exit code. The pet just won't reflect this session (plan
299
+ // section 39 — wrappers degrade gracefully).
300
+ return { send: noopSend, close: noopClose };
301
+ }
302
+
303
+ if (started) {
304
+ process.stderr.write("haya-pet: started the companion overlay.\n");
305
+ }
306
+
307
+ return {
308
+ send: (message) => client.send(message),
309
+ close: () => client.close()
310
+ };
311
+ }
312
+
313
+ // Connects to the companion, optionally auto-starting it if it isn't running.
314
+ function connectCompanion(dependencies, autoStart) {
315
+ const createIpcClient = dependencies.createIpcClient ?? defaultCreateIpcClient;
316
+ const endpoint = dependencies.ipcEndpoint ?? getDefaultPaths({
317
+ platform: dependencies.platform,
318
+ env: dependencies.env,
319
+ homeDir: dependencies.homeDir
320
+ }).ipcEndpoint;
321
+ const launch = dependencies.launchCompanion ?? defaultLaunchCompanion;
322
+
323
+ return ensureCompanionConnection({
324
+ connect: () => createIpcClient({ endpoint }),
325
+ launch,
326
+ autoStart,
327
+ attempts: dependencies.connectAttempts,
328
+ intervalMs: dependencies.connectIntervalMs,
329
+ sleep: dependencies.sleep
330
+ });
331
+ }
332
+
333
+ function shouldAutoStart(dependencies) {
334
+ if (dependencies.autoStart !== undefined) {
335
+ return dependencies.autoStart;
336
+ }
337
+ const env = dependencies.env ?? process.env;
338
+ const flag = env.HAYA_PET_NO_AUTOSTART;
339
+ return flag !== "1" && flag !== "true";
340
+ }
341
+
342
+ // Spawns the Electron companion as a detached background process so it outlives
343
+ // this `haya-pet run` invocation. `import("electron")` resolves to the binary path
344
+ // when required outside an Electron runtime; the companion app dir is the sibling
345
+ // `apps/companion` package (whose package.json `main` is the overlay entry).
346
+ async function defaultLaunchCompanion() {
347
+ const electronModule = await import("electron");
348
+ const electronPath = electronModule.default ?? electronModule;
349
+ if (typeof electronPath !== "string") {
350
+ throw new Error("could not resolve the Electron binary");
351
+ }
352
+
353
+ const companionDir = fileURLToPath(new URL("../../companion", import.meta.url));
354
+ const child = spawn(electronPath, [companionDir], {
355
+ detached: true,
356
+ stdio: "ignore",
357
+ windowsHide: false
358
+ });
359
+ child.unref();
360
+ }
361
+
362
+ async function noopSend() {}
363
+
364
+ async function noopClose() {}
365
+
366
+ if (isDirectRun(import.meta.url, process.argv[1])) {
367
+ main()
368
+ .catch((error) => {
369
+ console.error(error.message);
370
+ process.exitCode = 1;
371
+ })
372
+ .finally(() => {
373
+ // The wrapped command has finished and all messages are flushed by now.
374
+ // Exit explicitly: a PTY (observe mode) can otherwise keep the event loop
375
+ // alive after the child exits.
376
+ process.exit(process.exitCode ?? 0);
377
+ });
378
+ }
379
+
380
+ function isDirectRun(moduleUrl, scriptPath) {
381
+ if (!scriptPath) {
382
+ return false;
383
+ }
384
+
385
+ const modulePath = fileURLToPath(moduleUrl);
386
+
387
+ // Resolve symlinks on both sides so the guard still fires when the CLI is
388
+ // invoked through an `npm link` shim (argv[1] is the symlink path, while
389
+ // import.meta.url resolves to the real file).
390
+ try {
391
+ return realpathSync(modulePath) === realpathSync(scriptPath);
392
+ } catch {
393
+ return modulePath === scriptPath;
394
+ }
395
+ }