@hmharness/agent 0.23.21 → 0.23.23
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/code-wm-recorder.d.ts +23 -0
- package/dist/code-wm-recorder.js +71 -0
- package/dist/code-wm-sensor.d.ts +41 -0
- package/dist/code-wm-sensor.js +69 -0
- package/dist/extension-wm-sensor.d.ts +32 -0
- package/dist/extension-wm-sensor.js +21 -0
- package/dist/i18n.d.ts +4 -0
- package/dist/i18n.js +6 -0
- package/dist/runner.d.ts +1 -0
- package/dist/runner.js +20 -0
- package/dist/tools.js +21 -1
- package/package.json +11 -10
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @hmharness/agent - Code World Model recorder (production sensor wiring)
|
|
3
|
+
*
|
|
4
|
+
* Round 38 closes the loop the sensors were waiting for: extension page
|
|
5
|
+
* observations now reach a PERSISTED Code World Model. The recorder is a
|
|
6
|
+
* lazy per-home singleton — the first observation loads the model from
|
|
7
|
+
* HMH_HOME, the sensor feeds it, and a debounce flushes to disk. The LSP
|
|
8
|
+
* sensor (round 35) rides the same store once its tools gain an event
|
|
9
|
+
* hook; until then it stays library-only, honestly.
|
|
10
|
+
*
|
|
11
|
+
* Failure posture: everything best-effort — a storage error logs nothing
|
|
12
|
+
* and never breaks the tool that observed the page (evidence must not be
|
|
13
|
+
* able to kill the work it evidences). A crash loses at most one debounce
|
|
14
|
+
* window of observations.
|
|
15
|
+
*/
|
|
16
|
+
import { type CodeWorldModel } from '@hmharness/cognitive';
|
|
17
|
+
import type { RawPageData } from '@hmharness/extension';
|
|
18
|
+
/** The onPageRead hook handed to extensionTools — one closure per home. */
|
|
19
|
+
export declare function pageReadSink(home: string): (raw: RawPageData) => void;
|
|
20
|
+
/** Test isolation: drop caches + pending timers (never used in production). */
|
|
21
|
+
export declare function resetCodeWorldRecorder(): void;
|
|
22
|
+
/** Introspection for tests/status surfaces: the live model for a home. */
|
|
23
|
+
export declare function liveCodeWorldModel(home: string): Promise<CodeWorldModel | null>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @hmharness/agent - Code World Model recorder (production sensor wiring)
|
|
3
|
+
*
|
|
4
|
+
* Round 38 closes the loop the sensors were waiting for: extension page
|
|
5
|
+
* observations now reach a PERSISTED Code World Model. The recorder is a
|
|
6
|
+
* lazy per-home singleton — the first observation loads the model from
|
|
7
|
+
* HMH_HOME, the sensor feeds it, and a debounce flushes to disk. The LSP
|
|
8
|
+
* sensor (round 35) rides the same store once its tools gain an event
|
|
9
|
+
* hook; until then it stays library-only, honestly.
|
|
10
|
+
*
|
|
11
|
+
* Failure posture: everything best-effort — a storage error logs nothing
|
|
12
|
+
* and never breaks the tool that observed the page (evidence must not be
|
|
13
|
+
* able to kill the work it evidences). A crash loses at most one debounce
|
|
14
|
+
* window of observations.
|
|
15
|
+
*/
|
|
16
|
+
import { loadCodeWorldModel, saveCodeWorldModel } from '@hmharness/cognitive';
|
|
17
|
+
import { syncExtensionRuntime } from "./extension-wm-sensor.js";
|
|
18
|
+
const SAVE_DEBOUNCE_MS = 2_000;
|
|
19
|
+
const entries = new Map();
|
|
20
|
+
/** The onPageRead hook handed to extensionTools — one closure per home. */
|
|
21
|
+
export function pageReadSink(home) {
|
|
22
|
+
return (raw) => {
|
|
23
|
+
void observe(home, raw);
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
async function observe(home, raw) {
|
|
27
|
+
try {
|
|
28
|
+
let entry = entries.get(home);
|
|
29
|
+
if (!entry) {
|
|
30
|
+
entry = { load: loadCodeWorldModel(home), dirty: false, timer: null };
|
|
31
|
+
entries.set(home, entry);
|
|
32
|
+
}
|
|
33
|
+
const cwm = await entry.load;
|
|
34
|
+
syncExtensionRuntime(cwm, raw);
|
|
35
|
+
entry.dirty = true;
|
|
36
|
+
if (!entry.timer) {
|
|
37
|
+
entry.timer = setTimeout(() => {
|
|
38
|
+
if (!entry)
|
|
39
|
+
return;
|
|
40
|
+
entry.timer = null;
|
|
41
|
+
void flush(home);
|
|
42
|
+
}, SAVE_DEBOUNCE_MS);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch { /* observation is best-effort by contract */ }
|
|
46
|
+
}
|
|
47
|
+
async function flush(home) {
|
|
48
|
+
const entry = entries.get(home);
|
|
49
|
+
if (!entry || !entry.dirty)
|
|
50
|
+
return;
|
|
51
|
+
entry.dirty = false;
|
|
52
|
+
try {
|
|
53
|
+
const cwm = await entry.load;
|
|
54
|
+
await saveCodeWorldModel(home, cwm);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
entry.dirty = true; // keep the evidence for the next flush
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** Test isolation: drop caches + pending timers (never used in production). */
|
|
61
|
+
export function resetCodeWorldRecorder() {
|
|
62
|
+
for (const [, e] of entries)
|
|
63
|
+
if (e.timer)
|
|
64
|
+
clearTimeout(e.timer);
|
|
65
|
+
entries.clear();
|
|
66
|
+
}
|
|
67
|
+
/** Introspection for tests/status surfaces: the live model for a home. */
|
|
68
|
+
export async function liveCodeWorldModel(home) {
|
|
69
|
+
const entry = entries.get(home);
|
|
70
|
+
return entry ? entry.load : null;
|
|
71
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @hmharness/agent - LSP → Code World Model sensor wiring (audit line B)
|
|
3
|
+
*
|
|
4
|
+
* "LSP 提供 symbol/definition/reference/... 结构化观察,可作为 Code World
|
|
5
|
+
* Model 的传感器;不要把它做成 IDE" (02 doc). This adapter lives in agent
|
|
6
|
+
* — the composition layer that already depends on BOTH @hmharness/lsp and
|
|
7
|
+
* @hmharness/cognitive — so neither layering direction is violated.
|
|
8
|
+
*
|
|
9
|
+
* Mapping (LSP 3.18 shapes → audit ontology):
|
|
10
|
+
* DocumentSymbol tree → CodeEntity per symbol + 'defines' relation per
|
|
11
|
+
* parent→child edge (the only relation LSP symbols PROVE; calls/
|
|
12
|
+
* references need deeper requests — later sensors)
|
|
13
|
+
* publishDiagnostics payload → CodeDiagnostic (source: 'lsp' — the
|
|
14
|
+
* feedback-not-proof label travels into the model)
|
|
15
|
+
* BuildFact/RuntimeFact have their own sensors (build tool, runs) —
|
|
16
|
+
* not this adapter's job.
|
|
17
|
+
*/
|
|
18
|
+
import { CodeWorldModel, type CodeDiagnostic, type CodeEntity, type SymbolRelation } from '@hmharness/cognitive';
|
|
19
|
+
import type { Diagnostic, DocumentSymbol } from '@hmharness/lsp';
|
|
20
|
+
/**
|
|
21
|
+
* LSP DocumentSymbol tree → entities + 'defines' relations. Nested children
|
|
22
|
+
* become defines edges (a document defines its top-level symbols; a class
|
|
23
|
+
* defines its methods). Only what the symbols response PROVES is emitted.
|
|
24
|
+
*/
|
|
25
|
+
export declare function lspSymbolsToOntology(uri: string, symbols: DocumentSymbol[]): {
|
|
26
|
+
entities: CodeEntity[];
|
|
27
|
+
relations: SymbolRelation[];
|
|
28
|
+
};
|
|
29
|
+
/** LSP diagnostics payload → audit CodeDiagnostic (source pinned to 'lsp'). */
|
|
30
|
+
export declare function lspDiagnosticsToOntology(uri: string, diagnostics: Diagnostic[]): CodeDiagnostic[];
|
|
31
|
+
export interface CodeWorldSyncInput {
|
|
32
|
+
uri: string;
|
|
33
|
+
symbols?: DocumentSymbol[];
|
|
34
|
+
diagnostics?: Diagnostic[];
|
|
35
|
+
}
|
|
36
|
+
/** One sensor sync: symbols + diagnostics from LSP into the model. */
|
|
37
|
+
export declare function syncCodeWorldModel(cwm: CodeWorldModel, input: CodeWorldSyncInput): {
|
|
38
|
+
entitiesIngested: number;
|
|
39
|
+
relationsIngested: number;
|
|
40
|
+
diagnosticsIngested: number;
|
|
41
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** Map a LSP symbol kind number to the audit's entity kind (best effort). */
|
|
2
|
+
function entityKind(symbolKind) {
|
|
3
|
+
// LTS subset of LSP SymbolKind; unknown kinds land as 'field' — honest
|
|
4
|
+
// coarseness beats a wrong specific kind (the audit ontology has no
|
|
5
|
+
// 'property' kind — methods/fields cover the LSP property case)
|
|
6
|
+
if (symbolKind === 12)
|
|
7
|
+
return 'function';
|
|
8
|
+
if (symbolKind === 5)
|
|
9
|
+
return 'class';
|
|
10
|
+
if (symbolKind === 6)
|
|
11
|
+
return 'method';
|
|
12
|
+
if (symbolKind === 2)
|
|
13
|
+
return 'module';
|
|
14
|
+
if (symbolKind === 1)
|
|
15
|
+
return 'file';
|
|
16
|
+
return 'field';
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* LSP DocumentSymbol tree → entities + 'defines' relations. Nested children
|
|
20
|
+
* become defines edges (a document defines its top-level symbols; a class
|
|
21
|
+
* defines its methods). Only what the symbols response PROVES is emitted.
|
|
22
|
+
*/
|
|
23
|
+
export function lspSymbolsToOntology(uri, symbols) {
|
|
24
|
+
const entities = [];
|
|
25
|
+
const relations = [];
|
|
26
|
+
const walk = (list, parentId) => {
|
|
27
|
+
for (const s of list) {
|
|
28
|
+
const id = `${uri}#${s.name}`;
|
|
29
|
+
entities.push({
|
|
30
|
+
id,
|
|
31
|
+
kind: entityKind(s.kind),
|
|
32
|
+
name: s.name,
|
|
33
|
+
uri,
|
|
34
|
+
...(s.range?.start ? { range: { startLine: s.range.start.line + 1, endLine: (s.range.end?.line ?? s.range.start.line) + 1 } } : {}),
|
|
35
|
+
});
|
|
36
|
+
if (parentId)
|
|
37
|
+
relations.push({ from: parentId, to: id, kind: 'defines' });
|
|
38
|
+
if (s.children?.length)
|
|
39
|
+
walk(s.children, id);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
walk(symbols, null);
|
|
43
|
+
return { entities, relations };
|
|
44
|
+
}
|
|
45
|
+
/** LSP diagnostics payload → audit CodeDiagnostic (source pinned to 'lsp'). */
|
|
46
|
+
export function lspDiagnosticsToOntology(uri, diagnostics) {
|
|
47
|
+
return diagnostics.map((d) => ({
|
|
48
|
+
uri,
|
|
49
|
+
severity: (d.severity ?? 1),
|
|
50
|
+
message: d.message,
|
|
51
|
+
source: 'lsp', // the feedback-not-proof label rides into the model
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
/** One sensor sync: symbols + diagnostics from LSP into the model. */
|
|
55
|
+
export function syncCodeWorldModel(cwm, input) {
|
|
56
|
+
const result = { entitiesIngested: 0, relationsIngested: 0, diagnosticsIngested: 0 };
|
|
57
|
+
if (input.symbols?.length) {
|
|
58
|
+
const { entities, relations } = lspSymbolsToOntology(input.uri, input.symbols);
|
|
59
|
+
cwm.ingest({ entities, relations });
|
|
60
|
+
result.entitiesIngested = entities.length;
|
|
61
|
+
result.relationsIngested = relations.length;
|
|
62
|
+
}
|
|
63
|
+
if (input.diagnostics) {
|
|
64
|
+
const diags = lspDiagnosticsToOntology(input.uri, input.diagnostics);
|
|
65
|
+
cwm.ingest({ diagnostics: diags });
|
|
66
|
+
result.diagnosticsIngested = diags.length;
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @hmharness/agent - browser-extension → Code World Model runtime sensor
|
|
3
|
+
*
|
|
4
|
+
* The audit ontology's RuntimeFact ("runs" sensor) gets its first source:
|
|
5
|
+
* the user's REAL browser. Every extension_page_read observation lands as
|
|
6
|
+
* runtime facts — where the agent looked and what the user had selected.
|
|
7
|
+
* Like the LSP sensor (code-wm-sensor.ts, round 35), this adapter lives
|
|
8
|
+
* in agent — the composition layer that already depends on BOTH
|
|
9
|
+
* @hmharness/extension and @hmharness/cognitive — so neither layering
|
|
10
|
+
* direction is violated, and the mapping is a library call waiting for
|
|
11
|
+
* the model's persistence wiring (same honest posture as round 35).
|
|
12
|
+
*
|
|
13
|
+
* Mapping discipline (honest, bounded):
|
|
14
|
+
* every page read → ONE RuntimeFact kind 'ext.page.read'
|
|
15
|
+
* (title — url, capped; the identity is the URL)
|
|
16
|
+
* non-empty selection → ONE kind 'ext.page.selection' (user-intent
|
|
17
|
+
* signal — the human highlighted this text while
|
|
18
|
+
* the agent was working)
|
|
19
|
+
* nothing else → headings/links/text are CONTEXT for the loop's
|
|
20
|
+
* turn, not durable facts; flooding the model with
|
|
21
|
+
* per-heading facts would be noise dressed as
|
|
22
|
+
* evidence.
|
|
23
|
+
*/
|
|
24
|
+
import { CodeWorldModel, type RuntimeFact } from '@hmharness/cognitive';
|
|
25
|
+
import type { RawPageData } from '@hmharness/extension';
|
|
26
|
+
/** Page observation → audit RuntimeFacts (pure; no DOM, no I/O). */
|
|
27
|
+
export declare function pageReadToRuntimeFacts(raw: RawPageData, at?: string): RuntimeFact[];
|
|
28
|
+
export interface RuntimeSyncResult {
|
|
29
|
+
factsIngested: number;
|
|
30
|
+
}
|
|
31
|
+
/** One sensor sync: extension page observation → runtime facts. */
|
|
32
|
+
export declare function syncExtensionRuntime(cwm: CodeWorldModel, raw: RawPageData): RuntimeSyncResult;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const clip = (s, n) => {
|
|
2
|
+
const t = String(s ?? '').replace(/\s+/g, ' ').trim();
|
|
3
|
+
return t.length > n ? t.slice(0, n) : t;
|
|
4
|
+
};
|
|
5
|
+
/** Page observation → audit RuntimeFacts (pure; no DOM, no I/O). */
|
|
6
|
+
export function pageReadToRuntimeFacts(raw, at = new Date().toISOString()) {
|
|
7
|
+
const title = clip(raw.title, 120);
|
|
8
|
+
const url = clip(raw.url, 300);
|
|
9
|
+
const facts = [{ kind: 'ext.page.read', detail: title ? `${title} — ${url}` : url, at }];
|
|
10
|
+
const selection = clip(raw.selection, 200);
|
|
11
|
+
if (selection)
|
|
12
|
+
facts.push({ kind: 'ext.page.selection', detail: selection, at });
|
|
13
|
+
return facts;
|
|
14
|
+
}
|
|
15
|
+
/** One sensor sync: extension page observation → runtime facts. */
|
|
16
|
+
export function syncExtensionRuntime(cwm, raw) {
|
|
17
|
+
const facts = pageReadToRuntimeFacts(raw);
|
|
18
|
+
for (const f of facts)
|
|
19
|
+
cwm.ingest({ runtime: f });
|
|
20
|
+
return { factsIngested: facts.length };
|
|
21
|
+
}
|
package/dist/i18n.d.ts
CHANGED
|
@@ -139,6 +139,10 @@ export interface Strings {
|
|
|
139
139
|
opsHint: string;
|
|
140
140
|
tuiLongOutputHint: (tool: string, lines: number) => string;
|
|
141
141
|
tuiUserFolded: (hidden: number) => string;
|
|
142
|
+
/** update briefing trio (T27-v2): start line, completion line, first-launch note */
|
|
143
|
+
tuiUpdating: (v: string) => string;
|
|
144
|
+
tuiUpdated: (v: string) => string;
|
|
145
|
+
tuiBriefing: (v: string, note: string) => string;
|
|
142
146
|
tuiAutoUpdateFailed: (why: string) => string;
|
|
143
147
|
tuiEvolveDone: (proposals: number, insights: number, notes: number) => string;
|
|
144
148
|
tuiPassRate: (pct: string) => string;
|
package/dist/i18n.js
CHANGED
|
@@ -114,6 +114,9 @@ const zh = {
|
|
|
114
114
|
opsHint: '更多操作:/ops scan 立即扫描并生成今日简报 · /ops brief 查看最近简报',
|
|
115
115
|
tuiLongOutputHint: (tool, lines) => `⎿ ${tool} 输出 ${lines} 行(已折叠) · 按 z 展开最后一条 · Ctrl+T 看全文`,
|
|
116
116
|
tuiUserFolded: (hidden) => `(…还有 ${hidden} 行已折叠 · Ctrl+T 看全文)`,
|
|
117
|
+
tuiUpdating: (v) => `⤴ 发现新版本 ${v},后台更新中(不打扰当前会话,新版本下次启动生效)`,
|
|
118
|
+
tuiUpdated: (v) => `✓ 已更新到 ${v} · 重启 hmh 后生效`,
|
|
119
|
+
tuiBriefing: (v, note) => `📋 本次更新 ${v}:${note}`,
|
|
117
120
|
tuiAutoUpdateFailed: (why) => `⤴ 自动更新未能开始(${why}),下次再试;或手动: npm i -g @hmharness/cli@latest`,
|
|
118
121
|
tuiEvolveDone: (p, i, n) => `evolve 完成: ${p} 提案 · 洞察 ${i} · 记忆 ${n}`,
|
|
119
122
|
tuiPassRate: (pct) => `pass rate: ${pct}`,
|
|
@@ -267,6 +270,9 @@ const en = {
|
|
|
267
270
|
opsHint: 'more: /ops scan pull upstream + write today brief · /ops brief latest brief',
|
|
268
271
|
tuiLongOutputHint: (tool, lines) => `⎿ ${tool} produced ${lines} lines (folded) · z expands the last cell · Ctrl+T full transcript`,
|
|
269
272
|
tuiUserFolded: (hidden) => `(…${hidden} more lines folded · Ctrl+T full transcript)`,
|
|
273
|
+
tuiUpdating: (v) => `⤴ new version ${v} - updating in the background (this session is unaffected; effective on next launch)`,
|
|
274
|
+
tuiUpdated: (v) => `✓ updated to ${v} · restart hmh to activate`,
|
|
275
|
+
tuiBriefing: (v, note) => `📋 updated ${v}: ${note}`,
|
|
270
276
|
tuiAutoUpdateFailed: (why) => `⤴ auto-update could not start (${why}); or manually: npm i -g @hmharness/cli@latest`,
|
|
271
277
|
tuiEvolveDone: (p, i, n) => `evolve done: ${p} proposals · ${i} insights · ${n} notes`,
|
|
272
278
|
tuiPassRate: (pct) => `pass rate: ${pct}`,
|
package/dist/runner.d.ts
CHANGED
package/dist/runner.js
CHANGED
|
@@ -17,6 +17,8 @@ import { harmonyTools } from '@hmharness/domain-harmony';
|
|
|
17
17
|
import { opsTools } from '@hmharness/domain-ops';
|
|
18
18
|
import { discoverServers, lspTools } from '@hmharness/lsp';
|
|
19
19
|
import { discoverBrowsers, browserTools } from '@hmharness/browser';
|
|
20
|
+
import { discoverExtensionBridgeSync, extensionTools } from '@hmharness/extension';
|
|
21
|
+
import { pageReadSink } from "./code-wm-recorder.js";
|
|
20
22
|
import * as readline from 'node:readline/promises';
|
|
21
23
|
import { stdin } from 'node:process';
|
|
22
24
|
import { baseTools } from "./tools.js";
|
|
@@ -65,6 +67,24 @@ export function nativeRegistry(depth, opts = {}) {
|
|
|
65
67
|
}
|
|
66
68
|
catch { /* browser optional: never block tool assembly */ }
|
|
67
69
|
}
|
|
70
|
+
// extension_* family (2026-10-04, ChatGPT-style browser-extension bridge)
|
|
71
|
+
// — the user's REAL browser via the paired extension. Same dead-tools
|
|
72
|
+
// rule, but liveness is a STATE FILE the bridge keeps fresh (registry
|
|
73
|
+
// build stays sync); registration additionally requires an extension
|
|
74
|
+
// to be CONNECTED right now, and the tools themselves re-check the
|
|
75
|
+
// bridge over HTTP on every call.
|
|
76
|
+
if (opts.extension !== false) {
|
|
77
|
+
try {
|
|
78
|
+
const bridge = discoverExtensionBridgeSync(homeDir());
|
|
79
|
+
if (bridge.healthy && bridge.connected) {
|
|
80
|
+
// onPageRead feeds the PERSISTED Code World Model (round 38):
|
|
81
|
+
// every real-browser page observation becomes durable RuntimeFact
|
|
82
|
+
// evidence — best-effort, never able to fail the tool itself.
|
|
83
|
+
reg.registerAll(extensionTools({ home: homeDir(), onPageRead: pageReadSink(homeDir()) }));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch { /* extension optional: never block tool assembly */ }
|
|
87
|
+
}
|
|
68
88
|
if (depth < MAX_SPAWN_DEPTH) {
|
|
69
89
|
// blueprint M10: every spawn_agent is governed by the live topology
|
|
70
90
|
// (role contract + shared budget + immutable multi-agent.jsonl audit);
|
package/dist/tools.js
CHANGED
|
@@ -493,7 +493,7 @@ export const desktopTypeTool = {
|
|
|
493
493
|
*/
|
|
494
494
|
export const browserOpenTool = {
|
|
495
495
|
name: 'browser_open',
|
|
496
|
-
description: 'Open a URL in the user\'s default browser (visible). Then use desktop_screenshot + see_image to see the page, desktop_click/desktop_type to interact. Full workflow: browser_open → desktop_screenshot → see_image (find elements) → desktop_click (click) → desktop_screenshot (verify).',
|
|
496
|
+
description: 'Open a URL in the user\'s default browser (visible). Then use desktop_screenshot + see_image to see the page, desktop_click/desktop_type to interact. Full workflow: browser_open → desktop_screenshot → see_image (find elements) → desktop_click (click) → desktop_screenshot (verify). When running inside a host browser (BrowserOS ACP), this navigates the host\'s active tab instead of opening a new OS window — pair it with browser_snapshot/browser_read.',
|
|
497
497
|
parameters: {
|
|
498
498
|
type: 'object',
|
|
499
499
|
properties: {
|
|
@@ -506,6 +506,26 @@ export const browserOpenTool = {
|
|
|
506
506
|
const url = String(args.url ?? '').trim();
|
|
507
507
|
if (!/^https?:\/\//i.test(url))
|
|
508
508
|
return { output: 'only http(s) URLs', isError: true };
|
|
509
|
+
// Under a host browser (acp-serve/BrowserOS): opening the OS default
|
|
510
|
+
// browser pops a SECOND window per call — the exact bug users see as
|
|
511
|
+
// "a new browser window on every message". Navigate the host's active
|
|
512
|
+
// tab instead; the browser_* family reads/verifies from there.
|
|
513
|
+
const attachPort = Number(process.env.HMH_BROWSER_ATTACH ?? 0);
|
|
514
|
+
if (Number.isInteger(attachPort) && attachPort > 0) {
|
|
515
|
+
try {
|
|
516
|
+
const { CdpBrowser } = await import('@hmharness/browser');
|
|
517
|
+
const c = new CdpBrowser({ port: attachPort });
|
|
518
|
+
if (await c.up()) {
|
|
519
|
+
const tabs = (await c.tabs()).filter((t) => !t.url.startsWith('chrome-extension://'));
|
|
520
|
+
const page = tabs.find((t) => !t.url.startsWith('about:')) ?? tabs[0];
|
|
521
|
+
const tid = page?.targetId ?? (await c.openTab('about:blank'));
|
|
522
|
+
await c.navigate(tid, url);
|
|
523
|
+
const title = String(await c.evaluate(tid, 'document.title').catch(() => ''));
|
|
524
|
+
return { output: `navigated the host browser to ${url} (title: ${title || 'loading'}) — use browser_snapshot/browser_read to see the page` };
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
catch { /* fall through to the default-browser path if attach fails */ }
|
|
528
|
+
}
|
|
509
529
|
try {
|
|
510
530
|
if (process.platform === 'win32') {
|
|
511
531
|
const { execFile } = await import('node:child_process');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hmharness/agent",
|
|
3
|
-
"version": "0.23.
|
|
3
|
+
"version": "0.23.23",
|
|
4
4
|
"description": "hmharness agent execution layer: base tools, system prompt, sub-agent spawn, and the shared task runner that frontends (cli, web) drive.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -15,15 +15,16 @@
|
|
|
15
15
|
"build": "tsc -p tsconfig.build.json"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@hmharness/cognitive": "0.23.
|
|
19
|
-
"@hmharness/domain-harmony": "0.23.
|
|
20
|
-
"@hmharness/domain-ops": "0.23.
|
|
21
|
-
"@hmharness/evolution": "0.23.
|
|
22
|
-
"@hmharness/kernel": "0.23.
|
|
23
|
-
"@hmharness/observability": "0.23.
|
|
24
|
-
"@hmharness/sandbox": "0.23.
|
|
25
|
-
"@hmharness/lsp": "0.23.
|
|
26
|
-
"@hmharness/browser": "0.23.
|
|
18
|
+
"@hmharness/cognitive": "0.23.23",
|
|
19
|
+
"@hmharness/domain-harmony": "0.23.23",
|
|
20
|
+
"@hmharness/domain-ops": "0.23.23",
|
|
21
|
+
"@hmharness/evolution": "0.23.23",
|
|
22
|
+
"@hmharness/kernel": "0.23.23",
|
|
23
|
+
"@hmharness/observability": "0.23.23",
|
|
24
|
+
"@hmharness/sandbox": "0.23.23",
|
|
25
|
+
"@hmharness/lsp": "0.23.23",
|
|
26
|
+
"@hmharness/browser": "0.23.23",
|
|
27
|
+
"@hmharness/extension": "0.23.23"
|
|
27
28
|
},
|
|
28
29
|
"files": [
|
|
29
30
|
"dist"
|