@termlink/cli 0.1.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.
Files changed (147) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +84 -0
  3. package/dist/auth/login.d.ts +55 -0
  4. package/dist/auth/login.d.ts.map +1 -0
  5. package/dist/auth/login.js +238 -0
  6. package/dist/auth/login.js.map +1 -0
  7. package/dist/auth/oauth-defaults.d.ts +9 -0
  8. package/dist/auth/oauth-defaults.d.ts.map +1 -0
  9. package/dist/auth/oauth-defaults.js +9 -0
  10. package/dist/auth/oauth-defaults.js.map +1 -0
  11. package/dist/cli.d.ts +3 -0
  12. package/dist/cli.d.ts.map +1 -0
  13. package/dist/cli.js +319 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/errors.d.ts +8 -0
  16. package/dist/errors.d.ts.map +1 -0
  17. package/dist/errors.js +13 -0
  18. package/dist/errors.js.map +1 -0
  19. package/dist/index.d.ts +20 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +19 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/providers/claude/history.d.ts +15 -0
  24. package/dist/providers/claude/history.d.ts.map +1 -0
  25. package/dist/providers/claude/history.js +71 -0
  26. package/dist/providers/claude/history.js.map +1 -0
  27. package/dist/providers/claude/input-queue.d.ts +8 -0
  28. package/dist/providers/claude/input-queue.d.ts.map +1 -0
  29. package/dist/providers/claude/input-queue.js +34 -0
  30. package/dist/providers/claude/input-queue.js.map +1 -0
  31. package/dist/providers/claude/mapper.d.ts +21 -0
  32. package/dist/providers/claude/mapper.d.ts.map +1 -0
  33. package/dist/providers/claude/mapper.js +136 -0
  34. package/dist/providers/claude/mapper.js.map +1 -0
  35. package/dist/providers/claude/provider.d.ts +46 -0
  36. package/dist/providers/claude/provider.d.ts.map +1 -0
  37. package/dist/providers/claude/provider.js +148 -0
  38. package/dist/providers/claude/provider.js.map +1 -0
  39. package/dist/providers/claude/session.d.ts +27 -0
  40. package/dist/providers/claude/session.d.ts.map +1 -0
  41. package/dist/providers/claude/session.js +353 -0
  42. package/dist/providers/claude/session.js.map +1 -0
  43. package/dist/providers/codex/app-server.d.ts +24 -0
  44. package/dist/providers/codex/app-server.d.ts.map +1 -0
  45. package/dist/providers/codex/app-server.js +79 -0
  46. package/dist/providers/codex/app-server.js.map +1 -0
  47. package/dist/providers/codex/command.d.ts +12 -0
  48. package/dist/providers/codex/command.d.ts.map +1 -0
  49. package/dist/providers/codex/command.js +60 -0
  50. package/dist/providers/codex/command.js.map +1 -0
  51. package/dist/providers/codex/history.d.ts +5 -0
  52. package/dist/providers/codex/history.d.ts.map +1 -0
  53. package/dist/providers/codex/history.js +33 -0
  54. package/dist/providers/codex/history.js.map +1 -0
  55. package/dist/providers/codex/mapper.d.ts +13 -0
  56. package/dist/providers/codex/mapper.d.ts.map +1 -0
  57. package/dist/providers/codex/mapper.js +83 -0
  58. package/dist/providers/codex/mapper.js.map +1 -0
  59. package/dist/providers/codex/protocol.d.ts +223 -0
  60. package/dist/providers/codex/protocol.d.ts.map +1 -0
  61. package/dist/providers/codex/protocol.js +11 -0
  62. package/dist/providers/codex/protocol.js.map +1 -0
  63. package/dist/providers/codex/provider.d.ts +28 -0
  64. package/dist/providers/codex/provider.d.ts.map +1 -0
  65. package/dist/providers/codex/provider.js +116 -0
  66. package/dist/providers/codex/provider.js.map +1 -0
  67. package/dist/providers/codex/rpc.d.ts +23 -0
  68. package/dist/providers/codex/rpc.d.ts.map +1 -0
  69. package/dist/providers/codex/rpc.js +104 -0
  70. package/dist/providers/codex/rpc.js.map +1 -0
  71. package/dist/providers/codex/session.d.ts +16 -0
  72. package/dist/providers/codex/session.d.ts.map +1 -0
  73. package/dist/providers/codex/session.js +262 -0
  74. package/dist/providers/codex/session.js.map +1 -0
  75. package/dist/providers/display.d.ts +9 -0
  76. package/dist/providers/display.d.ts.map +1 -0
  77. package/dist/providers/display.js +21 -0
  78. package/dist/providers/display.js.map +1 -0
  79. package/dist/providers/terminal.d.ts +24 -0
  80. package/dist/providers/terminal.d.ts.map +1 -0
  81. package/dist/providers/terminal.js +33 -0
  82. package/dist/providers/terminal.js.map +1 -0
  83. package/dist/providers/types.d.ts +72 -0
  84. package/dist/providers/types.d.ts.map +1 -0
  85. package/dist/providers/types.js +2 -0
  86. package/dist/providers/types.js.map +1 -0
  87. package/dist/relay/api.d.ts +13 -0
  88. package/dist/relay/api.d.ts.map +1 -0
  89. package/dist/relay/api.js +75 -0
  90. package/dist/relay/api.js.map +1 -0
  91. package/dist/relay/device.d.ts +26 -0
  92. package/dist/relay/device.d.ts.map +1 -0
  93. package/dist/relay/device.js +83 -0
  94. package/dist/relay/device.js.map +1 -0
  95. package/dist/relay/relay-host.d.ts +41 -0
  96. package/dist/relay/relay-host.d.ts.map +1 -0
  97. package/dist/relay/relay-host.js +238 -0
  98. package/dist/relay/relay-host.js.map +1 -0
  99. package/dist/server/coalesce.d.ts +15 -0
  100. package/dist/server/coalesce.d.ts.map +1 -0
  101. package/dist/server/coalesce.js +50 -0
  102. package/dist/server/coalesce.js.map +1 -0
  103. package/dist/server/connection.d.ts +30 -0
  104. package/dist/server/connection.d.ts.map +1 -0
  105. package/dist/server/connection.js +182 -0
  106. package/dist/server/connection.js.map +1 -0
  107. package/dist/server/local-server.d.ts +27 -0
  108. package/dist/server/local-server.d.ts.map +1 -0
  109. package/dist/server/local-server.js +108 -0
  110. package/dist/server/local-server.js.map +1 -0
  111. package/dist/session/event-log.d.ts +17 -0
  112. package/dist/session/event-log.d.ts.map +1 -0
  113. package/dist/session/event-log.js +21 -0
  114. package/dist/session/event-log.js.map +1 -0
  115. package/dist/session/manager.d.ts +62 -0
  116. package/dist/session/manager.d.ts.map +1 -0
  117. package/dist/session/manager.js +282 -0
  118. package/dist/session/manager.js.map +1 -0
  119. package/dist/session/session.d.ts +43 -0
  120. package/dist/session/session.d.ts.map +1 -0
  121. package/dist/session/session.js +310 -0
  122. package/dist/session/session.js.map +1 -0
  123. package/dist/session/store.d.ts +22 -0
  124. package/dist/session/store.d.ts.map +1 -0
  125. package/dist/session/store.js +44 -0
  126. package/dist/session/store.js.map +1 -0
  127. package/dist/session/terminal-session.d.ts +52 -0
  128. package/dist/session/terminal-session.d.ts.map +1 -0
  129. package/dist/session/terminal-session.js +210 -0
  130. package/dist/session/terminal-session.js.map +1 -0
  131. package/dist/util/id.d.ts +2 -0
  132. package/dist/util/id.d.ts.map +1 -0
  133. package/dist/util/id.js +5 -0
  134. package/dist/util/id.js.map +1 -0
  135. package/dist/util/lock.d.ts +12 -0
  136. package/dist/util/lock.d.ts.map +1 -0
  137. package/dist/util/lock.js +52 -0
  138. package/dist/util/lock.js.map +1 -0
  139. package/dist/util/which.d.ts +3 -0
  140. package/dist/util/which.d.ts.map +1 -0
  141. package/dist/util/which.js +12 -0
  142. package/dist/util/which.js.map +1 -0
  143. package/dist/version.d.ts +2 -0
  144. package/dist/version.d.ts.map +1 -0
  145. package/dist/version.js +5 -0
  146. package/dist/version.js.map +1 -0
  147. package/package.json +56 -0
