@hamp10/agentforge 0.2.26 โ†’ 0.2.28

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hamp10/agentforge",
3
- "version": "0.2.26",
3
+ "version": "0.2.28",
4
4
  "description": "AgentForge worker โ€” connect your machine to agentforge.ai",
5
5
  "type": "module",
6
6
  "bin": {
@@ -35,6 +35,12 @@ const cases = [
35
35
  absent: ['example-com', 'delete', 'rebuild', 'readability', 'design', 'urls', 'site'],
36
36
  pageOnly: true,
37
37
  },
38
+ {
39
+ text: 'Delete and rebuild the listing pages for Alpha and Beta from a clean start. Only change those listing pages.',
40
+ slugs: ['alpha', 'beta'],
41
+ absent: ['beta-from', 'clean-start', 'from'],
42
+ pageOnly: true,
43
+ },
38
44
  {
39
45
  text: 'Work on Alpha pages for readability',
40
46
  slugs: ['alpha'],
@@ -214,6 +220,25 @@ try {
214
220
  const worker = Object.create(AgentForgeWorker.prototype);
215
221
  worker.cli = { isAnthropicApiKey: (key) => /^sk-ant-/i.test(String(key || '')) };
216
222
  const cli = Object.create(OpenClawCLI.prototype);
223
+ const deletedScopedPagePath = path.join(fixture.repo, 'public_html', 'domains', 'alpha.html');
224
+ rmSync(deletedScopedPagePath, { force: true });
225
+ assert.equal(
226
+ worker._buildDeletedScopedPageNudge(
227
+ [{ root: fixture.repo, head: fixture.head }],
228
+ 'Delete the Example.com listing page for Alpha. Only change that listing page.'
229
+ ),
230
+ '',
231
+ 'explicit page deletion tasks should be allowed to leave the scoped target page deleted'
232
+ );
233
+ assert.match(
234
+ worker._buildDeletedScopedPageNudge(
235
+ [{ root: fixture.repo, head: fixture.head }],
236
+ 'Delete and rebuild the Alpha listing page from a clean start, preserving the same URL. Only change that listing page.'
237
+ ),
238
+ /deleted target page source\(s\) still missing/i,
239
+ 'clean-start rebuild tasks should still require the scoped target page to be recreated'
240
+ );
241
+ git(fixture.repo, ['restore', '--', 'public_html/domains/alpha.html']);
217
242
  const projectsRoot = mkdtempSync(path.join(tmpdir(), 'agentforge-project-list-'));
218
243
  let agentWorkspace = null;
219
244
  try {
@@ -944,6 +969,16 @@ assert.match(
944
969
  /__agentforge_verify/i,
945
970
  'direct browser verification should cache-bust local verification URLs'
946
971
  );
972
+ assert.match(
973
+ openClawSource,
974
+ /normalizeForNavigationCompare\(target\?\.url \|\| ''\) === normalizeForNavigationCompare\(effectiveRequestedUrl\)/,
975
+ 'direct browser verification should reuse an existing cache-busted tab for the same requested local URL'
976
+ );
977
+ assert.match(
978
+ openClawSource,
979
+ /isLocalVerificationUrl[\s\S]*__agentforge_verify[\s\S]*closeDuplicateVerificationTargets[\s\S]*\/json\/close\//i,
980
+ 'direct browser verification should close duplicate cache-busted local verification tabs'
981
+ );
947
982
  assert.match(
948
983
  openClawSource,
949
984
  /forgetLocalScopedUrl[\s\S]*rendered page content did not identify target|rendered page content did not identify target[\s\S]*forgetLocalScopedUrl/i,
@@ -959,6 +994,16 @@ assert.match(
959
994
  /__agentforge_verify/i,
960
995
  'AgentForge browser tool should cache-bust localhost navigations'
961
996
  );
997
+ assert.match(
998
+ browserSource,
999
+ /sameBrowserTargetUrl[\s\S]*stripLocalCacheBust[\s\S]*getPage\(agentId, url\)/i,
1000
+ 'AgentForge browser tool should reuse cache-busted tabs for the same requested local URL'
1001
+ );
1002
+ assert.match(
1003
+ browserSource,
1004
+ /isLocalVerificationUrl[\s\S]*__agentforge_verify[\s\S]*closeStaleVerificationTabs[\s\S]*page\.close/i,
1005
+ 'AgentForge browser tool should close stale cache-busted localhost verification tabs'
1006
+ );
962
1007
  assert.match(
963
1008
  previewServerSource,
964
1009
  /extname\(urlPath\)[\s\S]*writeHead\(404[\s\S]*no-store/i,
@@ -2761,6 +2761,8 @@ export class OpenClawCLI extends EventEmitter {
2761
2761
  const normalizeForNavigationCompare = (value) => stripAgentForgeCacheBust(normalize(value));
2762
2762
  const isLocalHttpUrl = (value) =>
2763
2763
  /^https?:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])(?::\d+)?(?:\/|$)/i.test(String(value || ''));
2764
+ const isLocalVerificationUrl = (value) =>
2765
+ isLocalHttpUrl(value) && /[?&]__agentforge_verify=/.test(String(value || ''));
2764
2766
  const withVerificationCacheBust = (value) => {
2765
2767
  if (!value || !isLocalHttpUrl(value)) return value;
2766
2768
  try {
@@ -2790,9 +2792,35 @@ export class OpenClawCLI extends EventEmitter {
2790
2792
  const usablePages = !taskIsAboutAgentForge
2791
2793
  ? pages.filter(p => !isAgentForgeControlSurface(p.url))
2792
2794
  : pages;
2795
+ const matchesEffectiveRequestedUrl = (target) =>
2796
+ !!effectiveRequestedUrl && normalizeForNavigationCompare(target?.url || '') === normalizeForNavigationCompare(effectiveRequestedUrl);
2797
+ const requestedPageMatches = effectiveRequestedUrl
2798
+ ? pages.filter(matchesEffectiveRequestedUrl)
2799
+ : [];
2793
2800
  let page = effectiveRequestedUrl
2794
- ? pages.find(p => normalize(p.url) === normalize(effectiveRequestedUrl))
2801
+ ? requestedPageMatches.find(p => !isLocalVerificationUrl(p.url)) || requestedPageMatches[0] || null
2795
2802
  : usablePages.find(p => p.url && p.url !== 'about:blank') || null;
2803
+ const closeDuplicateVerificationTargets = async (keepTarget) => {
2804
+ if (!effectiveRequestedUrl || !keepTarget) return;
2805
+ const staleTargets = requestedPageMatches.filter(target =>
2806
+ target.id !== keepTarget.id &&
2807
+ isLocalVerificationUrl(target.url) &&
2808
+ normalizeForNavigationCompare(target.url) === normalizeForNavigationCompare(effectiveRequestedUrl)
2809
+ );
2810
+ if (staleTargets.length === 0) return;
2811
+ let closed = 0;
2812
+ await Promise.all(staleTargets.map(async (target) => {
2813
+ try {
2814
+ const closeResp = await fetch(`${cdpBase}/json/close/${encodeURIComponent(target.id)}`, {
2815
+ signal: AbortSignal.timeout(3_000),
2816
+ });
2817
+ if (closeResp.ok) closed += 1;
2818
+ } catch {}
2819
+ }));
2820
+ if (closed > 0) {
2821
+ console.log(` [${agentId}] ๐Ÿงน Closed ${closed} stale verification tab(s) for ${stripAgentForgeCacheBust(effectiveRequestedUrl)}`);
2822
+ }
2823
+ };
2796
2824
 
2797
2825
  const controlSurfaceUrl = effectiveRequestedUrl || page?.url || '';
2798
2826
  if (!taskIsAboutAgentForge && isAgentForgeControlSurface(controlSurfaceUrl)) {
@@ -2811,6 +2839,7 @@ export class OpenClawCLI extends EventEmitter {
2811
2839
  if (!newResp.ok) throw new Error(`CDP new page failed: HTTP ${newResp.status}`);
2812
2840
  page = await newResp.json();
2813
2841
  }
2842
+ await closeDuplicateVerificationTargets(page);
2814
2843
 
2815
2844
  const ws = new WebSocket(page.webSocketDebuggerUrl);
2816
2845
  let seq = 0;
@@ -2892,7 +2921,14 @@ export class OpenClawCLI extends EventEmitter {
2892
2921
  }
2893
2922
  return lastState;
2894
2923
  };
2895
- if (effectiveRequestedUrl && normalizeForNavigationCompare(page.url) !== normalizeForNavigationCompare(effectiveRequestedUrl)) {
2924
+ const currentPageMatchesRequest = effectiveRequestedUrl && normalizeForNavigationCompare(page.url) === normalizeForNavigationCompare(effectiveRequestedUrl);
2925
+ const shouldNavigateForFreshVerification = !!(
2926
+ effectiveRequestedUrl &&
2927
+ options?.afterMutation &&
2928
+ navigationRequestedUrl &&
2929
+ navigationRequestedUrl !== effectiveRequestedUrl
2930
+ );
2931
+ if (effectiveRequestedUrl && (!currentPageMatchesRequest || shouldNavigateForFreshVerification)) {
2896
2932
  await cdp('Page.navigate', { url: navigationRequestedUrl || effectiveRequestedUrl });
2897
2933
  await waitForRequestedDocument(effectiveRequestedUrl);
2898
2934
  } else if (effectiveRequestedUrl) {
package/src/browser.js CHANGED
@@ -25,6 +25,43 @@ async function getBrowser() {
25
25
  return _browser;
26
26
  }
27
27
 
28
+ function stripLocalCacheBust(value) {
29
+ try {
30
+ const parsed = new URL(value);
31
+ parsed.searchParams.delete('__agentforge_verify');
32
+ return parsed.href.replace(/\/$/, '');
33
+ } catch {
34
+ return String(value || '').replace(/([?&])__agentforge_verify=[^&#]+&?/, '$1').replace(/[?&]$/, '').replace(/\/$/, '');
35
+ }
36
+ }
37
+
38
+ function sameBrowserTargetUrl(left, right) {
39
+ if (!left || !right) return false;
40
+ return stripLocalCacheBust(left) === stripLocalCacheBust(right);
41
+ }
42
+
43
+ function isLocalVerificationUrl(value) {
44
+ return isLocalHttpUrl(value) && /[?&]__agentforge_verify=/.test(String(value || ''));
45
+ }
46
+
47
+ async function closeStaleVerificationTabs(browser, keepPage, targetUrl) {
48
+ const expected = targetUrl ? stripLocalCacheBust(targetUrl) : '';
49
+ const pages = await browser.pages();
50
+ let closed = 0;
51
+ await Promise.all(pages.map(async (page) => {
52
+ if (!page || page.isClosed() || page === keepPage) return;
53
+ let url = '';
54
+ try { url = page.url(); } catch { return; }
55
+ if (!isLocalVerificationUrl(url)) return;
56
+ if (expected && stripLocalCacheBust(url) !== expected) return;
57
+ try {
58
+ await page.close();
59
+ closed += 1;
60
+ } catch {}
61
+ }));
62
+ if (closed > 0) console.log(`[browser.js] Closed ${closed} stale verification tab(s) for ${targetUrl || 'local browser'}`);
63
+ }
64
+
28
65
  // Return the active page for this agent. Creates a fresh tab on first use.
29
66
  // preferUrl: hint โ€” if the agent's tab already has a better match among all open
30
67
  // tabs (e.g. a link opened a new tab), update the agent's pointer to it.
@@ -37,7 +74,7 @@ async function getPage(agentId, preferUrl) {
37
74
  // If preferUrl given, check if any tab matches better (handles new-tab links)
38
75
  if (preferUrl) {
39
76
  const pages = await browser.pages();
40
- const match = pages.find(p => !p.isClosed() && p.url().includes(preferUrl));
77
+ const match = pages.find(p => !p.isClosed() && sameBrowserTargetUrl(p.url(), preferUrl));
41
78
  if (match && match !== existing) {
42
79
  _agentPages.set(agentId, match);
43
80
  return match;
@@ -46,6 +83,15 @@ async function getPage(agentId, preferUrl) {
46
83
  return existing;
47
84
  }
48
85
 
86
+ if (preferUrl) {
87
+ const pages = await browser.pages();
88
+ const match = pages.find(p => !p.isClosed() && sameBrowserTargetUrl(p.url(), preferUrl));
89
+ if (match) {
90
+ _agentPages.set(agentId, match);
91
+ return match;
92
+ }
93
+ }
94
+
49
95
  // Agent has no page โ€” open a fresh tab so it doesn't land on another agent's tab
50
96
  const fresh = await browser.newPage();
51
97
  _agentPages.set(agentId, fresh);
@@ -71,23 +117,14 @@ function withLocalCacheBust(value) {
71
117
  }
72
118
 
73
119
  async function waitForPageReady(page, expectedUrl) {
74
- const stripCacheBust = (value) => {
75
- try {
76
- const parsed = new URL(value);
77
- parsed.searchParams.delete('__agentforge_verify');
78
- return parsed.href.replace(/\/$/, '');
79
- } catch {
80
- return String(value || '').replace(/([?&])__agentforge_verify=[^&#]+&?/, '$1').replace(/[?&]$/, '').replace(/\/$/, '');
81
- }
82
- };
83
- const expected = expectedUrl ? stripCacheBust(expectedUrl) : '';
120
+ const expected = expectedUrl ? stripLocalCacheBust(expectedUrl) : '';
84
121
  for (let i = 0; i < 45; i++) {
85
122
  try {
86
123
  const state = await page.evaluate(() => ({
87
124
  href: location.href,
88
125
  readyState: document.readyState,
89
126
  }));
90
- if ((!expected || stripCacheBust(state.href) === expected) && state.readyState !== 'loading') return;
127
+ if ((!expected || stripLocalCacheBust(state.href) === expected) && state.readyState !== 'loading') return;
91
128
  } catch {
92
129
  // Page contexts can disappear while navigation commits.
93
130
  }
@@ -200,7 +237,7 @@ async function _browserActionInner(input, agentId, browser) {
200
237
  case 'navigate':
201
238
  case 'open': {
202
239
  const url = input.url || input.targetUrl;
203
- const page = await getPage(agentId);
240
+ const page = await getPage(agentId, url);
204
241
  await page.setCacheEnabled(false).catch(() => null);
205
242
  const navigationUrl = withLocalCacheBust(url);
206
243
  // Use Promise.race to enforce a hard 20s cap โ€” page.goto timeout option can hang
@@ -215,6 +252,7 @@ async function _browserActionInner(input, agentId, browser) {
215
252
  _agentPages.set(agentId, page);
216
253
  // Brief pause for initial render, then snapshot
217
254
  await waitForPageReady(page, url);
255
+ await closeStaleVerificationTabs(browser, page, url);
218
256
  await _wait(400);
219
257
  return await _snapshot(page, agentId);
220
258
  }
@@ -112,7 +112,7 @@ export function extractNamedPageScopeSlugs(message) {
112
112
  addScopeCandidate(candidates, match[1]);
113
113
  }
114
114
 
115
- const afterPage = /\b(?:pages?|screens?|routes?|views?|listings?)\s+(?:for|of|called|named)\s+(?:the\s+)?([a-z0-9][a-z0-9 ._/-]{0,160}?)(?=$|[?!;]|\.(?:\s|$)|\s+(?:on|at|in|with|without|that|which|where|when|because|so|but)\b)/gi;
115
+ const afterPage = /\b(?:pages?|screens?|routes?|views?|listings?)\s+(?:for|of|called|named)\s+(?:the\s+)?([a-z0-9][a-z0-9 ._/-]{0,160}?)(?=$|[?!;]|\.(?:\s|$)|\s+(?:on|at|in|from|with|without|while|using|preserv(?:e|ing)|that|which|where|when|because|so|but)\b)/gi;
116
116
  for (const match of text.matchAll(afterPage)) {
117
117
  const before = text.slice(Math.max(0, match.index - 120), match.index);
118
118
  const precedingToken = before.match(/\b([a-z0-9][a-z0-9._/-]{2,})\s*$/i)?.[1] || '';
@@ -124,7 +124,7 @@ export function extractNamedPageScopeSlugs(message) {
124
124
 
125
125
  for (const match of text.matchAll(/\b[a-z0-9]+(?:[.-][a-z0-9]+)+\b/gi)) {
126
126
  const after = text.slice(match.index + match[0].length);
127
- const pageTarget = after.match(/^\s+(?:listing\s+)?(?:pages?|screens?|routes?|views?|listings?)\s+(?:for|of|called|named)\s+(?:the\s+)?([a-z0-9][a-z0-9 ._/-]{0,160}?)(?=$|[?!;]|\.(?:\s|$)|\s+(?:on|at|in|with|without|that|which|where|when|because|so|but)\b)/i);
127
+ const pageTarget = after.match(/^\s+(?:listing\s+)?(?:pages?|screens?|routes?|views?|listings?)\s+(?:for|of|called|named)\s+(?:the\s+)?([a-z0-9][a-z0-9 ._/-]{0,160}?)(?=$|[?!;]|\.(?:\s|$)|\s+(?:on|at|in|from|with|without|while|using|preserv(?:e|ing)|that|which|where|when|because|so|but)\b)/i);
128
128
  if (pageTarget) {
129
129
  const targetCandidates = new Set();
130
130
  addScopeCandidate(targetCandidates, pageTarget[1]);
package/src/worker.js CHANGED
@@ -501,6 +501,15 @@ export class AgentForgeWorker extends EventEmitter {
501
501
  return /\b(delete|remove|drop|strip|simplify|replace|rewrite|rebuild|from scratch|start over|nuke|wipe|clear out|throw away)\b/i.test(String(userMessage || ''));
502
502
  }
503
503
 
504
+ _allowsScopedPageSourcesToRemainDeleted(userMessage) {
505
+ const text = String(userMessage || '');
506
+ const asksForDeletion = /\b(delete|remove|drop|unpublish|decommission|take down|take offline)\b/i.test(text);
507
+ if (!asksForDeletion) return false;
508
+
509
+ const asksForRebuildOrRepair = /\b(rebuild|recreate|remake|rewrite|replace|restore|redesign|build|create|implement|fix|improve|polish|repair|start over|from scratch|clean start|fresh start|same urls?|same paths?|preserv(?:e|ing) the same urls?)\b/i.test(text);
510
+ return !asksForRebuildOrRepair;
511
+ }
512
+
504
513
  _parseNumstat(output, source) {
505
514
  const stats = [];
506
515
  for (const line of String(output || '').split('\n')) {
@@ -848,6 +857,7 @@ export class AgentForgeWorker extends EventEmitter {
848
857
 
849
858
  _findDeletedScopedPageSources(repoBaselines, userMessage) {
850
859
  if (!Array.isArray(repoBaselines) || repoBaselines.length === 0) return [];
860
+ if (this._allowsScopedPageSourcesToRemainDeleted(userMessage)) return [];
851
861
  const { slugs: allowedSlugs, pageOnly } = this._extractExplicitScope(userMessage);
852
862
  if (allowedSlugs.length === 0 || !pageOnly) return [];
853
863