@nordbyte/nordrelay 0.5.1 → 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.
Files changed (57) hide show
  1. package/.env.example +65 -11
  2. package/README.md +97 -23
  3. package/dist/access-control.js +1 -0
  4. package/dist/activity-events.js +44 -0
  5. package/dist/agent-updates.js +18 -2
  6. package/dist/audit-log.js +40 -2
  7. package/dist/bot-rendering.js +10 -7
  8. package/dist/bot.js +492 -7
  9. package/dist/channel-actions.js +7 -2
  10. package/dist/channel-adapter.js +34 -7
  11. package/dist/channel-command-service.js +156 -0
  12. package/dist/channel-turn-service.js +237 -0
  13. package/dist/codex-cli.js +1 -1
  14. package/dist/config-metadata.js +80 -13
  15. package/dist/config.js +77 -7
  16. package/dist/context-key.js +77 -5
  17. package/dist/discord-artifacts.js +165 -0
  18. package/dist/discord-bot.js +2014 -0
  19. package/dist/discord-channel-runtime.js +133 -0
  20. package/dist/discord-command-surface.js +119 -0
  21. package/dist/discord-rate-limit.js +141 -0
  22. package/dist/index.js +16 -5
  23. package/dist/job-store.js +127 -0
  24. package/dist/metrics.js +41 -0
  25. package/dist/operations.js +176 -119
  26. package/dist/relay-external-activity-monitor.js +47 -6
  27. package/dist/relay-runtime.js +1003 -268
  28. package/dist/runtime-cache.js +57 -0
  29. package/dist/session-locks.js +10 -7
  30. package/dist/state-backend.js +3 -0
  31. package/dist/support-bundle.js +18 -1
  32. package/dist/telegram-access-commands.js +15 -2
  33. package/dist/telegram-access-middleware.js +16 -3
  34. package/dist/telegram-agent-commands.js +25 -0
  35. package/dist/telegram-artifact-commands.js +46 -0
  36. package/dist/telegram-diagnostics-command.js +5 -50
  37. package/dist/telegram-general-commands.js +2 -6
  38. package/dist/telegram-operational-commands.js +14 -6
  39. package/dist/telegram-queue-commands.js +74 -4
  40. package/dist/telegram-support-command.js +7 -0
  41. package/dist/telegram-update-commands.js +27 -0
  42. package/dist/user-management.js +208 -0
  43. package/dist/web-api-contract.js +9 -0
  44. package/dist/web-dashboard-access-routes.js +74 -1
  45. package/dist/web-dashboard-artifact-routes.js +3 -3
  46. package/dist/web-dashboard-assets.js +2 -0
  47. package/dist/web-dashboard-pages.js +97 -13
  48. package/dist/web-dashboard-runtime-routes.js +53 -8
  49. package/dist/web-dashboard-session-routes.js +27 -20
  50. package/dist/web-dashboard-ui.js +1 -0
  51. package/dist/web-dashboard.js +149 -6
  52. package/dist/web-state.js +33 -2
  53. package/dist/webui-assets/dashboard.css +75 -1
  54. package/dist/webui-assets/dashboard.js +358 -47
  55. package/package.json +3 -1
  56. package/plugins/nordrelay/.codex-plugin/plugin.json +1 -1
  57. package/plugins/nordrelay/scripts/nordrelay.mjs +468 -22
