@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,476 @@
|
|
|
1
|
+
import { createReadStream } from "node:fs";
|
|
2
|
+
import { copyFile, mkdir, mkdtemp, open, readdir, readFile, rm, stat, } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { DatabaseSync } from "node:sqlite";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import zlib from "node:zlib";
|
|
8
|
+
import { redactSecrets } from "@ocean/core/domain/redaction/redaction.js";
|
|
9
|
+
import { atomicWrite } from "@ocean/core/shared/fs-utils.js";
|
|
10
|
+
import { oceanPath, SYSTEM_DIR } from "@ocean/core/shared/paths.js";
|
|
11
|
+
import { loadProjectIndex, placeFolder } from "./project-match.js";
|
|
12
|
+
const HEAD_BYTES = 64 * 1024;
|
|
13
|
+
const TITLE_CHARS = 120;
|
|
14
|
+
export const historyIndexFile = () => oceanPath(SYSTEM_DIR, "state", "history", "sessions.jsonl");
|
|
15
|
+
function title(text) {
|
|
16
|
+
if (typeof text !== "string")
|
|
17
|
+
return "";
|
|
18
|
+
return redactSecrets(text.replace(/\s+/g, " ").trim()).slice(0, TITLE_CHARS);
|
|
19
|
+
}
|
|
20
|
+
// The texts of a chat message's content: a string, or the text parts of a list.
|
|
21
|
+
function contentTexts(content) {
|
|
22
|
+
if (typeof content === "string")
|
|
23
|
+
return [content];
|
|
24
|
+
if (!Array.isArray(content))
|
|
25
|
+
return [];
|
|
26
|
+
return content.flatMap((part) => part && typeof part === "object" && typeof part.text === "string"
|
|
27
|
+
? [part.text]
|
|
28
|
+
: []);
|
|
29
|
+
}
|
|
30
|
+
// What the owner asked: the first message text left once injected context (leading
|
|
31
|
+
// `<tag>…</tag>` blocks: environment, opened file, reminders, command echoes) is taken off.
|
|
32
|
+
const LEADING_BLOCK = /^\s*<([A-Za-z][\w-]*)[^>]*>[\s\S]*?<\/\1>/;
|
|
33
|
+
function firstPrompt(contents) {
|
|
34
|
+
for (const content of contents)
|
|
35
|
+
for (let text of contentTexts(content)) {
|
|
36
|
+
while (LEADING_BLOCK.test(text))
|
|
37
|
+
text = text.replace(LEADING_BLOCK, "");
|
|
38
|
+
if (text.trim() && !/^\s*(<|# AGENTS\.md|Caveat:)/.test(text))
|
|
39
|
+
return text;
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
// Codex compresses older sessions (`.jsonl.zst`); zstd is in node:zlib from Node 22.15. Without
|
|
44
|
+
// it such a file is still indexed, untitled.
|
|
45
|
+
const zstd = zlib
|
|
46
|
+
.createZstdDecompress;
|
|
47
|
+
async function zstdHead(file, bytes) {
|
|
48
|
+
if (!zstd)
|
|
49
|
+
return [];
|
|
50
|
+
const chunks = [];
|
|
51
|
+
let size = 0;
|
|
52
|
+
const source = createReadStream(file);
|
|
53
|
+
const stream = source.pipe(zstd());
|
|
54
|
+
try {
|
|
55
|
+
for await (const chunk of stream) {
|
|
56
|
+
chunks.push(chunk);
|
|
57
|
+
size += chunk.length;
|
|
58
|
+
if (size >= bytes)
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
source.destroy();
|
|
64
|
+
stream.destroy();
|
|
65
|
+
}
|
|
66
|
+
const lines = Buffer.concat(chunks)
|
|
67
|
+
.subarray(0, bytes)
|
|
68
|
+
.toString("utf8")
|
|
69
|
+
.split("\n");
|
|
70
|
+
return size >= bytes ? lines.slice(0, -1) : lines;
|
|
71
|
+
}
|
|
72
|
+
async function head(file, bytes = HEAD_BYTES) {
|
|
73
|
+
if (file.endsWith(".zst"))
|
|
74
|
+
return zstdHead(file, bytes);
|
|
75
|
+
const handle = await open(file, "r");
|
|
76
|
+
try {
|
|
77
|
+
const buffer = Buffer.alloc(bytes);
|
|
78
|
+
const { bytesRead } = await handle.read(buffer, 0, bytes, 0);
|
|
79
|
+
const lines = buffer.subarray(0, bytesRead).toString("utf8").split("\n");
|
|
80
|
+
// The last line may be cut off mid-record.
|
|
81
|
+
return bytesRead === bytes ? lines.slice(0, -1) : lines;
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
await handle.close();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// The end of a long transcript, where titles set later in the session are appended.
|
|
88
|
+
async function tail(file) {
|
|
89
|
+
const handle = await open(file, "r");
|
|
90
|
+
try {
|
|
91
|
+
const { size } = await handle.stat();
|
|
92
|
+
if (size <= HEAD_BYTES)
|
|
93
|
+
return [];
|
|
94
|
+
const buffer = Buffer.alloc(HEAD_BYTES);
|
|
95
|
+
const { bytesRead } = await handle.read(buffer, 0, HEAD_BYTES, size - HEAD_BYTES);
|
|
96
|
+
// The first line may start mid-record.
|
|
97
|
+
return buffer.subarray(0, bytesRead).toString("utf8").split("\n").slice(1);
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
await handle.close();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function records(lines) {
|
|
104
|
+
const out = [];
|
|
105
|
+
for (const line of lines) {
|
|
106
|
+
if (!line.trim())
|
|
107
|
+
continue;
|
|
108
|
+
try {
|
|
109
|
+
const value = JSON.parse(line);
|
|
110
|
+
if (value && typeof value === "object")
|
|
111
|
+
out.push(value);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// a partial or foreign line
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
async function filesUnder(directory, match, depth) {
|
|
120
|
+
const entries = await readdir(directory, { withFileTypes: true }).catch(() => []);
|
|
121
|
+
const found = [];
|
|
122
|
+
for (const entry of entries) {
|
|
123
|
+
const target = path.join(directory, entry.name);
|
|
124
|
+
if (entry.isFile() && match(entry.name))
|
|
125
|
+
found.push(target);
|
|
126
|
+
else if (depth > 0 && entry.isDirectory() && !entry.name.startsWith("."))
|
|
127
|
+
found.push(...(await filesUnder(target, match, depth - 1)));
|
|
128
|
+
}
|
|
129
|
+
return found;
|
|
130
|
+
}
|
|
131
|
+
const at = (value) => {
|
|
132
|
+
const time = typeof value === "number" ? value : Date.parse(String(value ?? ""));
|
|
133
|
+
return Number.isFinite(time) ? new Date(time).toISOString() : undefined;
|
|
134
|
+
};
|
|
135
|
+
const object = (value) => value && typeof value === "object" ? value : {};
|
|
136
|
+
const readers = [
|
|
137
|
+
{
|
|
138
|
+
client: "claude-code",
|
|
139
|
+
files: (home) => filesUnder(path.join(home, ".claude", "projects"), (name) => name.endsWith(".jsonl"), 1),
|
|
140
|
+
async parse(file) {
|
|
141
|
+
const lines = records(await head(file));
|
|
142
|
+
const end = records(await tail(file));
|
|
143
|
+
// The title the owner set, then Claude Code's own, then the first prompt.
|
|
144
|
+
const named = (type, key) => [...lines, ...end].reverse().find((line) => line.type === type)?.[key];
|
|
145
|
+
const prompt = firstPrompt(lines
|
|
146
|
+
.filter((line) => line.type === "user" && !line.isMeta)
|
|
147
|
+
.map((line) => object(line.message).content));
|
|
148
|
+
return {
|
|
149
|
+
cwd: lines.find((line) => typeof line.cwd === "string")?.cwd,
|
|
150
|
+
title: title(named("custom-title", "customTitle") ??
|
|
151
|
+
named("ai-title", "aiTitle") ??
|
|
152
|
+
named("summary", "summary") ??
|
|
153
|
+
prompt),
|
|
154
|
+
};
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
client: "codex",
|
|
159
|
+
files: (home) => filesUnder(path.join(home, ".codex", "sessions"), (name) => name.startsWith("rollout-") && /\.jsonl(\.zst)?$/.test(name), 4),
|
|
160
|
+
async parse(file) {
|
|
161
|
+
// The first prompt follows tens of kilobytes of instructions and injected context.
|
|
162
|
+
const lines = records(await head(file, 4 * HEAD_BYTES));
|
|
163
|
+
const meta = object(lines.find((line) => line.type === "session_meta")?.payload);
|
|
164
|
+
const prompt = firstPrompt(lines
|
|
165
|
+
.map((line) => object(line.payload))
|
|
166
|
+
.filter((item) => item.type === "message" && item.role === "user")
|
|
167
|
+
.map((item) => item.content));
|
|
168
|
+
return {
|
|
169
|
+
id: typeof meta.id === "string" ? meta.id : undefined,
|
|
170
|
+
cwd: typeof meta.cwd === "string" ? meta.cwd : undefined,
|
|
171
|
+
title: title(prompt),
|
|
172
|
+
};
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
// Codex's own one-page summary of each past session.
|
|
177
|
+
client: "codex-summary",
|
|
178
|
+
files: (home) => filesUnder(path.join(home, ".codex", "memories", "rollout_summaries"), (name) => name.endsWith(".md"), 0),
|
|
179
|
+
async parse(file) {
|
|
180
|
+
const slug = path
|
|
181
|
+
.basename(file, ".md")
|
|
182
|
+
.replace(/^\d{4}-\d\d-\d\dT[\d-]+-[^-]+-/, "");
|
|
183
|
+
return { title: title(slug.replace(/_/g, " ")) };
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
client: "gemini",
|
|
188
|
+
files: (home) => filesUnder(path.join(home, ".gemini", "tmp"), (name) => name.startsWith("session") && name.endsWith(".jsonl"), 2),
|
|
189
|
+
async parse(file) {
|
|
190
|
+
// One `$set` line can hold every message so far: read the whole file, within a bound.
|
|
191
|
+
const lines = records(await head(file, 32 * HEAD_BYTES));
|
|
192
|
+
const first = lines[0] ?? {};
|
|
193
|
+
const root = await readFile(path.join(path.dirname(path.dirname(file)), ".project_root"), "utf8").catch(() => "");
|
|
194
|
+
// Messages are lines of their own, or batched under `$set.messages`.
|
|
195
|
+
const messages = lines.flatMap((line) => {
|
|
196
|
+
const batch = object(line.$set).messages;
|
|
197
|
+
return Array.isArray(batch) ? batch.map(object) : [line];
|
|
198
|
+
});
|
|
199
|
+
const prompt = firstPrompt(messages
|
|
200
|
+
.filter((message) => message.type === "user")
|
|
201
|
+
.map((message) => message.content));
|
|
202
|
+
return {
|
|
203
|
+
id: typeof first.sessionId === "string" ? first.sessionId : undefined,
|
|
204
|
+
cwd: root.trim() || undefined,
|
|
205
|
+
updated: at(first.lastUpdated),
|
|
206
|
+
title: title(prompt),
|
|
207
|
+
};
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
client: "cursor",
|
|
212
|
+
files: (home) => filesUnder(path.join(home, ".cursor", "chats"), (name) => name === "meta.json", 2),
|
|
213
|
+
async parse(file) {
|
|
214
|
+
const meta = object(JSON.parse(await readFile(file, "utf8")));
|
|
215
|
+
return {
|
|
216
|
+
id: path.basename(path.dirname(file)),
|
|
217
|
+
cwd: typeof meta.cwd === "string" ? meta.cwd : undefined,
|
|
218
|
+
updated: at(meta.updatedAtMs),
|
|
219
|
+
title: title(meta.title),
|
|
220
|
+
};
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
// Claude Desktop's Code sessions; their transcripts are Claude Code's, under ~/.claude.
|
|
225
|
+
client: "claude-desktop",
|
|
226
|
+
files: (home) => filesUnder(path.join(home, "Library", "Application Support", "Claude", "claude-code-sessions"), (name) => name.startsWith("local_") && name.endsWith(".json"), 2),
|
|
227
|
+
async parse(file) {
|
|
228
|
+
const session = object(JSON.parse(await readFile(file, "utf8")));
|
|
229
|
+
return {
|
|
230
|
+
id: typeof session.sessionId === "string" ? session.sessionId : undefined,
|
|
231
|
+
cwd: typeof session.cwd === "string" ? session.cwd : undefined,
|
|
232
|
+
updated: at(session.lastActivityAt),
|
|
233
|
+
title: title(session.title),
|
|
234
|
+
cliSession: typeof session.cliSessionId === "string"
|
|
235
|
+
? session.cliSessionId
|
|
236
|
+
: undefined,
|
|
237
|
+
};
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
];
|
|
241
|
+
// Antigravity lists its conversations in one SQLite file (the transcripts are protobuf). It is
|
|
242
|
+
// read from a private copy: the app keeps it open, and reading in place may need to write its
|
|
243
|
+
// lock files.
|
|
244
|
+
async function antigravityEntries(home) {
|
|
245
|
+
const folder = path.join(home, ".gemini", "antigravity");
|
|
246
|
+
const file = path.join(folder, "conversation_summaries.db");
|
|
247
|
+
if (!(await stat(file).catch(() => null)))
|
|
248
|
+
return [];
|
|
249
|
+
const scratch = await mkdtemp(path.join(os.tmpdir(), "ocean-agy-"));
|
|
250
|
+
let db;
|
|
251
|
+
try {
|
|
252
|
+
for (const suffix of ["", "-wal", "-shm"])
|
|
253
|
+
await copyFile(`${file}${suffix}`, path.join(scratch, `summaries.db${suffix}`)).catch(() => undefined);
|
|
254
|
+
db = new DatabaseSync(path.join(scratch, "summaries.db"), {
|
|
255
|
+
readOnly: true,
|
|
256
|
+
});
|
|
257
|
+
const rows = db
|
|
258
|
+
.prepare("select conversation_id, title, preview, last_modified_time, workspace_uris from conversation_summaries")
|
|
259
|
+
.all();
|
|
260
|
+
const transcripts = new Set(await readdir(path.join(folder, "conversations")).catch(() => []));
|
|
261
|
+
return rows.map((row) => {
|
|
262
|
+
const id = String(row.conversation_id);
|
|
263
|
+
let cwd;
|
|
264
|
+
try {
|
|
265
|
+
const uri = JSON.parse(String(row.workspace_uris || "[]"))[0];
|
|
266
|
+
cwd = uri?.startsWith("file:") ? fileURLToPath(uri) : undefined;
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
// no workspace recorded
|
|
270
|
+
}
|
|
271
|
+
const transcript = [`${id}.pb`, `${id}.db`].find((name) => transcripts.has(name));
|
|
272
|
+
return {
|
|
273
|
+
id,
|
|
274
|
+
file: transcript
|
|
275
|
+
? path.join(folder, "conversations", transcript)
|
|
276
|
+
: `${file}#${id}`,
|
|
277
|
+
cwd,
|
|
278
|
+
updated: at(String(row.last_modified_time ?? "").replace(" ", "T")),
|
|
279
|
+
title: title(String(row.title || row.preview || "")),
|
|
280
|
+
version: String(row.last_modified_time),
|
|
281
|
+
};
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
// another schema: nothing indexed this time
|
|
286
|
+
return [];
|
|
287
|
+
}
|
|
288
|
+
finally {
|
|
289
|
+
db?.close();
|
|
290
|
+
await rm(scratch, { recursive: true, force: true });
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
// OpenCode keeps its sessions in one SQLite file; each row is a conversation.
|
|
294
|
+
async function openCodeEntries(home) {
|
|
295
|
+
const file = path.join(home, ".local", "share", "opencode", "opencode.db");
|
|
296
|
+
if (!(await stat(file).catch(() => null)))
|
|
297
|
+
return [];
|
|
298
|
+
let db;
|
|
299
|
+
try {
|
|
300
|
+
db = new DatabaseSync(file, { readOnly: true });
|
|
301
|
+
const rows = db
|
|
302
|
+
.prepare("select id, title, directory, time_updated from session")
|
|
303
|
+
.all();
|
|
304
|
+
return rows.map((row) => ({
|
|
305
|
+
id: String(row.id),
|
|
306
|
+
file: `${file}#${String(row.id)}`,
|
|
307
|
+
cwd: typeof row.directory === "string" ? row.directory : undefined,
|
|
308
|
+
updated: at(row.time_updated),
|
|
309
|
+
title: title(row.title),
|
|
310
|
+
version: String(row.time_updated),
|
|
311
|
+
}));
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
// another schema or a locked file: nothing indexed this time
|
|
315
|
+
return [];
|
|
316
|
+
}
|
|
317
|
+
finally {
|
|
318
|
+
db?.close();
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
// Conversations that live only on a provider's servers (claude.ai, ChatGPT) have no local file.
|
|
322
|
+
// A capture writes one snapshot per client, `<system>/state/history/server/<client>.jsonl`, one
|
|
323
|
+
// line per conversation: {id, title, url, updated, project?, tags?}. The index keeps the link.
|
|
324
|
+
export const serverHistoryDir = () => oceanPath(SYSTEM_DIR, "state", "history", "server");
|
|
325
|
+
async function serverEntries() {
|
|
326
|
+
const directory = serverHistoryDir();
|
|
327
|
+
const names = await readdir(directory).catch(() => []);
|
|
328
|
+
const rows = [];
|
|
329
|
+
for (const name of names.filter((name) => name.endsWith(".jsonl"))) {
|
|
330
|
+
const text = await readFile(path.join(directory, name), "utf8").catch(() => "");
|
|
331
|
+
for (const row of records(text.split("\n"))) {
|
|
332
|
+
if (typeof row.id !== "string" || typeof row.url !== "string")
|
|
333
|
+
continue;
|
|
334
|
+
const updated = at(row.updated);
|
|
335
|
+
rows.push({
|
|
336
|
+
client: name.slice(0, -".jsonl".length),
|
|
337
|
+
id: row.id,
|
|
338
|
+
file: row.url,
|
|
339
|
+
updated,
|
|
340
|
+
title: title(row.title),
|
|
341
|
+
project: typeof row.project === "string" ? row.project : undefined,
|
|
342
|
+
tags: Array.isArray(row.tags)
|
|
343
|
+
? row.tags.filter((tag) => typeof tag === "string")
|
|
344
|
+
: undefined,
|
|
345
|
+
version: updated ?? "",
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return rows;
|
|
350
|
+
}
|
|
351
|
+
async function readIndex() {
|
|
352
|
+
const text = await readFile(historyIndexFile(), "utf8").catch(() => "");
|
|
353
|
+
return records(text.split("\n"));
|
|
354
|
+
}
|
|
355
|
+
// Rebuilds the index; a transcript whose size and time are unchanged is not read again, so a
|
|
356
|
+
// daily run reads only what is new. Projects are matched afresh every time: a folder bound since
|
|
357
|
+
// the last run moves its conversations.
|
|
358
|
+
export async function buildHistoryIndex(options = {}) {
|
|
359
|
+
const home = options.home ?? os.homedir();
|
|
360
|
+
const shown = (file) => file.startsWith(home) ? `~${file.slice(home.length)}` : file;
|
|
361
|
+
const previous = new Map((await readIndex()).map((entry) => [entry.file, entry]));
|
|
362
|
+
const projects = await loadProjectIndex(home);
|
|
363
|
+
const pulledAt = new Date().toISOString();
|
|
364
|
+
const entries = [];
|
|
365
|
+
const add = (client, file, parsed, fallback) => {
|
|
366
|
+
const place = placeFolder(projects, parsed.cwd);
|
|
367
|
+
const known = previous.get(file);
|
|
368
|
+
const updated = parsed.updated ?? fallback.updated;
|
|
369
|
+
const tags = parsed.tags ?? place.tags;
|
|
370
|
+
entries.push({
|
|
371
|
+
client,
|
|
372
|
+
project: parsed.project ?? place.project ?? "unassigned",
|
|
373
|
+
...(tags?.length ? { tags } : {}),
|
|
374
|
+
pull: true,
|
|
375
|
+
date: updated.slice(0, 10),
|
|
376
|
+
title: parsed.title ?? "",
|
|
377
|
+
file,
|
|
378
|
+
...(place.cwd ? { cwd: place.cwd } : {}),
|
|
379
|
+
id: parsed.id ??
|
|
380
|
+
path.basename(file).replace(/\.jsonl(\.zst)?$|\.[^.]+$/, ""),
|
|
381
|
+
updated,
|
|
382
|
+
// First seen, kept across runs.
|
|
383
|
+
pulled_at: known?.pulled_at ?? pulledAt,
|
|
384
|
+
bytes: fallback.bytes,
|
|
385
|
+
...(parsed.cliSession ? { cliSession: parsed.cliSession } : {}),
|
|
386
|
+
version: fallback.version,
|
|
387
|
+
});
|
|
388
|
+
};
|
|
389
|
+
for (const reader of readers)
|
|
390
|
+
for (const file of await reader.files(home)) {
|
|
391
|
+
const info = await stat(file).catch(() => null);
|
|
392
|
+
if (!info)
|
|
393
|
+
continue;
|
|
394
|
+
const version = `${info.size}:${info.mtimeMs}`;
|
|
395
|
+
const known = previous.get(shown(file));
|
|
396
|
+
const parsed = known?.client === reader.client && known.version === version
|
|
397
|
+
? {
|
|
398
|
+
id: known.id,
|
|
399
|
+
cwd: known.cwd,
|
|
400
|
+
updated: known.updated,
|
|
401
|
+
title: known.title,
|
|
402
|
+
cliSession: known.cliSession,
|
|
403
|
+
}
|
|
404
|
+
: await reader.parse(file).catch(() => ({}));
|
|
405
|
+
add(reader.client, shown(file), parsed, {
|
|
406
|
+
updated: info.mtime.toISOString(),
|
|
407
|
+
bytes: info.size,
|
|
408
|
+
version,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
for (const row of await openCodeEntries(home))
|
|
412
|
+
add("opencode", shown(row.file), row, {
|
|
413
|
+
updated: new Date(0).toISOString(),
|
|
414
|
+
bytes: 0,
|
|
415
|
+
version: row.version,
|
|
416
|
+
});
|
|
417
|
+
for (const row of await antigravityEntries(home))
|
|
418
|
+
add("antigravity", shown(row.file), row, {
|
|
419
|
+
updated: new Date(0).toISOString(),
|
|
420
|
+
bytes: 0,
|
|
421
|
+
version: row.version,
|
|
422
|
+
});
|
|
423
|
+
for (const row of await serverEntries())
|
|
424
|
+
add(row.client, row.file, row, {
|
|
425
|
+
updated: new Date(0).toISOString(),
|
|
426
|
+
bytes: 0,
|
|
427
|
+
version: row.version,
|
|
428
|
+
});
|
|
429
|
+
// Claude Desktop's Code sessions are Claude Code sessions: one line, marked.
|
|
430
|
+
const claudeCode = new Map(entries
|
|
431
|
+
.filter((entry) => entry.client === "claude-code")
|
|
432
|
+
.map((entry) => [entry.id, entry]));
|
|
433
|
+
const merged = entries.filter((entry) => {
|
|
434
|
+
const same = entry.client === "claude-desktop" && entry.cliSession
|
|
435
|
+
? claudeCode.get(entry.cliSession)
|
|
436
|
+
: undefined;
|
|
437
|
+
if (!same)
|
|
438
|
+
return true;
|
|
439
|
+
same.alsoIn = ["claude-desktop"];
|
|
440
|
+
if (!same.title)
|
|
441
|
+
same.title = entry.title;
|
|
442
|
+
return false;
|
|
443
|
+
});
|
|
444
|
+
merged.sort((a, b) => b.updated.localeCompare(a.updated));
|
|
445
|
+
if (options.write) {
|
|
446
|
+
await mkdir(path.dirname(historyIndexFile()), {
|
|
447
|
+
recursive: true,
|
|
448
|
+
mode: 0o700,
|
|
449
|
+
});
|
|
450
|
+
await atomicWrite(historyIndexFile(), merged.map((entry) => JSON.stringify(entry)).join("\n") +
|
|
451
|
+
(merged.length ? "\n" : ""), 0o600);
|
|
452
|
+
}
|
|
453
|
+
return merged;
|
|
454
|
+
}
|
|
455
|
+
// Every word must appear in the title, project, tags, folder, client or file; newest first.
|
|
456
|
+
export async function searchHistory(query, options = {}) {
|
|
457
|
+
const words = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
458
|
+
return (await readIndex())
|
|
459
|
+
.filter((entry) => !options.client || entry.client === options.client)
|
|
460
|
+
.filter((entry) => !options.project || entry.project === options.project)
|
|
461
|
+
.filter((entry) => {
|
|
462
|
+
const text = [
|
|
463
|
+
entry.title,
|
|
464
|
+
entry.project,
|
|
465
|
+
...(entry.tags ?? []),
|
|
466
|
+
entry.cwd ?? "",
|
|
467
|
+
entry.client,
|
|
468
|
+
entry.file,
|
|
469
|
+
]
|
|
470
|
+
.join(" ")
|
|
471
|
+
.toLowerCase();
|
|
472
|
+
return words.every((word) => text.includes(word));
|
|
473
|
+
})
|
|
474
|
+
.slice(0, options.limit ?? 20);
|
|
475
|
+
}
|
|
476
|
+
//# sourceMappingURL=history-index.js.map
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { expandHome, listProjectManifests, projectFormerPaths, projectTags, } from "@ocean/core/shared/project-resolution.js";
|
|
4
|
+
export const UNASSIGNED = "unassigned";
|
|
5
|
+
export const SCRATCH = "scratch";
|
|
6
|
+
const lower = (value) => value.toLowerCase();
|
|
7
|
+
export async function loadProjectIndex(home = os.homedir()) {
|
|
8
|
+
const projects = [];
|
|
9
|
+
for (const manifest of await listProjectManifests()) {
|
|
10
|
+
// A malformed optional list must not drop the project from sorting.
|
|
11
|
+
const safe = (read) => {
|
|
12
|
+
try {
|
|
13
|
+
return read();
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const former = safe(() => projectFormerPaths(manifest));
|
|
20
|
+
const folders = [...manifest.paths, ...former].map((entry) => path.resolve(expandHome(entry)));
|
|
21
|
+
projects.push({
|
|
22
|
+
id: manifest.id,
|
|
23
|
+
// A project is also known by its old folders' names (atlas for ocean) — not by its
|
|
24
|
+
// current folders' names, which can be as generic as `dashboard`.
|
|
25
|
+
names: [
|
|
26
|
+
...new Set([
|
|
27
|
+
manifest.id,
|
|
28
|
+
manifest.name,
|
|
29
|
+
...former.map((entry) => path.basename(expandHome(entry))),
|
|
30
|
+
]
|
|
31
|
+
.filter(Boolean)
|
|
32
|
+
.map(lower)),
|
|
33
|
+
],
|
|
34
|
+
tags: safe(() => projectTags(manifest)),
|
|
35
|
+
folders,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return { projects, home };
|
|
39
|
+
}
|
|
40
|
+
// `folder` is `root` or below it, in the platform's own separator (Windows resolves to `\`).
|
|
41
|
+
const under = (folder, root) => folder === root ||
|
|
42
|
+
folder.startsWith(root.endsWith(path.sep) ? root : `${root}${path.sep}`);
|
|
43
|
+
// macOS folder names ignore case: ~/Documents/ameer/Ocean-UIUX-MCP is the bound ocean-UIUX-MCP.
|
|
44
|
+
const within = (folder, root) => under(lower(folder), lower(root));
|
|
45
|
+
function isScratch(folder, home) {
|
|
46
|
+
const roots = [
|
|
47
|
+
os.tmpdir(),
|
|
48
|
+
...["/tmp", "/private/tmp", "/var/folders", "/private/var/folders"].map((root) => path.resolve(root)),
|
|
49
|
+
path.join(home, "Library", "Application Support", "Claude", "scratch-workspaces"),
|
|
50
|
+
];
|
|
51
|
+
return (folder === path.parse(folder).root ||
|
|
52
|
+
folder === home ||
|
|
53
|
+
roots.some((root) => under(folder, root)) ||
|
|
54
|
+
/(^|[-_/])scratch([-_/]|$)/i.test(path.basename(folder)));
|
|
55
|
+
}
|
|
56
|
+
// A conversation's working folder → its project.
|
|
57
|
+
export function placeFolder(index, cwd) {
|
|
58
|
+
if (!cwd)
|
|
59
|
+
return { project: UNASSIGNED };
|
|
60
|
+
const folder = path.resolve(expandHome(cwd.trim()));
|
|
61
|
+
const shown = folder.startsWith(index.home)
|
|
62
|
+
? `~${folder.slice(index.home.length)}`
|
|
63
|
+
: folder;
|
|
64
|
+
let best;
|
|
65
|
+
for (const project of index.projects)
|
|
66
|
+
for (const root of project.folders)
|
|
67
|
+
if (within(folder, root) && root.length > (best?.length ?? -1))
|
|
68
|
+
best = { id: project.id, tags: project.tags, length: root.length };
|
|
69
|
+
if (best)
|
|
70
|
+
return { project: best.id, tags: best.tags, cwd: shown, target: true };
|
|
71
|
+
if (isScratch(folder, index.home))
|
|
72
|
+
return { project: SCRATCH, cwd: shown };
|
|
73
|
+
// The deepest folder on the path named like a project (moved folders keep their names).
|
|
74
|
+
for (const part of folder.split(path.sep).filter(Boolean).reverse()) {
|
|
75
|
+
const project = index.projects.find((item) => item.names.includes(lower(part.trim())));
|
|
76
|
+
if (project)
|
|
77
|
+
return {
|
|
78
|
+
project: project.id,
|
|
79
|
+
tags: project.tags,
|
|
80
|
+
cwd: shown,
|
|
81
|
+
target: true,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return { project: UNASSIGNED, cwd: shown };
|
|
85
|
+
}
|
|
86
|
+
// The project whose name appears in a phrase as whole words; the longest name wins, so
|
|
87
|
+
// "Ocean IELTS plan" is ocean-ielts, not ocean.
|
|
88
|
+
const words = (value) => `-${value
|
|
89
|
+
.toLowerCase()
|
|
90
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
91
|
+
.replace(/^-+|-+$/g, "")}-`;
|
|
92
|
+
function projectNamedIn(index, phrase) {
|
|
93
|
+
const text = words(phrase);
|
|
94
|
+
return index.projects
|
|
95
|
+
.flatMap((project) => project.names.map((name) => ({ project, name: words(name) })))
|
|
96
|
+
.filter(({ name }) => name !== "--" && text.includes(name))
|
|
97
|
+
.sort((a, b) => b.name.length - a.name.length)[0]?.project;
|
|
98
|
+
}
|
|
99
|
+
// Claude Code names a project's folder by its path with every non-alphanumeric as `-`.
|
|
100
|
+
const claudeDir = (folder) => folder.replace(/[^A-Za-z0-9]/g, "-");
|
|
101
|
+
// A memory text → its project: Claude Code's per-project memory by its folder, a claude.ai
|
|
102
|
+
// project by its name, anything else by a project named in its first heading.
|
|
103
|
+
export function placeMemory(index, origin) {
|
|
104
|
+
if (origin.client === "claude-code") {
|
|
105
|
+
const dir = /\.claude[\\/]projects[\\/]([^\\/]+)[\\/]memory[\\/]/.exec(origin.file)?.[1];
|
|
106
|
+
if (dir) {
|
|
107
|
+
let best;
|
|
108
|
+
for (const project of index.projects)
|
|
109
|
+
for (const root of project.folders) {
|
|
110
|
+
const encoded = claudeDir(root);
|
|
111
|
+
if ((lower(dir) === lower(encoded) ||
|
|
112
|
+
lower(dir).startsWith(`${lower(encoded)}-`)) &&
|
|
113
|
+
encoded.length > (best?.length ?? -1))
|
|
114
|
+
best = {
|
|
115
|
+
id: project.id,
|
|
116
|
+
tags: project.tags,
|
|
117
|
+
length: encoded.length,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (best)
|
|
121
|
+
return { project: best.id, tags: best.tags, target: true };
|
|
122
|
+
return { project: UNASSIGNED };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const named = origin.name ?? /^#{1,6}\s+(.+)$/m.exec(origin.text)?.[1];
|
|
126
|
+
const project = named ? projectNamedIn(index, named) : undefined;
|
|
127
|
+
if (project)
|
|
128
|
+
return { project: project.id, tags: project.tags, target: true };
|
|
129
|
+
// Global memory and instructions are about the owner, not one project.
|
|
130
|
+
return { project: "personal" };
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=project-match.js.map
|