linkshell-cli 0.5.42 → 0.6.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/dist/cli/src/commands/host.d.ts +23 -0
- package/dist/cli/src/commands/host.js +264 -0
- package/dist/cli/src/commands/host.js.map +1 -0
- package/dist/cli/src/commands/pair.d.ts +1 -0
- package/dist/cli/src/commands/pair.js +48 -0
- package/dist/cli/src/commands/pair.js.map +1 -0
- package/dist/cli/src/index.js +68 -0
- package/dist/cli/src/index.js.map +1 -1
- package/dist/cli/src/utils/daemon.d.ts +1 -1
- package/dist/cli/src/utils/daemon.js +3 -2
- package/dist/cli/src/utils/daemon.js.map +1 -1
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/dist/host/src/drivers/acp/connection.d.ts +61 -0
- package/dist/host/src/drivers/acp/connection.js +121 -0
- package/dist/host/src/drivers/acp/connection.js.map +1 -0
- package/dist/host/src/drivers/acp/driver.d.ts +110 -0
- package/dist/host/src/drivers/acp/driver.js +438 -0
- package/dist/host/src/drivers/acp/driver.js.map +1 -0
- package/dist/host/src/drivers/acp/mapper.d.ts +49 -0
- package/dist/host/src/drivers/acp/mapper.js +411 -0
- package/dist/host/src/drivers/acp/mapper.js.map +1 -0
- package/dist/host/src/drivers/auth.d.ts +7 -0
- package/dist/host/src/drivers/auth.js +57 -0
- package/dist/host/src/drivers/auth.js.map +1 -0
- package/dist/host/src/drivers/claude/driver.d.ts +76 -0
- package/dist/host/src/drivers/claude/driver.js +331 -0
- package/dist/host/src/drivers/claude/driver.js.map +1 -0
- package/dist/host/src/drivers/claude/transcript.d.ts +335 -0
- package/dist/host/src/drivers/claude/transcript.js +516 -0
- package/dist/host/src/drivers/claude/transcript.js.map +1 -0
- package/dist/host/src/drivers/codex/app-server.d.ts +36 -0
- package/dist/host/src/drivers/codex/app-server.js +144 -0
- package/dist/host/src/drivers/codex/app-server.js.map +1 -0
- package/dist/host/src/drivers/codex/driver.d.ts +89 -0
- package/dist/host/src/drivers/codex/driver.js +452 -0
- package/dist/host/src/drivers/codex/driver.js.map +1 -0
- package/dist/host/src/drivers/codex/mapper.d.ts +61 -0
- package/dist/host/src/drivers/codex/mapper.js +623 -0
- package/dist/host/src/drivers/codex/mapper.js.map +1 -0
- package/dist/host/src/drivers/codex/settings.d.ts +51 -0
- package/dist/host/src/drivers/codex/settings.js +107 -0
- package/dist/host/src/drivers/codex/settings.js.map +1 -0
- package/dist/host/src/drivers/images.d.ts +11 -0
- package/dist/host/src/drivers/images.js +34 -0
- package/dist/host/src/drivers/images.js.map +1 -0
- package/dist/host/src/drivers/nesting.d.ts +6 -0
- package/dist/host/src/drivers/nesting.js +25 -0
- package/dist/host/src/drivers/nesting.js.map +1 -0
- package/dist/host/src/drivers/registry.d.ts +21 -0
- package/dist/host/src/drivers/registry.js +30 -0
- package/dist/host/src/drivers/registry.js.map +1 -0
- package/dist/host/src/drivers/types.d.ts +105 -0
- package/dist/host/src/drivers/types.js +2 -0
- package/dist/host/src/drivers/types.js.map +1 -0
- package/dist/host/src/fs.d.ts +69 -0
- package/dist/host/src/fs.js +198 -0
- package/dist/host/src/fs.js.map +1 -0
- package/dist/host/src/gateway.d.ts +55 -0
- package/dist/host/src/gateway.js +263 -0
- package/dist/host/src/gateway.js.map +1 -0
- package/dist/host/src/host.d.ts +51 -0
- package/dist/host/src/host.js +133 -0
- package/dist/host/src/host.js.map +1 -0
- package/dist/host/src/hub.d.ts +89 -0
- package/dist/host/src/hub.js +592 -0
- package/dist/host/src/hub.js.map +1 -0
- package/dist/host/src/index.d.ts +10 -0
- package/dist/host/src/index.js +10 -0
- package/dist/host/src/index.js.map +1 -0
- package/dist/host/src/ports.d.ts +24 -0
- package/dist/host/src/ports.js +201 -0
- package/dist/host/src/ports.js.map +1 -0
- package/dist/host/src/rpc/client.d.ts +9 -0
- package/dist/host/src/rpc/client.js +42 -0
- package/dist/host/src/rpc/client.js.map +1 -0
- package/dist/host/src/rpc/server.d.ts +52 -0
- package/dist/host/src/rpc/server.js +293 -0
- package/dist/host/src/rpc/server.js.map +1 -0
- package/dist/host/src/screen.d.ts +30 -0
- package/dist/host/src/screen.js +230 -0
- package/dist/host/src/screen.js.map +1 -0
- package/dist/host/src/shell-env.d.ts +8 -0
- package/dist/host/src/shell-env.js +39 -0
- package/dist/host/src/shell-env.js.map +1 -0
- package/dist/host/src/store.d.ts +75 -0
- package/dist/host/src/store.js +265 -0
- package/dist/host/src/store.js.map +1 -0
- package/dist/host/src/terminals.d.ts +55 -0
- package/dist/host/src/terminals.js +367 -0
- package/dist/host/src/terminals.js.map +1 -0
- package/dist/shared-protocol/src/index.d.ts +3345 -3345
- package/dist/wire/src/crypto.d.ts +65 -0
- package/dist/wire/src/crypto.js +253 -0
- package/dist/wire/src/crypto.js.map +1 -0
- package/dist/wire/src/index.d.ts +8 -0
- package/dist/wire/src/index.js +9 -0
- package/dist/wire/src/index.js.map +1 -0
- package/dist/wire/src/model.d.ts +704 -0
- package/dist/wire/src/model.js +163 -0
- package/dist/wire/src/model.js.map +1 -0
- package/dist/wire/src/peer.d.ts +42 -0
- package/dist/wire/src/peer.js +143 -0
- package/dist/wire/src/peer.js.map +1 -0
- package/dist/wire/src/relay-client.d.ts +82 -0
- package/dist/wire/src/relay-client.js +259 -0
- package/dist/wire/src/relay-client.js.map +1 -0
- package/dist/wire/src/relay.d.ts +481 -0
- package/dist/wire/src/relay.js +129 -0
- package/dist/wire/src/relay.js.map +1 -0
- package/dist/wire/src/rpc.d.ts +4170 -0
- package/dist/wire/src/rpc.js +332 -0
- package/dist/wire/src/rpc.js.map +1 -0
- package/dist/wire/src/shell.d.ts +2 -0
- package/dist/wire/src/shell.js +62 -0
- package/dist/wire/src/shell.js.map +1 -0
- package/dist/wire/src/updates.d.ts +3325 -0
- package/dist/wire/src/updates.js +210 -0
- package/dist/wire/src/updates.js.map +1 -0
- package/package.json +8 -3
- package/src/commands/host.ts +283 -0
- package/src/commands/pair.ts +47 -0
- package/src/index.ts +73 -0
- package/src/utils/daemon.ts +4 -3
- package/dist/cli/src/commands/hook.d.ts +0 -6
- package/dist/cli/src/commands/hook.js +0 -120
- package/dist/cli/src/commands/hook.js.map +0 -1
- package/dist/cli/src/runtime/acp/agent-workspace-git.d.ts +0 -63
- package/dist/cli/src/runtime/acp/agent-workspace-git.js +0 -297
- package/dist/cli/src/runtime/acp/agent-workspace-git.js.map +0 -1
- package/dist/cli/src/runtime/acp-relay.d.ts +0 -23
- package/dist/cli/src/runtime/acp-relay.js +0 -73
- package/dist/cli/src/runtime/acp-relay.js.map +0 -1
- package/dist/cli/src/runtime/hook-permission-server.d.ts +0 -36
- package/dist/cli/src/runtime/hook-permission-server.js +0 -147
- package/dist/cli/src/runtime/hook-permission-server.js.map +0 -1
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { permissionOptionSchema, sessionDriverSchema, sessionStateSchema, toolKindSchema } from "./model.js";
|
|
3
|
+
// Session updates mirror ACP's `SessionUpdate` shapes (discriminated by
|
|
4
|
+
// `sessionUpdate`) so one renderer works for every agent. LinkShell-specific
|
|
5
|
+
// updates use the `ls_` prefix.
|
|
6
|
+
export const contentBlockSchema = z.discriminatedUnion("type", [
|
|
7
|
+
z.object({ type: z.literal("text"), text: z.string() }),
|
|
8
|
+
z.object({
|
|
9
|
+
type: z.literal("image"),
|
|
10
|
+
mimeType: z.string(),
|
|
11
|
+
data: z.string().optional(),
|
|
12
|
+
uri: z.string().optional(),
|
|
13
|
+
}),
|
|
14
|
+
z.object({
|
|
15
|
+
type: z.literal("resource_link"),
|
|
16
|
+
uri: z.string(),
|
|
17
|
+
name: z.string(),
|
|
18
|
+
/** What the link names: a file (default), a skill the user invoked, or another agent. */
|
|
19
|
+
kind: z.enum(["file", "skill", "agent"]).optional(),
|
|
20
|
+
}),
|
|
21
|
+
]);
|
|
22
|
+
export const toolCallStatusSchema = z.enum(["pending", "in_progress", "completed", "failed"]);
|
|
23
|
+
export const toolCallContentSchema = z.discriminatedUnion("type", [
|
|
24
|
+
z.object({ type: z.literal("content"), content: contentBlockSchema }),
|
|
25
|
+
/** ACP diff: full before/after text. */
|
|
26
|
+
z.object({
|
|
27
|
+
type: z.literal("diff"),
|
|
28
|
+
path: z.string(),
|
|
29
|
+
oldText: z.string().nullable().optional(),
|
|
30
|
+
newText: z.string(),
|
|
31
|
+
}),
|
|
32
|
+
/** Unified diff, for agents (Codex) that report patches rather than file text. */
|
|
33
|
+
z.object({
|
|
34
|
+
type: z.literal("patch"),
|
|
35
|
+
path: z.string(),
|
|
36
|
+
change: z.enum(["add", "delete", "update"]),
|
|
37
|
+
movePath: z.string().optional(),
|
|
38
|
+
diff: z.string(),
|
|
39
|
+
}),
|
|
40
|
+
z.object({ type: z.literal("terminal"), terminalId: z.string() }),
|
|
41
|
+
]);
|
|
42
|
+
export const toolCallLocationSchema = z.object({
|
|
43
|
+
path: z.string(),
|
|
44
|
+
line: z.number().int().optional(),
|
|
45
|
+
});
|
|
46
|
+
/**
|
|
47
|
+
* What a tool call is, when the agent tells us more than ACP's coarse `kind`.
|
|
48
|
+
* Host drivers fill it from each agent's own tool names; clients render it the
|
|
49
|
+
* same way for every agent and fall back to `title` + `kind` without it.
|
|
50
|
+
*/
|
|
51
|
+
export const toolDetailSchema = z.discriminatedUnion("type", [
|
|
52
|
+
z.object({ type: z.literal("mcp"), server: z.string(), tool: z.string() }),
|
|
53
|
+
z.object({
|
|
54
|
+
type: z.literal("subagent"),
|
|
55
|
+
/** spawn starts a sub-agent (its work nests under this call); the rest talk to one. */
|
|
56
|
+
action: z.enum(["spawn", "message", "wait", "stop", "resume", "list"]).default("spawn"),
|
|
57
|
+
task: z.string().optional(),
|
|
58
|
+
/** The sub-agent's role or type, e.g. "Explore". */
|
|
59
|
+
agentType: z.string().optional(),
|
|
60
|
+
model: z.string().optional(),
|
|
61
|
+
}),
|
|
62
|
+
z.object({ type: z.literal("skill"), name: z.string(), description: z.string().optional() }),
|
|
63
|
+
z.object({ type: z.literal("web_search"), query: z.string().optional() }),
|
|
64
|
+
z.object({ type: z.literal("image_generation"), prompt: z.string().optional() }),
|
|
65
|
+
z.object({ type: z.literal("compaction") }),
|
|
66
|
+
z.object({ type: z.literal("review"), phase: z.enum(["started", "finished"]) }),
|
|
67
|
+
]);
|
|
68
|
+
export const planEntrySchema = z.object({
|
|
69
|
+
content: z.string(),
|
|
70
|
+
priority: z.enum(["high", "medium", "low"]).default("medium"),
|
|
71
|
+
status: z.enum(["pending", "in_progress", "completed"]),
|
|
72
|
+
});
|
|
73
|
+
export const stopReasonSchema = z.enum(["end_turn", "cancelled", "error", "max_tokens", "refusal"]);
|
|
74
|
+
export const sessionConfigOptionSchema = z.object({
|
|
75
|
+
/** Agent-defined id; "model" and "mode" are the common ones. */
|
|
76
|
+
id: z.string(),
|
|
77
|
+
name: z.string(),
|
|
78
|
+
category: z.enum(["model", "mode", "effort", "other"]).default("other"),
|
|
79
|
+
current: z.string(),
|
|
80
|
+
values: z.array(z.object({ value: z.string(), name: z.string(), description: z.string().optional() })),
|
|
81
|
+
});
|
|
82
|
+
export const sessionUpdateSchema = z.discriminatedUnion("sessionUpdate", [
|
|
83
|
+
z.object({
|
|
84
|
+
sessionUpdate: z.literal("user_message_chunk"),
|
|
85
|
+
messageId: z.string().optional(),
|
|
86
|
+
content: contentBlockSchema,
|
|
87
|
+
}),
|
|
88
|
+
z.object({
|
|
89
|
+
sessionUpdate: z.literal("agent_message_chunk"),
|
|
90
|
+
parentToolCallId: z.string().optional(),
|
|
91
|
+
messageId: z.string(),
|
|
92
|
+
content: contentBlockSchema,
|
|
93
|
+
}),
|
|
94
|
+
z.object({
|
|
95
|
+
sessionUpdate: z.literal("agent_thought_chunk"),
|
|
96
|
+
parentToolCallId: z.string().optional(),
|
|
97
|
+
messageId: z.string(),
|
|
98
|
+
content: contentBlockSchema,
|
|
99
|
+
}),
|
|
100
|
+
z.object({
|
|
101
|
+
sessionUpdate: z.literal("tool_call"),
|
|
102
|
+
toolCallId: z.string(),
|
|
103
|
+
/** Set when a sub-agent made this call: the sub-agent's own (spawn) tool call. */
|
|
104
|
+
parentToolCallId: z.string().optional(),
|
|
105
|
+
title: z.string(),
|
|
106
|
+
kind: toolKindSchema,
|
|
107
|
+
status: toolCallStatusSchema,
|
|
108
|
+
rawInput: z.unknown().optional(),
|
|
109
|
+
content: z.array(toolCallContentSchema).optional(),
|
|
110
|
+
locations: z.array(toolCallLocationSchema).optional(),
|
|
111
|
+
detail: toolDetailSchema.optional(),
|
|
112
|
+
}),
|
|
113
|
+
z.object({
|
|
114
|
+
sessionUpdate: z.literal("tool_call_update"),
|
|
115
|
+
toolCallId: z.string(),
|
|
116
|
+
parentToolCallId: z.string().optional(),
|
|
117
|
+
status: toolCallStatusSchema.optional(),
|
|
118
|
+
title: z.string().optional(),
|
|
119
|
+
detail: toolDetailSchema.optional(),
|
|
120
|
+
/** Replaces the tool call's content when present. */
|
|
121
|
+
content: z.array(toolCallContentSchema).optional(),
|
|
122
|
+
/** Streamed output appended to the tool call's running output. */
|
|
123
|
+
appendOutput: z.string().optional(),
|
|
124
|
+
rawOutput: z.unknown().optional(),
|
|
125
|
+
}),
|
|
126
|
+
z.object({
|
|
127
|
+
sessionUpdate: z.literal("plan"),
|
|
128
|
+
entries: z.array(planEntrySchema),
|
|
129
|
+
}),
|
|
130
|
+
z.object({
|
|
131
|
+
sessionUpdate: z.literal("available_commands_update"),
|
|
132
|
+
availableCommands: z.array(z.object({ name: z.string(), description: z.string(), hint: z.string().optional() })),
|
|
133
|
+
}),
|
|
134
|
+
z.object({
|
|
135
|
+
sessionUpdate: z.literal("current_mode_update"),
|
|
136
|
+
currentModeId: z.string(),
|
|
137
|
+
}),
|
|
138
|
+
z.object({
|
|
139
|
+
sessionUpdate: z.literal("session_info_update"),
|
|
140
|
+
title: z.string().optional(),
|
|
141
|
+
model: z.string().optional(),
|
|
142
|
+
}),
|
|
143
|
+
z.object({
|
|
144
|
+
sessionUpdate: z.literal("usage_update"),
|
|
145
|
+
usedTokens: z.number().optional(),
|
|
146
|
+
contextWindow: z.number().optional(),
|
|
147
|
+
}),
|
|
148
|
+
// ── LinkShell extensions ──
|
|
149
|
+
z.object({
|
|
150
|
+
sessionUpdate: z.literal("ls_message_done"),
|
|
151
|
+
parentToolCallId: z.string().optional(),
|
|
152
|
+
messageId: z.string(),
|
|
153
|
+
role: z.enum(["agent", "thought", "user"]),
|
|
154
|
+
}),
|
|
155
|
+
/** Selectable session settings (model, permission mode, effort…) as the agent reports them. */
|
|
156
|
+
z.object({
|
|
157
|
+
sessionUpdate: z.literal("ls_config"),
|
|
158
|
+
options: z.array(sessionConfigOptionSchema),
|
|
159
|
+
}),
|
|
160
|
+
z.object({
|
|
161
|
+
sessionUpdate: z.literal("ls_notice"),
|
|
162
|
+
level: z.enum(["info", "warning"]),
|
|
163
|
+
title: z.string(),
|
|
164
|
+
detail: z.string().optional(),
|
|
165
|
+
}),
|
|
166
|
+
z.object({
|
|
167
|
+
sessionUpdate: z.literal("ls_turn"),
|
|
168
|
+
/** A sub-agent's turn: whether the sub-agent under this call is working. */
|
|
169
|
+
parentToolCallId: z.string().optional(),
|
|
170
|
+
state: z.enum(["started", "ended"]),
|
|
171
|
+
turnId: z.string().optional(),
|
|
172
|
+
stopReason: stopReasonSchema.optional(),
|
|
173
|
+
}),
|
|
174
|
+
z.object({
|
|
175
|
+
sessionUpdate: z.literal("ls_permission"),
|
|
176
|
+
requestId: z.string(),
|
|
177
|
+
toolCallId: z.string().optional(),
|
|
178
|
+
title: z.string(),
|
|
179
|
+
detail: z.string().optional(),
|
|
180
|
+
options: z.array(permissionOptionSchema).min(1),
|
|
181
|
+
}),
|
|
182
|
+
z.object({
|
|
183
|
+
sessionUpdate: z.literal("ls_permission_resolved"),
|
|
184
|
+
requestId: z.string(),
|
|
185
|
+
optionId: z.string().optional(),
|
|
186
|
+
}),
|
|
187
|
+
z.object({
|
|
188
|
+
sessionUpdate: z.literal("ls_driver"),
|
|
189
|
+
driver: sessionDriverSchema,
|
|
190
|
+
deviceId: z.string().optional(),
|
|
191
|
+
}),
|
|
192
|
+
z.object({
|
|
193
|
+
sessionUpdate: z.literal("ls_status"),
|
|
194
|
+
state: sessionStateSchema,
|
|
195
|
+
}),
|
|
196
|
+
z.object({
|
|
197
|
+
sessionUpdate: z.literal("ls_error"),
|
|
198
|
+
code: z.string(),
|
|
199
|
+
message: z.string(),
|
|
200
|
+
hint: z.string().optional(),
|
|
201
|
+
}),
|
|
202
|
+
]);
|
|
203
|
+
/** One entry in a session's append-only event log. */
|
|
204
|
+
export const sessionEventSchema = z.object({
|
|
205
|
+
sessionId: z.string(),
|
|
206
|
+
seq: z.number().int().positive(),
|
|
207
|
+
ts: z.number(),
|
|
208
|
+
update: sessionUpdateSchema,
|
|
209
|
+
});
|
|
210
|
+
//# sourceMappingURL=updates.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"updates.js","sourceRoot":"","sources":["../../../../wire/src/updates.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "linkshell-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Remote terminal bridge for Claude Code / Codex / Gemini / Copilot — control from your phone",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -36,8 +36,10 @@
|
|
|
36
36
|
"qrcode-terminal": "^0.12.0",
|
|
37
37
|
"ws": "^8.18.1",
|
|
38
38
|
"zod": "^3.24.3",
|
|
39
|
-
"@linkshell/gateway": "0.
|
|
40
|
-
"@linkshell/protocol": "0.4.10"
|
|
39
|
+
"@linkshell/gateway": "0.5.0",
|
|
40
|
+
"@linkshell/protocol": "0.4.10",
|
|
41
|
+
"@linkshell/host": "0.1.0",
|
|
42
|
+
"@linkshell/wire": "0.1.0"
|
|
41
43
|
},
|
|
42
44
|
"optionalDependencies": {
|
|
43
45
|
"@anthropic-ai/claude-agent-sdk": "^0.2.128",
|
|
@@ -48,6 +50,9 @@
|
|
|
48
50
|
"tsx": "^4.19.3",
|
|
49
51
|
"vitest": "^3.2.1"
|
|
50
52
|
},
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">=22.13"
|
|
55
|
+
},
|
|
51
56
|
"scripts": {
|
|
52
57
|
"build": "tsc -p tsconfig.json && node scripts/copy-web.mjs",
|
|
53
58
|
"postinstall": "node scripts/postinstall.mjs",
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { getValidToken } from "../auth.js";
|
|
5
|
+
import * as daemon from "../utils/daemon.js";
|
|
6
|
+
|
|
7
|
+
// node:sqlite prints an ExperimentalWarning on load; it is expected here and
|
|
8
|
+
// only confuses users. Must run before the host (and node:sqlite) is imported.
|
|
9
|
+
function silenceSqliteWarning(): void {
|
|
10
|
+
const emit = process.emitWarning.bind(process);
|
|
11
|
+
process.emitWarning = ((warning: string | Error, ...rest: unknown[]) => {
|
|
12
|
+
const message = typeof warning === "string" ? warning : warning.message;
|
|
13
|
+
if (message.includes("SQLite")) return;
|
|
14
|
+
return (emit as (...args: unknown[]) => void)(warning, ...rest);
|
|
15
|
+
}) as typeof process.emitWarning;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface HostConfig {
|
|
19
|
+
/** v2 gateway base URL (ws:// or wss://). */
|
|
20
|
+
gateway?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function configPath(home: string): string {
|
|
24
|
+
return join(home, "config.json");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function readHostConfig(home: string): HostConfig {
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(readFileSync(configPath(home), "utf8")) as HostConfig;
|
|
30
|
+
} catch {
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function writeHostConfig(home: string, config: HostConfig): void {
|
|
36
|
+
mkdirSync(home, { recursive: true });
|
|
37
|
+
writeFileSync(configPath(home), `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The host keeps its state in node:sqlite, unflagged from Node 22.13. */
|
|
41
|
+
export function assertHostRuntime(): void {
|
|
42
|
+
const [major = 0, minor = 0] = process.versions.node.split(".").map(Number);
|
|
43
|
+
if (major > 22 || (major === 22 && minor >= 13)) return;
|
|
44
|
+
process.stderr.write(
|
|
45
|
+
`\n LinkShell's host needs Node.js 22.13 or newer (this is ${process.version}).\n` +
|
|
46
|
+
" Upgrade Node (for example: nvm install 22, or brew upgrade node), then try again.\n\n",
|
|
47
|
+
);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function loadHost() {
|
|
52
|
+
assertHostRuntime();
|
|
53
|
+
silenceSqliteWarning();
|
|
54
|
+
return import("@linkshell/host");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Runs the host in this process until SIGINT/SIGTERM. */
|
|
58
|
+
export async function runHostForeground(version: string): Promise<void> {
|
|
59
|
+
const host = await loadHost();
|
|
60
|
+
const log = (message: string) => process.stderr.write(`${new Date().toISOString()} ${message}\n`);
|
|
61
|
+
process.on("uncaughtException", (error) => log(`[host] uncaught: ${error.stack ?? error.message}`));
|
|
62
|
+
process.on("unhandledRejection", (reason) =>
|
|
63
|
+
log(`[host] unhandled rejection: ${reason instanceof Error ? (reason.stack ?? reason.message) : String(reason)}`),
|
|
64
|
+
);
|
|
65
|
+
// Started from a terminal (by `linkshell`): use that environment as is.
|
|
66
|
+
// Otherwise (launchd, a bare process) recover the login shell's.
|
|
67
|
+
const env = process.env.LINKSHELL_ENV_FROM_TERMINAL === "1" ? process.env : await host.resolveLoginShellEnv();
|
|
68
|
+
const adapter = process.env.LINKSHELL_CLAUDE_ADAPTER;
|
|
69
|
+
const devPort = process.env.LINKSHELL_DEV_PORT ? Number(process.env.LINKSHELL_DEV_PORT) : undefined;
|
|
70
|
+
// Where devices reach this machine from outside the LAN. The account from
|
|
71
|
+
// `linkshell login`, if any, lets that account's devices in without pairing.
|
|
72
|
+
const gatewayUrl = process.env.LINKSHELL_GATEWAY || readHostConfig(host.defaultHome()).gateway;
|
|
73
|
+
const running = await host.startHost({
|
|
74
|
+
version,
|
|
75
|
+
env,
|
|
76
|
+
log,
|
|
77
|
+
gateway: gatewayUrl ? { url: gatewayUrl, token: async () => (await getValidToken()) ?? undefined } : undefined,
|
|
78
|
+
tcpPort: devPort,
|
|
79
|
+
claudeCommand: process.env.LINKSHELL_CLAUDE_COMMAND || undefined,
|
|
80
|
+
claudeAdapter: adapter ? { command: adapter, args: [] } : undefined,
|
|
81
|
+
});
|
|
82
|
+
daemon.savePid("host", process.pid);
|
|
83
|
+
const info = running.machineInfo();
|
|
84
|
+
log(`[host] LinkShell host ${version} listening on ${running.paths.hostSocket}`);
|
|
85
|
+
if (devPort !== undefined) log(`[host] development API on ws://127.0.0.1:${running.server.tcpAddress() ?? devPort} (loopback only)`);
|
|
86
|
+
for (const agent of info.agents) {
|
|
87
|
+
log(
|
|
88
|
+
`[host] ${agent.label}: ${agent.installed ? `v${agent.version ?? "?"}${agent.problem ? ` — ${agent.problem}` : ""}` : "not installed"}`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
const shutdown = async () => {
|
|
92
|
+
log("[host] shutting down");
|
|
93
|
+
daemon.removePid("host");
|
|
94
|
+
await running.stop();
|
|
95
|
+
process.exit(0);
|
|
96
|
+
};
|
|
97
|
+
process.on("SIGINT", () => void shutdown());
|
|
98
|
+
process.on("SIGTERM", () => void shutdown());
|
|
99
|
+
await new Promise(() => {});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Starts the host in the background unless one already answers. */
|
|
103
|
+
export async function ensureHostRunning(): Promise<string> {
|
|
104
|
+
const host = await loadHost();
|
|
105
|
+
const { hostSocket } = host.hostPaths();
|
|
106
|
+
if (await host.isHostRunning(hostSocket)) return hostSocket;
|
|
107
|
+
daemon.spawnDaemon("host", ["host", "--_foreground-host"]);
|
|
108
|
+
const deadline = Date.now() + 20_000;
|
|
109
|
+
while (Date.now() < deadline) {
|
|
110
|
+
if (await host.isHostRunning(hostSocket)) return hostSocket;
|
|
111
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
112
|
+
}
|
|
113
|
+
throw new Error(`LinkShell host did not start; see ${daemon.getLogFile("host")}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function printHostStatus(): Promise<void> {
|
|
117
|
+
const host = await loadHost();
|
|
118
|
+
const { hostSocket } = host.hostPaths();
|
|
119
|
+
if (!(await host.isHostRunning(hostSocket))) {
|
|
120
|
+
process.stderr.write(" LinkShell host is not running. Start it with: linkshell host --daemon\n");
|
|
121
|
+
process.exitCode = 1;
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const client = await host.connectHost(hostSocket);
|
|
125
|
+
try {
|
|
126
|
+
const info = await client.call("machine.info", {});
|
|
127
|
+
const { sessions } = await client.call("sessions.list", { limit: 200 });
|
|
128
|
+
process.stdout.write(` Host ${info.hostVersion} on ${info.hostname} (${hostSocket})\n`);
|
|
129
|
+
for (const agent of info.agents) {
|
|
130
|
+
const state = agent.installed ? `v${agent.version ?? "?"}${agent.problem ? ` ⚠ ${agent.problem}` : ""}` : "not installed";
|
|
131
|
+
const auth =
|
|
132
|
+
agent.auth?.state === "ok"
|
|
133
|
+
? ` login: ${agent.auth.method ?? "ok"}`
|
|
134
|
+
: agent.auth?.state === "missing"
|
|
135
|
+
? ` ⚠ ${agent.auth.hint ?? "not logged in"}`
|
|
136
|
+
: "";
|
|
137
|
+
process.stdout.write(` ${agent.label.padEnd(10)} ${agent.tier.padEnd(13)} ${state}${auth}\n`);
|
|
138
|
+
}
|
|
139
|
+
const active = sessions.filter((s) => s.state === "running" || s.state === "waiting");
|
|
140
|
+
process.stdout.write(` Sessions: ${sessions.length} known, ${active.length} active\n`);
|
|
141
|
+
const gateway = await client.call("gateway.status", {});
|
|
142
|
+
if (gateway.status === "off") {
|
|
143
|
+
process.stdout.write(" Gateway: off (linkshell host --gateway <url>)\n");
|
|
144
|
+
} else {
|
|
145
|
+
const account = gateway.account ? `, account ${gateway.account.email ?? gateway.account.userId}` : "";
|
|
146
|
+
const problem = gateway.error ? ` ⚠ ${gateway.error.message}` : "";
|
|
147
|
+
process.stdout.write(` Gateway: ${gateway.status} ${gateway.url}${account}${problem}\n`);
|
|
148
|
+
for (const device of gateway.devices) {
|
|
149
|
+
process.stdout.write(` ${device.online ? "●" : "○"} ${device.name} paired ${new Date(device.pairedAt).toLocaleDateString()}\n`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} finally {
|
|
153
|
+
client.close();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
interface LaunchSpec {
|
|
158
|
+
command: string;
|
|
159
|
+
args: string[];
|
|
160
|
+
env?: Record<string, string>;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function runChild(launch: LaunchSpec, onSpawn?: (child: import("node:child_process").ChildProcess) => void): Promise<number> {
|
|
164
|
+
const child = spawn(launch.command, launch.args, {
|
|
165
|
+
stdio: "inherit",
|
|
166
|
+
env: { ...process.env, ...launch.env },
|
|
167
|
+
});
|
|
168
|
+
onSpawn?.(child);
|
|
169
|
+
// The terminal delivers Ctrl-C to the whole foreground group; the agent handles it.
|
|
170
|
+
const ignore = () => {};
|
|
171
|
+
process.on("SIGINT", ignore);
|
|
172
|
+
process.on("SIGQUIT", ignore);
|
|
173
|
+
return new Promise<number>((resolve) => {
|
|
174
|
+
const done = (code: number) => {
|
|
175
|
+
process.off("SIGINT", ignore);
|
|
176
|
+
process.off("SIGQUIT", ignore);
|
|
177
|
+
resolve(code);
|
|
178
|
+
};
|
|
179
|
+
child.once("exit", (exitCode, signal) => done(exitCode ?? (signal ? 128 + 15 : 1)));
|
|
180
|
+
child.once("error", (error) => {
|
|
181
|
+
process.stderr.write(`linkshell: could not start ${launch.command}: ${error.message}\n`);
|
|
182
|
+
done(127);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Undo what a killed full-screen TUI may have left on: alt screen, hidden cursor, mouse/paste modes. */
|
|
188
|
+
function restoreTerminal(): void {
|
|
189
|
+
if (!process.stdout.isTTY) return;
|
|
190
|
+
process.stdout.write("\x1b[?1049l\x1b[?25h\x1b[?2004l\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[0m\r\n");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Resolves with the first key pressed; "\u0003" for Ctrl-C. */
|
|
194
|
+
function waitForKey(): Promise<string> {
|
|
195
|
+
return new Promise((resolve) => {
|
|
196
|
+
const stdin = process.stdin;
|
|
197
|
+
const raw = stdin.isTTY;
|
|
198
|
+
if (raw) stdin.setRawMode(true);
|
|
199
|
+
stdin.resume();
|
|
200
|
+
stdin.once("data", (data) => {
|
|
201
|
+
if (raw) stdin.setRawMode(false);
|
|
202
|
+
stdin.pause();
|
|
203
|
+
resolve(data.toString("utf8"));
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* `linkshell <agent> [args…]`: starts the agent's native UI attached to the
|
|
210
|
+
* host, so the same session is live on the desktop and on every paired device.
|
|
211
|
+
*
|
|
212
|
+
* For handoff agents (Claude) the terminal stays the session's desktop seat:
|
|
213
|
+
* when a device takes over, the TUI exits and the terminal shows the remote
|
|
214
|
+
* progress; any key brings the session back to the TUI.
|
|
215
|
+
*/
|
|
216
|
+
export async function runAgentShim(agent: string, args: string[]): Promise<void> {
|
|
217
|
+
const host = await loadHost();
|
|
218
|
+
const socket = await ensureHostRunning();
|
|
219
|
+
const client = await host.connectHost(socket);
|
|
220
|
+
let launch: LaunchSpec & { sessionId?: string };
|
|
221
|
+
try {
|
|
222
|
+
launch = await client.call("desktop.launch", { agent, args, cwd: process.cwd() });
|
|
223
|
+
} catch (error) {
|
|
224
|
+
client.close();
|
|
225
|
+
process.stderr.write(`linkshell: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
if (!launch.sessionId) {
|
|
229
|
+
// Multi-client agents (Codex): the TUI simply joins the shared server.
|
|
230
|
+
client.close();
|
|
231
|
+
process.exit(await runChild(launch));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const sessionId = launch.sessionId;
|
|
235
|
+
let child: import("node:child_process").ChildProcess | undefined;
|
|
236
|
+
let yielded = false;
|
|
237
|
+
let showingRemote = false;
|
|
238
|
+
client.on("desktop.yield", ({ sessionId: target }) => {
|
|
239
|
+
if (target !== sessionId) return;
|
|
240
|
+
yielded = true;
|
|
241
|
+
child?.kill("SIGTERM");
|
|
242
|
+
});
|
|
243
|
+
client.on("desktop.remoteActivity", ({ sessionId: target, line }) => {
|
|
244
|
+
if (target === sessionId && showingRemote) process.stdout.write(`${line}\n`);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
for (;;) {
|
|
248
|
+
yielded = false;
|
|
249
|
+
const code = await runChild(launch, (spawned) => (child = spawned));
|
|
250
|
+
if (!yielded) {
|
|
251
|
+
// The user quit the TUI: the session stays available on every device.
|
|
252
|
+
client.close();
|
|
253
|
+
process.exit(code);
|
|
254
|
+
}
|
|
255
|
+
await client.call("desktop.yielded", { sessionId }).catch(() => {});
|
|
256
|
+
restoreTerminal();
|
|
257
|
+
process.stdout.write(
|
|
258
|
+
[
|
|
259
|
+
"─".repeat(48),
|
|
260
|
+
"📱 已由手机接管,会话在手机上继续",
|
|
261
|
+
" 按任意键收回到电脑 · Ctrl-C 退出(会话留在手机上)",
|
|
262
|
+
"─".repeat(48),
|
|
263
|
+
"",
|
|
264
|
+
].join("\n"),
|
|
265
|
+
);
|
|
266
|
+
showingRemote = true;
|
|
267
|
+
const key = await waitForKey();
|
|
268
|
+
showingRemote = false;
|
|
269
|
+
if (key === "\u0003") {
|
|
270
|
+
client.close();
|
|
271
|
+
process.exit(0);
|
|
272
|
+
}
|
|
273
|
+
process.stdout.write("↩︎ 正在收回到电脑…\n");
|
|
274
|
+
try {
|
|
275
|
+
const spec = await client.call("desktop.reclaim", { sessionId });
|
|
276
|
+
launch = { ...spec, sessionId };
|
|
277
|
+
} catch (error) {
|
|
278
|
+
process.stderr.write(`linkshell: 收回失败:${error instanceof Error ? error.message : String(error)}\n`);
|
|
279
|
+
client.close();
|
|
280
|
+
process.exit(1);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import qrcode from "qrcode-terminal";
|
|
2
|
+
|
|
3
|
+
// `linkshell pair`: show a QR code (and a code to type) that lets a phone reach
|
|
4
|
+
// this computer through the gateway, then wait for it to pair.
|
|
5
|
+
|
|
6
|
+
export async function runPair(): Promise<void> {
|
|
7
|
+
const { ensureHostRunning, assertHostRuntime } = await import("./host.js");
|
|
8
|
+
assertHostRuntime();
|
|
9
|
+
const { connectHost } = await import("@linkshell/host");
|
|
10
|
+
const socket = await ensureHostRunning();
|
|
11
|
+
const client = await connectHost(socket);
|
|
12
|
+
try {
|
|
13
|
+
const status = await client.call("gateway.status", {});
|
|
14
|
+
if (status.status === "off") {
|
|
15
|
+
process.stderr.write("\n No gateway configured. Choose one first:\n\n linkshell host --gateway wss://your-gateway\n\n");
|
|
16
|
+
process.exitCode = 1;
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
let offer;
|
|
20
|
+
try {
|
|
21
|
+
offer = await client.call("pairing.start", {}, 20_000);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
process.stderr.write(`\n Couldn't start pairing: ${error instanceof Error ? error.message : String(error)}\n\n`);
|
|
24
|
+
process.exitCode = 1;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
process.stdout.write("\n Scan with the LinkShell app (电脑 → 添加电脑):\n\n");
|
|
28
|
+
qrcode.generate(offer.link, { small: true }, (art) => process.stdout.write(`${art.replace(/^/gm, " ")}\n`));
|
|
29
|
+
const code = `${offer.code.slice(0, 3)} ${offer.code.slice(3)}`;
|
|
30
|
+
const minutes = Math.round((offer.expiresAt - Date.now()) / 60_000);
|
|
31
|
+
process.stdout.write(`\n Or enter the code: ${code} (valid ${minutes} min, gateway ${offer.gateway})\n\n Waiting for your phone…`);
|
|
32
|
+
const device = await new Promise<{ name: string } | undefined>((resolve) => {
|
|
33
|
+
const stop = client.on("pairing.done", ({ device }) => {
|
|
34
|
+
stop();
|
|
35
|
+
resolve(device);
|
|
36
|
+
});
|
|
37
|
+
setTimeout(() => {
|
|
38
|
+
stop();
|
|
39
|
+
resolve(undefined);
|
|
40
|
+
}, offer.expiresAt - Date.now());
|
|
41
|
+
process.on("SIGINT", () => resolve(undefined));
|
|
42
|
+
});
|
|
43
|
+
process.stdout.write(device ? `\r ✓ Paired with ${device.name}. It can reach this computer from anywhere now.\n\n` : "\r Pairing window closed. \n\n");
|
|
44
|
+
} finally {
|
|
45
|
+
client.close();
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -55,6 +55,79 @@ program
|
|
|
55
55
|
)
|
|
56
56
|
.version(pkg.version);
|
|
57
57
|
|
|
58
|
+
// ── host (v2 session daemon) ────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
const hostCmd = program
|
|
61
|
+
.command("host")
|
|
62
|
+
.description("Run the LinkShell host that owns agent sessions (v2)")
|
|
63
|
+
.option("--daemon", "Run in background (detached)")
|
|
64
|
+
.option("--dev-port <port>", "Also serve the host API on 127.0.0.1:<port> (local development clients only)")
|
|
65
|
+
.option("--gateway <url>", "Reach this computer through a v2 gateway (saved; 'off' to disable)")
|
|
66
|
+
.option("--_foreground-host", undefined) // internal
|
|
67
|
+
.action(async (options) => {
|
|
68
|
+
const { runHostForeground, ensureHostRunning, readHostConfig, writeHostConfig, assertHostRuntime } = await import("./commands/host.js");
|
|
69
|
+
assertHostRuntime();
|
|
70
|
+
if (options.devPort) process.env.LINKSHELL_DEV_PORT = String(options.devPort);
|
|
71
|
+
if (options.gateway) {
|
|
72
|
+
const { defaultHome } = await import("@linkshell/host");
|
|
73
|
+
const home = defaultHome();
|
|
74
|
+
writeHostConfig(home, { ...readHostConfig(home), gateway: options.gateway === "off" ? undefined : options.gateway });
|
|
75
|
+
}
|
|
76
|
+
if (options.daemon && !options._foregroundHost) {
|
|
77
|
+
const socket = await ensureHostRunning();
|
|
78
|
+
const daemon = await import("./utils/daemon.js");
|
|
79
|
+
process.stderr.write(`\n LinkShell host running (${socket})\n`);
|
|
80
|
+
process.stderr.write(` Log: ${daemon.getLogFile("host")}\n`);
|
|
81
|
+
process.stderr.write(` Status: linkshell host status\n`);
|
|
82
|
+
process.stderr.write(` Stop: linkshell host stop\n\n`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
await runHostForeground(pkg.version);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
hostCmd
|
|
89
|
+
.command("status")
|
|
90
|
+
.description("Show the host, its agents and sessions")
|
|
91
|
+
.action(async () => {
|
|
92
|
+
const { printHostStatus } = await import("./commands/host.js");
|
|
93
|
+
await printHostStatus();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
hostCmd
|
|
97
|
+
.command("stop")
|
|
98
|
+
.description("Stop the background host")
|
|
99
|
+
.action(async () => {
|
|
100
|
+
const daemon = await import("./utils/daemon.js");
|
|
101
|
+
process.stderr.write(daemon.stopDaemon("host") ? " LinkShell host stopped\n" : " LinkShell host is not running\n");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
program
|
|
105
|
+
.command("pair")
|
|
106
|
+
.description("Pair a phone with this computer through the gateway (shows a QR code)")
|
|
107
|
+
.action(async () => {
|
|
108
|
+
const { runPair } = await import("./commands/pair.js");
|
|
109
|
+
await runPair();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// `linkshell codex …` / `linkshell claude …` start the agent's own TUI attached
|
|
113
|
+
// to the host, so the session is live here and on every paired device.
|
|
114
|
+
// All arguments go to the agent.
|
|
115
|
+
for (const [agent, description] of [
|
|
116
|
+
["codex", "Start Codex attached to the LinkShell host (arguments pass through)"],
|
|
117
|
+
["claude", "Start Claude Code with phone handoff (arguments pass through)"],
|
|
118
|
+
] as const) {
|
|
119
|
+
program
|
|
120
|
+
.command(agent)
|
|
121
|
+
.description(description)
|
|
122
|
+
.argument("[args...]")
|
|
123
|
+
.allowUnknownOption(true)
|
|
124
|
+
.helpOption(false)
|
|
125
|
+
.action(async (args: string[]) => {
|
|
126
|
+
const { runAgentShim } = await import("./commands/host.js");
|
|
127
|
+
await runAgentShim(agent, args ?? []);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
58
131
|
// ── start ───────────────────────────────────────────────────────────
|
|
59
132
|
|
|
60
133
|
program
|