@pushpalsdev/cli 1.0.18 → 1.0.20

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 (108) hide show
  1. package/dist/pushpals-cli.js +291 -44
  2. package/package.json +1 -1
  3. package/runtime/configs/backend.toml +1 -1
  4. package/runtime/configs/default.toml +1 -1
  5. package/runtime/sandbox/apps/workerpals/.python-version +1 -0
  6. package/runtime/sandbox/apps/workerpals/Dockerfile.sandbox +71 -0
  7. package/runtime/sandbox/apps/workerpals/package.json +25 -0
  8. package/runtime/sandbox/apps/workerpals/pyproject.toml +8 -0
  9. package/runtime/sandbox/apps/workerpals/src/backends/backend_config.ts +119 -0
  10. package/runtime/sandbox/apps/workerpals/src/backends/miniswe/miniswe_executor.py +2029 -0
  11. package/runtime/sandbox/apps/workerpals/src/backends/miniswe_backend.ts +48 -0
  12. package/runtime/sandbox/apps/workerpals/src/backends/openai_codex/openai_codex_executor.py +1259 -0
  13. package/runtime/sandbox/apps/workerpals/src/backends/openai_codex/test_openai_codex_runtime_config.py +110 -0
  14. package/runtime/sandbox/apps/workerpals/src/backends/openai_codex_backend.ts +67 -0
  15. package/runtime/sandbox/apps/workerpals/src/backends/openhands/openhands_executor.py +563 -0
  16. package/runtime/sandbox/apps/workerpals/src/backends/openhands_backend.ts +161 -0
  17. package/runtime/sandbox/apps/workerpals/src/backends/openhands_task_execute.ts +536 -0
  18. package/runtime/sandbox/apps/workerpals/src/backends/shared/executor_base.py +746 -0
  19. package/runtime/sandbox/apps/workerpals/src/backends/shared/test_settings_resolver.py +60 -0
  20. package/runtime/sandbox/apps/workerpals/src/backends/task_execute_registry.ts +21 -0
  21. package/runtime/sandbox/apps/workerpals/src/backends/types.ts +52 -0
  22. package/runtime/sandbox/apps/workerpals/src/common/execution_utils.ts +149 -0
  23. package/runtime/sandbox/apps/workerpals/src/common/executor_backend.ts +15 -0
  24. package/runtime/sandbox/apps/workerpals/src/common/generic_python_executor.ts +210 -0
  25. package/runtime/sandbox/apps/workerpals/src/common/logger.ts +65 -0
  26. package/runtime/sandbox/apps/workerpals/src/common/types.ts +9 -0
  27. package/runtime/sandbox/apps/workerpals/src/common/worktree_cleanup.ts +66 -0
  28. package/runtime/sandbox/apps/workerpals/src/context_manager.ts +45 -0
  29. package/runtime/sandbox/apps/workerpals/src/docker_executor.ts +1842 -0
  30. package/runtime/sandbox/apps/workerpals/src/execute_job.ts +3063 -0
  31. package/runtime/sandbox/apps/workerpals/src/job_runner.ts +194 -0
  32. package/runtime/sandbox/apps/workerpals/src/shell_manager.ts +210 -0
  33. package/runtime/sandbox/apps/workerpals/src/timeout_policy.ts +24 -0
  34. package/runtime/sandbox/apps/workerpals/src/workerpals_main.ts +1436 -0
  35. package/runtime/sandbox/apps/workerpals/tsconfig.json +15 -0
  36. package/runtime/sandbox/apps/workerpals/uv.lock +2014 -0
  37. package/runtime/sandbox/bun.lock +2591 -0
  38. package/runtime/sandbox/configs/backend.toml +79 -0
  39. package/runtime/sandbox/configs/default.toml +260 -0
  40. package/runtime/sandbox/configs/dev.toml +2 -0
  41. package/runtime/sandbox/configs/local.example.toml +129 -0
  42. package/runtime/sandbox/package.json +65 -0
  43. package/runtime/sandbox/packages/protocol/README.md +168 -0
  44. package/runtime/sandbox/packages/protocol/package.json +37 -0
  45. package/runtime/sandbox/packages/protocol/scripts/copy-schemas.js +17 -0
  46. package/runtime/sandbox/packages/protocol/src/a2a/README.md +52 -0
  47. package/runtime/sandbox/packages/protocol/src/a2a/mapping.ts +55 -0
  48. package/runtime/sandbox/packages/protocol/src/index.browser.ts +25 -0
  49. package/runtime/sandbox/packages/protocol/src/index.ts +25 -0
  50. package/runtime/sandbox/packages/protocol/src/schemas/approvals.schema.json +6 -0
  51. package/runtime/sandbox/packages/protocol/src/schemas/envelope.schema.json +96 -0
  52. package/runtime/sandbox/packages/protocol/src/schemas/events.schema.json +679 -0
  53. package/runtime/sandbox/packages/protocol/src/schemas/http.schema.json +50 -0
  54. package/runtime/sandbox/packages/protocol/src/types.ts +267 -0
  55. package/runtime/sandbox/packages/protocol/src/validate.browser.ts +154 -0
  56. package/runtime/sandbox/packages/protocol/src/validate.ts +233 -0
  57. package/runtime/sandbox/packages/protocol/src/version.ts +1 -0
  58. package/runtime/sandbox/packages/protocol/tsconfig.json +20 -0
  59. package/runtime/sandbox/packages/shared/package.json +19 -0
  60. package/runtime/sandbox/packages/shared/src/autonomy_policy.ts +400 -0
  61. package/runtime/sandbox/packages/shared/src/client_preflight.ts +286 -0
  62. package/runtime/sandbox/packages/shared/src/communication.ts +313 -0
  63. package/runtime/sandbox/packages/shared/src/config.ts +2180 -0
  64. package/runtime/sandbox/packages/shared/src/config_template_parity.ts +70 -0
  65. package/runtime/sandbox/packages/shared/src/git_backend.ts +205 -0
  66. package/runtime/sandbox/packages/shared/src/index.ts +101 -0
  67. package/runtime/sandbox/packages/shared/src/local_network.ts +101 -0
  68. package/runtime/sandbox/packages/shared/src/localbuddy_runtime.ts +314 -0
  69. package/runtime/sandbox/packages/shared/src/prompts.ts +64 -0
  70. package/runtime/sandbox/packages/shared/src/repo.ts +134 -0
  71. package/runtime/sandbox/packages/shared/src/session_event_visibility.ts +25 -0
  72. package/runtime/sandbox/packages/shared/src/vision.ts +247 -0
  73. package/runtime/sandbox/packages/shared/tsconfig.json +16 -0
  74. package/runtime/sandbox/prompts/workerpals/codex_quality_critic_instruction_prompt.md +14 -0
  75. package/runtime/sandbox/prompts/workerpals/commit_message_prompt.md +36 -0
  76. package/runtime/sandbox/prompts/workerpals/commit_message_user_prompt.md +7 -0
  77. package/runtime/sandbox/prompts/workerpals/miniswe_broker_system_prompt.md +33 -0
  78. package/runtime/sandbox/prompts/workerpals/miniswe_broker_task_prompt.md +5 -0
  79. package/runtime/sandbox/prompts/workerpals/miniswe_completion_requirement.md +1 -0
  80. package/runtime/sandbox/prompts/workerpals/miniswe_context_compaction_retry_prompt.md +1 -0
  81. package/runtime/sandbox/prompts/workerpals/miniswe_explicit_targets_block.md +2 -0
  82. package/runtime/sandbox/prompts/workerpals/miniswe_recovery_guidance_base.md +4 -0
  83. package/runtime/sandbox/prompts/workerpals/miniswe_recovery_guidance_blocker_line.md +1 -0
  84. package/runtime/sandbox/prompts/workerpals/miniswe_strict_tool_use_guidance.md +6 -0
  85. package/runtime/sandbox/prompts/workerpals/miniswe_supplemental_guidance_section.md +2 -0
  86. package/runtime/sandbox/prompts/workerpals/miniswe_timeout_note.md +1 -0
  87. package/runtime/sandbox/prompts/workerpals/miniswe_toolcall_retry_guidance.md +1 -0
  88. package/runtime/sandbox/prompts/workerpals/openai_codex_default_system_prompt.md +4 -0
  89. package/runtime/sandbox/prompts/workerpals/openai_codex_instruction_wrapper.md +5 -0
  90. package/runtime/sandbox/prompts/workerpals/openai_codex_runtime_policy_appendix.md +5 -0
  91. package/runtime/sandbox/prompts/workerpals/openai_codex_supplemental_guidance_section.md +2 -0
  92. package/runtime/sandbox/prompts/workerpals/openai_codex_task_execute_system_prompt.md +12 -0
  93. package/runtime/sandbox/prompts/workerpals/openhands_minimal_security_policy.j2 +8 -0
  94. package/runtime/sandbox/prompts/workerpals/openhands_minimal_system_prompt.j2 +20 -0
  95. package/runtime/sandbox/prompts/workerpals/openhands_strict_tool_use_message.md +1 -0
  96. package/runtime/sandbox/prompts/workerpals/openhands_supplemental_guidance_message.md +2 -0
  97. package/runtime/sandbox/prompts/workerpals/openhands_task_execute_fallback_system_prompt.md +1 -0
  98. package/runtime/sandbox/prompts/workerpals/openhands_task_execute_system_prompt.md +21 -0
  99. package/runtime/sandbox/prompts/workerpals/openhands_task_user_prompt.md +6 -0
  100. package/runtime/sandbox/prompts/workerpals/openhands_timeout_note.md +1 -0
  101. package/runtime/sandbox/prompts/workerpals/pr_description.md +42 -0
  102. package/runtime/sandbox/prompts/workerpals/task_quality_critic_system_prompt.md +9 -0
  103. package/runtime/sandbox/prompts/workerpals/task_quality_critic_user_prompt.md +17 -0
  104. package/runtime/sandbox/prompts/workerpals/workerpals_system_prompt.md +115 -0
  105. package/runtime/sandbox/protocol/schemas/approvals.schema.json +6 -0
  106. package/runtime/sandbox/protocol/schemas/envelope.schema.json +96 -0
  107. package/runtime/sandbox/protocol/schemas/events.schema.json +679 -0
  108. package/runtime/sandbox/protocol/schemas/http.schema.json +50 -0
