@cydm/pie 1.0.7 → 1.0.9

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/pie",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "Pie AI Agent CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,7 +16,7 @@
16
16
  "validate:models": "tsx scripts/validate-models-config.ts",
17
17
  "test": "npm run test:offline",
18
18
  "pretest:offline": "npm run build --workspace @pie/agent-framework && npm run build --workspace @pie/shared-headless-capabilities && npm run build",
19
- "test:offline": "node --test --import tsx test/core-services.test.ts test/cli.test.ts test/execution-state-manager.test.ts test/queued-steer-execution-state.test.ts test/architecture-boundaries.test.ts test/attachment-resolver.test.ts test/kimi-attachment-extension.test.ts test/document-attachment-extension.test.ts test/auto-compact-overflow.test.ts test/hierarchical-command-system.test.ts test/message-queue-unit.test.ts test/evals-harness.test.ts test/session-trace.test.ts test/runtime-mode-context.test.ts test/tool-policy.test.ts test/yolo-command.test.ts test/source-size-guard.test.ts test/runtime-logger.test.ts test/release-readiness.test.ts test/code-intel-typescript-provider.test.ts test/mention-file-select.terminal.test.ts test/tool-partial-display.terminal.test.ts test/tool-name-rendering.test.ts test/tools-registry.test.ts test/system-prompt-contract.test.ts test/daily-agent-smoke.terminal.test.ts test/viewport-position-final.test.ts",
19
+ "test:offline": "node --test --import tsx test/core-services.test.ts test/cli.test.ts test/execution-state-manager.test.ts test/queued-steer-execution-state.test.ts test/architecture-boundaries.test.ts test/attachment-resolver.test.ts test/kimi-attachment-extension.test.ts test/document-attachment-extension.test.ts test/auto-compact-overflow.test.ts test/hierarchical-command-system.test.ts test/message-queue-unit.test.ts test/evals-harness.test.ts test/session-trace.test.ts test/runtime-mode-context.test.ts test/tool-policy.test.ts test/yolo-command.test.ts test/footer-lifecycle.test.ts test/files-utils.test.ts test/source-size-guard.test.ts test/runtime-logger.test.ts test/release-readiness.test.ts test/code-intel-typescript-provider.test.ts test/init-extension-contract.test.ts test/skills-runtime.test.ts test/mention-file-select.terminal.test.ts test/tool-partial-display.terminal.test.ts test/tool-name-rendering.test.ts test/tools-registry.test.ts test/system-prompt-contract.test.ts test/daily-agent-smoke.terminal.test.ts test/viewport-position-final.test.ts",
20
20
  "test:todo-tui": "PIE_REAL_TODO_TUI=1 node --test --import tsx test/todo-closure.terminal.test.ts",
21
21
  "test:daily-tui": "npm run test:daily-tui:quick",
22
22
  "test:daily-tui:quick": "PIE_REAL_DAILY_TUI=1 PIE_DAILY_TUI_LEVEL=quick node --test --import tsx test/daily-agent-smoke.terminal.test.ts",
