@adhdev/daemon-core 0.5.36 → 0.5.38
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/dist/index.d.ts +5 -0
- package/dist/index.js +66 -8
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/providers/_builtin/cli/claude-cli/provider.json +1 -1
- package/providers/_builtin/extension/codex/scripts/1.0/explore_dom.js +8 -2
- package/providers/_builtin/extension/codex/scripts/1.0/list_modes.js +138 -0
- package/providers/_builtin/extension/codex/scripts/1.0/read_chat.js +183 -34
- package/providers/_builtin/extension/codex/scripts/1.0/resolve_action.js +55 -15
- package/providers/_builtin/extension/codex/scripts/1.0/send_message.js +5 -25
- package/providers/_builtin/extension/codex/scripts/1.0/set_mode.js +165 -0
- package/src/agent-stream/provider-adapter.ts +1 -0
- package/src/agent-stream/types.ts +1 -0
- package/src/cdp/manager.ts +7 -1
- package/src/config/config.ts +19 -0
- package/src/daemon/dev-server.ts +5 -1
- package/src/providers/extension-provider-instance.ts +9 -1
- package/src/providers/ide-provider-instance.ts +4 -3
- package/src/providers/provider-loader.ts +26 -2
- package/src/status/reporter.ts +9 -1
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex Extension — set_mode
|
|
3
|
+
*
|
|
4
|
+
* Opens the mode dropdown (same discovery as list_modes), selects an item matching ${MODE}.
|
|
5
|
+
*
|
|
6
|
+
* Placeholder: ${MODE}
|
|
7
|
+
*/
|
|
8
|
+
(() => {
|
|
9
|
+
try {
|
|
10
|
+
const targetMode = ${MODE};
|
|
11
|
+
|
|
12
|
+
function resolveDoc() {
|
|
13
|
+
let doc = document;
|
|
14
|
+
let root = doc.getElementById('root');
|
|
15
|
+
if (!root) {
|
|
16
|
+
const iframes = doc.querySelectorAll('iframe');
|
|
17
|
+
for (const iframe of iframes) {
|
|
18
|
+
try {
|
|
19
|
+
const innerDoc = iframe.contentDocument || iframe.contentWindow?.document;
|
|
20
|
+
if (innerDoc?.getElementById('root')) {
|
|
21
|
+
doc = innerDoc;
|
|
22
|
+
root = innerDoc.getElementById('root');
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
} catch (e) { /* cross-origin */ }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return { doc, root };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isModelMenuButton(b) {
|
|
32
|
+
const text = (b.textContent || '').trim();
|
|
33
|
+
if (b.getAttribute('aria-haspopup') !== 'menu') return false;
|
|
34
|
+
return /^(GPT-|gpt-|o\d|claude-|sonnet|opus)/i.test(text);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function findModeMenuButton(doc) {
|
|
38
|
+
const composer =
|
|
39
|
+
doc.querySelector('[class*="thread-composer-max-width"]') ||
|
|
40
|
+
doc.querySelector('[class*="thread-composer"]') ||
|
|
41
|
+
doc.getElementById('root') ||
|
|
42
|
+
doc.body;
|
|
43
|
+
|
|
44
|
+
const buttons = Array.from(composer.querySelectorAll('button')).filter(
|
|
45
|
+
(b) => b.offsetWidth > 0 && b.offsetHeight > 0,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const menuTriggers = buttons.filter(
|
|
49
|
+
(b) => b.getAttribute('aria-haspopup') === 'menu' && !isModelMenuButton(b),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
if (menuTriggers.length === 0) return null;
|
|
53
|
+
|
|
54
|
+
const byAria = menuTriggers.find((b) => {
|
|
55
|
+
const al = (b.getAttribute('aria-label') || '').toLowerCase();
|
|
56
|
+
return /mode|agent|ask|plan|autonomy|codex|모드|에이전트|플랜/i.test(al);
|
|
57
|
+
});
|
|
58
|
+
if (byAria) return byAria;
|
|
59
|
+
|
|
60
|
+
return menuTriggers[0];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function openMenu(btn) {
|
|
64
|
+
const rect = btn.getBoundingClientRect();
|
|
65
|
+
const cx = rect.left + rect.width / 2;
|
|
66
|
+
const cy = rect.top + rect.height / 2;
|
|
67
|
+
btn.dispatchEvent(
|
|
68
|
+
new PointerEvent('pointerdown', { bubbles: true, clientX: cx, clientY: cy, pointerId: 1 }),
|
|
69
|
+
);
|
|
70
|
+
btn.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, clientX: cx, clientY: cy }));
|
|
71
|
+
btn.dispatchEvent(
|
|
72
|
+
new PointerEvent('pointerup', { bubbles: true, clientX: cx, clientY: cy, pointerId: 1 }),
|
|
73
|
+
);
|
|
74
|
+
btn.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, clientX: cx, clientY: cy }));
|
|
75
|
+
btn.dispatchEvent(new MouseEvent('click', { bubbles: true, clientX: cx, clientY: cy }));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function clickItem(el) {
|
|
79
|
+
const ir = el.getBoundingClientRect();
|
|
80
|
+
const ix = ir.left + ir.width / 2;
|
|
81
|
+
const iy = ir.top + ir.height / 2;
|
|
82
|
+
el.dispatchEvent(
|
|
83
|
+
new PointerEvent('pointerdown', { bubbles: true, clientX: ix, clientY: iy }),
|
|
84
|
+
);
|
|
85
|
+
el.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, clientX: ix, clientY: iy }));
|
|
86
|
+
el.dispatchEvent(new PointerEvent('pointerup', { bubbles: true, clientX: ix, clientY: iy }));
|
|
87
|
+
el.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, clientX: ix, clientY: iy }));
|
|
88
|
+
el.dispatchEvent(new MouseEvent('click', { bubbles: true, clientX: ix, clientY: iy }));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const { doc, root } = resolveDoc();
|
|
92
|
+
if (!root) return JSON.stringify({ success: false, error: 'no root' });
|
|
93
|
+
|
|
94
|
+
const want =
|
|
95
|
+
typeof targetMode === 'string'
|
|
96
|
+
? targetMode.trim()
|
|
97
|
+
: targetMode != null
|
|
98
|
+
? String(targetMode).trim()
|
|
99
|
+
: '';
|
|
100
|
+
if (!want) return JSON.stringify({ success: false, error: 'empty mode' });
|
|
101
|
+
|
|
102
|
+
const modeBtn = findModeMenuButton(doc);
|
|
103
|
+
if (!modeBtn) return JSON.stringify({ success: false, error: 'mode menu button not found' });
|
|
104
|
+
|
|
105
|
+
const currentLabel = (modeBtn.textContent || '').trim();
|
|
106
|
+
if (currentLabel.toLowerCase() === want.toLowerCase()) {
|
|
107
|
+
return JSON.stringify({ success: true, mode: currentLabel, changed: false });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
openMenu(modeBtn);
|
|
111
|
+
|
|
112
|
+
return new Promise((resolve) => {
|
|
113
|
+
setTimeout(() => {
|
|
114
|
+
let menu = doc.querySelector('[role="menu"][data-state="open"]');
|
|
115
|
+
if (!menu) menu = doc.querySelector('[role="menu"]');
|
|
116
|
+
|
|
117
|
+
if (!menu) {
|
|
118
|
+
doc.dispatchEvent(
|
|
119
|
+
new KeyboardEvent('keydown', { key: 'Escape', code: 'Escape', keyCode: 27, bubbles: true }),
|
|
120
|
+
);
|
|
121
|
+
return resolve(JSON.stringify({ success: false, error: 'mode menu did not open' }));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const items = Array.from(
|
|
125
|
+
menu.querySelectorAll(
|
|
126
|
+
'[role="menuitem"], [role="menuitemradio"], [role="option"], div[class*="cursor-interaction"]',
|
|
127
|
+
),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const norm = (s) => (s || '').trim().toLowerCase();
|
|
131
|
+
const wantN = norm(want);
|
|
132
|
+
|
|
133
|
+
let match = items.find((el) => norm(el.textContent) === wantN);
|
|
134
|
+
if (!match) {
|
|
135
|
+
match = items.find((el) => norm(el.textContent).includes(wantN) || wantN.includes(norm(el.textContent)));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!match) {
|
|
139
|
+
doc.dispatchEvent(
|
|
140
|
+
new KeyboardEvent('keydown', { key: 'Escape', code: 'Escape', keyCode: 27, bubbles: true }),
|
|
141
|
+
);
|
|
142
|
+
const available = items
|
|
143
|
+
.map((el) => (el.textContent || '').trim())
|
|
144
|
+
.filter((t) => t.length > 0 && t.length < 80);
|
|
145
|
+
return resolve(
|
|
146
|
+
JSON.stringify({
|
|
147
|
+
success: false,
|
|
148
|
+
error: `mode "${want}" not found`,
|
|
149
|
+
available,
|
|
150
|
+
}),
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const picked = (match.textContent || '').trim();
|
|
155
|
+
clickItem(match);
|
|
156
|
+
|
|
157
|
+
setTimeout(() => {
|
|
158
|
+
resolve(JSON.stringify({ success: true, mode: picked, changed: true }));
|
|
159
|
+
}, 350);
|
|
160
|
+
}, 550);
|
|
161
|
+
});
|
|
162
|
+
} catch (e) {
|
|
163
|
+
return JSON.stringify({ success: false, error: e.message || String(e) });
|
|
164
|
+
}
|
|
165
|
+
})();
|
package/src/cdp/manager.ts
CHANGED
|
@@ -689,8 +689,14 @@ export class DaemonCdpManager {
|
|
|
689
689
|
|
|
690
690
|
const value = result?.result?.value;
|
|
691
691
|
if (value != null) {
|
|
692
|
+
const strValue = typeof value === 'string' ? value : JSON.stringify(value);
|
|
693
|
+
// Let provider script explicitly tell us to skip this iframe and try the next one
|
|
694
|
+
if (strValue.includes('__adhdev_skip_iframe')) {
|
|
695
|
+
this.log(`[CDP] evaluateInWebviewFrame: script requested skip in ${iframe.targetId.substring(0, 12)}`);
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
692
698
|
this.log(`[CDP] evaluateInWebviewFrame: success in ${iframe.targetId.substring(0, 12)}`);
|
|
693
|
-
return
|
|
699
|
+
return strValue;
|
|
694
700
|
}
|
|
695
701
|
} catch (e: any) {
|
|
696
702
|
if (sessionId) {
|
package/src/config/config.ts
CHANGED
|
@@ -58,6 +58,9 @@ export interface ADHDevConfig {
|
|
|
58
58
|
// Machine nickname (user-customizable label for this machine)
|
|
59
59
|
machineNickname: string | null;
|
|
60
60
|
|
|
61
|
+
// Stable machine ID (prevents duplicate daemon entries when OS hostname changes dynamically)
|
|
62
|
+
machineId?: string;
|
|
63
|
+
|
|
61
64
|
// CLI launch history
|
|
62
65
|
cliHistory: CliHistoryEntry[];
|
|
63
66
|
|
|
@@ -98,6 +101,7 @@ const DEFAULT_CONFIG: ADHDevConfig = {
|
|
|
98
101
|
defaultWorkspaceId: null,
|
|
99
102
|
recentWorkspaceActivity: [],
|
|
100
103
|
machineNickname: null,
|
|
104
|
+
machineId: undefined,
|
|
101
105
|
cliHistory: [],
|
|
102
106
|
providerSettings: {},
|
|
103
107
|
ideSettings: {},
|
|
@@ -141,7 +145,22 @@ export function loadConfig(): ADHDevConfig {
|
|
|
141
145
|
delete (merged as any).activeWorkspaceId;
|
|
142
146
|
const hadStoredWorkspaces = Array.isArray(parsed.workspaces) && parsed.workspaces.length > 0;
|
|
143
147
|
migrateWorkspacesFromRecent(merged);
|
|
148
|
+
|
|
149
|
+
let configChanged = false;
|
|
150
|
+
if (!merged.machineId) {
|
|
151
|
+
const os = require('os');
|
|
152
|
+
const crypto = require('crypto');
|
|
153
|
+
const safeHostname = os.hostname().replace(/[^a-zA-Z0-9]/g, '_');
|
|
154
|
+
const machineHash = crypto.createHash('md5').update(os.hostname() + os.homedir()).digest('hex').slice(0, 8);
|
|
155
|
+
merged.machineId = `${safeHostname}_${machineHash}`;
|
|
156
|
+
configChanged = true;
|
|
157
|
+
}
|
|
158
|
+
|
|
144
159
|
if (!hadStoredWorkspaces && (merged.workspaces?.length || 0) > 0) {
|
|
160
|
+
configChanged = true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (configChanged) {
|
|
145
164
|
try {
|
|
146
165
|
saveConfig(merged);
|
|
147
166
|
} catch { /* ignore */ }
|
package/src/daemon/dev-server.ts
CHANGED
|
@@ -337,13 +337,17 @@ export class DevServer {
|
|
|
337
337
|
this.json(res, 500, { error: 'Script function returned null' });
|
|
338
338
|
return;
|
|
339
339
|
}
|
|
340
|
+
this.log(`Exec script length: ${scriptCode.length}, first 50 chars: ${scriptCode.slice(0, 50)}...`);
|
|
340
341
|
|
|
341
342
|
// Execute webview script via evaluateInWebviewFrame
|
|
342
|
-
const isWebviewScript = scriptName.toLowerCase().includes('webview');
|
|
343
|
+
const isWebviewScript = provider.category === 'extension' || scriptName.toLowerCase().includes('webview');
|
|
343
344
|
let raw: any;
|
|
344
345
|
if (isWebviewScript) {
|
|
345
346
|
const matchText = provider.webviewMatchText;
|
|
346
347
|
const matchFn = matchText ? (body: string) => body.includes(matchText) : undefined;
|
|
348
|
+
if (!cdp.evaluateInWebviewFrame) {
|
|
349
|
+
throw new Error(`CDP manager does not support evaluateInWebviewFrame`);
|
|
350
|
+
}
|
|
347
351
|
raw = await cdp.evaluateInWebviewFrame(scriptCode, matchFn);
|
|
348
352
|
} else {
|
|
349
353
|
raw = await cdp.evaluate(scriptCode, 30000);
|
|
@@ -23,6 +23,8 @@ export class ExtensionProviderInstance implements ProviderInstance {
|
|
|
23
23
|
private agentStreams: any[] = [];
|
|
24
24
|
private messages: any[] = [];
|
|
25
25
|
private activeModal: any = null;
|
|
26
|
+
private currentModel: string = '';
|
|
27
|
+
private currentMode: string = '';
|
|
26
28
|
private lastAgentStatus: string = 'idle';
|
|
27
29
|
private generatingStartedAt: number = 0;
|
|
28
30
|
private monitor: StatusMonitor;
|
|
@@ -74,6 +76,8 @@ export class ExtensionProviderInstance implements ProviderInstance {
|
|
|
74
76
|
activeModal: this.activeModal,
|
|
75
77
|
inputContent: '',
|
|
76
78
|
} : null,
|
|
79
|
+
currentModel: this.currentModel || undefined,
|
|
80
|
+
currentPlan: this.currentMode || undefined,
|
|
77
81
|
agentStreams: this.agentStreams,
|
|
78
82
|
instanceId: this.instanceId,
|
|
79
83
|
lastUpdated: Date.now(),
|
|
@@ -87,6 +91,9 @@ export class ExtensionProviderInstance implements ProviderInstance {
|
|
|
87
91
|
// Reflect data collected from agent-stream-manager
|
|
88
92
|
if (data?.streams) this.agentStreams = data.streams;
|
|
89
93
|
if (data?.messages) this.messages = data.messages;
|
|
94
|
+
if (data?.activeModal !== undefined) this.activeModal = data.activeModal;
|
|
95
|
+
if (data?.model) this.currentModel = data.model;
|
|
96
|
+
if (data?.mode) this.currentMode = data.mode;
|
|
90
97
|
if (data?.status) {
|
|
91
98
|
const newStatus = data.status;
|
|
92
99
|
this.detectTransition(newStatus, data);
|
|
@@ -125,11 +132,12 @@ export class ExtensionProviderInstance implements ProviderInstance {
|
|
|
125
132
|
this.pushEvent({ event: 'agent:generating_started', chatTitle, timestamp: now });
|
|
126
133
|
} else if (agentStatus === 'waiting_approval') {
|
|
127
134
|
if (!this.generatingStartedAt) this.generatingStartedAt = now;
|
|
135
|
+
const msg = data?.activeModal?.message || data?.modalMessage;
|
|
128
136
|
this.pushEvent({
|
|
129
137
|
event: 'agent:waiting_approval', chatTitle, timestamp: now,
|
|
130
138
|
ideType: this.ideType,
|
|
131
139
|
agentType: this.type,
|
|
132
|
-
modalMessage:
|
|
140
|
+
modalMessage: msg,
|
|
133
141
|
modalButtons: data?.activeModal?.buttons || data?.modalButtons,
|
|
134
142
|
});
|
|
135
143
|
} else if (agentStatus === 'idle' && (this.lastAgentStatus === 'generating' || this.lastAgentStatus === 'waiting_approval')) {
|
|
@@ -253,8 +253,8 @@ export class IdeProviderInstance implements ProviderInstance {
|
|
|
253
253
|
activeModal = undefined;
|
|
254
254
|
} else {
|
|
255
255
|
activeModal = {
|
|
256
|
-
message: activeModal.message?.slice(0,
|
|
257
|
-
buttons: (activeModal.buttons ?? []).filter((t: string) => t.length <
|
|
256
|
+
message: activeModal.message?.slice(0, 5000) ?? '',
|
|
257
|
+
buttons: (activeModal.buttons ?? []).filter((t: string) => t.length < 200),
|
|
258
258
|
};
|
|
259
259
|
}
|
|
260
260
|
}
|
|
@@ -345,9 +345,10 @@ export class IdeProviderInstance implements ProviderInstance {
|
|
|
345
345
|
this.pushEvent({ event: 'agent:generating_started', chatTitle, timestamp: now, ideType: this.type });
|
|
346
346
|
} else if (agentStatus === 'waiting_approval') {
|
|
347
347
|
if (!this.generatingStartedAt.has(agentKey)) this.generatingStartedAt.set(agentKey, now);
|
|
348
|
+
const msg = chatData.activeModal?.message;
|
|
348
349
|
this.pushEvent({
|
|
349
350
|
event: 'agent:waiting_approval', chatTitle, timestamp: now, ideType: this.type,
|
|
350
|
-
modalMessage:
|
|
351
|
+
modalMessage: msg,
|
|
351
352
|
modalButtons: chatData.activeModal?.buttons,
|
|
352
353
|
});
|
|
353
354
|
} else if (agentStatus === 'idle' && (lastStatus === 'generating' || lastStatus === 'waiting_approval')) {
|
|
@@ -904,8 +904,32 @@ export class ProviderLoader {
|
|
|
904
904
|
if (!file.endsWith('.js')) continue;
|
|
905
905
|
const scriptName = toCamel(file.replace('.js', ''));
|
|
906
906
|
const filePath = path.join(dir, file);
|
|
907
|
-
(result as any)[scriptName] = (...
|
|
908
|
-
try {
|
|
907
|
+
(result as any)[scriptName] = (...args: any[]): string => {
|
|
908
|
+
try {
|
|
909
|
+
let content = fs.readFileSync(filePath, 'utf-8');
|
|
910
|
+
if (args[0] && typeof args[0] === 'object') {
|
|
911
|
+
for (const [key, val] of Object.entries(args[0])) {
|
|
912
|
+
let v = val;
|
|
913
|
+
if (typeof v === 'string') {
|
|
914
|
+
// If it doesn't start with a quote, user probably passed raw text
|
|
915
|
+
if (!v.startsWith('"') && !v.startsWith("'") && !v.startsWith('`')) {
|
|
916
|
+
v = JSON.stringify(v);
|
|
917
|
+
}
|
|
918
|
+
} else {
|
|
919
|
+
v = JSON.stringify(v);
|
|
920
|
+
}
|
|
921
|
+
content = content.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), String(v));
|
|
922
|
+
}
|
|
923
|
+
} else if (args[0] !== undefined) {
|
|
924
|
+
// legacy fallback for single argument usually MESSAGE
|
|
925
|
+
let v = String(args[0]);
|
|
926
|
+
if (!v.startsWith('"') && !v.startsWith("'") && !v.startsWith('`')) {
|
|
927
|
+
v = JSON.stringify(v);
|
|
928
|
+
}
|
|
929
|
+
content = content.replace(/\$\{MESSAGE\}/g, v);
|
|
930
|
+
}
|
|
931
|
+
return content;
|
|
932
|
+
} catch { return ''; }
|
|
909
933
|
};
|
|
910
934
|
}
|
|
911
935
|
} catch { /* ignore */ }
|
package/src/status/reporter.ts
CHANGED
|
@@ -25,7 +25,7 @@ import type {
|
|
|
25
25
|
export interface StatusReporterDeps {
|
|
26
26
|
serverConn: { isConnected(): boolean; sendMessage(type: string, data: any): void; getUserPlan(): string } | null;
|
|
27
27
|
cdpManagers: Map<string, { isConnected: boolean }>;
|
|
28
|
-
p2p: { isConnected: boolean; isAvailable: boolean; connectionState: string; connectedPeerCount: number; screenshotActive: boolean; sendStatus(data: any): void } | null;
|
|
28
|
+
p2p: { isConnected: boolean; isAvailable: boolean; connectionState: string; connectedPeerCount: number; screenshotActive: boolean; sendStatus(data: any): void; sendStatusEvent?(event: Record<string, unknown>): boolean } | null;
|
|
29
29
|
providerLoader: { resolve(type: string): any; getAll(): any[] };
|
|
30
30
|
adapters: Map<string, { cliType: string; cliName: string; workingDir: string; getStatus(): any; getPartialResponse(): string }>;
|
|
31
31
|
detectedIdes: any[];
|
|
@@ -98,7 +98,15 @@ export class DaemonStatusReporter {
|
|
|
98
98
|
|
|
99
99
|
emitStatusEvent(event: Record<string, unknown>): void {
|
|
100
100
|
LOG.info('StatusEvent', `${event.event} (${event.providerType || event.ideType || ''})`);
|
|
101
|
+
// Send via WS (server relay → dashboard + push notifications)
|
|
101
102
|
this.deps.serverConn?.sendMessage('status_event', event);
|
|
103
|
+
// Also send via P2P (direct → dashboard, works even when WS is flaky)
|
|
104
|
+
// Frontend dedup prevents duplicate toasts
|
|
105
|
+
if (this.deps.p2p?.isConnected) {
|
|
106
|
+
try {
|
|
107
|
+
this.deps.p2p.sendStatusEvent?.(event);
|
|
108
|
+
} catch { /* P2P send failure is non-critical */ }
|
|
109
|
+
}
|
|
102
110
|
}
|
|
103
111
|
|
|
104
112
|
removeAgentTracking(_key: string): void { /* Managed by Instance itself */ }
|