@cueloop/daemon 0.1.0-alpha.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/package.json +29 -0
- package/src/api.test.ts +156 -0
- package/src/api.ts +200 -0
- package/src/client.ts +183 -0
- package/src/errors.ts +10 -0
- package/src/index.ts +8 -0
- package/src/main.ts +22 -0
- package/src/paths.ts +27 -0
- package/src/protocol.ts +40 -0
- package/src/server.test.ts +103 -0
- package/src/server.ts +250 -0
- package/src/single-instance.test.ts +101 -0
- package/src/store.ts +68 -0
- package/src/validate.test.ts +98 -0
- package/src/validate.ts +133 -0
package/src/validate.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime validation of the wire boundary (#14). Everything arriving over the
|
|
3
|
+
* socket is untrusted JSON: adapters, the CLI, extensions, and any script a
|
|
4
|
+
* user writes all speak this protocol. Validating here means DaemonCore only
|
|
5
|
+
* ever sees well-formed input, and a malformed request gets a precise error
|
|
6
|
+
* instead of a downstream crash.
|
|
7
|
+
*
|
|
8
|
+
* valibot is cueloop's validation library - modular, tiny, and tree-shakeable,
|
|
9
|
+
* which matters for a CLI whose startup latency the user feels on every review.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as v from "valibot";
|
|
13
|
+
import { SCHEMA_VERSION } from "@cueloop/schema";
|
|
14
|
+
import { DaemonError } from "./errors";
|
|
15
|
+
|
|
16
|
+
const NonEmpty = v.pipe(v.string(), v.minLength(1));
|
|
17
|
+
|
|
18
|
+
export const WorkspaceSchema = v.object({
|
|
19
|
+
repoRoot: NonEmpty,
|
|
20
|
+
branch: NonEmpty,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export const ArtifactSchema = v.object({
|
|
24
|
+
type: v.picklist(["plan", "diff"]),
|
|
25
|
+
content: v.string(),
|
|
26
|
+
meta: v.optional(
|
|
27
|
+
v.object({
|
|
28
|
+
cwd: v.optional(v.string()),
|
|
29
|
+
agent: v.optional(v.string()),
|
|
30
|
+
agentSessionId: v.optional(v.string()),
|
|
31
|
+
planPath: v.optional(v.string()),
|
|
32
|
+
title: v.optional(v.string()),
|
|
33
|
+
pr: v.optional(v.string()),
|
|
34
|
+
}),
|
|
35
|
+
{},
|
|
36
|
+
),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export const AnchorSchema = v.object({
|
|
40
|
+
quote: v.string(),
|
|
41
|
+
prefix: v.optional(v.string(), ""),
|
|
42
|
+
suffix: v.optional(v.string(), ""),
|
|
43
|
+
blockIndex: v.optional(v.number()),
|
|
44
|
+
start: v.optional(v.number()),
|
|
45
|
+
end: v.optional(v.number()),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export const AnnotationSchema = v.object({
|
|
49
|
+
id: NonEmpty,
|
|
50
|
+
/** Open kind set (#2): built-ins are comment and suggestion. */
|
|
51
|
+
kind: NonEmpty,
|
|
52
|
+
anchor: AnchorSchema,
|
|
53
|
+
body: v.string(),
|
|
54
|
+
orphan: v.optional(v.boolean()),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const SessionId = NonEmpty;
|
|
58
|
+
|
|
59
|
+
export const Params = {
|
|
60
|
+
"session.create": v.object({ workspace: WorkspaceSchema, artifact: ArtifactSchema }),
|
|
61
|
+
"session.get": v.object({ id: SessionId }),
|
|
62
|
+
"session.list": v.object({
|
|
63
|
+
filter: v.optional(
|
|
64
|
+
v.object({
|
|
65
|
+
status: v.optional(v.picklist(["pending", "resolved"])),
|
|
66
|
+
workspace: v.optional(v.partial(WorkspaceSchema)),
|
|
67
|
+
}),
|
|
68
|
+
),
|
|
69
|
+
}),
|
|
70
|
+
"session.wait": v.object({
|
|
71
|
+
id: SessionId,
|
|
72
|
+
// clamped: a negative or absurd timeout is a client bug, not a daemon one
|
|
73
|
+
timeoutMs: v.optional(v.pipe(v.number(), v.minValue(0), v.maxValue(24 * 60 * 60 * 1000)), 60_000),
|
|
74
|
+
}),
|
|
75
|
+
"session.annotate": v.object({ id: SessionId, annotation: AnnotationSchema }),
|
|
76
|
+
"session.removeAnnotation": v.object({ id: SessionId, annotationId: NonEmpty }),
|
|
77
|
+
"session.setWorkingCopy": v.object({ id: SessionId, workingCopy: v.optional(v.string()) }),
|
|
78
|
+
"session.resolve": v.object({
|
|
79
|
+
id: SessionId,
|
|
80
|
+
verdictKind: v.picklist(["comment", "approve", "request_changes"]),
|
|
81
|
+
summary: v.optional(v.string(), ""),
|
|
82
|
+
}),
|
|
83
|
+
"session.submitRevision": v.object({ id: SessionId, content: v.string() }),
|
|
84
|
+
"events.subscribe": v.object({}),
|
|
85
|
+
"daemon.ping": v.object({}),
|
|
86
|
+
"daemon.shutdown": v.object({}),
|
|
87
|
+
} as const;
|
|
88
|
+
|
|
89
|
+
export type MethodName = keyof typeof Params;
|
|
90
|
+
|
|
91
|
+
export function isKnownMethod(method: string): method is MethodName {
|
|
92
|
+
return method in Params;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Validate params for a method, or throw a DaemonError the client can read. */
|
|
96
|
+
export function parseParams<M extends MethodName>(method: M, params: unknown): v.InferOutput<(typeof Params)[M]> {
|
|
97
|
+
const result = v.safeParse(Params[method], params ?? {});
|
|
98
|
+
if (!result.success) {
|
|
99
|
+
const issue = result.issues[0]!;
|
|
100
|
+
const path = issue.path?.map((p) => String(p.key)).join(".") ?? "";
|
|
101
|
+
throw new DaemonError("invalid_params", `${method}: ${path ? path + " - " : ""}${issue.message}`);
|
|
102
|
+
}
|
|
103
|
+
return result.output;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Persisted records are validated on recovery: a bad file is skipped, not fatal. */
|
|
107
|
+
export const SessionRecordSchema = v.object({
|
|
108
|
+
schemaVersion: v.literal(SCHEMA_VERSION),
|
|
109
|
+
id: NonEmpty,
|
|
110
|
+
workspace: WorkspaceSchema,
|
|
111
|
+
artifact: ArtifactSchema,
|
|
112
|
+
revisions: v.array(v.object({ revision: v.number(), content: v.string(), submittedAt: v.string() })),
|
|
113
|
+
annotations: v.array(v.object({ ...AnnotationSchema.entries, createdAt: v.string() })),
|
|
114
|
+
workingCopy: v.optional(v.string()),
|
|
115
|
+
verdict: v.nullable(
|
|
116
|
+
v.object({
|
|
117
|
+
kind: v.picklist(["comment", "approve", "request_changes"]),
|
|
118
|
+
summary: v.string(),
|
|
119
|
+
feedback: v.string(),
|
|
120
|
+
resolvedAt: v.string(),
|
|
121
|
+
}),
|
|
122
|
+
),
|
|
123
|
+
status: v.picklist(["pending", "resolved"]),
|
|
124
|
+
createdAt: v.string(),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
export function validateSessionRecord(raw: unknown): { ok: true; value: v.InferOutput<typeof SessionRecordSchema> } | { ok: false; error: string } {
|
|
128
|
+
const result = v.safeParse(SessionRecordSchema, raw);
|
|
129
|
+
if (result.success) return { ok: true, value: result.output };
|
|
130
|
+
const issue = result.issues[0]!;
|
|
131
|
+
const path = issue.path?.map((p) => String(p.key)).join(".") ?? "";
|
|
132
|
+
return { ok: false, error: `${path ? path + ": " : ""}${issue.message}` };
|
|
133
|
+
}
|