@midhunxavier/code-flow 0.0.0-stage → 0.1.0-preview.3
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 +141 -2
- package/bin/codeflow.mjs +8 -0
- package/bin/launcher.mjs +127 -0
- package/dist/remote-host.json +1 -0
- package/licenses/inter.txt +92 -0
- package/licenses/preact.txt +21 -0
- package/licenses/signals-core.txt +21 -0
- package/licenses/signals.txt +21 -0
- package/licenses/typescript-notices.txt +193 -0
- package/licenses/typescript.txt +55 -0
- package/licenses/uqr.txt +22 -0
- package/licenses/xterm-fit.txt +19 -0
- package/licenses/xterm.txt +21 -0
- package/package.json +34 -4
- package/public/assets/changes-62ZIEQSN.js +1 -0
- package/public/assets/chunk-2PZVOKID.js +1 -0
- package/public/assets/chunk-4D5IENEB.js +1 -0
- package/public/assets/chunk-5SARP5MU.js +1 -0
- package/public/assets/chunk-63YLZ3PR.js +1 -0
- package/public/assets/chunk-7FUHKF2M.js +1 -0
- package/public/assets/chunk-APQEVJ2K.js +1 -0
- package/public/assets/chunk-CGOZQMEZ.js +2 -0
- package/public/assets/chunk-J4VYS3KV.js +2 -0
- package/public/assets/chunk-L7TUCIYF.js +1 -0
- package/public/assets/chunk-SIHDWM2Y.js +154 -0
- package/public/assets/chunk-TEJVYIKC.js +1 -0
- package/public/assets/code-browser-VB2HGZSG.js +4 -0
- package/public/assets/filtered-tasks-ES5CP4KV.js +1 -0
- package/public/assets/interactive-browser-7F5ZXRUV.js +7 -0
- package/public/assets/join-project-6RTFHNCA.js +1 -0
- package/public/assets/main-65EW73SB.js +1 -0
- package/public/assets/project-invitations-view-GMPWGWYM.js +1 -0
- package/public/assets/settings-UB5Q4AQM.js +9 -0
- package/public/assets/styles-18792BB4.css +32 -0
- package/public/assets/team-server-wizard-2GLDWS6D.js +8 -0
- package/public/assets/terminal-IWAIM4DE.js +62 -0
- package/public/assets.json +1 -0
- package/public/bundle-meta.json +1 -0
- package/public/docs/.nojekyll +0 -0
- package/public/docs/accessibility.html +19 -0
- package/public/docs/acp-agents.html +117 -0
- package/public/docs/agent-containers.html +79 -0
- package/public/docs/api.html +1130 -0
- package/public/docs/app.js +342 -0
- package/public/docs/application-improvements.html +275 -0
- package/public/docs/architecture-explorer.html +19 -0
- package/public/docs/architecture.html +149 -0
- package/public/docs/auto-team.html +148 -0
- package/public/docs/automations.html +109 -0
- package/public/docs/checkpoint-review.html +37 -0
- package/public/docs/code-hosts.html +35 -0
- package/public/docs/code-intelligence.html +35 -0
- package/public/docs/configuration.html +346 -0
- package/public/docs/context-inspection.html +23 -0
- package/public/docs/coordinated-changes.html +28 -0
- package/public/docs/deferred-runs.html +25 -0
- package/public/docs/dependency-discovery.html +19 -0
- package/public/docs/documentation.html +84 -0
- package/public/docs/dynamic-teams-evaluation.html +174 -0
- package/public/docs/dynamic-teams-implementation-plan.html +882 -0
- package/public/docs/dynamic-teams-implementation-review-1.html +141 -0
- package/public/docs/dynamic-teams-plan-review-0.html +204 -0
- package/public/docs/extensions.html +42 -0
- package/public/docs/external-sources.html +96 -0
- package/public/docs/filesystem-policy.html +26 -0
- package/public/docs/filtered-tasks.html +44 -0
- package/public/docs/font-license.txt +92 -0
- package/public/docs/getting-started.html +143 -0
- package/public/docs/github-review-threads.html +26 -0
- package/public/docs/github.html +157 -0
- package/public/docs/help.html +69 -0
- package/public/docs/index.html +88 -0
- package/public/docs/installation.html +80 -0
- package/public/docs/launch-profiles.html +48 -0
- package/public/docs/maintenance.html +60 -0
- package/public/docs/memory-claims.html +31 -0
- package/public/docs/memory-storage.html +58 -0
- package/public/docs/notifications.html +39 -0
- package/public/docs/patterns.html +195 -0
- package/public/docs/performance-results.html +210 -0
- package/public/docs/performance.html +166 -0
- package/public/docs/project-conventions.html +22 -0
- package/public/docs/project-first-navigation-plan.html +587 -0
- package/public/docs/project-teams-implementation-plan.html +772 -0
- package/public/docs/project-teams.html +129 -0
- package/public/docs/provider-compatibility.html +20 -0
- package/public/docs/releasing.html +65 -0
- package/public/docs/remote-workers.html +64 -0
- package/public/docs/review-inbox.html +26 -0
- package/public/docs/run-storage.html +23 -0
- package/public/docs/runs.html +141 -0
- package/public/docs/search.json +1 -0
- package/public/docs/security.html +133 -0
- package/public/docs/shared-projects.html +31 -0
- package/public/docs/ssh-projects.html +60 -0
- package/public/docs/style.css +768 -0
- package/public/docs/task-reruns.html +30 -0
- package/public/docs/team-collaboration.html +412 -0
- package/public/docs/verification.html +22 -0
- package/public/docs/vscode.html +53 -0
- package/public/docs/worker-permission-verification.html +131 -0
- package/public/index.html +17 -0
- package/scripts/check-node.mjs +7 -0
- package/scripts/codeflow.mjs +55 -0
- package/scripts/computer-use.swift +64 -0
- package/scripts/pty.py +273 -0
- package/src/acp-connections.js +110 -0
- package/src/adapters/acp-presets.js +37 -0
- package/src/adapters/acp-types.js +0 -0
- package/src/adapters/acp.js +328 -0
- package/src/adapters/agent-tools.js +28 -0
- package/src/adapters/agy-auth.js +67 -0
- package/src/adapters/agy-isolation.js +242 -0
- package/src/adapters/agy-tools.js +45 -0
- package/src/adapters/agy.js +326 -0
- package/src/adapters/claude-stream.js +58 -0
- package/src/adapters/claude.js +282 -0
- package/src/adapters/codex-app-server.js +555 -0
- package/src/adapters/codex.js +311 -0
- package/src/adapters/errors.js +57 -0
- package/src/adapters/index.js +91 -0
- package/src/adapters/managed-permissions.js +37 -0
- package/src/adapters/model-discovery.js +100 -0
- package/src/adapters/process.js +433 -0
- package/src/adapters/team-tools.js +103 -0
- package/src/adapters/types.js +0 -0
- package/src/agent-browser.js +688 -0
- package/src/agent-computer.js +88 -0
- package/src/agent-container.js +155 -0
- package/src/agent-team-definitions.js +131 -0
- package/src/agent-tool-client.mjs +79 -0
- package/src/agent-tools.js +296 -0
- package/src/approvals.js +134 -0
- package/src/architecture.js +456 -0
- package/src/assets.js +44 -0
- package/src/automation.js +901 -0
- package/src/browser-files.js +293 -0
- package/src/browser-ownership.js +150 -0
- package/src/certificate-renewal.js +75 -0
- package/src/chat-family.js +25 -0
- package/src/chat-handoff.js +87 -0
- package/src/checkpoint-review.js +72 -0
- package/src/checks.js +303 -0
- package/src/code-host-git.js +102 -0
- package/src/code-hosts-api.js +99 -0
- package/src/code-hosts.js +482 -0
- package/src/code-intelligence-worker.mjs +452 -0
- package/src/code-intelligence.js +141 -0
- package/src/collab/api.js +365 -0
- package/src/collab/checkpoints.js +198 -0
- package/src/collab/connections.js +139 -0
- package/src/collab/host-execution.js +347 -0
- package/src/collab/hub-client.js +210 -0
- package/src/collab/link-api.js +229 -0
- package/src/collab/link.js +595 -0
- package/src/collab/model.js +496 -0
- package/src/collab/runner.js +1014 -0
- package/src/collab/store.js +1452 -0
- package/src/collab/terminals.js +361 -0
- package/src/collaboration-reviews.js +116 -0
- package/src/config.js +48 -0
- package/src/context-budget.js +14 -0
- package/src/context-inspection.js +204 -0
- package/src/context.js +154 -0
- package/src/conventions.js +283 -0
- package/src/coordinated-api.js +247 -0
- package/src/deferred-launch.js +82 -0
- package/src/deferred-runs.js +515 -0
- package/src/dependency-health.js +35 -0
- package/src/diff.js +43 -0
- package/src/docs-help.js +316 -0
- package/src/doctor.js +232 -0
- package/src/documentation-changes.js +20 -0
- package/src/engine.js +5258 -0
- package/src/env.js +44 -0
- package/src/execution-budget.js +8 -0
- package/src/experience.js +50 -0
- package/src/extensions.js +144 -0
- package/src/filesystem-access.js +107 -0
- package/src/filesystem-policy.js +238 -0
- package/src/filtered-task-limits.js +93 -0
- package/src/filtered-tasks-api.js +53 -0
- package/src/filtered-tasks.js +576 -0
- package/src/frame-policy.js +66 -0
- package/src/fs.js +57 -0
- package/src/git.js +626 -0
- package/src/github-review-threads.js +181 -0
- package/src/github.js +538 -0
- package/src/http.js +69 -0
- package/src/improve-settings.js +38 -0
- package/src/improve.js +187 -0
- package/src/integrations.js +373 -0
- package/src/integrity.js +259 -0
- package/src/journey-api.js +77 -0
- package/src/journey-replay.js +65 -0
- package/src/journey-store.js +97 -0
- package/src/journeys.js +60 -0
- package/src/json.js +81 -0
- package/src/launch-profiles.js +292 -0
- package/src/learnings.js +80 -0
- package/src/local-app-url.js +19 -0
- package/src/log.js +39 -0
- package/src/maintenance.js +335 -0
- package/src/memory-analysis.js +182 -0
- package/src/memory-claims.js +153 -0
- package/src/memory-sqlite.js +236 -0
- package/src/memory.js +552 -0
- package/src/merge-validation.js +58 -0
- package/src/model-catalog.js +239 -0
- package/src/model-research.js +91 -0
- package/src/notes.js +176 -0
- package/src/notifications.js +413 -0
- package/src/patterns/auto-team-model.js +230 -0
- package/src/patterns/auto-team.js +816 -0
- package/src/patterns/council.js +199 -0
- package/src/patterns/debate.js +188 -0
- package/src/patterns/findings.js +141 -0
- package/src/patterns/index.js +21 -0
- package/src/patterns/loop.js +158 -0
- package/src/patterns/pipeline.js +125 -0
- package/src/patterns/prompts.js +181 -0
- package/src/patterns/race.js +147 -0
- package/src/patterns/schemas.js +40 -0
- package/src/patterns/solo.js +46 -0
- package/src/patterns/supervisor.js +235 -0
- package/src/patterns/task-draft.js +29 -0
- package/src/patterns/task-review.js +45 -0
- package/src/patterns/tdd.js +231 -0
- package/src/patterns/team-strategies.js +67 -0
- package/src/performance.js +43 -0
- package/src/persistence-status.js +35 -0
- package/src/phone-sessions.js +96 -0
- package/src/preview-overlay.js +392 -0
- package/src/preview.js +230 -0
- package/src/privacy.js +23 -0
- package/src/project-report.js +22 -0
- package/src/project-team-budget.js +22 -0
- package/src/project-team-model.js +225 -0
- package/src/project-team-policy.js +42 -0
- package/src/project-team-store.js +234 -0
- package/src/project-team-usage.js +24 -0
- package/src/project-teams-api.js +43 -0
- package/src/project-teams.js +644 -0
- package/src/projects.js +290 -0
- package/src/provider-eligibility.js +9 -0
- package/src/readiness.js +98 -0
- package/src/remote-tls.js +66 -0
- package/src/remote-workers/bundles.js +198 -0
- package/src/remote-workers/client.js +332 -0
- package/src/remote-workers/durability.js +37 -0
- package/src/remote-workers/protocol.js +68 -0
- package/src/remote-workers/stdio.js +138 -0
- package/src/remote-workers/worker.js +304 -0
- package/src/remote.js +204 -0
- package/src/repo-config.js +201 -0
- package/src/repo-coordination.js +725 -0
- package/src/repo-tree.js +106 -0
- package/src/repository-identity.js +13 -0
- package/src/retention-policy.js +466 -0
- package/src/review-inbox.js +38 -0
- package/src/routing.js +49 -0
- package/src/run-analysis.js +111 -0
- package/src/run-cold-store.js +231 -0
- package/src/run-environment.js +15 -0
- package/src/run-history.js +91 -0
- package/src/run-log.js +160 -0
- package/src/run-permissions.js +111 -0
- package/src/run-projection.js +77 -0
- package/src/run-recovery.js +53 -0
- package/src/run-status.js +56 -0
- package/src/serve.js +138 -0
- package/src/server.js +2871 -0
- package/src/shared-access.js +103 -0
- package/src/shared-accounts.js +381 -0
- package/src/shared-api.js +239 -0
- package/src/shell.js +346 -0
- package/src/ship.js +344 -0
- package/src/slots.js +74 -0
- package/src/snapshot.js +388 -0
- package/src/source-context.js +17 -0
- package/src/sse-writer.js +70 -0
- package/src/ssh-projects/bootstrap.mjs +95 -0
- package/src/ssh-projects/config.js +45 -0
- package/src/ssh-projects/gateway.js +143 -0
- package/src/ssh-projects/manager.js +320 -0
- package/src/ssh-projects/team-autostart.mjs +93 -0
- package/src/ssh-projects/team-remove.mjs +69 -0
- package/src/ssh-projects/team-status.mjs +39 -0
- package/src/ssh-projects/transport.js +133 -0
- package/src/stats.js +139 -0
- package/src/store.js +544 -0
- package/src/stream.js +91 -0
- package/src/sweeps.js +241 -0
- package/src/tailscale.js +101 -0
- package/src/task-draft.js +23 -0
- package/src/task-team.js +164 -0
- package/src/team-evaluation.js +243 -0
- package/src/team-mailbox.js +160 -0
- package/src/team-memory.js +84 -0
- package/src/team-rerun-graph.js +15 -0
- package/src/team-reruns.js +130 -0
- package/src/team-scheduler.js +121 -0
- package/src/team-steering.js +21 -0
- package/src/team-template-store.js +98 -0
- package/src/team-templates.js +117 -0
- package/src/team-tool-client.mjs +79 -0
- package/src/team-tools.js +235 -0
- package/src/teams.js +0 -0
- package/src/templates.js +62 -0
- package/src/transcript.js +134 -0
- package/src/verification-integrity.js +27 -0
- package/src/verification.js +183 -0
- package/src/watch.js +195 -0
- package/src/work-queue.js +47 -0
- package/src/workflow-control.js +83 -0
- package/src/workspace-api.js +207 -0
- package/src/workspace-changes.js +61 -0
- package/src/workspace.js +146 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { AGY_ISOLATION_VERSION, AGY_VERIFIED_CLI_VERSIONS } from "./adapters/agy-isolation.js";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import { mkdir, open, readFile, rename, rm } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { hostname } from "node:os";
|
|
6
|
+
import { CODEFLOW_HOME } from "./config.js";
|
|
7
|
+
import { getAdapter } from "./adapters/index.js";
|
|
8
|
+
import { researchModels, validateDocumentation } from "./model-research.js";
|
|
9
|
+
const TTL = 10 * 6e4;
|
|
10
|
+
const ID = /^[A-Za-z0-9][\w.:[\]/@-]{0,199}$/;
|
|
11
|
+
const effortValid = (v) => typeof v === "string" && /^[a-z]{2,12}$/.test(v) && v !== "ultracode";
|
|
12
|
+
function modelEnvironment(context = {}, remote) {
|
|
13
|
+
return createHash("sha256").update(JSON.stringify({ host: hostname(), home: process.env.HOME, codexHome: process.env.CODEX_HOME, claudeHome: process.env.CLAUDE_CONFIG_DIR, path: process.env.PATH, container: context.agentContainer, ...context.isolatedAgy ? { isolation: `agy-${AGY_ISOLATION_VERSION}/${AGY_VERIFIED_CLI_VERSIONS.join(",")}` } : {}, remote })).digest("hex");
|
|
14
|
+
}
|
|
15
|
+
function validateCapabilityCatalog(value) {
|
|
16
|
+
const c = value;
|
|
17
|
+
if (!c || typeof c !== "object" || Array.isArray(c) || Object.keys(c).some((k) => !["schemaVersion", "revision", "provider", "environment", "accountKey", "defaultModel", "discoveredAt", "attemptedAt", "state", "issue", "models"].includes(k)))
|
|
18
|
+
throw new Error("Invalid capability catalog fields");
|
|
19
|
+
if (!c || c.schemaVersion !== 1 || !Number.isSafeInteger(c.revision) || c.revision < 0 || typeof c.provider !== "string" || !ID.test(c.provider) || !/^[a-f0-9]{64}$/.test(c.environment) || !["current", "stale", "unknown"].includes(c.state) || !Number.isFinite(c.discoveredAt) || !Number.isFinite(c.attemptedAt) || !Array.isArray(c.models) || c.models.length > 800 || c.accountKey !== void 0 && !/^[a-f0-9]{64}$/.test(c.accountKey))
|
|
20
|
+
throw new Error("Invalid capability catalog");
|
|
21
|
+
const seen = /* @__PURE__ */ new Set();
|
|
22
|
+
for (const m of c.models) {
|
|
23
|
+
if (!m || Object.keys(m).some((k) => !["id", "label", "aliases", "efforts", "defaultEffort", "availability", "compatibility", "source", "verifiedAt", "confidence", "documented", "researchAttemptedAt", "observations"].includes(k)) || typeof m.id !== "string" || !ID.test(m.id) || seen.has(m.id) || typeof m.label !== "string" || m.label.length > 120 || !Array.isArray(m.aliases) || m.aliases.length > 30 || m.aliases.some((a) => typeof a !== "string" || !ID.test(a)) || !(m.efforts === null || Array.isArray(m.efforts) && m.efforts.length <= 20 && m.efforts.every(effortValid)) || m.defaultEffort !== void 0 && !m.efforts?.includes(m.defaultEffort) || !["reported", "unavailable"].includes(m.availability) || !["advertised", "unknown"].includes(m.compatibility) || m.confidence !== "reported" || typeof m.source !== "string" || m.source.length > 120 || !Number.isFinite(m.verifiedAt) || !Array.isArray(m.observations) || m.observations.length > 30)
|
|
24
|
+
throw new Error("Invalid model capability record");
|
|
25
|
+
seen.add(m.id);
|
|
26
|
+
if (m.researchAttemptedAt !== void 0 && (!Number.isFinite(m.researchAttemptedAt) || m.researchAttemptedAt < 0))
|
|
27
|
+
throw new Error("Invalid research timestamp");
|
|
28
|
+
if (m.documented)
|
|
29
|
+
validateDocumentation(m.documented, c.provider, m.id);
|
|
30
|
+
for (const o of m.observations)
|
|
31
|
+
if (!o || Object.keys(o).some((k) => !["runId", "at", "ok", "effort", "durationMs", "errorKind"].includes(k)) || typeof o.runId !== "string" || o.runId.length > 200 || !Number.isFinite(o.at) || typeof o.ok !== "boolean" || typeof o.effort !== "string" || o.effort.length > 12 || !Number.isFinite(o.durationMs) || o.durationMs < 0 || o.errorKind !== void 0 && !/^[a-z_]{1,40}$/.test(o.errorKind))
|
|
32
|
+
throw new Error("Invalid model observation");
|
|
33
|
+
}
|
|
34
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
35
|
+
for (const model of c.models) for (const alias of model.aliases) {
|
|
36
|
+
if (seen.has(alias) && alias !== model.id || aliases.has(alias) && aliases.get(alias) !== model.id) throw new Error("Ambiguous model alias");
|
|
37
|
+
aliases.set(alias, model.id);
|
|
38
|
+
}
|
|
39
|
+
if (c.defaultModel !== void 0 && !seen.has(c.defaultModel))
|
|
40
|
+
throw new Error("Unknown default model");
|
|
41
|
+
return c;
|
|
42
|
+
}
|
|
43
|
+
function assignmentProblem(c, model, effort) {
|
|
44
|
+
const entry = c.models.find((m) => m.id === (model || c.defaultModel) || m.aliases.includes(model));
|
|
45
|
+
if (!entry || entry.availability !== "reported" || entry.compatibility !== "advertised")
|
|
46
|
+
return `Model ${model || "default"} is not reported by ${c.provider} in this execution environment. Refresh its catalog or choose an eligible model.`;
|
|
47
|
+
if (effort && !entry.efforts?.includes(effort))
|
|
48
|
+
return `${entry.id} does not report support for effort ${effort}; choose a listed setting or leave effort at its default`;
|
|
49
|
+
}
|
|
50
|
+
function resolveModelAssignment(c, model, effort, rationale) {
|
|
51
|
+
const problem = assignmentProblem(c, model, effort);
|
|
52
|
+
if (problem)
|
|
53
|
+
throw new Error(problem);
|
|
54
|
+
const m = c.models.find((m2) => m2.id === (model || c.defaultModel) || m2.aliases.includes(model));
|
|
55
|
+
return { provider: c.provider, model: m.id, effort, rationale: rationale.slice(0, 2e3), catalogRevision: c.revision, environment: c.environment, verifiedAt: m.verifiedAt, source: m.source, stale: c.state !== "current" };
|
|
56
|
+
}
|
|
57
|
+
function workerConstraintProblem(c, assignment, providers, pin) {
|
|
58
|
+
if (assignment.provider !== c.provider || providers && !providers.includes(assignment.provider))
|
|
59
|
+
return "Worker provider is outside the allowed pool";
|
|
60
|
+
if (!pin)
|
|
61
|
+
return;
|
|
62
|
+
const choices = pin.variants?.length ? [pin, ...pin.variants] : [pin];
|
|
63
|
+
const permits = choices.some((choice) => {
|
|
64
|
+
const pinnedModel = choice.model || (pin.variants?.length ? c.defaultModel : void 0);
|
|
65
|
+
const exact = pinnedModel && c.models.find((m) => m.id === pinnedModel || m.aliases.includes(pinnedModel))?.id;
|
|
66
|
+
return (!pinnedModel || exact === assignment.model) && (!choice.effort || choice.effort === assignment.effort);
|
|
67
|
+
});
|
|
68
|
+
if (!permits)
|
|
69
|
+
return "Worker assignment does not preserve the user's model/effort pins";
|
|
70
|
+
}
|
|
71
|
+
class ModelCatalogStore {
|
|
72
|
+
constructor(home = CODEFLOW_HOME, discover = (id, context) => {
|
|
73
|
+
const adapter = getAdapter(id);
|
|
74
|
+
if (!adapter.discoverModels)
|
|
75
|
+
return Promise.reject(new Error("Unsupported discovery"));
|
|
76
|
+
return adapter.discoverModels(context);
|
|
77
|
+
}, now = Date.now) {
|
|
78
|
+
this.home = home;
|
|
79
|
+
this.discover = discover;
|
|
80
|
+
this.now = now;
|
|
81
|
+
}
|
|
82
|
+
tail = Promise.resolve();
|
|
83
|
+
flights = /* @__PURE__ */ new Map();
|
|
84
|
+
path(provider, environment) {
|
|
85
|
+
return join(this.home, "model-catalogs", createHash("sha256").update(provider + environment).digest("hex") + ".json");
|
|
86
|
+
}
|
|
87
|
+
async read(provider, environment) {
|
|
88
|
+
try {
|
|
89
|
+
const text = await readFile(this.path(provider, environment), "utf8");
|
|
90
|
+
if (text.length > 4e6)
|
|
91
|
+
throw new Error("Catalog too large");
|
|
92
|
+
const c = validateCapabilityCatalog(JSON.parse(text));
|
|
93
|
+
if (c.provider !== provider || c.environment !== environment)
|
|
94
|
+
throw new Error("Catalog identity mismatch");
|
|
95
|
+
return c;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if (error.code === "ENOENT")
|
|
98
|
+
return;
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
save(c) {
|
|
103
|
+
const work = async () => {
|
|
104
|
+
const latest = await this.read(c.provider, c.environment);
|
|
105
|
+
if (latest?.accountKey === c.accountKey)
|
|
106
|
+
for (const m of c.models) {
|
|
107
|
+
const observations = [...latest?.models.find((v) => v.id === m.id)?.observations ?? [], ...m.observations];
|
|
108
|
+
m.observations = [...new Map(observations.map((o) => [`${o.runId}:${o.at}:${o.effort}`, o])).values()].sort((a, b) => a.at - b.at).slice(-30);
|
|
109
|
+
}
|
|
110
|
+
if (latest && latest.revision > c.revision) {
|
|
111
|
+
for (const m of latest.models)
|
|
112
|
+
if (latest.accountKey === c.accountKey)
|
|
113
|
+
m.observations = c.models.find((v) => v.id === m.id)?.observations ?? m.observations;
|
|
114
|
+
c = latest;
|
|
115
|
+
}
|
|
116
|
+
validateCapabilityCatalog(c);
|
|
117
|
+
const dir = join(this.home, "model-catalogs");
|
|
118
|
+
await mkdir(dir, { recursive: true });
|
|
119
|
+
const path = this.path(c.provider, c.environment), temp = path + "." + randomUUID();
|
|
120
|
+
try {
|
|
121
|
+
const file = await open(temp, "wx", 384);
|
|
122
|
+
try {
|
|
123
|
+
await file.writeFile(JSON.stringify(c));
|
|
124
|
+
await file.sync();
|
|
125
|
+
} finally {
|
|
126
|
+
await file.close();
|
|
127
|
+
}
|
|
128
|
+
await rename(temp, path);
|
|
129
|
+
if (c.revision > 0) {
|
|
130
|
+
const snapshot = path.replace(/\.json$/, `.v${c.revision}.json`);
|
|
131
|
+
try {
|
|
132
|
+
const evidence = await open(snapshot, "wx", 384);
|
|
133
|
+
try {
|
|
134
|
+
await evidence.writeFile(JSON.stringify(c));
|
|
135
|
+
await evidence.sync();
|
|
136
|
+
} finally {
|
|
137
|
+
await evidence.close();
|
|
138
|
+
}
|
|
139
|
+
} catch (e) {
|
|
140
|
+
if (e.code !== "EEXIST")
|
|
141
|
+
throw e;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const folder = await open(dir, "r");
|
|
145
|
+
try {
|
|
146
|
+
await folder.sync();
|
|
147
|
+
} finally {
|
|
148
|
+
await folder.close();
|
|
149
|
+
}
|
|
150
|
+
} finally {
|
|
151
|
+
await rm(temp, { force: true });
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
const promise = this.tail.then(work);
|
|
155
|
+
this.tail = promise.catch(() => {
|
|
156
|
+
});
|
|
157
|
+
return promise;
|
|
158
|
+
}
|
|
159
|
+
async settings(allowResearch) {
|
|
160
|
+
const path = join(this.home, "model-catalog-settings.json");
|
|
161
|
+
if (allowResearch !== void 0) {
|
|
162
|
+
await mkdir(this.home, { recursive: true });
|
|
163
|
+
const temp = path + "." + randomUUID();
|
|
164
|
+
await (await import("node:fs/promises")).writeFile(temp, JSON.stringify({ allowResearch }), { mode: 384 });
|
|
165
|
+
await rename(temp, path);
|
|
166
|
+
}
|
|
167
|
+
return readFile(path, "utf8").then((text) => ({ allowResearch: JSON.parse(text).allowResearch === true })).catch(() => ({ allowResearch: false }));
|
|
168
|
+
}
|
|
169
|
+
get(provider, options = {}) {
|
|
170
|
+
const environment = options.environment ?? modelEnvironment(options.context), key = provider + environment;
|
|
171
|
+
if (options.offline || process.env.CODEFLOW_MODEL_NETWORK === "offline")
|
|
172
|
+
return this.refresh(provider, environment, { ...options, offline: true });
|
|
173
|
+
const active = this.flights.get(key);
|
|
174
|
+
if (active)
|
|
175
|
+
return active;
|
|
176
|
+
const work = this.refresh(provider, environment, options);
|
|
177
|
+
this.flights.set(key, work);
|
|
178
|
+
void work.finally(() => this.flights.delete(key)).catch(() => {
|
|
179
|
+
});
|
|
180
|
+
return work;
|
|
181
|
+
}
|
|
182
|
+
async refresh(provider, environment, options) {
|
|
183
|
+
const prior = await this.read(provider, environment), now = this.now();
|
|
184
|
+
const offline = options.offline || process.env.CODEFLOW_MODEL_NETWORK === "offline";
|
|
185
|
+
if (prior && !options.force && now - prior.attemptedAt < TTL && !offline)
|
|
186
|
+
return { ...prior, state: !prior.discoveredAt ? "unknown" : now - prior.discoveredAt < TTL ? "current" : "stale" };
|
|
187
|
+
if (offline)
|
|
188
|
+
return prior ? { ...prior, state: "stale", issue: "offline" } : { schemaVersion: 1, revision: 0, provider, environment, models: [], discoveredAt: 0, attemptedAt: now, state: "unknown", issue: "offline" };
|
|
189
|
+
let reported;
|
|
190
|
+
try {
|
|
191
|
+
reported = options.reported ?? await this.discover(provider, options.context ?? {});
|
|
192
|
+
} catch {
|
|
193
|
+
const failed = prior ? { ...prior, attemptedAt: now, state: "stale", issue: "discovery-unavailable" } : { schemaVersion: 1, revision: 0, provider, environment, models: [], discoveredAt: 0, attemptedAt: now, state: "unknown", issue: "discovery-unavailable" };
|
|
194
|
+
await this.save(failed);
|
|
195
|
+
return failed;
|
|
196
|
+
}
|
|
197
|
+
const sameAccount = prior?.accountKey === reported.accountKey;
|
|
198
|
+
const entries = reported.models.map((m) => {
|
|
199
|
+
const old = sameAccount ? prior?.models.find((p) => p.id === m.id) : void 0;
|
|
200
|
+
return { id: m.id, label: m.label, aliases: m.aliases ?? [], efforts: m.efforts ?? null, ...m.defaultEffort ? { defaultEffort: m.defaultEffort } : {}, availability: "reported", compatibility: reported.discovery ? "advertised" : "unknown", source: reported.discovery?.source ?? "legacy catalog (account access unknown)", verifiedAt: now, confidence: "reported", documented: old?.documented, researchAttemptedAt: old?.researchAttemptedAt, observations: old?.observations ?? [] };
|
|
201
|
+
});
|
|
202
|
+
const result = { schemaVersion: 1, revision: (prior?.revision ?? 0) + 1, provider, environment, accountKey: reported.accountKey, defaultModel: reported.defaultModel, models: entries, discoveredAt: now, attemptedAt: now, state: "current" };
|
|
203
|
+
validateCapabilityCatalog(result);
|
|
204
|
+
if ((await this.settings()).allowResearch && !options.context?.agentContainer && !options.reported) {
|
|
205
|
+
const pending = entries.filter((m) => (!m.documented || now - m.documented.verifiedAt > 7 * 864e5) && (!m.researchAttemptedAt || now - m.researchAttemptedAt > 7 * 864e5)).slice(0, provider === "codex" ? 4 : 800);
|
|
206
|
+
const proposals = await researchModels(provider, pending.map((m) => m.id)).catch(() => []);
|
|
207
|
+
for (const m of pending)
|
|
208
|
+
m.researchAttemptedAt = now;
|
|
209
|
+
for (const proposal of proposals) {
|
|
210
|
+
const m = entries.find((m2) => m2.id === proposal.model);
|
|
211
|
+
if (m)
|
|
212
|
+
m.documented = validateDocumentation(proposal, provider, m.id);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
await this.save(result);
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
async observe(assignment, observation) {
|
|
219
|
+
await this.tail;
|
|
220
|
+
const c = await this.read(assignment.provider, assignment.environment);
|
|
221
|
+
if (!c)
|
|
222
|
+
return;
|
|
223
|
+
const m = c.models.find((m2) => m2.id === assignment.model);
|
|
224
|
+
if (!m)
|
|
225
|
+
return;
|
|
226
|
+
m.observations = [...m.observations, observation].slice(-30);
|
|
227
|
+
await this.save(c);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const modelCatalogStore = new ModelCatalogStore();
|
|
231
|
+
export {
|
|
232
|
+
ModelCatalogStore,
|
|
233
|
+
assignmentProblem,
|
|
234
|
+
modelCatalogStore,
|
|
235
|
+
modelEnvironment,
|
|
236
|
+
resolveModelAssignment,
|
|
237
|
+
validateCapabilityCatalog,
|
|
238
|
+
workerConstraintProblem
|
|
239
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
const origins = /* @__PURE__ */ new Set(["developers.openai.com", "platform.openai.com", "learn.chatgpt.com", "platform.claude.com", "code.claude.com", "docs.cloud.google.com", "ai.google.dev", "antigravity.google", "www.antigravity.google"]);
|
|
3
|
+
function validateDocumentation(value, provider, model) {
|
|
4
|
+
const d = value;
|
|
5
|
+
const allowed = provider === "codex" ? ["developers.openai.com", "platform.openai.com", "learn.chatgpt.com"] : provider === "claude" ? ["platform.claude.com", "code.claude.com"] : ["docs.cloud.google.com", "ai.google.dev", "antigravity.google", "www.antigravity.google"];
|
|
6
|
+
let url;
|
|
7
|
+
try {
|
|
8
|
+
url = new URL(d.source);
|
|
9
|
+
} catch {
|
|
10
|
+
throw new Error("Invalid documentation source");
|
|
11
|
+
}
|
|
12
|
+
if (!d || Object.keys(d).some((k) => !["model", "contextTokens", "maxOutputTokens", "capabilities", "pricing", "source", "digest", "verifiedAt", "confidence"].includes(k)) || d.model !== model || url.protocol !== "https:" || !allowed.includes(url.hostname) || url.username || url.password || url.port || !/^[a-f0-9]{64}$/.test(d.digest) || !Number.isFinite(d.verifiedAt) || d.confidence !== "documented" || d.capabilities !== null && (!Array.isArray(d.capabilities) || d.capabilities.length > 12 || d.capabilities.some((c) => !["text", "image", "audio", "tools", "structured-output"].includes(c))))
|
|
13
|
+
throw new Error("Invalid documentation proposal");
|
|
14
|
+
for (const n of [d.contextTokens, d.maxOutputTokens])
|
|
15
|
+
if (n !== null && (!Number.isSafeInteger(n) || n < 1 || n > 1e8))
|
|
16
|
+
throw new Error("Invalid documented token limit");
|
|
17
|
+
if (d.pricing !== null && (!d.pricing || Object.keys(d.pricing).sort().join() !== "currency,inputPerMillion,outputPerMillion" || d.pricing.currency !== "USD" || [d.pricing.inputPerMillion, d.pricing.outputPerMillion].some((n) => !Number.isFinite(n) || n < 0 || n > 1e5)))
|
|
18
|
+
throw new Error("Invalid documented price");
|
|
19
|
+
return d;
|
|
20
|
+
}
|
|
21
|
+
function proposeDocumentation(provider, model, source, text, now = Date.now()) {
|
|
22
|
+
if (!text.includes(model))
|
|
23
|
+
return;
|
|
24
|
+
const plain = text.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<[^>]*>/g, " ").replace(/\s+/g, " ");
|
|
25
|
+
const number = (pattern) => {
|
|
26
|
+
const found = plain.match(pattern)?.[1];
|
|
27
|
+
return found ? Number(found.replaceAll(",", "")) : null;
|
|
28
|
+
};
|
|
29
|
+
const exactPage = provider === "codex" && new URL(source).pathname.replace(/\/$/, "").endsWith("/" + model);
|
|
30
|
+
const contextTokens = exactPage ? number(/([\d,]+)\s+(?:token\s+)?context window/i) : null;
|
|
31
|
+
const maxOutputTokens = exactPage ? number(/([\d,]+)\s+(?:max(?:imum)?\s+output|output limit)/i) : null;
|
|
32
|
+
const price = exactPage ? plain.match(/Input\s*\$([\d.]+)[\s\S]{0,160}?Output\s*\$([\d.]+)/i) : null;
|
|
33
|
+
const d = { model, contextTokens, maxOutputTokens, capabilities: null, pricing: price && /per\s+(?:1M|million)\s+tokens/i.test(plain) ? { currency: "USD", inputPerMillion: Number(price[1]), outputPerMillion: Number(price[2]) } : null, source, digest: createHash("sha256").update(text).digest("hex"), verifiedAt: now, confidence: "documented" };
|
|
34
|
+
return validateDocumentation(d, provider, model);
|
|
35
|
+
}
|
|
36
|
+
async function officialText(source, fetcher) {
|
|
37
|
+
let url = new URL(source);
|
|
38
|
+
for (let i = 0; i < 4; i++) {
|
|
39
|
+
if (url.protocol !== "https:" || !origins.has(url.hostname) || url.username || url.password || url.port)
|
|
40
|
+
throw new Error("Research must stay on official documentation origins");
|
|
41
|
+
const response = await fetcher(url, { redirect: "manual", signal: AbortSignal.timeout(6e3), headers: { Accept: "text/markdown, text/html, text/plain" } });
|
|
42
|
+
if ([301, 302, 303, 307, 308].includes(response.status)) {
|
|
43
|
+
const location = response.headers.get("location");
|
|
44
|
+
if (!location)
|
|
45
|
+
throw new Error("Invalid redirect");
|
|
46
|
+
url = new URL(location, url);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (!response.ok || !response.body)
|
|
50
|
+
throw new Error("Documentation unavailable");
|
|
51
|
+
const reader = response.body.getReader();
|
|
52
|
+
const chunks = [];
|
|
53
|
+
let bytes = 0;
|
|
54
|
+
try {
|
|
55
|
+
while (true) {
|
|
56
|
+
const { done, value } = await reader.read();
|
|
57
|
+
if (done)
|
|
58
|
+
break;
|
|
59
|
+
bytes += value.length;
|
|
60
|
+
if (bytes > 2e6)
|
|
61
|
+
throw new Error("Documentation too large");
|
|
62
|
+
chunks.push(value);
|
|
63
|
+
}
|
|
64
|
+
} finally {
|
|
65
|
+
await reader.cancel();
|
|
66
|
+
}
|
|
67
|
+
return { source: url.href, text: Buffer.concat(chunks).toString("utf8") };
|
|
68
|
+
}
|
|
69
|
+
throw new Error("Too many documentation redirects");
|
|
70
|
+
}
|
|
71
|
+
async function researchModels(provider, models, fetcher = fetch) {
|
|
72
|
+
const result = [];
|
|
73
|
+
const pages = provider === "codex" ? models.filter((m) => /^[\w.-]+$/.test(m)).slice(0, 4).map((m) => ({ source: `https://developers.openai.com/api/docs/models/${m}`, models: [m] })) : [{ source: provider === "claude" ? "https://platform.claude.com/docs/en/about-claude/models/overview" : "https://antigravity.google/docs/models", models }];
|
|
74
|
+
await Promise.all(pages.map(async (page) => {
|
|
75
|
+
try {
|
|
76
|
+
const { source, text } = await officialText(page.source, fetcher);
|
|
77
|
+
for (const model of page.models) {
|
|
78
|
+
const d = proposeDocumentation(provider, model, source, text);
|
|
79
|
+
if (d)
|
|
80
|
+
result.push(d);
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
}
|
|
84
|
+
}));
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
export {
|
|
88
|
+
proposeDocumentation,
|
|
89
|
+
researchModels,
|
|
90
|
+
validateDocumentation
|
|
91
|
+
};
|
package/src/notes.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, open, readFile, rename, rm } from "node:fs/promises";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { CODEFLOW_HOME } from "./config.js";
|
|
5
|
+
const FILE = join(CODEFLOW_HOME, "notes.json");
|
|
6
|
+
const MAX_NOTE_CHARS = 2e5;
|
|
7
|
+
const MAX_NOTE_REVISIONS = 50;
|
|
8
|
+
const queues = /* @__PURE__ */ new Map();
|
|
9
|
+
class NotesConflictError extends Error {
|
|
10
|
+
constructor(current) {
|
|
11
|
+
super(`Note revision conflict: the current note is at revision ${current.revision}`);
|
|
12
|
+
this.current = current;
|
|
13
|
+
this.name = "NotesConflictError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
class Notes {
|
|
17
|
+
file;
|
|
18
|
+
constructor(file = FILE) {
|
|
19
|
+
this.file = resolve(file);
|
|
20
|
+
}
|
|
21
|
+
async get(key) {
|
|
22
|
+
await queues.get(this.file);
|
|
23
|
+
const document = await readDocument(this.file);
|
|
24
|
+
return copy(document.notes[normalize(key)] ?? empty());
|
|
25
|
+
}
|
|
26
|
+
/** Omitting expectedRevision keeps older clients compatible; new clients should send it. */
|
|
27
|
+
set(key, text, expectedRevision) {
|
|
28
|
+
if (typeof text !== "string") return Promise.reject(new Error("Send the note's text"));
|
|
29
|
+
if (text.length > MAX_NOTE_CHARS) return Promise.reject(new Error(`A note can hold ${MAX_NOTE_CHARS.toLocaleString()} characters at most`));
|
|
30
|
+
if (!validRevision(expectedRevision)) return Promise.reject(new Error("Expected note revision must be a non-negative integer"));
|
|
31
|
+
return this.change((document) => this.update(document, normalize(key), text, expectedRevision));
|
|
32
|
+
}
|
|
33
|
+
/** Retained revisions, newest first. An untouched scratchpad has no history. */
|
|
34
|
+
async history(key) {
|
|
35
|
+
await queues.get(this.file);
|
|
36
|
+
const document = await readDocument(this.file);
|
|
37
|
+
const normalized = normalize(key);
|
|
38
|
+
return (document.history[normalized] ?? (document.notes[normalized] ? [document.notes[normalized]] : [])).map(copy);
|
|
39
|
+
}
|
|
40
|
+
restore(key, revision, expectedRevision) {
|
|
41
|
+
if (!Number.isSafeInteger(revision) || revision < 1) return Promise.reject(new Error("Choose a saved note revision to restore"));
|
|
42
|
+
if (expectedRevision === void 0 || !validRevision(expectedRevision)) return Promise.reject(new Error("Restoring a note needs its current revision"));
|
|
43
|
+
return this.change((document) => {
|
|
44
|
+
const normalized = normalize(key);
|
|
45
|
+
const current = document.notes[normalized] ?? empty();
|
|
46
|
+
if (current.revision !== expectedRevision) throw new NotesConflictError(copy(current));
|
|
47
|
+
const historical = document.history[normalized]?.find((note) => note.revision === revision) ?? (current.revision === revision ? current : void 0);
|
|
48
|
+
if (!historical) throw new Error("That note revision is no longer available");
|
|
49
|
+
return this.update(document, normalized, historical.text, expectedRevision, revision);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/** Which non-empty notes exist, newest first. */
|
|
53
|
+
async keys() {
|
|
54
|
+
await queues.get(this.file);
|
|
55
|
+
const document = await readDocument(this.file);
|
|
56
|
+
return Object.entries(document.notes).filter(([, note]) => !!note.text).map(([key, note]) => ({ key, updatedAt: note.updatedAt, revision: note.revision })).sort((a, b) => b.updatedAt - a.updatedAt || a.key.localeCompare(b.key));
|
|
57
|
+
}
|
|
58
|
+
/** Search current saved text only; historical/erased revisions are never returned by search. */
|
|
59
|
+
async search(query) {
|
|
60
|
+
if (typeof query !== "string" || query.length > 300) throw new Error("Notes search supports up to 300 characters");
|
|
61
|
+
await queues.get(this.file);
|
|
62
|
+
const document = await readDocument(this.file);
|
|
63
|
+
const needle = query.trim().toLowerCase();
|
|
64
|
+
const ordered = Object.entries(document.notes).filter(([, note]) => !!note.text).sort((a, b) => b[1].updatedAt - a[1].updatedAt);
|
|
65
|
+
const hits = [];
|
|
66
|
+
let inspected = 0;
|
|
67
|
+
for (const [key, note] of ordered) {
|
|
68
|
+
if (inspected >= 5e3 || hits.length >= 100) break;
|
|
69
|
+
inspected++;
|
|
70
|
+
const position = note.text.toLowerCase().indexOf(needle);
|
|
71
|
+
if (needle && position < 0 && !key.toLowerCase().includes(needle)) continue;
|
|
72
|
+
const start = Math.max(0, position - 60);
|
|
73
|
+
hits.push({ key, revision: note.revision, updatedAt: note.updatedAt, excerpt: note.text.slice(start, start + 300) });
|
|
74
|
+
}
|
|
75
|
+
return { hits, inspected, truncated: inspected < ordered.length };
|
|
76
|
+
}
|
|
77
|
+
update(document, key, text, expectedRevision, restoredFrom) {
|
|
78
|
+
const current = document.notes[key] ?? empty();
|
|
79
|
+
if (expectedRevision !== void 0 && current.revision !== expectedRevision) throw new NotesConflictError(copy(current));
|
|
80
|
+
if (text === current.text && restoredFrom === void 0) return copy(current);
|
|
81
|
+
const note = {
|
|
82
|
+
text,
|
|
83
|
+
updatedAt: Math.max(Date.now(), current.updatedAt + 1),
|
|
84
|
+
revision: current.revision + 1,
|
|
85
|
+
...restoredFrom === void 0 ? {} : { restoredFrom }
|
|
86
|
+
};
|
|
87
|
+
document.notes[key] = note;
|
|
88
|
+
document.history[key] = [note, ...document.history[key] ?? (current.revision ? [current] : [])].slice(0, MAX_NOTE_REVISIONS);
|
|
89
|
+
return copy(note);
|
|
90
|
+
}
|
|
91
|
+
change(mutate) {
|
|
92
|
+
const next = (queues.get(this.file) ?? Promise.resolve()).then(async () => {
|
|
93
|
+
const document = await readDocument(this.file);
|
|
94
|
+
const before = JSON.stringify(document);
|
|
95
|
+
const result = mutate(document);
|
|
96
|
+
if (JSON.stringify(document) !== before) await writeDocument(this.file, document);
|
|
97
|
+
return result;
|
|
98
|
+
});
|
|
99
|
+
const settled = next.catch(() => void 0);
|
|
100
|
+
queues.set(this.file, settled);
|
|
101
|
+
void settled.then(() => {
|
|
102
|
+
if (queues.get(this.file) === settled) queues.delete(this.file);
|
|
103
|
+
});
|
|
104
|
+
return next;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const empty = () => ({ text: "", updatedAt: 0, revision: 0 });
|
|
108
|
+
const copy = (note) => ({ ...note });
|
|
109
|
+
const validRevision = (revision) => revision === void 0 || Number.isSafeInteger(revision) && Number(revision) >= 0;
|
|
110
|
+
async function readDocument(file) {
|
|
111
|
+
const document = { version: 2, notes: /* @__PURE__ */ Object.create(null), history: /* @__PURE__ */ Object.create(null) };
|
|
112
|
+
let raw;
|
|
113
|
+
try {
|
|
114
|
+
raw = JSON.parse(await readFile(file, "utf8"));
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (error.code === "ENOENT") return document;
|
|
117
|
+
throw new Error(`Notes store cannot be read: ${error.message}`);
|
|
118
|
+
}
|
|
119
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("Notes store cannot be read: invalid document");
|
|
120
|
+
const versioned = raw.version === 2;
|
|
121
|
+
const notes = versioned ? raw.notes : raw;
|
|
122
|
+
if (!notes || typeof notes !== "object" || Array.isArray(notes)) throw new Error("Notes store cannot be read: invalid notes");
|
|
123
|
+
const decode = (value, legacy = false) => {
|
|
124
|
+
if (!value || typeof value.text !== "string" || value.text.length > MAX_NOTE_CHARS) throw new Error("Notes store cannot be read: invalid note");
|
|
125
|
+
const revision = legacy ? 1 : value.revision;
|
|
126
|
+
if (!Number.isSafeInteger(revision) || revision < 1) throw new Error("Notes store cannot be read: invalid revision");
|
|
127
|
+
return {
|
|
128
|
+
text: value.text,
|
|
129
|
+
updatedAt: Number(value.updatedAt) || 0,
|
|
130
|
+
revision,
|
|
131
|
+
...Number.isSafeInteger(value.restoredFrom) && value.restoredFrom >= 1 ? { restoredFrom: value.restoredFrom } : {}
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
for (const [key, value] of Object.entries(notes)) {
|
|
135
|
+
const normalized = normalize(key);
|
|
136
|
+
const current = decode(value, !versioned);
|
|
137
|
+
document.notes[normalized] = current;
|
|
138
|
+
const saved = versioned && raw.history && Array.isArray(raw.history[key]) ? raw.history[key].map((note) => decode(note)) : [];
|
|
139
|
+
const byRevision = new Map(saved.filter((note) => note.revision < current.revision).map((note) => [note.revision, note]));
|
|
140
|
+
document.history[normalized] = [current, ...[...byRevision.values()].sort((a, b) => b.revision - a.revision)].slice(0, MAX_NOTE_REVISIONS);
|
|
141
|
+
}
|
|
142
|
+
return document;
|
|
143
|
+
}
|
|
144
|
+
async function writeDocument(file, document) {
|
|
145
|
+
await mkdir(dirname(file), { recursive: true });
|
|
146
|
+
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
147
|
+
let handle;
|
|
148
|
+
try {
|
|
149
|
+
handle = await open(temporary, "wx", 384);
|
|
150
|
+
await handle.writeFile(JSON.stringify(document, null, 2));
|
|
151
|
+
await handle.sync();
|
|
152
|
+
await handle.close();
|
|
153
|
+
handle = void 0;
|
|
154
|
+
await rename(temporary, file);
|
|
155
|
+
if (process.platform !== "win32") {
|
|
156
|
+
const directory = await open(dirname(file), "r");
|
|
157
|
+
try {
|
|
158
|
+
await directory.sync();
|
|
159
|
+
} finally {
|
|
160
|
+
await directory.close();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
} finally {
|
|
164
|
+
await handle?.close().catch(() => {
|
|
165
|
+
});
|
|
166
|
+
await rm(temporary, { force: true }).catch(() => {
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const normalize = (key) => key.startsWith("/") ? key.replace(/\/+$/, "") || "/" : "general";
|
|
171
|
+
export {
|
|
172
|
+
MAX_NOTE_CHARS,
|
|
173
|
+
MAX_NOTE_REVISIONS,
|
|
174
|
+
Notes,
|
|
175
|
+
NotesConflictError
|
|
176
|
+
};
|