@anhlp/alp-protocol 0.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +7 -0
- package/README.md +17 -0
- package/conformance/cases.json +951 -0
- package/conformance/run.d.mts +32 -0
- package/conformance/run.mjs +403 -0
- package/dist/connection.d.ts +9 -0
- package/dist/connection.js +0 -0
- package/dist/contract-v1.d.ts +4171 -0
- package/dist/contract-v1.js +5 -0
- package/dist/contract.d.ts +25 -0
- package/dist/contract.js +28 -0
- package/dist/dto.d.ts +104 -0
- package/dist/dto.js +0 -0
- package/dist/events.d.ts +225 -0
- package/dist/events.js +0 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +2 -0
- package/dist/public.d.ts +1 -0
- package/dist/public.js +2 -0
- package/dist/rpc.d.ts +498 -0
- package/dist/rpc.js +0 -0
- package/dist/session.d.ts +134 -0
- package/dist/session.js +0 -0
- package/dist/validate.d.ts +12 -0
- package/dist/validate.js +147 -0
- package/dist/wire.d.ts +41 -0
- package/dist/wire.js +32 -0
- package/fixtures/ipc-v1.jsonl +4 -0
- package/fixtures/v1/cases.json +2827 -0
- package/package.json +129 -0
- package/schema/ipc-v1.json +2007 -0
- package/schema/methods-v1.json +4070 -0
- package/src/connection.ts +9 -0
- package/src/contract-v1.js +2 -0
- package/src/contract.js +44 -0
- package/src/dto.ts +78 -0
- package/src/events.ts +127 -0
- package/src/index.ts +6 -0
- package/src/public.js +2 -0
- package/src/rpc.ts +102 -0
- package/src/session.ts +94 -0
- package/src/validate.js +113 -0
- package/src/wire.js +26 -0
package/dist/validate.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// packages/protocol/src/validate.js
|
|
2
|
+
var MAX_ISSUES = 8;
|
|
3
|
+
function typeOf(value) {
|
|
4
|
+
if (value === null) return "null";
|
|
5
|
+
if (Array.isArray(value)) return "array";
|
|
6
|
+
if (typeof value === "number") return !Number.isFinite(value) ? "non-finite number" : Number.isInteger(value) ? "integer" : "number";
|
|
7
|
+
return typeof value;
|
|
8
|
+
}
|
|
9
|
+
var typeMatches = (want, have) => want === have || want === "number" && have === "integer";
|
|
10
|
+
function equal(a, b) {
|
|
11
|
+
if (a === b) return true;
|
|
12
|
+
if (typeOf(a) !== typeOf(b) || a === null || typeof a !== "object") return false;
|
|
13
|
+
if (Array.isArray(a)) return a.length === /** @type {unknown[]} */
|
|
14
|
+
b.length && a.every((item, index) => equal(
|
|
15
|
+
item,
|
|
16
|
+
/** @type {unknown[]} */
|
|
17
|
+
b[index]
|
|
18
|
+
));
|
|
19
|
+
const left = Object.keys(a).filter((key) => (
|
|
20
|
+
/** @type {any} */
|
|
21
|
+
a[key] !== void 0
|
|
22
|
+
));
|
|
23
|
+
const right = Object.keys(
|
|
24
|
+
/** @type {object} */
|
|
25
|
+
b
|
|
26
|
+
).filter((key) => (
|
|
27
|
+
/** @type {any} */
|
|
28
|
+
b[key] !== void 0
|
|
29
|
+
));
|
|
30
|
+
return left.length === right.length && left.every((key) => equal(
|
|
31
|
+
/** @type {any} */
|
|
32
|
+
a[key],
|
|
33
|
+
/** @type {any} */
|
|
34
|
+
b[key]
|
|
35
|
+
));
|
|
36
|
+
}
|
|
37
|
+
function resolve(root, ref) {
|
|
38
|
+
if (!ref.startsWith("#/")) throw new Error(`Unsupported schema reference ${ref}`);
|
|
39
|
+
let target = root;
|
|
40
|
+
for (const part of ref.slice(2).split("/")) target = target?.[part.replaceAll("~1", "/").replaceAll("~0", "~")];
|
|
41
|
+
if (target === void 0) throw new Error(`Unresolved schema reference ${ref}`);
|
|
42
|
+
return target;
|
|
43
|
+
}
|
|
44
|
+
function check(schema, value, at, root, issues) {
|
|
45
|
+
if (issues.length >= MAX_ISSUES || schema === true || schema === void 0) return;
|
|
46
|
+
if (schema === false) {
|
|
47
|
+
issues.push({ path: at, message: "is not allowed" });
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (schema.$ref) {
|
|
51
|
+
check(resolve(root, schema.$ref), value, at, root, issues);
|
|
52
|
+
if (Object.keys(schema).length === 1) return;
|
|
53
|
+
}
|
|
54
|
+
const kind = typeOf(value);
|
|
55
|
+
if (schema.type !== void 0) {
|
|
56
|
+
const wanted = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
57
|
+
if (!wanted.some(
|
|
58
|
+
/** @param {string} want */
|
|
59
|
+
(want) => typeMatches(want, kind)
|
|
60
|
+
)) {
|
|
61
|
+
issues.push({ path: at, message: `must be ${wanted.join(" or ")}` });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if ("const" in schema && !equal(schema.const, value)) {
|
|
66
|
+
issues.push({ path: at, message: `must be ${JSON.stringify(schema.const)}` });
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (schema.enum && !schema.enum.some(
|
|
70
|
+
/** @param {unknown} option */
|
|
71
|
+
(option) => equal(option, value)
|
|
72
|
+
)) {
|
|
73
|
+
issues.push({ path: at, message: `must be one of ${schema.enum.map(
|
|
74
|
+
/** @param {unknown} option */
|
|
75
|
+
(option) => JSON.stringify(option)
|
|
76
|
+
).join(", ")}` });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (kind === "integer" || kind === "number") {
|
|
80
|
+
const number = (
|
|
81
|
+
/** @type {number} */
|
|
82
|
+
value
|
|
83
|
+
);
|
|
84
|
+
if (schema.minimum !== void 0 && number < schema.minimum) issues.push({ path: at, message: `must be at least ${schema.minimum}` });
|
|
85
|
+
if (schema.maximum !== void 0 && number > schema.maximum) issues.push({ path: at, message: `must be at most ${schema.maximum}` });
|
|
86
|
+
}
|
|
87
|
+
if (kind === "string") {
|
|
88
|
+
const text = (
|
|
89
|
+
/** @type {string} */
|
|
90
|
+
value
|
|
91
|
+
);
|
|
92
|
+
let length = 0;
|
|
93
|
+
if (schema.minLength !== void 0 || schema.maxLength !== void 0) for (const _point of text) length++;
|
|
94
|
+
if (schema.minLength !== void 0 && length < schema.minLength) issues.push({ path: at, message: `must have at least ${schema.minLength} characters` });
|
|
95
|
+
if (schema.maxLength !== void 0 && length > schema.maxLength) issues.push({ path: at, message: `must have at most ${schema.maxLength} characters` });
|
|
96
|
+
if (schema.pattern !== void 0 && !new RegExp(schema.pattern, "u").test(text)) issues.push({ path: at, message: `must match ${schema.pattern}` });
|
|
97
|
+
}
|
|
98
|
+
if (kind === "array" && schema.items !== void 0) {
|
|
99
|
+
const array = (
|
|
100
|
+
/** @type {unknown[]} */
|
|
101
|
+
value
|
|
102
|
+
);
|
|
103
|
+
for (let index = 0; index < array.length && issues.length < MAX_ISSUES; index++) check(schema.items, array[index] === void 0 ? null : array[index], `${at}/${index}`, root, issues);
|
|
104
|
+
}
|
|
105
|
+
if (kind === "object") {
|
|
106
|
+
const object = (
|
|
107
|
+
/** @type {Record<string, unknown>} */
|
|
108
|
+
value
|
|
109
|
+
);
|
|
110
|
+
const properties = schema.properties ?? {};
|
|
111
|
+
for (const name of schema.required ?? []) if (object[name] === void 0) issues.push({ path: `${at}/${name}`, message: "is required" });
|
|
112
|
+
for (const [name, child] of Object.entries(object)) {
|
|
113
|
+
if (child === void 0) continue;
|
|
114
|
+
if (Object.hasOwn(properties, name)) check(properties[name], child, `${at}/${name}`, root, issues);
|
|
115
|
+
else if (schema.additionalProperties !== void 0) check(schema.additionalProperties, child, `${at}/${name}`, root, issues);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (schema.not !== void 0) {
|
|
119
|
+
const inner = [];
|
|
120
|
+
check(schema.not, value, at, root, inner);
|
|
121
|
+
if (!inner.length) issues.push({ path: at, message: "matches a schema it must not match" });
|
|
122
|
+
}
|
|
123
|
+
for (
|
|
124
|
+
const [keyword, exactlyOne] of
|
|
125
|
+
/** @type {const} */
|
|
126
|
+
[["anyOf", false], ["oneOf", true]]
|
|
127
|
+
) {
|
|
128
|
+
if (!schema[keyword]) continue;
|
|
129
|
+
const passing = schema[keyword].filter(
|
|
130
|
+
/** @param {any} option */
|
|
131
|
+
(option) => {
|
|
132
|
+
const inner = [];
|
|
133
|
+
check(option, value, at, root, inner);
|
|
134
|
+
return !inner.length;
|
|
135
|
+
}
|
|
136
|
+
).length;
|
|
137
|
+
if (exactlyOne ? passing !== 1 : passing === 0) issues.push({ path: at, message: exactlyOne && passing > 1 ? "matches more than one allowed shape" : "matches none of the allowed shapes" });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function validateSchema(schema, value, root = schema) {
|
|
141
|
+
const issues = [];
|
|
142
|
+
check(schema, value, "", root, issues);
|
|
143
|
+
return issues;
|
|
144
|
+
}
|
|
145
|
+
export {
|
|
146
|
+
validateSchema
|
|
147
|
+
};
|
package/dist/wire.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** UTF-8 socket writers use one newline-terminated JSON value per frame. @param {unknown} value */
|
|
2
|
+
export function encodeFrame(value: unknown): string;
|
|
3
|
+
/** Parse one complete frame. Buffering and failure handling belong to the socket owner. @param {string} line @returns {unknown} */
|
|
4
|
+
export function decodeFrame(line: string): unknown;
|
|
5
|
+
/** Wire protocol of alpd (plans/reference/ALPD.md §3). Bump only for incompatible changes. */
|
|
6
|
+
export const PROTOCOL_VERSION: 1;
|
|
7
|
+
export const MAX_FRAME: number;
|
|
8
|
+
export class AlpRpcError extends Error {
|
|
9
|
+
constructor(error: any);
|
|
10
|
+
code: any;
|
|
11
|
+
data: any;
|
|
12
|
+
}
|
|
13
|
+
export const ERROR: Readonly<{
|
|
14
|
+
failed: 1000;
|
|
15
|
+
notFound: 1001;
|
|
16
|
+
forbidden: 1003;
|
|
17
|
+
conflict: 1004;
|
|
18
|
+
unavailable: 1005;
|
|
19
|
+
protocol: 1006;
|
|
20
|
+
}>;
|
|
21
|
+
/** The JSON-RPC 2.0 codes alpd answers with (docs/protocol/alp-json-rpc-v1.md §5). */
|
|
22
|
+
export const RPC_ERROR: Readonly<{
|
|
23
|
+
parseError: -32700;
|
|
24
|
+
invalidRequest: -32600;
|
|
25
|
+
methodNotFound: -32601;
|
|
26
|
+
invalidParams: -32602;
|
|
27
|
+
}>;
|
|
28
|
+
/**
|
|
29
|
+
* Features a daemon lists in its daemon.hello result. A client uses a feature only when listed;
|
|
30
|
+
* a daemon that lists none (one older than this list) has none of them. Unknown names are ignored.
|
|
31
|
+
*/
|
|
32
|
+
export const CAPABILITY: Readonly<{
|
|
33
|
+
attachAfter: "attach.after";
|
|
34
|
+
promptReceipts: "prompt.receipts";
|
|
35
|
+
extensionMethods: "extension.methods";
|
|
36
|
+
team: "team";
|
|
37
|
+
task: "task";
|
|
38
|
+
library: "library";
|
|
39
|
+
settingsLanguage: "settings.language";
|
|
40
|
+
}>;
|
|
41
|
+
export const RPC_METHODS: readonly string[];
|
package/dist/wire.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// packages/protocol/src/wire.js
|
|
2
|
+
var PROTOCOL_VERSION = 1;
|
|
3
|
+
var MAX_FRAME = 16 * 1024 * 1024;
|
|
4
|
+
var AlpRpcError = class extends Error {
|
|
5
|
+
constructor(error) {
|
|
6
|
+
super(error?.message ?? "ALP daemon request failed");
|
|
7
|
+
this.code = error?.code;
|
|
8
|
+
this.data = error?.data;
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
var ERROR = Object.freeze({ failed: 1e3, notFound: 1001, forbidden: 1003, conflict: 1004, unavailable: 1005, protocol: 1006 });
|
|
12
|
+
var RPC_ERROR = Object.freeze({ parseError: -32700, invalidRequest: -32600, methodNotFound: -32601, invalidParams: -32602 });
|
|
13
|
+
var CAPABILITY = Object.freeze({ attachAfter: "attach.after", promptReceipts: "prompt.receipts", extensionMethods: "extension.methods", team: "team", task: "task", library: "library", settingsLanguage: "settings.language" });
|
|
14
|
+
var RPC_METHODS = Object.freeze(["assignment.recall", "board.list", "catalog.get", "daemon.hello", "daemon.pause", "daemon.pauses", "daemon.resume", "daemon.shutdown", "daemon.status", "daemon.usage", "extension.register", "extension.unregister", "library.delete", "library.duplicate", "library.get", "library.list", "library.models", "library.rename", "library.save", "library.skills.set", "question.answer", "question.list", "session.archive", "session.attach", "session.close", "session.configure", "session.create", "session.get", "session.interrupt", "session.list", "session.log", "session.message", "session.preview", "session.prompt", "session.release", "session.rename", "session.status", "settings.language.get", "settings.language.set", "task.create", "task.gate.resolve", "task.list", "task.update", "team.list"]);
|
|
15
|
+
function encodeFrame(value) {
|
|
16
|
+
return `${JSON.stringify(value)}
|
|
17
|
+
`;
|
|
18
|
+
}
|
|
19
|
+
function decodeFrame(line) {
|
|
20
|
+
return JSON.parse(line);
|
|
21
|
+
}
|
|
22
|
+
export {
|
|
23
|
+
AlpRpcError,
|
|
24
|
+
CAPABILITY,
|
|
25
|
+
ERROR,
|
|
26
|
+
MAX_FRAME,
|
|
27
|
+
PROTOCOL_VERSION,
|
|
28
|
+
RPC_ERROR,
|
|
29
|
+
RPC_METHODS,
|
|
30
|
+
decodeFrame,
|
|
31
|
+
encodeFrame
|
|
32
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
{"jsonrpc":"2.0","id":1,"method":"daemon.hello","params":{"protocolVersion":1,"client":{"name":"fixture","version":"0.9.5"}}}
|
|
2
|
+
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1}}
|
|
3
|
+
{"jsonrpc":"2.0","method":"event","params":{"sessionId":"ses_fixture","epoch":"fixture-epoch","seq":1,"ts":"2026-10-14T00:00:00.000Z","event":{"type":"session.ready"}}}
|
|
4
|
+
{"jsonrpc":"2.0","id":2,"error":{"code":1001,"message":"No session ses_fixture"}}
|