codex-agent-view 0.3.2 → 0.4.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.
- package/.codex-plugin/plugin.json +4 -5
- package/README.ko.md +281 -0
- package/README.md +47 -328
- package/bin/codex-agent-view.mjs +113 -15
- package/package.json +4 -2
- package/public/app.js +34 -13
- package/public/index.html +1 -1
- package/scripts/auto-start-monitor.mjs +89 -0
- package/scripts/send-hook.mjs +67 -4
- package/skills/codex-agent-view/SKILL.md +29 -2
- package/skills/show-agents/SKILL.md +64 -0
- package/skills/show-agents/agents/openai.yaml +5 -0
- package/src/runtime/config.mjs +15 -0
- package/src/runtime/server.mjs +24 -8
package/bin/codex-agent-view.mjs
CHANGED
|
@@ -442,16 +442,90 @@ async function inspectRuntime() {
|
|
|
442
442
|
}
|
|
443
443
|
}
|
|
444
444
|
|
|
445
|
-
async function
|
|
445
|
+
async function runtimeEndpointState(runtime) {
|
|
446
446
|
try {
|
|
447
|
-
await fetch(`http://${runtime.host}:${runtime.port}/api/state`, {
|
|
447
|
+
const response = await fetch(`http://${runtime.host}:${runtime.port}/api/state`, {
|
|
448
448
|
headers: { authorization: `Bearer ${runtime.token}` },
|
|
449
449
|
signal: AbortSignal.timeout(1_500),
|
|
450
450
|
});
|
|
451
|
-
|
|
451
|
+
if (!response.ok) {
|
|
452
|
+
await response.body?.cancel();
|
|
453
|
+
return "unrelated";
|
|
454
|
+
}
|
|
455
|
+
const snapshot = await response.json().catch(() => null);
|
|
456
|
+
return snapshot?.schema_version === 1 && snapshot?.source_of_truth === "hook"
|
|
457
|
+
? "owned"
|
|
458
|
+
: "unrelated";
|
|
452
459
|
} catch {
|
|
460
|
+
return "absent";
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async function runtimeResponds(runtime) {
|
|
465
|
+
return (await runtimeEndpointState(runtime)) === "owned";
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
async function stopRunningRuntime(preflight) {
|
|
469
|
+
if (preflight.kind !== "valid") {
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
472
|
+
const endpointState = await runtimeEndpointState(preflight.info);
|
|
473
|
+
if (endpointState === "absent") {
|
|
453
474
|
return false;
|
|
454
475
|
}
|
|
476
|
+
if (endpointState === "unrelated") {
|
|
477
|
+
throw new Error(
|
|
478
|
+
"the runtime endpoint was not identified as an owned monitor; plugin and runtime files were preserved",
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
let response;
|
|
483
|
+
try {
|
|
484
|
+
response = await fetch(
|
|
485
|
+
`http://${preflight.info.host}:${preflight.info.port}/api/internal/shutdown`,
|
|
486
|
+
{
|
|
487
|
+
headers: { authorization: `Bearer ${preflight.info.token}` },
|
|
488
|
+
method: "POST",
|
|
489
|
+
signal: AbortSignal.timeout(1_500),
|
|
490
|
+
},
|
|
491
|
+
);
|
|
492
|
+
} catch {
|
|
493
|
+
throw new Error("the owned monitor could not be stopped; plugin files were preserved");
|
|
494
|
+
}
|
|
495
|
+
if (!response.ok) {
|
|
496
|
+
throw new Error(
|
|
497
|
+
`the owned monitor refused shutdown with HTTP ${response.status}; plugin files were preserved`,
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
const result = await response.json().catch(() => null);
|
|
501
|
+
if (result?.status !== "shutting_down") {
|
|
502
|
+
throw new Error(
|
|
503
|
+
"the owned monitor returned an invalid shutdown response; plugin files were preserved",
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const deadline = Date.now() + 3_000;
|
|
508
|
+
while (Date.now() < deadline) {
|
|
509
|
+
const current = await inspectRuntime();
|
|
510
|
+
if (current.kind === "absent") {
|
|
511
|
+
return true;
|
|
512
|
+
}
|
|
513
|
+
if (current.kind === "unknown") {
|
|
514
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, 25));
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
if (current.info.token !== preflight.info.token) {
|
|
518
|
+
throw new Error(
|
|
519
|
+
"runtime ownership changed during uninstall; new or unrecognized runtime data was preserved",
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
if (!(await runtimeResponds(current.info))) {
|
|
523
|
+
await removeRuntimeInfo(current.info.token);
|
|
524
|
+
return true;
|
|
525
|
+
}
|
|
526
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, 25));
|
|
527
|
+
}
|
|
528
|
+
throw new Error("the owned monitor did not stop in time; plugin files were preserved");
|
|
455
529
|
}
|
|
456
530
|
|
|
457
531
|
async function inspectPluginBundle(destination) {
|
|
@@ -593,12 +667,19 @@ async function purgeStaleRuntime(preflight) {
|
|
|
593
667
|
}
|
|
594
668
|
|
|
595
669
|
const current = await inspectRuntime();
|
|
670
|
+
if (current.kind === "absent") {
|
|
671
|
+
return false;
|
|
672
|
+
}
|
|
596
673
|
if (current.kind !== "valid" || current.info.token !== preflight.info.token) {
|
|
597
674
|
return true;
|
|
598
675
|
}
|
|
599
|
-
|
|
676
|
+
const endpointState = await runtimeEndpointState(current.info);
|
|
677
|
+
if (endpointState === "owned") {
|
|
600
678
|
throw new Error("the Codex Agent View monitor started during uninstall; runtime data was preserved");
|
|
601
679
|
}
|
|
680
|
+
if (endpointState === "unrelated") {
|
|
681
|
+
return true;
|
|
682
|
+
}
|
|
602
683
|
await removeRuntimeInfo(current.info.token);
|
|
603
684
|
return false;
|
|
604
685
|
}
|
|
@@ -624,7 +705,28 @@ async function removeRuntimeRootIfEmpty(root) {
|
|
|
624
705
|
}
|
|
625
706
|
}
|
|
626
707
|
|
|
708
|
+
function parseUninstallArgs(args) {
|
|
709
|
+
let purge = false;
|
|
710
|
+
|
|
711
|
+
for (const argument of args) {
|
|
712
|
+
if (argument === "--purge") {
|
|
713
|
+
if (purge) {
|
|
714
|
+
throw new Error("--purge may only be specified once");
|
|
715
|
+
}
|
|
716
|
+
purge = true;
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
if (argument.startsWith("-")) {
|
|
720
|
+
throw new Error(`unknown uninstall option: ${argument}`);
|
|
721
|
+
}
|
|
722
|
+
throw new Error(`unexpected uninstall argument: ${argument}`);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
return { purge };
|
|
726
|
+
}
|
|
727
|
+
|
|
627
728
|
async function uninstall(args) {
|
|
729
|
+
const { purge } = parseUninstallArgs(args);
|
|
628
730
|
const root = runtimeDirectory();
|
|
629
731
|
const bundle = join(root, "marketplace");
|
|
630
732
|
if (dirname(bundle) !== root || bundle === root) {
|
|
@@ -636,16 +738,8 @@ async function uninstall(args) {
|
|
|
636
738
|
throw unmanagedBundleError(bundle);
|
|
637
739
|
}
|
|
638
740
|
|
|
639
|
-
const
|
|
640
|
-
const
|
|
641
|
-
if (
|
|
642
|
-
runtimePreflight.kind === "valid" &&
|
|
643
|
-
(await runtimeResponds(runtimePreflight.info))
|
|
644
|
-
) {
|
|
645
|
-
throw new Error(
|
|
646
|
-
"the Codex Agent View monitor is running; stop it before uninstalling with --purge",
|
|
647
|
-
);
|
|
648
|
-
}
|
|
741
|
+
const runtimePreflight = await inspectRuntime();
|
|
742
|
+
const stoppedMonitor = await stopRunningRuntime(runtimePreflight);
|
|
649
743
|
|
|
650
744
|
await run("codex", ["plugin", "remove", PLUGIN_ID, "--json"], { allowFailure: true });
|
|
651
745
|
await run("codex", ["plugin", "marketplace", "remove", MARKETPLACE_NAME, "--json"], {
|
|
@@ -668,7 +762,11 @@ async function uninstall(args) {
|
|
|
668
762
|
);
|
|
669
763
|
}
|
|
670
764
|
} else {
|
|
671
|
-
process.stdout.write(
|
|
765
|
+
process.stdout.write(
|
|
766
|
+
stoppedMonitor
|
|
767
|
+
? "Stopped the owned monitor and removed the plugin and marketplace bundle.\n"
|
|
768
|
+
: "Removed plugin and marketplace bundle. Runtime data was preserved.\n",
|
|
769
|
+
);
|
|
672
770
|
}
|
|
673
771
|
}
|
|
674
772
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-agent-view",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Read-only Codex app task view with
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Read-only Codex app task view with a trusted-hook auto-prepared local live backend.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/core/index.mjs",
|
|
7
7
|
"exports": "./src/core/index.mjs",
|
|
@@ -15,11 +15,13 @@
|
|
|
15
15
|
"bin/",
|
|
16
16
|
"hooks/",
|
|
17
17
|
"public/",
|
|
18
|
+
"scripts/auto-start-monitor.mjs",
|
|
18
19
|
"scripts/capture-hook.mjs",
|
|
19
20
|
"scripts/send-hook.mjs",
|
|
20
21
|
"skills/",
|
|
21
22
|
"src/",
|
|
22
23
|
"README.md",
|
|
24
|
+
"README.ko.md",
|
|
23
25
|
"LICENSE",
|
|
24
26
|
"NOTICE"
|
|
25
27
|
],
|
package/public/app.js
CHANGED
|
@@ -69,6 +69,7 @@ const viewState = {
|
|
|
69
69
|
hasLoaded: false,
|
|
70
70
|
errorMessage: "",
|
|
71
71
|
canRetry: true,
|
|
72
|
+
authenticationFailed: false,
|
|
72
73
|
requestInFlight: false,
|
|
73
74
|
};
|
|
74
75
|
|
|
@@ -531,9 +532,9 @@ function setEmptyObservationMessage() {
|
|
|
531
532
|
|
|
532
533
|
const steps = document.createElement("ol");
|
|
533
534
|
for (const step of [
|
|
534
|
-
"
|
|
535
|
-
"
|
|
536
|
-
"
|
|
535
|
+
"Plugin을 설치한 뒤 공식 Codex 앱을 완전히 재시작했는지 확인합니다.",
|
|
536
|
+
"새 task에서 표시되는 Codex Agent View hook command를 검토하고 직접 trust합니다.",
|
|
537
|
+
"Trust 이후 새 task를 시작해 subagent 작업을 실행합니다. Hook event가 이 목록에 자동으로 추가됩니다.",
|
|
537
538
|
]) {
|
|
538
539
|
const item = document.createElement("li");
|
|
539
540
|
item.textContent = step;
|
|
@@ -542,7 +543,7 @@ function setEmptyObservationMessage() {
|
|
|
542
543
|
|
|
543
544
|
const boundary = document.createElement("p");
|
|
544
545
|
boundary.className = "observation-boundary";
|
|
545
|
-
boundary.textContent = "
|
|
546
|
+
boundary.textContent = "관찰 window는 첫 trusted hook에서 시작합니다. 그 전에 이미 지나간 event와 로컬 상태 수집이 중단된 동안의 event는 재생되지 않으며, 수집이 다시 시작되면 새 관찰 window가 열립니다.";
|
|
546
547
|
|
|
547
548
|
guidance.append(guidanceTitle, automaticTracking, steps, boundary);
|
|
548
549
|
|
|
@@ -594,12 +595,16 @@ function renderSessions() {
|
|
|
594
595
|
|
|
595
596
|
if (viewState.errorMessage) {
|
|
596
597
|
elements.sessionList.hidden = !visibleSessions.length;
|
|
597
|
-
const description = viewState.
|
|
598
|
-
?
|
|
598
|
+
const description = viewState.canRetry
|
|
599
|
+
? viewState.sessions.length
|
|
600
|
+
? "2초마다 자동으로 다시 연결합니다. 마지막 정상 상태를 계속 표시합니다."
|
|
601
|
+
: "2초마다 자동으로 다시 연결합니다. Codex 앱에서 이 화면을 그대로 두어도 됩니다."
|
|
599
602
|
: viewState.errorMessage;
|
|
600
603
|
setStateMessage(
|
|
601
604
|
"error",
|
|
602
|
-
|
|
605
|
+
viewState.canRetry
|
|
606
|
+
? "로컬 상태 연결이 끊겨 다시 시도 중입니다."
|
|
607
|
+
: "이 live view를 인증할 수 없습니다.",
|
|
603
608
|
description,
|
|
604
609
|
viewState.canRetry,
|
|
605
610
|
);
|
|
@@ -632,26 +637,27 @@ function render() {
|
|
|
632
637
|
renderSessions();
|
|
633
638
|
}
|
|
634
639
|
|
|
635
|
-
function setConnectionStatus(status) {
|
|
640
|
+
function setConnectionStatus(status, labelOverride = "") {
|
|
636
641
|
elements.connectionStatus.dataset.status = status;
|
|
637
642
|
const labels = {
|
|
638
643
|
connecting: "로컬 상태 연결 중",
|
|
639
644
|
connected: "로컬 monitor 연결됨",
|
|
640
|
-
error: "연결 끊김",
|
|
645
|
+
error: "연결 끊김 · 재시도 중",
|
|
641
646
|
};
|
|
642
|
-
elements.connectionLabel.textContent = labels[status];
|
|
647
|
+
elements.connectionLabel.textContent = labelOverride || labels[status];
|
|
643
648
|
}
|
|
644
649
|
|
|
645
650
|
async function refreshState() {
|
|
646
|
-
if (viewState.requestInFlight) {
|
|
651
|
+
if (viewState.requestInFlight || viewState.authenticationFailed) {
|
|
647
652
|
return;
|
|
648
653
|
}
|
|
649
654
|
|
|
650
655
|
if (!accessToken) {
|
|
651
656
|
viewState.hasLoaded = true;
|
|
652
657
|
viewState.canRetry = false;
|
|
653
|
-
viewState.
|
|
654
|
-
|
|
658
|
+
viewState.authenticationFailed = true;
|
|
659
|
+
viewState.errorMessage = "이 탭에는 접근 token이 없습니다. Codex 앱에서 Codex Agent View에 live view 열기를 다시 요청하세요.";
|
|
660
|
+
setConnectionStatus("error", "live view 인증 필요");
|
|
655
661
|
render();
|
|
656
662
|
return;
|
|
657
663
|
}
|
|
@@ -671,6 +677,16 @@ async function refreshState() {
|
|
|
671
677
|
},
|
|
672
678
|
});
|
|
673
679
|
|
|
680
|
+
if (response.status === 401 || response.status === 403) {
|
|
681
|
+
await response.body?.cancel();
|
|
682
|
+
viewState.hasLoaded = true;
|
|
683
|
+
viewState.canRetry = false;
|
|
684
|
+
viewState.authenticationFailed = true;
|
|
685
|
+
viewState.errorMessage = "이 live view의 인증이 더 이상 유효하지 않습니다. Codex 앱에서 Codex Agent View에 live view 열기를 다시 요청하세요.";
|
|
686
|
+
setConnectionStatus("error", "live view 인증 필요");
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
|
|
674
690
|
if (!response.ok) {
|
|
675
691
|
throw new Error(`상태 요청 실패 (${response.status})`);
|
|
676
692
|
}
|
|
@@ -685,6 +701,7 @@ async function refreshState() {
|
|
|
685
701
|
setConnectionStatus("connected");
|
|
686
702
|
} catch (error) {
|
|
687
703
|
viewState.hasLoaded = true;
|
|
704
|
+
viewState.canRetry = true;
|
|
688
705
|
viewState.errorMessage = error instanceof Error
|
|
689
706
|
? error.message
|
|
690
707
|
: "알 수 없는 연결 오류가 발생했습니다.";
|
|
@@ -702,6 +719,10 @@ elements.toolbar.addEventListener("submit", (event) => {
|
|
|
702
719
|
});
|
|
703
720
|
window.addEventListener("online", refreshState);
|
|
704
721
|
window.addEventListener("offline", () => {
|
|
722
|
+
if (viewState.authenticationFailed) {
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
viewState.canRetry = true;
|
|
705
726
|
viewState.errorMessage = "이 기기가 오프라인입니다.";
|
|
706
727
|
setConnectionStatus("error");
|
|
707
728
|
render();
|
package/public/index.html
CHANGED
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
|
|
127
127
|
<div id="state-message" class="state-message" role="status" aria-live="polite">
|
|
128
128
|
<strong>상태를 불러오는 중입니다.</strong>
|
|
129
|
-
<span
|
|
129
|
+
<span>Codex 앱의 로컬 상태에 연결하고 있습니다.</span>
|
|
130
130
|
</div>
|
|
131
131
|
|
|
132
132
|
<ul id="session-list" class="session-list" aria-label="Codex 부모 task 목록" hidden></ul>
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { access } from "node:fs/promises";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
import { startMonitorServer } from "../src/runtime/server.mjs";
|
|
7
|
+
import { autoStartPort, readRuntimeInfo } from "../src/runtime/config.mjs";
|
|
8
|
+
|
|
9
|
+
const HEALTH_TIMEOUT_MS = 350;
|
|
10
|
+
const OWNER_CHECK_INTERVAL_MS = 1_000;
|
|
11
|
+
const OWNER_FILE = fileURLToPath(
|
|
12
|
+
new URL("../.codex-plugin/plugin.json", import.meta.url),
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
async function currentMonitorIsHealthy() {
|
|
16
|
+
try {
|
|
17
|
+
const runtime = await readRuntimeInfo();
|
|
18
|
+
const response = await fetch(
|
|
19
|
+
`http://${runtime.host}:${runtime.port}/api/state`,
|
|
20
|
+
{
|
|
21
|
+
headers: { authorization: `Bearer ${runtime.token}` },
|
|
22
|
+
signal: AbortSignal.timeout(HEALTH_TIMEOUT_MS),
|
|
23
|
+
},
|
|
24
|
+
);
|
|
25
|
+
return response.ok;
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function main() {
|
|
32
|
+
let monitor;
|
|
33
|
+
let ownerCheck;
|
|
34
|
+
let stopRequested = false;
|
|
35
|
+
let stopping = false;
|
|
36
|
+
const stop = async () => {
|
|
37
|
+
stopRequested = true;
|
|
38
|
+
if (!monitor || stopping) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
stopping = true;
|
|
42
|
+
clearInterval(ownerCheck);
|
|
43
|
+
try {
|
|
44
|
+
await monitor.close();
|
|
45
|
+
} finally {
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
process.once("SIGINT", stop);
|
|
51
|
+
process.once("SIGTERM", stop);
|
|
52
|
+
|
|
53
|
+
if (await currentMonitorIsHealthy()) {
|
|
54
|
+
process.off("SIGINT", stop);
|
|
55
|
+
process.off("SIGTERM", stop);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
monitor = await startMonitorServer({ port: autoStartPort() });
|
|
61
|
+
} catch (error) {
|
|
62
|
+
process.off("SIGINT", stop);
|
|
63
|
+
process.off("SIGTERM", stop);
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
if (stopRequested) {
|
|
67
|
+
await stop();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// An npm/plugin uninstall removes this owned bundle. The detached monitor
|
|
72
|
+
// then shuts itself down and removes only the runtime file it still owns.
|
|
73
|
+
ownerCheck = setInterval(async () => {
|
|
74
|
+
try {
|
|
75
|
+
await access(OWNER_FILE);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (error?.code === "ENOENT" || error?.code === "ENOTDIR") {
|
|
78
|
+
await stop();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}, OWNER_CHECK_INTERVAL_MS);
|
|
82
|
+
ownerCheck.unref();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Auto-start is an internal hook implementation detail. It must never write a
|
|
86
|
+
// tokenized URL, a local path, or hook-derived data to stdout/stderr.
|
|
87
|
+
main().catch(() => {
|
|
88
|
+
process.exitCode = 0;
|
|
89
|
+
});
|
package/scripts/send-hook.mjs
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
3
4
|
import { basename } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
4
6
|
|
|
5
7
|
import { minimizePayload } from "./capture-hook.mjs";
|
|
6
8
|
import { readRuntimeInfo } from "../src/runtime/config.mjs";
|
|
7
9
|
|
|
8
10
|
const MAX_STDIN_BYTES = 2 * 1024 * 1024;
|
|
9
|
-
const SEND_TIMEOUT_MS =
|
|
11
|
+
const SEND_TIMEOUT_MS = 500;
|
|
12
|
+
const AUTO_START_WAIT_MS = 1_600;
|
|
13
|
+
const AUTO_START_POLL_MS = 40;
|
|
10
14
|
const MAX_WORKSPACE_LABEL_LENGTH = 120;
|
|
11
15
|
const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f]/g;
|
|
12
16
|
|
|
@@ -55,8 +59,7 @@ function monitorEnvelope(payload) {
|
|
|
55
59
|
: minimized;
|
|
56
60
|
}
|
|
57
61
|
|
|
58
|
-
async function
|
|
59
|
-
const runtime = await readRuntimeInfo();
|
|
62
|
+
async function sendToRuntime(runtime, envelope) {
|
|
60
63
|
const response = await fetch(
|
|
61
64
|
`http://${runtime.host}:${runtime.port}/api/events`,
|
|
62
65
|
{
|
|
@@ -65,7 +68,7 @@ async function send(payload) {
|
|
|
65
68
|
authorization: `Bearer ${runtime.token}`,
|
|
66
69
|
"content-type": "application/json",
|
|
67
70
|
},
|
|
68
|
-
body: JSON.stringify(
|
|
71
|
+
body: JSON.stringify(envelope),
|
|
69
72
|
signal: AbortSignal.timeout(SEND_TIMEOUT_MS),
|
|
70
73
|
},
|
|
71
74
|
);
|
|
@@ -74,6 +77,66 @@ async function send(payload) {
|
|
|
74
77
|
}
|
|
75
78
|
}
|
|
76
79
|
|
|
80
|
+
function childEnvironment() {
|
|
81
|
+
const env = {};
|
|
82
|
+
for (const key of [
|
|
83
|
+
"CODEX_AGENT_VIEW_AUTO_START_PORT",
|
|
84
|
+
"CODEX_AGENT_VIEW_RUNTIME_DIR",
|
|
85
|
+
"SystemRoot",
|
|
86
|
+
]) {
|
|
87
|
+
if (typeof process.env[key] === "string") {
|
|
88
|
+
env[key] = process.env[key];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return env;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function startMonitorDetached() {
|
|
95
|
+
const child = spawn(
|
|
96
|
+
process.execPath,
|
|
97
|
+
[fileURLToPath(new URL("./auto-start-monitor.mjs", import.meta.url))],
|
|
98
|
+
{
|
|
99
|
+
detached: true,
|
|
100
|
+
env: childEnvironment(),
|
|
101
|
+
shell: false,
|
|
102
|
+
stdio: "ignore",
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
child.on("error", () => {});
|
|
106
|
+
child.unref();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function delay(milliseconds) {
|
|
110
|
+
await new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function tryDelivery(envelope) {
|
|
114
|
+
try {
|
|
115
|
+
await sendToRuntime(await readRuntimeInfo(), envelope);
|
|
116
|
+
return true;
|
|
117
|
+
} catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function send(payload) {
|
|
123
|
+
const envelope = monitorEnvelope(payload);
|
|
124
|
+
if (await tryDelivery(envelope)) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
startMonitorDetached();
|
|
129
|
+
const deadline = Date.now() + AUTO_START_WAIT_MS;
|
|
130
|
+
do {
|
|
131
|
+
await delay(AUTO_START_POLL_MS);
|
|
132
|
+
if (await tryDelivery(envelope)) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
} while (Date.now() < deadline);
|
|
136
|
+
|
|
137
|
+
throw new Error("monitor auto-start delivery timed out");
|
|
138
|
+
}
|
|
139
|
+
|
|
77
140
|
async function main() {
|
|
78
141
|
try {
|
|
79
142
|
await send(await readStdin());
|
|
@@ -109,6 +109,13 @@ means that monitor process observed no hook events; it does not prove that the
|
|
|
109
109
|
Codex app has no tasks. Restarting the in-memory monitor begins a new bounded
|
|
110
110
|
observation window.
|
|
111
111
|
|
|
112
|
+
After explicit installation, hook review/trust, and a Codex app restart, the
|
|
113
|
+
first trusted hook normally prepares the local backend internally and retries
|
|
114
|
+
delivery of that same privacy-minimized event. The user never registers a task
|
|
115
|
+
ID or runs `start`, `status`, or `doctor` as part of ordinary use. A bounded
|
|
116
|
+
auto-start failure remains fail-open and does not create a persistent replay
|
|
117
|
+
queue.
|
|
118
|
+
|
|
112
119
|
## Open the live view only on request
|
|
113
120
|
|
|
114
121
|
Only when the user explicitly asks to open, show, or start the live view:
|
|
@@ -117,7 +124,15 @@ The plugin agent performs the health check and any required start internally.
|
|
|
117
124
|
The user's entire interaction after installation remains inside the official
|
|
118
125
|
Codex app; do not turn the commands below into instructions for the user.
|
|
119
126
|
|
|
120
|
-
|
|
127
|
+
The public Codex plugin API cannot create a sidebar, panel, or Browser tab
|
|
128
|
+
without a prompt at app startup. The first live view therefore requires one
|
|
129
|
+
explicit request in a Codex app task. Do not claim that installation alone
|
|
130
|
+
opens a screen. An already-open in-app live tab refreshes and reconnects after
|
|
131
|
+
temporary disconnects while the same monitor observation window and its
|
|
132
|
+
session token remain valid.
|
|
133
|
+
|
|
134
|
+
1. Check monitor health with the packaged CLI. A trusted hook may already have
|
|
135
|
+
prepared it automatically.
|
|
121
136
|
2. If it is not running, start it with `codex-agent-view start --no-open` so the
|
|
122
137
|
CLI never launches the operating system's external browser.
|
|
123
138
|
3. Keep the returned tokenized localhost URL private. Never quote it, place it
|
|
@@ -133,6 +148,10 @@ Do not restart or replace a healthy monitor merely to recover its URL because
|
|
|
133
148
|
that would discard its in-memory observation window. Reuse an existing in-app
|
|
134
149
|
monitor tab when possible. Do not close user-owned browser tabs.
|
|
135
150
|
|
|
151
|
+
If an existing tab has lost its session token or the monitor restarted, do not
|
|
152
|
+
promise automatic recovery across observation windows. Reopen the live view
|
|
153
|
+
through the same explicit in-app workflow without exposing the private URL.
|
|
154
|
+
|
|
136
155
|
## Lifecycle and safety
|
|
137
156
|
|
|
138
157
|
Run `codex-agent-view install` or `codex-agent-view uninstall` only when the
|
|
@@ -140,7 +159,15 @@ user explicitly requests that lifecycle action. Explain that install changes
|
|
|
140
159
|
local Codex plugin registration and requires hook review/trust. Before
|
|
141
160
|
uninstalling, distinguish the default command, which preserves runtime data,
|
|
142
161
|
from `codex-agent-view uninstall --purge`, which removes the configured runtime
|
|
143
|
-
directory.
|
|
162
|
+
directory only within its owned-file safety boundary. Do not ask the user to
|
|
163
|
+
stop an auto-started or foreground monitor first. The uninstall command uses
|
|
164
|
+
the validated runtime bearer token to authenticate and internally shut down a
|
|
165
|
+
healthy owned monitor before removing plugin files. The default command
|
|
166
|
+
preserves remaining runtime-directory data. `--purge` additionally removes
|
|
167
|
+
only an owned stale runtime file and an empty runtime directory; it preserves
|
|
168
|
+
unrecognized files, unrelated loopback services, and non-empty directories.
|
|
169
|
+
If an owned monitor cannot be stopped safely or the endpoint is unrelated,
|
|
170
|
+
report that removal stopped with plugin and runtime files preserved.
|
|
144
171
|
|
|
145
172
|
Keep every workflow read-only with respect to Codex tasks. Never stop or
|
|
146
173
|
restart a task or subagent, send a message to an agent, approve or deny a
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: show-agents
|
|
3
|
+
description: Open the Codex Agent View live task and subagent monitor in the official Codex app. Use when the user explicitly selects the bundled Show Agents skill from the app's @ menu.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Show Agents
|
|
7
|
+
|
|
8
|
+
Treat selection of the bundled **Show Agents** skill from the Codex app's `@`
|
|
9
|
+
menu as an explicit request to open the live monitor, not as a request for
|
|
10
|
+
terminal instructions or a text-only snapshot. Keep the whole ordinary-use
|
|
11
|
+
workflow inside the calling Codex app task.
|
|
12
|
+
|
|
13
|
+
## Open the live view
|
|
14
|
+
|
|
15
|
+
1. Run `codex-agent-view doctor --json` internally and inspect only its
|
|
16
|
+
structured diagnostics. Capture the result internally; do not quote the
|
|
17
|
+
command, raw output, runtime path, IDs, or private URL in commentary or the
|
|
18
|
+
final response.
|
|
19
|
+
2. If diagnostics contain `plugin_version_mismatch`, stop the workflow before
|
|
20
|
+
running `codex-agent-view status --json`, starting a monitor, or opening a
|
|
21
|
+
panel. Briefly tell the user inside the current Codex app task that the
|
|
22
|
+
installed plugin and global CLI versions differ and that the exact intended
|
|
23
|
+
`codex-agent-view` version must be globally reinstalled before they select
|
|
24
|
+
**Show Agents** again. Do not perform the reinstall, change Codex settings,
|
|
25
|
+
expose paths, or quote the diagnostic payload.
|
|
26
|
+
3. Otherwise, check the packaged monitor with
|
|
27
|
+
`codex-agent-view status --json`. Capture the result internally; do not
|
|
28
|
+
quote the command, raw output, runtime path, IDs, or private URL in
|
|
29
|
+
commentary or the final response.
|
|
30
|
+
4. If the monitor is healthy, reuse it. Recover its authenticated URL from the
|
|
31
|
+
owned private runtime record without restarting it, because restarting would
|
|
32
|
+
discard the current in-memory observation window.
|
|
33
|
+
5. If the monitor is not healthy, run `codex-agent-view start --no-open` as a
|
|
34
|
+
persistent internal process and capture the authenticated URL it returns.
|
|
35
|
+
Never use `--open` or launch an external browser.
|
|
36
|
+
6. Accept the URL only when it uses `http`, host `127.0.0.1`, a valid local
|
|
37
|
+
port, and the expected non-empty fragment token. Treat every other target as
|
|
38
|
+
invalid and do not open it.
|
|
39
|
+
7. Call `codex_app__open_in_codex` for the calling task with a browser target,
|
|
40
|
+
the validated private URL, and `placement: "right"`. Omit `threadId`; never
|
|
41
|
+
navigate to or open the monitor in another task.
|
|
42
|
+
8. If a previous successful call in the current context supplied the same
|
|
43
|
+
monitor tab's `tabId`, prefer reopening that browser target by `tabId`.
|
|
44
|
+
Otherwise open the validated URL. Do not close or replace user-owned tabs.
|
|
45
|
+
|
|
46
|
+
The in-app Browser capability or site permission may be unavailable or may
|
|
47
|
+
require a user confirmation. Do not claim that the panel opened until
|
|
48
|
+
`codex_app__open_in_codex` reports success. Let Codex show its normal app
|
|
49
|
+
permission request when required; never replace it with terminal instructions.
|
|
50
|
+
|
|
51
|
+
Never place the tokenized localhost URL in Markdown, plain text, code, logs, or
|
|
52
|
+
user instructions. It may appear only as private agent-internal state and as
|
|
53
|
+
the browser target passed to `codex_app__open_in_codex`.
|
|
54
|
+
|
|
55
|
+
## Failure behavior
|
|
56
|
+
|
|
57
|
+
If the official app cannot open a browser panel, Browser is unavailable, or
|
|
58
|
+
site permission is denied, do not expose the private URL or suggest a terminal
|
|
59
|
+
or external-browser workaround. Briefly report that the live panel could not
|
|
60
|
+
be opened and offer the existing app-native task snapshot from the bundled
|
|
61
|
+
`codex-agent-view` skill.
|
|
62
|
+
|
|
63
|
+
Keep the workflow read-only. Never stop or restart a Codex task or subagent,
|
|
64
|
+
send messages to them, or approve or deny permission requests.
|
package/src/runtime/config.mjs
CHANGED
|
@@ -16,6 +16,21 @@ export const DEFAULT_PORT = 43127;
|
|
|
16
16
|
export const MAX_EVENT_BODY_BYTES = 64 * 1024;
|
|
17
17
|
export const RUNTIME_SCHEMA_VERSION = 1;
|
|
18
18
|
|
|
19
|
+
export function autoStartPort(env = process.env) {
|
|
20
|
+
const configured = env.CODEX_AGENT_VIEW_AUTO_START_PORT;
|
|
21
|
+
if (configured === undefined || configured === "") {
|
|
22
|
+
return DEFAULT_PORT;
|
|
23
|
+
}
|
|
24
|
+
if (!/^\d+$/.test(configured)) {
|
|
25
|
+
throw new Error("invalid Codex Agent View auto-start port");
|
|
26
|
+
}
|
|
27
|
+
const port = Number(configured);
|
|
28
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
29
|
+
throw new Error("invalid Codex Agent View auto-start port");
|
|
30
|
+
}
|
|
31
|
+
return port;
|
|
32
|
+
}
|
|
33
|
+
|
|
19
34
|
export function runtimeDirectory(env = process.env) {
|
|
20
35
|
return resolve(
|
|
21
36
|
env.CODEX_AGENT_VIEW_RUNTIME_DIR || join(homedir(), ".codex-agent-view"),
|