@awak-app/simy-cli 0.5.17 → 0.5.19

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": "@awak-app/simy-cli",
3
- "version": "0.5.17",
3
+ "version": "0.5.19",
4
4
  "description": "Local SIMY executor for non-coding Local Tasks and Agentic Loop runs.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/agent.js CHANGED
@@ -53,6 +53,7 @@ import {
53
53
  createConnectionStatusTracker,
54
54
  desktopAuthStatus,
55
55
  desktopTaskStatus,
56
+ desktopTaskDetail,
56
57
  } from "./desktop-status.js";
57
58
  import { recoveryContractForPreflight } from "./orchestrator/recovery.js";
58
59
  import {
@@ -1661,6 +1662,7 @@ export async function startAgent({
1661
1662
  },
1662
1663
  requestDesktopLogin,
1663
1664
  getDesktopStatus,
1665
+ getDesktopTask: (key) => desktopTaskDetail([registry, directRegistry, localTaskRegistry], key),
1664
1666
  async refreshDesktopStatus() {
1665
1667
  if (isSessionValid(session, Date.now(), apiOrigin)) {
1666
1668
  await heartbeat().catch((error) => reportHeartbeatError(error, quiet));
@@ -13,7 +13,7 @@ import {
13
13
  } from "./durable-local-task-steps-contract.js";
14
14
  import { buildExecutionCapabilityContract } from "./execution-capability-contract.js";
15
15
 
16
- export const CLI_VERSION = "0.5.17";
16
+ export const CLI_VERSION = "0.5.19";
17
17
  export const CLI_API_CONTRACT_VERSION = 10;
18
18
 
19
19
  export function withCliContract(
@@ -144,6 +144,12 @@ export async function startDesktopHost({ input = process.stdin, output = process
144
144
  queue = queue.then(async () => {
145
145
  try {
146
146
  const action = command.action;
147
+ if (action === "task_detail") {
148
+ const task = await agent.getDesktopTask?.(command.taskKey);
149
+ emit({ type: "task_detail", id: command.id, taskKey: command.taskKey, task: task ?? null });
150
+ emit({ type: "result", id: command.id, ok: Boolean(task) });
151
+ return;
152
+ }
147
153
  if (action === "refresh" || action === "reconnect") {
148
154
  await agent.refreshCodexStatus?.();
149
155
  await agent.refreshDesktopStatus();
@@ -210,7 +216,7 @@ if (isDesktopHostEntry()) {
210
216
  const conflict = error?.code === PROCESS_CONFLICT_CODE;
211
217
  process.stdout.write(`${JSON.stringify({
212
218
  type: "error",
213
- ...(conflict ? { code: PROCESS_CONFLICT_CODE } : {}),
219
+ ...(conflict ? { code: PROCESS_CONFLICT_CODE, conflicts: error.conflicts || [] } : {}),
214
220
  message: conflict
215
221
  ? PROCESS_CONFLICT_MESSAGE
216
222
  : "SIMY could not start. Check whether another SIMY window is already running, then reconnect.",
@@ -10,10 +10,11 @@ export const PROCESS_CONFLICT_MESSAGE =
10
10
  "Another SIMY background service is already running for this Web address. Stop the existing CLI daemon, then reopen SIMY CLI.";
11
11
 
12
12
  export class SimyProcessConflictError extends Error {
13
- constructor(message = PROCESS_CONFLICT_MESSAGE) {
13
+ constructor(message = PROCESS_CONFLICT_MESSAGE, conflicts = []) {
14
14
  super(message);
15
15
  this.name = "SimyProcessConflictError";
16
16
  this.code = PROCESS_CONFLICT_CODE;
17
+ this.conflicts = conflicts;
17
18
  }
18
19
  }
19
20
 
@@ -96,12 +97,13 @@ export async function assertNoDesktopHostForOrigin(
96
97
  throw new SimyProcessConflictError();
97
98
  }
98
99
 
99
- export async function assertNoLegacyCliDaemon(
100
+ export async function findLegacyCliDaemons(
100
101
  webOrigin,
101
102
  {
102
103
  processes = listSameUserProcesses,
103
104
  verifyEntry = verifySimyCliEntry,
104
105
  currentPid = process.pid,
106
+ identity = processStart,
105
107
  } = {},
106
108
  ) {
107
109
  const requestedOrigin = normalizeWebOrigin(webOrigin);
@@ -113,13 +115,48 @@ export async function assertNoLegacyCliDaemon(
113
115
  "SIMY could not verify whether another CLI service is running. Stop the existing CLI daemon, then reopen SIMY CLI.",
114
116
  );
115
117
  }
118
+ const conflicts = [];
116
119
  for (const processInfo of candidates) {
117
- if (processInfo.pid === currentPid) continue;
120
+ if (!Number.isSafeInteger(processInfo.pid) || processInfo.pid <= 0 || processInfo.pid === currentPid) continue;
118
121
  const parsed = parseLegacyAgentArguments(processInfo.command ?? processInfo.args);
119
122
  if (!parsed || !(await verifyEntry(parsed.entryPath))) continue;
120
123
  if (parsed.webOrigin && parsed.webOrigin !== requestedOrigin) continue;
121
- throw new SimyProcessConflictError();
124
+ const startedAt = await identity(processInfo.pid).catch(() => null);
125
+ conflicts.push({ pid: processInfo.pid, entryPath: parsed.entryPath, startedAt });
122
126
  }
127
+ return conflicts;
128
+ }
129
+
130
+
131
+ export async function assertNoLegacyCliDaemon(webOrigin, options) {
132
+ const conflicts = await findLegacyCliDaemons(webOrigin, options);
133
+ if (conflicts.length) throw new SimyProcessConflictError(PROCESS_CONFLICT_MESSAGE, conflicts);
134
+ }
135
+
136
+ // Re-discover same-user SIMY processes and compare their start time immediately
137
+ // before signaling. A PID alone is never sufficient authorization after a dialog.
138
+ export async function stopConfirmedCliConflicts(webOrigin, approved, { force = false,
139
+ discover = findLegacyCliDaemons, signal = process.kill.bind(process),
140
+ alive = processAlive, wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
141
+ } = {}) {
142
+ if (!Array.isArray(approved) || !approved.length || approved.length > 100) throw new Error("No confirmed SIMY process.");
143
+ const stopped = [];
144
+ for (const expected of approved) {
145
+ if (!Number.isSafeInteger(expected.pid) || expected.pid <= 0 || !expected.startedAt || typeof expected.entryPath !== "string") {
146
+ throw new Error("SIMY process identity is unavailable. Refresh and try again.");
147
+ }
148
+ const current = (await discover(webOrigin)).find((item) => item.pid === expected.pid);
149
+ if (!current) continue;
150
+ if (current.startedAt !== expected.startedAt || current.entryPath !== expected.entryPath) {
151
+ throw new Error("The process changed. Refresh before stopping it.");
152
+ }
153
+ try { signal(current.pid, force ? "SIGKILL" : "SIGTERM"); }
154
+ catch (error) { if (error.code !== "ESRCH") throw error; }
155
+ stopped.push(current);
156
+ }
157
+ for (let n = 0; n < 40 && stopped.some((item) => alive(item.pid)); n++) await wait(250);
158
+ const remaining = await discover(webOrigin);
159
+ return { remaining: remaining.filter((item) => stopped.some((old) => old.pid === item.pid && old.startedAt === item.startedAt && old.entryPath === item.entryPath)) };
123
160
  }
124
161
 
125
162
  export function parseLegacyAgentArguments(args) {
@@ -83,6 +83,42 @@ export function desktopTaskStatus(registries, { maxItems = MAX_STATUS_ITEMS } =
83
83
  };
84
84
  }
85
85
 
86
+ // Detail is sent only on selection, never in the periodic metadata broadcast.
87
+ export function desktopTaskDetail(registries, key) {
88
+ if (typeof key !== "string" || key.length > 256) return null;
89
+ const match = /^(\d+):(.+)$/.exec(key);
90
+ if (!match) return null;
91
+ const registryIndex = Number(match[1]);
92
+ const task = registries[registryIndex]?.list?.().find((item) => item.id === match[2]);
93
+ if (!task) return null;
94
+ const attempt = task.snapshot?.attempts?.at(-1);
95
+ const request = task.request?.requirement || task.instruction || task.snapshot?.charter?.requirement;
96
+ const result = typeof task.result === "string" ? task.result : task.result?.summary || attempt?.summary;
97
+ const links = [];
98
+ const pr = safeDesktopLink(attempt?.pr_url);
99
+ if (pr) links.push({ label: "Pull request", url: pr });
100
+ return {
101
+ key,
102
+ instruction: desktopContent(request),
103
+ result: desktopContent(result),
104
+ error: desktopContent(task.error, 1000),
105
+ truncated: [request, result].some((value) => typeof value === "string" && value.length > 16000),
106
+ links,
107
+ };
108
+ }
109
+
110
+ function desktopContent(value, limit = 16000) {
111
+ // Preserve paragraphs/Markdown, but never serialize runtime objects or logs.
112
+ return typeof value === "string" ? value.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, "").slice(0, limit) : "";
113
+ }
114
+
115
+ function safeDesktopLink(value) {
116
+ try {
117
+ const url = new URL(value);
118
+ return url.protocol === "https:" && !url.username && !url.password ? url.href : null;
119
+ } catch { return null; }
120
+ }
121
+
86
122
  export function createCodexStatusInspector({
87
123
  cacheMs = CODEX_STATUS_CACHE_MS,
88
124
  inspect = inspectCodexStatus,
@@ -178,6 +214,7 @@ function taskStatusItem(task, registryIndex) {
178
214
  48,
179
215
  );
180
216
  return {
217
+ key: `${registryIndex}:${bounded(task?.id || "", 240)}`,
181
218
  id: bounded(String(task?.id || `unknown-${registryIndex}`), MAX_LABEL_LENGTH),
182
219
  label: bounded(
183
220
  String(
@@ -0,0 +1,17 @@
1
+ import { stopConfirmedCliConflicts } from "./desktop-process-lock.js";
2
+ import { resolveWebOrigin } from "./web-origin.js";
3
+
4
+ // Invoked by the native confirmation dialog, never automatically at startup.
5
+ let input = "";
6
+ try {
7
+ for await (const chunk of process.stdin) {
8
+ input += chunk;
9
+ if (input.length > 32768) throw new Error("Confirmation is too large.");
10
+ }
11
+ const request = JSON.parse(input);
12
+ const result = await stopConfirmedCliConflicts(resolveWebOrigin(null, process.env), request.conflicts, { force: request.force === true });
13
+ process.stdout.write(JSON.stringify({ ok: true, ...result }));
14
+ } catch {
15
+ process.stdout.write(JSON.stringify({ ok: false }));
16
+ process.exitCode = 1;
17
+ }