@adhdev/daemon-core 0.5.20 → 0.5.21
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/providers/_builtin/extension/codex/provider.json +36 -0
- package/providers/_builtin/extension/codex/scripts/click_conversation_webview.js +24 -0
- package/providers/_builtin/extension/codex/scripts/explore_chat_webview.js +110 -0
- package/providers/_builtin/extension/codex/scripts/explore_controls_webview.js +75 -0
- package/providers/_builtin/extension/codex/scripts/explore_dom.js +88 -0
- package/providers/_builtin/extension/codex/scripts/explore_dropdown_webview.js +64 -0
- package/providers/_builtin/extension/codex/scripts/inspect_code_webview.js +55 -0
- package/providers/_builtin/extension/codex/scripts/list_models.js +62 -0
- package/providers/_builtin/extension/codex/scripts/message_structure_webview.js +79 -0
- package/providers/_builtin/extension/codex/scripts/new_session.js +26 -0
- package/providers/_builtin/extension/codex/scripts/read_chat.js +342 -0
- package/providers/_builtin/extension/codex/scripts/resolve_action.js +42 -0
- package/providers/_builtin/extension/codex/scripts/send_message.js +62 -0
- package/providers/_builtin/extension/codex/scripts/set_model.js +86 -0
- package/providers/_builtin/extension/codex/scripts.js +94 -0
- package/providers/_builtin/registry.json +6 -1
package/package.json
CHANGED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "codex",
|
|
3
|
+
"name": "Codex",
|
|
4
|
+
"category": "extension",
|
|
5
|
+
"extensionId": "openai.chatgpt",
|
|
6
|
+
"extensionIdPattern": "extensionId=openai\\.chat",
|
|
7
|
+
"extensionIdPattern_flags": "i",
|
|
8
|
+
"webviewMatchText": "openai.chat",
|
|
9
|
+
"vscodeCommands": {
|
|
10
|
+
"focusPanel": "openai-chatgpt.focus"
|
|
11
|
+
},
|
|
12
|
+
"settings": {
|
|
13
|
+
"approvalAlert": {
|
|
14
|
+
"type": "boolean",
|
|
15
|
+
"default": true,
|
|
16
|
+
"public": true,
|
|
17
|
+
"label": "Approval Notifications",
|
|
18
|
+
"description": "Show notification when approval is needed"
|
|
19
|
+
},
|
|
20
|
+
"longGeneratingAlert": {
|
|
21
|
+
"type": "boolean",
|
|
22
|
+
"default": true,
|
|
23
|
+
"public": true,
|
|
24
|
+
"label": "Long Generation Alert",
|
|
25
|
+
"description": "Alert when generation takes too long"
|
|
26
|
+
},
|
|
27
|
+
"longGeneratingThresholdSec": {
|
|
28
|
+
"type": "number",
|
|
29
|
+
"default": 180,
|
|
30
|
+
"public": true,
|
|
31
|
+
"label": "Long Generation Threshold (sec)",
|
|
32
|
+
"min": 30,
|
|
33
|
+
"max": 600
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Click the first conversation item to navigate into a chat
|
|
3
|
+
*/
|
|
4
|
+
(() => {
|
|
5
|
+
try {
|
|
6
|
+
const buttons = document.querySelectorAll('[role="button"]');
|
|
7
|
+
for (const btn of buttons) {
|
|
8
|
+
const text = (btn.textContent || '').trim();
|
|
9
|
+
if (text.includes('hello')) {
|
|
10
|
+
btn.click();
|
|
11
|
+
return JSON.stringify({ clicked: true, text: text.substring(0, 100) });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
// fallback: click first conversation button
|
|
15
|
+
if (buttons.length > 0) {
|
|
16
|
+
const first = buttons[0];
|
|
17
|
+
first.click();
|
|
18
|
+
return JSON.stringify({ clicked: true, text: (first.textContent||'').trim().substring(0, 100), fallback: true });
|
|
19
|
+
}
|
|
20
|
+
return JSON.stringify({ clicked: false, error: 'no conversation items found' });
|
|
21
|
+
} catch (e) {
|
|
22
|
+
return JSON.stringify({ error: e.message });
|
|
23
|
+
}
|
|
24
|
+
})()
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex Extension — Chat View DOM Explorer
|
|
3
|
+
* Run after clicking into a conversation to see message structure
|
|
4
|
+
*/
|
|
5
|
+
(() => {
|
|
6
|
+
try {
|
|
7
|
+
const root = document.getElementById('root');
|
|
8
|
+
if (!root) return JSON.stringify({ error: 'no root' });
|
|
9
|
+
|
|
10
|
+
// Header info
|
|
11
|
+
const headerEl = document.querySelector('[style*="view-transition-name: header-title"]');
|
|
12
|
+
const headerText = headerEl?.textContent?.trim() || '';
|
|
13
|
+
|
|
14
|
+
// Find thread/message area (look for common React patterns)
|
|
15
|
+
// Codex uses React + ProseMirror
|
|
16
|
+
const allDivs = document.querySelectorAll('div');
|
|
17
|
+
|
|
18
|
+
// Find message containers by looking for role/class patterns
|
|
19
|
+
const messageAreas = [];
|
|
20
|
+
for (const div of allDivs) {
|
|
21
|
+
const cls = (div.className && typeof div.className === 'string') ? div.className : '';
|
|
22
|
+
if (cls.includes('thread') || cls.includes('message') ||
|
|
23
|
+
cls.includes('turn-') || cls.includes('agent-') ||
|
|
24
|
+
div.getAttribute('data-message-id') ||
|
|
25
|
+
div.getAttribute('data-turn-id')) {
|
|
26
|
+
messageAreas.push({
|
|
27
|
+
class: cls.substring(0, 300),
|
|
28
|
+
dataMessageId: div.getAttribute('data-message-id'),
|
|
29
|
+
dataTurnId: div.getAttribute('data-turn-id'),
|
|
30
|
+
role: div.getAttribute('role'),
|
|
31
|
+
childCount: div.children?.length || 0,
|
|
32
|
+
text: (div.textContent || '').trim().substring(0, 300),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Look for the actual message list/scroll container
|
|
38
|
+
const scrollContainers = document.querySelectorAll('[class*="overflow-y-auto"], [class*="scroll"]');
|
|
39
|
+
const scrollInfo = Array.from(scrollContainers).slice(0, 10).map(el => ({
|
|
40
|
+
tag: el.tagName?.toLowerCase(),
|
|
41
|
+
class: (el.className && typeof el.className === 'string') ? el.className.substring(0, 300) : null,
|
|
42
|
+
childCount: el.children?.length || 0,
|
|
43
|
+
scrollHeight: el.scrollHeight,
|
|
44
|
+
clientHeight: el.clientHeight,
|
|
45
|
+
text: (el.textContent || '').trim().substring(0, 200),
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
// Find React Fiber data
|
|
49
|
+
let fiberInfo = null;
|
|
50
|
+
const rootEl = document.getElementById('root');
|
|
51
|
+
if (rootEl) {
|
|
52
|
+
const fiberKey = Object.keys(rootEl).find(k =>
|
|
53
|
+
k.startsWith('__reactFiber') || k.startsWith('__reactInternalInstance')
|
|
54
|
+
);
|
|
55
|
+
if (fiberKey) {
|
|
56
|
+
let fiber = rootEl[fiberKey];
|
|
57
|
+
const stateSnapshots = [];
|
|
58
|
+
for (let d = 0; d < 30 && fiber; d++) {
|
|
59
|
+
const props = fiber.memoizedProps || fiber.pendingProps;
|
|
60
|
+
const state = fiber.memoizedState;
|
|
61
|
+
if (props) {
|
|
62
|
+
const propKeys = Object.keys(props).filter(k => k !== 'children');
|
|
63
|
+
if (propKeys.length > 0) {
|
|
64
|
+
stateSnapshots.push({
|
|
65
|
+
depth: d,
|
|
66
|
+
type: fiber.type?.name || fiber.type?.displayName || (typeof fiber.type === 'string' ? fiber.type : '?'),
|
|
67
|
+
propKeys: propKeys.slice(0, 10),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
fiber = fiber.child || fiber.return;
|
|
72
|
+
if (stateSnapshots.length > 15) break;
|
|
73
|
+
}
|
|
74
|
+
fiberInfo = stateSnapshots;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Full structure dump of children of root
|
|
79
|
+
const rootChildren = rootEl ? Array.from(rootEl.children) : [];
|
|
80
|
+
const structure = [];
|
|
81
|
+
const dumpChildren = (el, depth, maxDepth) => {
|
|
82
|
+
if (depth > maxDepth) return;
|
|
83
|
+
for (const child of el.children) {
|
|
84
|
+
const cls = (child.className && typeof child.className === 'string') ? child.className : '';
|
|
85
|
+
structure.push({
|
|
86
|
+
depth,
|
|
87
|
+
tag: child.tagName?.toLowerCase(),
|
|
88
|
+
class: cls.substring(0, 200),
|
|
89
|
+
id: child.id || null,
|
|
90
|
+
childCount: child.children?.length || 0,
|
|
91
|
+
text: child.children?.length === 0 ? (child.textContent || '').trim().substring(0, 100) : '',
|
|
92
|
+
});
|
|
93
|
+
if (structure.length < 60) {
|
|
94
|
+
dumpChildren(child, depth + 1, maxDepth);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
if (rootEl) dumpChildren(rootEl, 0, 4);
|
|
99
|
+
|
|
100
|
+
return JSON.stringify({
|
|
101
|
+
headerText,
|
|
102
|
+
messageAreas: messageAreas.slice(0, 20),
|
|
103
|
+
scrollContainers: scrollInfo,
|
|
104
|
+
fiberInfo,
|
|
105
|
+
structure: structure.slice(0, 60),
|
|
106
|
+
});
|
|
107
|
+
} catch (e) {
|
|
108
|
+
return JSON.stringify({ error: e.message || String(e) });
|
|
109
|
+
}
|
|
110
|
+
})()
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex — Explore model/mode selectors and session management UI
|
|
3
|
+
*/
|
|
4
|
+
(() => {
|
|
5
|
+
try {
|
|
6
|
+
// 1. Model selector area — footer buttons
|
|
7
|
+
const footerArea = document.querySelector('[class*="thread-composer-max-width"][class*="pb-2"]')
|
|
8
|
+
|| document.querySelector('[class*="thread-composer-max-width"]');
|
|
9
|
+
|
|
10
|
+
const footerButtons = footerArea ? Array.from(footerArea.querySelectorAll('button')).map(b => ({
|
|
11
|
+
text: (b.textContent || '').trim().substring(0, 80),
|
|
12
|
+
ariaLabel: b.getAttribute('aria-label')?.substring(0, 80),
|
|
13
|
+
ariaHasPopup: b.getAttribute('aria-haspopup'),
|
|
14
|
+
ariaExpanded: b.getAttribute('aria-expanded'),
|
|
15
|
+
dataState: b.getAttribute('data-state'),
|
|
16
|
+
id: b.id || null,
|
|
17
|
+
class: b.className?.substring(0, 150),
|
|
18
|
+
})) : [];
|
|
19
|
+
|
|
20
|
+
// 2. Look for dropdown/popover menus
|
|
21
|
+
const popovers = document.querySelectorAll('[role="menu"], [role="listbox"], [data-radix-popper-content-wrapper], [data-side]');
|
|
22
|
+
const popoverInfo = Array.from(popovers).map(el => ({
|
|
23
|
+
tag: el.tagName?.toLowerCase(),
|
|
24
|
+
role: el.getAttribute('role'),
|
|
25
|
+
class: (el.className && typeof el.className === 'string') ? el.className.substring(0, 200) : null,
|
|
26
|
+
childCount: el.children?.length || 0,
|
|
27
|
+
text: (el.textContent || '').trim().substring(0, 300),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
// 3. Header buttons (back, new chat, etc.)
|
|
31
|
+
const headerArea = document.querySelector('[class*="draggable"]');
|
|
32
|
+
const headerButtons = headerArea ? Array.from(headerArea.querySelectorAll('button')).map(b => ({
|
|
33
|
+
text: (b.textContent || '').trim().substring(0, 60),
|
|
34
|
+
ariaLabel: b.getAttribute('aria-label')?.substring(0, 80),
|
|
35
|
+
class: b.className?.substring(0, 100),
|
|
36
|
+
})) : [];
|
|
37
|
+
|
|
38
|
+
// 4. All buttons with aria-labels (important for functionality mapping)
|
|
39
|
+
const allButtons = Array.from(document.querySelectorAll('button')).filter(b => b.offsetWidth > 0);
|
|
40
|
+
const labeledButtons = allButtons
|
|
41
|
+
.filter(b => b.getAttribute('aria-label'))
|
|
42
|
+
.map(b => ({
|
|
43
|
+
text: (b.textContent || '').trim().substring(0, 60),
|
|
44
|
+
ariaLabel: b.getAttribute('aria-label')?.substring(0, 80),
|
|
45
|
+
ariaHasPopup: b.getAttribute('aria-haspopup'),
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
// 5. Check for "new chat" button
|
|
49
|
+
const newChatBtn = allButtons.find(b => {
|
|
50
|
+
const label = (b.getAttribute('aria-label') || '').toLowerCase();
|
|
51
|
+
const text = (b.textContent || '').trim().toLowerCase();
|
|
52
|
+
return label.includes('new') || label.includes('새') || text.includes('new') || text.includes('새');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// 6. Look for back/navigation buttons
|
|
56
|
+
const backBtn = allButtons.find(b => {
|
|
57
|
+
const label = (b.getAttribute('aria-label') || '').toLowerCase();
|
|
58
|
+
return label.includes('back') || label.includes('뒤로') || label.includes('돌아가');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return JSON.stringify({
|
|
62
|
+
footerButtons,
|
|
63
|
+
headerButtons,
|
|
64
|
+
labeledButtons,
|
|
65
|
+
popoverCount: popovers.length,
|
|
66
|
+
popovers: popoverInfo,
|
|
67
|
+
hasNewChatBtn: !!newChatBtn,
|
|
68
|
+
newChatBtnLabel: newChatBtn?.getAttribute('aria-label') || null,
|
|
69
|
+
hasBackBtn: !!backBtn,
|
|
70
|
+
backBtnLabel: backBtn?.getAttribute('aria-label') || null,
|
|
71
|
+
});
|
|
72
|
+
} catch (e) {
|
|
73
|
+
return JSON.stringify({ error: e.message || String(e) });
|
|
74
|
+
}
|
|
75
|
+
})()
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex Extension — Deep DOM Explorer
|
|
3
|
+
* Explores the chat area, message list, input field, model selector etc.
|
|
4
|
+
*/
|
|
5
|
+
(() => {
|
|
6
|
+
try {
|
|
7
|
+
const root = document.getElementById('root');
|
|
8
|
+
if (!root) return JSON.stringify({ error: 'no root' });
|
|
9
|
+
|
|
10
|
+
// 1. Find chat input (textarea, contenteditable, etc.)
|
|
11
|
+
const textareas = document.querySelectorAll('textarea');
|
|
12
|
+
const contentEditables = document.querySelectorAll('[contenteditable="true"]');
|
|
13
|
+
const chatInputCandidates = document.querySelectorAll('[data-placeholder], [placeholder*="ask"], [placeholder*="message"], [placeholder*="type"]');
|
|
14
|
+
|
|
15
|
+
// 2. Find message-like containers
|
|
16
|
+
const messageContainers = [];
|
|
17
|
+
// Look for common message list patterns
|
|
18
|
+
const candidates = [
|
|
19
|
+
...document.querySelectorAll('[class*="message"]'),
|
|
20
|
+
...document.querySelectorAll('[class*="chat"]'),
|
|
21
|
+
...document.querySelectorAll('[class*="conversation"]'),
|
|
22
|
+
...document.querySelectorAll('[class*="thread"]'),
|
|
23
|
+
...document.querySelectorAll('[role="log"]'),
|
|
24
|
+
...document.querySelectorAll('[role="list"]'),
|
|
25
|
+
];
|
|
26
|
+
for (const el of candidates) {
|
|
27
|
+
messageContainers.push({
|
|
28
|
+
tag: el.tagName?.toLowerCase(),
|
|
29
|
+
class: (el.className && typeof el.className === 'string') ? el.className.substring(0, 200) : null,
|
|
30
|
+
role: el.getAttribute('role'),
|
|
31
|
+
childCount: el.children?.length || 0,
|
|
32
|
+
text: (el.textContent || '').trim().substring(0, 200),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 3. Find model/mode selectors
|
|
37
|
+
const modelCandidates = document.querySelectorAll('[class*="model"], [class*="gpt"], [aria-label*="model" i], [aria-label*="모델" i]');
|
|
38
|
+
|
|
39
|
+
// 4. Enumerate all role="button" items (conversation list items)
|
|
40
|
+
const roleButtons = document.querySelectorAll('[role="button"]');
|
|
41
|
+
|
|
42
|
+
// 5. Look for "GPT" text or model name in the UI
|
|
43
|
+
const allText = root.textContent || '';
|
|
44
|
+
const modelMatch = allText.match(/(GPT-[\d.]+|gpt-[\w.-]+|o\d+-[\w]+|claude-[\w.-]+)/i);
|
|
45
|
+
|
|
46
|
+
// 6. Check if we're on a task list or chat view
|
|
47
|
+
const hasConversationList = document.querySelectorAll('[role="button"][class*="rounded-lg"]').length > 0;
|
|
48
|
+
const headerText = document.querySelector('[style*="view-transition-name: header-title"]')?.textContent?.trim() || '';
|
|
49
|
+
|
|
50
|
+
return JSON.stringify({
|
|
51
|
+
headerText,
|
|
52
|
+
isTaskList: headerText === '작업' || headerText === 'Tasks',
|
|
53
|
+
modelFound: modelMatch ? modelMatch[0] : null,
|
|
54
|
+
textareaCount: textareas.length,
|
|
55
|
+
textareas: Array.from(textareas).map(t => ({
|
|
56
|
+
class: t.className?.substring(0, 200),
|
|
57
|
+
placeholder: t.placeholder?.substring(0, 200),
|
|
58
|
+
rows: t.rows,
|
|
59
|
+
value: (t.value || '').substring(0, 100),
|
|
60
|
+
rect: (() => { const r = t.getBoundingClientRect(); return { w: Math.round(r.width), h: Math.round(r.height) }; })(),
|
|
61
|
+
})),
|
|
62
|
+
contentEditableCount: contentEditables.length,
|
|
63
|
+
contentEditables: Array.from(contentEditables).slice(0, 5).map(el => ({
|
|
64
|
+
tag: el.tagName?.toLowerCase(),
|
|
65
|
+
class: (el.className && typeof el.className === 'string') ? el.className.substring(0, 200) : null,
|
|
66
|
+
text: (el.textContent || '').trim().substring(0, 100),
|
|
67
|
+
placeholder: el.getAttribute('data-placeholder')?.substring(0, 100),
|
|
68
|
+
})),
|
|
69
|
+
chatInputCandidates: Array.from(chatInputCandidates).slice(0, 5).map(el => ({
|
|
70
|
+
tag: el.tagName?.toLowerCase(),
|
|
71
|
+
class: (el.className && typeof el.className === 'string') ? el.className.substring(0, 200) : null,
|
|
72
|
+
placeholder: el.getAttribute('placeholder') || el.getAttribute('data-placeholder'),
|
|
73
|
+
})),
|
|
74
|
+
messageContainers: messageContainers.slice(0, 15),
|
|
75
|
+
modelCandidates: Array.from(modelCandidates).slice(0, 5).map(el => ({
|
|
76
|
+
tag: el.tagName?.toLowerCase(),
|
|
77
|
+
class: (el.className && typeof el.className === 'string') ? el.className.substring(0, 200) : null,
|
|
78
|
+
text: (el.textContent || '').trim().substring(0, 100),
|
|
79
|
+
})),
|
|
80
|
+
conversationButtons: Array.from(roleButtons).slice(0, 5).map(b => ({
|
|
81
|
+
class: (b.className && typeof b.className === 'string') ? b.className.substring(0, 200) : null,
|
|
82
|
+
text: (b.textContent || '').trim().substring(0, 150),
|
|
83
|
+
})),
|
|
84
|
+
});
|
|
85
|
+
} catch (e) {
|
|
86
|
+
return JSON.stringify({ error: e.message || String(e) });
|
|
87
|
+
}
|
|
88
|
+
})()
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex — Test different click methods on model button
|
|
3
|
+
*/
|
|
4
|
+
(() => {
|
|
5
|
+
try {
|
|
6
|
+
const buttons = Array.from(document.querySelectorAll('button')).filter(b => b.offsetWidth > 0);
|
|
7
|
+
const modelBtn = buttons.find(b => {
|
|
8
|
+
const text = (b.textContent || '').trim();
|
|
9
|
+
return /^(GPT-|gpt-|o\d|claude-)/i.test(text) && b.getAttribute('aria-haspopup') === 'menu';
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
if (!modelBtn) return JSON.stringify({ error: 'Model button not found' });
|
|
13
|
+
|
|
14
|
+
const rect = modelBtn.getBoundingClientRect();
|
|
15
|
+
const x = rect.left + rect.width / 2;
|
|
16
|
+
const y = rect.top + rect.height / 2;
|
|
17
|
+
|
|
18
|
+
// Method 1: MouseEvent sequence (more realistic)
|
|
19
|
+
modelBtn.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, clientX: x, clientY: y }));
|
|
20
|
+
modelBtn.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, clientX: x, clientY: y }));
|
|
21
|
+
modelBtn.dispatchEvent(new PointerEvent('pointerup', { bubbles: true, clientX: x, clientY: y }));
|
|
22
|
+
modelBtn.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, clientX: x, clientY: y }));
|
|
23
|
+
modelBtn.dispatchEvent(new MouseEvent('click', { bubbles: true, clientX: x, clientY: y }));
|
|
24
|
+
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
setTimeout(() => {
|
|
27
|
+
const btnState = {
|
|
28
|
+
ariaExpanded: modelBtn.getAttribute('aria-expanded'),
|
|
29
|
+
dataState: modelBtn.getAttribute('data-state'),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Check for open elements
|
|
33
|
+
const allElements = document.querySelectorAll('*');
|
|
34
|
+
const openElements = [];
|
|
35
|
+
for (const el of allElements) {
|
|
36
|
+
const ds = el.getAttribute('data-state');
|
|
37
|
+
const role = el.getAttribute('role');
|
|
38
|
+
if (ds === 'open' || role === 'menu' || role === 'listbox') {
|
|
39
|
+
openElements.push({
|
|
40
|
+
tag: el.tagName?.toLowerCase(),
|
|
41
|
+
role, dataState: ds,
|
|
42
|
+
childCount: el.children?.length || 0,
|
|
43
|
+
text: (el.textContent || '').trim().substring(0, 300),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Close
|
|
49
|
+
document.dispatchEvent(new KeyboardEvent('keydown', {
|
|
50
|
+
key: 'Escape', code: 'Escape', keyCode: 27, bubbles: true
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
resolve(JSON.stringify({
|
|
54
|
+
btnRect: { x: Math.round(x), y: Math.round(y), w: Math.round(rect.width), h: Math.round(rect.height) },
|
|
55
|
+
btnState,
|
|
56
|
+
openElementCount: openElements.length,
|
|
57
|
+
openElements,
|
|
58
|
+
}));
|
|
59
|
+
}, 800);
|
|
60
|
+
});
|
|
61
|
+
} catch (e) {
|
|
62
|
+
return JSON.stringify({ error: e.message || String(e) });
|
|
63
|
+
}
|
|
64
|
+
})()
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check the raw DOM structure of message 15 (code block message)
|
|
3
|
+
*/
|
|
4
|
+
(() => {
|
|
5
|
+
try {
|
|
6
|
+
const turnEls = document.querySelectorAll('[data-content-search-turn-key]');
|
|
7
|
+
const results = [];
|
|
8
|
+
|
|
9
|
+
for (const turnEl of turnEls) {
|
|
10
|
+
const unitEls = turnEl.querySelectorAll('[data-content-search-unit-key]');
|
|
11
|
+
for (const unitEl of unitEls) {
|
|
12
|
+
const unitKey = unitEl.getAttribute('data-content-search-unit-key') || '';
|
|
13
|
+
const parts = unitKey.split(':');
|
|
14
|
+
const role = parts[parts.length - 1];
|
|
15
|
+
|
|
16
|
+
if (role === 'assistant') {
|
|
17
|
+
// Look for code/pre elements
|
|
18
|
+
const pres = unitEl.querySelectorAll('pre');
|
|
19
|
+
const codes = unitEl.querySelectorAll('code');
|
|
20
|
+
|
|
21
|
+
if (pres.length > 0 || codes.length > 0) {
|
|
22
|
+
// Dump the inner structure
|
|
23
|
+
const dumpEl = (el, depth) => {
|
|
24
|
+
if (depth > 5) return [];
|
|
25
|
+
const items = [];
|
|
26
|
+
for (const child of el.children) {
|
|
27
|
+
items.push({
|
|
28
|
+
depth,
|
|
29
|
+
tag: child.tagName?.toLowerCase(),
|
|
30
|
+
class: (child.className && typeof child.className === 'string') ? child.className.substring(0, 150) : null,
|
|
31
|
+
text: child.children.length === 0 ? (child.textContent || '').substring(0, 100) : '',
|
|
32
|
+
childCount: child.children.length,
|
|
33
|
+
});
|
|
34
|
+
items.push(...dumpEl(child, depth + 1));
|
|
35
|
+
}
|
|
36
|
+
return items;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
results.push({
|
|
40
|
+
unitKey: unitKey.substring(0, 60),
|
|
41
|
+
preCount: pres.length,
|
|
42
|
+
codeCount: codes.length,
|
|
43
|
+
innerHTML: unitEl.innerHTML?.substring(0, 2000),
|
|
44
|
+
tree: dumpEl(unitEl, 0).slice(0, 30),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return JSON.stringify({ messages: results.slice(-3) });
|
|
52
|
+
} catch (e) {
|
|
53
|
+
return JSON.stringify({ error: e.message || String(e) });
|
|
54
|
+
}
|
|
55
|
+
})()
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex Extension — list_models
|
|
3
|
+
*
|
|
4
|
+
* Opens the model selector dropdown via pointer events,
|
|
5
|
+
* reads available models from the Radix menu, then closes.
|
|
6
|
+
*/
|
|
7
|
+
(() => {
|
|
8
|
+
try {
|
|
9
|
+
const buttons = Array.from(document.querySelectorAll('button')).filter(b => b.offsetWidth > 0);
|
|
10
|
+
const modelBtn = buttons.find(b => {
|
|
11
|
+
const text = (b.textContent || '').trim();
|
|
12
|
+
return /^(GPT-|gpt-|o\d|claude-)/i.test(text) && b.getAttribute('aria-haspopup') === 'menu';
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
if (!modelBtn) return JSON.stringify({ error: 'Model selector button not found' });
|
|
16
|
+
|
|
17
|
+
const currentModel = (modelBtn.textContent || '').trim();
|
|
18
|
+
|
|
19
|
+
// Open dropdown with full pointer event sequence (required for Radix)
|
|
20
|
+
const rect = modelBtn.getBoundingClientRect();
|
|
21
|
+
const x = rect.left + rect.width / 2;
|
|
22
|
+
const y = rect.top + rect.height / 2;
|
|
23
|
+
modelBtn.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, clientX: x, clientY: y }));
|
|
24
|
+
modelBtn.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, clientX: x, clientY: y }));
|
|
25
|
+
modelBtn.dispatchEvent(new PointerEvent('pointerup', { bubbles: true, clientX: x, clientY: y }));
|
|
26
|
+
modelBtn.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, clientX: x, clientY: y }));
|
|
27
|
+
modelBtn.dispatchEvent(new MouseEvent('click', { bubbles: true, clientX: x, clientY: y }));
|
|
28
|
+
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
setTimeout(() => {
|
|
31
|
+
// Read menu items
|
|
32
|
+
const menu = document.querySelector('[role="menu"][data-state="open"]');
|
|
33
|
+
const models = [];
|
|
34
|
+
if (menu) {
|
|
35
|
+
const items = menu.querySelectorAll('[role="menuitem"], [role="menuitemradio"], div[class*="cursor-interaction"]');
|
|
36
|
+
for (const item of items) {
|
|
37
|
+
const text = (item.textContent || '').trim();
|
|
38
|
+
if (text && text.length > 0 && text.length < 60 && !text.includes('모델 선택') && !text.includes('Model')) {
|
|
39
|
+
models.push({
|
|
40
|
+
name: text,
|
|
41
|
+
selected: text === currentModel,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Close dropdown
|
|
48
|
+
document.dispatchEvent(new KeyboardEvent('keydown', {
|
|
49
|
+
key: 'Escape', code: 'Escape', keyCode: 27, bubbles: true
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
resolve(JSON.stringify({
|
|
53
|
+
currentModel,
|
|
54
|
+
models,
|
|
55
|
+
count: models.length,
|
|
56
|
+
}));
|
|
57
|
+
}, 500);
|
|
58
|
+
});
|
|
59
|
+
} catch (e) {
|
|
60
|
+
return JSON.stringify({ error: e.message || String(e) });
|
|
61
|
+
}
|
|
62
|
+
})()
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex — Message structure deep dive
|
|
3
|
+
* Find the actual message DOM elements within the thread
|
|
4
|
+
*/
|
|
5
|
+
(() => {
|
|
6
|
+
try {
|
|
7
|
+
// The thread content area
|
|
8
|
+
const threadContent = document.querySelector('[class*="thread-content"]')
|
|
9
|
+
|| document.querySelector('[class*="overflow-y-auto"][class*="px-panel"]');
|
|
10
|
+
if (!threadContent) return JSON.stringify({ error: 'no thread content area' });
|
|
11
|
+
|
|
12
|
+
// Get ALL elements with data attributes
|
|
13
|
+
const dataAttrs = [];
|
|
14
|
+
threadContent.querySelectorAll('*').forEach(el => {
|
|
15
|
+
const attrs = el.attributes;
|
|
16
|
+
for (let i = 0; i < attrs.length; i++) {
|
|
17
|
+
if (attrs[i].name.startsWith('data-')) {
|
|
18
|
+
dataAttrs.push({
|
|
19
|
+
tag: el.tagName?.toLowerCase(),
|
|
20
|
+
attr: attrs[i].name,
|
|
21
|
+
value: attrs[i].value?.substring(0, 100),
|
|
22
|
+
class: (el.className && typeof el.className === 'string') ? el.className.substring(0, 150) : null,
|
|
23
|
+
text: (el.textContent || '').trim().substring(0, 100),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Dump full tree of message area (limited depth)
|
|
30
|
+
const tree = [];
|
|
31
|
+
const walk = (el, depth) => {
|
|
32
|
+
if (depth > 6 || tree.length > 80) return;
|
|
33
|
+
const cls = (el.className && typeof el.className === 'string') ? el.className : '';
|
|
34
|
+
tree.push({
|
|
35
|
+
depth,
|
|
36
|
+
tag: el.tagName?.toLowerCase(),
|
|
37
|
+
class: cls.substring(0, 250),
|
|
38
|
+
id: el.id || null,
|
|
39
|
+
role: el.getAttribute('role'),
|
|
40
|
+
childCount: el.children?.length || 0,
|
|
41
|
+
isLeaf: el.children?.length === 0,
|
|
42
|
+
text: el.children?.length === 0 ? (el.textContent || '').trim().substring(0, 150) : '',
|
|
43
|
+
});
|
|
44
|
+
for (const child of el.children) {
|
|
45
|
+
walk(child, depth + 1);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
walk(threadContent, 0);
|
|
49
|
+
|
|
50
|
+
// Also check the composer/input area
|
|
51
|
+
const proseMirror = document.querySelector('.ProseMirror');
|
|
52
|
+
const composerInfo = proseMirror ? {
|
|
53
|
+
tag: proseMirror.tagName?.toLowerCase(),
|
|
54
|
+
class: proseMirror.className?.substring(0, 200),
|
|
55
|
+
contentEditable: proseMirror.contentEditable,
|
|
56
|
+
text: (proseMirror.textContent || '').trim().substring(0, 100),
|
|
57
|
+
childCount: proseMirror.children?.length || 0,
|
|
58
|
+
innerHTML: proseMirror.innerHTML?.substring(0, 300),
|
|
59
|
+
} : null;
|
|
60
|
+
|
|
61
|
+
// Footer area (model, mode selectors)
|
|
62
|
+
const footer = document.querySelector('[class*="thread-composer-max-width"][class*="pb-2"]');
|
|
63
|
+
const footerButtons = footer ? Array.from(footer.querySelectorAll('button')).map(b => ({
|
|
64
|
+
text: (b.textContent || '').trim().substring(0, 60),
|
|
65
|
+
class: b.className?.substring(0, 100),
|
|
66
|
+
ariaLabel: b.getAttribute('aria-label')?.substring(0, 60),
|
|
67
|
+
})) : [];
|
|
68
|
+
|
|
69
|
+
return JSON.stringify({
|
|
70
|
+
threadContentClass: (threadContent.className && typeof threadContent.className === 'string') ? threadContent.className.substring(0, 300) : null,
|
|
71
|
+
dataAttrs: dataAttrs.slice(0, 30),
|
|
72
|
+
tree: tree.slice(0, 80),
|
|
73
|
+
composerInfo,
|
|
74
|
+
footerButtons,
|
|
75
|
+
});
|
|
76
|
+
} catch (e) {
|
|
77
|
+
return JSON.stringify({ error: e.message || String(e) });
|
|
78
|
+
}
|
|
79
|
+
})()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex Extension — new_session
|
|
3
|
+
* Clicks the "새 채팅" / "New chat" button
|
|
4
|
+
*/
|
|
5
|
+
(() => {
|
|
6
|
+
try {
|
|
7
|
+
const buttons = Array.from(document.querySelectorAll('button')).filter(b => b.offsetWidth > 0);
|
|
8
|
+
const newChatBtn = buttons.find(b => {
|
|
9
|
+
const label = (b.getAttribute('aria-label') || '').toLowerCase();
|
|
10
|
+
return label.includes('새 채팅') || label.includes('new chat') || label.includes('new');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
if (newChatBtn) {
|
|
14
|
+
newChatBtn.click();
|
|
15
|
+
return JSON.stringify({ success: true, action: 'new_session' });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return JSON.stringify({
|
|
19
|
+
success: false,
|
|
20
|
+
error: 'New chat button not found',
|
|
21
|
+
available: buttons.map(b => b.getAttribute('aria-label')).filter(Boolean),
|
|
22
|
+
});
|
|
23
|
+
} catch (e) {
|
|
24
|
+
return JSON.stringify({ error: e.message || String(e) });
|
|
25
|
+
}
|
|
26
|
+
})()
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex Extension — read_chat (v2 — rich content extraction)
|
|
3
|
+
*
|
|
4
|
+
* Structure (Codex — openai.chatgpt):
|
|
5
|
+
* - Root: #root (React app via ProseMirror)
|
|
6
|
+
* - Messages: data-content-search-turn-key="UUID" per turn
|
|
7
|
+
* - Unit: data-content-search-unit-key="UUID:index:role"
|
|
8
|
+
* - Thread: data-thread-find-target="conversation"
|
|
9
|
+
* - Input: .ProseMirror (contentEditable)
|
|
10
|
+
* - Model: footer button text (e.g. "GPT-5.4")
|
|
11
|
+
* - Status indicators: button aria-labels, presence of cancel button
|
|
12
|
+
*
|
|
13
|
+
* Rich content support:
|
|
14
|
+
* - Code blocks (PRE/CODE → ```lang\n...\n```)
|
|
15
|
+
* - Tables (TABLE → markdown table)
|
|
16
|
+
* - Lists (UL/OL → bullet/numbered items)
|
|
17
|
+
* - Inline code (CODE → `...`)
|
|
18
|
+
* - Block elements (P, DIV, H1-H6 → newlines)
|
|
19
|
+
* - Bold/Italic preservation
|
|
20
|
+
*
|
|
21
|
+
* Runs inside webview frame via evaluateInWebviewFrame.
|
|
22
|
+
*/
|
|
23
|
+
(() => {
|
|
24
|
+
try {
|
|
25
|
+
const root = document.getElementById('root');
|
|
26
|
+
if (!root) return JSON.stringify({ error: 'no root element' });
|
|
27
|
+
|
|
28
|
+
const isVisible = root.offsetHeight > 0;
|
|
29
|
+
|
|
30
|
+
const headerEl = document.querySelector('[style*="view-transition-name: header-title"]');
|
|
31
|
+
const headerText = (headerEl?.textContent || '').trim();
|
|
32
|
+
const isTaskList = headerText === '작업' || headerText === 'Tasks';
|
|
33
|
+
|
|
34
|
+
// ─── Rich content extractor ───
|
|
35
|
+
const BLOCK_TAGS = new Set(['DIV', 'P', 'BR', 'LI', 'TR', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'BLOCKQUOTE', 'HR', 'SECTION', 'ARTICLE']);
|
|
36
|
+
|
|
37
|
+
function extractRichContent(container) {
|
|
38
|
+
let out = '';
|
|
39
|
+
|
|
40
|
+
function extractCode(node) {
|
|
41
|
+
if (node.nodeType === 3) return node.textContent || '';
|
|
42
|
+
if (node.nodeType !== 1) return '';
|
|
43
|
+
if (node.tagName === 'BR') return '\n';
|
|
44
|
+
const parts = [];
|
|
45
|
+
for (const c of node.childNodes) {
|
|
46
|
+
const isBlock = c.nodeType === 1 && BLOCK_TAGS.has(c.tagName);
|
|
47
|
+
const tx = extractCode(c);
|
|
48
|
+
if (tx) {
|
|
49
|
+
if (isBlock && parts.length > 0) parts.push('\n');
|
|
50
|
+
parts.push(tx);
|
|
51
|
+
if (isBlock) parts.push('\n');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return parts.join('').replace(/\n{3,}/g, '\n\n');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function extractTable(table) {
|
|
58
|
+
const rows = [];
|
|
59
|
+
for (const tr of table.querySelectorAll('tr')) {
|
|
60
|
+
const cells = [];
|
|
61
|
+
for (const cell of tr.querySelectorAll('th, td')) {
|
|
62
|
+
cells.push((cell.textContent || '').trim().replace(/\|/g, '\\|'));
|
|
63
|
+
}
|
|
64
|
+
rows.push(cells);
|
|
65
|
+
}
|
|
66
|
+
if (rows.length === 0) return '';
|
|
67
|
+
|
|
68
|
+
const colCount = Math.max(...rows.map(r => r.length));
|
|
69
|
+
const lines = [];
|
|
70
|
+
|
|
71
|
+
// Header row
|
|
72
|
+
const header = rows[0] || [];
|
|
73
|
+
while (header.length < colCount) header.push('');
|
|
74
|
+
lines.push('| ' + header.join(' | ') + ' |');
|
|
75
|
+
lines.push('| ' + header.map(() => '---').join(' | ') + ' |');
|
|
76
|
+
|
|
77
|
+
// Data rows
|
|
78
|
+
for (let i = 1; i < rows.length; i++) {
|
|
79
|
+
const row = rows[i];
|
|
80
|
+
while (row.length < colCount) row.push('');
|
|
81
|
+
lines.push('| ' + row.join(' | ') + ' |');
|
|
82
|
+
}
|
|
83
|
+
return lines.join('\n');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function extractList(list) {
|
|
87
|
+
const items = [];
|
|
88
|
+
const isOrdered = list.tagName === 'OL';
|
|
89
|
+
let idx = 1;
|
|
90
|
+
for (const li of list.querySelectorAll(':scope > li')) {
|
|
91
|
+
const prefix = isOrdered ? `${idx++}. ` : '- ';
|
|
92
|
+
const text = (li.textContent || '').trim();
|
|
93
|
+
if (text) items.push(prefix + text);
|
|
94
|
+
}
|
|
95
|
+
return items.join('\n');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function walk(node) {
|
|
99
|
+
if (node.nodeType === 3) {
|
|
100
|
+
out += node.textContent;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (node.nodeType !== 1) return;
|
|
104
|
+
const el = node;
|
|
105
|
+
const tag = el.tagName;
|
|
106
|
+
const cls = (el.className && typeof el.className === 'string') ? el.className : '';
|
|
107
|
+
|
|
108
|
+
// ─ Codex code block wrapper (div.bg-token-text-code-block-background)
|
|
109
|
+
if (tag === 'DIV' && cls.includes('bg-token-text-code-block-background')) {
|
|
110
|
+
const langLabel = el.querySelector('.sticky .truncate, .sticky div:first-child');
|
|
111
|
+
const lang = langLabel ? (langLabel.textContent || '').trim() : '';
|
|
112
|
+
const codeEl = el.querySelector('code');
|
|
113
|
+
const codeText = codeEl ? extractCode(codeEl).trim() : (el.textContent || '').trim();
|
|
114
|
+
const cleanCode = (lang && codeText.startsWith(lang))
|
|
115
|
+
? codeText.substring(lang.length).trim()
|
|
116
|
+
: codeText;
|
|
117
|
+
out += '\n```' + lang + '\n' + cleanCode + '\n```\n';
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ─ Code block (PRE — standard fallback)
|
|
122
|
+
if (tag === 'PRE') {
|
|
123
|
+
const codeEl = el.querySelector('code');
|
|
124
|
+
const lang = codeEl
|
|
125
|
+
? (codeEl.className.match(/language-(\w+)/)?.[1] || '')
|
|
126
|
+
: '';
|
|
127
|
+
const codeText = extractCode(codeEl || el).trim();
|
|
128
|
+
out += '\n```' + lang + '\n' + codeText + '\n```\n';
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ─ Codex inline code (span._inlineMarkdown.font-mono)
|
|
133
|
+
if (tag === 'SPAN' && cls.includes('font-mono') && cls.includes('inline-markdown')) {
|
|
134
|
+
out += '`' + (el.textContent || '') + '`';
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ─ Inline code (CODE not inside PRE)
|
|
139
|
+
if (tag === 'CODE') {
|
|
140
|
+
out += '`' + (el.textContent || '') + '`';
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ─ Table
|
|
145
|
+
if (tag === 'TABLE') {
|
|
146
|
+
const md = extractTable(el);
|
|
147
|
+
if (md) {
|
|
148
|
+
if (out && !out.endsWith('\n')) out += '\n';
|
|
149
|
+
out += md + '\n';
|
|
150
|
+
}
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ─ Lists
|
|
155
|
+
if (tag === 'UL' || tag === 'OL') {
|
|
156
|
+
const md = extractList(el);
|
|
157
|
+
if (md) {
|
|
158
|
+
if (out && !out.endsWith('\n')) out += '\n';
|
|
159
|
+
out += md + '\n';
|
|
160
|
+
}
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ─ Headings
|
|
165
|
+
if (/^H[1-6]$/.test(tag)) {
|
|
166
|
+
const level = parseInt(tag[1]);
|
|
167
|
+
if (out && !out.endsWith('\n')) out += '\n';
|
|
168
|
+
out += '#'.repeat(level) + ' ' + (el.textContent || '').trim() + '\n';
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ─ Horizontal rule
|
|
173
|
+
if (tag === 'HR') {
|
|
174
|
+
out += '\n---\n';
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ─ BR
|
|
179
|
+
if (tag === 'BR') {
|
|
180
|
+
out += '\n';
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ─ Bold / Italic / Strong
|
|
185
|
+
if (tag === 'STRONG' || tag === 'B') {
|
|
186
|
+
out += '**' + (el.textContent || '') + '**';
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (tag === 'EM' || tag === 'I') {
|
|
190
|
+
out += '*' + (el.textContent || '') + '*';
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ─ Block elements (P, DIV, BLOCKQUOTE, SECTION, etc.)
|
|
195
|
+
if (BLOCK_TAGS.has(tag)) {
|
|
196
|
+
if (out && !out.endsWith('\n')) out += '\n';
|
|
197
|
+
if (tag === 'BLOCKQUOTE') out += '> ';
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Recurse children
|
|
201
|
+
for (const child of el.childNodes) walk(child);
|
|
202
|
+
|
|
203
|
+
// After block element, ensure newline
|
|
204
|
+
if (BLOCK_TAGS.has(tag) && out && !out.endsWith('\n')) {
|
|
205
|
+
out += '\n';
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
walk(container);
|
|
210
|
+
return out
|
|
211
|
+
.replace(/\n{3,}/g, '\n\n') // collapse excessive newlines
|
|
212
|
+
.replace(/^\n+/, '') // trim leading
|
|
213
|
+
.replace(/\n+$/, '') // trim trailing
|
|
214
|
+
.trim();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ─── 1. Messages ───
|
|
218
|
+
const messages = [];
|
|
219
|
+
const turnEls = document.querySelectorAll('[data-content-search-turn-key]');
|
|
220
|
+
|
|
221
|
+
for (const turnEl of turnEls) {
|
|
222
|
+
const turnKey = turnEl.getAttribute('data-content-search-turn-key');
|
|
223
|
+
const unitEls = turnEl.querySelectorAll('[data-content-search-unit-key]');
|
|
224
|
+
|
|
225
|
+
for (const unitEl of unitEls) {
|
|
226
|
+
const unitKey = unitEl.getAttribute('data-content-search-unit-key') || '';
|
|
227
|
+
const parts = unitKey.split(':');
|
|
228
|
+
const role = parts.length >= 3 ? parts[parts.length - 1] : 'assistant';
|
|
229
|
+
|
|
230
|
+
const content = extractRichContent(unitEl);
|
|
231
|
+
|
|
232
|
+
if (!content || content.length < 1) continue;
|
|
233
|
+
const trimmed = content.length > 3000 ? content.substring(0, 3000) + '…' : content;
|
|
234
|
+
|
|
235
|
+
messages.push({
|
|
236
|
+
role: role === 'user' ? 'user' : 'assistant',
|
|
237
|
+
content: trimmed,
|
|
238
|
+
timestamp: Date.now() - (turnEls.length - messages.length) * 1000,
|
|
239
|
+
_turnKey: turnKey,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Fallback
|
|
245
|
+
if (messages.length === 0 && !isTaskList) {
|
|
246
|
+
const threadArea = document.querySelector('[data-thread-find-target="conversation"]');
|
|
247
|
+
if (threadArea) {
|
|
248
|
+
const text = extractRichContent(threadArea);
|
|
249
|
+
if (text.length > 0) {
|
|
250
|
+
messages.push({
|
|
251
|
+
role: 'assistant',
|
|
252
|
+
content: text.substring(0, 3000),
|
|
253
|
+
timestamp: Date.now(),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ─── 2. Input field ───
|
|
260
|
+
let inputContent = '';
|
|
261
|
+
const proseMirror = document.querySelector('.ProseMirror');
|
|
262
|
+
if (proseMirror) {
|
|
263
|
+
const placeholder = proseMirror.querySelector('.placeholder');
|
|
264
|
+
const text = (proseMirror.textContent || '').trim();
|
|
265
|
+
if (text && (!placeholder || text !== (placeholder.textContent || '').trim())) {
|
|
266
|
+
inputContent = text;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ─── 3. Status ───
|
|
271
|
+
let status = 'idle';
|
|
272
|
+
const buttons = Array.from(document.querySelectorAll('button'))
|
|
273
|
+
.filter(b => b.offsetWidth > 0);
|
|
274
|
+
const buttonTexts = buttons.map(b => (b.textContent || '').trim().toLowerCase());
|
|
275
|
+
const buttonLabels = buttons.map(b => (b.getAttribute('aria-label') || '').toLowerCase());
|
|
276
|
+
|
|
277
|
+
if (buttonTexts.includes('cancel') || buttonTexts.includes('취소') ||
|
|
278
|
+
buttonLabels.some(l => l.includes('cancel') || l.includes('취소') || l.includes('stop') || l.includes('중지'))) {
|
|
279
|
+
status = 'generating';
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const approvalPatterns = /^(approve|accept|allow|confirm|run|proceed|yes|승인|허용|실행)/i;
|
|
283
|
+
if (buttonTexts.some(b => approvalPatterns.test(b)) ||
|
|
284
|
+
buttonLabels.some(l => approvalPatterns.test(l))) {
|
|
285
|
+
status = 'waiting_approval';
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (isTaskList) {
|
|
289
|
+
status = messages.length === 0 ? 'idle' : status;
|
|
290
|
+
}
|
|
291
|
+
if (!isVisible && messages.length === 0) status = 'panel_hidden';
|
|
292
|
+
|
|
293
|
+
// ─── 4. Model / Mode ───
|
|
294
|
+
let model = '';
|
|
295
|
+
let mode = '';
|
|
296
|
+
const footerButtons = document.querySelectorAll(
|
|
297
|
+
'[class*="thread-composer-max-width"] button, [class*="pb-2"] button'
|
|
298
|
+
);
|
|
299
|
+
for (const btn of footerButtons) {
|
|
300
|
+
const text = (btn.textContent || '').trim();
|
|
301
|
+
if (/^(GPT-|gpt-|o\d|claude-)/i.test(text)) model = text;
|
|
302
|
+
if (/^(낮음|중간|높음|low|medium|high)$/i.test(text)) mode = text;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ─── 5. Approval modal ───
|
|
306
|
+
let activeModal = null;
|
|
307
|
+
if (status === 'waiting_approval') {
|
|
308
|
+
const approvalBtns = buttons
|
|
309
|
+
.map(b => (b.textContent || '').trim())
|
|
310
|
+
.filter(t => t && t.length > 0 && t.length < 40 &&
|
|
311
|
+
/approve|accept|allow|confirm|run|proceed|cancel|deny|reject|승인|허용|실행|취소|거부/i.test(t));
|
|
312
|
+
if (approvalBtns.length > 0) {
|
|
313
|
+
activeModal = {
|
|
314
|
+
message: 'Codex wants to perform an action',
|
|
315
|
+
buttons: [...new Set(approvalBtns)],
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ─── 6. Task info ───
|
|
321
|
+
const taskBtn = document.querySelector('[aria-label*="작업"], [aria-label*="task" i]');
|
|
322
|
+
const taskInfo = taskBtn ? (taskBtn.textContent || '').trim() : '';
|
|
323
|
+
|
|
324
|
+
return JSON.stringify({
|
|
325
|
+
agentType: 'codex',
|
|
326
|
+
agentName: 'Codex',
|
|
327
|
+
extensionId: 'openai.chatgpt',
|
|
328
|
+
status,
|
|
329
|
+
isVisible,
|
|
330
|
+
isTaskList,
|
|
331
|
+
title: headerText || 'Codex',
|
|
332
|
+
messages: messages.slice(-30),
|
|
333
|
+
inputContent,
|
|
334
|
+
model,
|
|
335
|
+
mode,
|
|
336
|
+
taskInfo,
|
|
337
|
+
activeModal,
|
|
338
|
+
});
|
|
339
|
+
} catch (e) {
|
|
340
|
+
return JSON.stringify({ error: e.message || String(e) });
|
|
341
|
+
}
|
|
342
|
+
})()
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex Extension — resolve_action
|
|
3
|
+
*
|
|
4
|
+
* Clicks approval/denial buttons in the Codex UI.
|
|
5
|
+
* Actions: "approve", "deny", "cancel"
|
|
6
|
+
*
|
|
7
|
+
* Placeholder: ${ACTION}
|
|
8
|
+
*/
|
|
9
|
+
(() => {
|
|
10
|
+
try {
|
|
11
|
+
const action = ${ACTION};
|
|
12
|
+
|
|
13
|
+
const buttons = Array.from(document.querySelectorAll('button'))
|
|
14
|
+
.filter(b => b.offsetWidth > 0);
|
|
15
|
+
|
|
16
|
+
// Map action to button text patterns
|
|
17
|
+
const patterns = {
|
|
18
|
+
approve: /^(approve|accept|allow|confirm|run|proceed|yes|승인|허용|실행|확인)/i,
|
|
19
|
+
deny: /^(deny|reject|no|거부|아니오)/i,
|
|
20
|
+
cancel: /^(cancel|stop|취소|중지)/i,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const pattern = patterns[action.toLowerCase()] || patterns.approve;
|
|
24
|
+
|
|
25
|
+
for (const btn of buttons) {
|
|
26
|
+
const text = (btn.textContent || '').trim();
|
|
27
|
+
const label = btn.getAttribute('aria-label') || '';
|
|
28
|
+
if (pattern.test(text) || pattern.test(label)) {
|
|
29
|
+
btn.click();
|
|
30
|
+
return JSON.stringify({ success: true, action, clicked: text || label });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return JSON.stringify({
|
|
35
|
+
success: false,
|
|
36
|
+
error: `No button matching action '${action}' found`,
|
|
37
|
+
available: buttons.map(b => (b.textContent || '').trim()).filter(t => t.length > 0 && t.length < 40),
|
|
38
|
+
});
|
|
39
|
+
} catch (e) {
|
|
40
|
+
return JSON.stringify({ error: e.message || String(e) });
|
|
41
|
+
}
|
|
42
|
+
})()
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex Extension — send_message
|
|
3
|
+
*
|
|
4
|
+
* Types a message into the ProseMirror input and submits it.
|
|
5
|
+
* Uses InputEvent dispatch for ProseMirror compatibility.
|
|
6
|
+
*
|
|
7
|
+
* Placeholder: ${MESSAGE}
|
|
8
|
+
*/
|
|
9
|
+
(() => {
|
|
10
|
+
try {
|
|
11
|
+
const message = ${MESSAGE};
|
|
12
|
+
|
|
13
|
+
// Find ProseMirror editor
|
|
14
|
+
const editor = document.querySelector('.ProseMirror');
|
|
15
|
+
if (!editor) return JSON.stringify({ error: 'ProseMirror editor not found' });
|
|
16
|
+
|
|
17
|
+
// Focus the editor
|
|
18
|
+
editor.focus();
|
|
19
|
+
|
|
20
|
+
// Clear existing content
|
|
21
|
+
const existingP = editor.querySelector('p');
|
|
22
|
+
if (existingP) {
|
|
23
|
+
existingP.textContent = message;
|
|
24
|
+
// Dispatch input event for ProseMirror to detect the change
|
|
25
|
+
editor.dispatchEvent(new InputEvent('input', {
|
|
26
|
+
bubbles: true,
|
|
27
|
+
cancelable: true,
|
|
28
|
+
inputType: 'insertText',
|
|
29
|
+
data: message,
|
|
30
|
+
}));
|
|
31
|
+
} else {
|
|
32
|
+
// Fallback: create new paragraph
|
|
33
|
+
const p = document.createElement('p');
|
|
34
|
+
p.textContent = message;
|
|
35
|
+
editor.innerHTML = '';
|
|
36
|
+
editor.appendChild(p);
|
|
37
|
+
editor.dispatchEvent(new InputEvent('input', {
|
|
38
|
+
bubbles: true,
|
|
39
|
+
cancelable: true,
|
|
40
|
+
inputType: 'insertText',
|
|
41
|
+
data: message,
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Wait a tick then submit via Enter key
|
|
46
|
+
setTimeout(() => {
|
|
47
|
+
const enterEvent = new KeyboardEvent('keydown', {
|
|
48
|
+
key: 'Enter',
|
|
49
|
+
code: 'Enter',
|
|
50
|
+
keyCode: 13,
|
|
51
|
+
which: 13,
|
|
52
|
+
bubbles: true,
|
|
53
|
+
cancelable: true,
|
|
54
|
+
});
|
|
55
|
+
editor.dispatchEvent(enterEvent);
|
|
56
|
+
}, 100);
|
|
57
|
+
|
|
58
|
+
return JSON.stringify({ success: true, message: message.substring(0, 100) });
|
|
59
|
+
} catch (e) {
|
|
60
|
+
return JSON.stringify({ error: e.message || String(e) });
|
|
61
|
+
}
|
|
62
|
+
})()
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex Extension — set_model
|
|
3
|
+
*
|
|
4
|
+
* Opens the model dropdown via pointer events, clicks the target model.
|
|
5
|
+
*
|
|
6
|
+
* Placeholder: ${MODEL}
|
|
7
|
+
*/
|
|
8
|
+
(() => {
|
|
9
|
+
try {
|
|
10
|
+
const targetModel = ${MODEL};
|
|
11
|
+
|
|
12
|
+
const buttons = Array.from(document.querySelectorAll('button')).filter(b => b.offsetWidth > 0);
|
|
13
|
+
const modelBtn = buttons.find(b => {
|
|
14
|
+
const text = (b.textContent || '').trim();
|
|
15
|
+
return /^(GPT-|gpt-|o\d|claude-)/i.test(text) && b.getAttribute('aria-haspopup') === 'menu';
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
if (!modelBtn) return JSON.stringify({ error: 'Model selector button not found' });
|
|
19
|
+
|
|
20
|
+
const currentModel = (modelBtn.textContent || '').trim();
|
|
21
|
+
|
|
22
|
+
// Open dropdown with PointerEvent sequence
|
|
23
|
+
const rect = modelBtn.getBoundingClientRect();
|
|
24
|
+
const x = rect.left + rect.width / 2;
|
|
25
|
+
const y = rect.top + rect.height / 2;
|
|
26
|
+
modelBtn.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, clientX: x, clientY: y }));
|
|
27
|
+
modelBtn.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, clientX: x, clientY: y }));
|
|
28
|
+
modelBtn.dispatchEvent(new PointerEvent('pointerup', { bubbles: true, clientX: x, clientY: y }));
|
|
29
|
+
modelBtn.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, clientX: x, clientY: y }));
|
|
30
|
+
modelBtn.dispatchEvent(new MouseEvent('click', { bubbles: true, clientX: x, clientY: y }));
|
|
31
|
+
|
|
32
|
+
return new Promise((resolve) => {
|
|
33
|
+
setTimeout(() => {
|
|
34
|
+
const menu = document.querySelector('[role="menu"][data-state="open"]');
|
|
35
|
+
if (!menu) {
|
|
36
|
+
resolve(JSON.stringify({ success: false, error: 'Menu did not open' }));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Find all clickable items in the menu
|
|
41
|
+
const items = menu.querySelectorAll('[role="menuitem"], [role="menuitemradio"], div[class*="cursor-interaction"]');
|
|
42
|
+
|
|
43
|
+
for (const item of items) {
|
|
44
|
+
const text = (item.textContent || '').trim();
|
|
45
|
+
if (text.toLowerCase() === targetModel.toLowerCase() ||
|
|
46
|
+
text.toLowerCase().includes(targetModel.toLowerCase())) {
|
|
47
|
+
// Click with pointer events
|
|
48
|
+
const ir = item.getBoundingClientRect();
|
|
49
|
+
const ix = ir.left + ir.width / 2;
|
|
50
|
+
const iy = ir.top + ir.height / 2;
|
|
51
|
+
item.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, clientX: ix, clientY: iy }));
|
|
52
|
+
item.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, clientX: ix, clientY: iy }));
|
|
53
|
+
item.dispatchEvent(new PointerEvent('pointerup', { bubbles: true, clientX: ix, clientY: iy }));
|
|
54
|
+
item.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, clientX: ix, clientY: iy }));
|
|
55
|
+
item.dispatchEvent(new MouseEvent('click', { bubbles: true, clientX: ix, clientY: iy }));
|
|
56
|
+
|
|
57
|
+
resolve(JSON.stringify({
|
|
58
|
+
success: true,
|
|
59
|
+
previousModel: currentModel,
|
|
60
|
+
selectedModel: text,
|
|
61
|
+
}));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Not found — close and report
|
|
67
|
+
document.dispatchEvent(new KeyboardEvent('keydown', {
|
|
68
|
+
key: 'Escape', code: 'Escape', keyCode: 27, bubbles: true
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
const available = Array.from(items)
|
|
72
|
+
.map(el => (el.textContent || '').trim())
|
|
73
|
+
.filter(t => t.length > 0 && t.length < 60 && !t.includes('모델 선택'));
|
|
74
|
+
|
|
75
|
+
resolve(JSON.stringify({
|
|
76
|
+
success: false,
|
|
77
|
+
error: `Model '${targetModel}' not found`,
|
|
78
|
+
currentModel,
|
|
79
|
+
available,
|
|
80
|
+
}));
|
|
81
|
+
}, 500);
|
|
82
|
+
});
|
|
83
|
+
} catch (e) {
|
|
84
|
+
return JSON.stringify({ error: e.message || String(e) });
|
|
85
|
+
}
|
|
86
|
+
})()
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CDP Scripts for Codex (openai.chatgpt)
|
|
3
|
+
*
|
|
4
|
+
* All scripts run inside the Codex webview frame via evaluateInWebviewFrame.
|
|
5
|
+
* Script names containing "Webview" are automatically routed through
|
|
6
|
+
* evaluateInWebviewFrame by DevServer.
|
|
7
|
+
*
|
|
8
|
+
* For production (ProviderInstance), the AgentStreamAdapter calls
|
|
9
|
+
* evaluateInWebviewFrame directly using the provider's webviewMatchText.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const SCRIPTS_DIR = path.join(__dirname, 'scripts');
|
|
15
|
+
function loadScript(name) {
|
|
16
|
+
try { return fs.readFileSync(path.join(SCRIPTS_DIR, name), 'utf8'); }
|
|
17
|
+
catch { return null; }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ─── Production scripts ───
|
|
21
|
+
|
|
22
|
+
module.exports.readChat = function readChat() {
|
|
23
|
+
return loadScript('read_chat.js');
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
module.exports.sendMessage = function sendMessage(text) {
|
|
27
|
+
const script = loadScript('send_message.js');
|
|
28
|
+
if (!script) return null;
|
|
29
|
+
return script.replace(/\$\{\s*MESSAGE\s*\}/g, JSON.stringify(text));
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
module.exports.resolveAction = function resolveAction(action) {
|
|
33
|
+
const script = loadScript('resolve_action.js');
|
|
34
|
+
if (!script) return null;
|
|
35
|
+
return script.replace(/\$\{\s*ACTION\s*\}/g, JSON.stringify(action));
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
module.exports.listModels = function listModels() {
|
|
39
|
+
return loadScript('list_models.js');
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
module.exports.setModel = function setModel(params) {
|
|
43
|
+
const model = params?.model || params;
|
|
44
|
+
const script = loadScript('set_model.js');
|
|
45
|
+
if (!script) return null;
|
|
46
|
+
return script.replace(/\$\{\s*MODEL\s*\}/g, JSON.stringify(model));
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
module.exports.newSession = function newSession() {
|
|
50
|
+
return loadScript('new_session.js');
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// ─── DevConsole debug helpers (names contain "Webview" for auto-routing) ───
|
|
54
|
+
|
|
55
|
+
module.exports.readChatWebview = function readChatWebview() {
|
|
56
|
+
return loadScript('read_chat.js');
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
module.exports.sendMessageWebview = function sendMessageWebview(text) {
|
|
60
|
+
const script = loadScript('send_message.js');
|
|
61
|
+
if (!script) return null;
|
|
62
|
+
return script.replace(/\$\{\s*MESSAGE\s*\}/g, JSON.stringify(text));
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
module.exports.listModelsWebview = function listModelsWebview() {
|
|
66
|
+
return loadScript('list_models.js');
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
module.exports.setModelWebview = function setModelWebview(params) {
|
|
70
|
+
const model = params?.model || params;
|
|
71
|
+
const script = loadScript('set_model.js');
|
|
72
|
+
if (!script) return null;
|
|
73
|
+
return script.replace(/\$\{\s*MODEL\s*\}/g, JSON.stringify(model));
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
module.exports.newSessionWebview = function newSessionWebview() {
|
|
77
|
+
return loadScript('new_session.js');
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
module.exports.exploreWebview = function exploreWebview() {
|
|
81
|
+
return loadScript('explore_dom.js');
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
module.exports.exploreControlsWebview = function exploreControlsWebview() {
|
|
85
|
+
return loadScript('explore_controls_webview.js');
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
module.exports.exploreDropdownWebview = function exploreDropdownWebview() {
|
|
89
|
+
return loadScript('explore_dropdown_webview.js');
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
module.exports.inspectCodeWebview = function inspectCodeWebview() {
|
|
93
|
+
return loadScript('inspect_code_webview.js');
|
|
94
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "2026.03.
|
|
2
|
+
"version": "2026.03.22",
|
|
3
3
|
"providers": {
|
|
4
4
|
"agentpool-acp": {
|
|
5
5
|
"providerVersion": "0.0.0",
|
|
@@ -196,6 +196,11 @@
|
|
|
196
196
|
"category": "extension",
|
|
197
197
|
"name": "Cline"
|
|
198
198
|
},
|
|
199
|
+
"codex": {
|
|
200
|
+
"providerVersion": "0.0.0",
|
|
201
|
+
"category": "extension",
|
|
202
|
+
"name": "Codex"
|
|
203
|
+
},
|
|
199
204
|
"roo-code": {
|
|
200
205
|
"providerVersion": "0.0.0",
|
|
201
206
|
"category": "extension",
|