@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 +1 -1
- package/src/agent.js +2 -0
- package/src/cli-contract.js +1 -1
- package/src/desktop-host.js +7 -1
- package/src/desktop-process-lock.js +41 -4
- package/src/desktop-status.js +37 -0
- package/src/desktop-stop-conflicts.js +17 -0
package/package.json
CHANGED
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));
|
package/src/cli-contract.js
CHANGED
|
@@ -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.
|
|
16
|
+
export const CLI_VERSION = "0.5.19";
|
|
17
17
|
export const CLI_API_CONTRACT_VERSION = 10;
|
|
18
18
|
|
|
19
19
|
export function withCliContract(
|
package/src/desktop-host.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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) {
|
package/src/desktop-status.js
CHANGED
|
@@ -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
|
+
}
|