edsger 0.64.0 → 0.66.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.
@@ -0,0 +1,32 @@
1
+ /**
2
+ * `edsger session-serve <channelId>` — long-lived conversational agent for one
3
+ * chat session.
4
+ *
5
+ * Where `session-turn` runs a single turn and exits, `session-serve` prepares
6
+ * the session context once (repo clone, MCP toolbelt, system prompt) and then
7
+ * stays alive, running one turn per command it reads from stdin. The SDK
8
+ * session id is kept in memory and carried into the next turn automatically, so
9
+ * the conversation (and prompt cache) stays warm without a per-turn cold start.
10
+ *
11
+ * Protocol — one JSON object per line on stdin:
12
+ * {"type":"turn"} process pending messages
13
+ * {"type":"run-finished","runId":"…","command":"…"} report a finished cli_* run
14
+ * {"type":"shutdown"} drain + exit cleanly
15
+ *
16
+ * Turns run strictly one at a time (a session never has two turns in flight).
17
+ * When stdin closes, the process exits after the current turn drains. Markers
18
+ * (session id, background cli runs) are written to stdout exactly as the
19
+ * one-shot command emits them, so the desktop's existing parsing is unchanged.
20
+ */
21
+ import { type SessionTurnCliOptions } from '../session-turn/index.js';
22
+ /**
23
+ * Emitted once the session context is built and the process is ready to accept
24
+ * turn commands on stdin. The desktop may use it to know the daemon is live;
25
+ * it is harmless to ignore.
26
+ */
27
+ export declare const SESSION_READY_MARKER = "__EDSGER_SESSION_READY__";
28
+ export type SessionServeCliOptions = Omit<SessionTurnCliOptions, 'runFinished'> & {
29
+ /** SDK session id to resume on the first turn (e.g. after a crash-restart). */
30
+ resumeSessionId?: string;
31
+ };
32
+ export declare function runSessionServeCommand(options: SessionServeCliOptions): Promise<void>;
@@ -0,0 +1,100 @@
1
+ /**
2
+ * `edsger session-serve <channelId>` — long-lived conversational agent for one
3
+ * chat session.
4
+ *
5
+ * Where `session-turn` runs a single turn and exits, `session-serve` prepares
6
+ * the session context once (repo clone, MCP toolbelt, system prompt) and then
7
+ * stays alive, running one turn per command it reads from stdin. The SDK
8
+ * session id is kept in memory and carried into the next turn automatically, so
9
+ * the conversation (and prompt cache) stays warm without a per-turn cold start.
10
+ *
11
+ * Protocol — one JSON object per line on stdin:
12
+ * {"type":"turn"} process pending messages
13
+ * {"type":"run-finished","runId":"…","command":"…"} report a finished cli_* run
14
+ * {"type":"shutdown"} drain + exit cleanly
15
+ *
16
+ * Turns run strictly one at a time (a session never has two turns in flight).
17
+ * When stdin closes, the process exits after the current turn drains. Markers
18
+ * (session id, background cli runs) are written to stdout exactly as the
19
+ * one-shot command emits them, so the desktop's existing parsing is unchanged.
20
+ */
21
+ import { createInterface } from 'node:readline';
22
+ import { logError, logInfo } from '../../utils/logger.js';
23
+ import { prepareSessionContext, runTurn, } from '../session-turn/index.js';
24
+ /**
25
+ * Emitted once the session context is built and the process is ready to accept
26
+ * turn commands on stdin. The desktop may use it to know the daemon is live;
27
+ * it is harmless to ignore.
28
+ */
29
+ export const SESSION_READY_MARKER = '__EDSGER_SESSION_READY__';
30
+ export async function runSessionServeCommand(options) {
31
+ const ctx = await prepareSessionContext(options);
32
+ process.stdout.write(`\n${SESSION_READY_MARKER}\n`);
33
+ logInfo(`Session ${ctx.channelId} ready — awaiting turns on stdin.`);
34
+ // Commands queue up here; we drain them one at a time so a session never has
35
+ // two turns running concurrently. `shuttingDown` stops accepting new work.
36
+ const queue = [];
37
+ let running = false;
38
+ let shuttingDown = false;
39
+ let stdinEnded = false;
40
+ const maybeExit = () => {
41
+ if ((shuttingDown || stdinEnded) && !running && queue.length === 0) {
42
+ logInfo(`Session ${ctx.channelId} shutting down.`);
43
+ process.exit(0);
44
+ }
45
+ };
46
+ const drain = async () => {
47
+ if (running) {
48
+ return;
49
+ }
50
+ running = true;
51
+ try {
52
+ while (queue.length > 0) {
53
+ const cmd = queue.shift();
54
+ if (cmd.type === 'shutdown') {
55
+ shuttingDown = true;
56
+ continue;
57
+ }
58
+ try {
59
+ if (cmd.type === 'run-finished' && cmd.runId) {
60
+ await runTurn(ctx, {
61
+ runFinished: { runId: cmd.runId, command: cmd.command },
62
+ });
63
+ }
64
+ else {
65
+ await runTurn(ctx);
66
+ }
67
+ }
68
+ catch (error) {
69
+ logError(`Turn failed: ${error instanceof Error ? error.message : String(error)}`);
70
+ }
71
+ }
72
+ }
73
+ finally {
74
+ running = false;
75
+ maybeExit();
76
+ }
77
+ };
78
+ const rl = createInterface({ input: process.stdin });
79
+ rl.on('line', (line) => {
80
+ const trimmed = line.trim();
81
+ if (!trimmed || shuttingDown) {
82
+ return;
83
+ }
84
+ let cmd;
85
+ try {
86
+ cmd = JSON.parse(trimmed);
87
+ }
88
+ catch {
89
+ logError(`Ignoring malformed command: ${trimmed}`);
90
+ return;
91
+ }
92
+ queue.push(cmd);
93
+ void drain();
94
+ });
95
+ // stdin closing is the desktop's normal "stop this session" signal.
96
+ rl.on('close', () => {
97
+ stdinEnded = true;
98
+ maybeExit();
99
+ });
100
+ }
@@ -4,12 +4,70 @@
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__=";
15
+ import { loadExternalMcpServers } from 'edsger-tools';
7
16
  export interface SessionTurnCliOptions {
8
17
  channelId: string;
9
18
  productId: string;
10
19
  repositoryIds?: string[];
11
20
  /** SDK session id from a previous turn, to resume the same conversation. */
12
21
  resumeSessionId?: string;
22
+ /**
23
+ * Follow-up turn triggered by the desktop watcher when a background `cli_*`
24
+ * run finishes. When set, the turn is driven by a synthetic prompt instead of
25
+ * the channel's pending human messages: the agent inspects what the run
26
+ * produced and reports it to the user.
27
+ */
28
+ runFinished?: {
29
+ runId: string;
30
+ command?: string;
31
+ };
13
32
  verbose?: boolean;
14
33
  }
