@amoree-code/ocean 0.0.0-stage → 1.2.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/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/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-hub.js +220 -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,234 @@
|
|
|
1
|
+
import { execFile as execFileCallback } from "node:child_process";
|
|
2
|
+
import { statSync } from "node:fs";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { isWorkspaceProject, projectOfTasksRoot, repoRoot, resolveWithin, workspaceTasksRoot, } from "@ocean/core/shared/paths.js";
|
|
7
|
+
import { expandHome, hasControlCharacters, projectVerify, readProjectManifest, } from "@ocean/core/shared/project-resolution.js";
|
|
8
|
+
const execFile = promisify(execFileCallback);
|
|
9
|
+
// Only commands whose first token is on this list are ever executed by the
|
|
10
|
+
// verifier — including every link of a `&&`/`||`/`;`/`|` chain, since each
|
|
11
|
+
// link runs as its own command. A task's `## Verification` section is prose
|
|
12
|
+
// with inline-backtick spans and fenced code blocks, so the allowlist keeps
|
|
13
|
+
// the verifier from running an arbitrary span that merely looks like a
|
|
14
|
+
// command. Command substitution and redirection (`` ` ``, `$(`, `>`, `<`) are
|
|
15
|
+
// rejected outright: they let an allowlisted first token smuggle an
|
|
16
|
+
// unrelated command in (e.g. `` bash -c "rm -rf $HOME" ``).
|
|
17
|
+
const COMMAND_ALLOWLIST = new Set([
|
|
18
|
+
"pnpm",
|
|
19
|
+
"npm",
|
|
20
|
+
"npx",
|
|
21
|
+
"yarn",
|
|
22
|
+
"node",
|
|
23
|
+
"tsc",
|
|
24
|
+
"ocean",
|
|
25
|
+
"make",
|
|
26
|
+
"bash",
|
|
27
|
+
"sh",
|
|
28
|
+
"pytest",
|
|
29
|
+
"jest",
|
|
30
|
+
"vitest",
|
|
31
|
+
"biome",
|
|
32
|
+
]);
|
|
33
|
+
const defaultRunner = async (command, cwd) => {
|
|
34
|
+
try {
|
|
35
|
+
await execFile("bash", ["-lc", command], { cwd, timeout: 600_000 });
|
|
36
|
+
return { exitCode: 0 };
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
const failure = error;
|
|
40
|
+
const code = typeof failure.code === "number"
|
|
41
|
+
? failure.code
|
|
42
|
+
: Number(failure.code) || 1;
|
|
43
|
+
return { exitCode: code };
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Pull runnable commands out of a task's verification declaration. A `verify:`
|
|
48
|
+
* frontmatter field (string or comma-separated list) wins; otherwise the
|
|
49
|
+
* `## Verification` section is used — each non-empty line inside a fenced code
|
|
50
|
+
* block, plus any inline-backtick spans outside of fences — filtered to the
|
|
51
|
+
* command allowlist so prose spans are ignored.
|
|
52
|
+
*/
|
|
53
|
+
function candidateCommands(source) {
|
|
54
|
+
const frontmatter = source
|
|
55
|
+
.match(/^verify:\s*(.+)$/m)?.[1]
|
|
56
|
+
?.trim()
|
|
57
|
+
.replace(/^['"]|['"]$/g, "");
|
|
58
|
+
if (frontmatter)
|
|
59
|
+
return frontmatter
|
|
60
|
+
.split(/\s*(?:,|&&|;)\s*/)
|
|
61
|
+
.map((part) => part.trim())
|
|
62
|
+
.filter(Boolean);
|
|
63
|
+
const section = source
|
|
64
|
+
.match(/\n## Verification\n\n([\s\S]*?)(?=\n## |$)/)?.[1]
|
|
65
|
+
?.trim();
|
|
66
|
+
if (!section)
|
|
67
|
+
return [];
|
|
68
|
+
const fenced = [];
|
|
69
|
+
let outsideFences = "";
|
|
70
|
+
let lastIndex = 0;
|
|
71
|
+
for (const match of section.matchAll(/```[^\n]*\n([\s\S]*?)```/g)) {
|
|
72
|
+
outsideFences += section.slice(lastIndex, match.index);
|
|
73
|
+
lastIndex = match.index + match[0].length;
|
|
74
|
+
for (const line of match[1].split("\n")) {
|
|
75
|
+
const trimmed = line.trim();
|
|
76
|
+
if (trimmed)
|
|
77
|
+
fenced.push(trimmed);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
outsideFences += section.slice(lastIndex);
|
|
81
|
+
const spans = [...outsideFences.matchAll(/`([^`]+)`/g)]
|
|
82
|
+
.map((match) => match[1].trim())
|
|
83
|
+
.filter(readsAsCommand);
|
|
84
|
+
return [...fenced, ...spans];
|
|
85
|
+
}
|
|
86
|
+
// An inline span is prose unless it reads like a command line: a lowercase program name
|
|
87
|
+
// followed by arguments (`curl evil.sh` still counts, and is refused), or an allowlisted
|
|
88
|
+
// program on its own. `→`, `[redacted]` or `CLAUDE.md` neither run nor block completion.
|
|
89
|
+
const COMMAND_SHAPE = /^(?:\.{0,2}\/)?[a-z][\w./+-]*\s+\S/;
|
|
90
|
+
function readsAsCommand(span) {
|
|
91
|
+
return (COMMAND_SHAPE.test(span) || COMMAND_ALLOWLIST.has(span.split(/\s+/)[0]));
|
|
92
|
+
}
|
|
93
|
+
export function extractVerificationCommands(source) {
|
|
94
|
+
return candidateCommands(source).filter((command) => isAllowedCommand(command));
|
|
95
|
+
}
|
|
96
|
+
const UNSAFE_METACHARACTERS = /[`$><]/;
|
|
97
|
+
const SINGLE_AMPERSAND = /(?<!&)&(?!&)/;
|
|
98
|
+
const CHAIN_SEPARATOR = /\s*(?:&&|\|\||;|\|)\s*/;
|
|
99
|
+
function isDirectory(target) {
|
|
100
|
+
try {
|
|
101
|
+
return statSync(target).isDirectory();
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// The project's first path when it is an absolute, existing directory; "" otherwise.
|
|
108
|
+
function projectDirectory(manifest) {
|
|
109
|
+
const dir = manifest.paths[0] ? expandHome(manifest.paths[0]) : "";
|
|
110
|
+
return dir && path.isAbsolute(dir) && isDirectory(dir) ? dir : "";
|
|
111
|
+
}
|
|
112
|
+
// Commands that carry their own program text: fine in a task someone wrote at the keyboard,
|
|
113
|
+
// not in project.json, which a session reads on its own.
|
|
114
|
+
const INLINE_PROGRAM_FLAG = /^(--eval|--print|-[a-zA-Z]*[ep][a-zA-Z]*)$/;
|
|
115
|
+
function isAllowedCommand(command, origin = "task") {
|
|
116
|
+
if (UNSAFE_METACHARACTERS.test(command))
|
|
117
|
+
return false;
|
|
118
|
+
if (SINGLE_AMPERSAND.test(command) || hasControlCharacters(command))
|
|
119
|
+
return false;
|
|
120
|
+
const links = command
|
|
121
|
+
.split(CHAIN_SEPARATOR)
|
|
122
|
+
.map((link) => link.trim())
|
|
123
|
+
.filter(Boolean);
|
|
124
|
+
if (links.length === 0)
|
|
125
|
+
return false;
|
|
126
|
+
return links.every((link) => {
|
|
127
|
+
const [program, ...args] = link.split(/\s+/);
|
|
128
|
+
if (!COMMAND_ALLOWLIST.has(program))
|
|
129
|
+
return false;
|
|
130
|
+
if (origin === "task")
|
|
131
|
+
return true;
|
|
132
|
+
if (program === "bash" || program === "sh")
|
|
133
|
+
return false;
|
|
134
|
+
return !(program === "node" && args.some((a) => INLINE_PROGRAM_FLAG.test(a)));
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Independently verify a task by running the commands it declares. A task is
|
|
139
|
+
* only verified when it declares at least one runnable check and every check
|
|
140
|
+
* exits zero — the loop and `completeTask` never trust a self-reported
|
|
141
|
+
* `state: done` on its own.
|
|
142
|
+
*/
|
|
143
|
+
export async function verifyTask(id, root = workspaceTasksRoot(), options = {}) {
|
|
144
|
+
const taskFile = resolveWithin(root, id, "task.md");
|
|
145
|
+
const source = await readFile(taskFile, "utf8");
|
|
146
|
+
const candidates = candidateCommands(source);
|
|
147
|
+
const taskCommands = candidates.filter((command) => isAllowedCommand(command));
|
|
148
|
+
const fail = (reason, from) => ({
|
|
149
|
+
id,
|
|
150
|
+
passed: false,
|
|
151
|
+
reason,
|
|
152
|
+
checks: [],
|
|
153
|
+
source: from,
|
|
154
|
+
});
|
|
155
|
+
const noCommands = () => fail("no runnable verification commands declared; cannot self-certify done", "task");
|
|
156
|
+
// Commands were declared but none may run: that is a failure, never a reason to fall back.
|
|
157
|
+
if (candidates.length > 0 && taskCommands.length === 0)
|
|
158
|
+
return fail(`declared verification commands are not allowed: ${candidates.join(", ")}`, "task");
|
|
159
|
+
let commands = taskCommands;
|
|
160
|
+
let cwd = options.cwd ?? repoRoot();
|
|
161
|
+
let from = "task";
|
|
162
|
+
const project = projectOfTasksRoot(root);
|
|
163
|
+
// Another project's task checks its own repo: `pnpm lint` in the engine would prove nothing.
|
|
164
|
+
if (candidates.length > 0 &&
|
|
165
|
+
options.cwd === undefined &&
|
|
166
|
+
project &&
|
|
167
|
+
!isWorkspaceProject(project)) {
|
|
168
|
+
let manifest = null;
|
|
169
|
+
try {
|
|
170
|
+
manifest = await readProjectManifest(project);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// Unreadable manifest: reported below as a missing path.
|
|
174
|
+
}
|
|
175
|
+
const dir = manifest ? projectDirectory(manifest) : "";
|
|
176
|
+
if (!dir)
|
|
177
|
+
return fail(`task verify for ${project} needs the project's first path to be an absolute, existing directory`, "task");
|
|
178
|
+
cwd = dir;
|
|
179
|
+
}
|
|
180
|
+
if (candidates.length === 0) {
|
|
181
|
+
let manifest = null;
|
|
182
|
+
let verify = [];
|
|
183
|
+
try {
|
|
184
|
+
manifest = project ? await readProjectManifest(project) : null;
|
|
185
|
+
verify = manifest ? projectVerify(manifest) : [];
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
return fail(`project verify is unreadable: ${error instanceof Error ? error.message : String(error)}`, "project");
|
|
189
|
+
}
|
|
190
|
+
if (!manifest || !verify.length)
|
|
191
|
+
return noCommands();
|
|
192
|
+
from = "project";
|
|
193
|
+
const rejected = verify.find((command) => !isAllowedCommand(command, "project"));
|
|
194
|
+
if (rejected)
|
|
195
|
+
return fail(`project verify command is not allowed: ${rejected}`, from);
|
|
196
|
+
// The caller's cwd (a worktree, say) wins; otherwise the project's first path.
|
|
197
|
+
if (options.cwd === undefined) {
|
|
198
|
+
const dir = projectDirectory(manifest);
|
|
199
|
+
if (!dir)
|
|
200
|
+
return fail(`project verify needs the project's first path to be an absolute, existing directory: ${manifest.paths[0] ? expandHome(manifest.paths[0]) : "(none)"}`, from);
|
|
201
|
+
cwd = dir;
|
|
202
|
+
}
|
|
203
|
+
commands = verify;
|
|
204
|
+
}
|
|
205
|
+
const run = options.run ?? defaultRunner;
|
|
206
|
+
const checks = [];
|
|
207
|
+
for (const command of commands) {
|
|
208
|
+
const { exitCode } = await run(command, cwd);
|
|
209
|
+
checks.push({ command, exitCode, ok: exitCode === 0 });
|
|
210
|
+
}
|
|
211
|
+
const passed = checks.every((check) => check.ok);
|
|
212
|
+
return {
|
|
213
|
+
id,
|
|
214
|
+
passed,
|
|
215
|
+
reason: passed
|
|
216
|
+
? "all verification checks passed"
|
|
217
|
+
: `verification failed: ${checks
|
|
218
|
+
.filter((check) => !check.ok)
|
|
219
|
+
.map((check) => check.command)
|
|
220
|
+
.join(", ")}`,
|
|
221
|
+
checks,
|
|
222
|
+
source: from,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
// What a task declares, and which of it the allowlist refuses: a dry readiness check that runs
|
|
226
|
+
// nothing (T-291).
|
|
227
|
+
export function declaredVerificationCommands(source) {
|
|
228
|
+
const declared = candidateCommands(source);
|
|
229
|
+
return {
|
|
230
|
+
declared,
|
|
231
|
+
refused: declared.filter((command) => !isAllowedCommand(command)),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
//# sourceMappingURL=verify-task.js.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { expectRecord } from "./owned-directory.js";
|
|
4
|
+
function records(value, field) {
|
|
5
|
+
if (value === undefined)
|
|
6
|
+
return undefined;
|
|
7
|
+
if (!Array.isArray(value))
|
|
8
|
+
throw new Error(`server.json ${field} must be an array`);
|
|
9
|
+
return value.map((item, index) => expectRecord(item, `server.json ${field}[${index}]`));
|
|
10
|
+
}
|
|
11
|
+
// One file per installed server; client wiring reads this directory.
|
|
12
|
+
export function mcpServerFile(serversRoot, capabilityName) {
|
|
13
|
+
return path.join(serversRoot, `${capabilityName.replace(/[^a-z0-9._-]/g, "__")}.json`);
|
|
14
|
+
}
|
|
15
|
+
export function createMcpAdapter(serversRoot) {
|
|
16
|
+
return {
|
|
17
|
+
kind: "mcp",
|
|
18
|
+
parseSpec(spec) {
|
|
19
|
+
const record = expectRecord(spec, "server.json");
|
|
20
|
+
if (typeof record.name !== "string" || !record.name)
|
|
21
|
+
throw new Error("server.json name is required");
|
|
22
|
+
const packages = records(record.packages, "packages");
|
|
23
|
+
const remotes = records(record.remotes, "remotes");
|
|
24
|
+
if (!packages?.length && !remotes?.length)
|
|
25
|
+
throw new Error("server.json must declare at least one package or remote");
|
|
26
|
+
for (const item of packages ?? [])
|
|
27
|
+
if (typeof item.registryType !== "string" ||
|
|
28
|
+
typeof item.identifier !== "string")
|
|
29
|
+
throw new Error("server.json packages need registryType and identifier");
|
|
30
|
+
for (const item of remotes ?? [])
|
|
31
|
+
if (typeof item.url !== "string" || !item.url.startsWith("https://"))
|
|
32
|
+
throw new Error("server.json remotes need an https url");
|
|
33
|
+
return record;
|
|
34
|
+
},
|
|
35
|
+
async install({ manifest, spec }) {
|
|
36
|
+
await mkdir(serversRoot(), { recursive: true });
|
|
37
|
+
await writeFile(mcpServerFile(serversRoot(), manifest.name), `${JSON.stringify({ capability: manifest.name, version: manifest.version, permissions: manifest.permissions, server: spec }, null, 2)}\n`);
|
|
38
|
+
},
|
|
39
|
+
// Every `description` in server.json: the server's and its packages'.
|
|
40
|
+
async inspect({ spec }) {
|
|
41
|
+
const texts = [];
|
|
42
|
+
const walk = (value, at) => {
|
|
43
|
+
if (Array.isArray(value))
|
|
44
|
+
value.forEach((item, index) => {
|
|
45
|
+
walk(item, `${at}[${index}]`);
|
|
46
|
+
});
|
|
47
|
+
else if (value && typeof value === "object")
|
|
48
|
+
for (const [key, item] of Object.entries(value)) {
|
|
49
|
+
const where = at ? `${at}.${key}` : key;
|
|
50
|
+
if (key === "description" && typeof item === "string")
|
|
51
|
+
texts.push({ source: `server.json ${where}`, text: item });
|
|
52
|
+
else
|
|
53
|
+
walk(item, where);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
walk(spec, "");
|
|
57
|
+
return texts;
|
|
58
|
+
},
|
|
59
|
+
async uninstall({ manifest }) {
|
|
60
|
+
await rm(mcpServerFile(serversRoot(), manifest.name), { force: true });
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=mcp-adapter.js.map
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { cp, mkdir, readdir, readFile, rename, rm, stat, writeFile, } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { resolveWithin } from "@ocean/core/shared/paths.js";
|
|
5
|
+
// A directory the marketplace installed carries this marker. Anything without it — a core
|
|
6
|
+
// skill, something the owner made by hand — is never overwritten or removed by an adapter.
|
|
7
|
+
const MARKER = ".ocean-capability.json";
|
|
8
|
+
const directoryName = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
|
|
9
|
+
export function expectDirectoryName(value, field) {
|
|
10
|
+
if (typeof value !== "string" ||
|
|
11
|
+
!directoryName.test(value) ||
|
|
12
|
+
value.includes(".."))
|
|
13
|
+
throw new Error(`${field} must be a plain directory name`);
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
export function expectRecord(value, what) {
|
|
17
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
18
|
+
throw new Error(`${what} must be an object`);
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
async function markerOwner(directory) {
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(await readFile(path.join(directory, MARKER), "utf8")).name;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function exists(file) {
|
|
30
|
+
try {
|
|
31
|
+
await stat(file);
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export async function installOwnedDirectory(root, name, contentDir, owner) {
|
|
39
|
+
await mkdir(root, { recursive: true });
|
|
40
|
+
const target = resolveWithin(root, name);
|
|
41
|
+
if (await exists(target)) {
|
|
42
|
+
const current = await markerOwner(target);
|
|
43
|
+
if (current !== owner.name)
|
|
44
|
+
throw new Error(`${target} already exists and is ${current ? `owned by ${current}` : "not managed by the marketplace"}; refusing to overwrite it`);
|
|
45
|
+
}
|
|
46
|
+
const staging = resolveWithin(root, `.${name}.${randomUUID()}.staging`);
|
|
47
|
+
try {
|
|
48
|
+
await cp(contentDir, staging, {
|
|
49
|
+
recursive: true,
|
|
50
|
+
errorOnExist: true,
|
|
51
|
+
force: false,
|
|
52
|
+
});
|
|
53
|
+
await writeFile(path.join(staging, MARKER), `${JSON.stringify(owner)}\n`);
|
|
54
|
+
await rm(target, { recursive: true, force: true });
|
|
55
|
+
await rename(staging, target);
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
await rm(staging, { recursive: true, force: true });
|
|
59
|
+
}
|
|
60
|
+
return target;
|
|
61
|
+
}
|
|
62
|
+
// Missing is fine (already gone); owned by someone else is refused.
|
|
63
|
+
export async function removeOwnedDirectory(root, name, owner) {
|
|
64
|
+
const target = resolveWithin(root, name);
|
|
65
|
+
if (!(await exists(target)))
|
|
66
|
+
return;
|
|
67
|
+
const current = await markerOwner(target);
|
|
68
|
+
if (current !== owner)
|
|
69
|
+
throw new Error(`${target} is ${current ? `owned by ${current}` : "not managed by the marketplace"}; refusing to remove it`);
|
|
70
|
+
await rm(target, { recursive: true, force: true });
|
|
71
|
+
}
|
|
72
|
+
// Every markdown file in a payload: what a model reads from a skill or a plugin.
|
|
73
|
+
export async function markdownTexts(contentDir) {
|
|
74
|
+
const texts = [];
|
|
75
|
+
for (const entry of await readdir(contentDir, {
|
|
76
|
+
recursive: true,
|
|
77
|
+
withFileTypes: true,
|
|
78
|
+
})) {
|
|
79
|
+
if (!entry.isFile() || !entry.name.toLowerCase().endsWith(".md"))
|
|
80
|
+
continue;
|
|
81
|
+
const full = path.join(entry.parentPath, entry.name);
|
|
82
|
+
texts.push({
|
|
83
|
+
source: path.relative(contentDir, full),
|
|
84
|
+
text: await readFile(full, "utf8"),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return texts;
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=owned-directory.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { stat } from "node:fs/promises";
|
|
2
|
+
import { resolveWithin } from "@ocean/core/shared/paths.js";
|
|
3
|
+
import { expectDirectoryName, expectRecord, installOwnedDirectory, markdownTexts, removeOwnedDirectory, } from "./owned-directory.js";
|
|
4
|
+
export function createPluginAdapter(pluginsRoot) {
|
|
5
|
+
return {
|
|
6
|
+
kind: "plugin",
|
|
7
|
+
parseSpec(spec) {
|
|
8
|
+
const record = expectRecord(spec, "plugin spec");
|
|
9
|
+
const entry = record.entry ?? ".claude-plugin/plugin.json";
|
|
10
|
+
if (typeof entry !== "string" ||
|
|
11
|
+
!entry ||
|
|
12
|
+
entry.startsWith("/") ||
|
|
13
|
+
entry.split(/[\\/]/).includes(".."))
|
|
14
|
+
throw new Error("plugin spec.entry must be a relative path inside the payload");
|
|
15
|
+
return {
|
|
16
|
+
directory: expectDirectoryName(record.directory, "plugin spec.directory"),
|
|
17
|
+
entry,
|
|
18
|
+
};
|
|
19
|
+
},
|
|
20
|
+
async install({ manifest, spec, contentDir }) {
|
|
21
|
+
try {
|
|
22
|
+
if (!(await stat(resolveWithin(contentDir, spec.entry))).isFile())
|
|
23
|
+
throw new Error();
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
throw new Error(`${manifest.name}: payload has no ${spec.entry}`);
|
|
27
|
+
}
|
|
28
|
+
await installOwnedDirectory(pluginsRoot(), spec.directory, contentDir, {
|
|
29
|
+
name: manifest.name,
|
|
30
|
+
version: manifest.version,
|
|
31
|
+
});
|
|
32
|
+
},
|
|
33
|
+
inspect: ({ contentDir }) => markdownTexts(contentDir),
|
|
34
|
+
async uninstall({ manifest, spec }) {
|
|
35
|
+
await removeOwnedDirectory(pluginsRoot(), spec.directory, manifest.name);
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=plugin-adapter.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { skillHubRoot } from "../skills/skill-hub.js";
|
|
4
|
+
import { expectDirectoryName, expectRecord, installOwnedDirectory, markdownTexts, removeOwnedDirectory, } from "./owned-directory.js";
|
|
5
|
+
export function createSkillAdapter(hubRoot = skillHubRoot) {
|
|
6
|
+
return {
|
|
7
|
+
kind: "skill",
|
|
8
|
+
parseSpec(spec) {
|
|
9
|
+
const record = expectRecord(spec, "skill spec");
|
|
10
|
+
return {
|
|
11
|
+
directory: expectDirectoryName(record.directory, "skill spec.directory"),
|
|
12
|
+
};
|
|
13
|
+
},
|
|
14
|
+
async install({ manifest, spec, contentDir }) {
|
|
15
|
+
try {
|
|
16
|
+
await stat(path.join(contentDir, "SKILL.md"));
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
throw new Error(`${manifest.name}: payload has no SKILL.md at its root`);
|
|
20
|
+
}
|
|
21
|
+
await installOwnedDirectory(hubRoot(), spec.directory, contentDir, {
|
|
22
|
+
name: manifest.name,
|
|
23
|
+
version: manifest.version,
|
|
24
|
+
});
|
|
25
|
+
},
|
|
26
|
+
inspect: ({ contentDir }) => markdownTexts(contentDir),
|
|
27
|
+
async uninstall({ manifest, spec }) {
|
|
28
|
+
await removeOwnedDirectory(hubRoot(), spec.directory, manifest.name);
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=skill-adapter.js.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { marketplaceIndexSchema, } from "@ocean/core/domain/capabilities/capability-manifest.js";
|
|
4
|
+
import { compareSemver } from "./registry.js";
|
|
5
|
+
export class LocalIndexSource {
|
|
6
|
+
id;
|
|
7
|
+
#indexFile;
|
|
8
|
+
constructor(indexFile) {
|
|
9
|
+
this.#indexFile = path.resolve(indexFile);
|
|
10
|
+
this.id = `file:${this.#indexFile}`;
|
|
11
|
+
}
|
|
12
|
+
async list() {
|
|
13
|
+
let raw;
|
|
14
|
+
try {
|
|
15
|
+
raw = JSON.parse(await readFile(this.#indexFile, "utf8"));
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
throw new Error(`Cannot read marketplace index ${this.#indexFile}: ${error instanceof Error ? error.message : String(error)}`);
|
|
19
|
+
}
|
|
20
|
+
const parsed = marketplaceIndexSchema.safeParse(raw);
|
|
21
|
+
if (!parsed.success)
|
|
22
|
+
throw new Error(`Invalid marketplace index ${this.#indexFile}: ${parsed.error.issues
|
|
23
|
+
.map((issue) => `${issue.path.map(String).join(".")}: ${issue.message}`)
|
|
24
|
+
.join("; ")}`);
|
|
25
|
+
const baseDir = path.dirname(this.#indexFile);
|
|
26
|
+
return parsed.data.capabilities.map((manifest) => ({
|
|
27
|
+
manifest,
|
|
28
|
+
sourceId: this.id,
|
|
29
|
+
baseDir,
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export class CapabilityCatalog {
|
|
34
|
+
#sources;
|
|
35
|
+
constructor(sources) {
|
|
36
|
+
this.#sources = sources;
|
|
37
|
+
}
|
|
38
|
+
// Earlier sources win when two offer the same name@version.
|
|
39
|
+
async list() {
|
|
40
|
+
const seen = new Set();
|
|
41
|
+
const entries = [];
|
|
42
|
+
for (const source of this.#sources)
|
|
43
|
+
for (const entry of await source.list()) {
|
|
44
|
+
const key = `${entry.manifest.name}@${entry.manifest.version}`;
|
|
45
|
+
if (seen.has(key))
|
|
46
|
+
continue;
|
|
47
|
+
seen.add(key);
|
|
48
|
+
entries.push(entry);
|
|
49
|
+
}
|
|
50
|
+
return entries;
|
|
51
|
+
}
|
|
52
|
+
// An exact version when given, otherwise the newest non-prerelease (a prerelease only when
|
|
53
|
+
// nothing else exists).
|
|
54
|
+
async find(name, version) {
|
|
55
|
+
const candidates = (await this.list()).filter((entry) => entry.manifest.name === name);
|
|
56
|
+
if (version)
|
|
57
|
+
return candidates.find((entry) => entry.manifest.version === version);
|
|
58
|
+
const stable = candidates.filter((entry) => !entry.manifest.version.split("+")[0].includes("-"));
|
|
59
|
+
const pool = stable.length ? stable : candidates;
|
|
60
|
+
return pool.sort((a, b) => compareSemver(b.manifest.version, a.manifest.version))[0];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=catalog.js.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstat, readdir, readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
// The digest `integrity.sha256` pins. Every entry is framed with its type, path and byte length,
|
|
5
|
+
// so no two different trees can produce the same byte stream (a file `a` holding `bX` and a file
|
|
6
|
+
// `ab` holding `X` hash differently). Symlinks are refused: a verified payload must be closed.
|
|
7
|
+
export async function contentDigest(root) {
|
|
8
|
+
const hash = createHash("sha256");
|
|
9
|
+
const info = await lstat(root);
|
|
10
|
+
if (info.isSymbolicLink())
|
|
11
|
+
throw new Error(`${root} is a symlink`);
|
|
12
|
+
if (info.isFile()) {
|
|
13
|
+
const bytes = await readFile(root);
|
|
14
|
+
hash.update(`file\0\0${bytes.length}\0`).update(bytes);
|
|
15
|
+
return hash.digest("hex");
|
|
16
|
+
}
|
|
17
|
+
async function walk(current, prefix) {
|
|
18
|
+
const entries = (await readdir(current, { withFileTypes: true })).sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
const full = path.join(current, entry.name);
|
|
21
|
+
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
22
|
+
const entryInfo = await lstat(full);
|
|
23
|
+
if (entryInfo.isSymbolicLink())
|
|
24
|
+
throw new Error(`${relative} is a symlink inside the payload`);
|
|
25
|
+
if (entryInfo.isDirectory()) {
|
|
26
|
+
hash.update(`dir\0${relative}\0`);
|
|
27
|
+
await walk(full, relative);
|
|
28
|
+
}
|
|
29
|
+
else if (entryInfo.isFile()) {
|
|
30
|
+
const bytes = await readFile(full);
|
|
31
|
+
hash.update(`file\0${relative}\0${bytes.length}\0`).update(bytes);
|
|
32
|
+
}
|
|
33
|
+
else
|
|
34
|
+
throw new Error(`${relative} is not a regular file`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
await walk(root, "");
|
|
38
|
+
return hash.digest("hex");
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=content-digest.js.map
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { capabilityManifestSchema, semverPattern, } from "@ocean/core/domain/capabilities/capability-manifest.js";
|
|
2
|
+
export class CapabilityRegistry {
|
|
3
|
+
#adapters = new Map();
|
|
4
|
+
register(adapter) {
|
|
5
|
+
if (this.#adapters.has(adapter.kind))
|
|
6
|
+
throw new Error(`A capability adapter for kind "${adapter.kind}" is already registered`);
|
|
7
|
+
this.#adapters.set(adapter.kind, adapter);
|
|
8
|
+
}
|
|
9
|
+
kinds() {
|
|
10
|
+
return [...this.#adapters.keys()].sort();
|
|
11
|
+
}
|
|
12
|
+
adapterFor(kind) {
|
|
13
|
+
const adapter = this.#adapters.get(kind);
|
|
14
|
+
if (!adapter)
|
|
15
|
+
throw new Error(`No capability adapter for kind "${kind}" (registered: ${this.kinds().join(", ") || "none"})`);
|
|
16
|
+
return adapter;
|
|
17
|
+
}
|
|
18
|
+
// Validates the envelope, then the kind-specific spec through its adapter.
|
|
19
|
+
resolve(raw) {
|
|
20
|
+
const parsed = capabilityManifestSchema.safeParse(raw);
|
|
21
|
+
if (!parsed.success)
|
|
22
|
+
throw new Error(`Invalid capability manifest: ${parsed.error.issues.map(describeIssue).join("; ")}`);
|
|
23
|
+
const manifest = parsed.data;
|
|
24
|
+
const adapter = this.adapterFor(manifest.kind);
|
|
25
|
+
let spec;
|
|
26
|
+
try {
|
|
27
|
+
spec = adapter.parseSpec(manifest.spec);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
throw new Error(`Invalid ${manifest.kind} spec for ${manifest.name}@${manifest.version}: ${error instanceof Error ? error.message : String(error)}`);
|
|
31
|
+
}
|
|
32
|
+
return { manifest, adapter, spec };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function describeIssue(issue) {
|
|
36
|
+
return issue.path.length
|
|
37
|
+
? `${issue.path.map(String).join(".")}: ${issue.message}`
|
|
38
|
+
: issue.message;
|
|
39
|
+
}
|
|
40
|
+
// Semver precedence (semver.org §11), enough to pick the newest version of a capability.
|
|
41
|
+
export function compareSemver(a, b) {
|
|
42
|
+
const parse = (value) => {
|
|
43
|
+
const match = semverPattern.exec(value);
|
|
44
|
+
if (!match)
|
|
45
|
+
throw new Error(`Not a semver version: ${value}`);
|
|
46
|
+
return {
|
|
47
|
+
core: [Number(match[1]), Number(match[2]), Number(match[3])],
|
|
48
|
+
pre: match[4]?.split(".") ?? [],
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
const left = parse(a);
|
|
52
|
+
const right = parse(b);
|
|
53
|
+
for (let index = 0; index < 3; index++) {
|
|
54
|
+
const delta = left.core[index] - right.core[index];
|
|
55
|
+
if (delta !== 0)
|
|
56
|
+
return Math.sign(delta);
|
|
57
|
+
}
|
|
58
|
+
if (!left.pre.length || !right.pre.length)
|
|
59
|
+
return Math.sign(right.pre.length - left.pre.length);
|
|
60
|
+
for (let index = 0; index < Math.max(left.pre.length, right.pre.length); index++) {
|
|
61
|
+
const x = left.pre[index];
|
|
62
|
+
const y = right.pre[index];
|
|
63
|
+
if (x === undefined)
|
|
64
|
+
return -1;
|
|
65
|
+
if (y === undefined)
|
|
66
|
+
return 1;
|
|
67
|
+
const xNumeric = /^\d+$/.test(x);
|
|
68
|
+
const yNumeric = /^\d+$/.test(y);
|
|
69
|
+
if (xNumeric && yNumeric) {
|
|
70
|
+
const delta = Number(x) - Number(y);
|
|
71
|
+
if (delta !== 0)
|
|
72
|
+
return Math.sign(delta);
|
|
73
|
+
}
|
|
74
|
+
else if (xNumeric !== yNumeric)
|
|
75
|
+
return xNumeric ? -1 : 1;
|
|
76
|
+
else if (x !== y)
|
|
77
|
+
return x < y ? -1 : 1;
|
|
78
|
+
}
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=registry.js.map
|