edsger 0.64.0 → 0.65.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.
@@ -4,12 +4,30 @@
4
4
  * its own line and free of logger formatting so it parses cleanly.
5
5
  */
6
6
  export declare const SESSION_ID_MARKER = "__EDSGER_SESSION_ID__=";
7
+ /**
8
+ * Marker emitted on stdout for each background `cli_*` run still in flight when
9
+ * the turn ends. The desktop main process parses these, watches each run to
10
+ * completion (the turn process itself exits immediately), and then re-engages
11
+ * the agent via `--run-finished` so it reports results without the user having
12
+ * to ask. The JSON payload is `{ run_id, pid, log_path, command }`.
13
+ */
14
+ export declare const CLI_RUN_MARKER = "__EDSGER_CLI_RUN__=";
7
15
  export interface SessionTurnCliOptions {
8
16
  channelId: string;
9
17
  productId: string;
10
18
  repositoryIds?: string[];
11
19
  /** SDK session id from a previous turn, to resume the same conversation. */
12
20
  resumeSessionId?: string;
21
+ /**
22
+ * Follow-up turn triggered by the desktop watcher when a background `cli_*`
23
+ * run finishes. When set, the turn is driven by a synthetic prompt instead of
24
+ * the channel's pending human messages: the agent inspects what the run
25
+ * produced and reports it to the user.
26
+ */
27
+ runFinished?: {
28
+ runId: string;
29
+ command?: string;
30
+ };
13
31
  verbose?: boolean;
14
32
  }
15
33
  export declare function runSessionTurnCommand(options: SessionTurnCliOptions): Promise<void>;
@@ -4,6 +4,14 @@
4
4
  * its own line and free of logger formatting so it parses cleanly.
5
5
  */
6
6
  export const SESSION_ID_MARKER = '__EDSGER_SESSION_ID__=';
7
+ /**
8
+ * Marker emitted on stdout for each background `cli_*` run still in flight when
9
+ * the turn ends. The desktop main process parses these, watches each run to
10
+ * completion (the turn process itself exits immediately), and then re-engages
11
+ * the agent via `--run-finished` so it reports results without the user having
12
+ * to ask. The JSON payload is `{ run_id, pid, log_path, command }`.
13
+ */
14
+ export const CLI_RUN_MARKER = '__EDSGER_CLI_RUN__=';
7
15
  /**
8
16
  * `edsger session-turn <channelId>` — run one conversational agent turn for a
9
17
  * chat session (an ai_assistant channel bound to a product).
@@ -17,7 +25,7 @@ export const SESSION_ID_MARKER = '__EDSGER_SESSION_ID__=';
17
25
  * there is no fixed pipeline.
18
26
  */
19
27
  import { query } from '@anthropic-ai/claude-agent-sdk';
20
- import { buildSessionAgentOptions, buildSessionSystemPrompt, buildSessionUserPrompt, createSessionMcpServer, loadExternalMcpServers, SESSION_MAX_TURNS, } from 'edsger-tools';
28
+ import { buildSessionAgentOptions, buildSessionSystemPrompt, buildSessionUserPrompt, createSessionMcpServer, listActiveCliRuns, loadExternalMcpServers, SESSION_MAX_TURNS, } from 'edsger-tools';
21
29
  import { callMcpEndpoint } from '../../api/mcp-client.js';
22
30
  import { DEFAULT_MODEL } from '../../constants.js';
23
31
  import { getToolDeps } from '../../tools/bootstrap.js';
@@ -46,23 +54,44 @@ async function prepareSessionWorkspace(opts) {
46
54
  };
47
55
  }