34
+ /**
35
+ * Everything a session needs to run turns, built once and reused. The daemon
36
+ * keeps a single instance alive across turns so repo clones, the MCP toolbelt,
37
+ * the system prompt, and the SDK session id all persist.
38
+ */
39
+ export interface SessionContext {
40
+ channelId: string;
41
+ productId: string;
42
+ repositoryIds: string[];
43
+ verbose: boolean;
44
+ sessionDir?: string;
45
+ systemPrompt: string;
46
+ sessionServer: any;
47
+ external: ReturnType<typeof loadExternalMcpServers>;
48
+ /** Latest SDK session id; carried into the next turn's `resume`. Mutable. */
49
+ sdkSessionId: string;
50
+ }
51
+ /**
52
+ * Build the one-time session context: clone repos, assemble the MCP toolbelt
53
+ * and external servers, and build the system prompt. Cheap to call once per
54
+ * session process; expensive to call per turn (which is why the daemon doesn't).
55
+ */
56
+ export declare function prepareSessionContext(options: SessionTurnCliOptions): Promise<SessionContext>;
57
+ /**
58
+ * Run a single agent turn against an already-prepared {@link SessionContext}.
59
+ * Mutates `ctx.sdkSessionId` so the next turn resumes the same SDK conversation
60
+ * (and prompt cache). Safe to call repeatedly for the life of the process.
61
+ */
62
+ export declare function runTurn(ctx: SessionContext, opts?: {
63
+ runFinished?: {
64
+ runId: string;
65
+ command?: string;
66
+ };
67
+ }): Promise<void>;
68
+ /**
69
+ * One-shot `session-turn`: prepare the context and run a single turn. Retained
70
+ * for callers that want the previous per-turn-process behaviour (and as the
71
+ * building block the daemon reuses).
72
+ */
15
73
  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).
