@h1v35/hivex 0.2.0 → 0.2.2
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 +55 -163
- package/docs/CONTEXT.md +20 -36
- package/docs/README.md +6 -12
- package/docs/adr/0003-independent-bun-installation.md +5 -19
- package/docs/adr/0010-practical-knowledge-assistance.md +28 -81
- package/docs/adr/0011-shared-knowledge-and-selective-history.md +16 -43
- package/docs/guidelines/engineering.md +74 -0
- package/docs/procedures/self-hosted-runner.md +7 -0
- package/package.json +32 -11
- package/skills/hivex/SKILL.md +28 -92
- package/skills/hivex/references/markdown.md +12 -42
- package/src/cli/diagnostic.ts +21 -11
- package/src/cli.ts +46 -36
- package/src/documents.ts +502 -320
- package/src/errors.ts +8 -6
- package/src/implementation.ts +185 -87
- package/src/ingestion-units.ts +107 -64
- package/src/knowledge-maintenance.ts +35 -22
- package/src/knowledge-model.ts +386 -268
- package/src/knowledge-serialization.ts +239 -0
- package/src/knowledge-snapshot.ts +100 -77
- package/src/knowledge-store.ts +634 -453
- package/src/knowledge.ts +1001 -758
- package/src/markdown.ts +107 -45
- package/src/model/connection.ts +134 -76
- package/src/model/failure.ts +46 -23
- package/src/model/invoke.ts +346 -166
- package/src/model/profile.ts +201 -103
- package/src/model/rpc-error.ts +21 -0
- package/src/model/server.ts +151 -82
- package/src/model/thread.ts +24 -14
- package/src/model/transcript.ts +87 -46
- package/src/ordering.ts +9 -0
- package/src/retrieval/lexical.ts +64 -41
- package/src/review.ts +83 -55
- package/src/runtime.d.ts +4 -0
- package/src/snapshot-command.ts +82 -43
- package/src/source-relocation.ts +222 -0
- package/docs/engineering.md +0 -174
package/src/markdown.ts
CHANGED
|
@@ -1,98 +1,160 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import
|
|
2
|
+
import pathModule from 'node:path';
|
|
3
3
|
import { fromMarkdown } from 'mdast-util-from-markdown';
|
|
4
|
-
import { toString } from 'mdast-util-to-string';
|
|
5
4
|
import { gfmFromMarkdown } from 'mdast-util-gfm';
|
|
6
5
|
import { gfm } from 'micromark-extension-gfm';
|
|
7
6
|
import { frontmatterFromMarkdown } from 'mdast-util-frontmatter';
|
|
8
7
|
import { frontmatter } from 'micromark-extension-frontmatter';
|
|
9
8
|
import { normalizeIdentifier } from 'micromark-util-normalize-identifier';
|
|
9
|
+
import { toString as mdastToString } from 'mdast-util-to-string';
|
|
10
10
|
import { parseDocument } from 'yaml';
|
|
11
11
|
|
|
12
12
|
export const hash = (text: string) => createHash('sha256').update(text).digest('hex');
|
|
13
|
-
export const isMarkdownPath = (path: string) => /\.(?:md|markdown|mdown)$/
|
|
13
|
+
export const isMarkdownPath = (path: string) => /\.(?:md|markdown|mdown)$/iu.test(path);
|
|
14
14
|
|
|
15
|
-
export function parseMarkdown(content: string) {
|
|
15
|
+
export const parseMarkdown = function parseMarkdown(content: string) {
|
|
16
16
|
return fromMarkdown(content, {
|
|
17
17
|
extensions: [gfm(), frontmatter(['yaml'])],
|
|
18
18
|
mdastExtensions: [gfmFromMarkdown(), frontmatterFromMarkdown(['yaml'])],
|
|
19
19
|
});
|
|
20
|
-
}
|
|
20
|
+
};
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
interface MarkdownNode {
|
|
23
23
|
type: string;
|
|
24
24
|
children?: MarkdownNode[];
|
|
25
25
|
url?: string;
|
|
26
26
|
identifier?: string;
|
|
27
|
-
position?: {
|
|
28
|
-
};
|
|
27
|
+
position?: {
|
|
28
|
+
start: { line: number; offset?: number };
|
|
29
|
+
end: { line: number; offset?: number };
|
|
30
|
+
};
|
|
31
|
+
}
|
|
29
32
|
|
|
30
|
-
export function* descendants(tree: MarkdownNode): Generator<MarkdownNode> {
|
|
33
|
+
export const descendants = function* descendants(tree: MarkdownNode): Generator<MarkdownNode> {
|
|
31
34
|
const pending = [tree];
|
|
32
35
|
while (pending.length) {
|
|
33
36
|
const node = pending.pop();
|
|
34
|
-
if (!node)
|
|
37
|
+
if (!node) {
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
35
40
|
yield node;
|
|
36
|
-
for (let index = (node.children?.length ?? 0) - 1; index >= 0; index
|
|
41
|
+
for (let index = (node.children?.length ?? 0) - 1; index >= 0; index -= 1) {
|
|
37
42
|
const child = node.children?.[index];
|
|
38
|
-
if (child)
|
|
43
|
+
if (child) {
|
|
44
|
+
pending.push(child);
|
|
45
|
+
}
|
|
39
46
|
}
|
|
40
47
|
}
|
|
41
|
-
}
|
|
48
|
+
};
|
|
42
49
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
50
|
+
const emptyMetadata = { status: null, title: null };
|
|
51
|
+
|
|
52
|
+
const yamlValue = function yamlValue(content: string): unknown {
|
|
53
|
+
let parsed: ReturnType<typeof parseDocument>;
|
|
46
54
|
try {
|
|
47
|
-
|
|
48
|
-
if (parsed.errors.length) return { title: null, status: null };
|
|
49
|
-
const value: unknown = parsed.toJS({ maxAliasCount: 0 });
|
|
50
|
-
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
51
|
-
return { title: null, status: null };
|
|
52
|
-
return {
|
|
53
|
-
title:
|
|
54
|
-
'title' in value && typeof value.title === 'string' && value.title.trim()
|
|
55
|
-
? value.title
|
|
56
|
-
: null,
|
|
57
|
-
status: 'status' in value && typeof value.status === 'string' ? value.status : null,
|
|
58
|
-
};
|
|
55
|
+
parsed = parseDocument(content, { uniqueKeys: true });
|
|
59
56
|
} catch {
|
|
60
|
-
return
|
|
57
|
+
return null;
|
|
61
58
|
}
|
|
62
|
-
|
|
59
|
+
if (parsed.errors.length) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
return parsed.toJS({ maxAliasCount: 0 });
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const isRecord = function isRecord(value: unknown): value is Record<string, unknown> {
|
|
70
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const metadata = function metadata(tree: ReturnType<typeof parseMarkdown>) {
|
|
74
|
+
const node = tree.children.find((entry) => entry.type === 'yaml');
|
|
75
|
+
if (node?.type !== 'yaml') {
|
|
76
|
+
return emptyMetadata;
|
|
77
|
+
}
|
|
78
|
+
const value = yamlValue(node.value);
|
|
79
|
+
if (!isRecord(value)) {
|
|
80
|
+
return emptyMetadata;
|
|
81
|
+
}
|
|
82
|
+
const { status: statusValue, title: titleValue } = value;
|
|
83
|
+
return {
|
|
84
|
+
status: typeof statusValue === 'string' ? statusValue : null,
|
|
85
|
+
title: typeof titleValue === 'string' && titleValue.trim().length > 0 ? titleValue : null,
|
|
86
|
+
};
|
|
87
|
+
};
|
|
63
88
|
|
|
64
|
-
export function describeMarkdown(path: string, content: string) {
|
|
89
|
+
export const describeMarkdown = function describeMarkdown(path: string, content: string) {
|
|
65
90
|
const tree = parseMarkdown(content);
|
|
66
91
|
const front = metadata(tree);
|
|
67
92
|
const heading = tree.children.find((node) => node.type === 'heading');
|
|
68
93
|
const definitions = new Map<string, string>();
|
|
69
94
|
for (const node of descendants(tree)) {
|
|
70
|
-
if (node.type
|
|
95
|
+
if (node.type !== 'definition') {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (
|
|
99
|
+
typeof node.identifier === 'string' &&
|
|
100
|
+
node.identifier.length > 0 &&
|
|
101
|
+
typeof node.url === 'string' &&
|
|
102
|
+
node.url.length > 0
|
|
103
|
+
) {
|
|
71
104
|
const id = normalizeIdentifier(node.identifier);
|
|
72
|
-
if (!definitions.has(id))
|
|
105
|
+
if (!definitions.has(id)) {
|
|
106
|
+
definitions.set(id, node.url);
|
|
107
|
+
}
|
|
73
108
|
}
|
|
74
109
|
}
|
|
75
110
|
const links = [...descendants(tree)].flatMap((node) => {
|
|
76
|
-
if (node.type === 'link' && node.url
|
|
77
|
-
|
|
111
|
+
if (node.type === 'link' && typeof node.url === 'string' && node.url.length > 0) {
|
|
112
|
+
return [node.url];
|
|
113
|
+
}
|
|
114
|
+
if (
|
|
115
|
+
node.type !== 'linkReference' ||
|
|
116
|
+
typeof node.identifier !== 'string' ||
|
|
117
|
+
node.identifier.length === 0
|
|
118
|
+
) {
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
78
121
|
const url = definitions.get(normalizeIdentifier(node.identifier));
|
|
79
|
-
|
|
122
|
+
if (url === undefined) {
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
return [url];
|
|
80
126
|
});
|
|
81
127
|
return {
|
|
82
|
-
title: front.title ?? (heading ? toString(heading) : basename(path)),
|
|
83
|
-
status: front.status,
|
|
84
128
|
links,
|
|
129
|
+
status: front.status,
|
|
130
|
+
title: front.title ?? (heading ? mdastToString(heading) : pathModule.basename(path)),
|
|
85
131
|
};
|
|
86
|
-
}
|
|
132
|
+
};
|
|
87
133
|
|
|
88
|
-
export function rawMarkdownLines(text: string): string[] {
|
|
89
|
-
const lines
|
|
134
|
+
export const rawMarkdownLines = function rawMarkdownLines(text: string): string[] {
|
|
135
|
+
const lines: string[] = [];
|
|
136
|
+
let lineStart = 0;
|
|
137
|
+
let index = 0;
|
|
138
|
+
while (index < text.length) {
|
|
139
|
+
const character = text[index];
|
|
140
|
+
if (character === '\r' || character === '\n') {
|
|
141
|
+
const lineEnd = index + (character === '\r' && text[index + 1] === '\n' ? 2 : 1);
|
|
142
|
+
lines.push(text.slice(lineStart, lineEnd));
|
|
143
|
+
lineStart = lineEnd;
|
|
144
|
+
index = lineEnd;
|
|
145
|
+
} else {
|
|
146
|
+
index += 1;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (lineStart < text.length) {
|
|
150
|
+
lines.push(text.slice(lineStart));
|
|
151
|
+
}
|
|
90
152
|
return lines.length ? lines : [''];
|
|
91
|
-
}
|
|
153
|
+
};
|
|
92
154
|
|
|
93
|
-
export const lineContent = (line: string) => line.replace(/(?:\r\n|\r|\n)
|
|
155
|
+
export const lineContent = (line: string) => line.replace(/(?:\r\n|\r|\n)$/u, '');
|
|
94
156
|
|
|
95
|
-
export function sourceRange(text: string, from: number, to: number) {
|
|
157
|
+
export const sourceRange = function sourceRange(text: string, from: number, to: number) {
|
|
96
158
|
const selected = rawMarkdownLines(text).slice(from - 1, to);
|
|
97
159
|
return selected.slice(0, -1).join('') + lineContent(selected.at(-1) ?? '');
|
|
98
|
-
}
|
|
160
|
+
};
|
package/src/model/connection.ts
CHANGED
|
@@ -1,26 +1,39 @@
|
|
|
1
|
-
import { createInterface
|
|
2
|
-
import type { Readable, Writable } from 'node:stream';
|
|
1
|
+
import { createInterface } from 'node:readline';
|
|
3
2
|
import { z } from 'zod';
|
|
3
|
+
import { AppServerRpcError, RemoteErrorSchema } from './rpc-error.ts';
|
|
4
|
+
import type { Interface } from 'node:readline';
|
|
5
|
+
import type { Readable, Writable } from 'node:stream';
|
|
6
|
+
|
|
7
|
+
export { AppServerRpcError } from './rpc-error.ts';
|
|
8
|
+
|
|
9
|
+
const observeRejection = async (promise: Promise<unknown>): Promise<void> => {
|
|
10
|
+
try {
|
|
11
|
+
await promise;
|
|
12
|
+
} catch {
|
|
13
|
+
// The rejection is handled by the caller through the public promise.
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const createEnding = () => {
|
|
18
|
+
const ending = Promise.withResolvers<never>();
|
|
19
|
+
void observeRejection(ending.promise);
|
|
20
|
+
return ending;
|
|
21
|
+
};
|
|
4
22
|
|
|
5
23
|
const IdSchema = z.union([z.string(), z.number().int()]);
|
|
6
|
-
const RemoteErrorSchema = z.looseObject({
|
|
7
|
-
code: z.number().int(),
|
|
8
|
-
message: z.string(),
|
|
9
|
-
data: z.unknown().optional(),
|
|
10
|
-
});
|
|
11
24
|
const FrameSchema = z.looseObject({
|
|
25
|
+
error: RemoteErrorSchema.optional(),
|
|
12
26
|
id: IdSchema.optional(),
|
|
13
27
|
method: z.string().optional(),
|
|
14
28
|
params: z.unknown().optional(),
|
|
15
29
|
result: z.unknown().optional(),
|
|
16
|
-
error: RemoteErrorSchema.optional(),
|
|
17
30
|
});
|
|
18
31
|
type Frame = z.infer<typeof FrameSchema>;
|
|
19
32
|
|
|
20
33
|
interface ConnectionOptions {
|
|
21
34
|
readonly input: Writable;
|
|
22
35
|
readonly output: Readable;
|
|
23
|
-
readonly onNotification: (method: string,
|
|
36
|
+
readonly onNotification: (method: string, parameters: unknown) => void;
|
|
24
37
|
readonly onInteractiveRequest?: (method: string) => void;
|
|
25
38
|
}
|
|
26
39
|
interface RequestOptions {
|
|
@@ -34,87 +47,113 @@ interface PendingRequest {
|
|
|
34
47
|
readonly cleanup: () => void;
|
|
35
48
|
}
|
|
36
49
|
|
|
37
|
-
|
|
38
|
-
readonly
|
|
39
|
-
readonly data:
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
super(error.message);
|
|
43
|
-
this.name = 'AppServerRpcError';
|
|
44
|
-
this.code = error.code;
|
|
45
|
-
this.data = error.data;
|
|
46
|
-
}
|
|
50
|
+
interface ConnectionListeners {
|
|
51
|
+
readonly close: () => void;
|
|
52
|
+
readonly data: (chunk: Buffer) => void;
|
|
53
|
+
readonly error: (error: Error) => void;
|
|
54
|
+
readonly line: (line: string) => void;
|
|
47
55
|
}
|
|
48
56
|
|
|
49
57
|
export class AppServerConnection {
|
|
50
58
|
private readonly options: ConnectionOptions;
|
|
51
59
|
private readonly reader: Interface;
|
|
52
60
|
private readonly pending = new Map<string | number, PendingRequest>();
|
|
53
|
-
private readonly ending =
|
|
61
|
+
private readonly ending = createEnding();
|
|
54
62
|
private failure: Error | undefined;
|
|
63
|
+
private readonly listeners: ConnectionListeners;
|
|
55
64
|
private nextId = 1;
|
|
56
65
|
private trailingBytes = 0;
|
|
57
66
|
private streamBytes = 0;
|
|
58
|
-
readonly closed = this.ending.promise;
|
|
59
67
|
|
|
60
68
|
constructor(options: ConnectionOptions) {
|
|
61
69
|
this.options = options;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
70
|
+
this.listeners = {
|
|
71
|
+
close: this.receiveClose.bind(this),
|
|
72
|
+
data: this.measureChunk.bind(this),
|
|
73
|
+
error: this.receiveStreamError.bind(this),
|
|
74
|
+
line: this.receiveLine.bind(this),
|
|
75
|
+
};
|
|
76
|
+
options.output.on('data', this.listeners.data);
|
|
77
|
+
this.reader = createInterface({
|
|
78
|
+
crlfDelay: Infinity,
|
|
79
|
+
input: options.output,
|
|
80
|
+
});
|
|
81
|
+
this.reader.on('line', this.listeners.line);
|
|
82
|
+
this.reader.on('close', this.listeners.close);
|
|
83
|
+
options.input.on('error', this.listeners.error);
|
|
84
|
+
options.output.on('error', this.listeners.error);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
get closed(): Promise<never> {
|
|
88
|
+
return this.ending.promise;
|
|
69
89
|
}
|
|
70
90
|
|
|
71
|
-
request(
|
|
72
|
-
|
|
73
|
-
|
|
91
|
+
async request(
|
|
92
|
+
method: string,
|
|
93
|
+
parameters: unknown,
|
|
94
|
+
options: RequestOptions = {}
|
|
95
|
+
): Promise<unknown> {
|
|
96
|
+
if (this.failure !== undefined) {
|
|
97
|
+
throw this.failure;
|
|
98
|
+
}
|
|
99
|
+
if (options.signal?.aborted === true) {
|
|
100
|
+
throw new Error('request cancelled');
|
|
101
|
+
}
|
|
74
102
|
const timeout = options.timeoutMilliseconds ?? 30_000;
|
|
75
103
|
if (!Number.isSafeInteger(timeout) || timeout <= 0) {
|
|
76
|
-
|
|
104
|
+
throw new Error('request timeout must be a positive integer');
|
|
77
105
|
}
|
|
78
|
-
const id = this.nextId
|
|
106
|
+
const id = this.nextId;
|
|
107
|
+
this.nextId += 1;
|
|
79
108
|
const result = Promise.withResolvers<unknown>();
|
|
80
|
-
const abort = () =>
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
109
|
+
const abort = (): void => {
|
|
110
|
+
this.rejectRequest(id, new Error('request cancelled'));
|
|
111
|
+
};
|
|
112
|
+
const timer = setTimeout(() => {
|
|
113
|
+
this.rejectRequest(id, new Error(`request timed out: ${method}`));
|
|
114
|
+
}, timeout);
|
|
85
115
|
this.pending.set(id, {
|
|
86
|
-
resolve: result.resolve,
|
|
87
|
-
reject: result.reject,
|
|
88
116
|
cleanup: () => {
|
|
89
117
|
clearTimeout(timer);
|
|
90
118
|
options.signal?.removeEventListener('abort', abort);
|
|
91
119
|
},
|
|
120
|
+
reject: result.reject,
|
|
121
|
+
resolve: result.resolve,
|
|
92
122
|
});
|
|
93
123
|
options.signal?.addEventListener('abort', abort, { once: true });
|
|
94
|
-
this.send({ id, method, params });
|
|
95
|
-
return result.promise;
|
|
124
|
+
this.send({ id, method, params: parameters });
|
|
125
|
+
return await result.promise;
|
|
96
126
|
}
|
|
97
127
|
|
|
98
|
-
notify(method: string,
|
|
99
|
-
if (this.failure !== undefined)
|
|
100
|
-
|
|
128
|
+
notify(method: string, parameters?: unknown): void {
|
|
129
|
+
if (this.failure !== undefined) {
|
|
130
|
+
throw this.failure;
|
|
131
|
+
}
|
|
132
|
+
this.send({ method, params: parameters });
|
|
101
133
|
}
|
|
102
134
|
|
|
103
135
|
dispose(): void {
|
|
104
136
|
this.fail(new Error('connection closed'));
|
|
105
|
-
this.reader.off('line', this.
|
|
106
|
-
this.reader.off('close', this.
|
|
137
|
+
this.reader.off('line', this.listeners.line);
|
|
138
|
+
this.reader.off('close', this.listeners.close);
|
|
107
139
|
this.reader.close();
|
|
108
|
-
this.options.input.off('error', this.
|
|
109
|
-
this.options.output.off('error', this.
|
|
110
|
-
this.options.output.off('data', this.
|
|
140
|
+
this.options.input.off('error', this.listeners.error);
|
|
141
|
+
this.options.output.off('error', this.listeners.error);
|
|
142
|
+
this.options.output.off('data', this.listeners.data);
|
|
111
143
|
}
|
|
112
144
|
|
|
113
|
-
|
|
114
|
-
|
|
145
|
+
protected receiveClose(): void {
|
|
146
|
+
this.fail(new Error('connection closed'));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
protected receiveStreamError(error: Error): void {
|
|
150
|
+
this.fail(error);
|
|
151
|
+
}
|
|
115
152
|
|
|
116
|
-
|
|
117
|
-
if (this.failure)
|
|
153
|
+
protected measureChunk(chunk: Buffer): void {
|
|
154
|
+
if (this.failure !== undefined) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
118
157
|
this.streamBytes += chunk.byteLength;
|
|
119
158
|
if (this.streamBytes > 33_554_432) {
|
|
120
159
|
this.fail(new Error('app-server stream exceeded 32 MiB'));
|
|
@@ -131,40 +170,52 @@ export class AppServerConnection {
|
|
|
131
170
|
offset = newline + 1;
|
|
132
171
|
}
|
|
133
172
|
this.trailingBytes += chunk.byteLength - offset;
|
|
134
|
-
if (this.trailingBytes > 4_194_304)
|
|
135
|
-
|
|
173
|
+
if (this.trailingBytes > 4_194_304) {
|
|
174
|
+
this.fail(new Error('app-server frame exceeded 4 MiB'));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
136
177
|
|
|
137
|
-
|
|
138
|
-
if (this.failure !== undefined)
|
|
178
|
+
protected receiveLine(line: string): void {
|
|
179
|
+
if (this.failure !== undefined) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
139
182
|
try {
|
|
140
183
|
const frame = FrameSchema.parse(JSON.parse(line) as unknown);
|
|
141
184
|
this.receiveFrame(frame);
|
|
142
185
|
} catch (error: unknown) {
|
|
143
186
|
this.fail(new Error('invalid app-server frame', { cause: error }));
|
|
144
187
|
}
|
|
145
|
-
}
|
|
188
|
+
}
|
|
146
189
|
|
|
147
|
-
|
|
190
|
+
protected receiveFrame(frame: Frame): void {
|
|
148
191
|
if (frame.method !== undefined) {
|
|
149
192
|
if (Object.hasOwn(frame, 'result') || frame.error !== undefined) {
|
|
150
193
|
throw new Error('a method frame cannot also be a response');
|
|
151
194
|
}
|
|
152
|
-
if (frame.id === undefined)
|
|
153
|
-
|
|
195
|
+
if (frame.id === undefined) {
|
|
196
|
+
this.options.onNotification(frame.method, frame.params);
|
|
197
|
+
} else {
|
|
198
|
+
this.declineInteractiveRequest(frame.id, frame.method);
|
|
199
|
+
}
|
|
154
200
|
return;
|
|
155
201
|
}
|
|
156
202
|
if (frame.id === undefined || Object.hasOwn(frame, 'result') === (frame.error !== undefined)) {
|
|
157
203
|
throw new Error('a response requires an id and exactly one outcome');
|
|
158
204
|
}
|
|
159
205
|
const pending = this.pending.get(frame.id);
|
|
160
|
-
if (pending === undefined)
|
|
206
|
+
if (pending === undefined) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
161
209
|
this.pending.delete(frame.id);
|
|
162
210
|
pending.cleanup();
|
|
163
|
-
if (frame.error
|
|
164
|
-
|
|
211
|
+
if (frame.error === undefined) {
|
|
212
|
+
pending.resolve(frame.result);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
pending.reject(new AppServerRpcError(frame.error));
|
|
165
216
|
}
|
|
166
217
|
|
|
167
|
-
|
|
218
|
+
protected declineInteractiveRequest(id: string | number, method: string): void {
|
|
168
219
|
this.options.onInteractiveRequest?.(method);
|
|
169
220
|
if (
|
|
170
221
|
method === 'item/commandExecution/requestApproval' ||
|
|
@@ -174,34 +225,41 @@ export class AppServerConnection {
|
|
|
174
225
|
return;
|
|
175
226
|
}
|
|
176
227
|
this.send({
|
|
228
|
+
error: {
|
|
229
|
+
code: -32_601,
|
|
230
|
+
message: 'interactive requests are outside this pilot',
|
|
231
|
+
},
|
|
177
232
|
id,
|
|
178
|
-
error: { code: -32601, message: 'interactive requests are outside this pilot' },
|
|
179
233
|
});
|
|
180
234
|
}
|
|
181
235
|
|
|
182
|
-
|
|
236
|
+
protected send(frame: unknown): void {
|
|
183
237
|
try {
|
|
184
|
-
this.options.input.write(`${JSON.stringify(frame)}\n
|
|
185
|
-
if (error !== null && error !== undefined) this.fail(error);
|
|
186
|
-
});
|
|
238
|
+
this.options.input.write(`${JSON.stringify(frame)}\n`);
|
|
187
239
|
} catch (error: unknown) {
|
|
188
240
|
this.fail(new Error('app-server write failed', { cause: error }));
|
|
189
241
|
}
|
|
190
242
|
}
|
|
191
243
|
|
|
192
|
-
|
|
244
|
+
protected rejectRequest(id: string | number, error: Error): void {
|
|
193
245
|
const pending = this.pending.get(id);
|
|
194
|
-
if (pending === undefined)
|
|
246
|
+
if (pending === undefined) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
195
249
|
this.pending.delete(id);
|
|
196
250
|
pending.cleanup();
|
|
197
251
|
pending.reject(error);
|
|
198
252
|
}
|
|
199
253
|
|
|
200
|
-
|
|
201
|
-
if (this.failure !== undefined)
|
|
254
|
+
protected fail(error: Error): void {
|
|
255
|
+
if (this.failure !== undefined) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
202
258
|
this.failure = error;
|
|
203
259
|
this.reader.close();
|
|
204
|
-
for (const id of this.pending.keys())
|
|
260
|
+
for (const id of this.pending.keys()) {
|
|
261
|
+
this.rejectRequest(id, error);
|
|
262
|
+
}
|
|
205
263
|
this.ending.reject(error);
|
|
206
264
|
}
|
|
207
265
|
}
|
package/src/model/failure.ts
CHANGED
|
@@ -1,33 +1,56 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { AppServerRpcError } from './
|
|
2
|
+
import { AppServerRpcError } from './rpc-error.ts';
|
|
3
3
|
import type { ProfileEvidence } from './profile.ts';
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
class ServerAdmissionFailureError extends Error {
|
|
6
|
+
name = 'ServerAdmissionFailureError';
|
|
7
|
+
readonly admission: (ProfileEvidence & { launchPolicyHash: string }) | undefined;
|
|
6
8
|
readonly cleanup: 'confirmed' | 'failed';
|
|
7
9
|
readonly processId: number;
|
|
8
|
-
|
|
9
|
-
constructor(
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
10
|
+
|
|
11
|
+
constructor(
|
|
12
|
+
parameters: {
|
|
13
|
+
admission?: ProfileEvidence & { launchPolicyHash: string };
|
|
14
|
+
cause: unknown;
|
|
15
|
+
cleanup: 'confirmed' | 'failed';
|
|
16
|
+
processId: number;
|
|
17
|
+
},
|
|
18
|
+
options?: ErrorOptions
|
|
19
|
+
) {
|
|
20
|
+
super('Native server admission failed', options);
|
|
21
|
+
Object.defineProperty(this, 'cause', {
|
|
22
|
+
configurable: true,
|
|
23
|
+
enumerable: false,
|
|
24
|
+
value: parameters.cause,
|
|
25
|
+
writable: true,
|
|
26
|
+
});
|
|
27
|
+
this.admission = parameters.admission;
|
|
28
|
+
this.cleanup = parameters.cleanup;
|
|
29
|
+
this.processId = parameters.processId;
|
|
19
30
|
}
|
|
20
31
|
}
|
|
21
32
|
|
|
22
|
-
export
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
33
|
+
export { ServerAdmissionFailureError as ServerAdmissionFailure };
|
|
34
|
+
|
|
35
|
+
export const failureDiagnostic = (error: unknown): Record<string, unknown> => {
|
|
36
|
+
let current = error;
|
|
37
|
+
while (current instanceof ServerAdmissionFailureError) {
|
|
38
|
+
current = current.cause;
|
|
39
|
+
}
|
|
40
|
+
if (current instanceof z.ZodError) {
|
|
41
|
+
const diagnosticKind = { kind: 'invalid-native-response' };
|
|
42
|
+
const diagnosticFields = {
|
|
43
|
+
fields: current.issues.map((issue) => issue.path.join('.')),
|
|
28
44
|
};
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
45
|
+
return { ...diagnosticKind, ...diagnosticFields };
|
|
46
|
+
}
|
|
47
|
+
if (current instanceof AppServerRpcError) {
|
|
48
|
+
const diagnosticKind = { kind: 'rpc-rejection' };
|
|
49
|
+
const diagnosticCode = { code: current.code };
|
|
50
|
+
return { ...diagnosticKind, ...diagnosticCode };
|
|
51
|
+
}
|
|
52
|
+
if (Error.isError(current) && current.constructor === Error) {
|
|
53
|
+
return { kind: 'native-admission', message: current.message };
|
|
54
|
+
}
|
|
32
55
|
return { kind: 'native-failure' };
|
|
33
|
-
}
|
|
56
|
+
};
|