@midhunxavier/code-flow 0.0.0-stage → 0.1.0-preview.1
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 +122 -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/browser-7XI52A6V.js +8 -0
- package/public/assets/changes-EYFPASEI.js +1 -0
- package/public/assets/chunk-7H3ZLTKA.js +2 -0
- package/public/assets/chunk-L7TUCIYF.js +1 -0
- package/public/assets/chunk-LLMQG23J.js +163 -0
- package/public/assets/main-KIRM2OEK.js +1 -0
- package/public/assets/settings-QJZVW22Y.js +9 -0
- package/public/assets/styles-615EBB20.css +32 -0
- package/public/assets/terminal-A2FI2NAX.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 +142 -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 +118 -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 +77 -0
- package/public/docs/installation.html +74 -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 +99 -0
- package/public/docs/provider-compatibility.html +20 -0
- package/public/docs/releasing.html +61 -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 +379 -0
- package/public/docs/verification.html +22 -0
- package/public/docs/vscode.html +53 -0
- package/public/docs/worker-permission-verification.html +122 -0
- package/public/index.html +17 -0
- package/scripts/check-node.mjs +7 -0
- package/scripts/codeflow.mjs +55 -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/agy-auth.js +67 -0
- package/src/adapters/agy-isolation.js +223 -0
- package/src/adapters/agy.js +297 -0
- package/src/adapters/claude.js +220 -0
- package/src/adapters/codex-app-server.js +518 -0
- package/src/adapters/codex.js +299 -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 +418 -0
- package/src/adapters/team-tools.js +102 -0
- package/src/adapters/types.js +0 -0
- package/src/agent-container.js +155 -0
- package/src/approvals.js +121 -0
- package/src/architecture.js +456 -0
- package/src/assets.js +44 -0
- package/src/automation.js +901 -0
- package/src/certificate-renewal.js +75 -0
- package/src/chat-handoff.js +55 -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/engine.js +4366 -0
- package/src/env.js +44 -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/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/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 +238 -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 +212 -0
- package/src/patterns/auto-team.js +659 -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 +19 -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 +49 -0
- package/src/patterns/supervisor.js +235 -0
- package/src/patterns/task-draft.js +29 -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 +383 -0
- package/src/preview.js +219 -0
- package/src/privacy.js +23 -0
- package/src/project-report.js +22 -0
- package/src/project-team-model.js +178 -0
- package/src/project-team-policy.js +42 -0
- package/src/project-team-store.js +213 -0
- package/src/project-teams-api.js +36 -0
- package/src/project-teams.js +551 -0
- package/src/projects.js +286 -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 +732 -0
- package/src/repo-tree.js +106 -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 +110 -0
- package/src/run-projection.js +75 -0
- package/src/run-recovery.js +53 -0
- package/src/run-status.js +54 -0
- package/src/serve.js +138 -0
- package/src/server.js +2738 -0
- package/src/shared-access.js +103 -0
- package/src/shared-accounts.js +301 -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 +116 -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/team-evaluation.js +242 -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-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 +176 -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,361 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { mkdir, open, realpath, rename, rm } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { randomBytes } from "node:crypto";
|
|
5
|
+
import { CODEFLOW_HOME } from "../config.js";
|
|
6
|
+
import { readJson, send } from "../http.js";
|
|
7
|
+
import { logError } from "../log.js";
|
|
8
|
+
import { Shells } from "../shell.js";
|
|
9
|
+
import { roleAtLeast } from "../shared-accounts.js";
|
|
10
|
+
import { CollabError } from "./model.js";
|
|
11
|
+
const object = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
12
|
+
const id = (v) => typeof v === "string" && /^[a-zA-Z0-9_-]{1,100}$/.test(v);
|
|
13
|
+
const keys = (v, allowed) => {
|
|
14
|
+
if (Object.keys(v).some((key) => !allowed.includes(key))) throw new CollabError("Unknown terminal field");
|
|
15
|
+
};
|
|
16
|
+
function grant(value) {
|
|
17
|
+
if (!object(value)) throw new CollabError("Invalid terminal grant");
|
|
18
|
+
keys(value, ["projectId", "enabled", "userIds"]);
|
|
19
|
+
if (!id(value.projectId) || typeof value.enabled !== "boolean" || !Array.isArray(value.userIds) || value.userIds.length > 128 || !value.userIds.every(id) || new Set(value.userIds).size !== value.userIds.length) throw new CollabError("Choose a project and its permitted teammates");
|
|
20
|
+
return structuredClone(value);
|
|
21
|
+
}
|
|
22
|
+
function size(raw) {
|
|
23
|
+
if (!Number.isInteger(raw.cols) || !Number.isInteger(raw.rows) || Number(raw.cols) < 2 || Number(raw.cols) > 500 || Number(raw.rows) < 1 || Number(raw.rows) > 200) throw new CollabError("Terminal size must be 2\u2013500 columns and 1\u2013200 rows");
|
|
24
|
+
return { cols: raw.cols, rows: raw.rows };
|
|
25
|
+
}
|
|
26
|
+
const metadata = ({ id: id2, projectId, repository, userId, displayName, startedAt, status }) => ({ id: id2, projectId, repository, userId, displayName, startedAt, status });
|
|
27
|
+
const TERMINAL_IDLE_MS = 30 * 6e4;
|
|
28
|
+
class TeamTerminals {
|
|
29
|
+
constructor(options) {
|
|
30
|
+
this.options = options;
|
|
31
|
+
this.shells = options.shells ?? new Shells();
|
|
32
|
+
this.file = options.file ?? join(CODEFLOW_HOME, "terminal-access.json");
|
|
33
|
+
this.now = options.now ?? Date.now;
|
|
34
|
+
}
|
|
35
|
+
state = { version: 1, revision: 0, grants: [], audit: [] };
|
|
36
|
+
sessions = /* @__PURE__ */ new Map();
|
|
37
|
+
listeners = /* @__PURE__ */ new Map();
|
|
38
|
+
ready = false;
|
|
39
|
+
closed = false;
|
|
40
|
+
chain = Promise.resolve();
|
|
41
|
+
timer;
|
|
42
|
+
shells;
|
|
43
|
+
file;
|
|
44
|
+
now;
|
|
45
|
+
serial(fn) {
|
|
46
|
+
const next = this.chain.catch(() => {
|
|
47
|
+
}).then(fn);
|
|
48
|
+
this.chain = next;
|
|
49
|
+
return next;
|
|
50
|
+
}
|
|
51
|
+
require() {
|
|
52
|
+
if (!this.ready || this.closed) throw new CollabError("Team terminals are unavailable", 503);
|
|
53
|
+
}
|
|
54
|
+
async load() {
|
|
55
|
+
try {
|
|
56
|
+
const handle = await open(this.file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
57
|
+
let raw;
|
|
58
|
+
try {
|
|
59
|
+
const stat = await handle.stat();
|
|
60
|
+
if (!stat.isFile() || stat.size > 2 * 1024 * 1024) throw new Error("Invalid terminal access storage");
|
|
61
|
+
raw = JSON.parse(await handle.readFile("utf8"));
|
|
62
|
+
} finally {
|
|
63
|
+
await handle.close();
|
|
64
|
+
}
|
|
65
|
+
if (!object(raw) || raw.version !== 1 || !Number.isSafeInteger(raw.revision) || Number(raw.revision) < 0 || !Array.isArray(raw.grants) || raw.grants.length > 200 || !Array.isArray(raw.audit) || raw.audit.length > 500) throw new Error("Invalid terminal access storage");
|
|
66
|
+
raw.grants.forEach(grant);
|
|
67
|
+
if (new Set(raw.grants.map((g) => g.projectId)).size !== raw.grants.length) throw new Error("Duplicate terminal grants");
|
|
68
|
+
for (const event of raw.audit) if (!object(event) || !["opened", "closed"].includes(String(event.action)) || !Number.isFinite(event.at) || !id(event.by) || !object(event.session) || !id(event.session.id) || !id(event.session.projectId) || !id(event.session.userId) || !id(event.session.repository) || typeof event.session.displayName !== "string" || !Number.isFinite(event.session.startedAt) || !["running", "exited"].includes(String(event.session.status)) || event.reason !== void 0 && typeof event.reason !== "string") throw new Error("Invalid terminal audit record");
|
|
69
|
+
this.state = raw;
|
|
70
|
+
const openSessions = /* @__PURE__ */ new Map();
|
|
71
|
+
for (const event of this.state.audit) {
|
|
72
|
+
if (event.action === "opened") openSessions.set(event.session.id, event.session);
|
|
73
|
+
else openSessions.delete(event.session.id);
|
|
74
|
+
}
|
|
75
|
+
for (const session of openSessions.values()) this.record("closed", session, "owner", "Server restarted");
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (error.code !== "ENOENT") throw error;
|
|
78
|
+
}
|
|
79
|
+
await this.persist();
|
|
80
|
+
this.ready = true;
|
|
81
|
+
}
|
|
82
|
+
start() {
|
|
83
|
+
this.timer ??= setInterval(() => void this.reap().catch((error) => logError("Team terminal cleanup failed", error)), 1e3);
|
|
84
|
+
this.timer.unref();
|
|
85
|
+
}
|
|
86
|
+
active() {
|
|
87
|
+
return this.sessions.size;
|
|
88
|
+
}
|
|
89
|
+
user(userId) {
|
|
90
|
+
return this.options.users().find((user) => user.id === userId && user.enabled);
|
|
91
|
+
}
|
|
92
|
+
allowed(userId, projectId) {
|
|
93
|
+
const user = this.user(userId), g = this.state.grants.find((g2) => g2.projectId === projectId);
|
|
94
|
+
return !!this.options.projectRepos(projectId) && !!user && roleAtLeast(user.roles[projectId], "contributor") && !!g?.enabled && g.userIds.includes(userId) && this.options.enabled();
|
|
95
|
+
}
|
|
96
|
+
valid(session) {
|
|
97
|
+
return this.allowed(session.userId, session.projectId) && this.user(session.userId)?.revision === session.accountRevision && this.options.resolveRepo(session.projectId, session.repository) === session.cwd && !!this.options.projectRepos(session.projectId)?.includes(session.cwd);
|
|
98
|
+
}
|
|
99
|
+
view(actor) {
|
|
100
|
+
this.require();
|
|
101
|
+
const owner = actor.kind === "owner";
|
|
102
|
+
const visible = (p) => !!this.options.projectRepos(p) && (owner || !!this.user(actor.userId)?.roles[p]);
|
|
103
|
+
return {
|
|
104
|
+
revision: this.state.revision,
|
|
105
|
+
available: this.options.enabled(),
|
|
106
|
+
grants: this.state.grants.filter((g) => visible(g.projectId)).map((g) => owner ? structuredClone(g) : { projectId: g.projectId, allowed: this.allowed(actor.userId, g.projectId) }),
|
|
107
|
+
sessions: [...this.sessions.values()].filter((s) => owner || s.userId === actor.userId && visible(s.projectId) && this.valid(s)).map(metadata),
|
|
108
|
+
audit: owner ? structuredClone(this.state.audit.slice(-100)) : [],
|
|
109
|
+
idleMinutes: 30,
|
|
110
|
+
maxPerUser: 2
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
configure(raw) {
|
|
114
|
+
return this.serial(async () => {
|
|
115
|
+
this.require();
|
|
116
|
+
keys(raw, ["revision", "grant"]);
|
|
117
|
+
if (raw.revision !== this.state.revision) throw new CollabError("Terminal settings changed; reload before saving", 409);
|
|
118
|
+
const next = grant(raw.grant);
|
|
119
|
+
if (!this.options.projectRepos(next.projectId)) throw new CollabError("Project not found", 404);
|
|
120
|
+
if (next.userIds.some((uid) => {
|
|
121
|
+
const user = this.user(uid);
|
|
122
|
+
return !user || !roleAtLeast(user.roles[next.projectId], "contributor");
|
|
123
|
+
})) throw new CollabError("Grant terminals only to enabled project contributors or maintainers");
|
|
124
|
+
if (!this.state.grants.some((g) => g.projectId === next.projectId) && this.state.grants.length >= 200) throw new CollabError("At most 200 project terminal policies may be configured");
|
|
125
|
+
this.state.grants = [...this.state.grants.filter((g) => g.projectId !== next.projectId), next];
|
|
126
|
+
this.state.revision++;
|
|
127
|
+
await this.persist();
|
|
128
|
+
await this.reapNow();
|
|
129
|
+
this.options.changed(next.projectId);
|
|
130
|
+
return this.view({ kind: "owner", userId: "owner", displayName: "Owner" });
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
open(actor, raw, authorize) {
|
|
134
|
+
return this.serial(async () => {
|
|
135
|
+
this.require();
|
|
136
|
+
authorize();
|
|
137
|
+
keys(raw, ["projectId", "repository", "cols", "rows"]);
|
|
138
|
+
if (actor.kind !== "member" || actor.via !== "browser") throw new CollabError("Use your teammate account to open a team terminal", 403);
|
|
139
|
+
if (!id(raw.projectId) || !id(raw.repository) || !this.allowed(actor.userId, raw.projectId)) throw new CollabError("The owner has not granted terminal access to this project", 403);
|
|
140
|
+
const cwd = this.options.resolveRepo(raw.projectId, raw.repository);
|
|
141
|
+
if (!cwd || !this.options.projectRepos(raw.projectId)?.includes(cwd) || await realpath(cwd).catch(() => void 0) !== cwd) throw new CollabError("The host repository mapping is unavailable", 409);
|
|
142
|
+
const dimensions = size(raw);
|
|
143
|
+
await this.reapNow();
|
|
144
|
+
if (this.sessions.size >= 8 || [...this.sessions.values()].filter((s) => s.userId === actor.userId).length >= 2) throw new CollabError("Close a terminal first: at most two per teammate and eight on the host", 409);
|
|
145
|
+
if (!await this.shells.interactive()) throw new CollabError("Interactive terminals require Python 3 PTY support on the host", 409);
|
|
146
|
+
authorize();
|
|
147
|
+
if (!this.allowed(actor.userId, raw.projectId)) throw new CollabError("Terminal permission changed", 403);
|
|
148
|
+
const user = this.user(actor.userId), revision = user.revision;
|
|
149
|
+
const shell = await this.shells.open(cwd, { pty: true, ...dimensions });
|
|
150
|
+
const session = { id: shell.id, projectId: raw.projectId, repository: raw.repository, userId: user.id, displayName: user.displayName, startedAt: this.now(), lastInput: this.now(), accountRevision: revision, cwd, status: shell.status };
|
|
151
|
+
try {
|
|
152
|
+
authorize();
|
|
153
|
+
if (!this.valid(session) || !shell.interactive || this.closed) throw new CollabError("Terminal access changed while opening", 403);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
this.shells.close(shell.id);
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
this.sessions.set(shell.id, session);
|
|
159
|
+
this.record("opened", session, actor.userId);
|
|
160
|
+
await this.persist();
|
|
161
|
+
this.options.activity?.();
|
|
162
|
+
this.options.changed(session.projectId);
|
|
163
|
+
return metadata(session);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
own(actor, sessionId) {
|
|
167
|
+
this.require();
|
|
168
|
+
const session = this.sessions.get(sessionId);
|
|
169
|
+
if (!session || session.userId !== actor.userId || !this.valid(session)) throw new CollabError("Terminal unavailable", 404);
|
|
170
|
+
return session;
|
|
171
|
+
}
|
|
172
|
+
input(actor, sessionId, raw) {
|
|
173
|
+
const session = this.own(actor, sessionId);
|
|
174
|
+
keys(raw, ["data", "binary"]);
|
|
175
|
+
if (raw.data === void 0 === (raw.binary === void 0)) throw new CollabError("Send data or binary keystrokes");
|
|
176
|
+
const data = raw.data ?? raw.binary;
|
|
177
|
+
if (typeof data !== "string" || Buffer.byteLength(data) > 32 * 1024 || raw.binary !== void 0 && /[^\x00-\xff]/.test(data)) throw new CollabError("Send at most 32 KiB of terminal input");
|
|
178
|
+
if (this.shells.pendingInputBytes(sessionId) > 128 * 1024) throw new CollabError("Terminal input is backed up; wait before sending more", 429);
|
|
179
|
+
if (!this.shells.type(sessionId, data, raw.binary === void 0 ? "utf8" : "latin1")) throw new CollabError("Terminal has exited", 409);
|
|
180
|
+
session.lastInput = this.now();
|
|
181
|
+
this.options.activity?.();
|
|
182
|
+
return { ok: true };
|
|
183
|
+
}
|
|
184
|
+
resize(actor, sessionId, raw) {
|
|
185
|
+
this.own(actor, sessionId);
|
|
186
|
+
keys(raw, ["cols", "rows"]);
|
|
187
|
+
const s = size(raw);
|
|
188
|
+
if (!this.shells.resize(sessionId, s.cols, s.rows)) throw new CollabError("Terminal has exited", 409);
|
|
189
|
+
return { ok: true };
|
|
190
|
+
}
|
|
191
|
+
record(action, session, by, reason) {
|
|
192
|
+
this.state.audit.push({ at: this.now(), action, session: metadata(session), by, ...reason ? { reason } : {} });
|
|
193
|
+
this.state.audit = this.state.audit.slice(-500);
|
|
194
|
+
}
|
|
195
|
+
end(session, by, reason) {
|
|
196
|
+
this.shells.close(session.id);
|
|
197
|
+
this.sessions.delete(session.id);
|
|
198
|
+
for (const listener of this.listeners.get(session.id) ?? []) listener();
|
|
199
|
+
this.listeners.delete(session.id);
|
|
200
|
+
this.record("closed", session, by, reason);
|
|
201
|
+
this.options.changed(session.projectId);
|
|
202
|
+
}
|
|
203
|
+
closeSession(actor, sessionId) {
|
|
204
|
+
return this.serial(async () => {
|
|
205
|
+
this.require();
|
|
206
|
+
const session = this.sessions.get(sessionId);
|
|
207
|
+
if (!session || actor.kind !== "owner" && session.userId !== actor.userId) throw new CollabError("Terminal unavailable", 404);
|
|
208
|
+
this.end(session, actor.userId, "Closed by user");
|
|
209
|
+
await this.persist();
|
|
210
|
+
return { closed: true };
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
reap() {
|
|
214
|
+
return this.serial(() => this.reapNow());
|
|
215
|
+
}
|
|
216
|
+
async reapNow() {
|
|
217
|
+
if (!this.ready || this.closed) return;
|
|
218
|
+
let changed = false;
|
|
219
|
+
for (const session of this.sessions.values()) {
|
|
220
|
+
const shell = this.shells.get(session.id);
|
|
221
|
+
const reason = !this.valid(session) ? "Access revoked or repository changed" : this.now() - session.lastInput >= TERMINAL_IDLE_MS ? "No keyboard input for 30 minutes" : !shell || shell.status === "exited" ? "Shell exited" : void 0;
|
|
222
|
+
if (reason) {
|
|
223
|
+
this.end(session, "owner", reason);
|
|
224
|
+
changed = true;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (changed) await this.persist();
|
|
228
|
+
}
|
|
229
|
+
watch(actor, sessionId, res, authorize) {
|
|
230
|
+
this.own(actor, sessionId);
|
|
231
|
+
authorize();
|
|
232
|
+
const watching = this.listeners.get(sessionId) ?? /* @__PURE__ */ new Set();
|
|
233
|
+
if (watching.size >= 2) throw new CollabError("This terminal is already open in two tabs", 429);
|
|
234
|
+
let closed = false, stop, timer;
|
|
235
|
+
const close = () => {
|
|
236
|
+
if (closed) return;
|
|
237
|
+
closed = true;
|
|
238
|
+
stop?.();
|
|
239
|
+
if (timer) clearInterval(timer);
|
|
240
|
+
watching.delete(close);
|
|
241
|
+
res.destroy();
|
|
242
|
+
};
|
|
243
|
+
watching.add(close);
|
|
244
|
+
this.listeners.set(sessionId, watching);
|
|
245
|
+
res.once("close", close);
|
|
246
|
+
const check = () => {
|
|
247
|
+
authorize();
|
|
248
|
+
this.own(actor, sessionId);
|
|
249
|
+
};
|
|
250
|
+
const emit = (event) => {
|
|
251
|
+
if (closed) return;
|
|
252
|
+
try {
|
|
253
|
+
check();
|
|
254
|
+
if (res.writableLength > 512 * 1024) return close();
|
|
255
|
+
res.write(`data: ${JSON.stringify(event)}
|
|
256
|
+
|
|
257
|
+
`);
|
|
258
|
+
} catch {
|
|
259
|
+
close();
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-store", "connection": "keep-alive", "x-accel-buffering": "no" });
|
|
263
|
+
res.flushHeaders();
|
|
264
|
+
stop = this.shells.subscribe(sessionId, emit);
|
|
265
|
+
emit({ type: "hello", shell: this.shells.get(sessionId), history: this.shells.history(sessionId) });
|
|
266
|
+
if (closed) {
|
|
267
|
+
stop?.();
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
timer = setInterval(() => {
|
|
271
|
+
try {
|
|
272
|
+
check();
|
|
273
|
+
if (res.writableLength > 512 * 1024) return close();
|
|
274
|
+
res.write(": connected\n\n");
|
|
275
|
+
} catch {
|
|
276
|
+
close();
|
|
277
|
+
}
|
|
278
|
+
}, 1e3);
|
|
279
|
+
timer.unref();
|
|
280
|
+
}
|
|
281
|
+
async persist() {
|
|
282
|
+
const temp = `${this.file}.${randomBytes(8).toString("hex")}.tmp`;
|
|
283
|
+
try {
|
|
284
|
+
await mkdir(dirname(this.file), { recursive: true, mode: 448 });
|
|
285
|
+
const handle = await open(temp, "wx", 384);
|
|
286
|
+
try {
|
|
287
|
+
await handle.writeFile(JSON.stringify(this.state));
|
|
288
|
+
await handle.sync();
|
|
289
|
+
} finally {
|
|
290
|
+
await handle.close();
|
|
291
|
+
}
|
|
292
|
+
await rename(temp, this.file);
|
|
293
|
+
if (process.platform !== "win32") {
|
|
294
|
+
const folder = await open(dirname(this.file), "r");
|
|
295
|
+
try {
|
|
296
|
+
await folder.sync();
|
|
297
|
+
} finally {
|
|
298
|
+
await folder.close();
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
} catch (error) {
|
|
302
|
+
this.ready = false;
|
|
303
|
+
for (const session of [...this.sessions.values()]) this.end(session, "owner", "Audit storage unavailable");
|
|
304
|
+
throw error;
|
|
305
|
+
} finally {
|
|
306
|
+
await rm(temp, { force: true }).catch(() => {
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
async close() {
|
|
311
|
+
if (this.timer) clearInterval(this.timer);
|
|
312
|
+
this.closed = true;
|
|
313
|
+
await this.chain.catch(() => {
|
|
314
|
+
});
|
|
315
|
+
for (const session of [...this.sessions.values()]) this.end(session, "owner", "Server stopped");
|
|
316
|
+
if (this.ready) await this.persist();
|
|
317
|
+
this.shells.closeAll();
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
async function handleTeamTerminals(req, res, parts, service, current, mutation) {
|
|
321
|
+
try {
|
|
322
|
+
const method = req.method ?? "GET", actor = current();
|
|
323
|
+
if (method !== "GET") mutation();
|
|
324
|
+
const body = () => readJson(req, 192 * 1024, { timeoutMs: 1e4 });
|
|
325
|
+
if (parts.length === 2 && method === "GET") return send(res, 200, service.view(actor));
|
|
326
|
+
if (parts.length === 2 && method === "POST") {
|
|
327
|
+
const raw = await body();
|
|
328
|
+
return send(res, 201, await service.open(current(), raw, () => {
|
|
329
|
+
current();
|
|
330
|
+
}));
|
|
331
|
+
}
|
|
332
|
+
if (parts.length === 3 && parts[2] === "settings" && method === "PUT") {
|
|
333
|
+
if (actor.kind !== "owner") throw new CollabError("Only the host owner may grant terminals", 403);
|
|
334
|
+
const raw = await body();
|
|
335
|
+
current();
|
|
336
|
+
return send(res, 200, await service.configure(raw));
|
|
337
|
+
}
|
|
338
|
+
const sessionId = parts[2];
|
|
339
|
+
if (sessionId && parts.length === 3 && method === "DELETE") {
|
|
340
|
+
const raw = await body();
|
|
341
|
+
keys(raw, []);
|
|
342
|
+
return send(res, 200, await service.closeSession(current(), sessionId));
|
|
343
|
+
}
|
|
344
|
+
if (sessionId && parts.length === 4 && method === "GET" && parts[3] === "events") return service.watch(actor, sessionId, res, () => {
|
|
345
|
+
current();
|
|
346
|
+
});
|
|
347
|
+
if (sessionId && parts.length === 4 && method === "POST" && ["input", "resize"].includes(parts[3])) {
|
|
348
|
+
const raw = await body();
|
|
349
|
+
return send(res, 200, parts[3] === "input" ? service.input(current(), sessionId, raw) : service.resize(current(), sessionId, raw));
|
|
350
|
+
}
|
|
351
|
+
throw new CollabError("Unknown team terminal operation", 404);
|
|
352
|
+
} catch (error) {
|
|
353
|
+
if (error instanceof CollabError) return send(res, error.status, { error: error.message });
|
|
354
|
+
throw error;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
export {
|
|
358
|
+
TERMINAL_IDLE_MS,
|
|
359
|
+
TeamTerminals,
|
|
360
|
+
handleTeamTerminals
|
|
361
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { mkdir, open, rename, rm } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { CODEFLOW_HOME } from "./config.js";
|
|
6
|
+
import { redactExport } from "./privacy.js";
|
|
7
|
+
class CollaborationReviewError extends Error {
|
|
8
|
+
constructor(message, status = 400) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.status = status;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const queues = /* @__PURE__ */ new Map();
|
|
14
|
+
const validId = (value) => typeof value === "string" && /^[a-zA-Z0-9_-]{1,100}$/.test(value);
|
|
15
|
+
const validHead = (value) => typeof value === "string" && /^[a-f0-9]{40,64}$/.test(value);
|
|
16
|
+
function actor(value) {
|
|
17
|
+
if (!value || typeof value.id !== "string" || !/^(?:owner|shared:[a-zA-Z0-9_-]{1,100})$/.test(value.id) || typeof value.name !== "string" || !value.name.trim() || value.name.length > 120 || /[\x00-\x1f\x7f]/.test(value.name)) throw new CollaborationReviewError("A server-authenticated review identity is required");
|
|
18
|
+
return { id: value.id, name: value.name.trim() };
|
|
19
|
+
}
|
|
20
|
+
function branch(value) {
|
|
21
|
+
if (typeof value !== "string" || !value || value.length > 250 || /[\x00-\x20\x7f]/.test(value)) throw new CollaborationReviewError("Choose a result branch");
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
class CollaborationReviews {
|
|
25
|
+
constructor(home = CODEFLOW_HOME) {
|
|
26
|
+
this.home = home;
|
|
27
|
+
}
|
|
28
|
+
path(runId) {
|
|
29
|
+
if (!validId(runId)) throw new CollaborationReviewError("Invalid run identity");
|
|
30
|
+
return join(this.home, "shared-reviews", `${runId}.json`);
|
|
31
|
+
}
|
|
32
|
+
async list(runId) {
|
|
33
|
+
return redactExport(await this.read(runId));
|
|
34
|
+
}
|
|
35
|
+
async read(runId) {
|
|
36
|
+
const file = this.path(runId);
|
|
37
|
+
let source;
|
|
38
|
+
try {
|
|
39
|
+
const handle = await open(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
40
|
+
try {
|
|
41
|
+
const stat = await handle.stat();
|
|
42
|
+
if (!stat.isFile() || stat.size > 2 * 1024 * 1024) throw new Error("Invalid comment storage");
|
|
43
|
+
source = await handle.readFile("utf8");
|
|
44
|
+
} finally {
|
|
45
|
+
await handle.close();
|
|
46
|
+
}
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (error.code === "ENOENT") return { version: 1, revision: 0, comments: [] };
|
|
49
|
+
throw new CollaborationReviewError("Review comments cannot be read; existing records were left untouched", 503);
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const value = JSON.parse(source);
|
|
53
|
+
if (Object.keys(value).some((key) => !["version", "revision", "comments"].includes(key)) || value.version !== 1 || !Number.isSafeInteger(value.revision) || value.revision < 0 || !Array.isArray(value.comments) || value.comments.length > 200) throw new Error("Invalid thread");
|
|
54
|
+
for (const comment of value.comments) {
|
|
55
|
+
if (!comment || Object.keys(comment).some((key) => !["id", "branch", "head", "text", "actor", "at"].includes(key)) || typeof comment.id !== "string" || !/^[a-f0-9-]{36}$/.test(comment.id) || branch(comment.branch) !== comment.branch || !validHead(comment.head) || typeof comment.text !== "string" || !comment.text.trim() || comment.text.length > 4e3 || !Number.isSafeInteger(comment.at) || comment.at < 0) throw new Error("Invalid comment");
|
|
56
|
+
actor(comment.actor);
|
|
57
|
+
if (Object.keys(comment.actor).some((key) => !["id", "name"].includes(key))) throw new Error("Invalid actor");
|
|
58
|
+
}
|
|
59
|
+
if (new Set(value.comments.map((comment) => comment.id)).size !== value.comments.length) throw new Error("Duplicate comment");
|
|
60
|
+
return value;
|
|
61
|
+
} catch {
|
|
62
|
+
throw new CollaborationReviewError("Review comments are invalid; existing records were left untouched", 503);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async add(runId, input, identity, resolveHead, authorize = () => {
|
|
66
|
+
}) {
|
|
67
|
+
const file = this.path(runId);
|
|
68
|
+
const resultBranch = branch(input.branch);
|
|
69
|
+
const author = actor(identity);
|
|
70
|
+
if (!validHead(input.expectedHead)) throw new CollaborationReviewError("Choose an exact result commit");
|
|
71
|
+
if (typeof input.text !== "string" || !input.text.trim() || input.text.length > 4e3 || /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(input.text)) throw new CollaborationReviewError("Review comments need 1\u20134000 characters of text");
|
|
72
|
+
const text = input.text.trim(), expectedHead = input.expectedHead;
|
|
73
|
+
const next = (queues.get(file) ?? Promise.resolve()).catch(() => void 0).then(async () => {
|
|
74
|
+
const current = await this.read(runId);
|
|
75
|
+
if (current.comments.length >= 200) throw new CollaborationReviewError("This thread has reached its 200-comment limit", 409);
|
|
76
|
+
if (await resolveHead() !== expectedHead) throw new CollaborationReviewError("The result commit changed; refresh before commenting", 409);
|
|
77
|
+
authorize();
|
|
78
|
+
const updated = { version: 1, revision: current.revision + 1, comments: [...current.comments, { id: randomUUID(), branch: resultBranch, head: expectedHead, text, actor: author, at: Date.now() }] };
|
|
79
|
+
await mkdir(dirname(file), { recursive: true, mode: 448 });
|
|
80
|
+
const temp = `${file}.${randomUUID()}.tmp`;
|
|
81
|
+
try {
|
|
82
|
+
const handle = await open(temp, "wx", 384);
|
|
83
|
+
try {
|
|
84
|
+
await handle.writeFile(JSON.stringify(updated));
|
|
85
|
+
await handle.sync();
|
|
86
|
+
} finally {
|
|
87
|
+
await handle.close();
|
|
88
|
+
}
|
|
89
|
+
authorize();
|
|
90
|
+
await rename(temp, file);
|
|
91
|
+
if (process.platform !== "win32") {
|
|
92
|
+
const directory = await open(dirname(file), "r");
|
|
93
|
+
try {
|
|
94
|
+
await directory.sync();
|
|
95
|
+
} finally {
|
|
96
|
+
await directory.close();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} finally {
|
|
100
|
+
await rm(temp, { force: true }).catch(() => {
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return redactExport(updated);
|
|
104
|
+
});
|
|
105
|
+
queues.set(file, next);
|
|
106
|
+
try {
|
|
107
|
+
return await next;
|
|
108
|
+
} finally {
|
|
109
|
+
if (queues.get(file) === next) queues.delete(file);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
export {
|
|
114
|
+
CollaborationReviewError,
|
|
115
|
+
CollaborationReviews
|
|
116
|
+
};
|
package/src/config.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
const CODEFLOW_HOME = process.env.CODEFLOW_HOME ?? join(homedir(), ".codeflow");
|
|
4
|
+
const MAX_TIMER_MS = 2147483647;
|
|
5
|
+
const warned = /* @__PURE__ */ new Set();
|
|
6
|
+
function envNumber(name, fallback, range = {}) {
|
|
7
|
+
const raw = process.env[name];
|
|
8
|
+
if (raw === void 0 || raw.trim() === "") return fallback;
|
|
9
|
+
const value = Number(raw);
|
|
10
|
+
const { min = -Infinity, max = Infinity } = range;
|
|
11
|
+
if (!Number.isFinite(value) || value < min || value > max) {
|
|
12
|
+
if (!warned.has(name)) {
|
|
13
|
+
warned.add(name);
|
|
14
|
+
const bounds = [min > -Infinity ? `at least ${min}` : "", max < Infinity ? `at most ${max}` : ""].filter(Boolean).join(", ");
|
|
15
|
+
console.warn(`code-flow: ignoring ${name}=${JSON.stringify(raw)} (${bounds ? `must be a number, ${bounds}` : "not a number"}); using ${fallback}`);
|
|
16
|
+
}
|
|
17
|
+
return fallback;
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
function timerMs(ms, min = 0) {
|
|
22
|
+
return Number.isFinite(ms) ? Math.min(MAX_TIMER_MS, Math.max(min, ms)) : MAX_TIMER_MS;
|
|
23
|
+
}
|
|
24
|
+
const WORKTREE_DIR = process.env.CODEFLOW_WORKTREE_DIR ?? ".worktrees";
|
|
25
|
+
const PORT = envNumber("PORT", 4317, { min: 1, max: 65535 });
|
|
26
|
+
const MAX_DIFF_CHARS = envNumber("CODEFLOW_MAX_DIFF_CHARS", 6e4, { min: 1e3 });
|
|
27
|
+
const STEP_TIMEOUT_MS = timerMs(envNumber("CODEFLOW_STEP_TIMEOUT_MIN", 30, { min: 0.01, max: 24 * 60 }) * 6e4);
|
|
28
|
+
const STEP_IDLE_MS = timerMs(envNumber("CODEFLOW_STEP_IDLE_MIN", 15, { min: 0.01, max: 24 * 60 }) * 6e4);
|
|
29
|
+
function codexTransport() {
|
|
30
|
+
const value = process.env.CODEFLOW_CODEX_TRANSPORT;
|
|
31
|
+
return value === "app-server" || value === "exec" ? value : "auto";
|
|
32
|
+
}
|
|
33
|
+
function autoTeamEnabled() {
|
|
34
|
+
return !["0", "false", "off"].includes((process.env.CODEFLOW_AUTO_TEAM_ENABLED ?? "true").toLowerCase());
|
|
35
|
+
}
|
|
36
|
+
export {
|
|
37
|
+
CODEFLOW_HOME,
|
|
38
|
+
MAX_DIFF_CHARS,
|
|
39
|
+
MAX_TIMER_MS,
|
|
40
|
+
PORT,
|
|
41
|
+
STEP_IDLE_MS,
|
|
42
|
+
STEP_TIMEOUT_MS,
|
|
43
|
+
WORKTREE_DIR,
|
|
44
|
+
autoTeamEnabled,
|
|
45
|
+
codexTransport,
|
|
46
|
+
envNumber,
|
|
47
|
+
timerMs
|
|
48
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
function validateContextBudget(value) {
|
|
2
|
+
if (value === void 0 || value === null) return void 0;
|
|
3
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Context budget must be an object.");
|
|
4
|
+
const budget = value;
|
|
5
|
+
if (Object.keys(budget).some((key) => !["maxPromptBytes", "maxApproximateTokens", "policy"].includes(key))) throw new Error("Unknown context budget field.");
|
|
6
|
+
const integer = (number, max) => Number.isSafeInteger(number) && Number(number) >= 1 && Number(number) <= max;
|
|
7
|
+
if (budget.maxPromptBytes !== void 0 && !integer(budget.maxPromptBytes, 64 * 1024 * 1024) || budget.maxApproximateTokens !== void 0 && !integer(budget.maxApproximateTokens, 16 * 1024 * 1024)) throw new Error("Context ceilings must be positive bounded integers.");
|
|
8
|
+
if (budget.maxPromptBytes === void 0 && budget.maxApproximateTokens === void 0) throw new Error("Choose at least one context ceiling.");
|
|
9
|
+
if (budget.policy !== void 0 && budget.policy !== "warn" && budget.policy !== "block") throw new Error("Context budget policy must be warn or block.");
|
|
10
|
+
return { ...budget.maxPromptBytes !== void 0 ? { maxPromptBytes: budget.maxPromptBytes } : {}, ...budget.maxApproximateTokens !== void 0 ? { maxApproximateTokens: budget.maxApproximateTokens } : {}, policy: budget.policy ?? "warn" };
|
|
11
|
+
}
|
|
12
|
+
export {
|
|
13
|
+
validateContextBudget
|
|
14
|
+
};
|