@@ -15,9 +23,15 @@ export const SESSION_ID_MARKER = '__EDSGER_SESSION_ID__=';
15
23
  * to the channel and marks the messages processed. The agent decides what to
16
24
  * do (answer, generate stories/test cases, launch cli_* analyses, open PRs);
17
25
  * there is no fixed pipeline.
26
+ *
27
+ * The per-turn work is split into {@link prepareSessionContext} (one-time
28
+ * workspace clone + toolbelt + prompt build) and {@link runTurn} (one agent
29
+ * turn against that context). The one-shot `session-turn` command does both
30
+ * once; the long-lived `session-serve` daemon prepares the context once and
31
+ * calls {@link runTurn} repeatedly, keeping the SDK session warm across turns.
18
32
  */
19
33
  import { query } from '@anthropic-ai/claude-agent-sdk';
20
- import { buildSessionAgentOptions, buildSessionSystemPrompt, buildSessionUserPrompt, createSessionMcpServer, loadExternalMcpServers, SESSION_MAX_TURNS, } from 'edsger-tools';
34
+ import { buildSessionAgentOptions, buildSessionSystemPrompt, buildSessionUserPrompt, createSessionMcpServer, listActiveCliRuns, loadExternalMcpServers, SESSION_MAX_TURNS, } from 'edsger-tools';
21
35
  import { callMcpEndpoint } from '../../api/mcp-client.js';
22
36
  import { DEFAULT_MODEL } from '../../constants.js';
23
37
  import { getToolDeps } from '../../tools/bootstrap.js';
@@ -45,33 +59,21 @@ async function prepareSessionWorkspace(opts) {
45
59
  repoScopeNote: workspace ? describeSessionRepos(workspace.repos) : '',
46
60
  };
47
61
  }