@@ -1,121 +0,0 @@
1
- ---
2
- name: pie-unity-rpc
3
- description: Connect to a local pie-unity instance for Unity project discovery, health checks, manifest lookup, and a small set of stable RPC/tool calls. Use when working on a Unity project that imports pie-unity.
4
- ---
5
-
6
- # Pie Unity RPC
7
-
8
- Use the helper script `pie-unity-rpc.js` to talk to local `pie-unity` instances. Prefer the shell-style host workflow:
9
-
10
- 1. Discover instances
11
- 2. Select the target project or instance
12
- 3. Check health
13
- 4. Read only the manifest slice you need
14
- 5. Query or inspect Unity state
15
- 6. Edit the scene, read logs, refresh Unity after file changes, or run a frame-scheduled Unity script task
16
- 7. Verify the result
17
-
18
- Do not dump the full manifest unless the user explicitly asks for it.
19
-
20
- Script resource: `pie-unity-rpc.js`
21
-
22
- Command templates below use `<script>` as a placeholder for the resolved script path in the current host.
23
-
24
- ## Discover
25
-
26
- ```bash
27
- node <script> instances
28
- node <script> instances --project "/abs/path/to/project"
29
- ```
30
-
31
- Selection behavior:
32
-
33
- - if there is only one active instance, the helper uses it automatically
34
- - `--project` is treated as a matching hint, not only as an exact path
35
- - if multiple candidates remain, the helper returns a structured candidate list; show it and ask the user which instance to use
36
- - prefer `--instance` when the user already knows the target instance ID
37
-
38
- ## Health
39
-
40
- ```bash
41
- node <script> health --project "/abs/path/to/project"
42
- node <script> health --instance "<instance-id>"
43
- ```
44
-
45
- ## Manifest
46
-
47
- Read only what you need:
48
-
49
- ```bash
50
- node <script> manifest --project "/abs/path/to/project"
51
- node <script> manifest --project "/abs/path/to/project" --namespace chat
52
- node <script> manifest --project "/abs/path/to/project" --name unity_project_inspect
53
- ```
54
-
55
- ## Host Workflow
56
-
57
- Prefer the host commands over raw `tool` names:
58
-
59
- ```bash
60
- node <script> query --project "/abs/path/to/project" --data '{"scope":"scene_object","name":"Main Camera","limit":1}'
61
- node <script> inspect --project "/abs/path/to/project" --data '{"target":{"path":"Main Camera"}}'
62
- node <script> edit --project "/abs/path/to/project" --data '{"action":"create_scene_object","name":"CubeA","primitiveType":"Cube","x":0,"y":1,"z":0}'
63
- node <script> log-read --project "/abs/path/to/project" --data '{"source":"active","tailLines":120,"contains":"error"}'
64
- ```
65
-
66
- For Unity project files, use normal Pie file tools rooted at the Unity project. The RPC helper deliberately does not provide separate project file read/write commands.
67
-
68
- ## Script Run
69
-
70
- Use `script-run` only for JavaScript generator tasks that should execute inside Unity. The script must define a generator entrypoint. For multi-frame work, yield `ctx.nextFrame()`, `ctx.waitFrames(n)`, or `ctx.waitSeconds(s)`. The command returns after the task completes, fails, or times out.
71
-
72
- ```bash
73
- node <script> script-run --project "/abs/path/to/project" --data '{"script":"export function* run(ctx, args) { return ctx.query({ scope: \"scene_object\", limit: 3 }); }"}'
74
- node <script> script-run --project "/abs/path/to/project" --data '{"script":"export function* run(ctx, args) { for (let i = 0; i < 60; i++) { ctx.log(\"frame\", i); yield ctx.nextFrame(); } return { ok: true }; }","totalTimeoutMs":10000,"maxFrames":120}'
75
- ```
76
-
77
- Do not pass C#, shader source, or raw file contents to `script-run`. Use normal Pie file tools for Unity project files, then call `unity_refresh` through `tool` if Unity must import changed assets. Do not write long synchronous loops. Express long goals as short generator steps that yield between frames.
78
-
79
- ## Raw Tool / RPC
80
-
81
- Use raw `tool` or `rpc` only when the shell-style commands do not cover what you need.
82
-
83
- ## Retry Behavior
84
-
85
- The helper automatically retries when `pie-unity` reports temporary unavailability during compilation or domain reload. Use `--wait-ms` to override the default polling delay.
86
-
87
- When using `rpc --method pie_chat.set_config`, the helper waits for the Unity-side `configAppliedVersion` ack when available, then adds a short settle window before returning. Use `--config-settle-ms` to override that delay if needed.
88
-
89
- ## Guidance
90
-
91
- - Start with `instances` if the target Unity project is not yet confirmed.
92
- - Start with `health` if the project is known.
93
- - Read this skill contract first. Do not start by scanning the local repo or helper source code unless the task is explicitly about the repo/tooling itself.
94
- - If a command returns an ambiguous instance error, present the candidate list and ask the user to choose one instance.
95
- - Read `manifest --namespace unity` or `manifest --namespace unity.script` before using unfamiliar host commands.
96
- - Once the target `projectPath` is known, keep filesystem exploration scoped to that Unity project. Do not search unrelated repo directories to rediscover the project.
97
- - For scene setup with named objects such as `Player`, `Goal`, or `Main Camera`, start with `query` for exact names before creating anything.
98
- - If `query` finds exactly one match, reuse and update it instead of creating a duplicate.
99
- - If `query` finds multiple matches, do not create another duplicate; inspect/select one explicit result or ask the user.
100
- - Prefer `query -> inspect -> edit -> inspect` over ad-hoc menu execution. Use `edit` actions `add_component`, `remove_component`, `set_component_enabled`, and `set_component_property` for common scene object component changes instead of writing one-off script-run code.
101
- - Use normal file tools with the Unity project root for project text files; do not route file access through Unity RPC.
102
- - Read helper source code only when command behavior is unclear after reading this skill contract or manifest output.
103
- - After editing C# scripts under `Assets`, call `tool --tool unity_refresh --data {}` if needed, then expect a brief compile/domain-reload window. Re-check `health` or retry the next host command instead of assuming Unity is stuck. If compile/runtime errors appear, use `log-read` instead of guessing.
104
- - Prefer `script-run` when the task needs filtering, derived values, or multi-frame composition inside Unity.
105
-
106
- ## Smoke Test
107
-
108
- Recommended minimum verification sequence:
109
-
110
- ```bash
111
- node <script> health --project "/abs/path/to/project"
112
- node <script> manifest --project "/abs/path/to/project" --namespace unity
113
- node <script> tool --project "/abs/path/to/project" --tool chat_send --data '{"text":"respond with exactly pong"}'
114
- node <script> tool --project "/abs/path/to/project" --tool chat_get_state
115
- node <script> tool --project "/abs/path/to/project" --tool chat_get_messages
116
- ```
117
-
118
- Expected result:
119
-
120
- - `chat_get_state` eventually returns `isBusy: false`
121
- - the final assistant message contains the expected response
@@ -1,417 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { readFileSync, existsSync } from "node:fs";
4
- import { homedir } from "node:os";
5
- import path from "node:path";
6
- import { pathToFileURL } from "node:url";
7
-
8
- const REGISTRY_FILE = path.join(homedir(), ".pie-unity", "registry.json");
9
- const DEFAULT_WAIT_MS = 600;
10
- const DEFAULT_RETRIES = 30;
11
- const ACTIVE_INSTANCE_MAX_AGE_SEC = 120;
12
-
13
- class InstanceSelectionError extends Error {
14
- constructor(code, message, details = {}) {
15
- super(message);
16
- this.name = "InstanceSelectionError";
17
- this.code = code;
18
- this.details = details;
19
- }
20
- }
21
-
22
- export function parseArgs(argv) {
23
- const args = argv.slice(2);
24
- const command = args[0] || "instances";
25
- const flags = {};
26
- for (let i = 1; i < args.length; i += 1) {
27
- const item = args[i];
28
- if (!item.startsWith("--")) continue;
29
- const key = item.slice(2);
30
- const next = args[i + 1];
31
- if (!next || next.startsWith("--")) {
32
- flags[key] = "true";
33
- continue;
34
- }
35
- flags[key] = next;
36
- i += 1;
37
- }
38
- return { command, flags };
39
- }
40
-
41
- export function loadRegistry(filePath = REGISTRY_FILE) {
42
- if (!existsSync(filePath)) {
43
- return [];
44
- }
45
- try {
46
- const raw = readFileSync(filePath, "utf8").replace(/^\uFEFF/, "");
47
- const json = JSON.parse(raw);
48
- const items = Array.isArray(json?.instances) ? json.instances : [];
49
- return dedupeInstances(items.filter(Boolean));
50
- } catch {
51
- return [];
52
- }
53
- }
54
-
55
- export function getActiveInstances(instances, nowUnix = Math.floor(Date.now() / 1000)) {
56
- const recent = instances.filter((item) => {
57
- const lastSeenUnix = Number(item?.lastSeenUnix || 0);
58
- return lastSeenUnix > 0 && nowUnix - lastSeenUnix <= ACTIVE_INSTANCE_MAX_AGE_SEC;
59
- });
60
- return recent.length > 0 ? recent : instances;
61
- }
62
-
63
- function basenameMatch(left, right) {
64
- return path.basename(left || "") !== ""
65
- && path.basename(left || "") === path.basename(right || "");
66
- }
67
-
68
- function computeProjectMatchScore(requestedProject, candidateProject) {
69
- const requested = normalizePath(requestedProject || "");
70
- const candidate = normalizePath(candidateProject || "");
71
- if (!requested || !candidate) return 0;
72
- if (requested === candidate) return 1000;
73
- if (basenameMatch(requested, candidate)) return 700;
74
-
75
- const requestedWithSlash = requested.endsWith("/") ? requested : `${requested}/`;
76
- const candidateWithSlash = candidate.endsWith("/") ? candidate : `${candidate}/`;
77
- if (requestedWithSlash.startsWith(candidateWithSlash) || candidateWithSlash.startsWith(requestedWithSlash)) {
78
- const distance = Math.abs(requested.length - candidate.length);
79
- return 500 - Math.min(distance, 400);
80
- }
81
-
82
- return 0;
83
- }
84
-
85
- function formatCandidate(item) {
86
- return {
87
- instanceId: String(item?.instanceId || ""),
88
- projectPath: normalizePath(item?.projectPath || ""),
89
- projectName: String(item?.projectName || ""),
90
- mode: String(item?.mode || ""),
91
- port: Number(item?.port || 0),
92
- lastSeenUnix: Number(item?.lastSeenUnix || 0),
93
- };
94
- }
95
-
96
- function buildSelectionError(code, message, flags, candidates) {
97
- return new InstanceSelectionError(code, message, {
98
- reason: code === "PIE_UNITY_INSTANCE_AMBIGUOUS" ? "ambiguous" : "no_match",
99
- requestedProject: normalizePath(flags.project || ""),
100
- requestedInstance: String(flags.instance || "").trim(),
101
- requestedPort: Number(flags.port || 0) || 0,
102
- candidates: candidates.map(formatCandidate),
103
- });
104
- }
105
-
106
- export function selectInstance(instances, flags) {
107
- const activeInstances = getActiveInstances(instances);
108
- const project = normalizePath(flags.project || "");
109
- const instanceId = String(flags.instance || "").trim();
110
- const explicitPort = Number(flags.port || 0);
111
-
112
- if (explicitPort > 0) {
113
- return {
114
- instanceId: instanceId || `port_${explicitPort}`,
115
- projectPath: project,
116
- port: explicitPort,
117
- mode: "unknown",
118
- };
119
- }
120
-
121
- if (instanceId) {
122
- const match = activeInstances.find((item) => String(item.instanceId || "") === instanceId);
123
- if (match) return match;
124
- throw buildSelectionError(
125
- "PIE_UNITY_INSTANCE_NOT_FOUND",
126
- `No pie-unity instance found for --instance ${instanceId}.`,
127
- flags,
128
- activeInstances,
129
- );
130
- }
131
-
132
- if (project && activeInstances.length > 0) {
133
- const scored = activeInstances
134
- .map((item) => ({ item, score: computeProjectMatchScore(project, item.projectPath || "") }))
135
- .filter((entry) => entry.score > 0)
136
- .sort((left, right) => right.score - left.score || Number(right.item.lastSeenUnix || 0) - Number(left.item.lastSeenUnix || 0));
137
-
138
- if (scored.length === 1) {
139
- return scored[0].item;
140
- }
141
-
142
- if (scored.length > 1) {
143
- const bestScore = scored[0].score;
144
- const best = scored.filter((entry) => entry.score === bestScore).map((entry) => entry.item);
145
- if (best.length === 1) {
146
- return best[0];
147
- }
148
- throw buildSelectionError(
149
- "PIE_UNITY_INSTANCE_AMBIGUOUS",
150
- `Multiple pie-unity instances match project hint ${project}. Pass --instance or --port.`,
151
- flags,
152
- best,
153
- );
154
- }
155
- }
156
-
157
- if (activeInstances.length === 1) {
158
- return activeInstances[0];
159
- }
160
-
161
- if (activeInstances.length > 1) {
162
- throw buildSelectionError(
163
- "PIE_UNITY_INSTANCE_AMBIGUOUS",
164
- "Multiple pie-unity instances found. Pass --instance, --port, or a more specific --project.",
165
- flags,
166
- activeInstances,
167
- );
168
- }
169
-
170
- throw buildSelectionError(
171
- "PIE_UNITY_INSTANCE_NOT_FOUND",
172
- project
173
- ? `No matching pie-unity instance found for project hint ${project}.`
174
- : "No matching pie-unity instance found.",
175
- flags,
176
- activeInstances,
177
- );
178
- }
179
-
180
- export async function requestJson(url, init = {}, options = {}) {
181
- const waitMs = Number(options.waitMs || DEFAULT_WAIT_MS);
182
- const retries = Number(options.retries || DEFAULT_RETRIES);
183
- let lastError;
184
- for (let attempt = 0; attempt < retries; attempt += 1) {
185
- try {
186
- const response = await fetch(url, init);
187
- const text = await response.text();
188
- const json = text ? JSON.parse(text) : {};
189
-
190
- if (!response.ok) {
191
- throw new Error(`HTTP ${response.status}: ${text}`);
192
- }
193
-
194
- const unavailable = extractAvailability(json);
195
- if (unavailable) {
196
- lastError = new Error(unavailable.message || "pie-unity temporarily unavailable");
197
- if (attempt < retries - 1) {
198
- await sleep(waitMs);
199
- continue;
200
- }
201
- }
202
-
203
- return json;
204
- } catch (error) {
205
- lastError = error;
206
- if (attempt >= retries - 1) break;
207
- await sleep(waitMs);
208
- }
209
- }
210
- throw lastError || new Error("Request failed");
211
- }
212
-
213
- export async function runCli(argv = process.argv) {
214
- const { command, flags } = parseArgs(argv);
215
- if (command === "instances") {
216
- const instances = getActiveInstances(loadRegistry());
217
- const filtered = flags.project
218
- ? instances.filter((item) => computeProjectMatchScore(flags.project, item.projectPath || "") > 0)
219
- : instances;
220
- writeJson({ service: "pie-unity", instances: filtered });
221
- return;
222
- }
223
-
224
- const instance = selectInstance(loadRegistry(), flags);
225
- const baseUrl = `http://127.0.0.1:${instance.port}`;
226
-
227
- switch (command) {
228
- case "health": {
229
- writeJson(normalizeEnvelope(await requestJson(`${baseUrl}/health`, {}, flags)));
230
- return;
231
- }
232
- case "manifest": {
233
- const query = new URLSearchParams();
234
- if (flags.namespace) query.set("namespace", flags.namespace);
235
- if (flags.name) query.set("name", flags.name);
236
- const suffix = query.toString() ? `?${query.toString()}` : "";
237
- writeJson(normalizeEnvelope(await requestJson(`${baseUrl}/manifest${suffix}`, {}, flags)));
238
- return;
239
- }
240
- case "query": {
241
- writeJson(normalizeEnvelope(await requestJson(`${baseUrl}/tool/${encodeURIComponent("unity_scene_query")}`, postJson(parseData(flags.data)), flags)));
242
- return;
243
- }
244
- case "inspect": {
245
- writeJson(normalizeEnvelope(await requestJson(`${baseUrl}/tool/${encodeURIComponent("unity_scene_object_inspect")}`, postJson(parseData(flags.data)), flags)));
246
- return;
247
- }
248
- case "edit": {
249
- writeJson(normalizeEnvelope(await requestJson(`${baseUrl}/tool/${encodeURIComponent("unity_scene_object_edit")}`, postJson(parseData(flags.data)), flags)));
250
- return;
251
- }
252
- case "log-read": {
253
- writeJson(normalizeEnvelope(await requestJson(`${baseUrl}/tool/${encodeURIComponent("unity_log_read")}`, postJson(parseData(flags.data)), flags)));
254
- return;
255
- }
256
- case "script-run": {
257
- writeJson(normalizeEnvelope(await requestJson(`${baseUrl}/tool/${encodeURIComponent("unity_script_run")}`, postJson(parseData(flags.data)), flags)));
258
- return;
259
- }
260
- case "tool": {
261
- const name = String(flags.tool || "").trim();
262
- if (!name) throw new Error("--tool is required");
263
- const body = parseData(flags.data);
264
- writeJson(normalizeEnvelope(await requestJson(`${baseUrl}/tool/${encodeURIComponent(name)}`, postJson(body), flags)));
265
- return;
266
- }
267
- case "rpc": {
268
- const name = String(flags.method || "").trim();
269
- if (!name) throw new Error("--method is required");
270
- const body = parseData(flags.data);
271
- const response = normalizeEnvelope(await requestJson(`${baseUrl}/rpc/${encodeURIComponent(name)}`, postJson(body), flags));
272
- if (name === "pie_chat.set_config") {
273
- await settleChatConfig(baseUrl, flags, Number(response?.result?.configAppliedVersion || 0));
274
- }
275
- writeJson(response);
276
- return;
277
- }
278
- default:
279
- throw new Error(`Unknown command: ${command}`);
280
- }
281
- }
282
-
283
- function extractAvailability(json) {
284
- const raw = json?.serverAvailability || json?.availability || "";
285
- if (!raw) return null;
286
- if (typeof raw === "string") {
287
- try {
288
- return JSON.parse(raw);
289
- } catch {
290
- return { message: raw };
291
- }
292
- }
293
- return raw;
294
- }
295
-
296
- function normalizeEnvelope(json) {
297
- if (!json || typeof json !== "object") return json;
298
- if (!("result" in json) && !("serverAvailability" in json)) return json;
299
-
300
- const next = { ...json };
301
- if (typeof next.result === "string") {
302
- const trimmed = next.result.trim();
303
- if (trimmed === "null") {
304
- next.result = null;
305
- } else if ((trimmed.startsWith("{") && trimmed.endsWith("}")) || (trimmed.startsWith("[") && trimmed.endsWith("]"))) {
306
- try {
307
- next.result = JSON.parse(trimmed);
308
- } catch {
309
- // Keep original string.
310
- }
311
- }
312
- }
313
-
314
- if (typeof next.serverAvailability === "string" && next.serverAvailability.trim()) {
315
- try {
316
- next.serverAvailability = JSON.parse(next.serverAvailability);
317
- } catch {
318
- // Keep original string.
319
- }
320
- }
321
-
322
- return next;
323
- }
324
-
325
- function parseData(input) {
326
- if (!input) return {};
327
- return JSON.parse(input);
328
- }
329
-
330
- function postJson(body) {
331
- return {
332
- method: "POST",
333
- headers: { "Content-Type": "application/json" },
334
- body: JSON.stringify(body || {}),
335
- };
336
- }
337
-
338
- async function settleChatConfig(baseUrl, flags, expectedVersion = 0) {
339
- const settleMs = Math.max(0, Number(flags.configSettleMs || 500));
340
- const pollMs = Math.max(100, Math.min(settleMs || 200, 250));
341
- const deadline = Date.now() + Math.max(1500, settleMs * 4 || 1500);
342
-
343
- while (Date.now() < deadline) {
344
- try {
345
- const state = normalizeEnvelope(
346
- await requestJson(`${baseUrl}/tool/${encodeURIComponent("chat_get_state")}`, {}, {
347
- waitMs: pollMs,
348
- retries: 1,
349
- }),
350
- );
351
- const appliedVersion = Number(state?.result?.configAppliedVersion || 0);
352
- const versionApplied = expectedVersion > 0 ? appliedVersion >= expectedVersion : false;
353
- if (versionApplied || !state?.result?.isBusy) {
354
- if (settleMs > 0) {
355
- await sleep(settleMs);
356
- }
357
- return;
358
- }
359
- } catch {
360
- // Ignore transient polling failures while the bridge settles.
361
- }
362
- await sleep(pollMs);
363
- }
364
-
365
- if (settleMs > 0) {
366
- await sleep(settleMs);
367
- }
368
- }
369
-
370
- function sleep(ms) {
371
- return new Promise((resolve) => setTimeout(resolve, ms));
372
- }
373
-
374
- function normalizePath(value) {
375
- return String(value || "").replace(/\\/g, "/");
376
- }
377
-
378
- function writeJson(payload) {
379
- process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
380
- }
381
-
382
- function dedupeInstances(items) {
383
- const sorted = [...items].sort((a, b) => {
384
- const timeA = Number(a?.lastSeenUnix || 0);
385
- const timeB = Number(b?.lastSeenUnix || 0);
386
- return timeB - timeA;
387
- });
388
- const seen = new Set();
389
- const next = [];
390
- for (const item of sorted) {
391
- const key = [
392
- String(item?.instanceId || ""),
393
- normalizePath(item?.projectPath || ""),
394
- String(item?.mode || ""),
395
- ].join("|");
396
- if (seen.has(key)) continue;
397
- seen.add(key);
398
- next.push(item);
399
- }
400
- return next;
401
- }
402
-
403
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
404
- runCli().catch((error) => {
405
- if (error instanceof InstanceSelectionError) {
406
- process.stderr.write(`${JSON.stringify({
407
- error: error.message,
408
- code: error.code,
409
- ...error.details,
410
- }, null, 2)}\n`);
411
- process.exitCode = 2;
412
- return;
413
- }
414
- process.stderr.write(`${error?.message || error}\n`);
415
- process.exitCode = 1;
416
- });
417
- }