@hamp10/agentforge 0.2.25 → 0.2.26

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.25",
3
+ "version": "0.2.26",
4
4
  "description": "AgentForge worker — connect your machine to agentforge.ai",
5
5
  "type": "module",
6
6
  "bin": {
@@ -668,6 +668,7 @@ try {
668
668
  const openClawSource = readFileSync(new URL('../src/OpenClawCLI.js', import.meta.url), 'utf-8');
669
669
  const workerSource = readFileSync(new URL('../src/worker.js', import.meta.url), 'utf-8');
670
670
  const workerBinSource = readFileSync(new URL('../bin/agentforge.js', import.meta.url), 'utf-8');
671
+ const selfUpdateSource = readFileSync(new URL('../src/selfUpdate.js', import.meta.url), 'utf-8');
671
672
  const defaultGuidesSource = readFileSync(new URL('../src/default-task-guides.js', import.meta.url), 'utf-8');
672
673
  const browserSource = readFileSync(new URL('../src/browser.js', import.meta.url), 'utf-8');
673
674
  const previewServerSource = readFileSync(new URL('../src/preview-server.js', import.meta.url), 'utf-8');
@@ -838,16 +839,41 @@ assert.match(
838
839
  /app\.delete\('\/api\/agents\/delete\/cleanup-empty'[\s\S]*activeTasks[\s\S]*type: 'agents_pruned'/i,
839
840
  'empty-agent cleanup should preserve live tasks and broadcast removed agents'
840
841
  );
842
+ assert.match(
843
+ serverSource,
844
+ /CREATE TABLE IF NOT EXISTS agent_flows[\s\S]*config JSONB DEFAULT NULL/i,
845
+ 'agent flow emergency schema patch should include config so model routing survives skipped migrations'
846
+ );
847
+ assert.match(
848
+ serverSource,
849
+ /ALTER TABLE agent_flows ADD COLUMN IF NOT EXISTS config JSONB DEFAULT NULL/i,
850
+ 'agent flow startup should self-heal missing config column'
851
+ );
852
+ assert.match(
853
+ serverSource,
854
+ /ON CONFLICT \(id\)[\s\S]*WHERE agent_flows\.user_id = EXCLUDED\.user_id[\s\S]*RETURNING id/i,
855
+ 'flow upserts should not update another user flow with the same id'
856
+ );
841
857
  assert.match(
842
858
  dashboardSource,
843
859
  /confirmDeleteAgent[\s\S]*await fetch[\s\S]*removeAgentFromLocalState/i,
844
860
  'single-agent delete should only remove local UI after the server confirms deletion'
845
861
  );
862
+ assert.match(
863
+ dashboardSource,
864
+ /confirmPruneAgents[\s\S]*\/api\/agents\/delete\/prune\?keep=15[\s\S]*removeAgentFromLocalState/i,
865
+ 'dashboard should expose old-agent pruning through the safe keep-latest endpoint'
866
+ );
846
867
  assert.match(
847
868
  dashboardSource,
848
869
  /function finalizeLiveFeedBeforeGuide[\s\S]*commitChunkBuffer[\s\S]*hideTypingIndicator/i,
849
870
  'live Guide messages should close the current visible agent feed before rendering the user bubble'
850
871
  );
872
+ assert.match(
873
+ selfUpdateSource,
874
+ /ensureWritableGlobalInstallEnv[\s\S]*canWriteNpmRoot[\s\S]*configureUserOwnedNpmGlobalEnv/i,
875
+ 'self-update should preflight npm global permissions before printing a failed system-prefix install'
876
+ );
851
877
  assert.match(
852
878
  dashboardSource,
853
879
  /async function sendGuideMessage[\s\S]*agent\._guideInflight[\s\S]*finalizeLiveFeedBeforeGuide\(agentId\)[\s\S]*addMessage/i,
@@ -953,6 +979,16 @@ assert.doesNotMatch(
953
979
  /taskRequiresVisualVerification\s*&&\s*directIteration\s*<=\s*1\s*&&\s*explicitScopeForTask\.pageOnly/s,
954
980
  'comparable UI context gate should apply on retries, not only first iteration'
955
981
  );
982
+ assert.match(
983
+ openClawSource,
984
+ /read-only project context, not as a target page/,
985
+ 'read-only comparable page inspection should not be narrated as target-page work'
986
+ );
987
+ assert.match(
988
+ openClawSource,
989
+ /verifiedLocalUrl = isLocalUiUrl\(resultUrl \|\| url\)/,
990
+ 'scoped auto-verification should count a clean inferred local target URL even when browser output formatting is imperfect'
991
+ );
956
992
  assert.match(
957
993
  openClawSource,
958
994
  /const minRatio = isLargeText \? 3 : 4\.5;/,
@@ -4629,8 +4629,10 @@ export class OpenClawCLI extends EventEmitter {
4629
4629
  recordDirectToolSummary('browser', result);
4630
4630
  continue;
4631
4631
  }
4632
- rememberLocalScopedUrl(browserResultUrl(result));
4633
- if (isLocalUiVerificationResult(result)) {
4632
+ const resultUrl = browserResultUrl(result);
4633
+ rememberLocalScopedUrl(resultUrl);
4634
+ const verifiedLocalUrl = isLocalUiUrl(resultUrl || url);
4635
+ if (isLocalUiVerificationResult(result) || verifiedLocalUrl) {
4634
4636
  recordCleanLocalVerification(result, [slug]);
4635
4637
  verifiedAny = true;
4636
4638
  }
@@ -4766,6 +4768,13 @@ export class OpenClawCLI extends EventEmitter {
4766
4768
  return `I am checking the live browser${url} to judge the actual product surface before deciding the next change.`;
4767
4769
  }
4768
4770
  if (name === 'read_file' || name === 'read') {
4771
+ const scope = extractExplicitScope(task);
4772
+ const targetText = String(targetPath || base || '').toLowerCase();
4773
+ const looksLikePageSource = /\.(html?|css|s[ac]ss|jsx?|tsx?|vue|svelte|astro|mdx?)$/i.test(targetText);
4774
+ const isNamedTarget = scope.slugs.length > 0 && scopeSlugsMatchingText(targetText, scope.slugs).length > 0;
4775
+ if (base && looksLikePageSource && scope.slugs.length > 0 && !isNamedTarget) {
4776
+ return `I am reading ${base} as read-only project context, not as a target page.`;
4777
+ }
4769
4778
  return base
4770
4779
  ? `I am reading ${base} to work from the current implementation instead of guessing.`
4771
4780
  : `I am reading the current code before making the next change.`;
@@ -5097,6 +5106,10 @@ export class OpenClawCLI extends EventEmitter {
5097
5106
  lastDirectVisualWarning = visualWarning;
5098
5107
  } else if (!taskRequiresVisualVerification || isLocalUiVerificationResult(result)) {
5099
5108
  recordCleanLocalVerification(result);
5109
+ } else {
5110
+ const verifiedUrl = resultUrl || args?.url || '';
5111
+ const verifiedSlugs = isLocalUiUrl(verifiedUrl) ? scopeSlugsInText(verifiedUrl) : [];
5112
+ if (verifiedSlugs.length > 0) recordCleanLocalVerification(result, verifiedSlugs);
5100
5113
  }
5101
5114
  }
5102
5115
  }
package/src/selfUpdate.js CHANGED
@@ -6,8 +6,8 @@
6
6
  * prevent infinite re-exec loops).
7
7
  */
8
8
 
9
- import { execSync, spawn } from 'child_process';
10
- import { mkdirSync } from 'fs';
9
+ import { execFileSync, execSync, spawn } from 'child_process';
10
+ import { mkdirSync, rmSync } from 'fs';
11
11
  import { homedir } from 'os';
12
12
  import path from 'path';
13
13
 
@@ -75,22 +75,58 @@ export async function checkAndUpdate(packageName, currentVersion) {
75
75
 
76
76
  function installGlobalPackage(pkg, { force = false } = {}) {
77
77
  const forceFlag = force ? ' --force' : '';
78
+ const preflightEnv = ensureWritableGlobalInstallEnv();
78
79
  try {
79
- execSync(`npm install -g${forceFlag} ${pkg}`, { stdio: 'inherit' });
80
+ execSync(`npm install -g${forceFlag} ${pkg}`, { stdio: 'inherit', env: preflightEnv });
80
81
  return;
81
82
  } catch (error) {
82
83
  if (!looksLikePermissionFailure(error)) throw error;
83
84
  }
84
85
 
86
+ const env = configureUserOwnedNpmGlobalEnv();
87
+ execSync(`npm install -g${forceFlag} ${pkg}`, { stdio: 'inherit', env });
88
+ }
89
+
90
+ function npmConfigGet(args) {
91
+ try {
92
+ return execFileSync('npm', args, {
93
+ encoding: 'utf8',
94
+ stdio: ['ignore', 'pipe', 'ignore'],
95
+ }).trim();
96
+ } catch {
97
+ return '';
98
+ }
99
+ }
100
+
101
+ function canWriteNpmRoot(rootDir) {
102
+ if (!rootDir) return false;
103
+ const testDir = path.join(rootDir, `.agentforge-write-test-${process.pid}`);
104
+ try {
105
+ mkdirSync(testDir, { recursive: true });
106
+ rmSync(testDir, { recursive: true, force: true });
107
+ return true;
108
+ } catch {
109
+ try { rmSync(testDir, { recursive: true, force: true }); } catch {}
110
+ return false;
111
+ }
112
+ }
113
+
114
+ function ensureWritableGlobalInstallEnv() {
115
+ const root = npmConfigGet(['root', '-g']);
116
+ if (canWriteNpmRoot(root)) return process.env;
117
+ return configureUserOwnedNpmGlobalEnv();
118
+ }
119
+
120
+ function configureUserOwnedNpmGlobalEnv() {
85
121
  const prefix = path.join(homedir(), '.npm-global');
86
122
  mkdirSync(path.join(prefix, 'bin'), { recursive: true });
87
123
  mkdirSync(path.join(prefix, 'lib', 'node_modules'), { recursive: true });
88
- execSync(`npm config set prefix "${prefix}"`, { stdio: 'inherit' });
124
+ execFileSync('npm', ['config', 'set', 'prefix', prefix], { stdio: ['ignore', 'ignore', 'inherit'] });
89
125
  const env = {
90
126
  ...process.env,
91
127
  PATH: `${path.join(prefix, 'bin')}${path.delimiter}${process.env.PATH || ''}`,
92
128
  };
93
- execSync(`npm install -g${forceFlag} ${pkg}`, { stdio: 'inherit', env });
129
+ return env;
94
130
  }
95
131
 
96
132
  function looksLikePermissionFailure(error) {