48
- export async function runSessionTurnCommand(options) {
62
+ /**
63
+ * Build the one-time session context: clone repos, assemble the MCP toolbelt
64
+ * and external servers, and build the system prompt. Cheap to call once per
65
+ * session process; expensive to call per turn (which is why the daemon doesn't).
66
+ */
67
+ export async function prepareSessionContext(options) {
49
68
  const { channelId, productId, repositoryIds = [], resumeSessionId, verbose = false, } = options;
50
- // Emit the SDK session id so the desktop can persist it for the next turn.
51
- const emitSessionId = (id) => {
52
- if (id) {
53
- process.stdout.write(`\n${SESSION_ID_MARKER}${id}\n`);
54
- }
55
- };
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
- }
65
- logInfo(`Processing ${messages.length} message(s) for session ${channelId}`);
66
- // 2. Clone the product's in-scope repositories into a local session
67
- // directory so the agent's Read/Grep/Glob can inspect the real code.
69
+ // Clone the product's in-scope repositories into a local session directory
70
+ // so the agent's Read/Grep/Glob can inspect the real code.
68
71
  const { sessionDir, repoScopeNote } = await prepareSessionWorkspace({
69
72
  channelId,
70
73
  productId,
71
74
  repositoryIds,
72
75
  verbose,
73
76
  });
74
- // 3. Build the session toolbelt + prompts.
75
77
  const deps = getToolDeps({
76
78
  verbose,
77
79
  context: { productId, channelId, repositoryIds },
@@ -89,36 +91,95 @@ export async function runSessionTurnCommand(options) {
89
91
  repoScopeNote,
90
92
  externalMcpNames: external.names,
91
93
  });
92
- const userPrompt = buildSessionUserPrompt(messages);
93
- // 4. Run one SDK turn. Resume the prior SDK session when we have its id so
94
+ return {
95
+ channelId,
96
+ productId,
97
+ repositoryIds,
98
+ verbose,
99
+ sessionDir,
100
+ systemPrompt,
101
+ sessionServer,
102
+ external,
103
+ sdkSessionId: resumeSessionId ?? '',
104
+ };
105
+ }
106
+ /** Emit the SDK session id so the desktop can persist it for the next turn. */
107
+ function emitSessionId(id) {
108
+ if (id) {
109
+ process.stdout.write(`\n${SESSION_ID_MARKER}${id}\n`);
110
+ }
111
+ }
112
+ /**
113
+ * Emit a marker for every background cli_* run still in flight, so the desktop
114
+ * watcher can poll each to completion and re-engage the agent.
115
+ */
116
+ function emitActiveCliRuns() {
117
+ for (const run of listActiveCliRuns()) {
118
+ const payload = JSON.stringify({
119
+ run_id: run.run_id,
120
+ pid: run.pid,
121
+ log_path: run.log_path,
122
+ command: run.command,
123
+ });
124
+ process.stdout.write(`\n${CLI_RUN_MARKER}${payload}\n`);
125
+ }
126
+ }
127
+ /**
128
+ * Run a single agent turn against an already-prepared {@link SessionContext}.
129
+ * Mutates `ctx.sdkSessionId` so the next turn resumes the same SDK conversation
130
+ * (and prompt cache). Safe to call repeatedly for the life of the process.
131
+ */
132
+ export async function runTurn(ctx, opts = {}) {
133
+ const { runFinished } = opts;
134
+ // 1. Decide what drives this turn. A normal turn replays the channel's
135
+ // pending human messages; a watcher-triggered follow-up replays a
136
+ // synthetic prompt about the background run that just finished.
137
+ let messages = [];
138
+ if (runFinished) {
139
+ logInfo(`Reporting completion of background run ${runFinished.runId} for session ${ctx.channelId}`);
140
+ }
141
+ else {
142
+ const pendingResult = (await callMcpEndpoint('chat/messages/pending', {
143
+ channel_id: ctx.channelId,
144
+ }));
145
+ messages = pendingResult.messages ?? [];
146
+ if (messages.length === 0) {
147
+ logInfo('No pending messages for this session — nothing to do.');
148
+ return;
149
+ }
150
+ logInfo(`Processing ${messages.length} message(s) for session ${ctx.channelId}`);
151
+ }
152
+ const userPrompt = runFinished
153
+ ? buildRunFinishedPrompt(runFinished)
154
+ : buildSessionUserPrompt(messages);
155
+ // 2. Run one SDK turn. Resume the prior SDK session when we have its id so
94
156
  // the conversation (and prompt cache) carries across turns. Point the
95
157
  // agent's cwd at the session directory so its read-only file tools resolve
96
158
  // against the cloned repos. Prompt + tool/MCP wiring come from the shared
97
159
  // session core (edsger-tools) so CLI and worker can't drift apart.
98
160
  let finalResponse = '';
99
- let sdkSessionId = resumeSessionId ?? '';
100
161
  try {
101
162
  for await (const message of query({
102
163
  prompt: userPrompt,
103
164
  options: {
104
- ...buildSessionAgentOptions(systemPrompt, {
105
- sessionServer,
106
- externalServers: external.servers,
107
- externalNames: external.names,
108
- sessionDir,
165
+ ...buildSessionAgentOptions(ctx.systemPrompt, {
166
+ sessionServer: ctx.sessionServer,
167
+ externalServers: ctx.external.servers,
168
+ externalNames: ctx.external.names,
169
+ sessionDir: ctx.sessionDir,
109
170
  maxTurns: SESSION_MAX_TURNS,
110
171
  }),
111
172
  model: DEFAULT_MODEL,
112
- ...(resumeSessionId ? { resume: resumeSessionId } : {}),
173
+ ...(ctx.sdkSessionId ? { resume: ctx.sdkSessionId } : {}),
113
174
  },
114
175
  })) {
115
176
  const msg = message;
116
177
  // The system/init and result messages carry the (possibly new) session
117
178
  // id — keep the latest so we persist what the next turn should resume.
118
179
  if (msg.session_id) {
119
- sdkSessionId = msg.session_id;
180
+ ctx.sdkSessionId = msg.session_id;
120
181
  }
121
- if (verbose && msg.type === 'assistant') {
182
+ if (ctx.verbose && msg.type === 'assistant') {
122
183
  logInfo('· agent step');
123
184
  }
124
185
  if (msg.type === 'result') {
@@ -131,21 +192,22 @@ export async function runSessionTurnCommand(options) {
131
192
  const reason = error instanceof Error ? error.message : String(error);
132
193
  logError(`Session turn failed: ${reason}`);
133
194
  await callMcpEndpoint('chat/messages/send_ai', {
134
- channel_id: channelId,
195
+ channel_id: ctx.channelId,
135
196
  content: `I hit an error processing that: ${reason}`,
136
197
  message_type: 'text',
137
198
  metadata: {},
138
199
  }).catch(() => undefined);
139
- emitSessionId(sdkSessionId);
200
+ emitSessionId(ctx.sdkSessionId);
201
+ emitActiveCliRuns();
140
202
  await markProcessed(messages);
141
203
  return;
142
204
  }
143
- // 5. Post the final reply (the agent may also have posted via
205
+ // 3. Post the final reply (the agent may also have posted via
144
206
  // send_chat_message during the turn; this carries the closing summary).
145
207
  const reply = finalResponse.trim();
146
208
  if (reply) {
147
209
  await callMcpEndpoint('chat/messages/send_ai', {
148
- channel_id: channelId,
210
+ channel_id: ctx.channelId,
149
211
  content: reply,
150
212
  message_type: 'text',
151
213
  metadata: {},
@@ -153,12 +215,36 @@ export async function runSessionTurnCommand(options) {
153
215
  logError(`Failed to post reply: ${e instanceof Error ? e.message : String(e)}`);
154
216
  });
155
217
  }
156
- // 6. Mark the triggering messages processed so they aren't re-run.
218
+ // 4. Mark the triggering messages processed so they aren't re-run. (No-op for
219
+ // a watcher follow-up, which has no triggering human messages.)
157
220
  await markProcessed(messages);
158
- // 7. Emit the SDK session id so the desktop persists it for the next turn.
159
- emitSessionId(sdkSessionId);
221
+ // 5. Emit the SDK session id so the desktop persists it for the next turn,
222
+ // plus a marker for any background run launched this turn so the watcher
223
+ // keeps following it to completion.
224
+ emitSessionId(ctx.sdkSessionId);
225
+ emitActiveCliRuns();
160
226
  logSuccess('Session turn complete.');
161
227
  }
228
+ /**
229
+ * One-shot `session-turn`: prepare the context and run a single turn. Retained
230
+ * for callers that want the previous per-turn-process behaviour (and as the
231
+ * building block the daemon reuses).
232
+ */
233
+ export async function runSessionTurnCommand(options) {
234
+ const ctx = await prepareSessionContext(options);
235
+ await runTurn(ctx, { runFinished: options.runFinished });
236
+ }
237
+ /**
238
+ * The synthetic user prompt for a watcher-triggered follow-up turn. Steers the
239
+ * agent to inspect what the finished run produced and report it — explicitly
240
+ * NOT to ask the user whether it should check.
241
+ */
242
+ function buildRunFinishedPrompt(runFinished) {
243
+ const which = runFinished.command
244
+ ? `the \`${runFinished.command}\` analysis (run ${runFinished.runId})`
245
+ : `a background analysis you launched earlier (run ${runFinished.runId})`;
246
+ 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.`;
247
+ }
162
248
  async function markProcessed(messages) {
163
249
  for (const m of messages) {
164
250
  await callMcpEndpoint('chat/messages/mark_processed', {
package/dist/index.js CHANGED
@@ -32,6 +32,7 @@ import { runRefactor } from './commands/refactor/refactor.js';
32
32
  import { runReleaseSyncCommand } from './commands/release-sync/index.js';
33
33
  import { runRunSheetCommand } from './commands/run-sheet/index.js';
34
34
  import { runScreenFlow } from './commands/screen-flow/index.js';
35
+ import { runSessionServeCommand } from './commands/session-serve/index.js';
35
36
  import { runSessionTurnCommand } from './commands/session-turn/index.js';
36
37
  import { runSmokeTestCommand } from './commands/smoke-test/index.js';
37
38
  import { runSyncAws } from './commands/sync-aws/index.js';
@@ -441,10 +442,43 @@ program
441
442
  .requiredOption('--product <productId>', 'Product the session is bound to')
442
443
  .option('--repos <ids>', 'Comma-separated repository IDs the agent may touch', '')
443
444
  .option('--resume <sessionId>', 'SDK session id from a previous turn to resume the same conversation')
445
+ .option('--run-finished <runId>', 'Follow-up turn: report the results of a background cli_* run that finished')
446
+ .option('--run-command <command>', 'The command name of the finished run (used with --run-finished)')
444
447
  .option('-v, --verbose', 'Verbose output')
445
448
  .action(async (channelId, opts) => {
446
449
  try {
447
450
  await runSessionTurnCommand({
451
+ channelId,
452
+ productId: opts.product,
453
+ repositoryIds: (opts.repos ?? '')
454
+ .split(',')
455
+ .map((s) => s.trim())
456
+ .filter(Boolean),
457
+ resumeSessionId: opts.resume,
458
+ runFinished: opts.runFinished
459
+ ? { runId: opts.runFinished, command: opts.runCommand }
460
+ : undefined,
461
+ verbose: opts.verbose,
462
+ });
463
+ }
464
+ catch (error) {
465
+ logError(error instanceof Error ? error.message : String(error));
466
+ process.exit(1);
467
+ }
468
+ });
469
+ // ============================================================
470
+ // Subcommand: edsger session-serve <channelId>
471
+ // ============================================================
472
+ program
473
+ .command('session-serve <channelId>')
474
+ .description('Run a long-lived conversational agent for a chat session, taking one turn per command read from stdin')
475
+ .requiredOption('--product <productId>', 'Product the session is bound to')
476
+ .option('--repos <ids>', 'Comma-separated repository IDs the agent may touch', '')
477
+ .option('--resume <sessionId>', 'SDK session id to resume on the first turn (e.g. after a crash-restart)')
478
+ .option('-v, --verbose', 'Verbose output')
479
+ .action(async (channelId, opts) => {
480
+ try {
481
+ await runSessionServeCommand({
448
482
  channelId,
449
483
  productId: opts.product,
450
484
  repositoryIds: (opts.repos ?? '')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edsger",
3
- "version": "0.64.0",
3
+ "version": "0.66.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
  },