@oharness/engine 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/agent.d.ts +196 -0
- package/dist/agent/agent.js +758 -0
- package/dist/agent/agent.js.map +1 -0
- package/dist/agent/compaction.d.ts +68 -0
- package/dist/agent/compaction.js +179 -0
- package/dist/agent/compaction.js.map +1 -0
- package/dist/agent/hooks.d.ts +77 -0
- package/dist/agent/hooks.js +152 -0
- package/dist/agent/hooks.js.map +1 -0
- package/dist/agent/memory.d.ts +45 -0
- package/dist/agent/memory.js +147 -0
- package/dist/agent/memory.js.map +1 -0
- package/dist/agent/overflow.d.ts +35 -0
- package/dist/agent/overflow.js +69 -0
- package/dist/agent/overflow.js.map +1 -0
- package/dist/agent/permissions.d.ts +181 -0
- package/dist/agent/permissions.js +289 -0
- package/dist/agent/permissions.js.map +1 -0
- package/dist/agent/prompt.d.ts +25 -0
- package/dist/agent/prompt.js +63 -0
- package/dist/agent/prompt.js.map +1 -0
- package/dist/agent/session.d.ts +108 -0
- package/dist/agent/session.js +231 -0
- package/dist/agent/session.js.map +1 -0
- package/dist/agent/settings.d.ts +37 -0
- package/dist/agent/settings.js +113 -0
- package/dist/agent/settings.js.map +1 -0
- package/dist/agent/skills.d.ts +58 -0
- package/dist/agent/skills.js +225 -0
- package/dist/agent/skills.js.map +1 -0
- package/dist/agent/store.d.ts +308 -0
- package/dist/agent/store.js +596 -0
- package/dist/agent/store.js.map +1 -0
- package/dist/agent/subagent.d.ts +44 -0
- package/dist/agent/subagent.js +138 -0
- package/dist/agent/subagent.js.map +1 -0
- package/dist/agent/titles.d.ts +8 -0
- package/dist/agent/titles.js +23 -0
- package/dist/agent/titles.js.map +1 -0
- package/dist/agent/workspace.d.ts +60 -0
- package/dist/agent/workspace.js +156 -0
- package/dist/agent/workspace.js.map +1 -0
- package/dist/artifacts/publish.d.ts +94 -0
- package/dist/artifacts/publish.js +104 -0
- package/dist/artifacts/publish.js.map +1 -0
- package/dist/artifacts/store.d.ts +324 -0
- package/dist/artifacts/store.js +500 -0
- package/dist/artifacts/store.js.map +1 -0
- package/dist/auth/index.d.ts +13 -0
- package/dist/auth/index.js +10 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/manager.d.ts +83 -0
- package/dist/auth/manager.js +266 -0
- package/dist/auth/manager.js.map +1 -0
- package/dist/auth/oauth.d.ts +83 -0
- package/dist/auth/oauth.js +327 -0
- package/dist/auth/oauth.js.map +1 -0
- package/dist/auth/providers/account.d.ts +59 -0
- package/dist/auth/providers/account.js +282 -0
- package/dist/auth/providers/account.js.map +1 -0
- package/dist/auth/providers/anthropic.d.ts +24 -0
- package/dist/auth/providers/anthropic.js +209 -0
- package/dist/auth/providers/anthropic.js.map +1 -0
- package/dist/auth/providers/generic.d.ts +57 -0
- package/dist/auth/providers/generic.js +155 -0
- package/dist/auth/providers/generic.js.map +1 -0
- package/dist/auth/providers/official.d.ts +3 -0
- package/dist/auth/providers/official.js +90 -0
- package/dist/auth/providers/official.js.map +1 -0
- package/dist/auth/providers/ottoport.d.ts +3 -0
- package/dist/auth/providers/ottoport.js +60 -0
- package/dist/auth/providers/ottoport.js.map +1 -0
- package/dist/auth/store.d.ts +22 -0
- package/dist/auth/store.js +138 -0
- package/dist/auth/store.js.map +1 -0
- package/dist/auth/types.d.ts +167 -0
- package/dist/auth/types.js +16 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/config.d.ts +197 -0
- package/dist/config.js +195 -0
- package/dist/config.js.map +1 -0
- package/dist/core/errors.d.ts +46 -0
- package/dist/core/errors.js +83 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/tokens.d.ts +12 -0
- package/dist/core/tokens.js +56 -0
- package/dist/core/tokens.js.map +1 -0
- package/dist/core/types.d.ts +91 -0
- package/dist/core/types.js +49 -0
- package/dist/core/types.js.map +1 -0
- package/dist/harness.d.ts +302 -0
- package/dist/harness.js +695 -0
- package/dist/harness.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/client.d.ts +55 -0
- package/dist/mcp/client.js +199 -0
- package/dist/mcp/client.js.map +1 -0
- package/dist/mcp/index.d.ts +13 -0
- package/dist/mcp/index.js +8 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/manager.d.ts +77 -0
- package/dist/mcp/manager.js +325 -0
- package/dist/mcp/manager.js.map +1 -0
- package/dist/mcp/oauth.d.ts +63 -0
- package/dist/mcp/oauth.js +302 -0
- package/dist/mcp/oauth.js.map +1 -0
- package/dist/mcp/protocol.d.ts +130 -0
- package/dist/mcp/protocol.js +14 -0
- package/dist/mcp/protocol.js.map +1 -0
- package/dist/mcp/registry.d.ts +52 -0
- package/dist/mcp/registry.js +100 -0
- package/dist/mcp/registry.js.map +1 -0
- package/dist/mcp/registry.json +293 -0
- package/dist/mcp/secrets.d.ts +20 -0
- package/dist/mcp/secrets.js +75 -0
- package/dist/mcp/secrets.js.map +1 -0
- package/dist/mcp/transport.d.ts +51 -0
- package/dist/mcp/transport.js +158 -0
- package/dist/mcp/transport.js.map +1 -0
- package/dist/paths.d.ts +37 -0
- package/dist/paths.js +63 -0
- package/dist/paths.js.map +1 -0
- package/dist/plugins.d.ts +55 -0
- package/dist/plugins.js +201 -0
- package/dist/plugins.js.map +1 -0
- package/dist/providers/builtin.d.ts +23 -0
- package/dist/providers/builtin.js +141 -0
- package/dist/providers/builtin.js.map +1 -0
- package/dist/providers/catalog.d.ts +129 -0
- package/dist/providers/catalog.js +254 -0
- package/dist/providers/catalog.js.map +1 -0
- package/dist/providers/catalog.json +472 -0
- package/dist/providers/discovery.d.ts +16 -0
- package/dist/providers/discovery.js +161 -0
- package/dist/providers/discovery.js.map +1 -0
- package/dist/providers/http.d.ts +24 -0
- package/dist/providers/http.js +139 -0
- package/dist/providers/http.js.map +1 -0
- package/dist/providers/index.d.ts +10 -0
- package/dist/providers/index.js +8 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/piai.d.ts +124 -0
- package/dist/providers/piai.js +551 -0
- package/dist/providers/piai.js.map +1 -0
- package/dist/providers/registry.d.ts +65 -0
- package/dist/providers/registry.js +195 -0
- package/dist/providers/registry.js.map +1 -0
- package/dist/providers/resilient.d.ts +46 -0
- package/dist/providers/resilient.js +117 -0
- package/dist/providers/resilient.js.map +1 -0
- package/dist/providers/types.d.ts +182 -0
- package/dist/providers/types.js +50 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/schedule/cron.d.ts +35 -0
- package/dist/schedule/cron.js +125 -0
- package/dist/schedule/cron.js.map +1 -0
- package/dist/schedule/scheduler.d.ts +87 -0
- package/dist/schedule/scheduler.js +237 -0
- package/dist/schedule/scheduler.js.map +1 -0
- package/dist/storage/kv.d.ts +56 -0
- package/dist/storage/kv.js +173 -0
- package/dist/storage/kv.js.map +1 -0
- package/dist/storage/migrate.d.ts +11 -0
- package/dist/storage/migrate.js +93 -0
- package/dist/storage/migrate.js.map +1 -0
- package/dist/storage/project.d.ts +9 -0
- package/dist/storage/project.js +57 -0
- package/dist/storage/project.js.map +1 -0
- package/dist/storage/records.d.ts +32 -0
- package/dist/storage/records.js +48 -0
- package/dist/storage/records.js.map +1 -0
- package/dist/sync/client.d.ts +79 -0
- package/dist/sync/client.js +118 -0
- package/dist/sync/client.js.map +1 -0
- package/dist/sync/mirror.d.ts +85 -0
- package/dist/sync/mirror.js +389 -0
- package/dist/sync/mirror.js.map +1 -0
- package/dist/sync/protocol.d.ts +67 -0
- package/dist/sync/protocol.js +16 -0
- package/dist/sync/protocol.js.map +1 -0
- package/dist/sync/settings.d.ts +56 -0
- package/dist/sync/settings.js +60 -0
- package/dist/sync/settings.js.map +1 -0
- package/dist/tools/builtin/artifact.d.ts +16 -0
- package/dist/tools/builtin/artifact.js +134 -0
- package/dist/tools/builtin/artifact.js.map +1 -0
- package/dist/tools/builtin/bash.d.ts +3 -0
- package/dist/tools/builtin/bash.js +142 -0
- package/dist/tools/builtin/bash.js.map +1 -0
- package/dist/tools/builtin/fs.d.ts +19 -0
- package/dist/tools/builtin/fs.js +280 -0
- package/dist/tools/builtin/fs.js.map +1 -0
- package/dist/tools/builtin/index.d.ts +22 -0
- package/dist/tools/builtin/index.js +42 -0
- package/dist/tools/builtin/index.js.map +1 -0
- package/dist/tools/builtin/mcp-resource.d.ts +3 -0
- package/dist/tools/builtin/mcp-resource.js +220 -0
- package/dist/tools/builtin/mcp-resource.js.map +1 -0
- package/dist/tools/builtin/memory.d.ts +10 -0
- package/dist/tools/builtin/memory.js +76 -0
- package/dist/tools/builtin/memory.js.map +1 -0
- package/dist/tools/builtin/recall.d.ts +13 -0
- package/dist/tools/builtin/recall.js +163 -0
- package/dist/tools/builtin/recall.js.map +1 -0
- package/dist/tools/builtin/search.d.ts +3 -0
- package/dist/tools/builtin/search.js +240 -0
- package/dist/tools/builtin/search.js.map +1 -0
- package/dist/tools/builtin/skill.d.ts +10 -0
- package/dist/tools/builtin/skill.js +46 -0
- package/dist/tools/builtin/skill.js.map +1 -0
- package/dist/tools/builtin/task.d.ts +9 -0
- package/dist/tools/builtin/task.js +68 -0
- package/dist/tools/builtin/task.js.map +1 -0
- package/dist/tools/builtin/todo.d.ts +20 -0
- package/dist/tools/builtin/todo.js +63 -0
- package/dist/tools/builtin/todo.js.map +1 -0
- package/dist/tools/define.d.ts +19 -0
- package/dist/tools/define.js +39 -0
- package/dist/tools/define.js.map +1 -0
- package/dist/tools/index.d.ts +7 -0
- package/dist/tools/index.js +5 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/registry.d.ts +39 -0
- package/dist/tools/registry.js +101 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/types.d.ts +112 -0
- package/dist/tools/types.js +22 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/util/id.d.ts +9 -0
- package/dist/util/id.js +19 -0
- package/dist/util/id.js.map +1 -0
- package/dist/util/json.d.ts +25 -0
- package/dist/util/json.js +100 -0
- package/dist/util/json.js.map +1 -0
- package/dist/util/jwt.d.ts +74 -0
- package/dist/util/jwt.js +177 -0
- package/dist/util/jwt.js.map +1 -0
- package/dist/util/logger.d.ts +13 -0
- package/dist/util/logger.js +59 -0
- package/dist/util/logger.js.map +1 -0
- package/dist/util/queue.d.ts +26 -0
- package/dist/util/queue.js +106 -0
- package/dist/util/queue.js.map +1 -0
- package/dist/util/schema.d.ts +17 -0
- package/dist/util/schema.js +98 -0
- package/dist/util/schema.js.map +1 -0
- package/dist/util/shell.d.ts +20 -0
- package/dist/util/shell.js +24 -0
- package/dist/util/shell.js.map +1 -0
- package/official.json +4 -0
- package/package.json +38 -0
- package/skills/artifact-authoring/SKILL.md +81 -0
- package/skills/canvas/SKILL.md +85 -0
- package/skills/skill-authoring/SKILL.md +85 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { appendFile, mkdir, open, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { parseJson } from "../util/json.js";
|
|
4
|
+
/**
|
|
5
|
+
* A directory of JSON files, addressed by key.
|
|
6
|
+
*
|
|
7
|
+
* A key is a path: `["session", projectId, sessionId]` is one file. That is the
|
|
8
|
+
* whole abstraction, and it is deliberate — the store has to stay something a
|
|
9
|
+
* person can open in a file browser and read, because a transcript nobody can
|
|
10
|
+
* recover by hand is a transcript that can be lost by a bug in this file.
|
|
11
|
+
*
|
|
12
|
+
* Two shapes live here. Documents (`.json`) are read and written whole; logs
|
|
13
|
+
* (`.jsonl`) are appended a line at a time so a crashed process still leaves
|
|
14
|
+
* everything written before it, and so appending never rewrites what is
|
|
15
|
+
* already there.
|
|
16
|
+
*/
|
|
17
|
+
export class JsonStore {
|
|
18
|
+
root;
|
|
19
|
+
constructor(root) {
|
|
20
|
+
this.root = root;
|
|
21
|
+
}
|
|
22
|
+
/** Path of a document key. */
|
|
23
|
+
file(key) {
|
|
24
|
+
return path.join(this.root, ...key) + ".json";
|
|
25
|
+
}
|
|
26
|
+
/** Path of a log key. */
|
|
27
|
+
log(key) {
|
|
28
|
+
return path.join(this.root, ...key) + ".jsonl";
|
|
29
|
+
}
|
|
30
|
+
async read(key) {
|
|
31
|
+
let raw;
|
|
32
|
+
try {
|
|
33
|
+
raw = await readFile(this.file(key), "utf8");
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
const parsed = parseJson(raw);
|
|
39
|
+
return parsed.ok ? parsed.value : undefined;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Write a document.
|
|
43
|
+
*
|
|
44
|
+
* Temp file plus rename: a process killed mid-write would otherwise leave a
|
|
45
|
+
* truncated file, and a half-written meta.json is a session that has
|
|
46
|
+
* disappeared as far as every reader is concerned.
|
|
47
|
+
*/
|
|
48
|
+
async write(key, value) {
|
|
49
|
+
const target = this.file(key);
|
|
50
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
51
|
+
const temp = `${target}.${process.pid}.${(tempSeq += 1)}.tmp`;
|
|
52
|
+
await writeFile(temp, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
53
|
+
await rename(temp, target);
|
|
54
|
+
}
|
|
55
|
+
async remove(key) {
|
|
56
|
+
await rm(this.file(key), { force: true });
|
|
57
|
+
}
|
|
58
|
+
async removeLog(key) {
|
|
59
|
+
await rm(this.log(key), { force: true });
|
|
60
|
+
}
|
|
61
|
+
/** Every document key under a prefix, depth-first, sorted. */
|
|
62
|
+
async list(prefix) {
|
|
63
|
+
return this.keysUnder(prefix, ".json");
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Every log key under a prefix, depth-first, sorted.
|
|
67
|
+
*
|
|
68
|
+
* The counterpart to `list`, and it exists for the same reason: something
|
|
69
|
+
* has to be able to answer "which sessions wrote artifacts" without opening
|
|
70
|
+
* a session to ask it. A readdir answers that; reading the logs to find out
|
|
71
|
+
* would be reading everything on the machine to decide what to read.
|
|
72
|
+
*/
|
|
73
|
+
async listLogs(prefix) {
|
|
74
|
+
return this.keysUnder(prefix, ".jsonl");
|
|
75
|
+
}
|
|
76
|
+
async keysUnder(prefix, ext) {
|
|
77
|
+
const base = path.join(this.root, ...prefix);
|
|
78
|
+
const out = [];
|
|
79
|
+
const walk = async (dir, trail) => {
|
|
80
|
+
let entries;
|
|
81
|
+
try {
|
|
82
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
for (const entry of entries) {
|
|
88
|
+
if (entry.isDirectory()) {
|
|
89
|
+
await walk(path.join(dir, entry.name), [...trail, entry.name]);
|
|
90
|
+
}
|
|
91
|
+
else if (entry.name.endsWith(ext)) {
|
|
92
|
+
out.push([...prefix, ...trail, entry.name.slice(0, -ext.length)]);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
await walk(base, []);
|
|
97
|
+
return out.sort((a, b) => a.join("/").localeCompare(b.join("/")));
|
|
98
|
+
}
|
|
99
|
+
async append(key, lines) {
|
|
100
|
+
if (lines.length === 0)
|
|
101
|
+
return;
|
|
102
|
+
const target = this.log(key);
|
|
103
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
104
|
+
await appendFile(target, lines.map((line) => JSON.stringify(line)).join("\n") + "\n", "utf8");
|
|
105
|
+
}
|
|
106
|
+
/** Rewrite a log wholesale. Only compaction does this; appends are the norm. */
|
|
107
|
+
async replaceLog(key, lines) {
|
|
108
|
+
const target = this.log(key);
|
|
109
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
110
|
+
const body = lines.map((line) => JSON.stringify(line)).join("\n");
|
|
111
|
+
await writeFile(target, body === "" ? "" : `${body}\n`, "utf8");
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Read a log.
|
|
115
|
+
*
|
|
116
|
+
* Corrupt lines are skipped rather than failing the read: a torn final line
|
|
117
|
+
* from a killed process should cost that one line, not the transcript.
|
|
118
|
+
* `maxBytes` reads the tail instead of the whole file, for logs that grow
|
|
119
|
+
* without bound.
|
|
120
|
+
*/
|
|
121
|
+
async readLog(key, maxBytes) {
|
|
122
|
+
const target = this.log(key);
|
|
123
|
+
let raw;
|
|
124
|
+
if (maxBytes === undefined) {
|
|
125
|
+
try {
|
|
126
|
+
raw = await readFile(target, "utf8");
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
let handle;
|
|
134
|
+
try {
|
|
135
|
+
handle = await open(target, "r");
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
const { size } = await handle.stat();
|
|
142
|
+
const start = size > maxBytes ? size - maxBytes : 0;
|
|
143
|
+
const buffer = Buffer.alloc(Math.min(size, maxBytes));
|
|
144
|
+
await handle.read(buffer, 0, buffer.length, start);
|
|
145
|
+
raw = buffer.toString("utf8");
|
|
146
|
+
// Starting mid-file lands mid-line; drop the fragment rather than
|
|
147
|
+
// letting it fail to parse and look like corruption.
|
|
148
|
+
if (start > 0)
|
|
149
|
+
raw = raw.slice(raw.indexOf("\n") + 1);
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
await handle.close();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const out = [];
|
|
156
|
+
for (const line of raw.split("\n")) {
|
|
157
|
+
if (line.trim() === "")
|
|
158
|
+
continue;
|
|
159
|
+
const parsed = parseJson(line);
|
|
160
|
+
if (parsed.ok)
|
|
161
|
+
out.push(parsed.value);
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* A unique name per in-flight write.
|
|
168
|
+
*
|
|
169
|
+
* `file.<pid>.tmp` collides when one process writes twice at once: the first
|
|
170
|
+
* rename moves the shared temp file away and the second fails with ENOENT.
|
|
171
|
+
*/
|
|
172
|
+
let tempSeq = 0;
|
|
173
|
+
//# sourceMappingURL=kv.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kv.js","sourceRoot":"","sources":["../../src/storage/kv.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { JsonStore } from "./kv.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Layout changes, applied in order and recorded by index.
|
|
4
|
+
*
|
|
5
|
+
* A marker file holds how many have run. A step that throws stops the run
|
|
6
|
+
* without advancing the marker, so the next start retries it rather than
|
|
7
|
+
* skipping ahead and leaving the store half-converted.
|
|
8
|
+
*/
|
|
9
|
+
export type Migration = (store: JsonStore, legacyRoot?: string) => Promise<void>;
|
|
10
|
+
export declare const MIGRATIONS: Migration[];
|
|
11
|
+
export declare function migrate(store: JsonStore, legacyRoot?: string): Promise<void>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { copyFile, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { parseJson } from "../util/json.js";
|
|
4
|
+
import { projectId } from "./project.js";
|
|
5
|
+
import { asRecord } from "./records.js";
|
|
6
|
+
export const MIGRATIONS = [importFlatSessions];
|
|
7
|
+
export async function migrate(store, legacyRoot) {
|
|
8
|
+
const marker = path.join(store.root, "migration");
|
|
9
|
+
let done = 0;
|
|
10
|
+
try {
|
|
11
|
+
done = Number.parseInt(await readFile(marker, "utf8"), 10) || 0;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
done = 0;
|
|
15
|
+
}
|
|
16
|
+
if (done >= MIGRATIONS.length)
|
|
17
|
+
return;
|
|
18
|
+
for (let index = done; index < MIGRATIONS.length; index++) {
|
|
19
|
+
await MIGRATIONS[index](store, legacyRoot);
|
|
20
|
+
await mkdir(store.root, { recursive: true });
|
|
21
|
+
await writeFile(marker, String(index + 1), "utf8");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Import `sessions/<id>/{meta,messages,archive}` into the keyed layout.
|
|
26
|
+
*
|
|
27
|
+
* The old tree is left where it is. Deleting a user's entire transcript
|
|
28
|
+
* history on the strength of a conversion that has not yet been read back once
|
|
29
|
+
* is not a trade worth making; the copy is cheap and the original stays until
|
|
30
|
+
* someone chooses to remove it.
|
|
31
|
+
*/
|
|
32
|
+
async function importFlatSessions(store, legacyRoot) {
|
|
33
|
+
if (!legacyRoot)
|
|
34
|
+
return;
|
|
35
|
+
let entries;
|
|
36
|
+
try {
|
|
37
|
+
entries = await readdir(legacyRoot);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const projects = new Map();
|
|
43
|
+
for (const id of entries) {
|
|
44
|
+
const metaRaw = await readFile(path.join(legacyRoot, id, "meta.json"), "utf8").catch(() => undefined);
|
|
45
|
+
if (!metaRaw)
|
|
46
|
+
continue;
|
|
47
|
+
const parsed = parseJson(metaRaw);
|
|
48
|
+
if (!parsed.ok)
|
|
49
|
+
continue;
|
|
50
|
+
const meta = parsed.value;
|
|
51
|
+
const cwd = typeof meta["cwd"] === "string" ? meta["cwd"] : "";
|
|
52
|
+
const pid = await projectId(cwd || legacyRoot);
|
|
53
|
+
// Already imported: a rerun after a failure part-way should not rewrite
|
|
54
|
+
// sessions that have moved on since.
|
|
55
|
+
if (await store.read(["session", pid, id]))
|
|
56
|
+
continue;
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
const known = projects.get(pid);
|
|
59
|
+
projects.set(pid, {
|
|
60
|
+
id: pid,
|
|
61
|
+
vcs: pid.startsWith("dir-") ? "none" : "git",
|
|
62
|
+
worktree: cwd,
|
|
63
|
+
createdAt: known?.createdAt ?? now,
|
|
64
|
+
updatedAt: now,
|
|
65
|
+
});
|
|
66
|
+
const raw = await readFile(path.join(legacyRoot, id, "messages.jsonl"), "utf8").catch(() => "");
|
|
67
|
+
const records = raw
|
|
68
|
+
.split("\n")
|
|
69
|
+
.filter((line) => line.trim() !== "")
|
|
70
|
+
.map((line, index) => {
|
|
71
|
+
const line_ = parseJson(line);
|
|
72
|
+
return line_.ok ? asRecord(line_.value, index) : undefined;
|
|
73
|
+
})
|
|
74
|
+
.filter((record) => Boolean(record));
|
|
75
|
+
await store.replaceLog(["message", id], records);
|
|
76
|
+
await store.write(["session", pid, id], {
|
|
77
|
+
...meta,
|
|
78
|
+
projectId: pid,
|
|
79
|
+
gen: 0,
|
|
80
|
+
seq: records.length,
|
|
81
|
+
});
|
|
82
|
+
const archive = path.join(legacyRoot, id, "archive.jsonl");
|
|
83
|
+
const target = store.log(["archive", id]);
|
|
84
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
85
|
+
await copyFile(archive, target).catch(() => { });
|
|
86
|
+
}
|
|
87
|
+
for (const record of projects.values()) {
|
|
88
|
+
if (await store.read(["project", record.id]))
|
|
89
|
+
continue;
|
|
90
|
+
await store.write(["project", record.id], record);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=migrate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrate.js","sourceRoot":"","sources":["../../src/storage/migrate.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function projectId(cwd: string): Promise<string>;
|
|
2
|
+
export interface ProjectRecord {
|
|
3
|
+
id: string;
|
|
4
|
+
vcs: "git" | "none";
|
|
5
|
+
/** Where it was last seen. A hint for humans reading the store, not identity. */
|
|
6
|
+
worktree: string;
|
|
7
|
+
createdAt: number;
|
|
8
|
+
updatedAt: number;
|
|
9
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { realpath } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
/**
|
|
6
|
+
* The identity of a project, as something other than its path.
|
|
7
|
+
*
|
|
8
|
+
* Sessions have to belong to something stable. A working directory is not:
|
|
9
|
+
* clones land in different places, worktrees multiply, and a renamed folder
|
|
10
|
+
* would orphan every transcript recorded under the old name — which is exactly
|
|
11
|
+
* how a session list "loses" work that is still on disk.
|
|
12
|
+
*
|
|
13
|
+
* So a git repository is identified by its root commit, which every clone and
|
|
14
|
+
* every worktree of that repository shares and which nothing can rename.
|
|
15
|
+
* Anything else falls back to a hash of the resolved path, which at least
|
|
16
|
+
* survives symlinks and trailing slashes.
|
|
17
|
+
*/
|
|
18
|
+
const cache = new Map();
|
|
19
|
+
export function projectId(cwd) {
|
|
20
|
+
const key = path.resolve(cwd);
|
|
21
|
+
let pending = cache.get(key);
|
|
22
|
+
if (!pending) {
|
|
23
|
+
pending = resolve(key);
|
|
24
|
+
cache.set(key, pending);
|
|
25
|
+
}
|
|
26
|
+
return pending;
|
|
27
|
+
}
|
|
28
|
+
async function resolve(cwd) {
|
|
29
|
+
const root = await rootCommit(cwd);
|
|
30
|
+
if (root)
|
|
31
|
+
return root;
|
|
32
|
+
const real = await realpath(cwd).catch(() => cwd);
|
|
33
|
+
return `dir-${createHash("sha256").update(real).digest("hex").slice(0, 16)}`;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The repository's first commit.
|
|
37
|
+
*
|
|
38
|
+
* `--all` rather than HEAD: a detached head or an unborn branch should still
|
|
39
|
+
* identify the repository. Several roots is possible (grafted or merged
|
|
40
|
+
* histories), so they are sorted and the first is taken — any rule works as
|
|
41
|
+
* long as it is the same rule everywhere.
|
|
42
|
+
*/
|
|
43
|
+
function rootCommit(cwd) {
|
|
44
|
+
return new Promise((done) => {
|
|
45
|
+
execFile("git", ["rev-list", "--max-parents=0", "--all"], { cwd, timeout: 3000, windowsHide: true }, (err, stdout) => {
|
|
46
|
+
if (err)
|
|
47
|
+
return done(undefined);
|
|
48
|
+
const ids = String(stdout)
|
|
49
|
+
.split("\n")
|
|
50
|
+
.map((line) => line.trim())
|
|
51
|
+
.filter(Boolean)
|
|
52
|
+
.sort();
|
|
53
|
+
done(ids[0]);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=project.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project.js","sourceRoot":"","sources":["../../src/storage/project.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B;;;;;;;;;;;;GAYG;AAEH,MAAM,KAAK,GAAG,IAAI,GAAG,EAA2B,CAAC;AAEjD,MAAM,UAAU,SAAS,CAAC,GAAW;IACnC,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC9B,IAAI,OAAO,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC7B,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;QACvB,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IAC1B,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,KAAK,UAAU,OAAO,CAAC,GAAW;IAChC,MAAM,IAAI,GAAG,MAAM,UAAU,CAAC,GAAG,CAAC,CAAC;IACnC,IAAI,IAAI;QAAE,OAAO,IAAI,CAAC;IACtB,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC;IAClD,OAAO,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;AAC/E,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,UAAU,CAAC,GAAW;IAC7B,OAAO,IAAI,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE;QAC1B,QAAQ,CACN,KAAK,EACL,CAAC,UAAU,EAAE,iBAAiB,EAAE,OAAO,CAAC,EACxC,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,EACzC,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE;YACd,IAAI,GAAG;gBAAE,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC;YAChC,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC;iBACvB,KAAK,CAAC,IAAI,CAAC;iBACX,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;iBAC1B,MAAM,CAAC,OAAO,CAAC;iBACf,IAAI,EAAE,CAAC;YACV,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QACf,CAAC,CACF,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Message } from "../core/types.ts";
|
|
2
|
+
/**
|
|
3
|
+
* One line of a transcript, as stored.
|
|
4
|
+
*
|
|
5
|
+
* The message alone was enough while a session only ever existed on the
|
|
6
|
+
* machine that wrote it. It stops being enough the moment the same session can
|
|
7
|
+
* be continued from a second device: a merge needs to know which lines are the
|
|
8
|
+
* same line (`id`), what order they go in (`seq`), and who to believe when two
|
|
9
|
+
* devices choose the same `seq` (`device`).
|
|
10
|
+
*
|
|
11
|
+
* `seq` is a Lamport counter, not a timestamp — wall clocks disagree between
|
|
12
|
+
* machines, and a transcript reordered by a skewed clock is a transcript that
|
|
13
|
+
* reads as nonsense.
|
|
14
|
+
*/
|
|
15
|
+
export interface MessageRecord {
|
|
16
|
+
id: string;
|
|
17
|
+
seq: number;
|
|
18
|
+
device: string;
|
|
19
|
+
at: number;
|
|
20
|
+
message: Message;
|
|
21
|
+
}
|
|
22
|
+
/** Records ordered as a transcript, with duplicates from a merge removed. */
|
|
23
|
+
export declare function orderRecords(records: MessageRecord[]): MessageRecord[];
|
|
24
|
+
export declare function messageId(): string;
|
|
25
|
+
/**
|
|
26
|
+
* Read a log line written before records existed.
|
|
27
|
+
*
|
|
28
|
+
* Old transcripts are bare messages, one per line. They are still readable
|
|
29
|
+
* transcripts, and refusing to load them would throw away history to gain
|
|
30
|
+
* nothing.
|
|
31
|
+
*/
|
|
32
|
+
export declare function asRecord(value: unknown, index: number): MessageRecord | undefined;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { shortId } from "../util/id.js";
|
|
2
|
+
/** Records ordered as a transcript, with duplicates from a merge removed. */
|
|
3
|
+
export function orderRecords(records) {
|
|
4
|
+
const byId = new Map();
|
|
5
|
+
for (const record of records) {
|
|
6
|
+
const seen = byId.get(record.id);
|
|
7
|
+
// A record that arrives twice is the same line; keep the earlier claim so
|
|
8
|
+
// replay is stable rather than dependent on which copy was read last.
|
|
9
|
+
if (!seen || record.seq < seen.seq)
|
|
10
|
+
byId.set(record.id, record);
|
|
11
|
+
}
|
|
12
|
+
return [...byId.values()].sort((a, b) => a.seq - b.seq || a.device.localeCompare(b.device) || a.id.localeCompare(b.id));
|
|
13
|
+
}
|
|
14
|
+
export function messageId() {
|
|
15
|
+
return `msg_${shortId(8)}`;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Read a log line written before records existed.
|
|
19
|
+
*
|
|
20
|
+
* Old transcripts are bare messages, one per line. They are still readable
|
|
21
|
+
* transcripts, and refusing to load them would throw away history to gain
|
|
22
|
+
* nothing.
|
|
23
|
+
*/
|
|
24
|
+
export function asRecord(value, index) {
|
|
25
|
+
if (!value || typeof value !== "object")
|
|
26
|
+
return undefined;
|
|
27
|
+
const candidate = value;
|
|
28
|
+
if (candidate.message && typeof candidate.id === "string") {
|
|
29
|
+
return {
|
|
30
|
+
id: candidate.id,
|
|
31
|
+
seq: typeof candidate.seq === "number" ? candidate.seq : index,
|
|
32
|
+
device: typeof candidate.device === "string" ? candidate.device : "unknown",
|
|
33
|
+
at: typeof candidate.at === "number" ? candidate.at : 0,
|
|
34
|
+
message: candidate.message,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (typeof candidate.role === "string") {
|
|
38
|
+
return {
|
|
39
|
+
id: `legacy_${index}`,
|
|
40
|
+
seq: index,
|
|
41
|
+
device: "legacy",
|
|
42
|
+
at: 0,
|
|
43
|
+
message: value,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=records.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"records.js","sourceRoot":"","sources":["../../src/storage/records.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAuBxC,6EAA6E;AAC7E,MAAM,UAAU,YAAY,CAAC,OAAwB;IACnD,MAAM,IAAI,GAAG,IAAI,GAAG,EAAyB,CAAC;IAC9C,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACjC,0EAA0E;QAC1E,sEAAsE;QACtE,IAAI,CAAC,IAAI,IAAI,MAAM,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG;YAAE,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;IAClE,CAAC;IACD,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAC5B,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC,CACxF,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,SAAS;IACvB,OAAO,OAAO,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;AAC7B,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,QAAQ,CAAC,KAAc,EAAE,KAAa;IACpD,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IAC1D,MAAM,SAAS,GAAG,KAAmD,CAAC;IACtE,IAAI,SAAS,CAAC,OAAO,IAAI,OAAO,SAAS,CAAC,EAAE,KAAK,QAAQ,EAAE,CAAC;QAC1D,OAAO;YACL,EAAE,EAAE,SAAS,CAAC,EAAE;YAChB,GAAG,EAAE,OAAO,SAAS,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK;YAC9D,MAAM,EAAE,OAAO,SAAS,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS;YAC3E,EAAE,EAAE,OAAO,SAAS,CAAC,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YACvD,OAAO,EAAE,SAAS,CAAC,OAAO;SAC3B,CAAC;IACJ,CAAC;IACD,IAAI,OAAO,SAAS,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QACvC,OAAO;YACL,EAAE,EAAE,UAAU,KAAK,EAAE;YACrB,GAAG,EAAE,KAAK;YACV,MAAM,EAAE,QAAQ;YAChB,EAAE,EAAE,CAAC;YACL,OAAO,EAAE,KAAgB;SAC1B,CAAC;IACJ,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { PullResponse, PushResponse, WireRecord, WireSession } from "./protocol.ts";
|
|
2
|
+
/**
|
|
3
|
+
* The client half of sync.
|
|
4
|
+
*
|
|
5
|
+
* Speaks the three endpoints in `protocol.ts` and nothing more. It holds no
|
|
6
|
+
* state: what has been sent and how far a feed has been read are the mirror's
|
|
7
|
+
* business, because those answers have to survive a restart and a socket
|
|
8
|
+
* cannot.
|
|
9
|
+
*
|
|
10
|
+
* Every method throws on failure rather than swallowing. Sync is best-effort
|
|
11
|
+
* *as a policy*, but that policy belongs to the caller — a client that hides
|
|
12
|
+
* a 401 leaves the user with a feature that silently does nothing, which is
|
|
13
|
+
* the worst of both.
|
|
14
|
+
*/
|
|
15
|
+
export interface SyncClientOptions {
|
|
16
|
+
/** Base URL of the sync server, e.g. `https://sync.example.com`. */
|
|
17
|
+
url: string;
|
|
18
|
+
/**
|
|
19
|
+
* Bearer token, or a function returning the current one.
|
|
20
|
+
*
|
|
21
|
+
* A function for anything that expires. An account token lasts an hour and
|
|
22
|
+
* is refreshed underneath, so a client holding the string it was built with
|
|
23
|
+
* starts failing an hour after sign-in — and since a mirror stops itself on
|
|
24
|
+
* a rejected token rather than retrying a dead credential, that failure is
|
|
25
|
+
* silent and permanent. Read it per request and a refresh is picked up with
|
|
26
|
+
* no one having to notice.
|
|
27
|
+
*
|
|
28
|
+
* A self-hosted server's token does not expire, so a plain string stays
|
|
29
|
+
* correct there.
|
|
30
|
+
*/
|
|
31
|
+
token: string | (() => string | Promise<string>);
|
|
32
|
+
/**
|
|
33
|
+
* Sync into a team's feed rather than your own.
|
|
34
|
+
*
|
|
35
|
+
* A property of the client because it is a property of the destination, not
|
|
36
|
+
* of any one call: a mirror that pushed to a team and pulled from a personal
|
|
37
|
+
* feed would appear to work and would lose every teammate's turn.
|
|
38
|
+
*/
|
|
39
|
+
team?: string;
|
|
40
|
+
/** Overridable so tests can drive the client without a socket. */
|
|
41
|
+
fetch?: typeof globalThis.fetch;
|
|
42
|
+
}
|
|
43
|
+
export declare class SyncClient {
|
|
44
|
+
private readonly base;
|
|
45
|
+
private readonly token;
|
|
46
|
+
private readonly team;
|
|
47
|
+
private readonly http;
|
|
48
|
+
constructor(opts: SyncClientOptions);
|
|
49
|
+
/** Projects this account has a feed for, so a new device knows what to pull. */
|
|
50
|
+
projects(signal?: AbortSignal): Promise<string[]>;
|
|
51
|
+
/**
|
|
52
|
+
* Hand records to the server.
|
|
53
|
+
*
|
|
54
|
+
* Batches are capped by the protocol and the server refuses an oversized one
|
|
55
|
+
* outright rather than truncating, so splitting is the caller's job — see
|
|
56
|
+
* `chunk`. Retrying a push is free: the server dedupes on record id.
|
|
57
|
+
*/
|
|
58
|
+
push(project: string, records: WireRecord[], sessions: WireSession[], signal?: AbortSignal): Promise<PushResponse>;
|
|
59
|
+
/**
|
|
60
|
+
* Read forward from a cursor.
|
|
61
|
+
*
|
|
62
|
+
* `wait` holds the connection open until something arrives, which is how a
|
|
63
|
+
* second device sees a turn while it is still being generated instead of on
|
|
64
|
+
* the next timer tick. Zero polls once and returns.
|
|
65
|
+
*/
|
|
66
|
+
pull(project: string, since: number, opts?: {
|
|
67
|
+
limit?: number;
|
|
68
|
+
wait?: number;
|
|
69
|
+
signal?: AbortSignal;
|
|
70
|
+
}): Promise<PullResponse>;
|
|
71
|
+
private request;
|
|
72
|
+
}
|
|
73
|
+
/** Carries the status so the caller can tell "bad token" from "server down". */
|
|
74
|
+
export declare class SyncHttpError extends Error {
|
|
75
|
+
readonly status: number;
|
|
76
|
+
constructor(status: number, message: string);
|
|
77
|
+
}
|
|
78
|
+
/** Split a list into protocol-sized batches. */
|
|
79
|
+
export declare function chunk<T>(items: T[], size?: number): T[][];
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { errorMessage } from "../core/errors.js";
|
|
2
|
+
import { parseJson } from "../util/json.js";
|
|
3
|
+
import { DEFAULT_PULL_LIMIT, MAX_PUSH_RECORDS, MAX_WAIT_SECONDS } from "./protocol.js";
|
|
4
|
+
export class SyncClient {
|
|
5
|
+
base;
|
|
6
|
+
token;
|
|
7
|
+
team;
|
|
8
|
+
http;
|
|
9
|
+
constructor(opts) {
|
|
10
|
+
this.base = opts.url.replace(/\/+$/, "");
|
|
11
|
+
this.token = typeof opts.token === "function" ? opts.token : () => opts.token;
|
|
12
|
+
this.team = opts.team?.trim() || undefined;
|
|
13
|
+
this.http = opts.fetch ?? globalThis.fetch;
|
|
14
|
+
}
|
|
15
|
+
/** Projects this account has a feed for, so a new device knows what to pull. */
|
|
16
|
+
async projects(signal) {
|
|
17
|
+
const query = this.team ? `?team=${encodeURIComponent(this.team)}` : "";
|
|
18
|
+
const body = await this.request("GET", `/v1/projects${query}`, {
|
|
19
|
+
signal,
|
|
20
|
+
});
|
|
21
|
+
return Array.isArray(body.projects) ? body.projects : [];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Hand records to the server.
|
|
25
|
+
*
|
|
26
|
+
* Batches are capped by the protocol and the server refuses an oversized one
|
|
27
|
+
* outright rather than truncating, so splitting is the caller's job — see
|
|
28
|
+
* `chunk`. Retrying a push is free: the server dedupes on record id.
|
|
29
|
+
*/
|
|
30
|
+
async push(project, records, sessions, signal) {
|
|
31
|
+
if (records.length > MAX_PUSH_RECORDS || sessions.length > MAX_PUSH_RECORDS) {
|
|
32
|
+
throw new Error(`push exceeds ${MAX_PUSH_RECORDS} entries; split it first`);
|
|
33
|
+
}
|
|
34
|
+
return this.request("POST", "/v1/push", {
|
|
35
|
+
body: { project, records, sessions, ...(this.team ? { team: this.team } : {}) },
|
|
36
|
+
signal,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Read forward from a cursor.
|
|
41
|
+
*
|
|
42
|
+
* `wait` holds the connection open until something arrives, which is how a
|
|
43
|
+
* second device sees a turn while it is still being generated instead of on
|
|
44
|
+
* the next timer tick. Zero polls once and returns.
|
|
45
|
+
*/
|
|
46
|
+
async pull(project, since, opts = {}) {
|
|
47
|
+
const query = new URLSearchParams({ project, since: String(since) });
|
|
48
|
+
if (this.team)
|
|
49
|
+
query.set("team", this.team);
|
|
50
|
+
query.set("limit", String(Math.min(opts.limit ?? DEFAULT_PULL_LIMIT, DEFAULT_PULL_LIMIT)));
|
|
51
|
+
if (opts.wait)
|
|
52
|
+
query.set("wait", String(Math.min(opts.wait, MAX_WAIT_SECONDS)));
|
|
53
|
+
const body = await this.request("GET", `/v1/pull?${query}`, {
|
|
54
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
55
|
+
});
|
|
56
|
+
return {
|
|
57
|
+
cursor: typeof body.cursor === "number" ? body.cursor : since,
|
|
58
|
+
records: Array.isArray(body.records) ? body.records : [],
|
|
59
|
+
sessions: Array.isArray(body.sessions) ? body.sessions : [],
|
|
60
|
+
more: body.more === true,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
async request(method, path, opts = {}) {
|
|
64
|
+
const token = await this.token();
|
|
65
|
+
let response;
|
|
66
|
+
try {
|
|
67
|
+
response = await this.http(`${this.base}${path}`, {
|
|
68
|
+
method,
|
|
69
|
+
headers: {
|
|
70
|
+
authorization: `Bearer ${token}`,
|
|
71
|
+
...(opts.body === undefined ? {} : { "content-type": "application/json" }),
|
|
72
|
+
},
|
|
73
|
+
...(opts.body === undefined ? {} : { body: JSON.stringify(opts.body) }),
|
|
74
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
// Rethrown untouched when the caller aborted. Wrapping it loses the
|
|
79
|
+
// `AbortError` name, and the name is the only thing that distinguishes
|
|
80
|
+
// "we cancelled this" from "the network failed" — a caller that cannot
|
|
81
|
+
// tell them apart reports its own shutdown as an error.
|
|
82
|
+
if (err instanceof Error && err.name === "AbortError")
|
|
83
|
+
throw err;
|
|
84
|
+
throw new Error(`sync ${method} ${path} failed: ${errorMessage(err)}`);
|
|
85
|
+
}
|
|
86
|
+
const text = await response.text().catch(() => "");
|
|
87
|
+
if (!response.ok) {
|
|
88
|
+
// The server answers errors as `{error}`; fall back to the status when
|
|
89
|
+
// something else is on the other end (a proxy, a captive portal).
|
|
90
|
+
const parsed = parseJson(text);
|
|
91
|
+
const detail = parsed.ok && parsed.value.error ? parsed.value.error : response.statusText;
|
|
92
|
+
throw new SyncHttpError(response.status, `sync ${method} ${path}: ${detail}`);
|
|
93
|
+
}
|
|
94
|
+
const parsed = parseJson(text);
|
|
95
|
+
if (!parsed.ok)
|
|
96
|
+
throw new Error(`sync ${method} ${path}: response was not JSON`);
|
|
97
|
+
return parsed.value;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** Carries the status so the caller can tell "bad token" from "server down". */
|
|
101
|
+
export class SyncHttpError extends Error {
|
|
102
|
+
status;
|
|
103
|
+
constructor(status, message) {
|
|
104
|
+
super(message);
|
|
105
|
+
this.name = "SyncHttpError";
|
|
106
|
+
this.status = status;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** Split a list into protocol-sized batches. */
|
|
110
|
+
export function chunk(items, size = MAX_PUSH_RECORDS) {
|
|
111
|
+
if (items.length <= size)
|
|
112
|
+
return items.length ? [items] : [];
|
|
113
|
+
const out = [];
|
|
114
|
+
for (let at = 0; at < items.length; at += size)
|
|
115
|
+
out.push(items.slice(at, at + size));
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=client.js.map
|