@alisio/core 0.1.0-alpha.9 → 0.1.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/README.md +52 -7
- package/dist/agents/active.d.ts +99 -0
- package/dist/agents/active.js +218 -0
- package/dist/agents/capabilities.d.ts +26 -0
- package/dist/agents/capabilities.js +120 -0
- package/dist/agents/create-agent-skill.d.ts +11 -0
- package/dist/agents/create-agent-skill.js +60 -0
- package/dist/agents/definitions.d.ts +88 -0
- package/dist/agents/definitions.js +404 -0
- package/dist/agents/draft.d.ts +41 -0
- package/dist/agents/draft.js +133 -0
- package/dist/agents/service.d.ts +57 -0
- package/dist/agents/service.js +86 -0
- package/dist/agents/templates.d.ts +8 -0
- package/dist/agents/templates.js +102 -0
- package/dist/analysis/capabilities.d.ts +48 -0
- package/dist/analysis/capabilities.js +90 -0
- package/dist/analysis/data/csv.d.ts +46 -0
- package/dist/analysis/data/csv.js +240 -0
- package/dist/analysis/data/datasets.d.ts +132 -0
- package/dist/analysis/data/datasets.js +441 -0
- package/dist/analysis/data/describe.d.ts +27 -0
- package/dist/analysis/data/describe.js +103 -0
- package/dist/analysis/data/engine-client.d.ts +58 -0
- package/dist/analysis/data/engine-client.js +304 -0
- package/dist/analysis/data/engine-entry.d.ts +1 -0
- package/dist/analysis/data/engine-entry.js +59 -0
- package/dist/analysis/data/engine-source.d.ts +4 -0
- package/dist/analysis/data/engine-source.js +6 -0
- package/dist/analysis/data/engine-types.d.ts +86 -0
- package/dist/analysis/data/engine-types.js +7 -0
- package/dist/analysis/data/infer.d.ts +42 -0
- package/dist/analysis/data/infer.js +118 -0
- package/dist/analysis/data/ingest-worker.d.ts +17 -0
- package/dist/analysis/data/ingest-worker.js +365 -0
- package/dist/analysis/data/json.d.ts +19 -0
- package/dist/analysis/data/json.js +44 -0
- package/dist/analysis/data/query-worker.d.ts +12 -0
- package/dist/analysis/data/query-worker.js +75 -0
- package/dist/analysis/data/rows.d.ts +39 -0
- package/dist/analysis/data/rows.js +120 -0
- package/dist/analysis/data/sql-guard.d.ts +20 -0
- package/dist/analysis/data/sql-guard.js +129 -0
- package/dist/analysis/data/stats.d.ts +18 -0
- package/dist/analysis/data/stats.js +77 -0
- package/dist/analysis/install-hints.d.ts +33 -0
- package/dist/analysis/install-hints.js +144 -0
- package/dist/analysis/janitor.d.ts +60 -0
- package/dist/analysis/janitor.js +264 -0
- package/dist/analysis/jobs.d.ts +94 -0
- package/dist/analysis/jobs.js +165 -0
- package/dist/analysis/oci.d.ts +129 -0
- package/dist/analysis/oci.js +330 -0
- package/dist/analysis/python/sources.d.ts +2 -0
- package/dist/analysis/python/sources.js +10 -0
- package/dist/analysis/requirements.d.ts +32 -0
- package/dist/analysis/requirements.js +73 -0
- package/dist/analysis/rerun.d.ts +40 -0
- package/dist/analysis/rerun.js +136 -0
- package/dist/analysis/runtime-manager.d.ts +146 -0
- package/dist/analysis/runtime-manager.js +472 -0
- package/dist/analysis/status.d.ts +21 -0
- package/dist/analysis/status.js +52 -0
- package/dist/application.d.ts +105 -3
- package/dist/application.js +446 -18
- package/dist/artifacts/kinds.d.ts +35 -0
- package/dist/artifacts/kinds.js +177 -0
- package/dist/artifacts/publisher.d.ts +7 -0
- package/dist/artifacts/publisher.js +29 -0
- package/dist/artifacts/store.d.ts +119 -0
- package/dist/artifacts/store.js +581 -0
- package/dist/background/janitor.d.ts +22 -0
- package/dist/background/janitor.js +62 -0
- package/dist/background/mirror.d.ts +14 -0
- package/dist/background/mirror.js +41 -0
- package/dist/background/notify.d.ts +58 -0
- package/dist/background/notify.js +172 -0
- package/dist/background/output.d.ts +46 -0
- package/dist/background/output.js +179 -0
- package/dist/background/service.d.ts +137 -0
- package/dist/background/service.js +410 -0
- package/dist/background/store.d.ts +103 -0
- package/dist/background/store.js +195 -0
- package/dist/changelog/data.d.ts +6 -0
- package/dist/changelog/data.js +306 -0
- package/dist/changelog/index.d.ts +5 -0
- package/dist/changelog/index.js +7 -0
- package/dist/changelog/parse.d.ts +53 -0
- package/dist/changelog/parse.js +160 -0
- package/dist/commands/builtins.d.ts +10 -0
- package/dist/commands/builtins.js +161 -0
- package/dist/commands/catalog.d.ts +86 -0
- package/dist/commands/catalog.js +304 -0
- package/dist/config.d.ts +206 -6
- package/dist/config.js +426 -20
- package/dist/core/compaction.js +13 -2
- package/dist/core/contracts.d.ts +180 -3
- package/dist/core/opt-in.d.ts +12 -0
- package/dist/core/opt-in.js +12 -0
- package/dist/core/output-limit.d.ts +34 -0
- package/dist/core/output-limit.js +39 -0
- package/dist/core/run-stats.d.ts +16 -0
- package/dist/core/run-stats.js +8 -0
- package/dist/core/runner.d.ts +134 -4
- package/dist/core/runner.js +780 -138
- package/dist/core/timeout.d.ts +15 -0
- package/dist/core/timeout.js +61 -0
- package/dist/core/truncation.d.ts +30 -0
- package/dist/core/truncation.js +83 -0
- package/dist/goal/blockers.d.ts +45 -0
- package/dist/goal/blockers.js +51 -0
- package/dist/goal/budget.d.ts +24 -0
- package/dist/goal/budget.js +53 -0
- package/dist/goal/command.d.ts +36 -0
- package/dist/goal/command.js +98 -0
- package/dist/goal/format.d.ts +19 -0
- package/dist/goal/format.js +102 -0
- package/dist/goal/host.d.ts +29 -0
- package/dist/goal/host.js +42 -0
- package/dist/goal/index.d.ts +9 -0
- package/dist/goal/index.js +9 -0
- package/dist/goal/machine.d.ts +186 -0
- package/dist/goal/machine.js +344 -0
- package/dist/goal/prompts.d.ts +10 -0
- package/dist/goal/prompts.js +75 -0
- package/dist/goal/service.d.ts +113 -0
- package/dist/goal/service.js +273 -0
- package/dist/goal/store.d.ts +59 -0
- package/dist/goal/store.js +184 -0
- package/dist/index.d.ts +50 -5
- package/dist/index.js +49 -4
- package/dist/mcp/connector.js +38 -6
- package/dist/mcp/rich.d.ts +41 -0
- package/dist/mcp/rich.js +362 -0
- package/dist/permissions/modes.d.ts +72 -0
- package/dist/permissions/modes.js +98 -0
- package/dist/plan/exit-plan.d.ts +25 -0
- package/dist/plan/exit-plan.js +130 -0
- package/dist/plan/state.d.ts +102 -0
- package/dist/plan/state.js +165 -0
- package/dist/plugins/host.d.ts +34 -1
- package/dist/plugins/host.js +143 -9
- package/dist/providers/settings.d.ts +15 -0
- package/dist/providers/settings.js +38 -0
- package/dist/reload.d.ts +75 -0
- package/dist/reload.js +204 -0
- package/dist/resources/context.js +6 -1
- package/dist/runtime/access.d.ts +62 -0
- package/dist/runtime/access.js +115 -0
- package/dist/runtime/blobs.d.ts +51 -0
- package/dist/runtime/blobs.js +177 -0
- package/dist/runtime/diff.d.ts +23 -0
- package/dist/runtime/diff.js +154 -0
- package/dist/runtime/ids.d.ts +8 -0
- package/dist/runtime/ids.js +32 -0
- package/dist/runtime/paths.d.ts +11 -0
- package/dist/runtime/paths.js +17 -1
- package/dist/runtime/process.d.ts +22 -0
- package/dist/runtime/process.js +100 -15
- package/dist/runtime/sqlite.d.ts +2 -0
- package/dist/runtime/sqlite.js +18 -0
- package/dist/runtime/store.d.ts +91 -3
- package/dist/runtime/store.js +465 -11
- package/dist/runtime/zip.d.ts +24 -0
- package/dist/runtime/zip.js +126 -0
- package/dist/sessions/children.js +17 -1
- package/dist/sessions/side-questions.d.ts +57 -0
- package/dist/sessions/side-questions.js +161 -0
- package/dist/tools/analysis.d.ts +37 -0
- package/dist/tools/analysis.js +573 -0
- package/dist/tools/artifacts.d.ts +14 -0
- package/dist/tools/artifacts.js +143 -0
- package/dist/tools/background.d.ts +7 -0
- package/dist/tools/background.js +165 -0
- package/dist/tools/data.d.ts +7 -0
- package/dist/tools/data.js +160 -0
- package/dist/tools/execute.d.ts +5 -0
- package/dist/tools/execute.js +15 -3
- package/dist/tools/goal.d.ts +19 -0
- package/dist/tools/goal.js +126 -0
- package/dist/tools/search.d.ts +17 -3
- package/dist/tools/search.js +153 -0
- package/dist/tools/standard.d.ts +4 -1
- package/dist/tools/standard.js +99 -18
- package/dist/version.d.ts +5 -3
- package/dist/version.js +35 -6
- package/package.json +2 -2
package/dist/runtime/store.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { textResult, } from "@alisio/sdk";
|
|
2
|
+
import { isProcessAlive } from "./process.js";
|
|
2
3
|
import { openDatabase } from "./sqlite.js";
|
|
4
|
+
const alive = isProcessAlive;
|
|
5
|
+
const TERMINAL_RUN = "('completed','turns_exceeded','failed','cancelled','interrupted')";
|
|
3
6
|
export class SQLiteStore {
|
|
4
7
|
db;
|
|
5
8
|
constructor(path) {
|
|
@@ -45,12 +48,210 @@ export class SQLiteStore {
|
|
|
45
48
|
this.db.exec("CREATE INDEX IF NOT EXISTS sessions_parent ON sessions(parent_id)");
|
|
46
49
|
this.db.exec("INSERT OR IGNORE INTO schema_migrations VALUES(3)");
|
|
47
50
|
});
|
|
51
|
+
if (!has(4))
|
|
52
|
+
this.db.transaction(() => {
|
|
53
|
+
// v4 (additive): run journal, workspace UI metadata, blob index and nullable columns.
|
|
54
|
+
this.db.exec(`CREATE TABLE IF NOT EXISTS runs(
|
|
55
|
+
id TEXT PRIMARY KEY,
|
|
56
|
+
session TEXT NOT NULL REFERENCES sessions(id),
|
|
57
|
+
status TEXT NOT NULL,
|
|
58
|
+
request_id TEXT,
|
|
59
|
+
correlation_id TEXT,
|
|
60
|
+
owner_pid INTEGER,
|
|
61
|
+
model TEXT,
|
|
62
|
+
created_at INTEGER NOT NULL,
|
|
63
|
+
started_at INTEGER,
|
|
64
|
+
ended_at INTEGER,
|
|
65
|
+
error TEXT,
|
|
66
|
+
usage TEXT);
|
|
67
|
+
CREATE UNIQUE INDEX IF NOT EXISTS runs_request ON runs(session, request_id) WHERE request_id IS NOT NULL;
|
|
68
|
+
CREATE INDEX IF NOT EXISTS runs_session ON runs(session, created_at);
|
|
69
|
+
CREATE TABLE IF NOT EXISTS workspaces(
|
|
70
|
+
path TEXT PRIMARY KEY,
|
|
71
|
+
label TEXT,
|
|
72
|
+
pinned INTEGER NOT NULL DEFAULT 0,
|
|
73
|
+
last_opened_at INTEGER);
|
|
74
|
+
CREATE TABLE IF NOT EXISTS blobs(
|
|
75
|
+
hash TEXT PRIMARY KEY,
|
|
76
|
+
mime TEXT NOT NULL,
|
|
77
|
+
size INTEGER NOT NULL,
|
|
78
|
+
width INTEGER,
|
|
79
|
+
height INTEGER,
|
|
80
|
+
created_at INTEGER NOT NULL);`);
|
|
81
|
+
const add = (table, name, type) => {
|
|
82
|
+
const columns = this.db.prepare(`PRAGMA table_info(${table})`).all();
|
|
83
|
+
if (!columns.some((c) => c.name === name))
|
|
84
|
+
this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${name} ${type}`);
|
|
85
|
+
};
|
|
86
|
+
add("sessions", "pinned", "INTEGER NOT NULL DEFAULT 0");
|
|
87
|
+
add("sessions", "archived_at", "INTEGER");
|
|
88
|
+
add("events", "created_at", "INTEGER");
|
|
89
|
+
add("events", "correlation_id", "TEXT");
|
|
90
|
+
add("tool_calls", "run_id", "TEXT");
|
|
91
|
+
add("tool_calls", "name", "TEXT");
|
|
92
|
+
add("tool_calls", "effect", "TEXT");
|
|
93
|
+
add("tool_calls", "started_at", "INTEGER");
|
|
94
|
+
add("tool_calls", "ended_at", "INTEGER");
|
|
95
|
+
this.db.exec(`CREATE INDEX IF NOT EXISTS events_session ON events(session, seq);
|
|
96
|
+
CREATE INDEX IF NOT EXISTS messages_session ON messages(session, seq);
|
|
97
|
+
INSERT OR IGNORE INTO schema_migrations VALUES(4);`);
|
|
98
|
+
});
|
|
99
|
+
if (!has(5))
|
|
100
|
+
this.db.transaction(() => {
|
|
101
|
+
// v5 (additive): archived workspaces (hidden from the default web list).
|
|
102
|
+
const columns = this.db.prepare("PRAGMA table_info(workspaces)").all();
|
|
103
|
+
if (!columns.some((c) => c.name === "archived_at"))
|
|
104
|
+
this.db.exec("ALTER TABLE workspaces ADD COLUMN archived_at INTEGER");
|
|
105
|
+
this.db.exec("INSERT OR IGNORE INTO schema_migrations VALUES(5)");
|
|
106
|
+
});
|
|
107
|
+
if (!has(6))
|
|
108
|
+
this.db.transaction(() => {
|
|
109
|
+
// v6 (additive): Python analysis executions, published artifacts, persisted capability
|
|
110
|
+
// grants (audit included) and tabular datasets (used from the data phase on).
|
|
111
|
+
this.db.exec(`CREATE TABLE IF NOT EXISTS analysis_executions(
|
|
112
|
+
id TEXT PRIMARY KEY,
|
|
113
|
+
session TEXT NOT NULL,
|
|
114
|
+
root_session TEXT NOT NULL,
|
|
115
|
+
workspace TEXT NOT NULL,
|
|
116
|
+
run_id TEXT, call_id TEXT,
|
|
117
|
+
runtime TEXT NOT NULL CHECK(runtime IN ('managed','oci')),
|
|
118
|
+
status TEXT NOT NULL CHECK(status IN ('running','completed','failed','cancelled','timed_out')),
|
|
119
|
+
script_sha256 TEXT NOT NULL,
|
|
120
|
+
exit_code INTEGER, error TEXT,
|
|
121
|
+
rel_dir TEXT NOT NULL,
|
|
122
|
+
rerun_of TEXT,
|
|
123
|
+
created_at INTEGER NOT NULL, ended_at INTEGER);
|
|
124
|
+
CREATE INDEX IF NOT EXISTS analysis_executions_session
|
|
125
|
+
ON analysis_executions(root_session, created_at);
|
|
126
|
+
CREATE TABLE IF NOT EXISTS artifacts(
|
|
127
|
+
id TEXT PRIMARY KEY,
|
|
128
|
+
session TEXT NOT NULL, root_session TEXT NOT NULL, workspace TEXT NOT NULL,
|
|
129
|
+
run_id TEXT, call_id TEXT,
|
|
130
|
+
execution_id TEXT REFERENCES analysis_executions(id),
|
|
131
|
+
source_path TEXT,
|
|
132
|
+
title TEXT NOT NULL, file_name TEXT NOT NULL,
|
|
133
|
+
kind TEXT NOT NULL, mime TEXT NOT NULL,
|
|
134
|
+
bytes INTEGER NOT NULL, file_count INTEGER NOT NULL DEFAULT 1,
|
|
135
|
+
sha256 TEXT NOT NULL, entry TEXT,
|
|
136
|
+
rel_dir TEXT NOT NULL,
|
|
137
|
+
status TEXT NOT NULL CHECK(status IN ('ready','deleted','expired')),
|
|
138
|
+
provenance TEXT NOT NULL,
|
|
139
|
+
created_at INTEGER NOT NULL, deleted_at INTEGER,
|
|
140
|
+
UNIQUE(execution_id, source_path));
|
|
141
|
+
CREATE INDEX IF NOT EXISTS artifacts_session ON artifacts(root_session, created_at);
|
|
142
|
+
CREATE TABLE IF NOT EXISTS capability_grants(
|
|
143
|
+
id TEXT PRIMARY KEY,
|
|
144
|
+
capability TEXT NOT NULL,
|
|
145
|
+
session TEXT NOT NULL,
|
|
146
|
+
workspace TEXT NOT NULL,
|
|
147
|
+
scope TEXT NOT NULL CHECK(scope IN ('once','session')),
|
|
148
|
+
decision TEXT NOT NULL CHECK(decision IN ('allow','deny')),
|
|
149
|
+
source TEXT NOT NULL CHECK(source IN ('tui','web','flag','headless-grant')),
|
|
150
|
+
call_id TEXT, run_id TEXT, correlation_id TEXT,
|
|
151
|
+
created_at INTEGER NOT NULL,
|
|
152
|
+
revoked_at INTEGER, revoked_by TEXT);
|
|
153
|
+
CREATE INDEX IF NOT EXISTS capability_grants_lookup
|
|
154
|
+
ON capability_grants(capability, session, scope, decision, revoked_at);
|
|
155
|
+
CREATE TABLE IF NOT EXISTS datasets(
|
|
156
|
+
id TEXT PRIMARY KEY,
|
|
157
|
+
session TEXT NOT NULL, root_session TEXT NOT NULL,
|
|
158
|
+
name TEXT NOT NULL, format TEXT NOT NULL,
|
|
159
|
+
sha256 TEXT NOT NULL, bytes INTEGER NOT NULL,
|
|
160
|
+
blob_hash TEXT,
|
|
161
|
+
source_path TEXT,
|
|
162
|
+
db_rel_path TEXT NOT NULL,
|
|
163
|
+
ingest_version INTEGER NOT NULL,
|
|
164
|
+
sheets TEXT NOT NULL,
|
|
165
|
+
created_at INTEGER NOT NULL);
|
|
166
|
+
INSERT OR IGNORE INTO schema_migrations VALUES(6);`);
|
|
167
|
+
});
|
|
168
|
+
if (!has(7))
|
|
169
|
+
this.db.transaction(() => {
|
|
170
|
+
// v7 (additive): background tasks (`bg_run`, with their owner process, so a restart can
|
|
171
|
+
// tell a dead owner's tasks from another live process's) and the per-session goal
|
|
172
|
+
// (`session_goals`, written from the goal phase on).
|
|
173
|
+
this.db.exec(`CREATE TABLE IF NOT EXISTS background_tasks(
|
|
174
|
+
id TEXT PRIMARY KEY,
|
|
175
|
+
session TEXT NOT NULL,
|
|
176
|
+
root_session TEXT NOT NULL,
|
|
177
|
+
workspace TEXT NOT NULL,
|
|
178
|
+
kind TEXT NOT NULL CHECK(kind IN ('shell','subagent')),
|
|
179
|
+
label TEXT NOT NULL,
|
|
180
|
+
command TEXT, cwd TEXT,
|
|
181
|
+
status TEXT NOT NULL CHECK(status IN
|
|
182
|
+
('queued','running','stopping','succeeded','failed','cancelled','lost')),
|
|
183
|
+
exit_code INTEGER, signal TEXT,
|
|
184
|
+
error_code TEXT,
|
|
185
|
+
abort_origin TEXT CHECK(abort_origin IN ('user','model','timeout','shutdown')),
|
|
186
|
+
pid INTEGER, owner_pid INTEGER NOT NULL,
|
|
187
|
+
timeout_ms INTEGER,
|
|
188
|
+
log_path TEXT NOT NULL,
|
|
189
|
+
bytes INTEGER NOT NULL DEFAULT 0,
|
|
190
|
+
truncated INTEGER NOT NULL DEFAULT 0,
|
|
191
|
+
created_at INTEGER NOT NULL, started_at INTEGER, ended_at INTEGER,
|
|
192
|
+
delivered_at INTEGER);
|
|
193
|
+
CREATE INDEX IF NOT EXISTS background_tasks_owner
|
|
194
|
+
ON background_tasks(root_session, created_at);
|
|
195
|
+
CREATE INDEX IF NOT EXISTS background_tasks_status
|
|
196
|
+
ON background_tasks(status, owner_pid);
|
|
197
|
+
CREATE TABLE IF NOT EXISTS session_goals(
|
|
198
|
+
session TEXT PRIMARY KEY REFERENCES sessions(id),
|
|
199
|
+
objective TEXT NOT NULL,
|
|
200
|
+
status TEXT NOT NULL CHECK(status IN
|
|
201
|
+
('active','paused','blocked','budget_limited','complete')),
|
|
202
|
+
reason TEXT, evidence TEXT,
|
|
203
|
+
epoch INTEGER NOT NULL DEFAULT 1,
|
|
204
|
+
token_budget INTEGER, tokens_used INTEGER NOT NULL DEFAULT 0,
|
|
205
|
+
turns_used INTEGER NOT NULL DEFAULT 0,
|
|
206
|
+
max_turns INTEGER NOT NULL, max_wall_ms INTEGER NOT NULL,
|
|
207
|
+
active_ms INTEGER NOT NULL DEFAULT 0,
|
|
208
|
+
no_tool_streak INTEGER NOT NULL DEFAULT 0, repeat_streak INTEGER NOT NULL DEFAULT 0,
|
|
209
|
+
last_reply_hash TEXT, blocked_streak INTEGER NOT NULL DEFAULT 0,
|
|
210
|
+
created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, completed_at INTEGER);
|
|
211
|
+
INSERT OR IGNORE INTO schema_migrations VALUES(7);`);
|
|
212
|
+
});
|
|
213
|
+
if (!has(8))
|
|
214
|
+
this.db.transaction(() => {
|
|
215
|
+
// v8 (additive): the goal runtime's own columns (the continuation counter and its
|
|
216
|
+
// in-flight request id, the last settled run, the contract-sent flag, the driving
|
|
217
|
+
// process). `session_goals` was created by v7 and had no reader until the goal phase.
|
|
218
|
+
const columns = new Set(this.db.prepare("PRAGMA table_info(session_goals)").all().map((c) => c.name));
|
|
219
|
+
const add = (name, type) => {
|
|
220
|
+
if (!columns.has(name))
|
|
221
|
+
this.db.exec(`ALTER TABLE session_goals ADD COLUMN ${name} ${type}`);
|
|
222
|
+
};
|
|
223
|
+
add("goal_id", "TEXT");
|
|
224
|
+
add("detail", "TEXT");
|
|
225
|
+
add("summary", "TEXT");
|
|
226
|
+
add("blocked_run", "TEXT");
|
|
227
|
+
add("continuations", "INTEGER NOT NULL DEFAULT 0");
|
|
228
|
+
add("inflight", "TEXT");
|
|
229
|
+
add("last_run_id", "TEXT");
|
|
230
|
+
add("kickoff_sent", "INTEGER NOT NULL DEFAULT 0");
|
|
231
|
+
add("owner_pid", "INTEGER");
|
|
232
|
+
this.db.exec("INSERT OR IGNORE INTO schema_migrations VALUES(8)");
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* The root of a session (itself for roots), following `parent_id`. Unknown ids are their own
|
|
237
|
+
* root, so callers never throw on a session another process has not written yet.
|
|
238
|
+
*/
|
|
239
|
+
rootOf(id) {
|
|
240
|
+
let current = id;
|
|
241
|
+
for (let depth = 0; depth < 32; depth++) {
|
|
242
|
+
const row = this.db.prepare("SELECT parent_id FROM sessions WHERE id=?").get(current);
|
|
243
|
+
if (!row?.parent_id)
|
|
244
|
+
return current;
|
|
245
|
+
current = row.parent_id;
|
|
246
|
+
}
|
|
247
|
+
return current;
|
|
48
248
|
}
|
|
49
249
|
create(workspace, provider, model) {
|
|
50
250
|
const id = crypto.randomUUID();
|
|
251
|
+
const now = Date.now();
|
|
51
252
|
this.db
|
|
52
|
-
.prepare("INSERT INTO sessions(id,workspace,provider,model) VALUES(
|
|
53
|
-
.run(id, workspace, provider, model);
|
|
253
|
+
.prepare("INSERT INTO sessions(id,workspace,provider,model,created_at,updated_at) VALUES(?,?,?,?,?,?)")
|
|
254
|
+
.run(id, workspace, provider, model, now, now);
|
|
54
255
|
return { id, workspace, provider, model };
|
|
55
256
|
}
|
|
56
257
|
row(r) {
|
|
@@ -68,6 +269,8 @@ export class SQLiteStore {
|
|
|
68
269
|
...(r.options ? { options: JSON.parse(String(r.options)) } : {}),
|
|
69
270
|
...(r.created_at ? { createdAt: Number(r.created_at) } : {}),
|
|
70
271
|
...(r.updated_at ? { updatedAt: Number(r.updated_at) } : {}),
|
|
272
|
+
...(r.pinned ? { pinned: true } : {}),
|
|
273
|
+
...(r.archived_at ? { archivedAt: Number(r.archived_at) } : {}),
|
|
71
274
|
};
|
|
72
275
|
}
|
|
73
276
|
get(id) {
|
|
@@ -147,6 +350,10 @@ export class SQLiteStore {
|
|
|
147
350
|
this.db
|
|
148
351
|
.prepare("INSERT INTO messages(session,body) VALUES(?,?)")
|
|
149
352
|
.run(id, JSON.stringify(message));
|
|
353
|
+
this.touch(id);
|
|
354
|
+
}
|
|
355
|
+
touch(id) {
|
|
356
|
+
this.db.prepare("UPDATE sessions SET updated_at=? WHERE id=?").run(Date.now(), id);
|
|
150
357
|
}
|
|
151
358
|
setModel(id, model) {
|
|
152
359
|
if (!model.trim())
|
|
@@ -201,15 +408,37 @@ export class SQLiteStore {
|
|
|
201
408
|
.prepare("UPDATE sessions SET locked_pid=NULL WHERE id=? AND locked_pid=?")
|
|
202
409
|
.run(id, process.pid);
|
|
203
410
|
}
|
|
204
|
-
beginCall(id, call) {
|
|
411
|
+
beginCall(id, call, meta = {}) {
|
|
205
412
|
this.db
|
|
206
|
-
.prepare("INSERT INTO tool_calls(session,call_id,status) VALUES(?,?,'pending')")
|
|
207
|
-
.run(id, call.id);
|
|
413
|
+
.prepare("INSERT INTO tool_calls(session,call_id,status,run_id,name,effect,started_at) VALUES(?,?,'pending',?,?,?,?)")
|
|
414
|
+
.run(id, call.id, meta.runId ?? null, call.name, meta.effect ?? null, Date.now());
|
|
208
415
|
}
|
|
209
416
|
endCall(id, call, result) {
|
|
210
417
|
this.db
|
|
211
|
-
.prepare("INSERT INTO tool_calls(session,call_id,status,result) VALUES(?,?,'completed'
|
|
212
|
-
.run(id, call.id, JSON.stringify(result));
|
|
418
|
+
.prepare("INSERT INTO tool_calls(session,call_id,status,result,name,ended_at) VALUES(?,?,'completed',?,?,?) ON CONFLICT(session,call_id) DO UPDATE SET status='completed',result=excluded.result,ended_at=excluded.ended_at,name=COALESCE(tool_calls.name,excluded.name)")
|
|
419
|
+
.run(id, call.id, JSON.stringify(result), call.name, Date.now());
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Calls of a session with one effect (for example `write`), oldest first. Needs the v4
|
|
423
|
+
* `effect`/`run_id`/`started_at` columns; rows written by older versions have no effect.
|
|
424
|
+
*/
|
|
425
|
+
effectCalls(session, effect) {
|
|
426
|
+
const rows = this.db
|
|
427
|
+
.prepare("SELECT call_id,name,run_id,started_at FROM tool_calls WHERE session=? AND effect=? ORDER BY started_at, rowid")
|
|
428
|
+
.all(session, effect);
|
|
429
|
+
return rows.map((row) => ({
|
|
430
|
+
callId: row.call_id,
|
|
431
|
+
name: row.name ?? "",
|
|
432
|
+
...(row.run_id ? { runId: row.run_id } : {}),
|
|
433
|
+
...(row.started_at != null ? { startedAt: Number(row.started_at) } : {}),
|
|
434
|
+
}));
|
|
435
|
+
}
|
|
436
|
+
/** Rich (ui/image) parts persist as part of the serialized result and replay verbatim. */
|
|
437
|
+
callResult(id, callId) {
|
|
438
|
+
const row = this.db
|
|
439
|
+
.prepare("SELECT result FROM tool_calls WHERE session=? AND call_id=? AND status='completed'")
|
|
440
|
+
.get(id, callId);
|
|
441
|
+
return row?.result ? JSON.parse(row.result) : undefined;
|
|
213
442
|
}
|
|
214
443
|
reconcile(id, acknowledge = false) {
|
|
215
444
|
this.db.transaction(() => {
|
|
@@ -237,10 +466,235 @@ export class SQLiteStore {
|
|
|
237
466
|
}
|
|
238
467
|
});
|
|
239
468
|
}
|
|
240
|
-
event
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
.
|
|
469
|
+
/** Persists a durable event and returns its global `events.seq`. */
|
|
470
|
+
event(id, runId, type, data, meta = {}) {
|
|
471
|
+
const { lastInsertRowid } = this.db
|
|
472
|
+
.prepare("INSERT INTO events(session,run_id,type,body,created_at,correlation_id) VALUES(?,?,?,?,?,?)")
|
|
473
|
+
.run(id, runId, type, JSON.stringify(data), Date.now(), meta.correlationId ?? null);
|
|
474
|
+
return Number(lastInsertRowid);
|
|
475
|
+
}
|
|
476
|
+
runRow(r) {
|
|
477
|
+
return {
|
|
478
|
+
id: String(r.id),
|
|
479
|
+
session: String(r.session),
|
|
480
|
+
status: r.status,
|
|
481
|
+
...(r.request_id != null ? { requestId: String(r.request_id) } : {}),
|
|
482
|
+
...(r.correlation_id != null ? { correlationId: String(r.correlation_id) } : {}),
|
|
483
|
+
...(r.owner_pid != null ? { ownerPid: Number(r.owner_pid) } : {}),
|
|
484
|
+
...(r.model != null ? { model: String(r.model) } : {}),
|
|
485
|
+
createdAt: Number(r.created_at),
|
|
486
|
+
...(r.started_at != null ? { startedAt: Number(r.started_at) } : {}),
|
|
487
|
+
...(r.ended_at != null ? { endedAt: Number(r.ended_at) } : {}),
|
|
488
|
+
...(r.error != null ? { error: String(r.error) } : {}),
|
|
489
|
+
...(r.usage != null ? { usage: JSON.parse(String(r.usage)) } : {}),
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
runById(id) {
|
|
493
|
+
const row = this.db.prepare("SELECT * FROM runs WHERE id=?").get(id);
|
|
494
|
+
return row ? this.runRow(row) : undefined;
|
|
495
|
+
}
|
|
496
|
+
beginRun(input) {
|
|
497
|
+
return this.db.transaction(() => {
|
|
498
|
+
const status = input.status ?? "running";
|
|
499
|
+
const now = Date.now();
|
|
500
|
+
const existing = this.runById(input.id);
|
|
501
|
+
if (existing) {
|
|
502
|
+
if (existing.status === "queued" && status === "running")
|
|
503
|
+
this.db
|
|
504
|
+
.prepare("UPDATE runs SET status='running', started_at=?, owner_pid=?, model=COALESCE(?,model) WHERE id=?")
|
|
505
|
+
.run(now, process.pid, input.model ?? null, input.id);
|
|
506
|
+
return { run: this.runById(input.id), created: false };
|
|
507
|
+
}
|
|
508
|
+
if (input.requestId !== undefined) {
|
|
509
|
+
const retried = this.runByRequest(input.session, input.requestId);
|
|
510
|
+
if (retried)
|
|
511
|
+
return { run: retried, created: false };
|
|
512
|
+
}
|
|
513
|
+
this.db
|
|
514
|
+
.prepare(`INSERT INTO runs(id,session,status,request_id,correlation_id,owner_pid,model,created_at,started_at)
|
|
515
|
+
VALUES(?,?,?,?,?,?,?,?,?)`)
|
|
516
|
+
.run(input.id, input.session, status, input.requestId ?? null, input.correlationId ?? null, process.pid, input.model ?? null, now, status === "running" ? now : null);
|
|
517
|
+
return { run: this.runById(input.id), created: true };
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
endRun(id, input) {
|
|
521
|
+
const now = Date.now();
|
|
522
|
+
this.db.transaction(() => {
|
|
523
|
+
this.db
|
|
524
|
+
.prepare(`UPDATE runs SET status=?, ended_at=?, error=?, usage=COALESCE(?,usage)
|
|
525
|
+
WHERE id=? AND status NOT IN ${TERMINAL_RUN}`)
|
|
526
|
+
.run(input.status, now, input.error ?? null, input.usage ? JSON.stringify(input.usage) : null, id);
|
|
527
|
+
const run = this.runById(id);
|
|
528
|
+
if (run)
|
|
529
|
+
this.touch(run.session);
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
runByRequest(session, requestId) {
|
|
533
|
+
const row = this.db
|
|
534
|
+
.prepare("SELECT * FROM runs WHERE session=? AND request_id=?")
|
|
535
|
+
.get(session, requestId);
|
|
536
|
+
return row ? this.runRow(row) : undefined;
|
|
537
|
+
}
|
|
538
|
+
runs(session, options = {}) {
|
|
539
|
+
return this.db
|
|
540
|
+
.prepare("SELECT * FROM runs WHERE session=? ORDER BY created_at DESC, rowid DESC LIMIT ?")
|
|
541
|
+
.all(session, options.limit ?? 100)
|
|
542
|
+
.map((r) => this.runRow(r));
|
|
543
|
+
}
|
|
544
|
+
interruptRuns() {
|
|
545
|
+
const rows = this.db
|
|
546
|
+
.prepare("SELECT id, owner_pid FROM runs WHERE status IN ('queued','running')")
|
|
547
|
+
.all();
|
|
548
|
+
let count = 0;
|
|
549
|
+
for (const row of rows) {
|
|
550
|
+
// Runs of this process (another Application in it) or of another live process stay.
|
|
551
|
+
if (row.owner_pid && (row.owner_pid === process.pid || alive(row.owner_pid)))
|
|
552
|
+
continue;
|
|
553
|
+
this.endRun(row.id, { status: "interrupted" });
|
|
554
|
+
count++;
|
|
555
|
+
}
|
|
556
|
+
return count;
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* A page of messages by `seq`. With `after`, the first `limit` rows after it; otherwise the
|
|
560
|
+
* newest `limit` rows (before `before` when given). Items are always in ascending order.
|
|
561
|
+
*/
|
|
562
|
+
messagesPage(session, options = {}) {
|
|
563
|
+
const limit = Math.max(1, Math.min(options.limit ?? 100, 1000));
|
|
564
|
+
const active = options.compacted ? "" : " AND compacted=0";
|
|
565
|
+
const rows = (options.after !== undefined
|
|
566
|
+
? this.db
|
|
567
|
+
.prepare(`SELECT seq,body,compacted FROM messages WHERE session=? AND seq>?${active} ORDER BY seq LIMIT ?`)
|
|
568
|
+
.all(session, options.after, limit + 1)
|
|
569
|
+
: this.db
|
|
570
|
+
.prepare(`SELECT seq,body,compacted FROM messages WHERE session=? AND seq<?${active} ORDER BY seq DESC LIMIT ?`)
|
|
571
|
+
.all(session, options.before ?? Number.MAX_SAFE_INTEGER, limit + 1));
|
|
572
|
+
const hasMore = rows.length > limit;
|
|
573
|
+
const page = rows.slice(0, limit);
|
|
574
|
+
if (options.after === undefined)
|
|
575
|
+
page.reverse();
|
|
576
|
+
return {
|
|
577
|
+
items: page.map((r) => ({
|
|
578
|
+
seq: Number(r.seq),
|
|
579
|
+
message: JSON.parse(r.body),
|
|
580
|
+
compacted: !!r.compacted,
|
|
581
|
+
})),
|
|
582
|
+
hasMore,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
/** Durable events of a session after `after` (a previous `eventId`), ascending. */
|
|
586
|
+
eventsPage(session, options = {}) {
|
|
587
|
+
const limit = Math.max(1, Math.min(options.limit ?? 500, 5000));
|
|
588
|
+
const rows = this.db
|
|
589
|
+
.prepare("SELECT seq,run_id,type,body,created_at,correlation_id FROM events WHERE session=? AND seq>? AND seq<? ORDER BY seq LIMIT ?")
|
|
590
|
+
.all(session, options.after ?? 0, options.before ?? Number.MAX_SAFE_INTEGER, limit + 1);
|
|
591
|
+
return {
|
|
592
|
+
items: rows.slice(0, limit).map((r) => ({
|
|
593
|
+
eventId: String(r.seq),
|
|
594
|
+
runId: String(r.run_id),
|
|
595
|
+
type: String(r.type),
|
|
596
|
+
data: r.body == null ? undefined : JSON.parse(String(r.body)),
|
|
597
|
+
...(r.created_at != null ? { createdAt: Number(r.created_at) } : {}),
|
|
598
|
+
...(r.correlation_id != null ? { correlationId: String(r.correlation_id) } : {}),
|
|
599
|
+
})),
|
|
600
|
+
hasMore: rows.length > limit,
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Known workspaces for web clients: the workspaces of root sessions plus the ones recorded with
|
|
605
|
+
* `recordWorkspace` (v4 `workspaces` table), with their UI metadata (v5: `archivedAt`) and
|
|
606
|
+
* root-session count.
|
|
607
|
+
*/
|
|
608
|
+
workspaces() {
|
|
609
|
+
const rows = this.db
|
|
610
|
+
.prepare(`SELECT p.path, w.label, COALESCE(w.pinned, 0) AS pinned, w.last_opened_at, w.archived_at,
|
|
611
|
+
(SELECT count(*) FROM sessions s WHERE s.workspace=p.path AND s.parent_id IS NULL) AS sessions
|
|
612
|
+
FROM (SELECT workspace AS path FROM sessions WHERE parent_id IS NULL AND workspace IS NOT NULL
|
|
613
|
+
UNION SELECT path FROM workspaces) p
|
|
614
|
+
LEFT JOIN workspaces w ON w.path=p.path
|
|
615
|
+
ORDER BY p.path`)
|
|
616
|
+
.all();
|
|
617
|
+
return rows.map((r) => ({
|
|
618
|
+
path: String(r.path),
|
|
619
|
+
...(r.label != null ? { label: String(r.label) } : {}),
|
|
620
|
+
pinned: !!r.pinned,
|
|
621
|
+
...(r.last_opened_at != null ? { lastOpenedAt: Number(r.last_opened_at) } : {}),
|
|
622
|
+
...(r.archived_at != null ? { archivedAt: Number(r.archived_at) } : {}),
|
|
623
|
+
sessions: Number(r.sessions ?? 0),
|
|
624
|
+
}));
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Records (upserts) a workspace and patches its UI metadata (`label: null` clears the label;
|
|
628
|
+
* `archived` stamps or clears `archived_at` and never touches its sessions).
|
|
629
|
+
*/
|
|
630
|
+
recordWorkspace(path, patch = {}) {
|
|
631
|
+
this.db.transaction(() => {
|
|
632
|
+
this.db.prepare("INSERT OR IGNORE INTO workspaces(path) VALUES(?)").run(path);
|
|
633
|
+
if (patch.label !== undefined)
|
|
634
|
+
this.db.prepare("UPDATE workspaces SET label=? WHERE path=?").run(patch.label, path);
|
|
635
|
+
if (patch.pinned !== undefined)
|
|
636
|
+
this.db
|
|
637
|
+
.prepare("UPDATE workspaces SET pinned=? WHERE path=?")
|
|
638
|
+
.run(patch.pinned ? 1 : 0, path);
|
|
639
|
+
if (patch.lastOpenedAt !== undefined)
|
|
640
|
+
this.db
|
|
641
|
+
.prepare("UPDATE workspaces SET last_opened_at=? WHERE path=?")
|
|
642
|
+
.run(patch.lastOpenedAt, path);
|
|
643
|
+
if (patch.archived !== undefined)
|
|
644
|
+
this.db
|
|
645
|
+
.prepare("UPDATE workspaces SET archived_at=? WHERE path=?")
|
|
646
|
+
.run(patch.archived ? Date.now() : null, path);
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Patches web-facing session metadata: title, pin, archive flag and `options` (shallow-merged
|
|
651
|
+
* into the stored options object). Does not touch the run-relevant columns.
|
|
652
|
+
*/
|
|
653
|
+
updateSessionMeta(id, patch) {
|
|
654
|
+
this.db.transaction(() => {
|
|
655
|
+
const current = this.get(id);
|
|
656
|
+
if (patch.title !== undefined)
|
|
657
|
+
this.db.prepare("UPDATE sessions SET title=? WHERE id=?").run(patch.title, id);
|
|
658
|
+
if (patch.pinned !== undefined)
|
|
659
|
+
this.db.prepare("UPDATE sessions SET pinned=? WHERE id=?").run(patch.pinned ? 1 : 0, id);
|
|
660
|
+
if (patch.archived !== undefined)
|
|
661
|
+
this.db
|
|
662
|
+
.prepare("UPDATE sessions SET archived_at=? WHERE id=?")
|
|
663
|
+
.run(patch.archived ? Date.now() : null, id);
|
|
664
|
+
if (patch.options)
|
|
665
|
+
this.db
|
|
666
|
+
.prepare("UPDATE sessions SET options=? WHERE id=?")
|
|
667
|
+
.run(JSON.stringify({ ...(current.options ?? {}), ...patch.options }), id);
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Atomic read-modify-write of `sessions.options` (`BEGIN IMMEDIATE`: safe across processes that
|
|
672
|
+
* share the database). `change` receives the current options and returns the keys to merge
|
|
673
|
+
* (`undefined` removes a key; `undefined` as a whole changes nothing) and a value for the caller.
|
|
674
|
+
*/
|
|
675
|
+
mutateSessionOptions(id, change) {
|
|
676
|
+
return this.db.transaction(() => {
|
|
677
|
+
const current = this.get(id).options ?? {};
|
|
678
|
+
const { patch, value } = change(current);
|
|
679
|
+
if (patch)
|
|
680
|
+
this.db
|
|
681
|
+
.prepare("UPDATE sessions SET options=? WHERE id=?")
|
|
682
|
+
.run(JSON.stringify({ ...current, ...patch }), id);
|
|
683
|
+
return value;
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
/** `MAX(events.seq)` of a session (0 without events): the web snapshot cursor. */
|
|
687
|
+
lastEventId(session) {
|
|
688
|
+
const row = this.db
|
|
689
|
+
.prepare("SELECT MAX(seq) AS seq FROM events WHERE session=?")
|
|
690
|
+
.get(session);
|
|
691
|
+
return Number(row?.seq ?? 0);
|
|
692
|
+
}
|
|
693
|
+
/** The pid of another live process holding the session lock, if any. */
|
|
694
|
+
lockedBy(id) {
|
|
695
|
+
const row = this.db.prepare("SELECT locked_pid FROM sessions WHERE id=?").get(id);
|
|
696
|
+
const pid = row?.locked_pid;
|
|
697
|
+
return pid && pid !== process.pid && alive(pid) ? pid : undefined;
|
|
244
698
|
}
|
|
245
699
|
getState(plugin, key) {
|
|
246
700
|
const row = this.db
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type ZipEntry = {
|
|
2
|
+
name: string;
|
|
3
|
+
source: string;
|
|
4
|
+
} | {
|
|
5
|
+
name: string;
|
|
6
|
+
data: Uint8Array;
|
|
7
|
+
};
|
|
8
|
+
/** CRC-32 (IEEE) of `bytes`, continuing from `crc`. */
|
|
9
|
+
export declare function crc32(bytes: Uint8Array, crc?: number): number;
|
|
10
|
+
/** Something to write the archive to (an HTTP response, a file stream…), honoring backpressure. */
|
|
11
|
+
export interface ZipSink {
|
|
12
|
+
write(chunk: Buffer): boolean;
|
|
13
|
+
once(event: "drain", listener: () => void): unknown;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Writes `entries` as a ZIP archive to `out`, streaming file contents. Entry names use `/` and
|
|
17
|
+
* must be relative; the caller validates them. Throws when the archive would need ZIP64.
|
|
18
|
+
*/
|
|
19
|
+
export declare function writeZip(entries: ZipEntry[], out: ZipSink, options?: {
|
|
20
|
+
now?: Date;
|
|
21
|
+
signal?: AbortSignal;
|
|
22
|
+
}): Promise<{
|
|
23
|
+
bytes: number;
|
|
24
|
+
}>;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming ZIP writer on `node:zlib` (no dependency): DEFLATE entries with a data descriptor,
|
|
3
|
+
* UTF-8 names (general-purpose bit 11) and no ZIP64, so archives and entries must stay below
|
|
4
|
+
* 4 GiB. Used for multi-file artifact downloads and for folders published as one archive.
|
|
5
|
+
*/
|
|
6
|
+
import { createReadStream } from "node:fs";
|
|
7
|
+
import { createDeflateRaw } from "node:zlib";
|
|
8
|
+
let table;
|
|
9
|
+
/** CRC-32 (IEEE) of `bytes`, continuing from `crc`. */
|
|
10
|
+
export function crc32(bytes, crc = 0) {
|
|
11
|
+
if (!table) {
|
|
12
|
+
table = new Uint32Array(256);
|
|
13
|
+
for (let n = 0; n < 256; n++) {
|
|
14
|
+
let c = n;
|
|
15
|
+
for (let k = 0; k < 8; k++)
|
|
16
|
+
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
17
|
+
table[n] = c >>> 0;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
let value = ~crc >>> 0;
|
|
21
|
+
for (let i = 0; i < bytes.length; i++)
|
|
22
|
+
value = table[(value ^ bytes[i]) & 0xff] ^ (value >>> 8);
|
|
23
|
+
return ~value >>> 0;
|
|
24
|
+
}
|
|
25
|
+
/** MS-DOS date and time of `date` (local time, 2-second resolution, 1980 minimum). */
|
|
26
|
+
function dosTime(date) {
|
|
27
|
+
const year = Math.max(1980, date.getFullYear());
|
|
28
|
+
return {
|
|
29
|
+
time: (date.getHours() << 11) | (date.getMinutes() << 5) | Math.floor(date.getSeconds() / 2),
|
|
30
|
+
date: ((year - 1980) << 9) | ((date.getMonth() + 1) << 5) | date.getDate(),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const LIMIT = 0xffff_ffff;
|
|
34
|
+
/**
|
|
35
|
+
* Writes `entries` as a ZIP archive to `out`, streaming file contents. Entry names use `/` and
|
|
36
|
+
* must be relative; the caller validates them. Throws when the archive would need ZIP64.
|
|
37
|
+
*/
|
|
38
|
+
export async function writeZip(entries, out, options = {}) {
|
|
39
|
+
if (entries.length > 0xffff)
|
|
40
|
+
throw new Error("ZIP archives hold at most 65535 entries");
|
|
41
|
+
let offset = 0;
|
|
42
|
+
const put = async (chunk) => {
|
|
43
|
+
options.signal?.throwIfAborted();
|
|
44
|
+
offset += chunk.byteLength;
|
|
45
|
+
if (offset > LIMIT)
|
|
46
|
+
throw new Error("ZIP archives larger than 4 GiB are not supported");
|
|
47
|
+
if (!out.write(chunk))
|
|
48
|
+
await new Promise((resolve) => out.once("drain", resolve));
|
|
49
|
+
};
|
|
50
|
+
const { time, date } = dosTime(options.now ?? new Date());
|
|
51
|
+
const central = [];
|
|
52
|
+
for (const entry of entries) {
|
|
53
|
+
const name = Buffer.from(entry.name.replace(/\\/g, "/"), "utf8");
|
|
54
|
+
const start = offset;
|
|
55
|
+
const local = Buffer.alloc(30);
|
|
56
|
+
local.writeUInt32LE(0x04034b50, 0);
|
|
57
|
+
local.writeUInt16LE(20, 4); // version needed
|
|
58
|
+
local.writeUInt16LE(0x0808, 6); // data descriptor + UTF-8 names
|
|
59
|
+
local.writeUInt16LE(8, 8); // deflate
|
|
60
|
+
local.writeUInt16LE(time, 10);
|
|
61
|
+
local.writeUInt16LE(date, 12);
|
|
62
|
+
local.writeUInt16LE(name.length, 26);
|
|
63
|
+
await put(local);
|
|
64
|
+
await put(name);
|
|
65
|
+
let crc = 0, size = 0, compressed = 0;
|
|
66
|
+
const deflate = createDeflateRaw();
|
|
67
|
+
const feed = "data" in entry
|
|
68
|
+
? (async () => {
|
|
69
|
+
crc = crc32(entry.data);
|
|
70
|
+
size = entry.data.byteLength;
|
|
71
|
+
deflate.end(Buffer.from(entry.data));
|
|
72
|
+
})()
|
|
73
|
+
: new Promise((resolve, reject) => {
|
|
74
|
+
const input = createReadStream(entry.source);
|
|
75
|
+
input.on("data", (chunk) => {
|
|
76
|
+
const bytes = chunk;
|
|
77
|
+
crc = crc32(bytes, crc);
|
|
78
|
+
size += bytes.byteLength;
|
|
79
|
+
});
|
|
80
|
+
input.once("error", (error) => {
|
|
81
|
+
deflate.destroy(error);
|
|
82
|
+
reject(error);
|
|
83
|
+
});
|
|
84
|
+
input.once("end", resolve);
|
|
85
|
+
input.pipe(deflate);
|
|
86
|
+
});
|
|
87
|
+
for await (const chunk of deflate) {
|
|
88
|
+
compressed += chunk.byteLength;
|
|
89
|
+
await put(chunk);
|
|
90
|
+
}
|
|
91
|
+
await feed;
|
|
92
|
+
if (size > LIMIT || compressed > LIMIT)
|
|
93
|
+
throw new Error("ZIP entries larger than 4 GiB are not supported");
|
|
94
|
+
const descriptor = Buffer.alloc(16);
|
|
95
|
+
descriptor.writeUInt32LE(0x08074b50, 0);
|
|
96
|
+
descriptor.writeUInt32LE(crc, 4);
|
|
97
|
+
descriptor.writeUInt32LE(compressed, 8);
|
|
98
|
+
descriptor.writeUInt32LE(size, 12);
|
|
99
|
+
await put(descriptor);
|
|
100
|
+
const header = Buffer.alloc(46);
|
|
101
|
+
header.writeUInt32LE(0x02014b50, 0);
|
|
102
|
+
header.writeUInt16LE(20, 4); // version made by
|
|
103
|
+
header.writeUInt16LE(20, 6); // version needed
|
|
104
|
+
header.writeUInt16LE(0x0808, 8);
|
|
105
|
+
header.writeUInt16LE(8, 10);
|
|
106
|
+
header.writeUInt16LE(time, 12);
|
|
107
|
+
header.writeUInt16LE(date, 14);
|
|
108
|
+
header.writeUInt32LE(crc, 16);
|
|
109
|
+
header.writeUInt32LE(compressed, 20);
|
|
110
|
+
header.writeUInt32LE(size, 24);
|
|
111
|
+
header.writeUInt16LE(name.length, 28);
|
|
112
|
+
header.writeUInt32LE(start, 42);
|
|
113
|
+
central.push(header, name);
|
|
114
|
+
}
|
|
115
|
+
const directoryStart = offset;
|
|
116
|
+
for (const part of central)
|
|
117
|
+
await put(part);
|
|
118
|
+
const end = Buffer.alloc(22);
|
|
119
|
+
end.writeUInt32LE(0x06054b50, 0);
|
|
120
|
+
end.writeUInt16LE(entries.length, 8);
|
|
121
|
+
end.writeUInt16LE(entries.length, 10);
|
|
122
|
+
end.writeUInt32LE(offset - directoryStart, 12);
|
|
123
|
+
end.writeUInt32LE(directoryStart, 16);
|
|
124
|
+
await put(end);
|
|
125
|
+
return { bytes: offset };
|
|
126
|
+
}
|