@linkshell/wire 0.2.21 → 0.2.23
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/acp.d.ts +68 -430
- package/dist/acp.js +5 -5
- package/dist/acp.js.map +1 -1
- package/dist/async-questions.d.ts +2 -10
- package/dist/computer-preview.d.ts +1 -15
- package/dist/computer-preview.js +1 -1
- package/dist/computer-preview.js.map +1 -1
- package/dist/content.d.ts +211 -376
- package/dist/content.js +4 -4
- package/dist/content.js.map +1 -1
- package/dist/editor.d.ts +48 -592
- package/dist/envelopes.d.ts +53 -0
- package/dist/envelopes.js +263 -0
- package/dist/envelopes.js.map +1 -0
- package/dist/goal.d.ts +15 -48
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/live-activity.d.ts +83 -212
- package/dist/live-activity.js +1 -1
- package/dist/live-activity.js.map +1 -1
- package/dist/model.d.ts +466 -2289
- package/dist/model.js +24 -0
- package/dist/model.js.map +1 -1
- package/dist/relay.d.ts +41 -267
- package/dist/relay.js +1 -1
- package/dist/relay.js.map +1 -1
- package/dist/rpc-mcp.d.ts +84 -0
- package/dist/rpc-mcp.js +78 -0
- package/dist/rpc-mcp.js.map +1 -0
- package/dist/rpc.d.ts +6983 -31165
- package/dist/rpc.js +35 -7
- package/dist/rpc.js.map +1 -1
- package/dist/side.d.ts +13 -61
- package/dist/updates.d.ts +1827 -6736
- package/dist/updates.js +14 -1
- package/dist/updates.js.map +1 -1
- package/dist/usage.d.ts +17 -195
- package/dist/watch.d.ts +223 -1141
- package/dist/workflow.d.ts +46 -139
- package/package.json +2 -2
- package/src/acp.ts +5 -5
- package/src/computer-preview.ts +1 -1
- package/src/content.ts +5 -5
- package/src/envelopes.ts +288 -0
- package/src/index.ts +2 -0
- package/src/live-activity.ts +1 -1
- package/src/model.ts +24 -0
- package/src/relay.ts +1 -1
- package/src/rpc-mcp.ts +81 -0
- package/src/rpc.ts +35 -7
- package/src/updates.ts +15 -1
package/dist/workflow.d.ts
CHANGED
|
@@ -1,14 +1,35 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
export declare const workflowStateSchema: z.ZodEnum<
|
|
2
|
+
export declare const workflowStateSchema: z.ZodEnum<{
|
|
3
|
+
paused: "paused";
|
|
4
|
+
running: "running";
|
|
5
|
+
completed: "completed";
|
|
6
|
+
failed: "failed";
|
|
7
|
+
stopped: "stopped";
|
|
8
|
+
}>;
|
|
3
9
|
export type WorkflowState = z.infer<typeof workflowStateSchema>;
|
|
4
|
-
export declare const workflowAgentStateSchema: z.ZodEnum<
|
|
10
|
+
export declare const workflowAgentStateSchema: z.ZodEnum<{
|
|
11
|
+
unknown: "unknown";
|
|
12
|
+
paused: "paused";
|
|
13
|
+
running: "running";
|
|
14
|
+
completed: "completed";
|
|
15
|
+
failed: "failed";
|
|
16
|
+
stopped: "stopped";
|
|
17
|
+
pending: "pending";
|
|
18
|
+
}>;
|
|
5
19
|
export type WorkflowAgentState = z.infer<typeof workflowAgentStateSchema>;
|
|
6
20
|
export declare const workflowAgentSchema: z.ZodObject<{
|
|
7
21
|
id: z.ZodString;
|
|
8
|
-
/** Present only after the host has imported this agent's transcript. */
|
|
9
22
|
toolCallId: z.ZodOptional<z.ZodString>;
|
|
10
23
|
title: z.ZodString;
|
|
11
|
-
state: z.ZodEnum<
|
|
24
|
+
state: z.ZodEnum<{
|
|
25
|
+
unknown: "unknown";
|
|
26
|
+
paused: "paused";
|
|
27
|
+
running: "running";
|
|
28
|
+
completed: "completed";
|
|
29
|
+
failed: "failed";
|
|
30
|
+
stopped: "stopped";
|
|
31
|
+
pending: "pending";
|
|
32
|
+
}>;
|
|
12
33
|
phaseId: z.ZodOptional<z.ZodString>;
|
|
13
34
|
model: z.ZodOptional<z.ZodString>;
|
|
14
35
|
tokens: z.ZodOptional<z.ZodNumber>;
|
|
@@ -17,54 +38,25 @@ export declare const workflowAgentSchema: z.ZodObject<{
|
|
|
17
38
|
startedAt: z.ZodOptional<z.ZodNumber>;
|
|
18
39
|
endedAt: z.ZodOptional<z.ZodNumber>;
|
|
19
40
|
result: z.ZodOptional<z.ZodString>;
|
|
20
|
-
},
|
|
21
|
-
id: string;
|
|
22
|
-
title: string;
|
|
23
|
-
state: "unknown" | "paused" | "running" | "completed" | "failed" | "stopped" | "pending";
|
|
24
|
-
toolCallId?: string | undefined;
|
|
25
|
-
phaseId?: string | undefined;
|
|
26
|
-
model?: string | undefined;
|
|
27
|
-
tokens?: number | undefined;
|
|
28
|
-
toolCalls?: number | undefined;
|
|
29
|
-
durationMs?: number | undefined;
|
|
30
|
-
startedAt?: number | undefined;
|
|
31
|
-
endedAt?: number | undefined;
|
|
32
|
-
result?: string | undefined;
|
|
33
|
-
}, {
|
|
34
|
-
id: string;
|
|
35
|
-
title: string;
|
|
36
|
-
state: "unknown" | "paused" | "running" | "completed" | "failed" | "stopped" | "pending";
|
|
37
|
-
toolCallId?: string | undefined;
|
|
38
|
-
phaseId?: string | undefined;
|
|
39
|
-
model?: string | undefined;
|
|
40
|
-
tokens?: number | undefined;
|
|
41
|
-
toolCalls?: number | undefined;
|
|
42
|
-
durationMs?: number | undefined;
|
|
43
|
-
startedAt?: number | undefined;
|
|
44
|
-
endedAt?: number | undefined;
|
|
45
|
-
result?: string | undefined;
|
|
46
|
-
}>;
|
|
41
|
+
}, z.core.$strip>;
|
|
47
42
|
export type WorkflowAgent = z.infer<typeof workflowAgentSchema>;
|
|
48
43
|
export declare const workflowPhaseSchema: z.ZodObject<{
|
|
49
44
|
id: z.ZodString;
|
|
50
45
|
title: z.ZodString;
|
|
51
46
|
order: z.ZodNumber;
|
|
52
|
-
},
|
|
53
|
-
id: string;
|
|
54
|
-
title: string;
|
|
55
|
-
order: number;
|
|
56
|
-
}, {
|
|
57
|
-
id: string;
|
|
58
|
-
title: string;
|
|
59
|
-
order: number;
|
|
60
|
-
}>;
|
|
47
|
+
}, z.core.$strip>;
|
|
61
48
|
export type WorkflowPhase = z.infer<typeof workflowPhaseSchema>;
|
|
62
49
|
/** A complete run snapshot, independent of the loaded conversation window. */
|
|
63
50
|
export declare const workflowSchema: z.ZodObject<{
|
|
64
51
|
runId: z.ZodOptional<z.ZodString>;
|
|
65
52
|
name: z.ZodOptional<z.ZodString>;
|
|
66
|
-
state: z.ZodOptional<z.ZodEnum<
|
|
67
|
-
|
|
53
|
+
state: z.ZodOptional<z.ZodEnum<{
|
|
54
|
+
paused: "paused";
|
|
55
|
+
running: "running";
|
|
56
|
+
completed: "completed";
|
|
57
|
+
failed: "failed";
|
|
58
|
+
stopped: "stopped";
|
|
59
|
+
}>>;
|
|
68
60
|
started: z.ZodOptional<z.ZodNumber>;
|
|
69
61
|
completed: z.ZodOptional<z.ZodNumber>;
|
|
70
62
|
tokens: z.ZodOptional<z.ZodNumber>;
|
|
@@ -75,21 +67,20 @@ export declare const workflowSchema: z.ZodObject<{
|
|
|
75
67
|
id: z.ZodString;
|
|
76
68
|
title: z.ZodString;
|
|
77
69
|
order: z.ZodNumber;
|
|
78
|
-
},
|
|
79
|
-
id: string;
|
|
80
|
-
title: string;
|
|
81
|
-
order: number;
|
|
82
|
-
}, {
|
|
83
|
-
id: string;
|
|
84
|
-
title: string;
|
|
85
|
-
order: number;
|
|
86
|
-
}>, "many">>;
|
|
70
|
+
}, z.core.$strip>>>;
|
|
87
71
|
agents: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
88
72
|
id: z.ZodString;
|
|
89
|
-
/** Present only after the host has imported this agent's transcript. */
|
|
90
73
|
toolCallId: z.ZodOptional<z.ZodString>;
|
|
91
74
|
title: z.ZodString;
|
|
92
|
-
state: z.ZodEnum<
|
|
75
|
+
state: z.ZodEnum<{
|
|
76
|
+
unknown: "unknown";
|
|
77
|
+
paused: "paused";
|
|
78
|
+
running: "running";
|
|
79
|
+
completed: "completed";
|
|
80
|
+
failed: "failed";
|
|
81
|
+
stopped: "stopped";
|
|
82
|
+
pending: "pending";
|
|
83
|
+
}>;
|
|
93
84
|
phaseId: z.ZodOptional<z.ZodString>;
|
|
94
85
|
model: z.ZodOptional<z.ZodString>;
|
|
95
86
|
tokens: z.ZodOptional<z.ZodNumber>;
|
|
@@ -98,90 +89,6 @@ export declare const workflowSchema: z.ZodObject<{
|
|
|
98
89
|
startedAt: z.ZodOptional<z.ZodNumber>;
|
|
99
90
|
endedAt: z.ZodOptional<z.ZodNumber>;
|
|
100
91
|
result: z.ZodOptional<z.ZodString>;
|
|
101
|
-
},
|
|
102
|
-
|
|
103
|
-
title: string;
|
|
104
|
-
state: "unknown" | "paused" | "running" | "completed" | "failed" | "stopped" | "pending";
|
|
105
|
-
toolCallId?: string | undefined;
|
|
106
|
-
phaseId?: string | undefined;
|
|
107
|
-
model?: string | undefined;
|
|
108
|
-
tokens?: number | undefined;
|
|
109
|
-
toolCalls?: number | undefined;
|
|
110
|
-
durationMs?: number | undefined;
|
|
111
|
-
startedAt?: number | undefined;
|
|
112
|
-
endedAt?: number | undefined;
|
|
113
|
-
result?: string | undefined;
|
|
114
|
-
}, {
|
|
115
|
-
id: string;
|
|
116
|
-
title: string;
|
|
117
|
-
state: "unknown" | "paused" | "running" | "completed" | "failed" | "stopped" | "pending";
|
|
118
|
-
toolCallId?: string | undefined;
|
|
119
|
-
phaseId?: string | undefined;
|
|
120
|
-
model?: string | undefined;
|
|
121
|
-
tokens?: number | undefined;
|
|
122
|
-
toolCalls?: number | undefined;
|
|
123
|
-
durationMs?: number | undefined;
|
|
124
|
-
startedAt?: number | undefined;
|
|
125
|
-
endedAt?: number | undefined;
|
|
126
|
-
result?: string | undefined;
|
|
127
|
-
}>, "many">>;
|
|
128
|
-
}, "strip", z.ZodTypeAny, {
|
|
129
|
-
name?: string | undefined;
|
|
130
|
-
completed?: number | undefined;
|
|
131
|
-
state?: "paused" | "running" | "completed" | "failed" | "stopped" | undefined;
|
|
132
|
-
tokens?: number | undefined;
|
|
133
|
-
durationMs?: number | undefined;
|
|
134
|
-
startedAt?: number | undefined;
|
|
135
|
-
endedAt?: number | undefined;
|
|
136
|
-
runId?: string | undefined;
|
|
137
|
-
started?: number | undefined;
|
|
138
|
-
phases?: {
|
|
139
|
-
id: string;
|
|
140
|
-
title: string;
|
|
141
|
-
order: number;
|
|
142
|
-
}[] | undefined;
|
|
143
|
-
agents?: {
|
|
144
|
-
id: string;
|
|
145
|
-
title: string;
|
|
146
|
-
state: "unknown" | "paused" | "running" | "completed" | "failed" | "stopped" | "pending";
|
|
147
|
-
toolCallId?: string | undefined;
|
|
148
|
-
phaseId?: string | undefined;
|
|
149
|
-
model?: string | undefined;
|
|
150
|
-
tokens?: number | undefined;
|
|
151
|
-
toolCalls?: number | undefined;
|
|
152
|
-
durationMs?: number | undefined;
|
|
153
|
-
startedAt?: number | undefined;
|
|
154
|
-
endedAt?: number | undefined;
|
|
155
|
-
result?: string | undefined;
|
|
156
|
-
}[] | undefined;
|
|
157
|
-
}, {
|
|
158
|
-
name?: string | undefined;
|
|
159
|
-
completed?: number | undefined;
|
|
160
|
-
state?: "paused" | "running" | "completed" | "failed" | "stopped" | undefined;
|
|
161
|
-
tokens?: number | undefined;
|
|
162
|
-
durationMs?: number | undefined;
|
|
163
|
-
startedAt?: number | undefined;
|
|
164
|
-
endedAt?: number | undefined;
|
|
165
|
-
runId?: string | undefined;
|
|
166
|
-
started?: number | undefined;
|
|
167
|
-
phases?: {
|
|
168
|
-
id: string;
|
|
169
|
-
title: string;
|
|
170
|
-
order: number;
|
|
171
|
-
}[] | undefined;
|
|
172
|
-
agents?: {
|
|
173
|
-
id: string;
|
|
174
|
-
title: string;
|
|
175
|
-
state: "unknown" | "paused" | "running" | "completed" | "failed" | "stopped" | "pending";
|
|
176
|
-
toolCallId?: string | undefined;
|
|
177
|
-
phaseId?: string | undefined;
|
|
178
|
-
model?: string | undefined;
|
|
179
|
-
tokens?: number | undefined;
|
|
180
|
-
toolCalls?: number | undefined;
|
|
181
|
-
durationMs?: number | undefined;
|
|
182
|
-
startedAt?: number | undefined;
|
|
183
|
-
endedAt?: number | undefined;
|
|
184
|
-
result?: string | undefined;
|
|
185
|
-
}[] | undefined;
|
|
186
|
-
}>;
|
|
92
|
+
}, z.core.$strip>>>;
|
|
93
|
+
}, z.core.$strip>;
|
|
187
94
|
export type Workflow = z.infer<typeof workflowSchema>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linkshell/wire",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.23",
|
|
4
4
|
"description": "Wire protocol for LinkShell v2: session model, JSON-RPC methods, end-to-end encryption and the relay client.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"@noble/ciphers": "^2.4.0",
|
|
30
30
|
"@noble/curves": "^1.9.7",
|
|
31
31
|
"@noble/hashes": "^1.8.0",
|
|
32
|
-
"zod": "^
|
|
32
|
+
"zod": "^4.6.7"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"vitest": "^3.2.1"
|
package/src/acp.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
-
const env = z.record(z.string());
|
|
3
|
+
const env = z.record(z.string(), z.string());
|
|
4
4
|
export const mcpServerSchema = z.discriminatedUnion("type", [
|
|
5
5
|
z.object({ type: z.literal("stdio"), name: z.string().min(1), command: z.string().min(1), args: z.array(z.string()).default([]), env: env.default({}) }),
|
|
6
|
-
z.object({ type: z.literal("http"), name: z.string().min(1), url: z.
|
|
7
|
-
z.object({ type: z.literal("sse"), name: z.string().min(1), url: z.
|
|
6
|
+
z.object({ type: z.literal("http"), name: z.string().min(1), url: z.url(), headers: env.default({}) }),
|
|
7
|
+
z.object({ type: z.literal("sse"), name: z.string().min(1), url: z.url(), headers: env.default({}) }),
|
|
8
8
|
z.object({ type: z.literal("acp"), name: z.string().min(1), command: z.string().min(1), args: z.array(z.string()).default([]), env: env.default({}) }),
|
|
9
9
|
]);
|
|
10
10
|
export type AcpMcpServer = z.infer<typeof mcpServerSchema>;
|
|
@@ -23,10 +23,10 @@ export const acpRemoteAgentSchema = z.object({
|
|
|
23
23
|
label: z.string().min(1).max(80),
|
|
24
24
|
transport: z.enum(["stdio", "http", "websocket"]),
|
|
25
25
|
command: z.string().optional(), args: z.array(z.string()).default([]),
|
|
26
|
-
url: z.
|
|
26
|
+
url: z.url().optional(),
|
|
27
27
|
/** Header name to environment-variable name. The UI does not store secrets. */
|
|
28
28
|
headerEnv: env.default({}),
|
|
29
|
-
settings: acpAgentSettingsSchema.
|
|
29
|
+
settings: acpAgentSettingsSchema.prefault({}),
|
|
30
30
|
});
|
|
31
31
|
export type AcpRemoteAgent = z.infer<typeof acpRemoteAgentSchema>;
|
|
32
32
|
|
package/src/computer-preview.ts
CHANGED
|
@@ -4,7 +4,7 @@ export const PREVIEW_MAX_BYTES = 48 * 1024;
|
|
|
4
4
|
export const previewFrameSchema = z.object({
|
|
5
5
|
id: z.string().regex(/^[a-f0-9]{64}$/),
|
|
6
6
|
target: z.string().max(1024),
|
|
7
|
-
capturedAt: z.number().
|
|
7
|
+
capturedAt: z.number().nonnegative(),
|
|
8
8
|
width: z.number().int().positive().max(2048),
|
|
9
9
|
height: z.number().int().positive().max(2048),
|
|
10
10
|
mimeType: z.literal("image/webp"),
|
package/src/content.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
-
const optionalString = z.string().nullish().transform((value) => value ?? undefined);
|
|
4
|
-
const optionalMeta = z.record(z.unknown()).nullish().transform((value) => value ?? undefined);
|
|
3
|
+
const optionalString = z.string().nullish().transform((value) => value ?? undefined).optional();
|
|
4
|
+
const optionalMeta = z.record(z.string(), z.unknown()).nullish().transform((value) => value ?? undefined).optional();
|
|
5
5
|
const contentMetadata = {
|
|
6
|
-
annotations: z.
|
|
6
|
+
annotations: z.looseObject({ audience: z.array(z.enum(["user", "assistant"])).nullish().transform((value) => value ?? undefined).optional(), priority: z.number().nullish().transform((value) => value ?? undefined).optional(), lastModified: optionalString }).nullish().transform((value) => value ?? undefined).optional(),
|
|
7
7
|
_meta: optionalMeta,
|
|
8
8
|
};
|
|
9
9
|
const contentVariants = z.discriminatedUnion("type", [
|
|
@@ -28,7 +28,7 @@ const contentVariants = z.discriminatedUnion("type", [
|
|
|
28
28
|
title: optionalString,
|
|
29
29
|
description: optionalString,
|
|
30
30
|
mimeType: optionalString,
|
|
31
|
-
size: z.number().nullish().transform((value) => value ?? undefined),
|
|
31
|
+
size: z.number().nullish().transform((value) => value ?? undefined).optional(),
|
|
32
32
|
...contentMetadata,
|
|
33
33
|
/** What the link names: a file (default), a skill the user invoked, or another agent. */
|
|
34
34
|
kind: z.enum(["file", "skill", "agent"]).optional(),
|
|
@@ -36,7 +36,7 @@ const contentVariants = z.discriminatedUnion("type", [
|
|
|
36
36
|
]);
|
|
37
37
|
export type ContentBlock = z.infer<typeof contentVariants>;
|
|
38
38
|
// Hide the nested validator implementation from every exported RPC declaration.
|
|
39
|
-
export const contentBlockSchema: z.ZodType<ContentBlock,
|
|
39
|
+
export const contentBlockSchema: z.ZodType<ContentBlock, unknown> = contentVariants;
|
|
40
40
|
|
|
41
41
|
export const toolCallStatusSchema = z.enum(["pending", "in_progress", "completed", "failed"]);
|
|
42
42
|
export type ToolCallStatus = z.infer<typeof toolCallStatusSchema>;
|
package/src/envelopes.ts
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
// What agents and their desktop clients wrap around a person's words. Claude Code adds reminders
|
|
2
|
+
// and IDE state, the Claude app adds session events and peer mail, Codex Desktop adds the files,
|
|
3
|
+
// selection and browser state it attached: context for the model, not what the person typed.
|
|
4
|
+
// Readers show the words, and the context as a chip or an event line where it means something to
|
|
5
|
+
// them, the way Claude's and Codex's own apps do. Shared by the host (titles, previews, what it
|
|
6
|
+
// keeps from Claude's transcripts) and every client.
|
|
7
|
+
|
|
8
|
+
/** Tags whose whole content is for the model only: the Claude app's own list, then Codex's. */
|
|
9
|
+
const HIDDEN_TAGS = [
|
|
10
|
+
"system-reminder",
|
|
11
|
+
"task-notification",
|
|
12
|
+
"slack_context",
|
|
13
|
+
"uploaded_files",
|
|
14
|
+
"available-deferred-tools",
|
|
15
|
+
"new-diagnostics",
|
|
16
|
+
"critical_user_preferences_reminder",
|
|
17
|
+
"persisted-output",
|
|
18
|
+
"local-command-caveat",
|
|
19
|
+
"tick",
|
|
20
|
+
"goal",
|
|
21
|
+
"mcp-resource",
|
|
22
|
+
"mcp-resource-update",
|
|
23
|
+
"mcp-polling-update",
|
|
24
|
+
"teammate-message",
|
|
25
|
+
"cross-session-message",
|
|
26
|
+
"launch-selected-element",
|
|
27
|
+
"connected_office_files",
|
|
28
|
+
"thinking",
|
|
29
|
+
"ide_opened_file",
|
|
30
|
+
"ide_selection",
|
|
31
|
+
"ide_diagnostics",
|
|
32
|
+
"browser_instruction",
|
|
33
|
+
"cell",
|
|
34
|
+
"cell_type",
|
|
35
|
+
"language",
|
|
36
|
+
"command-message",
|
|
37
|
+
"command-args",
|
|
38
|
+
"no-action-needed",
|
|
39
|
+
"comprehensive-mode-instructions",
|
|
40
|
+
"cu_window_hints",
|
|
41
|
+
"widget_context_hint",
|
|
42
|
+
"preview-annotation-context",
|
|
43
|
+
"terminal-selection",
|
|
44
|
+
"environment_context",
|
|
45
|
+
"user_instructions",
|
|
46
|
+
"turn_aborted",
|
|
47
|
+
"subagent_notification",
|
|
48
|
+
"external_codex_apps_open_page",
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const hiddenPattern = (tags: string[]) => new RegExp(`<(${tags.join("|")})(?:\\s[^>]*)?>[\\s\\S]*?</\\1>`, "gi");
|
|
52
|
+
const HIDDEN = hiddenPattern(HIDDEN_TAGS);
|
|
53
|
+
/** Mail between sessions and agents: hidden in a person's bubble, but a message of its own when it is the whole message. */
|
|
54
|
+
const PEER_TAGS = ["teammate-message", "cross-session-message"];
|
|
55
|
+
const HIDDEN_BUT_PEERS = hiddenPattern(HIDDEN_TAGS.filter((tag) => !PEER_TAGS.includes(tag)));
|
|
56
|
+
|
|
57
|
+
/** Something attached to a message, shown as a chip beside the words. */
|
|
58
|
+
export interface UserContext {
|
|
59
|
+
kind: "file" | "image" | "pasted" | "selection" | "comments" | "checks" | "annotations" | "browser" | "review" | "pull-request" | "merge" | "history" | "other";
|
|
60
|
+
label: string;
|
|
61
|
+
/** A path, or the start of the attached text. */
|
|
62
|
+
detail?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** A message that is an event, not something said: one line in the conversation. */
|
|
66
|
+
export type UserEvent =
|
|
67
|
+
| { kind: "ci"; count: number }
|
|
68
|
+
| { kind: "github"; count: number }
|
|
69
|
+
| { kind: "sessions"; sessions: { id?: string; title: string }[] }
|
|
70
|
+
| { kind: "scheduled"; name: string; detail?: string };
|
|
71
|
+
|
|
72
|
+
export interface UserTextView {
|
|
73
|
+
/** The words to show; empty when the message is only context, an event or a command's output. */
|
|
74
|
+
text: string;
|
|
75
|
+
context: UserContext[];
|
|
76
|
+
/** A shell command run from the prompt (Claude's `!` mode), and what it printed. */
|
|
77
|
+
shell?: { command?: string; stdout?: string; stderr?: string };
|
|
78
|
+
/** What a local slash command printed (`/cost`, `/context`). */
|
|
79
|
+
output?: string;
|
|
80
|
+
event?: UserEvent;
|
|
81
|
+
/** Nothing in it is for a reader. */
|
|
82
|
+
hidden: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The text without what is for the model only; `keepPeers` keeps mail from other sessions and agents. */
|
|
86
|
+
export function stripHiddenContext(text: string, { keepPeers = false }: { keepPeers?: boolean } = {}): string {
|
|
87
|
+
return text
|
|
88
|
+
.replace(/\r\n/g, "\n")
|
|
89
|
+
.replace(keepPeers ? HIDDEN_BUT_PEERS : HIDDEN, "")
|
|
90
|
+
.replace(/<\/?tool_use_error>/g, "")
|
|
91
|
+
// Claude's pointers to a background task's files, for itself.
|
|
92
|
+
.replace(/^(?:Full transcript available at|Read the output file to retrieve the result):.*$/gm, "")
|
|
93
|
+
.replace(/\s*\[event:[\da-f-]{36}\]\s*$/i, "")
|
|
94
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
95
|
+
.trim();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// eslint-disable-next-line no-control-regex -- terminal colour codes start with ESC.
|
|
99
|
+
const ANSI = /\u001b\[[\d;?]*[ -/]*[@-~]/g;
|
|
100
|
+
|
|
101
|
+
function tagText(source: string, tag: string): string | undefined {
|
|
102
|
+
const match = new RegExp(`<${tag}(?:\\s[^>]*)?>([\\s\\S]*?)</${tag}>`, "i").exec(source);
|
|
103
|
+
return match ? match[1]! : undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function attribute(attrs: string, name: string): string | undefined {
|
|
107
|
+
const match = new RegExp(`\\b${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)')`, "i").exec(attrs);
|
|
108
|
+
return match ? (match[1] ?? match[2]) : undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const EVENT_TAGS = ["side-sessions-event", "ci-monitor-event", "github-webhook-activity"] as const;
|
|
112
|
+
const EVENT_PATTERNS = new Map(EVENT_TAGS.map((tag) => [tag, new RegExp(`<${tag}\\b[^>]*>[\\s\\S]*?</${tag}>`, "gi")]));
|
|
113
|
+
|
|
114
|
+
/** A message made only of one kind of tag, repeated: the count, else undefined. */
|
|
115
|
+
function only(text: string, tag: (typeof EVENT_TAGS)[number]): number | undefined {
|
|
116
|
+
if (!text.startsWith(`<${tag}`)) return undefined;
|
|
117
|
+
const pattern = EVENT_PATTERNS.get(tag)!;
|
|
118
|
+
const all = text.match(pattern);
|
|
119
|
+
if (!all) return undefined;
|
|
120
|
+
return text.replace(pattern, "").trim() ? undefined : all.length;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Codex Desktop's sections before "## My request:", by heading. */
|
|
124
|
+
const CONTEXT_SECTIONS: Record<string, UserContext["kind"]> = {
|
|
125
|
+
"Files mentioned by the user": "file",
|
|
126
|
+
"Files pasted by the user": "pasted",
|
|
127
|
+
"Selected text": "selection",
|
|
128
|
+
"Diff comments": "comments",
|
|
129
|
+
"Browser comments": "comments",
|
|
130
|
+
"Failing PR checks": "checks",
|
|
131
|
+
"Response annotations": "annotations",
|
|
132
|
+
};
|
|
133
|
+
const MODES: Record<string, [UserContext["kind"], string]> = {
|
|
134
|
+
"Code review guidelines": ["review", "审查模式"],
|
|
135
|
+
"Pull request fix": ["pull-request", "PR 修复"],
|
|
136
|
+
"Auto resolve merge": ["merge", "自动解决冲突"],
|
|
137
|
+
"Prior conversation with Codex": ["history", "参考历史对话"],
|
|
138
|
+
};
|
|
139
|
+
const SECTION_LABELS: Record<string, string> = {
|
|
140
|
+
"Selected text": "选中的文本",
|
|
141
|
+
"Diff comments": "差异评论",
|
|
142
|
+
"Browser comments": "浏览器评论",
|
|
143
|
+
"Failing PR checks": "失败的 PR 检查",
|
|
144
|
+
"Response annotations": "回复批注",
|
|
145
|
+
};
|
|
146
|
+
const REQUEST = /^## My request(?: for Codex)?:[ \t]*$/gm;
|
|
147
|
+
|
|
148
|
+
const baseName = (path: string) => path.split(/[\\/]/).filter(Boolean).at(-1) ?? path;
|
|
149
|
+
|
|
150
|
+
function codexContext(text: string, images: number): { text: string; context: UserContext[] } | undefined {
|
|
151
|
+
const start = /^(#{1,2}) ([^\n:]+):[ \t]*$/m.exec(text);
|
|
152
|
+
// Only an envelope Codex wrote: it opens the message with one of its own headings.
|
|
153
|
+
if (!start || start.index !== 0) return undefined;
|
|
154
|
+
const heading = start[2]!;
|
|
155
|
+
if (!(start[1] === "#" ? heading in CONTEXT_SECTIONS : heading in MODES || /^My request(?: for Codex)?$/.test(heading))) return undefined;
|
|
156
|
+
let cut = -1;
|
|
157
|
+
let after = 0;
|
|
158
|
+
for (const match of text.matchAll(REQUEST)) {
|
|
159
|
+
cut = match.index!;
|
|
160
|
+
after = match.index! + match[0].length;
|
|
161
|
+
}
|
|
162
|
+
const preface = cut < 0 ? text : text.slice(0, cut);
|
|
163
|
+
const request = cut < 0 ? "" : text.slice(after).trim();
|
|
164
|
+
const context: UserContext[] = [];
|
|
165
|
+
let imageEntries = 0;
|
|
166
|
+
const sections = preface.split(/^(?=#{1,2} [^\n:]+:[ \t]*$)/m);
|
|
167
|
+
for (const section of sections) {
|
|
168
|
+
const head = /^(#{1,2}) ([^\n:]+):[ \t]*\n?/.exec(section);
|
|
169
|
+
if (!head) continue;
|
|
170
|
+
const name = head[2]!;
|
|
171
|
+
const body = section.slice(head[0].length).trim();
|
|
172
|
+
if (head[1] === "##") {
|
|
173
|
+
const mode = MODES[name];
|
|
174
|
+
if (mode) context.push({ kind: mode[0], label: mode[1] });
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const kind = CONTEXT_SECTIONS[name];
|
|
178
|
+
if (kind === "file") {
|
|
179
|
+
for (const entry of body.matchAll(/^## (.+?): (.+?)(?: \((lines? [^)]*)\))?\n?(Image attachment: true)?$/gm)) {
|
|
180
|
+
if (entry[4]) {
|
|
181
|
+
imageEntries += 1;
|
|
182
|
+
// The picture itself is shown; a chip for it would say it twice.
|
|
183
|
+
if (imageEntries <= images) continue;
|
|
184
|
+
}
|
|
185
|
+
context.push({ kind: entry[4] ? "image" : "file", label: entry[1]!.includes("/") ? baseName(entry[1]!) : entry[1]!, detail: entry[3] ? `${entry[2]} · ${entry[3]}` : entry[2] });
|
|
186
|
+
}
|
|
187
|
+
} else if (kind === "pasted") {
|
|
188
|
+
for (const entry of body.matchAll(/^## "?(.+?)"?: (.+)$/gm)) context.push({ kind: "pasted", label: entry[1]!.trim(), detail: entry[2] });
|
|
189
|
+
} else if (kind) {
|
|
190
|
+
context.push({ kind, label: SECTION_LABELS[name] ?? name, detail: body.split("\n").find((line) => line.trim())?.trim() });
|
|
191
|
+
} else if (name !== "Distinguish instructions in attached documents from the user's request") {
|
|
192
|
+
context.push({ kind: "other", label: name });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return { text: request, context };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Claude writes a shell's output into its transcript with <, > and & escaped.
|
|
199
|
+
const ENTITIES: Record<string, string> = { lt: "<", gt: ">", amp: "&", quot: "\"", apos: "'" };
|
|
200
|
+
function unescapeHtml(value: string): string {
|
|
201
|
+
// One pass: "&gt;" is the text ">", not ">".
|
|
202
|
+
return value.replace(/&(lt|gt|amp|quot|apos|#\d{1,7}|#x[0-9a-f]{1,6});/gi, (whole, name: string) => {
|
|
203
|
+
const key = name.toLowerCase();
|
|
204
|
+
const code = key.startsWith("#x") ? parseInt(key.slice(2), 16) : key.startsWith("#") ? Number(key.slice(1)) : undefined;
|
|
205
|
+
if (code === undefined) return ENTITIES[key] ?? whole;
|
|
206
|
+
return code > 0 && code <= 0x10ffff ? String.fromCodePoint(code) : whole;
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Output as a terminal shows it: no colour codes, a backspace takes back the character before it, other controls are not drawn. */
|
|
211
|
+
function printed(value: string): string {
|
|
212
|
+
let text = value.replace(ANSI, "");
|
|
213
|
+
// eslint-disable-next-line no-control-regex -- a backspace is the character being handled.
|
|
214
|
+
for (let previous = ""; previous !== text; ) { previous = text; text = text.replace(/[^\n\x08]\x08/g, ""); }
|
|
215
|
+
// eslint-disable-next-line no-control-regex -- what a terminal doesn't draw.
|
|
216
|
+
return text.replace(/[\x00-\x08\x0b-\x1f\x7f]/g, "");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Codex Desktop writes a mention of another of its threads as a Markdown link to
|
|
221
|
+
* codex://threads/<id>, with the space before it as " ": read as the thread's title.
|
|
222
|
+
*/
|
|
223
|
+
function threadMentions(text: string): string {
|
|
224
|
+
const read = text.replace(/&#(?:x20|32);/gi, " ").replace(/\[([^\]\n]+)\]\(codex:\/\/threads\/[\w-]+\)/g, "「$1」");
|
|
225
|
+
return read === text ? text : read.trim();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* How to show a person's message: its words, what came with it, or the one event it is. `images`
|
|
230
|
+
* is how many pictures the message carries besides its text, so an envelope naming them doesn't
|
|
231
|
+
* also chip them.
|
|
232
|
+
*/
|
|
233
|
+
export function readUserText(raw: string, { images = 0 }: { images?: number } = {}): UserTextView {
|
|
234
|
+
let text = raw.replace(/\r\n/g, "\n");
|
|
235
|
+
const context: UserContext[] = [];
|
|
236
|
+
const view = (rest: Partial<UserTextView>): UserTextView => ({ text: "", context, hidden: false, ...rest });
|
|
237
|
+
|
|
238
|
+
// The Claude app tells a session about the sessions it started in a reminder beside the event.
|
|
239
|
+
const sessions = [...text.matchAll(/The session "([^"\n]+)" you started \((local_[\w-]+)\)/g)].map((match) => ({ title: match[1]!, id: match[2] }));
|
|
240
|
+
const browser = /<in-app-browser-context\b[^>]*>[\s\S]*?<\/in-app-browser-context>/gi;
|
|
241
|
+
const withoutBrowser = text.replace(browser, "");
|
|
242
|
+
if (withoutBrowser !== text) {
|
|
243
|
+
context.push({ kind: "browser", label: "浏览器上下文" });
|
|
244
|
+
text = withoutBrowser;
|
|
245
|
+
}
|
|
246
|
+
text = stripHiddenContext(text);
|
|
247
|
+
|
|
248
|
+
if (only(text, "side-sessions-event")) return view({ event: { kind: "sessions", sessions } });
|
|
249
|
+
if (!text) return view({ hidden: images === 0 && context.length === 0 });
|
|
250
|
+
if (/^\[Request interrupted by user(?: for tool use)?\]$/.test(text)) return view({ hidden: true });
|
|
251
|
+
const ci = only(text, "ci-monitor-event");
|
|
252
|
+
if (ci) return view({ event: { kind: "ci", count: ci } });
|
|
253
|
+
const github = only(text, "github-webhook-activity");
|
|
254
|
+
if (github) return view({ event: { kind: "github", count: github } });
|
|
255
|
+
const scheduled = /^<scheduled-task\b([^>]*)>([\s\S]*?)<\/scheduled-task>$/i.exec(text);
|
|
256
|
+
if (scheduled) {
|
|
257
|
+
const name = attribute(scheduled[1]!, "name") ?? baseName(attribute(scheduled[1]!, "file") ?? "").replace(/\.md$/i, "");
|
|
258
|
+
return view({ event: { kind: "scheduled", name: name || "定时任务", detail: scheduled[2]!.trim() || undefined } });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (/^<bash-(?:input|stdout|stderr)>/.test(text)) {
|
|
262
|
+
const clean = (value?: string) => (value === undefined ? undefined : printed(unescapeHtml(value)).replace(/\n+$/, "")) || undefined;
|
|
263
|
+
const command = clean(tagText(text, "bash-input"))?.trim();
|
|
264
|
+
const stdout = clean(tagText(text, "bash-stdout"));
|
|
265
|
+
const stderr = clean(tagText(text, "bash-stderr"));
|
|
266
|
+
const rest = text.replace(/<(bash-(?:input|stdout|stderr|exit-code))>[\s\S]*?<\/\1>/g, "").trim();
|
|
267
|
+
return view({ shell: { command, stdout, stderr }, text: rest, hidden: !command && !stdout && !stderr && !rest });
|
|
268
|
+
}
|
|
269
|
+
if (/^<local-command-(?:stdout|stderr)>/.test(text)) {
|
|
270
|
+
const raw = [tagText(text, "local-command-stdout"), tagText(text, "local-command-stderr")]
|
|
271
|
+
.filter((part): part is string => !!part?.trim())
|
|
272
|
+
.join("\n")
|
|
273
|
+
.replace(ANSI, "");
|
|
274
|
+
const output = unescapeHtml(raw).trim();
|
|
275
|
+
// /model's confirmation: the Claude app doesn't show the switch as a line either.
|
|
276
|
+
return view({ output: output || undefined, hidden: !output || /^Set model to\b/.test(output) });
|
|
277
|
+
}
|
|
278
|
+
// Slash-command plumbing the host couldn't read as a command, and notifications it didn't match.
|
|
279
|
+
if (/^<(?:command-name|local-command-|task-notification)/.test(text)) return view({ hidden: true });
|
|
280
|
+
|
|
281
|
+
const codex = codexContext(text, images);
|
|
282
|
+
if (codex) {
|
|
283
|
+
context.push(...codex.context);
|
|
284
|
+
const words = threadMentions(codex.text);
|
|
285
|
+
return view({ text: words, hidden: !words && images === 0 && context.length === 0 });
|
|
286
|
+
}
|
|
287
|
+
return view({ text: threadMentions(text) });
|
|
288
|
+
}
|
package/src/index.ts
CHANGED
package/src/live-activity.ts
CHANGED
|
@@ -42,7 +42,7 @@ export interface LiveActivityPayload extends Partial<LiveActivityContentState> {
|
|
|
42
42
|
export const liveActivityIdSchema = z.string().min(1).max(128).regex(/^[a-zA-Z0-9-]+$/);
|
|
43
43
|
export const liveActivitySubscriptionSchema = z.object({
|
|
44
44
|
activityId: liveActivityIdSchema,
|
|
45
|
-
serviceUrl: z.
|
|
45
|
+
serviceUrl: z.url().max(256),
|
|
46
46
|
grant: z.string().regex(/^[a-f0-9]{64}$/),
|
|
47
47
|
expiresAt: z.number().int().positive(),
|
|
48
48
|
computerKey: z.string().min(1).max(512),
|
package/src/model.ts
CHANGED
|
@@ -254,6 +254,25 @@ export const sessionSummarySchema = z.object({
|
|
|
254
254
|
* (the project the session belongs to).
|
|
255
255
|
*/
|
|
256
256
|
worktree: z.object({ branch: z.string(), source: z.string() }).optional(),
|
|
257
|
+
/**
|
|
258
|
+
* The LinkShell session that started this one (e.g. a Claude desktop
|
|
259
|
+
* session's start_session or hand-off): the sidebar nests it there. Set
|
|
260
|
+
* only when this host has that session too. Absent from older hosts.
|
|
261
|
+
*/
|
|
262
|
+
parentId: z.string().optional(),
|
|
263
|
+
/**
|
|
264
|
+
* The LinkShell session this one was forked from (made here with
|
|
265
|
+
* `sessions.fork`, or recorded by the agent), when this host has it. Kept
|
|
266
|
+
* apart from `parentId`: a fork is a copy, not something that session
|
|
267
|
+
* started, so whether it nests is the list's choice.
|
|
268
|
+
*/
|
|
269
|
+
forkedFrom: z.string().optional(),
|
|
270
|
+
/**
|
|
271
|
+
* The id an app of the agent's knows the session by (the Claude desktop
|
|
272
|
+
* app's `local_…`): mail between its sessions names the sender by it, so a
|
|
273
|
+
* client can open the sender. Absent from older hosts.
|
|
274
|
+
*/
|
|
275
|
+
appId: z.string().optional(),
|
|
257
276
|
/** Live only, for sessions that started sub-agents: how many, and how many are working now (`sessions.subagents` lists them). */
|
|
258
277
|
subagents: z.object({ total: z.number().int(), running: z.number().int() }).optional(),
|
|
259
278
|
/** Live only, for sessions that left commands running: how many, and how many still run (`sessions.tasks` lists them). */
|
|
@@ -363,6 +382,11 @@ export const terminalInfoSchema = z.object({
|
|
|
363
382
|
exitCode: z.number().int().nullable().optional(),
|
|
364
383
|
/** Ended without an exit code: the host stopped while it ran. */
|
|
365
384
|
interrupted: z.boolean().optional(),
|
|
385
|
+
/**
|
|
386
|
+
* The session it was opened for. Such a terminal belongs to the session: it closes when its
|
|
387
|
+
* shell exits or the session is archived or deleted, and isn't kept across a host restart.
|
|
388
|
+
*/
|
|
389
|
+
sessionId: z.string().optional(),
|
|
366
390
|
});
|
|
367
391
|
export type TerminalInfo = z.infer<typeof terminalInfoSchema>;
|
|
368
392
|
|
package/src/relay.ts
CHANGED
|
@@ -74,7 +74,7 @@ export const serverFrameSchema = z.discriminatedUnion("t", [
|
|
|
74
74
|
result: z.unknown().optional(),
|
|
75
75
|
error: z.object({ code: z.string(), message: z.string() }).optional(),
|
|
76
76
|
}),
|
|
77
|
-
z.object({ t: z.literal("event"), name: z.string(), data: z.unknown() }),
|
|
77
|
+
z.object({ t: z.literal("event"), name: z.string(), data: z.unknown().optional() }),
|
|
78
78
|
/** An encrypted frame from a peer, with the keys the gateway verified it holds. */
|
|
79
79
|
z.object({ t: z.literal("from"), from: publicIdentitySchema, via: z.enum(["paired", "account"]), d: z.string() }),
|
|
80
80
|
/** A frame couldn't be delivered: the peer is offline or not reachable. */
|