48
56
  export async function runSessionTurnCommand(options) {
49
- const { channelId, productId, repositoryIds = [], resumeSessionId, verbose = false, } = options;
57
+ const { channelId, productId, repositoryIds = [], resumeSessionId, runFinished, verbose = false, } = options;
50
58
  // Emit the SDK session id so the desktop can persist it for the next turn.
51
59
  const emitSessionId = (id) => {
52
60
  if (id) {
53
61
  process.stdout.write(`\n${SESSION_ID_MARKER}${id}\n`);
54
62
  }
55
63
  };
56
- // 1. Load the pending human messages that triggered this turn.
57
- const pendingResult = (await callMcpEndpoint('chat/messages/pending', {
58
- channel_id: channelId,
59
- }));
60
- const messages = pendingResult.messages ?? [];
61
- if (messages.length === 0) {
62
- logInfo('No pending messages for this session — nothing to do.');
63
- return;
64
+ // Emit a marker for every background cli_* run still in flight, so the
65
+ // desktop watcher can poll each to completion and re-engage the agent.
66
+ const emitActiveCliRuns = () => {
67
+ for (const run of listActiveCliRuns()) {
68
+ const payload = JSON.stringify({
69
+ run_id: run.run_id,
70
+ pid: run.pid,
71
+ log_path: run.log_path,
72
+ command: run.command,
73
+ });
74
+ process.stdout.write(`\n${CLI_RUN_MARKER}${payload}\n`);
75
+ }
76
+ };
77
+ // 1. Decide what drives this turn. A normal turn replays the channel's
78
+ // pending human messages; a watcher-triggered follow-up replays a
79
+ // synthetic prompt about the background run that just finished.
80
+ let messages = [];
81
+ if (runFinished) {
82
+ logInfo(`Reporting completion of background run ${runFinished.runId} for session ${channelId}`);
83
+ }
84
+ else {
85
+ const pendingResult = (await callMcpEndpoint('chat/messages/pending', {
86
+ channel_id: channelId,
87
+ }));
88
+ messages = pendingResult.messages ?? [];
89
+ if (messages.length === 0) {
90
+ logInfo('No pending messages for this session — nothing to do.');
91
+ return;
92
+ }
93
+ logInfo(`Processing ${messages.length} message(s) for session ${channelId}`);
64
94
  }
65
- logInfo(`Processing ${messages.length} message(s) for session ${channelId}`);
66
95
  // 2. Clone the product's in-scope repositories into a local session
67
96
  // directory so the agent's Read/Grep/Glob can inspect the real code.
68
97
  const { sessionDir, repoScopeNote } = await prepareSessionWorkspace({
@@ -89,7 +118,9 @@ export async function runSessionTurnCommand(options) {
89
118
  repoScopeNote,
90
119
  externalMcpNames: external.names,
91
120
  });
92
- const userPrompt = buildSessionUserPrompt(messages);
121
+ const userPrompt = runFinished
122
+ ? buildRunFinishedPrompt(runFinished)
123
+ : buildSessionUserPrompt(messages);
93
124
  // 4. Run one SDK turn. Resume the prior SDK session when we have its id so
94
125
  // the conversation (and prompt cache) carries across turns. Point the
95
126
  // agent's cwd at the session directory so its read-only file tools resolve
@@ -137,6 +168,7 @@ export async function runSessionTurnCommand(options) {
137
168
  metadata: {},
138
169
  }).catch(() => undefined);
139
170
  emitSessionId(sdkSessionId);
171
+ emitActiveCliRuns();
140
172
  await markProcessed(messages);
141
173
  return;
142
174
  }
@@ -153,12 +185,27 @@ export async function runSessionTurnCommand(options) {
153
185
  logError(`Failed to post reply: ${e instanceof Error ? e.message : String(e)}`);
154
186
  });
155
187
  }
156
- // 6. Mark the triggering messages processed so they aren't re-run.
188
+ // 6. Mark the triggering messages processed so they aren't re-run. (No-op for
189
+ // a watcher follow-up, which has no triggering human messages.)
157
190
  await markProcessed(messages);
158
- // 7. Emit the SDK session id so the desktop persists it for the next turn.
191
+ // 7. Emit the SDK session id so the desktop persists it for the next turn,
192
+ // plus a marker for any background run launched this turn so the watcher
193
+ // keeps following it to completion.
159
194
  emitSessionId(sdkSessionId);
195
+ emitActiveCliRuns();
160
196
  logSuccess('Session turn complete.');
161
197
  }
198
+ /**
199
+ * The synthetic user prompt for a watcher-triggered follow-up turn. Steers the
200
+ * agent to inspect what the finished run produced and report it — explicitly
201
+ * NOT to ask the user whether it should check.
202
+ */
203
+ function buildRunFinishedPrompt(runFinished) {
204
+ const which = runFinished.command
205
+ ? `the \`${runFinished.command}\` analysis (run ${runFinished.runId})`
206
+ : `a background analysis you launched earlier (run ${runFinished.runId})`;
207
+ return `${which} just finished. Inspect what it produced — call get_cli_run with run_id "${runFinished.runId}" for the log tail, then use the list_* / get_* tools (e.g. list_issues) to see the findings it filed. Summarize the results for the user via send_chat_message. Do not ask whether you should check — just report what you found. If nothing of note was produced, say so briefly.`;
208
+ }
162
209
  async function markProcessed(messages) {
163
210
  for (const m of messages) {
164
211
  await callMcpEndpoint('chat/messages/mark_processed', {
package/dist/index.js CHANGED
@@ -441,6 +441,8 @@ program
441
441
  .requiredOption('--product <productId>', 'Product the session is bound to')
442
442
  .option('--repos <ids>', 'Comma-separated repository IDs the agent may touch', '')
443
443
  .option('--resume <sessionId>', 'SDK session id from a previous turn to resume the same conversation')
444
+ .option('--run-finished <runId>', 'Follow-up turn: report the results of a background cli_* run that finished')
445
+ .option('--run-command <command>', 'The command name of the finished run (used with --run-finished)')
444
446
  .option('-v, --verbose', 'Verbose output')
445
447
  .action(async (channelId, opts) => {
446
448
  try {
@@ -452,6 +454,9 @@ program
452
454
  .map((s) => s.trim())
453
455
  .filter(Boolean),
454
456
  resumeSessionId: opts.resume,
457
+ runFinished: opts.runFinished
458
+ ? { runId: opts.runFinished, command: opts.runCommand }
459
+ : undefined,
455
460
  verbose: opts.verbose,
456
461
  });
457
462
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edsger",
3
- "version": "0.64.0",
3
+ "version": "0.65.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "edsger": "dist/index.js"
@@ -51,7 +51,7 @@
51
51
  "cosmiconfig": "^9.0.0",
52
52
  "dotenv": "^16.4.5",
53
53
  "edsger-contract": "0.7.0",
54
- "edsger-tools": "0.7.0",
54
+ "edsger-tools": "0.8.0",
55
55
  "gray-matter": "^4.0.3",
56
56
  "zod": "^4.0.0"
57
57
  },