@amoree-code/ocean 0.0.0-stage → 1.1.0
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/LICENSE +21 -0
- package/README.md +284 -2
- package/dist/application/auth/auth-orchestrator.js +77 -0
- package/dist/application/clients/clients.js +646 -0
- package/dist/application/clients/editors.js +398 -0
- package/dist/application/clients/hooks.js +335 -0
- package/dist/application/clients/native.js +410 -0
- package/dist/application/clients/opencode-plugin.js +158 -0
- package/dist/application/clients/registry.js +188 -0
- package/dist/application/gateway/webhook-gateway.js +137 -0
- package/dist/application/install/provider-installer.js +184 -0
- package/dist/application/integrations/charter-copies.js +76 -0
- package/dist/application/integrations/claude-vscode.js +106 -0
- package/dist/application/integrations/engine-entry-repair.js +105 -0
- package/dist/application/integrations/skill-filter.js +254 -0
- package/dist/application/layout/layout-apply.js +443 -0
- package/dist/application/layout/layout-plan.js +236 -0
- package/dist/application/layout/workspace-template.js +34 -0
- package/dist/application/onboarding/state.js +23 -0
- package/dist/application/onboarding/steps.js +249 -0
- package/dist/application/profiles/profile-distribution.js +53 -0
- package/dist/application/profiles/profile-suggestion.js +39 -0
- package/dist/application/profiles/profile-upgrade-service.js +208 -0
- package/dist/application/pull/history-index.js +476 -0
- package/dist/application/pull/project-match.js +132 -0
- package/dist/application/pull/pull.js +202 -0
- package/dist/application/pull/server-capture.js +307 -0
- package/dist/application/scheduler/local-scheduler.js +210 -0
- package/dist/composition/runtime.js +64 -0
- package/dist/engine-root.js +9 -0
- package/dist/infrastructure/filesystem/profile-loader.js +46 -0
- package/dist/infrastructure/filesystem/runtime-config-loader.js +29 -0
- package/dist/infrastructure/filesystem/skill-loader.js +84 -0
- package/dist/infrastructure/filesystem/workspace-capability.js +37 -0
- package/dist/infrastructure/observability/runtime-logger.js +16 -0
- package/dist/infrastructure/process/agent-tool-executor.js +99 -0
- package/dist/infrastructure/process/cli-process.js +49 -0
- package/dist/infrastructure/process/interactive-process.js +145 -0
- package/dist/infrastructure/process/process-supervisor.js +194 -0
- package/dist/infrastructure/process/service.js +252 -0
- package/dist/infrastructure/providers/browser-provider.js +2 -0
- package/dist/infrastructure/providers/claude-sdk.js +137 -0
- package/dist/infrastructure/providers/client-home.js +14 -0
- package/dist/infrastructure/providers/fake-browser-provider.js +146 -0
- package/dist/infrastructure/providers/ollama-embedder.js +74 -0
- package/dist/infrastructure/providers/openai-compatible.js +268 -0
- package/dist/infrastructure/providers/playwright-browser-provider.js +380 -0
- package/dist/infrastructure/providers/provider-capabilities.js +50 -0
- package/dist/infrastructure/providers/provider-invocation.js +184 -0
- package/dist/infrastructure/providers/provider-registry.js +77 -0
- package/dist/infrastructure/providers/providers.js +134 -0
- package/dist/infrastructure/wrappers/wrapper-manager.js +374 -0
- package/dist/interfaces/cli/auth-command.js +54 -0
- package/dist/interfaces/cli/browser-bridge-command.js +93 -0
- package/dist/interfaces/cli/browser-command.js +119 -0
- package/dist/interfaces/cli/capture-command.js +125 -0
- package/dist/interfaces/cli/client-test-command.js +133 -0
- package/dist/interfaces/cli/clients-command.js +129 -0
- package/dist/interfaces/cli/command-registry.js +1211 -0
- package/dist/interfaces/cli/context-command.js +172 -0
- package/dist/interfaces/cli/daily-command.js +131 -0
- package/dist/interfaces/cli/governance-command.js +55 -0
- package/dist/interfaces/cli/handoff-command.js +35 -0
- package/dist/interfaces/cli/help-command.js +207 -0
- package/dist/interfaces/cli/idea-command.js +47 -0
- package/dist/interfaces/cli/intercept-command.js +317 -0
- package/dist/interfaces/cli/layout-command.js +27 -0
- package/dist/interfaces/cli/market-command.js +52 -0
- package/dist/interfaces/cli/mcp-gateway-command.js +30 -0
- package/dist/interfaces/cli/memory-command.js +317 -0
- package/dist/interfaces/cli/migrate-command.js +26 -0
- package/dist/interfaces/cli/observe-command.js +78 -0
- package/dist/interfaces/cli/onboard-command.js +123 -0
- package/dist/interfaces/cli/operate-command.js +59 -0
- package/dist/interfaces/cli/profiles-command.js +70 -0
- package/dist/interfaces/cli/pull-command.js +54 -0
- package/dist/interfaces/cli/setup-command.js +143 -0
- package/dist/interfaces/cli/tasks-command.js +249 -0
- package/dist/main.js +49 -0
- package/dist/node-warnings.js +17 -0
- package/dist/version.js +8 -0
- package/market/mcp/chrome-devtools/ocean.capability.json +28 -0
- package/market/mcp/gateway/ocean.capability.json +21 -0
- package/market/mcp/obsidian/ocean.capability.json +21 -0
- package/market/mcp/ocean/ocean.capability.json +21 -0
- package/market/mcp/playwright/ocean.capability.json +23 -0
- package/node_modules/@ocean/brain/dist/brain/brain-eval.js +29 -0
- package/node_modules/@ocean/brain/dist/brain/brain-markdown.js +226 -0
- package/node_modules/@ocean/brain/dist/brain/brain-reindex.js +247 -0
- package/node_modules/@ocean/brain/dist/brain/brain-service.js +320 -0
- package/node_modules/@ocean/brain/dist/capture/inbox-sync.js +22 -0
- package/node_modules/@ocean/brain/dist/evidence/verification-report.js +13 -0
- package/node_modules/@ocean/brain/dist/infrastructure/brain/brain-index.js +285 -0
- package/node_modules/@ocean/brain/dist/memory/brain-dump.js +145 -0
- package/node_modules/@ocean/brain/dist/memory/daily-narrative.js +142 -0
- package/node_modules/@ocean/brain/dist/memory/index-sync.js +124 -0
- package/node_modules/@ocean/brain/dist/memory/lesson-activation.js +93 -0
- package/node_modules/@ocean/brain/dist/memory/lesson-learning.js +188 -0
- package/node_modules/@ocean/brain/dist/memory/lessons.js +321 -0
- package/node_modules/@ocean/brain/dist/memory/memory-audit.js +100 -0
- package/node_modules/@ocean/brain/dist/memory/memory-digest.js +49 -0
- package/node_modules/@ocean/brain/dist/memory/memory-harvest.js +345 -0
- package/node_modules/@ocean/brain/dist/memory/memory-paths.js +174 -0
- package/node_modules/@ocean/brain/dist/memory/memory-review.js +278 -0
- package/node_modules/@ocean/brain/dist/memory/memory-trash.js +152 -0
- package/node_modules/@ocean/brain/dist/memory/memory-write.js +216 -0
- package/node_modules/@ocean/brain/dist/memory/model-narrative.js +129 -0
- package/node_modules/@ocean/brain/dist/memory/profile-facts.js +103 -0
- package/node_modules/@ocean/brain/dist/memory/session-promotion.js +61 -0
- package/node_modules/@ocean/brain/dist/memory/session-summary.js +113 -0
- package/node_modules/@ocean/brain/dist/memory/signal-detection.js +66 -0
- package/node_modules/@ocean/brain/package.json +11 -0
- package/node_modules/@ocean/context/dist/context/context-compression.js +66 -0
- package/node_modules/@ocean/context/dist/context/context-cost.js +587 -0
- package/node_modules/@ocean/context/dist/context/context-ladder.js +363 -0
- package/node_modules/@ocean/context/dist/context/context-packet.js +213 -0
- package/node_modules/@ocean/context/dist/context/context-references.js +255 -0
- package/node_modules/@ocean/context/dist/context/intent-router.js +259 -0
- package/node_modules/@ocean/context/dist/context/resource-injection.js +36 -0
- package/node_modules/@ocean/context/package.json +11 -0
- package/node_modules/@ocean/core/dist/domain/brain/brain-record.js +22 -0
- package/node_modules/@ocean/core/dist/domain/brain/record-scope.js +22 -0
- package/node_modules/@ocean/core/dist/domain/capabilities/browser-contract.js +64 -0
- package/node_modules/@ocean/core/dist/domain/capabilities/capability-contract.js +16 -0
- package/node_modules/@ocean/core/dist/domain/capabilities/capability-manifest.js +72 -0
- package/node_modules/@ocean/core/dist/domain/context/context-validator.js +5 -0
- package/node_modules/@ocean/core/dist/domain/context/context.js +34 -0
- package/node_modules/@ocean/core/dist/domain/context/project-id.js +30 -0
- package/node_modules/@ocean/core/dist/domain/evidence/evidence.js +16 -0
- package/node_modules/@ocean/core/dist/domain/mcp/mcp-contract.js +36 -0
- package/node_modules/@ocean/core/dist/domain/mcp/memory-contract.js +41 -0
- package/node_modules/@ocean/core/dist/domain/ports/brain-index-port.js +7 -0
- package/node_modules/@ocean/core/dist/domain/ports/browser-port.js +8 -0
- package/node_modules/@ocean/core/dist/domain/ports/embedder-port.js +2 -0
- package/node_modules/@ocean/core/dist/domain/ports/platform-ports.js +2 -0
- package/node_modules/@ocean/core/dist/domain/ports/runtime-ports.js +2 -0
- package/node_modules/@ocean/core/dist/domain/ports/session-store-port.js +2 -0
- package/node_modules/@ocean/core/dist/domain/process/process-events.js +5 -0
- package/node_modules/@ocean/core/dist/domain/profiles/profile-distribution.js +19 -0
- package/node_modules/@ocean/core/dist/domain/profiles/profile-policy.js +34 -0
- package/node_modules/@ocean/core/dist/domain/profiles/profile-selection.js +73 -0
- package/node_modules/@ocean/core/dist/domain/profiles/profile-upgrade.js +123 -0
- package/node_modules/@ocean/core/dist/domain/profiles/profile-validator.js +5 -0
- package/node_modules/@ocean/core/dist/domain/profiles/profile.js +218 -0
- package/node_modules/@ocean/core/dist/domain/profiles/runtime-config.js +30 -0
- package/node_modules/@ocean/core/dist/domain/providers/provider-capability.js +28 -0
- package/node_modules/@ocean/core/dist/domain/providers/provider.js +2 -0
- package/node_modules/@ocean/core/dist/domain/redaction/redaction.js +86 -0
- package/node_modules/@ocean/core/dist/domain/runs/agent-tools.js +92 -0
- package/node_modules/@ocean/core/dist/domain/runs/execution-plan.js +71 -0
- package/node_modules/@ocean/core/dist/domain/runs/run-contract.js +38 -0
- package/node_modules/@ocean/core/dist/domain/sessions/entry-contract.js +31 -0
- package/node_modules/@ocean/core/dist/domain/sessions/handoff.js +78 -0
- package/node_modules/@ocean/core/dist/domain/sessions/session.js +58 -0
- package/node_modules/@ocean/core/dist/domain/skills/default-skills.js +29 -0
- package/node_modules/@ocean/core/dist/domain/skills/skill-validator.js +5 -0
- package/node_modules/@ocean/core/dist/domain/skills/skill.js +11 -0
- package/node_modules/@ocean/core/dist/domain/tasks/task-id.js +85 -0
- package/node_modules/@ocean/core/dist/shared/fs-utils.js +212 -0
- package/node_modules/@ocean/core/dist/shared/local-date.js +16 -0
- package/node_modules/@ocean/core/dist/shared/paths.js +324 -0
- package/node_modules/@ocean/core/dist/shared/project-resolution.js +1065 -0
- package/node_modules/@ocean/core/dist/shared/task-reference.js +133 -0
- package/node_modules/@ocean/core/package.json +14 -0
- package/node_modules/@ocean/loop/dist/handoff/handoff-service.js +133 -0
- package/node_modules/@ocean/loop/dist/hooks/lifecycle-hooks.js +15 -0
- package/node_modules/@ocean/loop/dist/hooks/session-end-hook.js +183 -0
- package/node_modules/@ocean/loop/dist/hooks/session-start-hook.js +237 -0
- package/node_modules/@ocean/loop/dist/infrastructure/persistence/session-store.js +690 -0
- package/node_modules/@ocean/loop/dist/infrastructure/persistence/session-validator.js +5 -0
- package/node_modules/@ocean/loop/dist/loops/iteration-graph.js +147 -0
- package/node_modules/@ocean/loop/dist/loops/task-loop-record.js +2 -0
- package/node_modules/@ocean/loop/dist/loops/task-loop.js +281 -0
- package/node_modules/@ocean/loop/dist/operations/memory-knowledge-operations.js +263 -0
- package/node_modules/@ocean/loop/dist/operations/operation-contract.js +227 -0
- package/node_modules/@ocean/loop/dist/operations/operation-scope.js +28 -0
- package/node_modules/@ocean/loop/dist/operations/project-operations.js +93 -0
- package/node_modules/@ocean/loop/dist/operations/record-io.js +65 -0
- package/node_modules/@ocean/loop/dist/operations/record-operations.js +45 -0
- package/node_modules/@ocean/loop/dist/operations/task-linker.js +40 -0
- package/node_modules/@ocean/loop/dist/operations/task-operations.js +279 -0
- package/node_modules/@ocean/loop/dist/operations/write-guard.js +157 -0
- package/node_modules/@ocean/loop/dist/runs/prompt-assembly.js +112 -0
- package/node_modules/@ocean/loop/dist/runs/run-agent.js +425 -0
- package/node_modules/@ocean/loop/dist/runs/run-authorization.js +24 -0
- package/node_modules/@ocean/loop/dist/runs/tool-loop.js +173 -0
- package/node_modules/@ocean/loop/dist/sessions/session-closeout.js +150 -0
- package/node_modules/@ocean/loop/dist/sessions/session-pointer.js +133 -0
- package/node_modules/@ocean/loop/dist/sessions/session-projects.js +26 -0
- package/node_modules/@ocean/loop/dist/sessions/session-retention.js +132 -0
- package/node_modules/@ocean/loop/dist/tasks/archive-tasks.js +210 -0
- package/node_modules/@ocean/loop/dist/tasks/checkpoint-task.js +31 -0
- package/node_modules/@ocean/loop/dist/tasks/claim-task.js +126 -0
- package/node_modules/@ocean/loop/dist/tasks/list-tasks.js +113 -0
- package/node_modules/@ocean/loop/dist/tasks/new-task.js +42 -0
- package/node_modules/@ocean/loop/dist/tasks/renumber-tasks.js +134 -0
- package/node_modules/@ocean/loop/dist/tasks/verification-readiness.js +78 -0
- package/node_modules/@ocean/loop/dist/tasks/verify-task.js +234 -0
- package/node_modules/@ocean/loop/package.json +11 -0
- package/node_modules/@ocean/market/dist/adapters/mcp-adapter.js +64 -0
- package/node_modules/@ocean/market/dist/adapters/owned-directory.js +89 -0
- package/node_modules/@ocean/market/dist/adapters/plugin-adapter.js +39 -0
- package/node_modules/@ocean/market/dist/adapters/skill-adapter.js +32 -0
- package/node_modules/@ocean/market/dist/capability/adapter.js +2 -0
- package/node_modules/@ocean/market/dist/capability/catalog.js +63 -0
- package/node_modules/@ocean/market/dist/capability/content-digest.js +40 -0
- package/node_modules/@ocean/market/dist/capability/registry.js +81 -0
- package/node_modules/@ocean/market/dist/install/default-marketplace.js +47 -0
- package/node_modules/@ocean/market/dist/install/lock.js +34 -0
- package/node_modules/@ocean/market/dist/install/marketplace-service.js +165 -0
- package/node_modules/@ocean/market/dist/mcp/browser-bridge.js +263 -0
- package/node_modules/@ocean/market/dist/mcp/mcp-connection.js +67 -0
- package/node_modules/@ocean/market/dist/mcp/mcp-gateway.js +317 -0
- package/node_modules/@ocean/market/dist/mcp/mcp-launch.js +102 -0
- package/node_modules/@ocean/market/dist/mcp/mcp-runner.js +32 -0
- package/node_modules/@ocean/market/dist/security/description-scan.js +45 -0
- package/node_modules/@ocean/market/dist/security/permissions.js +57 -0
- package/node_modules/@ocean/market/dist/security/sandbox.js +74 -0
- package/node_modules/@ocean/market/dist/skills/core-skill-sync.js +113 -0
- package/node_modules/@ocean/market/dist/skills/signal-detection.js +84 -0
- package/node_modules/@ocean/market/dist/skills/skill-curation.js +123 -0
- package/node_modules/@ocean/market/dist/skills/skill-hub.js +220 -0
- package/node_modules/@ocean/market/dist/skills/task-observer.js +165 -0
- package/node_modules/@ocean/market/package.json +11 -0
- package/node_modules/@ocean/services/dist/browser/browser-service.js +124 -0
- package/node_modules/@ocean/services/dist/browser/browser-session.js +287 -0
- package/node_modules/@ocean/services/dist/browser/browser-task-runner.js +70 -0
- package/node_modules/@ocean/services/dist/doctor/workspace-doctor.js +547 -0
- package/node_modules/@ocean/services/dist/infrastructure/mcp/call-ledger.js +114 -0
- package/node_modules/@ocean/services/dist/infrastructure/mcp/mcp-client.js +162 -0
- package/node_modules/@ocean/services/dist/infrastructure/mcp/obsidian-server.js +251 -0
- package/node_modules/@ocean/services/dist/infrastructure/mcp/ocean-server.js +880 -0
- package/node_modules/@ocean/services/dist/infrastructure/mcp/server-version.js +20 -0
- package/node_modules/@ocean/services/dist/infrastructure/mcp/tool-registry.js +29 -0
- package/node_modules/@ocean/services/dist/obsidian/conflict-log.js +68 -0
- package/node_modules/@ocean/services/dist/obsidian/inbox-promotion.js +80 -0
- package/node_modules/@ocean/services/dist/obsidian/vault-discovery.js +113 -0
- package/node_modules/@ocean/services/dist/obsidian/vault-ingestion.js +77 -0
- package/node_modules/@ocean/services/dist/obsidian/vault-sync.js +73 -0
- package/node_modules/@ocean/services/dist/obsidian/vault-writer.js +75 -0
- package/node_modules/@ocean/services/package.json +11 -0
- package/ocean-marketplace.json +415 -0
- package/package.json +67 -4
- package/skills/core/business-logic/SKILL.md +22 -0
- package/skills/core/catch-up/SKILL.md +17 -0
- package/skills/core/core-thinking/SKILL.md +11 -0
- package/skills/core/design-thinking/SKILL.md +19 -0
- package/skills/core/graft/SKILL.md +156 -0
- package/skills/core/loop/SKILL.md +20 -0
- package/skills/core/session-handoff/SKILL.md +15 -0
- package/skills/core/use-browser/SKILL.md +69 -0
- package/skills/core/verification/SKILL.md +11 -0
- package/skills/index.json +56 -0
- package/templates/README.md +4 -0
- package/templates/profiles/default.json +51 -0
- package/templates/profiles/developer.json +51 -0
- package/templates/profiles/reviewer.json +51 -0
- package/templates/profiles/strategist.json +51 -0
- package/templates/workspace/00-inbox/INBOX.md +7 -0
- package/templates/workspace/00-inbox/candidates/.gitkeep +0 -0
- package/templates/workspace/00-inbox/native/.gitkeep +0 -0
- package/templates/workspace/01-daily/.gitkeep +0 -0
- package/templates/workspace/02-personal/CONFLICTS.md +7 -0
- package/templates/workspace/02-personal/MEMORY.md +15 -0
- package/templates/workspace/02-personal/goals/.gitkeep +0 -0
- package/templates/workspace/02-personal/identity/.gitkeep +0 -0
- package/templates/workspace/02-personal/preferences/.gitkeep +0 -0
- package/templates/workspace/03-professional/.gitkeep +0 -0
- package/templates/workspace/04-projects/PROJECTS.md +5 -0
- package/templates/workspace/04-projects/_template/docs/.gitkeep +0 -0
- package/templates/workspace/04-projects/_template/memory/.gitkeep +0 -0
- package/templates/workspace/04-projects/_template/tasks/.gitkeep +0 -0
- package/templates/workspace/05-knowledge/KNOWLEDGE.md +8 -0
- package/templates/workspace/05-knowledge/decisions/.gitkeep +0 -0
- package/templates/workspace/05-knowledge/lessons/.gitkeep +0 -0
- package/templates/workspace/06-templates/TEMPLATES.md +25 -0
- package/templates/workspace/06-templates/decision.md +8 -0
- package/templates/workspace/06-templates/intelligence.md +32 -0
- package/templates/workspace/06-templates/project-claude.md +48 -0
- package/templates/workspace/06-templates/session.md +38 -0
- package/templates/workspace/06-templates/task.md +42 -0
- package/templates/workspace/MANIFEST.json +51 -0
- package/templates/workspace/bridge/config/CONFIG.md +21 -0
- package/templates/workspace/bridge/config/runtime.json +4 -0
- package/templates/workspace/charter/core.md +104 -0
- package/templates/workspace/charter/policies/README.md +31 -0
- package/templates/workspace/charter/policies/claude-code-context.md +90 -0
- package/templates/workspace/charter/policies/concise-output.md +29 -0
- package/templates/workspace/charter/policies/context.md +91 -0
- package/templates/workspace/charter/policies/contracts/task.md +189 -0
- package/templates/workspace/charter/policies/git.md +60 -0
- package/templates/workspace/charter/policies/graph.md +47 -0
- package/templates/workspace/charter/policies/knowledge.md +33 -0
- package/templates/workspace/charter/policies/lifecycle.md +119 -0
- package/templates/workspace/charter/policies/memory.md +67 -0
- package/templates/workspace/charter/policies/minimal-change.md +43 -0
- package/templates/workspace/charter/policies/models.md +57 -0
- package/templates/workspace/charter/policies/privacy-terms.txt +3 -0
- package/templates/workspace/charter/policies/response.md +39 -0
- package/templates/workspace/charter/policies/skill-observer.md +44 -0
- package/templates/workspace/charter/policies/stack.md +23 -0
- package/templates/workspace/charter/policies/task.md +139 -0
- package/templates/workspace/charter/policies/verification.md +29 -0
- package/templates/workspace/sessions/.gitkeep +0 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { redactRuntimeText } from "@ocean/core/domain/redaction/redaction.js";
|
|
5
|
+
import { oceanPath, SYSTEM_DIR } from "@ocean/core/shared/paths.js";
|
|
6
|
+
import { addSkillCandidate } from "./skill-curation.js";
|
|
7
|
+
const observationsFile = () => oceanPath(SYSTEM_DIR, "skills", "observations.json");
|
|
8
|
+
async function load() {
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(await readFile(observationsFile(), "utf8"));
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
if (error.code === "ENOENT")
|
|
14
|
+
return [];
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async function save(items) {
|
|
19
|
+
await mkdir(path.dirname(observationsFile()), { recursive: true });
|
|
20
|
+
await writeFile(observationsFile(), `${JSON.stringify(items.slice(-200), null, 2)}\n`, { mode: 0o600 });
|
|
21
|
+
}
|
|
22
|
+
function observationId(sessionId, signalType, summary) {
|
|
23
|
+
return `obs-${createHash("sha256").update(`${sessionId}:${signalType}:${summary}`).digest("hex").slice(0, 16)}`;
|
|
24
|
+
}
|
|
25
|
+
const CORRECTION_PATTERN = /\bno,|\bactually,|\bnot that\b|\binstead of\b|\bshould use\b|\bthat'?s wrong\b/i;
|
|
26
|
+
function userInputTexts(events) {
|
|
27
|
+
return events
|
|
28
|
+
.filter((event) => event.type === "user_input")
|
|
29
|
+
.map((event) => ({
|
|
30
|
+
id: event.eventId,
|
|
31
|
+
text: redactRuntimeText(event.data).replace(/\s+/g, " ").trim(),
|
|
32
|
+
}))
|
|
33
|
+
.filter((event) => event.text.length > 12)
|
|
34
|
+
.slice(0, 80);
|
|
35
|
+
}
|
|
36
|
+
// True when the user pushed back in this session ("no, use X", "that's wrong").
|
|
37
|
+
export function hasUserCorrection(events) {
|
|
38
|
+
return userInputTexts(events).some((event) => CORRECTION_PATTERN.test(event.text));
|
|
39
|
+
}
|
|
40
|
+
// What a completed, proven session is worth remembering. Pure: reads events, writes nothing.
|
|
41
|
+
export function detectSessionSignals(events) {
|
|
42
|
+
const proven = events.filter((event) => event.type === "evidence" && /"result"\s*:\s*"proven"/.test(event.data));
|
|
43
|
+
if (!proven.length)
|
|
44
|
+
return [];
|
|
45
|
+
const outputs = events
|
|
46
|
+
.filter((event) => ["provider_output", "text", "json"].includes(event.type))
|
|
47
|
+
.map((event) => ({
|
|
48
|
+
id: event.eventId,
|
|
49
|
+
text: redactRuntimeText(event.data).replace(/\s+/g, " ").trim(),
|
|
50
|
+
}))
|
|
51
|
+
.filter((event) => event.text.length > 12)
|
|
52
|
+
.slice(0, 80);
|
|
53
|
+
const userInputs = userInputTexts(events);
|
|
54
|
+
const candidates = [];
|
|
55
|
+
const decisions = outputs.filter((event) => /\bdecision\s*:/i.test(event.text));
|
|
56
|
+
if (decisions.length)
|
|
57
|
+
candidates.push({
|
|
58
|
+
signalType: "explicit-decision",
|
|
59
|
+
summary: decisions[0].text.slice(0, 500),
|
|
60
|
+
refs: decisions.slice(0, 4).map((event) => `event:${event.id}`),
|
|
61
|
+
confidence: 0.9,
|
|
62
|
+
});
|
|
63
|
+
const counts = new Map();
|
|
64
|
+
for (const event of outputs) {
|
|
65
|
+
const normalized = event.text.toLocaleLowerCase();
|
|
66
|
+
const current = counts.get(normalized) ?? { count: 0, refs: [] };
|
|
67
|
+
current.count += 1;
|
|
68
|
+
current.refs.push(`event:${event.id}`);
|
|
69
|
+
counts.set(normalized, current);
|
|
70
|
+
}
|
|
71
|
+
const repeated = [...counts.entries()].find(([, value]) => value.count >= 2);
|
|
72
|
+
if (repeated)
|
|
73
|
+
candidates.push({
|
|
74
|
+
signalType: "repeated-procedure",
|
|
75
|
+
summary: repeated[0].slice(0, 500),
|
|
76
|
+
refs: repeated[1].refs.slice(0, 4),
|
|
77
|
+
confidence: 0.8,
|
|
78
|
+
});
|
|
79
|
+
const corrections = userInputs.filter((event) => CORRECTION_PATTERN.test(event.text));
|
|
80
|
+
if (corrections.length)
|
|
81
|
+
candidates.push({
|
|
82
|
+
signalType: "repeated-correction",
|
|
83
|
+
summary: corrections[0].text.slice(0, 500),
|
|
84
|
+
refs: corrections.slice(0, 4).map((event) => `event:${event.id}`),
|
|
85
|
+
confidence: 0.7,
|
|
86
|
+
});
|
|
87
|
+
return candidates;
|
|
88
|
+
}
|
|
89
|
+
export async function observeSessionWithStore(store, sessionId) {
|
|
90
|
+
const session = store.get(sessionId);
|
|
91
|
+
if (!session)
|
|
92
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
93
|
+
if (session.status !== "completed")
|
|
94
|
+
return [];
|
|
95
|
+
const events = store.listEvents(sessionId);
|
|
96
|
+
const candidates = detectSessionSignals(events);
|
|
97
|
+
const items = await load();
|
|
98
|
+
const created = [];
|
|
99
|
+
for (const candidate of candidates.slice(0, 4)) {
|
|
100
|
+
const id = observationId(sessionId, candidate.signalType, candidate.summary);
|
|
101
|
+
if (items.some((item) => item.observationId === id))
|
|
102
|
+
continue;
|
|
103
|
+
created.push({
|
|
104
|
+
observationId: id,
|
|
105
|
+
sourceSessionId: sessionId,
|
|
106
|
+
taskId: session.taskId,
|
|
107
|
+
profileId: session.profile,
|
|
108
|
+
signalType: candidate.signalType,
|
|
109
|
+
summary: candidate.summary,
|
|
110
|
+
evidenceRefs: candidate.refs,
|
|
111
|
+
confidence: candidate.confidence,
|
|
112
|
+
status: "observed",
|
|
113
|
+
createdAt: new Date().toISOString(),
|
|
114
|
+
reviewedAt: null,
|
|
115
|
+
skillCandidateId: null,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
if (created.length)
|
|
119
|
+
await save([...items, ...created]);
|
|
120
|
+
return created;
|
|
121
|
+
}
|
|
122
|
+
export async function observeSession(sessionId, openStore) {
|
|
123
|
+
const store = await openStore();
|
|
124
|
+
try {
|
|
125
|
+
return await observeSessionWithStore(store, sessionId);
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
store.close();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
export async function listObservations() {
|
|
132
|
+
return load();
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Approving an observation creates the skill candidate it was promising —
|
|
136
|
+
* reviewObservation used to only flip a status flag with nothing downstream.
|
|
137
|
+
* A duplicate or invalid candidate (see addSkillCandidate) leaves the
|
|
138
|
+
* approval standing but skillCandidateId null; the summary is short and
|
|
139
|
+
* redacted, so this should be rare.
|
|
140
|
+
*/
|
|
141
|
+
export async function reviewObservation(observationIdValue, status) {
|
|
142
|
+
const items = await load();
|
|
143
|
+
const item = items.find((candidate) => candidate.observationId === observationIdValue);
|
|
144
|
+
if (!item)
|
|
145
|
+
throw new Error(`Observation not found: ${observationIdValue}`);
|
|
146
|
+
item.status = status;
|
|
147
|
+
item.reviewedAt = new Date().toISOString();
|
|
148
|
+
if (status === "approved" && !item.skillCandidateId) {
|
|
149
|
+
try {
|
|
150
|
+
const candidate = await addSkillCandidate({
|
|
151
|
+
id: `learned-${item.observationId.slice(4, 16)}`,
|
|
152
|
+
name: `Learned: ${item.signalType} (${item.observationId.slice(4, 10)})`,
|
|
153
|
+
instructions: item.summary,
|
|
154
|
+
sourceSessionId: item.sourceSessionId,
|
|
155
|
+
});
|
|
156
|
+
item.skillCandidateId = candidate.id;
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
// Duplicate id/name or invalid content — approval stands, no candidate.
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
await save(items);
|
|
163
|
+
return item;
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=task-observer.js.map
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Enforces the browser capability's authority/approval policy and drives live
|
|
2
|
+
// verification. This is where operation gating lives — providers only execute what
|
|
3
|
+
// they are asked and report what happened; they never decide whether an action was
|
|
4
|
+
// approved or verified.
|
|
5
|
+
import { browserContracts, } from "@ocean/core/domain/capabilities/browser-contract.js";
|
|
6
|
+
export class BrowserApprovalRequiredError extends Error {
|
|
7
|
+
constructor(operation, reason) {
|
|
8
|
+
super(`Browser operation '${operation}' requires owner approval: ${reason}`);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function isExternalOrigin(currentUrl, targetUrl) {
|
|
12
|
+
if (currentUrl === "about:blank")
|
|
13
|
+
return false;
|
|
14
|
+
try {
|
|
15
|
+
return new URL(currentUrl).origin !== new URL(targetUrl).origin;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export class BrowserService {
|
|
22
|
+
provider;
|
|
23
|
+
constructor(provider) {
|
|
24
|
+
this.provider = provider;
|
|
25
|
+
}
|
|
26
|
+
detect() {
|
|
27
|
+
return this.provider.detect();
|
|
28
|
+
}
|
|
29
|
+
launch(profileDir, port) {
|
|
30
|
+
return this.provider.launch(profileDir, port);
|
|
31
|
+
}
|
|
32
|
+
connect(launch) {
|
|
33
|
+
return this.provider.connect(launch);
|
|
34
|
+
}
|
|
35
|
+
close(launch) {
|
|
36
|
+
return this.provider.close(launch);
|
|
37
|
+
}
|
|
38
|
+
requireApproval(operation, approved, reason) {
|
|
39
|
+
const contract = browserContracts[operation];
|
|
40
|
+
if (contract.approval === "required" && !approved)
|
|
41
|
+
throw new BrowserApprovalRequiredError(operation, reason);
|
|
42
|
+
}
|
|
43
|
+
async navigate(handle, url, options = {}) {
|
|
44
|
+
const before = await handle.state();
|
|
45
|
+
const crossOrigin = isExternalOrigin(before.url, url);
|
|
46
|
+
if (crossOrigin && !options.approved)
|
|
47
|
+
throw new BrowserApprovalRequiredError("navigate", `destination origin differs from current origin (${before.url} -> ${url})`);
|
|
48
|
+
const result = await handle.navigate(url, options.timeoutMs);
|
|
49
|
+
const verified = result.url === url || result.url.startsWith(url);
|
|
50
|
+
return {
|
|
51
|
+
operation: "navigate",
|
|
52
|
+
approved: crossOrigin ? Boolean(options.approved) : true,
|
|
53
|
+
verified,
|
|
54
|
+
result,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
async upload(handle, selector, paths, options = {}) {
|
|
58
|
+
this.requireApproval("upload", Boolean(options.approved), "attaching local files to a page is irreversible from Ocean's point of view");
|
|
59
|
+
const result = await handle.upload(selector, paths);
|
|
60
|
+
return {
|
|
61
|
+
operation: "upload",
|
|
62
|
+
approved: true,
|
|
63
|
+
verified: result.attached.length === paths.length,
|
|
64
|
+
result,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
async download(handle, selector, destinationDir, options = {}) {
|
|
68
|
+
this.requireApproval("download", Boolean(options.approved), "saving a file to disk is irreversible from Ocean's point of view");
|
|
69
|
+
const result = await handle.download(selector, destinationDir, options.timeoutMs);
|
|
70
|
+
return {
|
|
71
|
+
operation: "download",
|
|
72
|
+
approved: true,
|
|
73
|
+
verified: result.size > 0,
|
|
74
|
+
result,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
async submit(handle, selector, options = {}) {
|
|
78
|
+
this.requireApproval("submit", Boolean(options.approved), "submitting a form is irreversible from Ocean's point of view");
|
|
79
|
+
const result = await handle.submit(selector, options.timeoutMs);
|
|
80
|
+
const verified = result.url !== result.urlBefore || result.bodyAfter !== result.bodyBefore;
|
|
81
|
+
return { operation: "submit", approved: true, verified, result };
|
|
82
|
+
}
|
|
83
|
+
async click(handle, selector, expectation, timeoutMs) {
|
|
84
|
+
const result = await handle.click(selector, timeoutMs);
|
|
85
|
+
let verified;
|
|
86
|
+
if (expectation.kind === "navigates")
|
|
87
|
+
verified = result.url !== result.urlBefore;
|
|
88
|
+
else if (expectation.kind === "stays")
|
|
89
|
+
verified = result.url === result.urlBefore;
|
|
90
|
+
else
|
|
91
|
+
verified =
|
|
92
|
+
(await handle.extract(expectation.selector, null)).count ===
|
|
93
|
+
expectation.expected;
|
|
94
|
+
return { operation: "click", approved: true, verified, result };
|
|
95
|
+
}
|
|
96
|
+
async type(handle, selector, text, clear) {
|
|
97
|
+
const result = await handle.type(selector, text, clear);
|
|
98
|
+
return {
|
|
99
|
+
operation: "type",
|
|
100
|
+
approved: true,
|
|
101
|
+
verified: result.value.includes(text) || !clear,
|
|
102
|
+
result,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
async select(handle, selector, value) {
|
|
106
|
+
const result = await handle.select(selector, value);
|
|
107
|
+
return {
|
|
108
|
+
operation: "select",
|
|
109
|
+
approved: true,
|
|
110
|
+
verified: result.value === value,
|
|
111
|
+
result,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
async replaceText(handle, selector, oldText, newText, occurrence = 1) {
|
|
115
|
+
const result = await handle.replaceText(selector, oldText, newText, occurrence);
|
|
116
|
+
return {
|
|
117
|
+
operation: "replace-text",
|
|
118
|
+
approved: true,
|
|
119
|
+
verified: result.value.includes(newText),
|
|
120
|
+
result,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=browser-service.js.map
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir } from "node:fs/promises";
|
|
3
|
+
import { validateSessionEntryContract } from "@ocean/core/domain/sessions/entry-contract.js";
|
|
4
|
+
import { oceanPath, oceanRoot, SYSTEM_DIR } from "@ocean/core/shared/paths.js";
|
|
5
|
+
import { sessionProjectId } from "@ocean/loop/sessions/session-projects.js";
|
|
6
|
+
function safeProfileKey(value) {
|
|
7
|
+
const key = value.trim();
|
|
8
|
+
if (!key ||
|
|
9
|
+
key === "." ||
|
|
10
|
+
key === ".." ||
|
|
11
|
+
!/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(key)) {
|
|
12
|
+
throw new Error("Browser profile key must contain only letters, numbers, hyphens, and underscores.");
|
|
13
|
+
}
|
|
14
|
+
return key;
|
|
15
|
+
}
|
|
16
|
+
function normalizeBrowserUrl(value) {
|
|
17
|
+
const trimmed = value.trim();
|
|
18
|
+
const markdown = trimmed.match(/^\[([^\]]+)\]\([^)]*\)$/);
|
|
19
|
+
return (markdown?.[1] ?? trimmed).trim();
|
|
20
|
+
}
|
|
21
|
+
function parseResumeData(session) {
|
|
22
|
+
if (session.provider !== "browser" || !session.resumeData)
|
|
23
|
+
throw new Error(`Browser session is not reconnectable: ${session.sessionId}`);
|
|
24
|
+
try {
|
|
25
|
+
const value = JSON.parse(session.resumeData);
|
|
26
|
+
if (!value.profileDir ||
|
|
27
|
+
value.port === undefined ||
|
|
28
|
+
value.pid === undefined ||
|
|
29
|
+
value.provider !== "playwright")
|
|
30
|
+
throw new Error("incomplete browser metadata");
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
throw new Error(`Browser session metadata is invalid: ${session.sessionId}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export class BrowserSessionManager {
|
|
38
|
+
store;
|
|
39
|
+
service;
|
|
40
|
+
constructor(store, service) {
|
|
41
|
+
this.store = store;
|
|
42
|
+
this.service = service;
|
|
43
|
+
}
|
|
44
|
+
async detect() {
|
|
45
|
+
return this.service.detect();
|
|
46
|
+
}
|
|
47
|
+
async open(profileKey = "default", port) {
|
|
48
|
+
const key = safeProfileKey(profileKey);
|
|
49
|
+
const existing = this.store.list().find((session) => {
|
|
50
|
+
if (session.provider !== "browser" || session.status !== "running")
|
|
51
|
+
return false;
|
|
52
|
+
try {
|
|
53
|
+
return parseResumeData(session).profileKey === key;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
if (existing) {
|
|
60
|
+
this.store.appendEvent(existing.sessionId, "browser_reused", JSON.stringify({
|
|
61
|
+
profileKey: key,
|
|
62
|
+
reason: "running-session-for-profile",
|
|
63
|
+
}));
|
|
64
|
+
return existing;
|
|
65
|
+
}
|
|
66
|
+
const recoverable = this.store.list().find((session) => {
|
|
67
|
+
if (session.provider !== "browser" || session.status !== "failed")
|
|
68
|
+
return false;
|
|
69
|
+
try {
|
|
70
|
+
return parseResumeData(session).profileKey === key;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
if (recoverable) {
|
|
77
|
+
const resume = parseResumeData(recoverable);
|
|
78
|
+
try {
|
|
79
|
+
const handle = await this.service.connect(resume);
|
|
80
|
+
await handle.release();
|
|
81
|
+
this.store.updateStatus(recoverable.sessionId, "running");
|
|
82
|
+
this.store.appendEvent(recoverable.sessionId, "browser_recovered", JSON.stringify({ profileKey: key, reason: "endpoint-reachable" }));
|
|
83
|
+
return this.requireSession(recoverable.sessionId);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Stale failed session; continue with a new launch below.
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const sessionId = randomUUID();
|
|
90
|
+
const profileDir = oceanPath(SYSTEM_DIR, "browser", "profiles", key);
|
|
91
|
+
await mkdir(profileDir, { recursive: true });
|
|
92
|
+
this.store.create({
|
|
93
|
+
sessionId,
|
|
94
|
+
provider: "browser",
|
|
95
|
+
providerSessionId: `browser:${key}:${sessionId}`,
|
|
96
|
+
parentSessionId: null,
|
|
97
|
+
profile: "browser",
|
|
98
|
+
profileIdentity: "browser-v1",
|
|
99
|
+
workingDirectory: oceanRoot(),
|
|
100
|
+
projectId: await sessionProjectId(oceanRoot()),
|
|
101
|
+
resumeData: null,
|
|
102
|
+
});
|
|
103
|
+
this.store.appendEvent(sessionId, "session_entry_contract", JSON.stringify(validateSessionEntryContract({
|
|
104
|
+
entryPoint: "ocean-run",
|
|
105
|
+
controlLevel: "full-head",
|
|
106
|
+
inputCapture: "semantic",
|
|
107
|
+
contextTransport: "browser-capability-contract",
|
|
108
|
+
policyEnforcement: "browser-approval-and-post-condition-contract",
|
|
109
|
+
promotion: "explicit-review",
|
|
110
|
+
resume: "browser-profile-session",
|
|
111
|
+
})));
|
|
112
|
+
// Browser startup is a provider run. Mark it running before launching so a
|
|
113
|
+
// launch failure can transition legally to failed without weakening the
|
|
114
|
+
// global session state machine with created -> failed.
|
|
115
|
+
this.store.updateStatus(sessionId, "running");
|
|
116
|
+
try {
|
|
117
|
+
const launch = await this.service.launch(profileDir, port);
|
|
118
|
+
const resumeData = {
|
|
119
|
+
...launch,
|
|
120
|
+
profileKey: key,
|
|
121
|
+
provider: "playwright",
|
|
122
|
+
};
|
|
123
|
+
this.store.updateStatus(sessionId, "running", JSON.stringify(resumeData));
|
|
124
|
+
this.store.appendEvent(sessionId, "browser_opened", JSON.stringify({ profileKey: key, port: launch.port, pid: launch.pid }));
|
|
125
|
+
return this.requireSession(sessionId);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
this.store.updateStatus(sessionId, "failed");
|
|
129
|
+
this.store.appendEvent(sessionId, "error", error instanceof Error ? error.message : String(error));
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
show(sessionId) {
|
|
134
|
+
return this.requireSession(sessionId);
|
|
135
|
+
}
|
|
136
|
+
events(sessionId) {
|
|
137
|
+
this.requireSession(sessionId);
|
|
138
|
+
return this.store.listEvents(sessionId);
|
|
139
|
+
}
|
|
140
|
+
async close(sessionId) {
|
|
141
|
+
const session = this.requireSession(sessionId);
|
|
142
|
+
if (session.status === "completed")
|
|
143
|
+
return session;
|
|
144
|
+
const launch = parseResumeData(session);
|
|
145
|
+
try {
|
|
146
|
+
await this.service.close(launch);
|
|
147
|
+
this.store.updateStatus(sessionId, "completed");
|
|
148
|
+
this.store.appendEvent(sessionId, "browser_closed", JSON.stringify({ pid: launch.pid, port: launch.port }));
|
|
149
|
+
return this.requireSession(sessionId);
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
this.store.updateStatus(sessionId, "failed");
|
|
153
|
+
this.store.appendEvent(sessionId, "error", error instanceof Error ? error.message : String(error));
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async approve(sessionId) {
|
|
158
|
+
const session = this.requireSession(sessionId, false);
|
|
159
|
+
const resume = parseResumeData(session);
|
|
160
|
+
this.store.updateStatus(sessionId, session.status, JSON.stringify({ ...resume, approved: true }));
|
|
161
|
+
this.store.appendEvent(sessionId, "browser_session_approved", JSON.stringify({ scope: "session", expires: "on-close" }));
|
|
162
|
+
return this.requireSession(sessionId);
|
|
163
|
+
}
|
|
164
|
+
async navigate(sessionId, url, approved, timeoutMs) {
|
|
165
|
+
const normalizedUrl = normalizeBrowserUrl(url);
|
|
166
|
+
return this.withHandle(sessionId, "navigate", (handle) => this.service.navigate(handle, normalizedUrl, {
|
|
167
|
+
approved: approved || this.isApproved(sessionId),
|
|
168
|
+
timeoutMs,
|
|
169
|
+
}));
|
|
170
|
+
}
|
|
171
|
+
async read(sessionId) {
|
|
172
|
+
return this.withHandle(sessionId, "read", (handle) => handle.readText());
|
|
173
|
+
}
|
|
174
|
+
async observe(sessionId) {
|
|
175
|
+
return this.withHandle(sessionId, "observe", (handle) => handle.observe());
|
|
176
|
+
}
|
|
177
|
+
async extract(sessionId, selector, attribute) {
|
|
178
|
+
return this.withHandle(sessionId, "extract", (handle) => handle.extract(selector, attribute));
|
|
179
|
+
}
|
|
180
|
+
async click(sessionId, selector, expectation, timeoutMs) {
|
|
181
|
+
return this.withHandle(sessionId, "click", (handle) => this.service.click(handle, selector, expectation, timeoutMs));
|
|
182
|
+
}
|
|
183
|
+
async type(sessionId, selector, text, clear) {
|
|
184
|
+
return this.withHandle(sessionId, "type", (handle) => this.service.type(handle, selector, text, clear));
|
|
185
|
+
}
|
|
186
|
+
async select(sessionId, selector, value) {
|
|
187
|
+
return this.withHandle(sessionId, "select", (handle) => this.service.select(handle, selector, value));
|
|
188
|
+
}
|
|
189
|
+
async replaceText(sessionId, selector, oldText, newText, occurrence) {
|
|
190
|
+
return this.withHandle(sessionId, "replace-text", (handle) => this.service.replaceText(handle, selector, oldText, newText, occurrence));
|
|
191
|
+
}
|
|
192
|
+
async scroll(sessionId, deltaY) {
|
|
193
|
+
return this.withHandle(sessionId, "scroll", (handle) => handle.scroll(deltaY));
|
|
194
|
+
}
|
|
195
|
+
async wait(sessionId, options) {
|
|
196
|
+
return this.withHandle(sessionId, "wait", (handle) => handle.wait(options));
|
|
197
|
+
}
|
|
198
|
+
async upload(sessionId, selector, paths, approved) {
|
|
199
|
+
return this.withHandle(sessionId, "upload", (handle) => this.service.upload(handle, selector, paths, {
|
|
200
|
+
approved: approved || this.isApproved(sessionId),
|
|
201
|
+
}));
|
|
202
|
+
}
|
|
203
|
+
async download(sessionId, selector, destinationDir, approved, timeoutMs) {
|
|
204
|
+
return this.withHandle(sessionId, "download", (handle) => this.service.download(handle, selector, destinationDir, {
|
|
205
|
+
approved: approved || this.isApproved(sessionId),
|
|
206
|
+
timeoutMs,
|
|
207
|
+
}));
|
|
208
|
+
}
|
|
209
|
+
async submit(sessionId, selector, approved, timeoutMs) {
|
|
210
|
+
return this.withHandle(sessionId, "submit", (handle) => this.service.submit(handle, selector, {
|
|
211
|
+
approved: approved || this.isApproved(sessionId),
|
|
212
|
+
timeoutMs,
|
|
213
|
+
}));
|
|
214
|
+
}
|
|
215
|
+
async withHandle(sessionId, operationName, operation) {
|
|
216
|
+
const session = this.requireSession(sessionId, false);
|
|
217
|
+
const launch = parseResumeData(session);
|
|
218
|
+
let handle;
|
|
219
|
+
try {
|
|
220
|
+
try {
|
|
221
|
+
handle = await this.service.connect(launch);
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
this.store.appendEvent(sessionId, "browser_unreachable", JSON.stringify({
|
|
225
|
+
error: error instanceof Error ? error.message : String(error),
|
|
226
|
+
}));
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
const result = await operation(handle);
|
|
230
|
+
this.store.appendEvent(sessionId, "browser_operation", JSON.stringify({
|
|
231
|
+
operation: operationName,
|
|
232
|
+
result: summarizeResult(result),
|
|
233
|
+
}));
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
this.store.appendEvent(sessionId, "browser_operation_failed", JSON.stringify({
|
|
238
|
+
error: error instanceof Error ? error.message : String(error),
|
|
239
|
+
}));
|
|
240
|
+
throw error;
|
|
241
|
+
}
|
|
242
|
+
finally {
|
|
243
|
+
await handle?.release().catch(() => undefined);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
isApproved(sessionId) {
|
|
247
|
+
const session = this.requireSession(sessionId, false);
|
|
248
|
+
try {
|
|
249
|
+
return parseResumeData(session).approved === true;
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
requireSession(sessionId, allowCompleted = true) {
|
|
256
|
+
const session = this.store.get(sessionId);
|
|
257
|
+
if (!session)
|
|
258
|
+
throw new Error(`Browser session not found: ${sessionId}`);
|
|
259
|
+
if (session.provider !== "browser")
|
|
260
|
+
throw new Error(`Session is not a browser session: ${sessionId}`);
|
|
261
|
+
if (session.status !== "running" &&
|
|
262
|
+
(!allowCompleted || session.status !== "completed"))
|
|
263
|
+
throw new Error(`Browser session is not available: ${session.status}`);
|
|
264
|
+
return session;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function summarizeResult(value) {
|
|
268
|
+
if (!value || typeof value !== "object")
|
|
269
|
+
return value;
|
|
270
|
+
const result = value;
|
|
271
|
+
return {
|
|
272
|
+
operation: result.operation,
|
|
273
|
+
approved: result.approved,
|
|
274
|
+
verified: result.verified,
|
|
275
|
+
result: result.result && typeof result.result === "object"
|
|
276
|
+
? {
|
|
277
|
+
url: result.result.url,
|
|
278
|
+
title: result.result.title,
|
|
279
|
+
size: result.result.size,
|
|
280
|
+
}
|
|
281
|
+
: undefined,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
export function browserDownloadsPath(sessionId) {
|
|
285
|
+
return oceanPath(SYSTEM_DIR, "browser", "downloads", sessionId);
|
|
286
|
+
}
|
|
287
|
+
//# sourceMappingURL=browser-session.js.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
export class BrowserTaskRunner {
|
|
3
|
+
manager;
|
|
4
|
+
constructor(manager) {
|
|
5
|
+
this.manager = manager;
|
|
6
|
+
}
|
|
7
|
+
async run(sessionId, task, approved = false) {
|
|
8
|
+
if (!Array.isArray(task.steps) || task.steps.length === 0)
|
|
9
|
+
throw new Error("Browser task must contain at least one step.");
|
|
10
|
+
if (task.steps.length > 100)
|
|
11
|
+
throw new Error("Browser task exceeds the 100-step limit.");
|
|
12
|
+
const retries = Math.max(0, Math.min(3, task.retries ?? 0));
|
|
13
|
+
const results = [];
|
|
14
|
+
for (const step of task.steps) {
|
|
15
|
+
let lastError;
|
|
16
|
+
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
|
17
|
+
try {
|
|
18
|
+
const result = await this.execute(sessionId, step, approved);
|
|
19
|
+
if (isUnverified(result))
|
|
20
|
+
throw new Error(`Browser step was not verified: ${step.action}`);
|
|
21
|
+
results.push(result);
|
|
22
|
+
lastError = undefined;
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
lastError = error;
|
|
27
|
+
if (attempt < retries)
|
|
28
|
+
await new Promise((resolve) => setTimeout(resolve, 100 * (attempt + 1)));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (lastError)
|
|
32
|
+
throw lastError;
|
|
33
|
+
}
|
|
34
|
+
return { completed: true, steps: results.length, results };
|
|
35
|
+
}
|
|
36
|
+
static async fromFile(path) {
|
|
37
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
38
|
+
}
|
|
39
|
+
execute(sessionId, step, approved) {
|
|
40
|
+
switch (step.action) {
|
|
41
|
+
case "navigate":
|
|
42
|
+
return this.manager.navigate(sessionId, step.url, approved);
|
|
43
|
+
case "click":
|
|
44
|
+
return this.manager.click(sessionId, step.selector, {
|
|
45
|
+
kind: step.expect ?? "stays",
|
|
46
|
+
});
|
|
47
|
+
case "type":
|
|
48
|
+
return this.manager.type(sessionId, step.selector, step.text, step.clear ?? true);
|
|
49
|
+
case "select":
|
|
50
|
+
return this.manager.select(sessionId, step.selector, step.value);
|
|
51
|
+
case "scroll":
|
|
52
|
+
return this.manager.scroll(sessionId, step.deltaY ?? 600);
|
|
53
|
+
case "wait":
|
|
54
|
+
return this.manager.wait(sessionId, step);
|
|
55
|
+
case "read":
|
|
56
|
+
return this.manager.read(sessionId);
|
|
57
|
+
case "extract":
|
|
58
|
+
return this.manager.extract(sessionId, step.selector, step.attribute ?? null);
|
|
59
|
+
case "submit":
|
|
60
|
+
return this.manager.submit(sessionId, step.selector, approved);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function isUnverified(value) {
|
|
65
|
+
return Boolean(value &&
|
|
66
|
+
typeof value === "object" &&
|
|
67
|
+
"verified" in value &&
|
|
68
|
+
value.verified === false);
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=browser-task-runner.js.map
|