@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,278 @@
|
|
|
1
|
+
import { mkdir, readdir, readFile, rename } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { MemoryContractError } from "@ocean/core/domain/mcp/memory-contract.js";
|
|
4
|
+
import { atomicWrite, withFileLock } from "@ocean/core/shared/fs-utils.js";
|
|
5
|
+
import { localDate } from "@ocean/core/shared/local-date.js";
|
|
6
|
+
import { oceanPath, PROJECTS_DIR, projectDir, } from "@ocean/core/shared/paths.js";
|
|
7
|
+
import { findProject, readClaudeProjects, registerNamedProject, } from "@ocean/core/shared/project-resolution.js";
|
|
8
|
+
import { appendAudit, memorySettings, newId, sha256, writeDurable, } from "./memory-audit.js";
|
|
9
|
+
import { assertNoLinks, candidatesRoot, storedCopy } from "./memory-paths.js";
|
|
10
|
+
import { checkContent, proposeMemory, readMemoryRecord, writeMemoryRecord, } from "./memory-write.js";
|
|
11
|
+
function parse(text) {
|
|
12
|
+
const match = /^---\n([\s\S]*?)\n---\n\n?/.exec(text);
|
|
13
|
+
if (!match)
|
|
14
|
+
return null;
|
|
15
|
+
const fields = {};
|
|
16
|
+
for (const line of match[1].split("\n")) {
|
|
17
|
+
const index = line.indexOf(":");
|
|
18
|
+
if (index < 0)
|
|
19
|
+
continue;
|
|
20
|
+
const raw = line.slice(index + 1).trim();
|
|
21
|
+
let value = raw;
|
|
22
|
+
try {
|
|
23
|
+
value =
|
|
24
|
+
/^["[]/.test(raw) || raw === "true" || raw === "false"
|
|
25
|
+
? JSON.parse(raw)
|
|
26
|
+
: raw;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// a hand-edited value stays as written
|
|
30
|
+
}
|
|
31
|
+
fields[line.slice(0, index).trim()] = value;
|
|
32
|
+
}
|
|
33
|
+
const known = [
|
|
34
|
+
"candidate",
|
|
35
|
+
"origin",
|
|
36
|
+
"source",
|
|
37
|
+
"target",
|
|
38
|
+
"reason",
|
|
39
|
+
"created",
|
|
40
|
+
];
|
|
41
|
+
const field = (key) => typeof fields[key] === "string" ? fields[key] : "";
|
|
42
|
+
return {
|
|
43
|
+
origin: field("origin"),
|
|
44
|
+
source: field("source"),
|
|
45
|
+
target: field("target"),
|
|
46
|
+
reason: field("reason"),
|
|
47
|
+
created: field("created"),
|
|
48
|
+
meta: Object.fromEntries(Object.entries(fields).filter(([key]) => !known.includes(key))),
|
|
49
|
+
content: text.slice(match[0].length),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export async function listCandidates() {
|
|
53
|
+
const root = candidatesRoot();
|
|
54
|
+
const found = [];
|
|
55
|
+
const days = await readdir(root, { withFileTypes: true }).catch(() => []);
|
|
56
|
+
for (const day of days.filter((entry) => entry.isDirectory()).sort()) {
|
|
57
|
+
for (const name of (await readdir(path.join(root, day.name))).sort()) {
|
|
58
|
+
if (!name.endsWith(".md"))
|
|
59
|
+
continue;
|
|
60
|
+
const file = path.join(root, day.name, name);
|
|
61
|
+
const parsed = parse(await readFile(file, "utf8"));
|
|
62
|
+
if (!parsed)
|
|
63
|
+
continue;
|
|
64
|
+
found.push({
|
|
65
|
+
id: name.slice(0, -3),
|
|
66
|
+
file: path.relative(oceanPath(), file).split(path.sep).join("/"),
|
|
67
|
+
day: day.name,
|
|
68
|
+
...parsed,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return found;
|
|
73
|
+
}
|
|
74
|
+
async function candidate(id) {
|
|
75
|
+
const found = (await listCandidates()).find((item) => item.id === id);
|
|
76
|
+
if (!found)
|
|
77
|
+
throw new MemoryContractError("MISSING", `no candidate '${id}' to review`);
|
|
78
|
+
return found;
|
|
79
|
+
}
|
|
80
|
+
async function retire(item, action, source = "owner-review") {
|
|
81
|
+
const id = newId();
|
|
82
|
+
const copy = storedCopy("trash", id, item.file);
|
|
83
|
+
await mkdir(path.dirname(copy), { recursive: true, mode: 0o700 });
|
|
84
|
+
const text = await readFile(oceanPath(item.file), "utf8");
|
|
85
|
+
await rename(oceanPath(item.file), copy);
|
|
86
|
+
await appendAudit({
|
|
87
|
+
id,
|
|
88
|
+
action,
|
|
89
|
+
path: item.file,
|
|
90
|
+
source,
|
|
91
|
+
origin: item.origin === "imported" ? "imported" : "inferred",
|
|
92
|
+
beforeSha: sha256(text),
|
|
93
|
+
afterSha: null,
|
|
94
|
+
copy: path.relative(oceanPath(), copy).split(path.sep).join("/"),
|
|
95
|
+
});
|
|
96
|
+
return id;
|
|
97
|
+
}
|
|
98
|
+
// Writes the candidate's content to `to` (default: its proposed target). A new record is
|
|
99
|
+
// created; an existing one gets the content appended, guarded by its current hash — unless
|
|
100
|
+
// `createOnly` (an approval nobody looked at), which leaves that one to the owner's review.
|
|
101
|
+
export async function approveCandidate(options) {
|
|
102
|
+
const source = options.source ?? "owner-review";
|
|
103
|
+
const item = await candidate(options.id);
|
|
104
|
+
const provenance = {
|
|
105
|
+
origin: item.origin === "imported" ? "imported" : "inferred",
|
|
106
|
+
candidate: options.id,
|
|
107
|
+
};
|
|
108
|
+
if (item.meta.kind === "todo" && !options.to)
|
|
109
|
+
return approveTodo(item, source);
|
|
110
|
+
const target = options.to ?? item.target;
|
|
111
|
+
if (!target)
|
|
112
|
+
throw new MemoryContractError("INVALID", "the candidate names no target; pass --to <record path>");
|
|
113
|
+
// A project doc is only ever created: an existing one stays the owner's (T-307).
|
|
114
|
+
if (/^04-projects\/[^/]+\/docs\//.test(target)) {
|
|
115
|
+
const written = await writeMemoryRecord({
|
|
116
|
+
path: target,
|
|
117
|
+
content: item.content,
|
|
118
|
+
mode: "create",
|
|
119
|
+
source,
|
|
120
|
+
provenance,
|
|
121
|
+
document: true,
|
|
122
|
+
});
|
|
123
|
+
await retire(item, "approve", source);
|
|
124
|
+
return { path: written.path, mode: "create", auditId: written.auditId };
|
|
125
|
+
}
|
|
126
|
+
const existing = await readMemoryRecord(target).catch((error) => {
|
|
127
|
+
if (error instanceof MemoryContractError && error.code === "MISSING")
|
|
128
|
+
return null;
|
|
129
|
+
throw error;
|
|
130
|
+
});
|
|
131
|
+
if (existing && options.createOnly)
|
|
132
|
+
throw new MemoryContractError("CONFLICT", `${target} exists; adding to it needs review`);
|
|
133
|
+
const written = existing
|
|
134
|
+
? await writeMemoryRecord({
|
|
135
|
+
path: target,
|
|
136
|
+
content: `${existing.content.replace(/\n*$/, "\n\n")}${item.content}`,
|
|
137
|
+
mode: "update",
|
|
138
|
+
expectedHash: existing.sha256,
|
|
139
|
+
source,
|
|
140
|
+
provenance,
|
|
141
|
+
})
|
|
142
|
+
: await writeMemoryRecord({
|
|
143
|
+
path: target,
|
|
144
|
+
content: item.content,
|
|
145
|
+
mode: "create",
|
|
146
|
+
source,
|
|
147
|
+
provenance,
|
|
148
|
+
});
|
|
149
|
+
await retire(item, "approve", source);
|
|
150
|
+
return {
|
|
151
|
+
path: written.path,
|
|
152
|
+
mode: written.mode,
|
|
153
|
+
auditId: written.auditId,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
const BACKLOG = `${PROJECTS_DIR}/backlog.md`;
|
|
157
|
+
// An approved todo is one line under the backlog's "## Open" heading, never a task folder:
|
|
158
|
+
// a task is opened only at the owner's request. The backlog before the change is kept in
|
|
159
|
+
// versions/ and the audit names the candidate it came from.
|
|
160
|
+
async function approveTodo(item, source) {
|
|
161
|
+
checkContent(item.content);
|
|
162
|
+
const project = typeof item.meta.project === "string" ? item.meta.project : "workspace";
|
|
163
|
+
const text = item.content.replace(/\s+/g, " ").trim().slice(0, 300);
|
|
164
|
+
const line = `- [TODO] (P2) ${project} — ${text} {${localDate()}}`;
|
|
165
|
+
const file = oceanPath(BACKLOG);
|
|
166
|
+
assertNoLinks(oceanPath(), file);
|
|
167
|
+
const id = newId();
|
|
168
|
+
const { before, after, copy } = await withFileLock(file, async () => {
|
|
169
|
+
const current = await readFile(file, "utf8").catch((error) => {
|
|
170
|
+
if (error.code === "ENOENT")
|
|
171
|
+
return null;
|
|
172
|
+
throw error;
|
|
173
|
+
});
|
|
174
|
+
const base = current ?? "# Backlog\n\n## Open\n\n";
|
|
175
|
+
const open = "## Open\n\n";
|
|
176
|
+
const at = base.indexOf(open);
|
|
177
|
+
const next = at < 0
|
|
178
|
+
? `${base.replace(/\n*$/, "\n\n")}${line}\n`
|
|
179
|
+
: `${base.slice(0, at + open.length)}${line}\n${base.slice(at + open.length)}`;
|
|
180
|
+
let copy;
|
|
181
|
+
if (current !== null) {
|
|
182
|
+
const stored = storedCopy("versions", id, BACKLOG);
|
|
183
|
+
await writeDurable(stored, current);
|
|
184
|
+
copy = path.relative(oceanPath(), stored).split(path.sep).join("/");
|
|
185
|
+
}
|
|
186
|
+
await atomicWrite(file, next);
|
|
187
|
+
return { before: current, after: next, copy };
|
|
188
|
+
});
|
|
189
|
+
await appendAudit({
|
|
190
|
+
id,
|
|
191
|
+
action: before === null ? "create" : "update",
|
|
192
|
+
path: BACKLOG,
|
|
193
|
+
source,
|
|
194
|
+
origin: item.origin === "imported" ? "imported" : "inferred",
|
|
195
|
+
candidate: item.id,
|
|
196
|
+
beforeSha: before === null ? null : sha256(before),
|
|
197
|
+
afterSha: sha256(after),
|
|
198
|
+
copy,
|
|
199
|
+
});
|
|
200
|
+
await retire(item, "approve", source);
|
|
201
|
+
return { path: BACKLOG, mode: "update", auditId: id };
|
|
202
|
+
}
|
|
203
|
+
const KINDS = ["fact", "doc", "todo"];
|
|
204
|
+
const NAME = /^[a-z0-9][a-z0-9-]{0,59}$/;
|
|
205
|
+
// A client's proposal made inside a project (T-307). The project is named by its Ocean id or its
|
|
206
|
+
// name (`LetSpeak` finds `letspeak`). Ocean, not the client, picks the place from the kind: a
|
|
207
|
+
// fact goes to the project's memory/ and a doc to its docs/, each as the new file `<name>.md`; a
|
|
208
|
+
// todo waits for review and becomes one backlog line. With proposeAutoApprove on, a fact or doc
|
|
209
|
+
// for an existing project is written at once — never over or into an existing file — and keeps
|
|
210
|
+
// its origin and candidate in the audit; a name Ocean does not know is registered as a project
|
|
211
|
+
// only when it is one of the owner's Claude projects this machine has seen, never for a topic a
|
|
212
|
+
// client calls a project. Everything else stays a candidate.
|
|
213
|
+
export async function fileProposal(options) {
|
|
214
|
+
if (!KINDS.includes(options.kind))
|
|
215
|
+
throw new MemoryContractError("INVALID", "kind must be fact, doc or todo");
|
|
216
|
+
const ref = typeof options.project === "string" ? options.project.trim() : "";
|
|
217
|
+
if (!ref || ref.length > 64 || /[\r\n/\\]/.test(ref))
|
|
218
|
+
throw new MemoryContractError("INVALID", "project must be a project id or name of at most 64 characters");
|
|
219
|
+
if (options.kind !== "todo" &&
|
|
220
|
+
(typeof options.name !== "string" || !NAME.test(options.name)))
|
|
221
|
+
throw new MemoryContractError("INVALID", "name must be lowercase kebab-case, at most 60 characters");
|
|
222
|
+
const auto = (await memorySettings()).proposeAutoApprove;
|
|
223
|
+
let project = await findProject(ref);
|
|
224
|
+
let registered;
|
|
225
|
+
if (!project &&
|
|
226
|
+
auto &&
|
|
227
|
+
(await readClaudeProjects()).some((name) => name.trim().toLowerCase() === ref.toLowerCase())) {
|
|
228
|
+
try {
|
|
229
|
+
project = (await registerNamedProject(ref, ["claude.ai"])).id;
|
|
230
|
+
registered = project;
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
// an id that cannot be made, or a folder without a manifest: the owner decides
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const folder = options.kind === "fact" ? "memory" : "docs";
|
|
237
|
+
const target = project && options.kind !== "todo"
|
|
238
|
+
? `${projectDir(project)}/${folder}/${options.name}.md`
|
|
239
|
+
: undefined;
|
|
240
|
+
const proposed = await proposeMemory({
|
|
241
|
+
content: options.content,
|
|
242
|
+
origin: options.origin,
|
|
243
|
+
target,
|
|
244
|
+
reason: options.reason,
|
|
245
|
+
source: options.source,
|
|
246
|
+
meta: { kind: options.kind, project: project ?? ref },
|
|
247
|
+
});
|
|
248
|
+
const result = { ...proposed, ...(registered ? { registered } : {}) };
|
|
249
|
+
if (!project)
|
|
250
|
+
return {
|
|
251
|
+
...result,
|
|
252
|
+
review: `'${ref}' is neither an Ocean project nor a Claude project this machine has seen; waits for review`,
|
|
253
|
+
};
|
|
254
|
+
if (!target)
|
|
255
|
+
return { ...result, review: "a todo waits for review" };
|
|
256
|
+
if (!auto)
|
|
257
|
+
return {
|
|
258
|
+
...result,
|
|
259
|
+
review: "proposeAutoApprove is off; waits for review",
|
|
260
|
+
};
|
|
261
|
+
try {
|
|
262
|
+
const written = await approveCandidate({
|
|
263
|
+
id: path.basename(proposed.candidate, ".md"),
|
|
264
|
+
createOnly: true,
|
|
265
|
+
source: "propose-auto-approve",
|
|
266
|
+
});
|
|
267
|
+
return { ...result, filed: written.path };
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
if (!(error instanceof MemoryContractError))
|
|
271
|
+
throw error;
|
|
272
|
+
return { ...result, review: `${error.message}; waits for review` };
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
export async function rejectCandidate(id) {
|
|
276
|
+
return { id, auditId: await retire(await candidate(id), "reject") };
|
|
277
|
+
}
|
|
278
|
+
//# sourceMappingURL=memory-review.js.map
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { readdir, readFile, rm } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { MemoryContractError } from "@ocean/core/domain/mcp/memory-contract.js";
|
|
4
|
+
import { atomicWrite, withFileLock } from "@ocean/core/shared/fs-utils.js";
|
|
5
|
+
import { oceanPath, resolveWithin, withoutProjectCategory, } from "@ocean/core/shared/paths.js";
|
|
6
|
+
import { syncMemoryIndexes } from "./index-sync.js";
|
|
7
|
+
import { appendAudit, idTime, memorySettings, newId, readAudit, sha256, writeDurable, } from "./memory-audit.js";
|
|
8
|
+
import { permanentRecord, storedCopy, trashRoot, versionsRoot, } from "./memory-paths.js";
|
|
9
|
+
import { checkContent } from "./memory-write.js";
|
|
10
|
+
// Soft delete, history, restore and purge (T-296). Nothing over MCP removes content for good:
|
|
11
|
+
// a delete moves the record to trash/, an update keeps the old content in versions/, and only
|
|
12
|
+
// `ocean memory purge --yes` (CLI) drops copies older than the retention period.
|
|
13
|
+
const relativeToWorkspace = (file) => path.relative(oceanPath(), file).split(path.sep).join("/");
|
|
14
|
+
export async function deleteMemoryRecord(options) {
|
|
15
|
+
const record = permanentRecord(options.path);
|
|
16
|
+
const id = newId();
|
|
17
|
+
const { before, trash } = await withFileLock(record.file, async () => {
|
|
18
|
+
let current;
|
|
19
|
+
try {
|
|
20
|
+
current = await readFile(record.file, "utf8");
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (error.code === "ENOENT")
|
|
24
|
+
throw new MemoryContractError("MISSING", `${record.relative} not found`);
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
if (options.expectedHash !== sha256(current))
|
|
28
|
+
throw new MemoryContractError("CONFLICT", "expectedHash does not match the record; read it again before deleting", { sha256: sha256(current) });
|
|
29
|
+
const stored = storedCopy("trash", id, record.relative);
|
|
30
|
+
await writeDurable(stored, current);
|
|
31
|
+
await rm(record.file);
|
|
32
|
+
return { before: current, trash: relativeToWorkspace(stored) };
|
|
33
|
+
});
|
|
34
|
+
await appendAudit({
|
|
35
|
+
id,
|
|
36
|
+
action: "delete",
|
|
37
|
+
path: record.relative,
|
|
38
|
+
source: options.source ?? "unknown",
|
|
39
|
+
beforeSha: sha256(before),
|
|
40
|
+
afterSha: null,
|
|
41
|
+
copy: trash,
|
|
42
|
+
});
|
|
43
|
+
if (record.personal)
|
|
44
|
+
await syncMemoryIndexes(true);
|
|
45
|
+
return { path: record.relative, auditId: id, trash };
|
|
46
|
+
}
|
|
47
|
+
export async function memoryHistory(options = {}) {
|
|
48
|
+
// Either layout names the same record: an entry written before its project moved into a
|
|
49
|
+
// category folder still answers to the new path, and the reverse (T-311).
|
|
50
|
+
const wanted = options.path
|
|
51
|
+
? withoutProjectCategory(options.path.split("\\").join("/"))
|
|
52
|
+
: undefined;
|
|
53
|
+
const entries = (await readAudit()).filter((entry) => !wanted || withoutProjectCategory(entry.path) === wanted);
|
|
54
|
+
const limit = Math.min(100, Math.max(1, options.limit ?? 20));
|
|
55
|
+
return { entries: entries.slice(-limit).reverse() };
|
|
56
|
+
}
|
|
57
|
+
// Puts back the content an audit entry preserved: a deleted record (only while its path is
|
|
58
|
+
// free) or an earlier version (the current content is versioned first, and the caller must
|
|
59
|
+
// name it by hash, as for an update).
|
|
60
|
+
export async function restoreMemory(options) {
|
|
61
|
+
const entry = (await readAudit()).find((item) => item.id === options.id && item.copy);
|
|
62
|
+
if (!entry?.copy)
|
|
63
|
+
throw new MemoryContractError("MISSING", `no stored copy for audit entry ${options.id}`);
|
|
64
|
+
const copyFile = resolveCopy(entry.copy);
|
|
65
|
+
let content;
|
|
66
|
+
try {
|
|
67
|
+
content = await readFile(copyFile, "utf8");
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
if (error.code === "ENOENT")
|
|
71
|
+
throw new MemoryContractError("MISSING", `stored copy ${entry.copy} was purged`);
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
checkContent(content);
|
|
75
|
+
const record = permanentRecord(entry.path);
|
|
76
|
+
const id = newId();
|
|
77
|
+
const replaced = await withFileLock(record.file, async () => {
|
|
78
|
+
let current = null;
|
|
79
|
+
try {
|
|
80
|
+
current = await readFile(record.file, "utf8");
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (error.code !== "ENOENT")
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
let copy;
|
|
87
|
+
if (current !== null) {
|
|
88
|
+
if (entry.action === "delete")
|
|
89
|
+
throw new MemoryContractError("EXISTS", `${record.relative} was recreated after the delete; restore an earlier version instead`);
|
|
90
|
+
if (options.expectedHash !== sha256(current))
|
|
91
|
+
throw new MemoryContractError("CONFLICT", "expectedHash does not match the record; read it again before restoring", { sha256: sha256(current) });
|
|
92
|
+
const stored = storedCopy("versions", id, record.relative);
|
|
93
|
+
await writeDurable(stored, current);
|
|
94
|
+
copy = relativeToWorkspace(stored);
|
|
95
|
+
}
|
|
96
|
+
await atomicWrite(record.file, content);
|
|
97
|
+
return { before: current, copy };
|
|
98
|
+
});
|
|
99
|
+
await appendAudit({
|
|
100
|
+
id,
|
|
101
|
+
action: "restore",
|
|
102
|
+
path: record.relative,
|
|
103
|
+
source: options.source ?? "unknown",
|
|
104
|
+
beforeSha: replaced.before === null ? null : sha256(replaced.before),
|
|
105
|
+
afterSha: sha256(content),
|
|
106
|
+
copy: replaced.copy,
|
|
107
|
+
restoredFrom: entry.id,
|
|
108
|
+
});
|
|
109
|
+
if (record.personal)
|
|
110
|
+
await syncMemoryIndexes(true);
|
|
111
|
+
return { path: record.relative, auditId: id, sha256: sha256(content) };
|
|
112
|
+
}
|
|
113
|
+
// An audit entry's copy must still name a file under versions/ or trash/.
|
|
114
|
+
function resolveCopy(copy) {
|
|
115
|
+
const file = oceanPath(...copy.split("/"));
|
|
116
|
+
for (const root of [versionsRoot(), trashRoot()]) {
|
|
117
|
+
try {
|
|
118
|
+
return resolveWithin(root, path.relative(root, file));
|
|
119
|
+
}
|
|
120
|
+
catch { }
|
|
121
|
+
}
|
|
122
|
+
throw new MemoryContractError("OUT_OF_ROOT", `stored copy ${copy} is outside versions/ and trash/`);
|
|
123
|
+
}
|
|
124
|
+
// CLI only. Lists stored copies older than the retention period; removes them only with apply.
|
|
125
|
+
export async function purgeMemoryCopies(options) {
|
|
126
|
+
const days = options.days ?? (await memorySettings()).retentionDays;
|
|
127
|
+
if (!Number.isInteger(days) || days < 1)
|
|
128
|
+
throw new MemoryContractError("INVALID", "days must be a whole number ≥ 1");
|
|
129
|
+
const cutoff = new Date((options.now ?? new Date()).getTime() - days * 86_400_000);
|
|
130
|
+
const copies = [];
|
|
131
|
+
for (const root of [versionsRoot(), trashRoot()]) {
|
|
132
|
+
const ids = await readdir(root).catch(() => []);
|
|
133
|
+
for (const id of ids.sort()) {
|
|
134
|
+
const time = idTime(id);
|
|
135
|
+
if (time && time < cutoff)
|
|
136
|
+
copies.push(relativeToWorkspace(path.join(root, id)));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (options.apply)
|
|
140
|
+
for (const copy of copies) {
|
|
141
|
+
await rm(oceanPath(...copy.split("/")), { recursive: true, force: true });
|
|
142
|
+
await appendAudit({
|
|
143
|
+
action: "purge",
|
|
144
|
+
path: copy,
|
|
145
|
+
source: "cli",
|
|
146
|
+
beforeSha: null,
|
|
147
|
+
afterSha: null,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return { cutoff: cutoff.toISOString(), copies, applied: options.apply };
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=memory-trash.js.map
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { mkdir, readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { MemoryContractError } from "@ocean/core/domain/mcp/memory-contract.js";
|
|
4
|
+
import { containsKnownSecret } from "@ocean/core/domain/redaction/redaction.js";
|
|
5
|
+
import { atomicWrite, withFileLock } from "@ocean/core/shared/fs-utils.js";
|
|
6
|
+
import { localDate } from "@ocean/core/shared/local-date.js";
|
|
7
|
+
import { listProjectDirs, oceanPath, PERSONAL_DIR, } from "@ocean/core/shared/paths.js";
|
|
8
|
+
import { syncMemoryIndexes } from "./index-sync.js";
|
|
9
|
+
import { appendAudit, newId, sha256, writeDurable } from "./memory-audit.js";
|
|
10
|
+
import { assertNoLinks, candidateFile, candidatesRoot, permanentRecord, projectDocument, storedCopy, } from "./memory-paths.js";
|
|
11
|
+
const maxRecordBytes = 32_000;
|
|
12
|
+
export function checkContent(content) {
|
|
13
|
+
if (typeof content !== "string" || !content.trim())
|
|
14
|
+
throw new MemoryContractError("INVALID", "content is empty");
|
|
15
|
+
if (Buffer.byteLength(content) > maxRecordBytes)
|
|
16
|
+
throw new MemoryContractError("TOO_LARGE", `content exceeds ${maxRecordBytes} bytes`);
|
|
17
|
+
if (containsKnownSecret(content))
|
|
18
|
+
throw new MemoryContractError("SECRET", "content holds a secret; memory never stores one");
|
|
19
|
+
}
|
|
20
|
+
async function readIfPresent(file) {
|
|
21
|
+
try {
|
|
22
|
+
return await readFile(file, "utf8");
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (error.code === "ENOENT")
|
|
26
|
+
return null;
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function withNewline(content) {
|
|
31
|
+
return content.endsWith("\n") ? content : `${content}\n`;
|
|
32
|
+
}
|
|
33
|
+
export async function readMemoryRecord(relative) {
|
|
34
|
+
const record = permanentRecord(relative);
|
|
35
|
+
const content = await readIfPresent(record.file);
|
|
36
|
+
if (content === null)
|
|
37
|
+
throw new MemoryContractError("MISSING", `${record.relative} not found`);
|
|
38
|
+
return { path: record.relative, content, sha256: sha256(content) };
|
|
39
|
+
}
|
|
40
|
+
async function markdownFiles(directory) {
|
|
41
|
+
let entries;
|
|
42
|
+
try {
|
|
43
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (error.code === "ENOENT")
|
|
47
|
+
return [];
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
const files = [];
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
if (entry.name.startsWith("."))
|
|
53
|
+
continue;
|
|
54
|
+
const full = path.join(directory, entry.name);
|
|
55
|
+
if (entry.isDirectory())
|
|
56
|
+
files.push(...(await markdownFiles(full)));
|
|
57
|
+
else if (entry.isFile() && entry.name.endsWith(".md"))
|
|
58
|
+
files.push(full);
|
|
59
|
+
}
|
|
60
|
+
return files;
|
|
61
|
+
}
|
|
62
|
+
const normalize = (value) => value
|
|
63
|
+
.toLowerCase()
|
|
64
|
+
.replace(/\.md$/, "")
|
|
65
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
66
|
+
.replace(/^-|-$/g, "");
|
|
67
|
+
function title(content) {
|
|
68
|
+
const frontmatter = /^---\n([\s\S]*?)\n---/.exec(content)?.[1] ?? "";
|
|
69
|
+
return (/^(?:name|title):\s*["']?(.+?)["']?\s*$/m.exec(frontmatter)?.[1] ??
|
|
70
|
+
/^#\s+(.+)$/m.exec(content)?.[1] ??
|
|
71
|
+
"");
|
|
72
|
+
}
|
|
73
|
+
// A new record whose file name or title matches an existing one is probably the same fact:
|
|
74
|
+
// the caller must update that record, or say it is a different one (allowSimilar).
|
|
75
|
+
async function similarRecords(file, content) {
|
|
76
|
+
const roots = [
|
|
77
|
+
oceanPath(PERSONAL_DIR),
|
|
78
|
+
...listProjectDirs().map(({ dir }) => oceanPath(dir, "memory")),
|
|
79
|
+
];
|
|
80
|
+
const name = normalize(path.basename(file));
|
|
81
|
+
const heading = normalize(title(content));
|
|
82
|
+
const matches = [];
|
|
83
|
+
for (const root of roots)
|
|
84
|
+
for (const existing of await markdownFiles(root)) {
|
|
85
|
+
// The target itself is not "similar": an exact existing path is EXISTS.
|
|
86
|
+
if (existing === file ||
|
|
87
|
+
path.basename(existing).toLowerCase() === "memory.md")
|
|
88
|
+
continue;
|
|
89
|
+
const sameName = normalize(path.basename(existing)) === name;
|
|
90
|
+
const sameTitle = heading !== "" &&
|
|
91
|
+
normalize(title(await readFile(existing, "utf8"))) === heading;
|
|
92
|
+
if (sameName || sameTitle)
|
|
93
|
+
matches.push(path.relative(oceanPath(), existing).split(path.sep).join("/"));
|
|
94
|
+
}
|
|
95
|
+
return matches;
|
|
96
|
+
}
|
|
97
|
+
// An explicit save. Search-first is enforced by mode: `create` refuses an existing or similar
|
|
98
|
+
// record, `update` needs the sha256 the caller read, so a stale or blind write cannot clobber.
|
|
99
|
+
// The replaced content is kept in versions/ before the record changes.
|
|
100
|
+
export async function writeMemoryRecord(options) {
|
|
101
|
+
const record = options.document
|
|
102
|
+
? projectDocument(options.path)
|
|
103
|
+
: permanentRecord(options.path);
|
|
104
|
+
checkContent(options.content);
|
|
105
|
+
if (options.mode !== "create" && options.mode !== "update")
|
|
106
|
+
throw new MemoryContractError("INVALID", "mode must be create or update");
|
|
107
|
+
if (options.document && options.mode !== "create")
|
|
108
|
+
throw new MemoryContractError("INVALID", "a doc is only ever created");
|
|
109
|
+
if (options.mode === "create" && !options.allowSimilar && !options.document) {
|
|
110
|
+
const similar = await similarRecords(record.file, options.content);
|
|
111
|
+
if (similar.length)
|
|
112
|
+
throw new MemoryContractError("EXISTS_SIMILAR", "a record with the same name or title exists; update it, or pass allowSimilar if this is a different fact", { paths: similar });
|
|
113
|
+
}
|
|
114
|
+
const content = withNewline(options.content);
|
|
115
|
+
const id = newId();
|
|
116
|
+
await mkdir(path.dirname(record.file), { recursive: true });
|
|
117
|
+
assertNoLinks(record.root, record.file);
|
|
118
|
+
const { before, copy } = await withFileLock(record.file, async () => {
|
|
119
|
+
const current = await readIfPresent(record.file);
|
|
120
|
+
if (options.mode === "create" && current !== null)
|
|
121
|
+
throw new MemoryContractError("EXISTS", "record already exists; read it and write it back with mode update");
|
|
122
|
+
if (options.mode === "update") {
|
|
123
|
+
if (current === null)
|
|
124
|
+
throw new MemoryContractError("MISSING", "record does not exist; use mode create");
|
|
125
|
+
if (options.expectedHash !== sha256(current))
|
|
126
|
+
throw new MemoryContractError("CONFLICT", "expectedHash does not match the record; read it again before updating", { sha256: sha256(current) });
|
|
127
|
+
}
|
|
128
|
+
let copy;
|
|
129
|
+
if (current !== null) {
|
|
130
|
+
const stored = storedCopy("versions", id, record.relative);
|
|
131
|
+
await writeDurable(stored, current);
|
|
132
|
+
copy = path.relative(oceanPath(), stored).split(path.sep).join("/");
|
|
133
|
+
}
|
|
134
|
+
await atomicWrite(record.file, content);
|
|
135
|
+
return { before: current, copy };
|
|
136
|
+
});
|
|
137
|
+
await appendAudit({
|
|
138
|
+
id,
|
|
139
|
+
action: options.mode,
|
|
140
|
+
path: record.relative,
|
|
141
|
+
source: options.source ?? "unknown",
|
|
142
|
+
origin: options.provenance?.origin ?? "explicit",
|
|
143
|
+
...(options.provenance ? { candidate: options.provenance.candidate } : {}),
|
|
144
|
+
beforeSha: before === null ? null : sha256(before),
|
|
145
|
+
afterSha: sha256(content),
|
|
146
|
+
copy,
|
|
147
|
+
});
|
|
148
|
+
if (record.personal)
|
|
149
|
+
await syncMemoryIndexes(true);
|
|
150
|
+
return {
|
|
151
|
+
path: record.relative,
|
|
152
|
+
mode: options.mode,
|
|
153
|
+
sha256: sha256(content),
|
|
154
|
+
auditId: id,
|
|
155
|
+
indexed: record.personal,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const singleLine = (value, field) => {
|
|
159
|
+
if (value === undefined)
|
|
160
|
+
return "";
|
|
161
|
+
if (typeof value !== "string" || /[\r\n]/.test(value) || value.length > 200)
|
|
162
|
+
throw new MemoryContractError("INVALID", `${field} must be one line of at most 200 characters`);
|
|
163
|
+
return value;
|
|
164
|
+
};
|
|
165
|
+
const RESERVED = new Set([
|
|
166
|
+
"candidate",
|
|
167
|
+
"origin",
|
|
168
|
+
"source",
|
|
169
|
+
"target",
|
|
170
|
+
"reason",
|
|
171
|
+
"created",
|
|
172
|
+
]);
|
|
173
|
+
function metaLines(meta) {
|
|
174
|
+
let lines = "";
|
|
175
|
+
for (const [key, value] of Object.entries(meta ?? {})) {
|
|
176
|
+
const rendered = JSON.stringify(value);
|
|
177
|
+
if (!/^[a-z][a-z_]{0,30}$/.test(key) ||
|
|
178
|
+
RESERVED.has(key) ||
|
|
179
|
+
rendered.length > 600)
|
|
180
|
+
throw new MemoryContractError("INVALID", `bad candidate field '${key}'`);
|
|
181
|
+
lines += `${key}: ${rendered}\n`;
|
|
182
|
+
}
|
|
183
|
+
return lines;
|
|
184
|
+
}
|
|
185
|
+
// An inferred or imported fact waits in candidates/ for the owner's review. The server names
|
|
186
|
+
// the file; a proposed target is validated as a record path but never written here.
|
|
187
|
+
export async function proposeMemory(options) {
|
|
188
|
+
checkContent(options.content);
|
|
189
|
+
const extra = metaLines(options.meta);
|
|
190
|
+
if (options.origin !== "inferred" && options.origin !== "imported")
|
|
191
|
+
throw new MemoryContractError("INVALID", "origin must be inferred or imported");
|
|
192
|
+
const target = !options.target
|
|
193
|
+
? ""
|
|
194
|
+
: options.meta?.kind === "doc"
|
|
195
|
+
? projectDocument(options.target).relative
|
|
196
|
+
: permanentRecord(options.target).relative;
|
|
197
|
+
const reason = singleLine(options.reason, "reason");
|
|
198
|
+
const source = singleLine(options.source, "source") || "unknown";
|
|
199
|
+
const id = newId();
|
|
200
|
+
const file = candidateFile(localDate(), id);
|
|
201
|
+
assertNoLinks(candidatesRoot(), file);
|
|
202
|
+
const body = `---\ncandidate: ${id}\norigin: ${options.origin}\nsource: ${JSON.stringify(source)}\ntarget: ${JSON.stringify(target)}\nreason: ${JSON.stringify(reason)}\ncreated: ${new Date().toISOString()}\n${extra}---\n\n${withNewline(options.content)}`;
|
|
203
|
+
await writeDurable(file, body);
|
|
204
|
+
const candidate = path.relative(oceanPath(), file).split(path.sep).join("/");
|
|
205
|
+
await appendAudit({
|
|
206
|
+
id,
|
|
207
|
+
action: "propose",
|
|
208
|
+
path: candidate,
|
|
209
|
+
source,
|
|
210
|
+
origin: options.origin,
|
|
211
|
+
beforeSha: null,
|
|
212
|
+
afterSha: sha256(body),
|
|
213
|
+
});
|
|
214
|
+
return { candidate, auditId: id };
|
|
215
|
+
}
|
|
216
|
+
//# sourceMappingURL=memory-write.js.map
|