@link-assistant/agent 0.5.3 → 0.6.1

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.
package/src/bun/index.ts CHANGED
@@ -17,10 +17,11 @@ export namespace BunProc {
17
17
  cmd: string[],
18
18
  options?: Bun.SpawnOptions.OptionsObject<any, any, any>
19
19
  ) {
20
- log.info('running', {
20
+ log.info(() => ({
21
+ message: 'running',
21
22
  cmd: [which(), ...cmd],
22
23
  ...options,
23
- });
24
+ }));
24
25
  const result = Bun.spawn([which(), ...cmd], {
25
26
  ...options,
26
27
  stdout: 'pipe',
@@ -42,11 +43,7 @@ export namespace BunProc {
42
43
  ? result.stderr
43
44
  : await readableStreamToText(result.stderr)
44
45
  : undefined;
45
- log.info('done', {
46
- code,
47
- stdout,
48
- stderr,
49
- });
46
+ log.info(() => ({ message: 'done', code, stdout, stderr }));
50
47
  if (code !== 0) {
51
48
  const parts = [`Command failed with exit code ${result.exitCode}`];
52
49
  if (stderr) parts.push(`stderr: ${stderr}`);
@@ -111,14 +108,13 @@ export namespace BunProc {
111
108
 
112
109
  // Check for dry-run mode
113
110
  if (Flag.OPENCODE_DRY_RUN) {
114
- log.info(
115
- '[DRY RUN] Would install package (skipping actual installation)',
116
- {
117
- pkg,
118
- version,
119
- targetPath: mod,
120
- }
121
- );
111
+ log.info(() => ({
112
+ message:
113
+ '[DRY RUN] Would install package (skipping actual installation)',
114
+ pkg,
115
+ version,
116
+ targetPath: mod,
117
+ }));
122
118
  // In dry-run mode, pretend the package is installed
123
119
  return mod;
124
120
  }
@@ -137,10 +133,11 @@ export namespace BunProc {
137
133
  // - If .npmrc files exist, Bun will use them automatically
138
134
  // - If no .npmrc files exist, Bun will default to https://registry.npmjs.org
139
135
  // - No need to pass --registry flag
140
- log.info("installing package using Bun's default registry resolution", {
136
+ log.info(() => ({
137
+ message: "installing package using Bun's default registry resolution",
141
138
  pkg,
142
139
  version,
143
- });
140
+ }));
144
141
 
145
142
  // Retry logic for cache-related errors
146
143
  let lastError: Error | undefined;
@@ -150,7 +147,12 @@ export namespace BunProc {
150
147
  cwd: Global.Path.cache,
151
148
  });
152
149
 
153
- log.info('package installed successfully', { pkg, version, attempt });
150
+ log.info(() => ({
151
+ message: 'package installed successfully',
152
+ pkg,
153
+ version,
154
+ attempt,
155
+ }));
154
156
  parsed.dependencies[pkg] = version;
155
157
  await Bun.write(pkgjson.name!, JSON.stringify(parsed, null, 2));
156
158
  return mod;
@@ -158,43 +160,47 @@ export namespace BunProc {
158
160
  const errorMsg = e instanceof Error ? e.message : String(e);
159
161
  const isCacheError = isCacheRelatedError(errorMsg);
160
162
 
161
- log.warn('package installation attempt failed', {
163
+ log.warn(() => ({
164
+ message: 'package installation attempt failed',
162
165
  pkg,
163
166
  version,
164
167
  attempt,
165
168
  maxRetries: MAX_RETRIES,
166
169
  error: errorMsg,
167
170
  isCacheError,
168
- });
171
+ }));
169
172
 
170
173
  if (isCacheError && attempt < MAX_RETRIES) {
171
- log.info('retrying installation after cache-related error', {
174
+ log.info(() => ({
175
+ message: 'retrying installation after cache-related error',
172
176
  pkg,
173
177
  version,
174
178
  attempt,
175
179
  nextAttempt: attempt + 1,
176
180
  delayMs: RETRY_DELAY_MS,
177
- });
181
+ }));
178
182
  await delay(RETRY_DELAY_MS);
179
183
  lastError = e instanceof Error ? e : new Error(errorMsg);
180
184
  continue;
181
185
  }
182
186
 
183
187
  // Non-cache error or final attempt - log and throw
184
- log.error('package installation failed', {
188
+ log.error(() => ({
189
+ message: 'package installation failed',
185
190
  pkg,
186
191
  version,
187
192
  error: errorMsg,
188
193
  stack: e instanceof Error ? e.stack : undefined,
189
194
  possibleCacheCorruption: isCacheError,
190
195
  attempts: attempt,
191
- });
196
+ }));
192
197
 
193
198
  // Provide helpful recovery instructions for cache-related errors
194
199
  if (isCacheError) {
195
- log.error(
196
- 'Bun package cache may be corrupted. Try clearing the cache with: bun pm cache rm'
197
- );
200
+ log.error(() => ({
201
+ message:
202
+ 'Bun package cache may be corrupted. Try clearing the cache with: bun pm cache rm',
203
+ }));
198
204
  }
199
205
 
200
206
  throw new InstallFailedError(
package/src/bus/index.ts CHANGED
@@ -63,9 +63,7 @@ export namespace Bus {
63
63
  type: def.type,
64
64
  properties,
65
65
  };
66
- log.info('publishing', {
67
- type: def.type,
68
- });
66
+ log.info(() => ({ message: 'publishing', type: def.type }));
69
67
  const pending = [];
70
68
  for (const key of [def.type, '*']) {
71
69
  const match = state().subscriptions.get(key);
@@ -107,14 +105,14 @@ export namespace Bus {
107
105
  }
108
106
 
109
107
  function raw(type: string, callback: (event: any) => void) {
110
- log.info('subscribing', { type });
108
+ log.info(() => ({ message: 'subscribing', type }));
111
109
  const subscriptions = state().subscriptions;
112
110
  let match = subscriptions.get(type) ?? [];
113
111
  match.push(callback);
114
112
  subscriptions.set(type, match);
115
113
 
116
114
  return () => {
117
- log.info('unsubscribing', { type });
115
+ log.info(() => ({ message: 'unsubscribing', type }));
118
116
  const match = subscriptions.get(type);
119
117
  if (!match) return;
120
118
  const index = match.indexOf(callback);
@@ -53,16 +53,21 @@ export namespace Config {
53
53
  if (!newDirExists) {
54
54
  try {
55
55
  // Perform migration by copying the entire directory
56
- log.info(
57
- `Migrating config from ${oldDir} to ${newDir} for smooth transition`
58
- );
56
+ log.info(() => ({
57
+ message: `Migrating config from ${oldDir} to ${newDir} for smooth transition`,
58
+ }));
59
59
 
60
60
  // Use fs-extra style recursive copy
61
61
  await copyDirectory(oldDir, newDir);
62
62
 
63
- log.info(`Successfully migrated config to ${newDir}`);
63
+ log.info(() => ({
64
+ message: `Successfully migrated config to ${newDir}`,
65
+ }));
64
66
  } catch (error) {
65
- log.error(`Failed to migrate config from ${oldDir}:`, error);
67
+ log.error(() => ({
68
+ message: `Failed to migrate config from ${oldDir}:`,
69
+ error,
70
+ }));
66
71
  // Don't throw - allow the app to continue with the old config
67
72
  }
68
73
  }
@@ -83,14 +88,19 @@ export namespace Config {
83
88
  .catch(() => false);
84
89
 
85
90
  if (oldGlobalExists && !newGlobalExists) {
86
- log.info(
87
- `Migrating global config from ${oldGlobalPath} to ${newGlobalPath}`
88
- );
91
+ log.info(() => ({
92
+ message: `Migrating global config from ${oldGlobalPath} to ${newGlobalPath}`,
93
+ }));
89
94
  await copyDirectory(oldGlobalPath, newGlobalPath);
90
- log.info(`Successfully migrated global config to ${newGlobalPath}`);
95
+ log.info(() => ({
96
+ message: `Successfully migrated global config to ${newGlobalPath}`,
97
+ }));
91
98
  }
92
99
  } catch (error) {
93
- log.error('Failed to migrate global config:', error);
100
+ log.error(() => ({
101
+ message: 'Failed to migrate global config:',
102
+ error,
103
+ }));
94
104
  // Don't throw - allow the app to continue
95
105
  }
96
106
  }
@@ -126,7 +136,10 @@ export namespace Config {
126
136
  // Override with custom config if provided
127
137
  if (Flag.OPENCODE_CONFIG) {
128
138
  result = mergeDeep(result, await loadFile(Flag.OPENCODE_CONFIG));
129
- log.debug('loaded custom config', { path: Flag.OPENCODE_CONFIG });
139
+ log.debug(() => ({
140
+ message: 'loaded custom config',
141
+ path: Flag.OPENCODE_CONFIG,
142
+ }));
130
143
  }
131
144
 
132
145
  for (const file of ['opencode.jsonc', 'opencode.json']) {
@@ -142,7 +155,9 @@ export namespace Config {
142
155
 
143
156
  if (Flag.OPENCODE_CONFIG_CONTENT) {
144
157
  result = mergeDeep(result, JSON.parse(Flag.OPENCODE_CONFIG_CONTENT));
145
- log.debug('loaded custom config from OPENCODE_CONFIG_CONTENT');
158
+ log.debug(() => ({
159
+ message: 'loaded custom config from OPENCODE_CONFIG_CONTENT',
160
+ }));
146
161
  }
147
162
 
148
163
  for (const [key, value] of Object.entries(auth)) {
@@ -182,12 +197,11 @@ export namespace Config {
182
197
  const filteredDirs = foundDirs.filter((dir) => {
183
198
  // If .link-assistant-agent exists, exclude .opencode directories
184
199
  if (hasNewConfig && dir.endsWith('.opencode')) {
185
- log.debug(
186
- 'Skipping .opencode directory (using .link-assistant-agent):',
187
- {
188
- path: dir,
189
- }
190
- );
200
+ log.debug(() => ({
201
+ message:
202
+ 'Skipping .opencode directory (using .link-assistant-agent):',
203
+ path: dir,
204
+ }));
191
205
  return false;
192
206
  }
193
207
  return true;
@@ -197,9 +211,10 @@ export namespace Config {
197
211
 
198
212
  if (Flag.OPENCODE_CONFIG_DIR) {
199
213
  directories.push(Flag.OPENCODE_CONFIG_DIR);
200
- log.debug('loading config from LINK_ASSISTANT_AGENT_CONFIG_DIR', {
214
+ log.debug(() => ({
215
+ message: 'loading config from LINK_ASSISTANT_AGENT_CONFIG_DIR',
201
216
  path: Flag.OPENCODE_CONFIG_DIR,
202
- });
217
+ }));
203
218
  }
204
219
 
205
220
  const promises: Promise<void>[] = [];
@@ -212,7 +227,9 @@ export namespace Config {
212
227
  dir === Flag.OPENCODE_CONFIG_DIR
213
228
  ) {
214
229
  for (const file of ['opencode.jsonc', 'opencode.json']) {
215
- log.debug(`loading config from ${path.join(dir, file)}`);
230
+ log.debug(() => ({
231
+ message: `loading config from ${path.join(dir, file)}`,
232
+ }));
216
233
  result = mergeDeep(result, await loadFile(path.join(dir, file)));
217
234
  // to satisy the type checker
218
235
  result.agent ??= {};
@@ -932,7 +949,7 @@ export namespace Config {
932
949
  });
933
950
 
934
951
  async function loadFile(filepath: string): Promise<Info> {
935
- log.info('loading', { path: filepath });
952
+ log.info(() => ({ message: 'loading', path: filepath }));
936
953
  let text = await Bun.file(filepath)
937
954
  .text()
938
955
  .catch((err) => {
@@ -275,7 +275,7 @@ export namespace Ripgrep {
275
275
  }
276
276
 
277
277
  export async function tree(input: { cwd: string; limit?: number }) {
278
- log.info('tree', input);
278
+ log.info(() => ({ message: 'tree', ...input }));
279
279
  const files = await Array.fromAsync(Ripgrep.files({ cwd: input.cwd }));
280
280
  interface Node {
281
281
  path: string[];
package/src/file/time.ts CHANGED
@@ -15,7 +15,7 @@ export namespace FileTime {
15
15
  });
16
16
 
17
17
  export function read(sessionID: string, file: string) {
18
- log.info('read', { sessionID, file });
18
+ log.info(() => ({ message: 'read', sessionID, file }));
19
19
  const { read } = state();
20
20
  read[sessionID] = read[sessionID] || {};
21
21
  read[sessionID][file] = new Date();
@@ -36,7 +36,7 @@ export namespace FileWatcher {
36
36
  const state = Instance.state(
37
37
  async () => {
38
38
  if (Instance.project.vcs !== 'git') return {};
39
- log.info('init');
39
+ log.info(() => ({ message: 'init' }));
40
40
  const cfg = await Config.get();
41
41
  const backend = (() => {
42
42
  if (process.platform === 'win32') return 'windows';
@@ -44,18 +44,23 @@ export namespace FileWatcher {
44
44
  if (process.platform === 'linux') return 'inotify';
45
45
  })();
46
46
  if (!backend) {
47
- log.error('watcher backend not supported', {
47
+ log.error(() => ({
48
+ message: 'watcher backend not supported',
48
49
  platform: process.platform,
49
- });
50
+ }));
50
51
  return {};
51
52
  }
52
- log.info('watcher backend', { platform: process.platform, backend });
53
+ log.info(() => ({
54
+ message: 'watcher backend',
55
+ platform: process.platform,
56
+ backend,
57
+ }));
53
58
  const sub = await watcher().subscribe(
54
59
  Instance.directory,
55
60
  (err, evts) => {
56
61
  if (err) return;
57
62
  for (const evt of evts) {
58
- log.info('event', evt);
63
+ log.info(() => ({ message: 'event', ...evt }));
59
64
  if (evt.type === 'create')
60
65
  Bus.publish(Event.Updated, { file: evt.path, event: 'add' });
61
66
  if (evt.type === 'update')
@@ -29,7 +29,7 @@ export namespace Format {
29
29
 
30
30
  const formatters: Record<string, Formatter.Info> = {};
31
31
  if (cfg.formatter === false) {
32
- log.info('all formatters are disabled');
32
+ log.info(() => ({ message: 'all formatters are disabled' }));
33
33
  return {
34
34
  enabled,
35
35
  formatters,
@@ -77,10 +77,10 @@ export namespace Format {
77
77
  const formatters = await state().then((x) => x.formatters);
78
78
  const result = [];
79
79
  for (const item of Object.values(formatters)) {
80
- log.info('checking', { name: item.name, ext });
80
+ log.info(() => ({ message: 'checking', name: item.name, ext }));
81
81
  if (!item.extensions.includes(ext)) continue;
82
82
  if (!(await isEnabled(item))) continue;
83
- log.info('enabled', { name: item.name, ext });
83
+ log.info(() => ({ message: 'enabled', name: item.name, ext }));
84
84
  result.push(item);
85
85
  }
86
86
  return result;
@@ -101,14 +101,14 @@ export namespace Format {
101
101
  }
102
102
 
103
103
  export function init() {
104
- log.info('init');
104
+ log.info(() => ({ message: 'init' }));
105
105
  Bus.subscribe(File.Event.Edited, async (payload) => {
106
106
  const file = payload.properties.file;
107
- log.info('formatting', { file });
107
+ log.info(() => ({ message: 'formatting', file }));
108
108
  const ext = path.extname(file);
109
109
 
110
110
  for (const item of await getFormatter(ext)) {
111
- log.info('running', { command: item.command });
111
+ log.info(() => ({ message: 'running', command: item.command }));
112
112
  try {
113
113
  const proc = Bun.spawn({
114
114
  cmd: item.command.map((x) => x.replace('$FILE', file)),
@@ -119,17 +119,19 @@ export namespace Format {
119
119
  });
120
120
  const exit = await proc.exited;
121
121
  if (exit !== 0)
122
- log.error('failed', {
122
+ log.error(() => ({
123
+ message: 'failed',
123
124
  command: item.command,
124
125
  ...item.environment,
125
- });
126
+ }));
126
127
  } catch (error) {
127
- log.error('failed to format file', {
128
+ log.error(() => ({
129
+ message: 'failed to format file',
128
130
  error,
129
131
  command: item.command,
130
132
  ...item.environment,
131
133
  file,
132
- });
134
+ }));
133
135
  }
134
136
  }
135
137
  });
package/src/index.js CHANGED
@@ -246,22 +246,19 @@ async function readSystemMessages(argv) {
246
246
  }
247
247
 
248
248
  async function runAgentMode(argv, request) {
249
- // Note: verbose flag and logging are now initialized in middleware
250
- // See main() function for the middleware that sets up Flag and Log.init()
251
-
252
- // Log version and command info in verbose mode
253
- if (Flag.OPENCODE_VERBOSE) {
254
- console.error(`Agent version: ${pkg.version}`);
255
- console.error(`Command: ${process.argv.join(' ')}`);
256
- console.error(`Working directory: ${process.cwd()}`);
257
- console.error(`Script path: ${import.meta.path}`);
258
- }
259
-
260
- // Log dry-run mode if enabled
249
+ // Log version and command info in verbose mode using lazy logging
250
+ Log.Default.lazy.info(() => ({
251
+ message: 'Agent started',
252
+ version: pkg.version,
253
+ command: process.argv.join(' '),
254
+ workingDirectory: process.cwd(),
255
+ scriptPath: import.meta.path,
256
+ }));
261
257
  if (Flag.OPENCODE_DRY_RUN) {
262
- console.error(
263
- `[DRY RUN MODE] No actual API calls or package installations will be made`
264
- );
258
+ Log.Default.lazy.info(() => ({
259
+ message: 'Dry run mode enabled',
260
+ mode: 'dry-run',
261
+ }));
265
262
  }
266
263
 
267
264
  const { providerID, modelID } = await parseModelConfig(argv);
@@ -269,9 +266,11 @@ async function runAgentMode(argv, request) {
269
266
  // Validate and get JSON standard
270
267
  const jsonStandard = argv['json-standard'];
271
268
  if (!isValidJsonStandard(jsonStandard)) {
272
- console.error(
273
- `Invalid JSON standard: ${jsonStandard}. Use "opencode" or "claude".`
274
- );
269
+ outputStatus({
270
+ type: 'error',
271
+ errorType: 'ValidationError',
272
+ message: `Invalid JSON standard: ${jsonStandard}. Use "opencode" or "claude".`,
273
+ });
275
274
  process.exit(1);
276
275
  }
277
276
 
@@ -317,24 +316,20 @@ async function runAgentMode(argv, request) {
317
316
  * @param {object} argv - Command line arguments
318
317
  */
319
318
  async function runContinuousAgentMode(argv) {
320
- // Note: verbose flag and logging are now initialized in middleware
321
- // See main() function for the middleware that sets up Flag and Log.init()
322
-
323
319
  const compactJson = argv['compact-json'] === true;
324
-
325
- // Log version and command info in verbose mode
326
- if (Flag.OPENCODE_VERBOSE) {
327
- console.error(`Agent version: ${pkg.version}`);
328
- console.error(`Command: ${process.argv.join(' ')}`);
329
- console.error(`Working directory: ${process.cwd()}`);
330
- console.error(`Script path: ${import.meta.path}`);
331
- }
332
-
333
- // Log dry-run mode if enabled
320
+ // Log version and command info in verbose mode using lazy logging
321
+ Log.Default.lazy.info(() => ({
322
+ message: 'Agent started (continuous mode)',
323
+ version: pkg.version,
324
+ command: process.argv.join(' '),
325
+ workingDirectory: process.cwd(),
326
+ scriptPath: import.meta.path,
327
+ }));
334
328
  if (Flag.OPENCODE_DRY_RUN) {
335
- console.error(
336
- `[DRY RUN MODE] No actual API calls or package installations will be made`
337
- );
329
+ Log.Default.lazy.info(() => ({
330
+ message: 'Dry run mode enabled',
331
+ mode: 'dry-run',
332
+ }));
338
333
  }
339
334
 
340
335
  const { providerID, modelID } = await parseModelConfig(argv);
@@ -935,7 +930,7 @@ async function main() {
935
930
  }
936
931
 
937
932
  // Initialize logging system
938
- // - If verbose: print logs to stderr for debugging
933
+ // - If verbose: print logs to stderr for debugging in JSON format
939
934
  // - Otherwise: write logs to file to keep CLI output clean
940
935
  await Log.init({
941
936
  print: Flag.OPENCODE_VERBOSE,
package/src/mcp/index.ts CHANGED
@@ -81,9 +81,10 @@ export namespace MCP {
81
81
  await Promise.all(
82
82
  Object.values(state.clients).map((client) =>
83
83
  client.close().catch((error) => {
84
- log.error('Failed to close MCP client', {
84
+ log.error(() => ({
85
+ message: 'Failed to close MCP client',
85
86
  error,
86
- });
87
+ }));
87
88
  })
88
89
  )
89
90
  );
@@ -119,10 +120,10 @@ export namespace MCP {
119
120
 
120
121
  async function create(key: string, mcp: Config.Mcp) {
121
122
  if (mcp.enabled === false) {
122
- log.info('mcp server disabled', { key });
123
+ log.info(() => ({ message: 'mcp server disabled', key }));
123
124
  return;
124
125
  }
125
- log.info('found', { key, type: mcp.type });
126
+ log.info(() => ({ message: 'found', key, type: mcp.type }));
126
127
  let mcpClient: MCPClient | undefined;
127
128
  let status: Status | undefined = undefined;
128
129
 
@@ -152,7 +153,11 @@ export namespace MCP {
152
153
  transport,
153
154
  })
154
155
  .then((client) => {
155
- log.info('connected', { key, transport: name });
156
+ log.info(() => ({
157
+ message: 'connected',
158
+ key,
159
+ transport: name,
160
+ }));
156
161
  mcpClient = client;
157
162
  status = { status: 'connected' };
158
163
  return true;
@@ -160,12 +165,13 @@ export namespace MCP {
160
165
  .catch((error) => {
161
166
  lastError =
162
167
  error instanceof Error ? error : new Error(String(error));
163
- log.debug('transport connection failed', {
168
+ log.debug(() => ({
169
+ message: 'transport connection failed',
164
170
  key,
165
171
  transport: name,
166
172
  url: mcp.url,
167
173
  error: lastError.message,
168
- });
174
+ }));
169
175
  status = {
170
176
  status: 'failed' as const,
171
177
  error: lastError.message,
@@ -198,11 +204,12 @@ export namespace MCP {
198
204
  };
199
205
  })
200
206
  .catch((error) => {
201
- log.error('local mcp startup failed', {
207
+ log.error(() => ({
208
+ message: 'local mcp startup failed',
202
209
  key,
203
210
  command: mcp.command,
204
211
  error: error instanceof Error ? error.message : String(error),
205
- });
212
+ }));
206
213
  status = {
207
214
  status: 'failed' as const,
208
215
  error: error instanceof Error ? error.message : String(error),
@@ -228,14 +235,19 @@ export namespace MCP {
228
235
  mcpClient.tools(),
229
236
  mcp.timeout ?? 5000
230
237
  ).catch((err) => {
231
- log.error('failed to get tools from client', { key, error: err });
238
+ log.error(() => ({
239
+ message: 'failed to get tools from client',
240
+ key,
241
+ error: err,
242
+ }));
232
243
  return undefined;
233
244
  });
234
245
  if (!result) {
235
246
  await mcpClient.close().catch((error) => {
236
- log.error('Failed to close MCP client', {
247
+ log.error(() => ({
248
+ message: 'Failed to close MCP client',
237
249
  error,
238
- });
250
+ }));
239
251
  });
240
252
  status = {
241
253
  status: 'failed',
@@ -250,10 +262,11 @@ export namespace MCP {
250
262
  };
251
263
  }
252
264
 
253
- log.info('create() successfully created client', {
265
+ log.info(() => ({
266
+ message: 'create() successfully created client',
254
267
  key,
255
268
  toolCount: Object.keys(result).length,
256
- });
269
+ }));
257
270
  return {
258
271
  mcpClient,
259
272
  status,
@@ -274,7 +287,11 @@ export namespace MCP {
274
287
  const clientsSnapshot = await clients();
275
288
  for (const [clientName, client] of Object.entries(clientsSnapshot)) {
276
289
  const tools = await client.tools().catch((e) => {
277
- log.error('failed to get tools', { clientName, error: e.message });
290
+ log.error(() => ({
291
+ message: 'failed to get tools',
292
+ clientName,
293
+ error: e.message,
294
+ }));
278
295
  const failedStatus = {
279
296
  status: 'failed' as const,
280
297
  error: e instanceof Error ? e.message : String(e),
@@ -532,13 +532,13 @@ export namespace Patch {
532
532
 
533
533
  await fs.writeFile(hunk.path, hunk.contents, 'utf-8');
534
534
  added.push(hunk.path);
535
- log.info(`Added file: ${hunk.path}`);
535
+ log.info(() => ({ message: 'Added file', path: hunk.path }));
536
536
  break;
537
537
 
538
538
  case 'delete':
539
539
  await fs.unlink(hunk.path);
540
540
  deleted.push(hunk.path);
541
- log.info(`Deleted file: ${hunk.path}`);
541
+ log.info(() => ({ message: 'Deleted file', path: hunk.path }));
542
542
  break;
543
543
 
544
544
  case 'update':
@@ -557,12 +557,16 @@ export namespace Patch {
557
557
  await fs.writeFile(hunk.move_path, fileUpdate.content, 'utf-8');
558
558
  await fs.unlink(hunk.path);
559
559
  modified.push(hunk.move_path);
560
- log.info(`Moved file: ${hunk.path} -> ${hunk.move_path}`);
560
+ log.info(() => ({
561
+ message: 'Moved file',
562
+ from: hunk.path,
563
+ to: hunk.move_path,
564
+ }));
561
565
  } else {
562
566
  // Regular update
563
567
  await fs.writeFile(hunk.path, fileUpdate.content, 'utf-8');
564
568
  modified.push(hunk.path);
565
- log.info(`Updated file: ${hunk.path}`);
569
+ log.info(() => ({ message: 'Updated file', path: hunk.path }));
566
570
  }
567
571
  break;
568
572
  }
@@ -24,7 +24,7 @@ export namespace Project {
24
24
  export type Info = z.infer<typeof Info>;
25
25
 
26
26
  export async function fromDirectory(directory: string) {
27
- log.info('fromDirectory', { directory });
27
+ log.info(() => ({ message: 'fromDirectory', directory }));
28
28
  const matches = Filesystem.up({ targets: ['.git'], start: directory });
29
29
  const git = await matches.next().then((x) => x.value);
30
30
  await matches.return();