@@ -0,0 +1,282 @@
1
+ import { realpath, stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { HostError, errorMessage } from "../errors.js";
4
+ import { newId } from "../util/id.js";
5
+ import { AgentSession } from "./session.js";
6
+ import { TerminalSession } from "./terminal-session.js";
7
+ const PERSIST_DELAY_MS = 100;
8
+ const DEFAULT_COLS = 80;
9
+ const DEFAULT_ROWS = 24;
10
+ /** Owns all sessions on this host and fans host-channel events out to every connection. */
11
+ export class SessionManager {
12
+ #providers = new Map();
13
+ #sessions = new Map();
14
+ #hostListeners = new Set();
15
+ #logCapacity;
16
+ #roots;
17
+ #store;
18
+ #autoApprove;
19
+ #flow;
20
+ #persistTimer = null;
21
+ #stopping = false;
22
+ constructor(options) {
23
+ for (const provider of options.providers)
24
+ this.#providers.set(provider.id, provider);
25
+ this.#logCapacity = options.logCapacity;
26
+ this.#roots = options.allowedRoots?.map((root) => path.resolve(root));
27
+ this.#store = options.store;
28
+ this.#autoApprove = options.autoApprove ?? "off";
29
+ this.#flow = options.flow ?? {};
30
+ }
31
+ /** Folders sessions may be opened in, or undefined when unrestricted. */
32
+ get roots() {
33
+ return this.#roots ? [...this.#roots] : undefined;
34
+ }
35
+ onHostEvent(listener) {
36
+ this.#hostListeners.add(listener);
37
+ return () => this.#hostListeners.delete(listener);
38
+ }
39
+ async providerStatuses() {
40
+ return Promise.all([...this.#providers.values()].map(async (provider) => {
41
+ const identity = { id: provider.id, kind: provider.kind, label: provider.label };
42
+ try {
43
+ return { ...identity, ...(await provider.probe()) };
44
+ }
45
+ catch (err) {
46
+ return { ...identity, available: false, version: null, detail: errorMessage(err) };
47
+ }
48
+ }));
49
+ }
50
+ list() {
51
+ return [...this.#sessions.values()].map((s) => s.info);
52
+ }
53
+ get(sessionId) {
54
+ const session = this.#sessions.get(sessionId);
55
+ if (!session)
56
+ throw new HostError("not_found", `no session ${sessionId}`);
57
+ return session;
58
+ }
59
+ /** The session, if it is an agent session; commands for turns and approvals need one. */
60
+ agent(sessionId) {
61
+ const session = this.get(sessionId);
62
+ if (!(session instanceof AgentSession))
63
+ throw new HostError("unsupported", `${sessionId} is a terminal session`);
64
+ return session;
65
+ }
66
+ terminal(sessionId) {
67
+ const session = this.get(sessionId);
68
+ if (!(session instanceof TerminalSession))
69
+ throw new HostError("unsupported", `${sessionId} is not a terminal session`);
70
+ return session;
71
+ }
72
+ async create(options) {
73
+ const provider = this.#providers.get(options.provider);
74
+ if (!provider)
75
+ throw new HostError("unsupported", `provider ${options.provider} is not enabled on this host`);
76
+ const cwd = await this.#resolveCwd(options.cwd);
77
+ return provider.kind === "terminal"
78
+ ? this.#createTerminal(provider, cwd, options)
79
+ : this.#createAgent(provider, cwd, options);
80
+ }
81
+ /** create() for callers that need an agent session, such as tests. */
82
+ async createAgent(options) {
83
+ const session = await this.create(options);
84
+ if (!(session instanceof AgentSession)) {
85
+ await this.close(session.id, "not an agent session");
86
+ throw new HostError("unsupported", `${options.provider} opens terminal sessions`);
87
+ }
88
+ return session;
89
+ }
90
+ /** create() for callers that need a terminal session. */
91
+ async createTerminal(options) {
92
+ const session = await this.create(options);
93
+ if (!(session instanceof TerminalSession)) {
94
+ await this.close(session.id, "not a terminal session");
95
+ throw new HostError("unsupported", `${options.provider} opens agent sessions`);
96
+ }
97
+ return session;
98
+ }
99
+ async #createAgent(provider, cwd, options) {
100
+ const now = Date.now();
101
+ const session = new AgentSession({
102
+ id: newId("ag"),
103
+ kind: "agent",
104
+ provider: provider.id,
105
+ providerSessionId: null,
106
+ cwd,
107
+ title: options.title ?? null,
108
+ status: "starting",
109
+ createdAt: now,
110
+ updatedAt: now,
111
+ lastSeq: 0,
112
+ autoApprove: options.autoApprove ?? this.#autoApprove,
113
+ }, this.#broadcast, this.#logCapacity);
114
+ this.#sessions.set(session.id, session);
115
+ session.announce();
116
+ try {
117
+ session.bind(await provider.start({ sessionId: session.id, cwd }, session.sink));
118
+ }
119
+ catch (err) {
120
+ await this.close(session.id, `failed to start: ${errorMessage(err)}`);
121
+ throw new HostError("internal", `failed to start ${provider.id}: ${errorMessage(err)}`);
122
+ }
123
+ return session;
124
+ }
125
+ async #createTerminal(provider, cwd, options) {
126
+ const now = Date.now();
127
+ const cols = options.cols ?? DEFAULT_COLS;
128
+ const rows = options.rows ?? DEFAULT_ROWS;
129
+ const session = new TerminalSession({
130
+ id: newId("tm"),
131
+ kind: "terminal",
132
+ provider: provider.id,
133
+ providerSessionId: null,
134
+ cwd,
135
+ title: options.title ?? null,
136
+ status: "starting",
137
+ createdAt: now,
138
+ updatedAt: now,
139
+ lastSeq: 0,
140
+ cols,
141
+ rows,
142
+ }, this.#broadcast, this.#flow);
143
+ this.#sessions.set(session.id, session);
144
+ session.announce();
145
+ try {
146
+ session.bind(await provider.open({ sessionId: session.id, cwd, cols, rows }));
147
+ }
148
+ catch (err) {
149
+ await this.close(session.id, `failed to start: ${errorMessage(err)}`);
150
+ throw new HostError("internal", `failed to start a terminal: ${errorMessage(err)}`);
151
+ }
152
+ return session;
153
+ }
154
+ /**
155
+ * Brings back the agent sessions the store remembers, with their recent history read
156
+ * from each provider's transcript. Providers resume on first use. Returns how many came back.
157
+ * Terminal sessions are never remembered: their shell died with the previous host.
158
+ */
159
+ async restore() {
160
+ if (!this.#store)
161
+ return 0;
162
+ let restored = 0;
163
+ for (const record of await this.#store.load()) {
164
+ const provider = this.#providers.get(record.provider);
165
+ if (provider?.kind !== "agent" || !provider.resumable || this.#sessions.has(record.id))
166
+ continue;
167
+ let cwd;
168
+ try {
169
+ cwd = await this.#resolveCwd(record.cwd);
170
+ }
171
+ catch {
172
+ continue; // The folder is gone or no longer allowed.
173
+ }
174
+ const session = new AgentSession({
175
+ id: record.id,
176
+ kind: "agent",
177
+ provider: provider.id,
178
+ providerSessionId: record.providerSessionId,
179
+ cwd,
180
+ title: record.title,
181
+ status: "starting",
182
+ createdAt: record.createdAt,
183
+ updatedAt: record.updatedAt,
184
+ lastSeq: 0,
185
+ autoApprove: record.autoApprove ?? this.#autoApprove,
186
+ }, this.#broadcast, this.#logCapacity);
187
+ if (provider.history) {
188
+ // History is a convenience; a session without it can still be resumed.
189
+ const turns = await provider.history(record.providerSessionId, cwd).catch(() => []);
190
+ session.importHistory(turns);
191
+ }
192
+ this.#sessions.set(session.id, session);
193
+ session.announce();
194
+ session.bindLazy(() => provider.start({ sessionId: session.id, cwd, resumeProviderSessionId: record.providerSessionId }, session.sink));
195
+ restored++;
196
+ }
197
+ return restored;
198
+ }
199
+ /** Closed by a client: the session is forgotten and will not come back after a restart. */
200
+ async close(sessionId, reason = null) {
201
+ const session = this.get(sessionId);
202
+ this.#sessions.delete(sessionId);
203
+ await session.close(reason);
204
+ this.#persistSoon();
205
+ }
206
+ async closeAll(reason = null) {
207
+ await Promise.all([...this.#sessions.keys()].map((id) => this.close(id, reason)));
208
+ }
209
+ /**
210
+ * Stops the host: saves resumable sessions, closes everything, then releases provider
211
+ * resources such as the Codex app-server. Unlike close(), agent sessions stay remembered.
212
+ */
213
+ async shutdown(reason = null) {
214
+ this.#stopping = true;
215
+ if (this.#persistTimer)
216
+ clearTimeout(this.#persistTimer);
217
+ await this.#persistNow();
218
+ await this.closeAll(reason);
219
+ await Promise.all([...this.#providers.values()].map((p) => (p.kind === "agent" ? p.dispose?.().catch(() => { }) : undefined)));
220
+ }
221
+ #persistSoon() {
222
+ if (!this.#store || this.#stopping)
223
+ return;
224
+ if (this.#persistTimer)
225
+ clearTimeout(this.#persistTimer);
226
+ this.#persistTimer = setTimeout(() => void this.#persistNow(), PERSIST_DELAY_MS);
227
+ }
228
+ async #persistNow() {
229
+ this.#persistTimer = null;
230
+ if (!this.#store)
231
+ return;
232
+ const records = [];
233
+ for (const session of this.#sessions.values()) {
234
+ if (!(session instanceof AgentSession))
235
+ continue;
236
+ const info = session.info;
237
+ const provider = this.#providers.get(info.provider);
238
+ // Only sessions with a conversation to go back to.
239
+ if (provider?.kind !== "agent" || !provider.resumable || !info.providerSessionId || info.lastSeq === 0)
240
+ continue;
241
+ records.push({
242
+ id: info.id,
243
+ provider: info.provider,
244
+ providerSessionId: info.providerSessionId,
245
+ cwd: info.cwd,
246
+ title: info.title,
247
+ createdAt: info.createdAt,
248
+ updatedAt: info.updatedAt,
249
+ ...(info.autoApprove ? { autoApprove: info.autoApprove } : {}),
250
+ });
251
+ }
252
+ await this.#store.save(records);
253
+ }
254
+ // Remote clients choose the folder, so it is checked after resolving symlinks.
255
+ async #resolveCwd(requested) {
256
+ const resolved = path.resolve(this.#roots?.[0] ?? process.cwd(), requested);
257
+ const real = await realpath(resolved).catch(() => null);
258
+ const stats = real ? await stat(real).catch(() => null) : null;
259
+ if (!real || !stats?.isDirectory())
260
+ throw new HostError("bad_request", `not a directory: ${resolved}`);
261
+ if (this.#roots) {
262
+ const roots = await Promise.all(this.#roots.map((root) => realpath(root).catch(() => root)));
263
+ if (!roots.some((root) => isInside(real, root))) {
264
+ throw new HostError("forbidden", `${resolved} is outside the folders this host allows`);
265
+ }
266
+ }
267
+ return real;
268
+ }
269
+ #broadcast = (event) => {
270
+ // A session that ended on its own (a shell that exited) is forgotten like a closed one.
271
+ if (event.type === "session.closed" && event.sessionId)
272
+ this.#sessions.delete(event.sessionId);
273
+ for (const listener of this.#hostListeners)
274
+ listener(event);
275
+ this.#persistSoon();
276
+ };
277
+ }
278
+ function isInside(child, root) {
279
+ const relative = path.relative(root, child);
280
+ return relative === "" || (relative.split(path.sep)[0] !== ".." && !path.isAbsolute(relative));
281
+ }
282
+ //# sourceMappingURL=manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manager.js","sourceRoot":"","sources":["../../src/session/manager.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ import { type AgentEvent, type AutoApprove, type SessionInfo } from "@termlink/protocol";
2
+ import type { EventSink, HistoryTurn, ProviderSession, UserInput } from "../providers/types.js";
3
+ import { EventLog } from "./event-log.js";
4
+ export type Listener = (event: AgentEvent) => void;
5
+ export interface Attachment {
6
+ /** Deliver right after the attach response: missed durable events, then catch-up snapshots. */
7
+ replay: AgentEvent[];
8
+ gap: boolean;
9
+ oldestSeq: number;
10
+ detach: () => void;
11
+ }
12
+ /**
13
+ * One agent session. Owns the seq counter, the replay log, the pending input
14
+ * requests and the status derived from them. Providers only see its sink.
15
+ */
16
+ export declare class AgentSession {
17
+ #private;
18
+ readonly id: string;
19
+ readonly log: EventLog;
20
+ constructor(info: SessionInfo, onLifecycle: Listener, logCapacity?: number);
21
+ get info(): SessionInfo;
22
+ get closed(): boolean;
23
+ readonly sink: EventSink;
24
+ announce(): void;
25
+ bind(provider: ProviderSession): void;
26
+ /** Binds a provider that is only started when the session is next used. */
27
+ bindLazy(start: () => Promise<ProviderSession>): void;
28
+ /** Changes session settings; a new autoApprove mode also answers approvals already waiting. */
29
+ configure(settings: {
30
+ autoApprove?: AutoApprove | undefined;
31
+ }): void;
32
+ /**
33
+ * Replays turns from the provider's transcript into this session's log, so a
34
+ * client attaching after a host restart sees the conversation so far.
35
+ */
36
+ importHistory(turns: HistoryTurn[]): void;
37
+ attach(listener: Listener, afterSeq?: number): Attachment;
38
+ send(input: UserInput): Promise<void>;
39
+ interrupt(): Promise<void>;
40
+ respond(requestId: string, decisionId: string, answers?: Record<string, string>): void;
41
+ close(reason: string | null): Promise<void>;
42
+ }
43
+ //# sourceMappingURL=session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/session/session.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,UAAU,EACf,KAAK,WAAW,EAOhB,KAAK,WAAW,EAEjB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAiB,eAAe,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAE/G,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE1C,MAAM,MAAM,QAAQ,GAAG,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;AAEnD,MAAM,WAAW,UAAU;IACzB,+FAA+F;IAC/F,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,GAAG,EAAE,OAAO,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB;AAcD;;;GAGG;AACH,qBAAa,YAAY;;IACvB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC;IAiBvB,YAAY,IAAI,EAAE,WAAW,EAAE,WAAW,EAAE,QAAQ,EAAE,WAAW,CAAC,EAAE,MAAM,EAKzE;IAED,IAAI,IAAI,IAAI,WAAW,CAEtB;IAED,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED,QAAQ,CAAC,IAAI,EAAE,SAAS,CAOtB;IAEF,QAAQ,IAAI,IAAI,CAEf;IAED,IAAI,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI,CAGpC;IAED,2EAA2E;IAC3E,QAAQ,CAAC,KAAK,EAAE,MAAM,OAAO,CAAC,eAAe,CAAC,GAAG,IAAI,CAGpD;IAED,+FAA+F;IAC/F,SAAS,CAAC,QAAQ,EAAE;QAAE,WAAW,CAAC,EAAE,WAAW,GAAG,SAAS,CAAA;KAAE,GAAG,IAAI,CAOnE;IAED;;;OAGG;IACH,aAAa,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,IAAI,CAcxC;IAED,MAAM,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,SAAI,GAAG,UAAU,CAoBnD;IAEK,IAAI,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CA2B1C;IAEK,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CAG/B;IAED,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAMrF;IAEK,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAkBhD;CAkIF"}
@@ -0,0 +1,310 @@
1
+ import { PROTOCOL_VERSION, applyDelta, isDurableEvent, } from "@termlink/protocol";
2
+ import { HostError, errorMessage } from "../errors.js";
3
+ import { newId } from "../util/id.js";
4
+ import { EventLog } from "./event-log.js";
5
+ /** Approval kinds each autoApprove mode answers. Questions and plans are never on this list. */
6
+ const AUTO_KINDS = {
7
+ off: [],
8
+ edits: ["file_approval"],
9
+ all: ["command_approval", "file_approval", "tool_approval"],
10
+ };
11
+ /**
12
+ * One agent session. Owns the seq counter, the replay log, the pending input
13
+ * requests and the status derived from them. Providers only see its sink.
14
+ */
15
+ export class AgentSession {
16
+ id;
17
+ log;
18
+ #info;
19
+ #onLifecycle;
20
+ #listeners = new Set();
21
+ #openItems = new Map();
22
+ #pending = new Map();
23
+ #seq = 0;
24
+ #turnActive = false;
25
+ #provider = null;
26
+ /** For a restored session: starts (resumes) the provider on first use. */
27
+ #starter = null;
28
+ #starting = null;
29
+ /** The user message waiting for its turn.started, which will name it. */
30
+ #pendingUserItemId = null;
31
+ /** Set while importing history, so replayed turns do not announce status changes. */
32
+ #quiet = false;
33
+ constructor(info, onLifecycle, logCapacity) {
34
+ this.id = info.id;
35
+ this.#info = { autoApprove: "off", ...info };
36
+ this.#onLifecycle = onLifecycle;
37
+ this.log = new EventLog(logCapacity);
38
+ }
39
+ get info() {
40
+ return { ...this.#info, lastSeq: this.#seq };
41
+ }
42
+ get closed() {
43
+ return this.#info.status === "closed";
44
+ }
45
+ sink = {
46
+ emit: (type, payload) => this.#emit(type, payload),
47
+ requestInput: (request, signal) => this.#requestInput(request, signal),
48
+ setProviderSessionId: (id) => {
49
+ this.#info.providerSessionId = id;
50
+ this.#announce("session.updated");
51
+ },
52
+ };
53
+ announce() {
54
+ this.#announce("session.created");
55
+ }
56
+ bind(provider) {
57
+ this.#provider = provider;
58
+ this.#setStatus("idle");
59
+ }
60
+ /** Binds a provider that is only started when the session is next used. */
61
+ bindLazy(start) {
62
+ this.#starter = start;
63
+ this.#setStatus("idle");
64
+ }
65
+ /** Changes session settings; a new autoApprove mode also answers approvals already waiting. */
66
+ configure(settings) {
67
+ if (this.closed)
68
+ throw new HostError("conflict", "session is closed");
69
+ if (settings.autoApprove && settings.autoApprove !== this.#info.autoApprove) {
70
+ this.#info.autoApprove = settings.autoApprove;
71
+ this.#announce("session.updated");
72
+ for (const [requestId, pending] of [...this.#pending])
73
+ this.#applyPolicy(requestId, pending);
74
+ }
75
+ }
76
+ /**
77
+ * Replays turns from the provider's transcript into this session's log, so a
78
+ * client attaching after a host restart sees the conversation so far.
79
+ */
80
+ importHistory(turns) {
81
+ this.#quiet = true;
82
+ try {
83
+ for (const turn of turns) {
84
+ const [first, ...rest] = turn.items;
85
+ const user = first?.kind === "message" && first.role === "user" ? first : undefined;
86
+ if (user)
87
+ this.#emit("item.completed", { turnId: null, item: user });
88
+ this.#emit("turn.started", { turnId: turn.turnId, ...(user ? { userItemId: user.id } : {}) });
89
+ for (const item of user ? rest : turn.items)
90
+ this.#emit("item.completed", { turnId: turn.turnId, item });
91
+ this.#emit("turn.completed", { turnId: turn.turnId, status: turn.status });
92
+ }
93
+ }
94
+ finally {
95
+ this.#quiet = false;
96
+ }
97
+ }
98
+ attach(listener, afterSeq = 0) {
99
+ if (this.closed)
100
+ throw new HostError("conflict", "session is closed");
101
+ // A client ahead of this log saw an earlier run of the host (seq restarts after a
102
+ // restore). Its view is stale, so it gets everything and is told to redraw.
103
+ const from = afterSeq > this.#seq ? 0 : afterSeq;
104
+ const { events, gap: evicted, oldestSeq } = this.log.since(from);
105
+ const gap = evicted || from !== afterSeq;
106
+ const replay = [...events];
107
+ for (const item of this.#openItems.values())
108
+ replay.push(this.#event("item.updated", { item }));
109
+ if (gap) {
110
+ // The input.required for a still-pending request may have been evicted.
111
+ for (const { request } of this.#pending.values()) {
112
+ const replayed = events.some((e) => e.type === "input.required" && e.payload.request.requestId === request.requestId);
113
+ if (!replayed)
114
+ replay.push(this.#event("input.required", { request }));
115
+ }
116
+ }
117
+ this.#listeners.add(listener);
118
+ return { replay, gap, oldestSeq, detach: () => this.#listeners.delete(listener) };
119
+ }
120
+ async send(input) {
121
+ if (this.closed)
122
+ throw new HostError("conflict", "session is closed");
123
+ if (this.#turnActive)
124
+ throw new HostError("conflict", "a turn is already running");
125
+ // Reserve the turn before any await, so a second send cannot slip in while a restored provider resumes.
126
+ this.#turnActive = true;
127
+ let provider;
128
+ try {
129
+ provider = await this.#ensureProvider();
130
+ }
131
+ catch (err) {
132
+ this.#turnActive = false;
133
+ throw err;
134
+ }
135
+ const userItemId = newId("it");
136
+ this.#emit("item.completed", {
137
+ turnId: null,
138
+ item: { id: userItemId, kind: "message", role: "user", text: input.text, status: "completed" },
139
+ });
140
+ this.#pendingUserItemId = userItemId;
141
+ this.#refreshStatus();
142
+ try {
143
+ await provider.send(input);
144
+ }
145
+ catch (err) {
146
+ this.#pendingUserItemId = null;
147
+ this.#turnActive = false;
148
+ this.#refreshStatus();
149
+ throw err;
150
+ }
151
+ }
152
+ async interrupt() {
153
+ if (this.closed)
154
+ throw new HostError("conflict", "session is closed");
155
+ if (this.#turnActive && this.#provider)
156
+ await this.#provider.interrupt();
157
+ }
158
+ respond(requestId, decisionId, answers) {
159
+ const pending = this.#pending.get(requestId);
160
+ if (!pending)
161
+ throw new HostError("not_found", `no pending input request ${requestId}`);
162
+ const decision = pending.request.decisions.find((d) => d.id === decisionId);
163
+ if (!decision)
164
+ throw new HostError("bad_request", `unknown decision ${decisionId}`);
165
+ this.#resolve(requestId, pending, decision, "user", answers);
166
+ }
167
+ async close(reason) {
168
+ if (this.closed)
169
+ return;
170
+ for (const [requestId, pending] of this.#pending) {
171
+ this.#pending.delete(requestId);
172
+ this.#emit("input.resolved", { requestId, decisionId: null, effect: "cancel", by: "host" });
173
+ pending.settle({ decisionId: null, effect: "cancel" });
174
+ }
175
+ try {
176
+ const provider = this.#provider ?? (this.#starting ? await this.#starting.catch(() => null) : null);
177
+ await provider?.close();
178
+ }
179
+ catch {
180
+ // The session is going away either way.
181
+ }
182
+ this.#turnActive = false;
183
+ this.#info.status = "closed";
184
+ this.#info.updatedAt = Date.now();
185
+ this.#onLifecycle(this.#event("session.closed", { session: this.info, reason }));
186
+ this.#listeners.clear();
187
+ }
188
+ async #ensureProvider() {
189
+ if (this.#provider)
190
+ return this.#provider;
191
+ if (!this.#starter)
192
+ throw new HostError("conflict", "session is still starting");
193
+ this.#starting ??= this.#starter().then((provider) => {
194
+ this.#provider = provider;
195
+ return provider;
196
+ }, (err) => {
197
+ this.#starting = null;
198
+ throw new HostError("internal", `could not resume the ${this.#info.provider} session: ${errorMessage(err)}`);
199
+ });
200
+ return this.#starting;
201
+ }
202
+ #requestInput(input, signal) {
203
+ if (this.closed)
204
+ return Promise.resolve({ decisionId: null, effect: "cancel" });
205
+ if (signal?.aborted)
206
+ return Promise.reject(signal.reason);
207
+ const request = { ...input, requestId: newId("in") };
208
+ return new Promise((resolve, reject) => {
209
+ const onAbort = () => {
210
+ if (!this.#pending.delete(request.requestId))
211
+ return;
212
+ this.#emit("input.resolved", { requestId: request.requestId, decisionId: null, effect: "cancel", by: "host" });
213
+ this.#refreshStatus();
214
+ reject(signal?.reason);
215
+ };
216
+ signal?.addEventListener("abort", onAbort, { once: true });
217
+ const pending = {
218
+ request,
219
+ settle: (response) => {
220
+ signal?.removeEventListener("abort", onAbort);
221
+ resolve(response);
222
+ },
223
+ };
224
+ this.#pending.set(request.requestId, pending);
225
+ // The request is always recorded, even when the policy answers it at once.
226
+ this.#emit("input.required", { request });
227
+ this.#refreshStatus();
228
+ this.#applyPolicy(request.requestId, pending);
229
+ });
230
+ }
231
+ #applyPolicy(requestId, pending) {
232
+ const mode = this.#info.autoApprove ?? "off";
233
+ if (!AUTO_KINDS[mode].includes(pending.request.kind))
234
+ return;
235
+ // Plain allow only: the policy never widens scope with allow_session.
236
+ const decision = pending.request.decisions.find((d) => d.effect === "allow");
237
+ if (decision)
238
+ this.#resolve(requestId, pending, decision, "policy");
239
+ }
240
+ #resolve(requestId, pending, decision, by, answers) {
241
+ this.#pending.delete(requestId);
242
+ this.#emit("input.resolved", { requestId, decisionId: decision.id, effect: decision.effect, by });
243
+ this.#refreshStatus();
244
+ pending.settle(answers ? { decisionId: decision.id, effect: decision.effect, answers } : { decisionId: decision.id, effect: decision.effect });
245
+ }
246
+ #event(type, payload) {
247
+ return { v: PROTOCOL_VERSION, kind: "evt", type, ts: Date.now(), sessionId: this.id, payload };
248
+ }
249
+ #emit(type, payload) {
250
+ if (this.closed)
251
+ return;
252
+ const event = this.#event(type, payload);
253
+ if (event.type === "turn.started" && this.#pendingUserItemId) {
254
+ event.payload = { ...event.payload, userItemId: this.#pendingUserItemId };
255
+ this.#pendingUserItemId = null;
256
+ }
257
+ this.#track(event);
258
+ if (isDurableEvent(type)) {
259
+ event.seq = ++this.#seq;
260
+ this.log.append(event);
261
+ }
262
+ for (const listener of this.#listeners)
263
+ listener(event);
264
+ }
265
+ #track(event) {
266
+ switch (event.type) {
267
+ case "turn.started":
268
+ this.#turnActive = true;
269
+ this.#refreshStatus();
270
+ break;
271
+ case "turn.completed":
272
+ this.#turnActive = false;
273
+ this.#refreshStatus();
274
+ break;
275
+ case "item.started":
276
+ this.#openItems.set(event.payload.item.id, event.payload.item);
277
+ break;
278
+ case "item.updated":
279
+ if (this.#openItems.has(event.payload.item.id))
280
+ this.#openItems.set(event.payload.item.id, event.payload.item);
281
+ break;
282
+ case "item.delta": {
283
+ const { itemId, field, delta } = event.payload;
284
+ const item = this.#openItems.get(itemId);
285
+ if (item)
286
+ this.#openItems.set(itemId, applyDelta(item, field, delta));
287
+ break;
288
+ }
289
+ case "item.completed":
290
+ this.#openItems.delete(event.payload.item.id);
291
+ break;
292
+ }
293
+ }
294
+ #refreshStatus() {
295
+ this.#setStatus(this.#pending.size > 0 ? "waiting_input" : this.#turnActive ? "running" : "idle");
296
+ }
297
+ #setStatus(status) {
298
+ if (this.closed || this.#info.status === status)
299
+ return;
300
+ this.#info.status = status;
301
+ this.#announce("session.updated");
302
+ }
303
+ #announce(type) {
304
+ if (this.#quiet)
305
+ return;
306
+ this.#info.updatedAt = Date.now();
307
+ this.#onLifecycle(this.#event(type, { session: this.info }));
308
+ }
309
+ }
310
+ //# sourceMappingURL=session.js.map