pinomad 0.3.0 → 0.5.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/README.md +4 -2
- package/dist/{chunk-3tf23vjq.js → chunk-1d9sppj0.js} +1 -1
- package/dist/chunk-26d04z02.js +126 -0
- package/dist/chunk-4by4x5c4.js +1376 -0
- package/dist/{chunk-kxws8b83.js → chunk-5ng86fmy.js} +4 -4
- package/dist/chunk-9d59jjmt.js +144 -0
- package/dist/{chunk-t9y2wfk5.js → chunk-aqj4wg33.js} +98 -6
- package/dist/{chunk-n60kkf1m.js → chunk-crv3dqzp.js} +4 -4
- package/dist/{chunk-tfqc1djy.js → chunk-d4wdt5qs.js} +976 -306
- package/dist/chunk-ebfaf3k2.js +190 -0
- package/dist/chunk-f0v3sht0.js +136 -0
- package/dist/{chunk-3qn8gbjb.js → chunk-gqr7ahv4.js} +1 -1
- package/dist/{chunk-c155p0t0.js → chunk-hsp12cjt.js} +111 -8
- package/dist/chunk-kmbh44dt.js +704 -0
- package/dist/{chunk-bggesvnj.js → chunk-m4q9wafw.js} +28 -92
- package/dist/{chunk-syf0z68f.js → chunk-zttf1hvh.js} +2 -2
- package/dist/pinomad.js +16 -11
- package/package.json +1 -1
- package/web/apple-touch-icon.png +0 -0
- package/web/assets/index-BkF0-im5.css +1 -0
- package/web/assets/index-D4lgm3Y4.js +383 -0
- package/web/favicon.svg +1 -0
- package/web/index.html +4 -2
- package/dist/chunk-4dj2vb8t.js +0 -284
- package/dist/chunk-7fqn69ys.js +0 -33
- package/dist/chunk-fxaewke2.js +0 -113
- package/dist/chunk-z4x88s0q.js +0 -347
- package/web/assets/index-CBXdQLua.js +0 -383
- package/web/assets/index-CG33GBxM.css +0 -1
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import {
|
|
2
|
+
fromBase64Url,
|
|
3
|
+
generateKeyPair,
|
|
4
|
+
keyPairFromPrivate,
|
|
5
|
+
toBase64Url
|
|
6
|
+
} from "./chunk-kmbh44dt.js";
|
|
7
|
+
|
|
8
|
+
// apps/host/src/devices.ts
|
|
9
|
+
import { link, readFile, unlink, writeFile } from "node:fs/promises";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
12
|
+
import { ed25519 } from "@noble/curves/ed25519.js";
|
|
13
|
+
import { defineDoc } from "@earendil-works/pi-durable";
|
|
14
|
+
var DevicesDoc = defineDoc({
|
|
15
|
+
kind: "pinomad.devices",
|
|
16
|
+
version: 1,
|
|
17
|
+
scope: "session",
|
|
18
|
+
initial: () => ({ devices: [] })
|
|
19
|
+
});
|
|
20
|
+
async function ensureDevices(harness, context) {
|
|
21
|
+
await harness.commit(async (tx) => void await tx.doc(DevicesDoc), context);
|
|
22
|
+
}
|
|
23
|
+
var RevocationsDoc = defineDoc({
|
|
24
|
+
kind: "pinomad.revocations",
|
|
25
|
+
version: 1,
|
|
26
|
+
scope: "session",
|
|
27
|
+
initial: () => ({ revoked: [] })
|
|
28
|
+
});
|
|
29
|
+
async function admitDevice(harness, input, context) {
|
|
30
|
+
return await harness.commit(async (tx) => {
|
|
31
|
+
const devices = await tx.doc(DevicesDoc);
|
|
32
|
+
if (devices.devices.some((entry2) => entry2.publicKey === input.publicKey)) {
|
|
33
|
+
return { outcome: "registered" };
|
|
34
|
+
}
|
|
35
|
+
if (input.pair === undefined)
|
|
36
|
+
return { outcome: "refused" };
|
|
37
|
+
const offer = input.offers.consume(input.pair.secret);
|
|
38
|
+
if (offer === undefined)
|
|
39
|
+
return { outcome: "refused" };
|
|
40
|
+
const revocations = await tx.doc(RevocationsDoc);
|
|
41
|
+
if (offer.source === "account") {
|
|
42
|
+
const binding = await input.serverBinding();
|
|
43
|
+
if (revocations.revoked.includes(input.publicKey) || binding === undefined || offer.binding !== binding || !input.acceptAccountDevices()) {
|
|
44
|
+
return { outcome: "refused" };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const name = input.pair.name.trim().slice(0, 64) || "Unknown device";
|
|
48
|
+
const entry = { publicKey: input.publicKey, name, pairedAt: Date.now(), source: offer.source };
|
|
49
|
+
devices.devices.push(entry);
|
|
50
|
+
if (revocations.revoked.includes(input.publicKey)) {
|
|
51
|
+
revocations.revoked = revocations.revoked.filter((key) => key !== input.publicKey);
|
|
52
|
+
}
|
|
53
|
+
return { outcome: "admitted", entry };
|
|
54
|
+
}, context);
|
|
55
|
+
}
|
|
56
|
+
async function loadHostKey(dataDir) {
|
|
57
|
+
const path = join(dataDir, "host-key");
|
|
58
|
+
try {
|
|
59
|
+
const text = (await readFile(path, "utf8")).trim();
|
|
60
|
+
return keyPairFromPrivate(fromBase64Url(text));
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (error.code !== "ENOENT")
|
|
63
|
+
throw error;
|
|
64
|
+
const pair = generateKeyPair();
|
|
65
|
+
if (!await createKeyFile(path, toBase64Url(pair.privateKey)))
|
|
66
|
+
return loadHostKey(dataDir);
|
|
67
|
+
return pair;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function loadRelayKey(dataDir) {
|
|
71
|
+
const path = join(dataDir, "relay-key");
|
|
72
|
+
try {
|
|
73
|
+
const secretKey = fromBase64Url((await readFile(path, "utf8")).trim());
|
|
74
|
+
if (secretKey.length !== 32)
|
|
75
|
+
throw new Error(`bad relay key in ${path}`);
|
|
76
|
+
return { secretKey, publicKey: ed25519.getPublicKey(secretKey) };
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (error.code !== "ENOENT")
|
|
79
|
+
throw error;
|
|
80
|
+
const { secretKey, publicKey } = ed25519.keygen();
|
|
81
|
+
if (!await createKeyFile(path, toBase64Url(secretKey)))
|
|
82
|
+
return loadRelayKey(dataDir);
|
|
83
|
+
return { secretKey, publicKey };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async function createKeyFile(path, key) {
|
|
87
|
+
const tmp = `${path}.${randomBytes(8).toString("hex")}.tmp`;
|
|
88
|
+
try {
|
|
89
|
+
await writeFile(tmp, `${key}
|
|
90
|
+
`, { flag: "wx", mode: 384 });
|
|
91
|
+
await link(tmp, path);
|
|
92
|
+
return true;
|
|
93
|
+
} catch (race) {
|
|
94
|
+
if (race.code !== "EEXIST")
|
|
95
|
+
throw race;
|
|
96
|
+
return false;
|
|
97
|
+
} finally {
|
|
98
|
+
await unlink(tmp).catch(() => {});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function pairingOffers(ttlMs = 5 * 60 * 1000) {
|
|
102
|
+
const live = new Map;
|
|
103
|
+
const prune = () => {
|
|
104
|
+
const now = Date.now();
|
|
105
|
+
for (const [secret, offer] of live)
|
|
106
|
+
if (offer.expiresAt <= now)
|
|
107
|
+
live.delete(secret);
|
|
108
|
+
};
|
|
109
|
+
const mint = (offer) => {
|
|
110
|
+
prune();
|
|
111
|
+
const secret = toBase64Url(randomBytes(16));
|
|
112
|
+
const expiresAt = Date.now() + ttlMs;
|
|
113
|
+
live.set(secret, { expiresAt, ...offer });
|
|
114
|
+
return { secret, expiresAt };
|
|
115
|
+
};
|
|
116
|
+
return {
|
|
117
|
+
create: (source) => mint({ source }),
|
|
118
|
+
createAccount: (binding) => mint({ source: "account", binding }),
|
|
119
|
+
consume(secret) {
|
|
120
|
+
prune();
|
|
121
|
+
const candidate = Buffer.from(secret);
|
|
122
|
+
for (const [key, offer] of live) {
|
|
123
|
+
const offerBytes = Buffer.from(key);
|
|
124
|
+
if (offerBytes.length !== candidate.length)
|
|
125
|
+
continue;
|
|
126
|
+
if (timingSafeEqual(offerBytes, candidate)) {
|
|
127
|
+
live.delete(key);
|
|
128
|
+
return offer;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return;
|
|
132
|
+
},
|
|
133
|
+
revokeAccount() {
|
|
134
|
+
for (const [key, offer] of live)
|
|
135
|
+
if (offer.source === "account")
|
|
136
|
+
live.delete(key);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// apps/host/src/server-config.ts
|
|
142
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
143
|
+
import { readFile as readFile2, rename, rm, writeFile as writeFile2 } from "node:fs/promises";
|
|
144
|
+
import { join as join2 } from "node:path";
|
|
145
|
+
var SERVER_CONFIG_FILE = "server.json";
|
|
146
|
+
var normalizeOrigin = (origin) => origin.replace(/\/+$/, "");
|
|
147
|
+
async function readServerConfig(dataDir) {
|
|
148
|
+
let text;
|
|
149
|
+
try {
|
|
150
|
+
text = await readFile2(join2(dataDir, SERVER_CONFIG_FILE), "utf8");
|
|
151
|
+
} catch (error) {
|
|
152
|
+
if (error.code === "ENOENT")
|
|
153
|
+
return;
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
let value;
|
|
157
|
+
try {
|
|
158
|
+
value = JSON.parse(text);
|
|
159
|
+
} catch {
|
|
160
|
+
throw new Error(`${join2(dataDir, SERVER_CONFIG_FILE)} is not valid JSON — delete it or run \`pinomad leave\``);
|
|
161
|
+
}
|
|
162
|
+
const origin = value?.origin;
|
|
163
|
+
if (typeof origin !== "string" || origin.trim() === "") {
|
|
164
|
+
throw new Error(`${join2(dataDir, SERVER_CONFIG_FILE)} is missing a string "origin"`);
|
|
165
|
+
}
|
|
166
|
+
const binding = value.binding;
|
|
167
|
+
return {
|
|
168
|
+
origin,
|
|
169
|
+
...typeof binding === "string" && binding !== "" ? { binding } : {}
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
async function writeServerConfig(dataDir, config) {
|
|
173
|
+
const path = join2(dataDir, SERVER_CONFIG_FILE);
|
|
174
|
+
const tmp = `${path}.tmp-${process.pid}`;
|
|
175
|
+
await writeFile2(tmp, `${JSON.stringify({ origin: normalizeOrigin(config.origin), binding: randomBytes2(16).toString("base64url") })}
|
|
176
|
+
`, { mode: 384 });
|
|
177
|
+
await rename(tmp, path);
|
|
178
|
+
}
|
|
179
|
+
var removeServerConfig = (dataDir) => rm(join2(dataDir, SERVER_CONFIG_FILE), { force: true });
|
|
180
|
+
async function readServerBinding(dataDir) {
|
|
181
|
+
return (await readServerConfig(dataDir))?.binding;
|
|
182
|
+
}
|
|
183
|
+
function resolveRelayOrigin(flag, config) {
|
|
184
|
+
if (flag !== undefined && config !== undefined && normalizeOrigin(flag) !== normalizeOrigin(config.origin)) {
|
|
185
|
+
throw new Error(`--relay ${flag} disagrees with ${SERVER_CONFIG_FILE} (${config.origin}) — remove one`);
|
|
186
|
+
}
|
|
187
|
+
return flag ?? config?.origin;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export { DevicesDoc, ensureDevices, RevocationsDoc, admitDevice, loadHostKey, loadRelayKey, pairingOffers, normalizeOrigin, readServerConfig, writeServerConfig, removeServerConfig, readServerBinding, resolveRelayOrigin };
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
assertSecureServerOrigin,
|
|
4
|
+
probeServerAccounts,
|
|
5
|
+
runServerOperation
|
|
6
|
+
} from "./chunk-26d04z02.js";
|
|
7
|
+
import {
|
|
8
|
+
loadHostKey,
|
|
9
|
+
loadRelayKey,
|
|
10
|
+
readServerConfig,
|
|
11
|
+
removeServerConfig
|
|
12
|
+
} from "./chunk-ebfaf3k2.js";
|
|
13
|
+
import {
|
|
14
|
+
relayHostId
|
|
15
|
+
} from "./chunk-kmbh44dt.js";
|
|
16
|
+
import {
|
|
17
|
+
argValue,
|
|
18
|
+
restartService,
|
|
19
|
+
serviceManager,
|
|
20
|
+
withoutRelayArgs
|
|
21
|
+
} from "./chunk-aqj4wg33.js";
|
|
22
|
+
import"./chunk-1d9sppj0.js";
|
|
23
|
+
import"./chunk-m4q9wafw.js";
|
|
24
|
+
import"./chunk-hsp12cjt.js";
|
|
25
|
+
import {
|
|
26
|
+
DEFAULT_DATA_DIR
|
|
27
|
+
} from "./chunk-cjfmfyjb.js";
|
|
28
|
+
import"./chunk-df32avw6.js";
|
|
29
|
+
|
|
30
|
+
// apps/host/src/cli/leave.ts
|
|
31
|
+
import { stat } from "node:fs/promises";
|
|
32
|
+
import { join, resolve } from "node:path";
|
|
33
|
+
import { parseArgs } from "node:util";
|
|
34
|
+
var message = (error) => error instanceof Error ? error.message : String(error);
|
|
35
|
+
async function leaveServer(args, deps) {
|
|
36
|
+
const dataDir = resolve(args.dataDir);
|
|
37
|
+
const config = await readServerConfig(dataDir);
|
|
38
|
+
if (config === undefined)
|
|
39
|
+
throw new Error(`not joined — no server.json in ${dataDir}`);
|
|
40
|
+
const origin = config.origin;
|
|
41
|
+
const warnings = [];
|
|
42
|
+
const notes = [];
|
|
43
|
+
let hostId;
|
|
44
|
+
assertSecureServerOrigin(origin);
|
|
45
|
+
if (await stat(join(dataDir, "relay-key")).then(() => true, () => false)) {
|
|
46
|
+
const signingKey = await loadRelayKey(dataDir);
|
|
47
|
+
const hostKey = await loadHostKey(dataDir);
|
|
48
|
+
hostId = relayHostId(signingKey.publicKey);
|
|
49
|
+
const probe = await probeServerAccounts(origin, deps?.fetch);
|
|
50
|
+
if (probe === "unreachable") {
|
|
51
|
+
warnings.push(`cannot reach ${origin} — the server-side binding was not removed`);
|
|
52
|
+
} else if (probe === "enabled") {
|
|
53
|
+
try {
|
|
54
|
+
const reply = await runServerOperation({
|
|
55
|
+
origin,
|
|
56
|
+
signingKey,
|
|
57
|
+
noisePublicKey: hostKey.publicKey,
|
|
58
|
+
op: "leave"
|
|
59
|
+
});
|
|
60
|
+
if (reply.t === "refused" && reply.reason !== "not_joined") {
|
|
61
|
+
warnings.push(`the server refused the leave: ${reply.reason}`);
|
|
62
|
+
} else if (reply.t === "refused") {
|
|
63
|
+
notes.push("the server had no binding for this host");
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
warnings.push(`the server could not be asked to unbind (${message(error)}) — its record of this host may remain`);
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
notes.push("the server runs no accounts — nothing was bound");
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
warnings.push("no relay key on disk — the server-side binding, if any, was not removed");
|
|
73
|
+
}
|
|
74
|
+
await removeServerConfig(dataDir);
|
|
75
|
+
const manager = deps?.manager ?? serviceManager();
|
|
76
|
+
try {
|
|
77
|
+
const installed = await manager.state().then((state) => state.installed, () => false);
|
|
78
|
+
const previous = installed ? await manager.read() : undefined;
|
|
79
|
+
if (!installed) {
|
|
80
|
+
notes.push("no service installed — restart any running host so it stops dialing the server");
|
|
81
|
+
} else if (previous !== undefined && resolve(previous.dataDir) !== dataDir) {
|
|
82
|
+
notes.push(`the installed service uses ${previous.dataDir}, not ${dataDir} — left untouched`);
|
|
83
|
+
} else {
|
|
84
|
+
const stripped = previous === undefined ? undefined : { ...previous, hostArgs: withoutRelayArgs(previous.hostArgs) };
|
|
85
|
+
const rewriting = stripped !== undefined && stripped.hostArgs.length !== previous.hostArgs.length;
|
|
86
|
+
const port = argValue(previous?.hostArgs ?? [], "--port");
|
|
87
|
+
const code = await restartService({
|
|
88
|
+
force: false,
|
|
89
|
+
argv: ["--data-dir", dataDir, ...port === undefined ? [] : ["--url", `ws://127.0.0.1:${port}`]],
|
|
90
|
+
manager,
|
|
91
|
+
...rewriting ? {
|
|
92
|
+
restart: async () => {
|
|
93
|
+
await manager.install(stripped);
|
|
94
|
+
}
|
|
95
|
+
} : {}
|
|
96
|
+
});
|
|
97
|
+
if (code !== 0)
|
|
98
|
+
warnings.push("the service restart did not finish — check `service status`");
|
|
99
|
+
}
|
|
100
|
+
} catch (error) {
|
|
101
|
+
warnings.push(`could not update the service definition: ${message(error)} — remove --relay from it and restart manually`);
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
origin,
|
|
105
|
+
...hostId === undefined ? {} : { hostId },
|
|
106
|
+
warnings,
|
|
107
|
+
notes
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
async function main(argv) {
|
|
111
|
+
const { values } = parseArgs({
|
|
112
|
+
args: [...argv],
|
|
113
|
+
options: { "data-dir": { type: "string" } }
|
|
114
|
+
});
|
|
115
|
+
try {
|
|
116
|
+
const outcome = await leaveServer({ dataDir: values["data-dir"] ?? DEFAULT_DATA_DIR });
|
|
117
|
+
console.log(`left ${outcome.origin}`);
|
|
118
|
+
if (outcome.hostId !== undefined)
|
|
119
|
+
console.log(`host id: ${outcome.hostId}`);
|
|
120
|
+
for (const note of outcome.notes)
|
|
121
|
+
console.log(`note: ${note}`);
|
|
122
|
+
for (const warning of outcome.warnings)
|
|
123
|
+
console.error(`warning: ${warning}`);
|
|
124
|
+
if (outcome.warnings.length > 0)
|
|
125
|
+
process.exitCode = 1;
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error(message(error));
|
|
128
|
+
process.exitCode = 1;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (false)
|
|
132
|
+
;
|
|
133
|
+
export {
|
|
134
|
+
main,
|
|
135
|
+
leaveServer
|
|
136
|
+
};
|
|
@@ -3,7 +3,8 @@ var HOST_SETTING_KEY_SET = {
|
|
|
3
3
|
defaultModel: true,
|
|
4
4
|
defaultThinkingLevel: true,
|
|
5
5
|
steeringMode: true,
|
|
6
|
-
followUpMode: true
|
|
6
|
+
followUpMode: true,
|
|
7
|
+
acceptAccountDevices: true
|
|
7
8
|
};
|
|
8
9
|
var HOST_SETTING_KEYS = Object.keys(HOST_SETTING_KEY_SET);
|
|
9
10
|
var THINKING_LEVEL_KEYS = { off: true, minimal: true, low: true, medium: true, high: true, xhigh: true, max: true };
|
|
@@ -21,7 +22,7 @@ var conversationStream = (id) => `conversation:${id}`;
|
|
|
21
22
|
var docStream = (kind, id) => `doc:${kind}:${id}`;
|
|
22
23
|
var UNAUTHORIZED_CLOSE_CODE = 4401;
|
|
23
24
|
var INVALID_FRAME_CLOSE_CODE = 4400;
|
|
24
|
-
var PROTOCOL = { major: 5, minor:
|
|
25
|
+
var PROTOCOL = { major: 5, minor: 10 };
|
|
25
26
|
var CALL_SINCE = {
|
|
26
27
|
logout: 1,
|
|
27
28
|
updateSettings: 2,
|
|
@@ -29,12 +30,14 @@ var CALL_SINCE = {
|
|
|
29
30
|
renameConversation: 4,
|
|
30
31
|
createAgent: 6,
|
|
31
32
|
updateAgent: 6,
|
|
32
|
-
deleteAgent: 6
|
|
33
|
+
deleteAgent: 6,
|
|
34
|
+
setPushToken: 7,
|
|
35
|
+
listFiles: 10
|
|
33
36
|
};
|
|
34
37
|
function hostSupportsCall(hostMinor, method) {
|
|
35
38
|
return hostMinor >= (CALL_SINCE[method] ?? 0);
|
|
36
39
|
}
|
|
37
|
-
var FEATURE_SINCE = { images: 5 };
|
|
40
|
+
var FEATURE_SINCE = { images: 5, acceptAccountDevices: 8, apnsPush: 9, agentConversations: 10 };
|
|
38
41
|
function hostSupportsFeature(hostMinor, feature) {
|
|
39
42
|
return hostMinor >= FEATURE_SINCE[feature];
|
|
40
43
|
}
|
|
@@ -89,13 +92,16 @@ var CALL_METHODS = [
|
|
|
89
92
|
"updateSettings",
|
|
90
93
|
"createAgent",
|
|
91
94
|
"updateAgent",
|
|
92
|
-
"deleteAgent"
|
|
95
|
+
"deleteAgent",
|
|
96
|
+
"setPushToken",
|
|
97
|
+
"listFiles"
|
|
93
98
|
];
|
|
94
99
|
var SETTING_VALIDATORS = {
|
|
95
100
|
defaultModel: (value) => value === null || record(value) && nonempty(value.provider) && nonempty(value.modelId),
|
|
96
101
|
defaultThinkingLevel: (value) => value === null || isThinkingLevel(value),
|
|
97
102
|
steeringMode: (value) => value === null || isQueueMode(value),
|
|
98
|
-
followUpMode: (value) => value === null || isQueueMode(value)
|
|
103
|
+
followUpMode: (value) => value === null || isQueueMode(value),
|
|
104
|
+
acceptAccountDevices: (value) => value === null || typeof value === "boolean"
|
|
99
105
|
};
|
|
100
106
|
function validCallArgs(method, args) {
|
|
101
107
|
const conversationId = () => typeof args.conversationId === "number" && Number.isSafeInteger(args.conversationId) && args.conversationId >= 1;
|
|
@@ -124,7 +130,9 @@ function validCallArgs(method, args) {
|
|
|
124
130
|
case "listFolders":
|
|
125
131
|
return args.path === undefined || nonempty(args.path);
|
|
126
132
|
case "createConversation":
|
|
127
|
-
return record(args.home) && (args.home.kind === "chat" || args.home.kind === "project" && nonempty(args.home.path)) && typeof args.text === "string" && (nonempty(args.text) || Array.isArray(args.images) && args.images.length > 0) && nonempty(args.requestId) && images() && (args.checkout === undefined || args.checkout === "worktree" || args.checkout === "project") && (args.model === undefined || record(args.model) && nonempty(args.model.provider) && nonempty(args.model.modelId)) && (args.thinkingLevel === undefined || nonempty(args.thinkingLevel));
|
|
133
|
+
return record(args.home) && (args.home.kind === "chat" || args.home.kind === "project" && nonempty(args.home.path)) && typeof args.text === "string" && (nonempty(args.text) || Array.isArray(args.images) && args.images.length > 0) && nonempty(args.requestId) && images() && (args.checkout === undefined || args.checkout === "worktree" || args.checkout === "project") && (args.model === undefined || record(args.model) && nonempty(args.model.provider) && nonempty(args.model.modelId)) && (args.thinkingLevel === undefined || nonempty(args.thinkingLevel)) && (args.agent === undefined || nonempty(args.agent));
|
|
134
|
+
case "listFiles":
|
|
135
|
+
return typeof args.query === "string" && record(args.scope) && (args.scope.kind === "conversation" && typeof args.scope.conversationId === "number" && Number.isSafeInteger(args.scope.conversationId) && args.scope.conversationId >= 1 || args.scope.kind === "project" && nonempty(args.scope.path) && (args.scope.checkout === "worktree" || args.scope.checkout === "project"));
|
|
128
136
|
case "archive":
|
|
129
137
|
return conversationId() && typeof args.archived === "boolean";
|
|
130
138
|
case "renameConversation":
|
|
@@ -140,6 +148,8 @@ function validCallArgs(method, args) {
|
|
|
140
148
|
return role();
|
|
141
149
|
case "deleteAgent":
|
|
142
150
|
return typeof args.name === "string";
|
|
151
|
+
case "setPushToken":
|
|
152
|
+
return args.token === null || typeof args.token === "string" || record(args.token) && typeof args.token.apns === "string" && (args.token.environment === "production" || args.token.environment === "development");
|
|
143
153
|
case "updateSettings":
|
|
144
154
|
return record(args.patch) && Object.keys(args.patch).every((key) => HOST_SETTING_KEYS.includes(key)) && Object.entries(args.patch).every(([key, value]) => SETTING_VALIDATORS[key]?.(value) === true);
|
|
145
155
|
}
|
|
@@ -151,4 +161,97 @@ function nonempty(value) {
|
|
|
151
161
|
return typeof value === "string" && value.length > 0;
|
|
152
162
|
}
|
|
153
163
|
|
|
154
|
-
|
|
164
|
+
// packages/protocol/src/organization.ts
|
|
165
|
+
function organize(index, summaries) {
|
|
166
|
+
const summaryById = new Map(summaries.map((summary) => [summary.id, summary]));
|
|
167
|
+
const nodes = new Map;
|
|
168
|
+
const topLevel = new Map;
|
|
169
|
+
for (const entry of index.conversations) {
|
|
170
|
+
const home = entry.home;
|
|
171
|
+
if (home.kind === "project" && !index.projects.some((project) => project.path === home.path))
|
|
172
|
+
continue;
|
|
173
|
+
const summary = summaryById.get(entry.id);
|
|
174
|
+
if (summary === undefined)
|
|
175
|
+
continue;
|
|
176
|
+
topLevel.set(entry.id, entry);
|
|
177
|
+
nodes.set(entry.id, { summary, children: [] });
|
|
178
|
+
}
|
|
179
|
+
for (const summary of summaries) {
|
|
180
|
+
const chain = [summary];
|
|
181
|
+
let current = summary;
|
|
182
|
+
while (!topLevel.has(current.id)) {
|
|
183
|
+
const parent = current.parent === undefined ? undefined : summaryById.get(current.parent);
|
|
184
|
+
if (parent === undefined || chain.includes(parent))
|
|
185
|
+
break;
|
|
186
|
+
chain.push(parent);
|
|
187
|
+
current = parent;
|
|
188
|
+
}
|
|
189
|
+
if (!topLevel.has(current.id))
|
|
190
|
+
continue;
|
|
191
|
+
for (let i = chain.length - 1;i >= 0; i--) {
|
|
192
|
+
const member = chain[i];
|
|
193
|
+
if (!nodes.has(member.id))
|
|
194
|
+
nodes.set(member.id, { summary: member, children: [] });
|
|
195
|
+
if (i < chain.length - 1) {
|
|
196
|
+
const parent = nodes.get(chain[i + 1].id);
|
|
197
|
+
const child = nodes.get(member.id);
|
|
198
|
+
if (!parent.children.includes(child))
|
|
199
|
+
parent.children.push(child);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const byIdAscending = (a, b) => a.summary.id - b.summary.id;
|
|
204
|
+
const nodeList = (entry) => sortChildren(nodes.get(entry.id));
|
|
205
|
+
const sortChildren = (node) => ({
|
|
206
|
+
summary: node.summary,
|
|
207
|
+
children: [...node.children].sort(byIdAscending).map(sortChildren)
|
|
208
|
+
});
|
|
209
|
+
const sorted = [...topLevel.values()].sort((a, b) => b.createdAt - a.createdAt);
|
|
210
|
+
const entries = sorted.filter((entry) => entry.archived !== true);
|
|
211
|
+
return {
|
|
212
|
+
chats: entries.filter((entry) => entry.home.kind === "chat").map(nodeList),
|
|
213
|
+
projects: [...index.projects].sort((a, b) => b.addedAt - a.addedAt).map((project) => ({
|
|
214
|
+
project,
|
|
215
|
+
conversations: entries.filter((entry) => entry.home.kind === "project" && entry.home.path === project.path).map(nodeList)
|
|
216
|
+
})),
|
|
217
|
+
archived: sorted.filter((entry) => entry.archived === true).map((entry) => {
|
|
218
|
+
const home = entry.home;
|
|
219
|
+
const project = home.kind === "project" ? index.projects.find((each) => each.path === home.path) : undefined;
|
|
220
|
+
return { node: nodeList(entry), ...project === undefined ? {} : { project } };
|
|
221
|
+
})
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function checkoutOf(index, summaries, id) {
|
|
225
|
+
const record2 = index.checkouts?.find((checkout) => checkout.conversationId === id);
|
|
226
|
+
if (record2 !== undefined)
|
|
227
|
+
return record2;
|
|
228
|
+
const summaryById = new Map(summaries.map((summary) => [summary.id, summary]));
|
|
229
|
+
const seen = new Set;
|
|
230
|
+
let current = summaryById.get(id);
|
|
231
|
+
while (current !== undefined && !seen.has(current.id)) {
|
|
232
|
+
seen.add(current.id);
|
|
233
|
+
const ancestor = index.checkouts?.find((checkout) => checkout.conversationId === current.id);
|
|
234
|
+
if (ancestor !== undefined)
|
|
235
|
+
return ancestor;
|
|
236
|
+
current = current.parent === undefined ? undefined : summaryById.get(current.parent);
|
|
237
|
+
}
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
function homeOf(index, summaries, id) {
|
|
241
|
+
const entry = index.conversations.find((candidate) => candidate.id === id);
|
|
242
|
+
if (entry !== undefined)
|
|
243
|
+
return entry.home;
|
|
244
|
+
const summaryById = new Map(summaries.map((summary) => [summary.id, summary]));
|
|
245
|
+
const seen = new Set;
|
|
246
|
+
let current = summaryById.get(id);
|
|
247
|
+
while (current !== undefined && !seen.has(current.id)) {
|
|
248
|
+
seen.add(current.id);
|
|
249
|
+
const ancestor = index.conversations.find((candidate) => candidate.id === current.id);
|
|
250
|
+
if (ancestor !== undefined)
|
|
251
|
+
return ancestor.home;
|
|
252
|
+
current = current.parent === undefined ? undefined : summaryById.get(current.parent);
|
|
253
|
+
}
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export { HOST_SETTING_KEYS, THINKING_LEVELS, isThinkingLevel, isQueueMode, HOST_STREAM_NAMES, hostStreams, conversationStream, docStream, UNAUTHORIZED_CLOSE_CODE, INVALID_FRAME_CLOSE_CODE, PROTOCOL, hostSupportsCall, hostSupportsFeature, parseClientFrame, organize, checkoutOf, homeOf };
|