@@ -7,7 +7,9 @@ export function renderChannelsAction(descriptors) {
7
7
  const plain = [
8
8
  "Channel adapters:",
9
9
  ...descriptors.map((descriptor) => {
10
- const status = descriptor.status === "available" ? "available" : "planned";
10
+ const status = descriptor.status === "available"
11
+ ? descriptor.enabled === false ? "available / disabled" : "available / enabled"
12
+ : "planned";
11
13
  return `${descriptor.label}: ${status} · ${descriptor.capabilities.join(", ")}`;
12
14
  }),
13
15
  ].join("\n");
@@ -15,8 +17,11 @@ export function renderChannelsAction(descriptors) {
15
17
  "<b>Channel adapters:</b>",
16
18
  ...descriptors.map((descriptor) => {
17
19
  const statusIcon = descriptor.status === "available" ? "✅" : "🟡";
20
+ const status = descriptor.status === "available"
21
+ ? descriptor.enabled === false ? "available / disabled" : "available / enabled"
22
+ : descriptor.status;
18
23
  const notes = descriptor.notes ? `\n ${escapeHTML(descriptor.notes)}` : "";
19
- return `${statusIcon} <b>${escapeHTML(descriptor.label)}</b> <code>${escapeHTML(descriptor.status)}</code>\n <code>${escapeHTML(descriptor.capabilities.join(", "))}</code>${notes}`;
24
+ return `${statusIcon} <b>${escapeHTML(descriptor.label)}</b> <code>${escapeHTML(status)}</code>\n <code>${escapeHTML(descriptor.capabilities.join(", "))}</code>${notes}`;
20
25
  }),
21
26
  ].join("\n");
22
27
  return { plain, html };
@@ -9,14 +9,17 @@ const TELEGRAM_CAPABILITIES = [
9
9
  "topics",
10
10
  "webhooks",
11
11
  ];
12
+ const DISCORD_CAPABILITIES = [
13
+ "text",
14
+ "streaming-edits",
15
+ "typing",
16
+ "inline-buttons",
17
+ "files",
18
+ "photos",
19
+ "voice",
20
+ "topics",
21
+ ];
12
22
  const PLANNED_CHANNELS = [
13
- {
14
- id: "discord",
15
- label: "Discord",
16
- capabilities: ["text", "streaming-edits", "typing", "inline-buttons", "files", "photos", "voice"],
17
- status: "planned",
18
- notes: "Adapter boundary is ready; runtime integration still needs bot credentials and event mapping.",
19
- },
20
23
  {
21
24
  id: "whatsapp",
22
25
  label: "WhatsApp",
@@ -42,17 +45,41 @@ export class TelegramChannelAdapter {
42
45
  label = "Telegram";
43
46
  capabilities = new Set(TELEGRAM_CAPABILITIES);
44
47
  describe() {
48
+ const enabled = process.env.TELEGRAM_ENABLED !== "false";
49
+ return {
50
+ id: this.id,
51
+ label: this.label,
52
+ capabilities: [...this.capabilities],
53
+ status: "available",
54
+ enabled,
55
+ notes: enabled
56
+ ? "Telegram bot runtime is enabled by default."
57
+ : "Telegram bot runtime is disabled.",
58
+ };
59
+ }
60
+ }
61
+ export class DiscordChannelAdapter {
62
+ id = "discord";
63
+ label = "Discord";
64
+ capabilities = new Set(DISCORD_CAPABILITIES);
65
+ describe() {
66
+ const enabled = process.env.DISCORD_ENABLED === "true";
45
67
  return {
46
68
  id: this.id,
47
69
  label: this.label,
48
70
  capabilities: [...this.capabilities],
49
71
  status: "available",
72
+ enabled,
73
+ notes: enabled
74
+ ? "Discord bot runtime is enabled."
75
+ : "Enable with DISCORD_ENABLED=true and DISCORD_BOT_TOKEN.",
50
76
  };
51
77
  }
52
78
  }
53
79
  export function listChannelDescriptors() {
54
80
  return [
55
81
  new TelegramChannelAdapter().describe(),
82
+ new DiscordChannelAdapter().describe(),
56
83
  ...PLANNED_CHANNELS,
57
84
  ];
58
85
  }
@@ -0,0 +1,156 @@
1
+ import { listAgentAdapterDescriptors } from "./agent-adapter.js";
2
+ import { enabledAgents } from "./agent-factory.js";
3
+ import { logTailRequests, parseLogsCommand, renderAgentsAction, renderChannelsAction, renderLogTailsAction, } from "./channel-actions.js";
4
+ import { listChannelDescriptors } from "./channel-adapter.js";
5
+ import { escapeHTML } from "./format.js";
6
+ import { getConnectorHealth, getVersionChecks, readConnectorState, readFormattedLogTail, } from "./operations.js";
7
+ import { formatCliPathHTML, formatCliPathPlain, renderActivityTimeline, renderAuditEvents, renderProgressHTML, renderProgressPlain, renderVersionCheckHTML, renderVersionCheckPlain, } from "./bot-rendering.js";
8
+ import { renderSessionInfoHTML, renderSessionInfoPlain } from "./session-format.js";
9
+ export class ChannelCommandService {
10
+ config;
11
+ constructor(config) {
12
+ this.config = config;
13
+ }
14
+ renderChannels() {
15
+ return renderChannelsAction(listChannelDescriptors());
16
+ }
17
+ renderAgents(agentIds = enabledAgents(this.config)) {
18
+ return renderAgentsAction(listAgentAdapterDescriptors(), agentIds);
19
+ }
20
+ async renderLogs(argument) {
21
+ const logRequest = parseLogsCommand(argument);
22
+ const logs = await Promise.all(logTailRequests(logRequest.target).map(async (request) => ({
23
+ title: request.title,
24
+ tail: await readFormattedLogTail(logRequest.lines, request.path),
25
+ })));
26
+ return renderLogTailsAction(logs);
27
+ }
28
+ async renderVersion() {
29
+ const health = await getConnectorHealth(cliPathOptions(this.config));
30
+ const state = await readConnectorState();
31
+ const versions = await getVersionChecks(cliPathOptions(this.config));
32
+ const plain = [
33
+ renderVersionCheckPlain(versions.nordrelay),
34
+ `Runtime status: ${state.status ?? "unknown"}`,
35
+ formatCliPathPlain("Codex CLI", health.codexCliPath, health.codexCli),
36
+ renderVersionCheckPlain(versions.codex),
37
+ formatCliPathPlain("Pi CLI", health.piCliPath, health.piCli),
38
+ renderVersionCheckPlain(versions.pi),
39
+ formatCliPathPlain("Hermes CLI", health.hermesCliPath, health.hermesCli),
40
+ renderVersionCheckPlain(versions.hermes),
41
+ formatCliPathPlain("OpenClaw CLI", health.openClawCliPath, health.openClawCli),
42
+ renderVersionCheckPlain(versions.openclaw),
43
+ formatCliPathPlain("Claude Code CLI", health.claudeCodeCliPath, health.claudeCodeCli),
44
+ renderVersionCheckPlain(versions.claudeCode),
45
+ ].join("\n");
46
+ const html = [
47
+ renderVersionCheckHTML(versions.nordrelay),
48
+ `<b>Runtime status:</b> <code>${escapeHTML(state.status ?? "unknown")}</code>`,
49
+ formatCliPathHTML("Codex CLI", health.codexCliPath, health.codexCli),
50
+ renderVersionCheckHTML(versions.codex),
51
+ formatCliPathHTML("Pi CLI", health.piCliPath, health.piCli),
52
+ renderVersionCheckHTML(versions.pi),
53
+ formatCliPathHTML("Hermes CLI", health.hermesCliPath, health.hermesCli),
54
+ renderVersionCheckHTML(versions.hermes),
55
+ formatCliPathHTML("OpenClaw CLI", health.openClawCliPath, health.openClawCli),
56
+ renderVersionCheckHTML(versions.openclaw),
57
+ formatCliPathHTML("Claude Code CLI", health.claudeCodeCliPath, health.claudeCodeCli),
58
+ renderVersionCheckHTML(versions.claudeCode),
59
+ ].join("\n");
60
+ return { plain, html };
61
+ }
62
+ renderAuthStatus(status) {
63
+ const icon = status.authenticated ? "✅" : "❌";
64
+ return {
65
+ plain: [
66
+ `${icon} ${status.label} auth: ${status.authenticated ? "authenticated" : "not authenticated"}`,
67
+ `Method: ${status.method ?? "-"}`,
68
+ `Detail: ${status.detail}`,
69
+ ].join("\n"),
70
+ html: [
71
+ `<b>${icon} ${escapeHTML(status.label)} auth:</b> <code>${status.authenticated ? "authenticated" : "not authenticated"}</code>`,
72
+ `<b>Method:</b> <code>${escapeHTML(status.method ?? "-")}</code>`,
73
+ `<b>Detail:</b> <code>${escapeHTML(status.detail)}</code>`,
74
+ ].join("\n"),
75
+ };
76
+ }
77
+ renderAuthActionResult(action, result) {
78
+ const label = action === "login" ? "Login" : "Logout";
79
+ const icon = result.success ? "✅" : "❌";
80
+ return {
81
+ plain: [`${icon} ${label} ${result.success ? "started" : "failed"}.`, "", result.message].join("\n"),
82
+ html: [`<b>${icon} ${escapeHTML(label)} ${result.success ? "started" : "failed"}.</b>`, "", `<code>${escapeHTML(result.message)}</code>`].join("\n"),
83
+ };
84
+ }
85
+ renderHostAuthInstruction(label, command, action) {
86
+ const text = `${label} ${action} is not managed remotely. Run this on the host: ${command}`;
87
+ return {
88
+ plain: text,
89
+ html: `<b>${escapeHTML(label)} ${escapeHTML(action)} is not managed remotely.</b>\nRun this on the host:\n<code>${escapeHTML(command)}</code>`,
90
+ };
91
+ }
92
+ renderProgress(progress, queueLength, busyState, info) {
93
+ return {
94
+ plain: renderProgressPlain(progress, queueLength, busyState, info),
95
+ html: renderProgressHTML(progress, queueLength, busyState, info),
96
+ };
97
+ }
98
+ renderActivity(threadId, events, options) {
99
+ return renderActivityTimeline(threadId, events, options);
100
+ }
101
+ renderAudit(events) {
102
+ return renderAuditEvents(events);
103
+ }
104
+ renderWorkspaces(info, workspaces) {
105
+ const unique = [...new Set(workspaces)].filter(Boolean);
106
+ const rows = unique.length > 0
107
+ ? unique.map((workspace, index) => `${index + 1}. ${workspace}${workspace === info.workspace ? " (current)" : ""}`)
108
+ : [`No workspaces found in ${info.agentLabel} state.`];
109
+ return {
110
+ plain: [`${info.agentLabel} workspaces:`, ...rows].join("\n"),
111
+ html: [
112
+ `<b>${escapeHTML(info.agentLabel)} workspaces:</b>`,
113
+ ...rows.map((line) => `<code>${escapeHTML(line)}</code>`),
114
+ ].join("\n"),
115
+ };
116
+ }
117
+ renderHandback(result) {
118
+ const command = result.command ?? (result.threadId
119
+ ? `cd ${shellEscape(result.workspace)} && codex resume ${shellEscape(result.threadId)}`
120
+ : "");
121
+ if (!result.threadId || !command) {
122
+ const text = "This thread has not started yet, so there is no resumable thread ID. Send a message to create one, or start a new session.";
123
+ return { plain: text, html: escapeHTML(text) };
124
+ }
125
+ const label = result.label ?? "Agent CLI";
126
+ return {
127
+ plain: [
128
+ `Thread handed back to ${label}.`,
129
+ "",
130
+ "Run this in your terminal:",
131
+ command,
132
+ "",
133
+ "Send any message here to start a new NordRelay thread.",
134
+ ].join("\n"),
135
+ html: [
136
+ `<b>Thread handed back to ${escapeHTML(label)}.</b>`,
137
+ "",
138
+ "Run this in your terminal:",
139
+ `<pre>${escapeHTML(command)}</pre>`,
140
+ "",
141
+ "Send any message here to start a new NordRelay thread.",
142
+ ].join("\n"),
143
+ };
144
+ }
145
+ }
146
+ export function cliPathOptions(config) {
147
+ return {
148
+ piCliPath: config.piCliPath,
149
+ hermesCliPath: config.hermesCliPath,
150
+ openClawCliPath: config.openClawCliPath,
151
+ claudeCodeCliPath: config.claudeCodeCliPath,
152
+ };
153
+ }
154
+ function shellEscape(value) {
155
+ return `'${value.replace(/'/g, `'\\''`)}'`;
156
+ }
@@ -0,0 +1,237 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { CODEX_AGENT_CAPABILITIES, agentLabel, } from "./agent.js";
3
+ import { friendlyErrorText } from "./error-messages.js";
4
+ export class ChannelTurnService {
5
+ options;
6
+ constructor(options) {
7
+ this.options = options;
8
+ }
9
+ async run(session, envelope) {
10
+ const actor = envelope.activityActor;
11
+ await this.options.ensureActiveThread(session);
12
+ const info = session.getInfo();
13
+ if ((info.capabilities ?? CODEX_AGENT_CAPABILITIES).auth) {
14
+ const auth = await this.options.checkAuth(info);
15
+ if (!auth.authenticated) {
16
+ throw new Error(`${agentLabel(info.agentId)} is not authenticated: ${auth.detail}`);
17
+ }
18
+ }
19
+ const turnId = randomUUID().slice(0, 12);
20
+ const startedMs = Date.now();
21
+ this.options.setCurrentTurn(turnId, startedMs, "");
22
+ this.options.setCurrentProgress({
23
+ id: turnId,
24
+ source: this.options.source,
25
+ status: "running",
26
+ prompt: envelope.description,
27
+ agentId: info.agentId,
28
+ agentLabel: info.agentLabel,
29
+ threadId: info.threadId,
30
+ workspace: info.workspace,
31
+ startedAt: new Date(startedMs).toISOString(),
32
+ updatedAt: new Date(startedMs).toISOString(),
33
+ durationMs: 0,
34
+ outputChars: 0,
35
+ tools: [],
36
+ });
37
+ this.options.setLastPrompt(envelope);
38
+ const startedDate = new Date();
39
+ const startedAt = startedDate.toISOString();
40
+ this.options.chatStore.append({
41
+ threadId: info.threadId ?? "pending",
42
+ role: "user",
43
+ text: envelope.description,
44
+ source: this.options.source,
45
+ turnId,
46
+ timestamp: startedAt,
47
+ });
48
+ this.options.appendActivity({
49
+ source: this.options.source,
50
+ status: "running",
51
+ type: "prompt_started",
52
+ threadId: info.threadId,
53
+ workspace: info.workspace,
54
+ agentId: info.agentId,
55
+ actor,
56
+ prompt: envelope.description,
57
+ });
58
+ this.options.appendAudit({
59
+ action: "prompt_started",
60
+ status: "ok",
61
+ contextKey: this.options.contextKey,
62
+ agentId: info.agentId,
63
+ threadId: info.threadId,
64
+ workspace: info.workspace,
65
+ actor,
66
+ description: envelope.description,
67
+ });
68
+ this.options.broadcast({ type: "turn_start", id: turnId, prompt: envelope.description, at: startedAt, source: this.options.source });
69
+ try {
70
+ await session.prompt(envelope.input, this.callbacks(turnId, info, envelope, actor));
71
+ this.options.updateSession(session);
72
+ await this.options.artifactService.persistWorkspaceArtifactsForTurn(session.getInfo().workspace, turnId, startedDate);
73
+ const text = this.options.getAccumulatedText();
74
+ if (text.trim()) {
75
+ this.options.chatStore.append({
76
+ threadId: info.threadId ?? "pending",
77
+ role: "agent",
78
+ text,
79
+ source: this.options.source,
80
+ turnId,
81
+ });
82
+ }
83
+ this.options.appendActivity({
84
+ source: this.options.source,
85
+ status: "completed",
86
+ type: "prompt_completed",
87
+ threadId: info.threadId,
88
+ workspace: info.workspace,
89
+ agentId: info.agentId,
90
+ actor,
91
+ prompt: envelope.description,
92
+ durationMs: Date.now() - this.options.getCurrentTurnStartedAt(),
93
+ });
94
+ this.options.appendAudit({
95
+ action: "prompt_completed",
96
+ status: "ok",
97
+ contextKey: this.options.contextKey,
98
+ agentId: info.agentId,
99
+ threadId: info.threadId,
100
+ workspace: info.workspace,
101
+ actor,
102
+ description: envelope.description,
103
+ });
104
+ this.updateCurrentProgress({ status: "completed" });
105
+ this.options.broadcast({ type: "turn_complete", id: turnId, at: new Date().toISOString() });
106
+ this.options.broadcast({ type: "chat_history", messages: await this.options.chatHistory() });
107
+ }
108
+ catch (error) {
109
+ const errorText = friendlyErrorText(error);
110
+ this.options.chatStore.append({
111
+ threadId: info.threadId ?? "pending",
112
+ role: "system",
113
+ text: `Error: ${errorText}`,
114
+ source: this.options.source,
115
+ turnId,
116
+ });
117
+ this.options.appendActivity({
118
+ source: this.options.source,
119
+ status: "failed",
120
+ type: "prompt_failed",
121
+ threadId: info.threadId,
122
+ workspace: info.workspace,
123
+ agentId: info.agentId,
124
+ actor,
125
+ prompt: envelope.description,
126
+ detail: errorText,
127
+ durationMs: Date.now() - this.options.getCurrentTurnStartedAt(),
128
+ });
129
+ this.options.appendAudit({
130
+ action: "prompt_failed",
131
+ status: "failed",
132
+ contextKey: this.options.contextKey,
133
+ agentId: info.agentId,
134
+ threadId: info.threadId,
135
+ workspace: info.workspace,
136
+ actor,
137
+ description: envelope.description,
138
+ detail: errorText,
139
+ });
140
+ this.updateCurrentProgress({ status: "failed", detail: errorText });
141
+ this.options.broadcast({ type: "turn_error", id: turnId, error: errorText, at: new Date().toISOString() });
142
+ this.options.broadcast({ type: "chat_history", messages: await this.options.chatHistory() });
143
+ throw error;
144
+ }
145
+ finally {
146
+ const progress = this.options.getCurrentProgress();
147
+ if (progress) {
148
+ progress.durationMs = Date.now() - this.options.getCurrentTurnStartedAt();
149
+ progress.updatedAt = new Date().toISOString();
150
+ }
151
+ this.options.setCurrentTurn(null);
152
+ }
153
+ }
154
+ callbacks(turnId, info, envelope, actor) {
155
+ return {
156
+ onTextDelta: (delta) => {
157
+ const nextText = this.options.getAccumulatedText() + delta;
158
+ this.options.setAccumulatedText(nextText);
159
+ this.updateCurrentProgress({ outputChars: nextText.length });
160
+ this.options.broadcast({ type: "text_delta", id: turnId, delta });
161
+ },
162
+ onToolStart: (toolName, toolCallId) => {
163
+ this.addCurrentTool(toolName);
164
+ this.options.appendActivity({
165
+ source: this.options.source,
166
+ status: "running",
167
+ type: "tool_started",
168
+ threadId: info.threadId,
169
+ workspace: info.workspace,
170
+ agentId: info.agentId,
171
+ actor,
172
+ prompt: envelope.description,
173
+ detail: toolName,
174
+ });
175
+ this.options.broadcast({ type: "tool_start", id: turnId, toolCallId, toolName });
176
+ },
177
+ onToolUpdate: (toolCallId, partialResult) => {
178
+ this.updateCurrentProgress();
179
+ this.options.broadcast({ type: "tool_update", id: turnId, toolCallId, partialResult });
180
+ },
181
+ onToolEnd: (toolCallId, isError) => {
182
+ const progress = this.options.getCurrentProgress();
183
+ const toolName = progress?.currentTool ?? progress?.lastTool ?? toolCallId;
184
+ this.updateCurrentProgress({ currentTool: undefined });
185
+ this.options.appendActivity({
186
+ source: this.options.source,
187
+ status: isError ? "failed" : "completed",
188
+ type: isError ? "tool_failed" : "tool_completed",
189
+ threadId: info.threadId,
190
+ workspace: info.workspace,
191
+ agentId: info.agentId,
192
+ actor,
193
+ prompt: envelope.description,
194
+ detail: toolName,
195
+ });
196
+ this.options.broadcast({ type: "tool_end", id: turnId, toolCallId, isError });
197
+ },
198
+ onTodoUpdate: (items) => {
199
+ this.updateCurrentProgress({ detail: `Plan: ${items.filter((item) => item.completed).length}/${items.length} done` });
200
+ this.options.broadcast({ type: "todo_update", id: turnId, items });
201
+ },
202
+ onTurnComplete: () => { },
203
+ onAgentEnd: () => this.options.broadcast({ type: "turn_complete", id: turnId, at: new Date().toISOString() }),
204
+ };
205
+ }
206
+ updateCurrentProgress(patch = {}) {
207
+ const progress = this.options.getCurrentProgress();
208
+ if (!progress) {
209
+ return;
210
+ }
211
+ if ("currentTool" in patch) {
212
+ progress.currentTool = patch.currentTool;
213
+ const { currentTool: _currentTool, ...rest } = patch;
214
+ Object.assign(progress, rest);
215
+ }
216
+ else {
217
+ Object.assign(progress, patch);
218
+ }
219
+ progress.durationMs = Date.now() - this.options.getCurrentTurnStartedAt();
220
+ progress.updatedAt = new Date().toISOString();
221
+ this.options.setCurrentProgress(progress);
222
+ }
223
+ addCurrentTool(toolName) {
224
+ const progress = this.options.getCurrentProgress();
225
+ if (!progress) {
226
+ return;
227
+ }
228
+ const existing = progress.tools.find((tool) => tool.name === toolName);
229
+ if (existing) {
230
+ existing.count += 1;
231
+ }
232
+ else {
233
+ progress.tools.push({ name: toolName, count: 1 });
234
+ }
235
+ this.updateCurrentProgress({ currentTool: toolName, lastTool: toolName });
236
+ }
237
+ }
package/dist/codex-cli.js CHANGED
@@ -26,7 +26,7 @@ export function findExecutableOnPath(command, pathValue) {
26
26
  return undefined;
27
27
  }
28
28
  const extensions = process.platform === "win32"
29
- ? (process.env.PATHEXT || ".EXE;.CMD;.BAT;.COM").split(";")
29
+ ? ["", ...(process.env.PATHEXT || ".EXE;.CMD;.BAT;.COM").split(";")]
30
30
  : [""];
31
31
  for (const rawDirectory of pathValue.split(path.delimiter)) {
32
32
  const directory = rawDirectory.trim();