@@ -0,0 +1,286 @@
1
+ import { existsSync, readFileSync } from "fs";
2
+ import { relative, resolve } from "path";
3
+ import { loadPushPalsConfig, type PushPalsConfig } from "./config.js";
4
+ import { validateVisionDocStructure, type VisionDocValidation } from "./vision.js";
5
+
6
+ export type ClientPreflightCopyCommands = {
7
+ windowsPowerShell: string;
8
+ bash: string;
9
+ };
10
+
11
+ export type ClientPreflightIssue = {
12
+ code:
13
+ | "missing_env_file"
14
+ | "missing_local_toml"
15
+ | "missing_vision_doc"
16
+ | "unreadable_vision_doc"
17
+ | "empty_vision_doc"
18
+ | "invalid_vision_doc";
19
+ message: string;
20
+ detail?: string;
21
+ copyCommands?: ClientPreflightCopyCommands;
22
+ };
23
+
24
+ export type ClientRuntimePreflightResult = {
25
+ ok: boolean;
26
+ projectRoot: string;
27
+ runtimeRoot: string;
28
+ config: PushPalsConfig;
29
+ issues: ClientPreflightIssue[];
30
+ autonomyEnabled: boolean;
31
+ visionSummary: null | {
32
+ path: string;
33
+ chars: number;
34
+ sectionCount: number;
35
+ validation: VisionDocValidation;
36
+ };
37
+ };
38
+
39
+ type EvaluateClientRuntimePreflightOptions = {
40
+ projectRoot: string;
41
+ runtimeRoot?: string;
42
+ configDir?: string;
43
+ config?: PushPalsConfig;
44
+ visionTemplateRoot?: string;
45
+ };
46
+
47
+ function runtimeHasConfigDir(runtimeRoot: string, dirName: string): boolean {
48
+ const dirPath = resolve(runtimeRoot, dirName);
49
+ return (
50
+ existsSync(resolve(dirPath, "default.toml")) ||
51
+ existsSync(resolve(dirPath, "local.example.toml")) ||
52
+ existsSync(resolve(dirPath, "local.toml"))
53
+ );
54
+ }
55
+
56
+ function resolveClientConfigDir(
57
+ projectRoot: string,
58
+ runtimeRoot: string,
59
+ explicitConfigDir?: string,
60
+ ): string | undefined {
61
+ if (explicitConfigDir && explicitConfigDir.trim()) {
62
+ return resolve(explicitConfigDir);
63
+ }
64
+
65
+ const runtimeCanonical = resolve(runtimeRoot, "configs");
66
+ if (runtimeHasConfigDir(runtimeRoot, "configs")) {
67
+ return runtimeCanonical;
68
+ }
69
+
70
+ const projectCanonical = resolve(projectRoot, "configs");
71
+ if (runtimeHasConfigDir(projectRoot, "configs")) {
72
+ return projectCanonical;
73
+ }
74
+
75
+ return runtimeCanonical;
76
+ }
77
+
78
+ function toDisplayPath(currentRoot: string, pathValue: string): string {
79
+ const rel = relative(currentRoot, pathValue);
80
+ if (!rel || rel === "") return ".";
81
+ if (rel.startsWith("..")) return pathValue;
82
+ return rel.replace(/\\/g, "/");
83
+ }
84
+
85
+ function quotePowerShell(pathValue: string): string {
86
+ if (/^[A-Za-z0-9_./\\:-]+$/.test(pathValue)) return pathValue;
87
+ return `'${pathValue.replace(/'/g, "''")}'`;
88
+ }
89
+
90
+ function quoteBash(pathValue: string): string {
91
+ if (/^[A-Za-z0-9_./\\:-]+$/.test(pathValue)) return pathValue;
92
+ return "'" + pathValue.replace(/'/g, "'\"'\"'") + "'";
93
+ }
94
+
95
+ function buildCopyCommands(
96
+ workspaceRoot: string,
97
+ sourcePath: string,
98
+ destPath: string,
99
+ ): ClientPreflightCopyCommands {
100
+ const displaySource = toDisplayPath(workspaceRoot, sourcePath);
101
+ const displayDest = toDisplayPath(workspaceRoot, destPath);
102
+ return {
103
+ windowsPowerShell: `Copy-Item ${quotePowerShell(displaySource)} ${quotePowerShell(displayDest)}`,
104
+ bash: `cp ${quoteBash(displaySource)} ${quoteBash(displayDest)}`,
105
+ };
106
+ }
107
+
108
+ export function evaluateClientRuntimePreflight(
109
+ options: EvaluateClientRuntimePreflightOptions,
110
+ ): ClientRuntimePreflightResult {
111
+ const projectRoot = resolve(options.projectRoot);
112
+ const runtimeRoot = resolve(options.runtimeRoot ?? projectRoot);
113
+ const configDir = resolveClientConfigDir(projectRoot, runtimeRoot, options.configDir);
114
+ const visionTemplateRoot = resolve(options.visionTemplateRoot ?? runtimeRoot);
115
+ const config =
116
+ options.config ??
117
+ loadPushPalsConfig({
118
+ projectRoot,
119
+ configDir,
120
+ reload: true,
121
+ });
122
+
123
+ const issues: ClientPreflightIssue[] = [];
124
+
125
+ const envPath = resolve(runtimeRoot, ".env");
126
+ if (!existsSync(envPath)) {
127
+ const envExamplePath = resolve(runtimeRoot, ".env.example");
128
+ issues.push({
129
+ code: "missing_env_file",
130
+ message: `Missing required local env file: ${toDisplayPath(projectRoot, envPath)}.`,
131
+ copyCommands: existsSync(envExamplePath)
132
+ ? buildCopyCommands(projectRoot, envExamplePath, envPath)
133
+ : undefined,
134
+ });
135
+ }
136
+
137
+ const localTomlPath = resolve(runtimeRoot, "configs", "local.toml");
138
+ if (!existsSync(localTomlPath)) {
139
+ const localExamplePath = resolve(runtimeRoot, "configs", "local.example.toml");
140
+ issues.push({
141
+ code: "missing_local_toml",
142
+ message: `Missing required local config file: ${toDisplayPath(projectRoot, localTomlPath)}.`,
143
+ copyCommands: existsSync(localExamplePath)
144
+ ? buildCopyCommands(projectRoot, localExamplePath, localTomlPath)
145
+ : undefined,
146
+ });
147
+ }
148
+
149
+ const autonomyEnabled = Boolean(config.remotebuddy.autonomy.enabled);
150
+ if (!autonomyEnabled) {
151
+ return {
152
+ ok: issues.length === 0,
153
+ projectRoot,
154
+ runtimeRoot,
155
+ config,
156
+ issues,
157
+ autonomyEnabled,
158
+ visionSummary: null,
159
+ };
160
+ }
161
+
162
+ const visionPath = resolve(projectRoot, "vision.md");
163
+ const visionTemplatePath = resolve(visionTemplateRoot, "vision.example.md");
164
+ if (!existsSync(visionPath)) {
165
+ issues.push({
166
+ code: "missing_vision_doc",
167
+ message:
168
+ "Missing required autonomy vision file: vision.md " +
169
+ "(required when remotebuddy.autonomy.enabled=true).",
170
+ copyCommands: existsSync(visionTemplatePath)
171
+ ? buildCopyCommands(projectRoot, visionTemplatePath, visionPath)
172
+ : undefined,
173
+ });
174
+ return {
175
+ ok: false,
176
+ projectRoot,
177
+ runtimeRoot,
178
+ config,
179
+ issues,
180
+ autonomyEnabled,
181
+ visionSummary: null,
182
+ };
183
+ }
184
+
185
+ let rawVision = "";
186
+ try {
187
+ rawVision = readFileSync(visionPath, "utf8");
188
+ } catch (err) {
189
+ issues.push({
190
+ code: "unreadable_vision_doc",
191
+ message: `Autonomy vision preflight failed: could not read vision.md.`,
192
+ detail: String(err),
193
+ });
194
+ return {
195
+ ok: false,
196
+ projectRoot,
197
+ runtimeRoot,
198
+ config,
199
+ issues,
200
+ autonomyEnabled,
201
+ visionSummary: null,
202
+ };
203
+ }
204
+
205
+ const visionText = rawVision.trim();
206
+ if (!visionText) {
207
+ issues.push({
208
+ code: "empty_vision_doc",
209
+ message: "Autonomy vision preflight failed: vision.md is empty.",
210
+ detail: "Add repository vision/goals before startup.",
211
+ });
212
+ return {
213
+ ok: false,
214
+ projectRoot,
215
+ runtimeRoot,
216
+ config,
217
+ issues,
218
+ autonomyEnabled,
219
+ visionSummary: null,
220
+ };
221
+ }
222
+
223
+ const validation = validateVisionDocStructure(visionText);
224
+ if (!validation.ok) {
225
+ issues.push({
226
+ code: "invalid_vision_doc",
227
+ message: "Autonomy vision preflight failed: vision.md is invalid.",
228
+ detail: validation.errors.join(" "),
229
+ });
230
+ return {
231
+ ok: false,
232
+ projectRoot,
233
+ runtimeRoot,
234
+ config,
235
+ issues,
236
+ autonomyEnabled,
237
+ visionSummary: null,
238
+ };
239
+ }
240
+
241
+ return {
242
+ ok: issues.length === 0,
243
+ projectRoot,
244
+ runtimeRoot,
245
+ config,
246
+ issues,
247
+ autonomyEnabled,
248
+ visionSummary: {
249
+ path: toDisplayPath(projectRoot, visionPath),
250
+ chars: visionText.length,
251
+ sectionCount: validation.sectionCount,
252
+ validation,
253
+ },
254
+ };
255
+ }
256
+
257
+ export function formatClientRuntimePreflightLines(
258
+ result: ClientRuntimePreflightResult,
259
+ prefix: string,
260
+ ): string[] {
261
+ const normalizedPrefix = prefix.trim();
262
+ const lines: string[] = [];
263
+ if (result.ok) {
264
+ if (result.visionSummary) {
265
+ lines.push(
266
+ `${normalizedPrefix} Autonomy preflight: loaded ${result.visionSummary.path} ` +
267
+ `(${result.visionSummary.chars} chars, ${result.visionSummary.sectionCount} section(s)).`,
268
+ );
269
+ }
270
+ return lines;
271
+ }
272
+
273
+ for (const issue of result.issues) {
274
+ lines.push(`${normalizedPrefix} ${issue.message}`);
275
+ if (issue.detail) {
276
+ lines.push(`${normalizedPrefix} ${issue.detail}`);
277
+ }
278
+ if (issue.copyCommands) {
279
+ lines.push(
280
+ `${normalizedPrefix} Windows (PowerShell): ${issue.copyCommands.windowsPowerShell}`,
281
+ );
282
+ lines.push(`${normalizedPrefix} Linux/macOS (bash): ${issue.copyCommands.bash}`);
283
+ }
284
+ }
285
+ return lines;
286
+ }
@@ -0,0 +1,313 @@
1
+ import type { EventEnvelope, EventType, EventTypePayloadMap } from "protocol";
2
+
3
+ type EventMeta = {
4
+ from?: string;
5
+ to?: string;
6
+ correlationId?: string;
7
+ turnId?: string;
8
+ parentId?: string;
9
+ };
10
+
11
+ type SessionEventsOptions = {
12
+ afterCursor?: number;
13
+ reconnectMs?: number;
14
+ onError?: (message: string) => void;
15
+ onOpen?: () => void;
16
+ };
17
+
18
+ function stripPresenceSourcePrefix(value: string): string {
19
+ return value.replace(/^(agent|client)(?:[\s:./_-]+)+/i, "");
20
+ }
21
+
22
+ export function normalizePresenceClientId(value: unknown): string {
23
+ const raw = stripPresenceSourcePrefix(String(value ?? "").trim());
24
+ return raw.replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^_+|_+$/g, "").trim();
25
+ }
26
+
27
+ export function normalizePresenceClientLabel(value: unknown): string {
28
+ return stripPresenceSourcePrefix(String(value ?? ""))
29
+ .replace(/\s+/g, " ")
30
+ .trim();
31
+ }
32
+
33
+ export function normalizePresenceLookupToken(value: unknown): string {
34
+ return normalizePresenceClientLabel(value).toLowerCase().replace(/[^a-z0-9]+/g, "");
35
+ }
36
+
37
+ type SessionTransportPresence = {
38
+ clientId: string;
39
+ kind: string;
40
+ label: string;
41
+ version: string;
42
+ platform: string;
43
+ repoRoot: string;
44
+ };
45
+
46
+ export interface CommunicationManagerOptions {
47
+ serverUrl: string;
48
+ sessionId: string;
49
+ from: string;
50
+ authToken?: string | null;
51
+ }
52
+
53
+ export class CommunicationManager {
54
+ private readonly serverUrl: string;
55
+ private readonly sessionId: string;
56
+ private readonly from: string;
57
+ private readonly authToken: string | null;
58
+
59
+ constructor(opts: CommunicationManagerOptions) {
60
+ this.serverUrl = opts.serverUrl;
61
+ this.sessionId = opts.sessionId;
62
+ this.from = opts.from;
63
+ this.authToken = opts.authToken ?? null;
64
+ }
65
+
66
+ private headers(): Record<string, string> {
67
+ const headers: Record<string, string> = { "Content-Type": "application/json" };
68
+ if (this.authToken) {
69
+ headers.Authorization = `Bearer ${this.authToken}`;
70
+ }
71
+ return headers;
72
+ }
73
+
74
+ private commandUrl(sessionId: string): string {
75
+ return `${this.serverUrl}/sessions/${encodeURIComponent(sessionId)}/command`;
76
+ }
77
+
78
+ private buildSessionTransportPresence(sessionId: string): SessionTransportPresence {
79
+ const normalizedFrom = normalizePresenceClientId(this.from);
80
+ const labelFrom = normalizePresenceClientLabel(this.from);
81
+ const normalizedSessionId = normalizePresenceClientId(sessionId);
82
+ const isDefaultSession = sessionId === this.sessionId;
83
+ const repoRoot = String(
84
+ process.env.PUSHPALS_REPO_ROOT_OVERRIDE ??
85
+ process.env.PUSHPALS_PROJECT_ROOT_OVERRIDE ??
86
+ process.cwd(),
87
+ ).trim();
88
+ return {
89
+ clientId: isDefaultSession
90
+ ? normalizedFrom || "agent"
91
+ : `${normalizedFrom || "agent"}__${normalizedSessionId || "session"}`,
92
+ kind: "agent",
93
+ label: labelFrom || normalizedFrom || "Agent",
94
+ version: String(process.env.PUSHPALS_RUNTIME_TAG ?? process.env.npm_package_version ?? "")
95
+ .trim(),
96
+ platform: `${process.platform}/${process.arch}`,
97
+ repoRoot,
98
+ };
99
+ }
100
+
101
+ async emitToSession<T extends EventType>(
102
+ sessionId: string,
103
+ type: T,
104
+ payload: EventTypePayloadMap[T],
105
+ meta: EventMeta = {},
106
+ ): Promise<boolean> {
107
+ try {
108
+ const body: Record<string, unknown> = {
109
+ type,
110
+ payload: payload as unknown as Record<string, unknown>,
111
+ from: meta.from ?? this.from,
112
+ };
113
+ if (meta.to) body.to = meta.to;
114
+ if (meta.correlationId) body.correlationId = meta.correlationId;
115
+ if (meta.turnId) body.turnId = meta.turnId;
116
+ if (meta.parentId) body.parentId = meta.parentId;
117
+
118
+ const response = await fetch(this.commandUrl(sessionId), {
119
+ method: "POST",
120
+ headers: this.headers(),
121
+ body: JSON.stringify(body),
122
+ });
123
+ return response.ok;
124
+ } catch {
125
+ return false;
126
+ }
127
+ }
128
+
129
+ async emit<T extends EventType>(
130
+ type: T,
131
+ payload: EventTypePayloadMap[T],
132
+ meta: EventMeta = {},
133
+ ): Promise<boolean> {
134
+ return this.emitToSession(this.sessionId, type, payload, meta);
135
+ }
136
+
137
+ async assistantMessageToSession(
138
+ sessionId: string,
139
+ text: string,
140
+ meta: EventMeta = {},
141
+ ): Promise<boolean> {
142
+ return this.emitToSession(sessionId, "assistant_message", { text }, meta);
143
+ }
144
+
145
+ async assistantMessage(text: string, meta: EventMeta = {}): Promise<boolean> {
146
+ return this.assistantMessageToSession(this.sessionId, text, meta);
147
+ }
148
+
149
+ async userMessageToSession(
150
+ sessionId: string,
151
+ text: string,
152
+ meta: EventMeta = {},
153
+ ): Promise<boolean> {
154
+ return this.emitToSession(sessionId, "message", { text }, {
155
+ ...meta,
156
+ from: meta.from ?? "client",
157
+ });
158
+ }
159
+
160
+ async userMessage(text: string, meta: EventMeta = {}): Promise<boolean> {
161
+ return this.userMessageToSession(this.sessionId, text, meta);
162
+ }
163
+
164
+ async taskProgressToSession(
165
+ sessionId: string,
166
+ taskId: string,
167
+ message: string,
168
+ percent?: number,
169
+ meta: EventMeta = {},
170
+ ): Promise<boolean> {
171
+ const payload: EventTypePayloadMap["task_progress"] =
172
+ percent == null ? { taskId, message } : { taskId, message, percent };
173
+ return this.emitToSession(sessionId, "task_progress", payload, meta);
174
+ }
175
+
176
+ async taskProgress(
177
+ taskId: string,
178
+ message: string,
179
+ percent?: number,
180
+ meta: EventMeta = {},
181
+ ): Promise<boolean> {
182
+ return this.taskProgressToSession(this.sessionId, taskId, message, percent, meta);
183
+ }
184
+
185
+ async statusToSession(
186
+ sessionId: string,
187
+ agentId: string,
188
+ state: EventTypePayloadMap["status"]["state"],
189
+ detail?: string,
190
+ meta: EventMeta = {},
191
+ ): Promise<boolean> {
192
+ const payload: EventTypePayloadMap["status"] =
193
+ detail == null ? { agentId, state } : { agentId, state, detail };
194
+ return this.emitToSession(sessionId, "status", payload, meta);
195
+ }
196
+
197
+ async status(
198
+ agentId: string,
199
+ state: EventTypePayloadMap["status"]["state"],
200
+ detail?: string,
201
+ meta: EventMeta = {},
202
+ ): Promise<boolean> {
203
+ return this.statusToSession(this.sessionId, agentId, state, detail, meta);
204
+ }
205
+
206
+ subscribeSessionEventsForSession(
207
+ sessionId: string,
208
+ onEvent: (envelope: EventEnvelope, cursor: number) => void,
209
+ options: SessionEventsOptions = {},
210
+ ): () => void {
211
+ let disposed = false;
212
+ let ws: WebSocket | null = null;
213
+ let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
214
+ let latestCursor = Math.max(0, options.afterCursor ?? 0);
215
+ const reconnectMs = Math.max(500, options.reconnectMs ?? 3000);
216
+ const onError =
217
+ options.onError ??
218
+ (() => {
219
+ // no-op
220
+ });
221
+ const onOpen =
222
+ options.onOpen ??
223
+ (() => {
224
+ // no-op
225
+ });
226
+
227
+ const connect = () => {
228
+ if (disposed) return;
229
+ try {
230
+ const url = new URL(this.serverUrl);
231
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
232
+ url.pathname = `/sessions/${encodeURIComponent(sessionId)}/ws`;
233
+ const presence = this.buildSessionTransportPresence(sessionId);
234
+ if (latestCursor > 0) {
235
+ url.searchParams.set("after", String(latestCursor));
236
+ }
237
+ url.searchParams.set("clientId", presence.clientId);
238
+ url.searchParams.set("clientKind", presence.kind);
239
+ url.searchParams.set("clientLabel", presence.label);
240
+ if (presence.version) {
241
+ url.searchParams.set("clientVersion", presence.version);
242
+ }
243
+ if (presence.platform) {
244
+ url.searchParams.set("clientPlatform", presence.platform);
245
+ }
246
+ if (presence.repoRoot) {
247
+ url.searchParams.set("clientRepoRoot", presence.repoRoot);
248
+ }
249
+ ws = new WebSocket(url.toString());
250
+ } catch (err) {
251
+ onError(`[SessionEvents] Failed to connect: ${String(err)}`);
252
+ if (!disposed) {
253
+ reconnectTimer = setTimeout(connect, reconnectMs);
254
+ }
255
+ return;
256
+ }
257
+
258
+ ws.onmessage = (event: MessageEvent) => {
259
+ try {
260
+ const raw =
261
+ typeof event.data === "string"
262
+ ? (JSON.parse(event.data) as Record<string, unknown>)
263
+ : null;
264
+ if (!raw) return;
265
+ const envelope = (raw.envelope ?? raw) as EventEnvelope;
266
+ const cursor = typeof raw.cursor === "number" ? raw.cursor : 0;
267
+ if (cursor > latestCursor) latestCursor = cursor;
268
+ onEvent(envelope, cursor);
269
+ } catch (err) {
270
+ onError(`[SessionEvents] Parse error: ${String(err)}`);
271
+ }
272
+ };
273
+
274
+ ws.onopen = () => {
275
+ onOpen();
276
+ };
277
+
278
+ ws.onerror = () => {
279
+ onError("[SessionEvents] WebSocket error");
280
+ };
281
+
282
+ ws.onclose = () => {
283
+ ws = null;
284
+ if (!disposed) {
285
+ reconnectTimer = setTimeout(connect, reconnectMs);
286
+ }
287
+ };
288
+ };
289
+
290
+ connect();
291
+
292
+ return () => {
293
+ disposed = true;
294
+ if (reconnectTimer) clearTimeout(reconnectTimer);
295
+ reconnectTimer = null;
296
+ if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
297
+ try {
298
+ ws.close();
299
+ } catch {
300
+ // ignore close errors
301
+ }
302
+ }
303
+ ws = null;
304
+ };
305
+ }
306
+
307
+ subscribeSessionEvents(
308
+ onEvent: (envelope: EventEnvelope, cursor: number) => void,
309
+ options: SessionEventsOptions = {},
310
+ ): () => void {
311
+ return this.subscribeSessionEventsForSession(this.sessionId, onEvent, options);
312
+ }
313
+ }