Skip to content

Commit e826493

Browse files
Zezi-Rayjackwener
andauthored
feat(instagram): add post, reel, story, and note publishing (#671)
* Add draft Instagram posting flow * Refine Instagram post flow * Add dynamic Instagram posting routes * Retry transient Instagram private setup failures * Add Instagram reel posting command * Add Instagram mixed-media carousel posting * Unify Instagram post media input * Add Instagram story posting command * Add Instagram note publishing command * fix(instagram): use JSON.stringify for constants in note evaluate string Replace template literal interpolation of Node-side constants with JSON.stringify() for consistency with codebase evaluate patterns. Use bracket notation for dynamic property access instead of template interpolation into a property chain. --------- Co-authored-by: jackwener <jakevingoo@gmail.com>
1 parent e0a66af commit e826493

26 files changed

+9298
-677
lines changed

extension/dist/background.js

Lines changed: 1052 additions & 664 deletions
Large diffs are not rendered by default.

extension/src/background.ts

Lines changed: 144 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ type AutomationSession = {
117117
windowId: number;
118118
idleTimer: ReturnType<typeof setTimeout> | null;
119119
idleDeadlineAt: number;
120+
owned: boolean;
121+
preferredTabId: number | null;
120122
};
121123

122124
const automationSessions = new Map<string, AutomationSession>();
@@ -134,6 +136,11 @@ function resetWindowIdleTimer(workspace: string): void {
134136
session.idleTimer = setTimeout(async () => {
135137
const current = automationSessions.get(workspace);
136138
if (!current) return;
139+
if (!current.owned) {
140+
console.log(`[opencli] Borrowed workspace ${workspace} detached from window ${current.windowId} (idle timeout)`);
141+
automationSessions.delete(workspace);
142+
return;
143+
}
137144
try {
138145
await chrome.windows.remove(current.windowId);
139146
console.log(`[opencli] Automation window ${current.windowId} (${workspace}) closed (idle timeout)`);
@@ -177,6 +184,8 @@ async function getAutomationWindow(workspace: string, initialUrl?: string): Prom
177184
windowId: win.id!,
178185
idleTimer: null,
179186
idleDeadlineAt: Date.now() + WINDOW_IDLE_TIMEOUT,
187+
owned: true,
188+
preferredTabId: null,
180189
};
181190
automationSessions.set(workspace, session);
182191
console.log(`[opencli] Created automation window ${session.windowId} (${workspace}, start=${startUrl})`);
@@ -279,6 +288,14 @@ async function handleCommand(cmd: Command): Promise<Result> {
279288
return await handleSessions(cmd);
280289
case 'set-file-input':
281290
return await handleSetFileInput(cmd, workspace);
291+
case 'insert-text':
292+
return await handleInsertText(cmd, workspace);
293+
case 'bind-current':
294+
return await handleBindCurrent(cmd, workspace);
295+
case 'network-capture-start':
296+
return await handleNetworkCaptureStart(cmd, workspace);
297+
case 'network-capture-read':
298+
return await handleNetworkCaptureRead(cmd, workspace);
282299
default:
283300
return { id: cmd.id, ok: false, error: `Unknown action: ${cmd.action}` };
284301
}
@@ -326,7 +343,31 @@ function isTargetUrl(currentUrl: string | undefined, targetUrl: string): boolean
326343
return normalizeUrlForComparison(currentUrl) === normalizeUrlForComparison(targetUrl);
327344
}
328345

329-
function setWorkspaceSession(workspace: string, session: Pick<AutomationSession, 'windowId'>): void {
346+
function matchesDomain(url: string | undefined, domain: string): boolean {
347+
if (!url) return false;
348+
try {
349+
const parsed = new URL(url);
350+
return parsed.hostname === domain || parsed.hostname.endsWith(`.${domain}`);
351+
} catch {
352+
return false;
353+
}
354+
}
355+
356+
function matchesBindCriteria(tab: chrome.tabs.Tab, cmd: Command): boolean {
357+
if (!tab.id || !isDebuggableUrl(tab.url)) return false;
358+
if (cmd.matchDomain && !matchesDomain(tab.url, cmd.matchDomain)) return false;
359+
if (cmd.matchPathPrefix) {
360+
try {
361+
const parsed = new URL(tab.url!);
362+
if (!parsed.pathname.startsWith(cmd.matchPathPrefix)) return false;
363+
} catch {
364+
return false;
365+
}
366+
}
367+
return true;
368+
}
369+
370+
function setWorkspaceSession(workspace: string, session: Omit<AutomationSession, 'idleTimer' | 'idleDeadlineAt'>): void {
330371
const existing = automationSessions.get(workspace);
331372
if (existing?.idleTimer) clearTimeout(existing.idleTimer);
332373
automationSessions.set(workspace, {
@@ -348,9 +389,11 @@ async function resolveTab(tabId: number | undefined, workspace: string, initialU
348389
try {
349390
const tab = await chrome.tabs.get(tabId);
350391
const session = automationSessions.get(workspace);
351-
const matchesSession = session ? tab.windowId === session.windowId : false;
392+
const matchesSession = session
393+
? (session.preferredTabId !== null ? session.preferredTabId === tabId : tab.windowId === session.windowId)
394+
: false;
352395
if (isDebuggableUrl(tab.url) && matchesSession) return { tabId, tab };
353-
if (session && !matchesSession && isDebuggableUrl(tab.url)) {
396+
if (session && !matchesSession && session.preferredTabId === null && isDebuggableUrl(tab.url)) {
354397
// Tab drifted to another window but content is still valid.
355398
// Try to move it back instead of abandoning it.
356399
console.warn(`[opencli] Tab ${tabId} drifted to window ${tab.windowId}, moving back to ${session.windowId}`);
@@ -371,6 +414,16 @@ async function resolveTab(tabId: number | undefined, workspace: string, initialU
371414
}
372415
}
373416

417+
const existingSession = automationSessions.get(workspace);
418+
if (existingSession?.preferredTabId !== null) {
419+
try {
420+
const preferredTab = await chrome.tabs.get(existingSession.preferredTabId);
421+
if (isDebuggableUrl(preferredTab.url)) return { tabId: preferredTab.id!, tab: preferredTab };
422+
} catch {
423+
automationSessions.delete(workspace);
424+
}
425+
}
426+
374427
// Get (or create) the automation window
375428
const windowId = await getAutomationWindow(workspace, initialUrl);
376429

@@ -408,6 +461,14 @@ async function resolveTabId(tabId: number | undefined, workspace: string, initia
408461
async function listAutomationTabs(workspace: string): Promise<chrome.tabs.Tab[]> {
409462
const session = automationSessions.get(workspace);
410463
if (!session) return [];
464+
if (session.preferredTabId !== null) {
465+
try {
466+
return [await chrome.tabs.get(session.preferredTabId)];
467+
} catch {
468+
automationSessions.delete(workspace);
469+
return [];
470+
}
471+
}
411472
try {
412473
return await chrome.tabs.query({ windowId: session.windowId });
413474
} catch {
@@ -681,10 +742,12 @@ async function handleCdp(cmd: Command, workspace: string): Promise<Result> {
681742
async function handleCloseWindow(cmd: Command, workspace: string): Promise<Result> {
682743
const session = automationSessions.get(workspace);
683744
if (session) {
684-
try {
685-
await chrome.windows.remove(session.windowId);
686-
} catch {
687-
// Window may already be closed
745+
if (session.owned) {
746+
try {
747+
await chrome.windows.remove(session.windowId);
748+
} catch {
749+
// Window may already be closed
750+
}
688751
}
689752
if (session.idleTimer) clearTimeout(session.idleTimer);
690753
automationSessions.delete(workspace);
@@ -705,6 +768,39 @@ async function handleSetFileInput(cmd: Command, workspace: string): Promise<Resu
705768
}
706769
}
707770

771+
async function handleInsertText(cmd: Command, workspace: string): Promise<Result> {
772+
if (typeof cmd.text !== 'string') {
773+
return { id: cmd.id, ok: false, error: 'Missing text payload' };
774+
}
775+
const tabId = await resolveTabId(cmd.tabId, workspace);
776+
try {
777+
await executor.insertText(tabId, cmd.text);
778+
return { id: cmd.id, ok: true, data: { inserted: true } };
779+
} catch (err) {
780+
return { id: cmd.id, ok: false, error: err instanceof Error ? err.message : String(err) };
781+
}
782+
}
783+
784+
async function handleNetworkCaptureStart(cmd: Command, workspace: string): Promise<Result> {
785+
const tabId = await resolveTabId(cmd.tabId, workspace);
786+
try {
787+
await executor.startNetworkCapture(tabId, cmd.pattern);
788+
return { id: cmd.id, ok: true, data: { started: true } };
789+
} catch (err) {
790+
return { id: cmd.id, ok: false, error: err instanceof Error ? err.message : String(err) };
791+
}
792+
}
793+
794+
async function handleNetworkCaptureRead(cmd: Command, workspace: string): Promise<Result> {
795+
const tabId = await resolveTabId(cmd.tabId, workspace);
796+
try {
797+
const data = await executor.readNetworkCapture(tabId);
798+
return { id: cmd.id, ok: true, data };
799+
} catch (err) {
800+
return { id: cmd.id, ok: false, error: err instanceof Error ? err.message : String(err) };
801+
}
802+
}
803+
708804
async function handleSessions(cmd: Command): Promise<Result> {
709805
const now = Date.now();
710806
const data = await Promise.all([...automationSessions.entries()].map(async ([workspace, session]) => ({
@@ -716,11 +812,49 @@ async function handleSessions(cmd: Command): Promise<Result> {
716812
return { id: cmd.id, ok: true, data };
717813
}
718814

815+
async function handleBindCurrent(cmd: Command, workspace: string): Promise<Result> {
816+
const activeTabs = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
817+
const fallbackTabs = await chrome.tabs.query({ lastFocusedWindow: true });
818+
const allTabs = await chrome.tabs.query({});
819+
const boundTab = activeTabs.find((tab) => matchesBindCriteria(tab, cmd))
820+
?? fallbackTabs.find((tab) => matchesBindCriteria(tab, cmd))
821+
?? allTabs.find((tab) => matchesBindCriteria(tab, cmd));
822+
if (!boundTab?.id) {
823+
return {
824+
id: cmd.id,
825+
ok: false,
826+
error: cmd.matchDomain || cmd.matchPathPrefix
827+
? `No visible tab matching ${cmd.matchDomain ?? 'domain'}${cmd.matchPathPrefix ? ` ${cmd.matchPathPrefix}` : ''}`
828+
: 'No active debuggable tab found',
829+
};
830+
}
831+
832+
setWorkspaceSession(workspace, {
833+
windowId: boundTab.windowId,
834+
owned: false,
835+
preferredTabId: boundTab.id,
836+
});
837+
resetWindowIdleTimer(workspace);
838+
console.log(`[opencli] Workspace ${workspace} explicitly bound to tab ${boundTab.id} (${boundTab.url})`);
839+
return {
840+
id: cmd.id,
841+
ok: true,
842+
data: {
843+
tabId: boundTab.id,
844+
windowId: boundTab.windowId,
845+
url: boundTab.url,
846+
title: boundTab.title,
847+
workspace,
848+
},
849+
};
850+
}
851+
719852
export const __test__ = {
720853
handleNavigate,
721854
isTargetUrl,
722855
handleTabs,
723856
handleSessions,
857+
handleBindCurrent,
724858
resolveTabId,
725859
resetWindowIdleTimer,
726860
getSession: (workspace: string = 'default') => automationSessions.get(workspace) ?? null,
@@ -734,9 +868,11 @@ export const __test__ = {
734868
}
735869
setWorkspaceSession(workspace, {
736870
windowId,
871+
owned: true,
872+
preferredTabId: null,
737873
});
738874
},
739-
setSession: (workspace: string, session: { windowId: number }) => {
875+
setSession: (workspace: string, session: { windowId: number; owned: boolean; preferredTabId: number | null }) => {
740876
setWorkspaceSession(workspace, session);
741877
},
742878
};

0 commit comments

Comments
 (0)