@link-assistant/agent 0.27.0 → 0.27.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/README.md +47 -5
- package/package.json +41 -42
- package/src/cli/argv.ts +1 -1
- package/src/cli/cmd/run.ts +7 -1
- package/src/cli/continuous-mode.js +38 -12
- package/src/cli/event-handler.js +3 -1
- package/src/cli/model-config.js +20 -4
- package/src/cli/run-options.js +144 -78
- package/src/config/config.ts +306 -105
- package/src/config/defaults.ts +13 -5
- package/src/config/file-config.ts +16 -0
- package/src/file/ripgrep.ts +38 -5
- package/src/file/time.ts +86 -9
- package/src/index.js +39 -33
- package/src/mcp/index.ts +370 -106
- package/src/permission/index.ts +22 -0
- package/src/project/instance.ts +5 -1
- package/src/project/project.ts +35 -2
- package/src/provider/free-tier-error.ts +1 -1
- package/src/provider/kilo-deprecation.ts +30 -0
- package/src/provider/kilo.ts +25 -7
- package/src/provider/provider.ts +53 -22
- package/src/provider/retry-fetch.ts +5 -0
- package/src/provider/transform.ts +139 -17
- package/src/session/compaction.ts +18 -2
- package/src/session/index.ts +30 -3
- package/src/session/message-v2.ts +318 -32
- package/src/session/processor.ts +274 -140
- package/src/session/prompt.ts +154 -128
- package/src/session/retry.ts +26 -13
- package/src/session/summary.ts +4 -1
- package/src/tool/bash.ts +232 -45
- package/src/tool/bash.txt +1 -1
- package/src/tool/codesearch.ts +15 -78
- package/src/tool/edit.ts +111 -30
- package/src/tool/glob.ts +2 -1
- package/src/tool/grep.ts +175 -57
- package/src/tool/ls.ts +2 -1
- package/src/tool/multiedit.ts +3 -1
- package/src/tool/read.ts +169 -41
- package/src/tool/task.ts +26 -18
- package/src/tool/tool.ts +4 -1
- package/src/tool/webfetch.ts +68 -56
- package/src/tool/webfetch.txt +4 -4
- package/src/tool/websearch.ts +20 -75
- package/src/tool/websearch.txt +2 -0
- package/src/tool/write.ts +23 -6
- package/src/util/bom.ts +32 -0
- package/src/util/exa-mcp.ts +82 -0
- package/src/util/legacy-error-names.ts +39 -0
- package/src/util/read-bounded.ts +32 -0
- package/src/util/sse-usage-extractor.ts +67 -13
- package/src/util/timeout.ts +3 -4
- package/src/util/verbose-fetch.ts +40 -1
package/src/file/time.ts
CHANGED
|
@@ -1,41 +1,118 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
1
3
|
import { Instance } from '../project/instance';
|
|
2
4
|
import { Log } from '../util/log';
|
|
3
5
|
|
|
4
6
|
export namespace FileTime {
|
|
5
7
|
const log = Log.create({ service: 'file.time' });
|
|
8
|
+
|
|
9
|
+
// What we knew about a file when the session last read (or wrote) it.
|
|
10
|
+
// Comparing mtime/size for equality instead of `mtime > readTime` avoids
|
|
11
|
+
// false "modified since read" errors for files whose mtime is in the
|
|
12
|
+
// future (clock skew, archives, network mounts) and still catches edits
|
|
13
|
+
// made within the same millisecond as the read (upstream #17835).
|
|
14
|
+
export type Stamp = {
|
|
15
|
+
read: Date;
|
|
16
|
+
mtime: number | undefined;
|
|
17
|
+
size: number | undefined;
|
|
18
|
+
};
|
|
19
|
+
|
|
6
20
|
export const state = Instance.state(() => {
|
|
7
21
|
const read: {
|
|
8
22
|
[sessionID: string]: {
|
|
9
|
-
[path: string]:
|
|
23
|
+
[path: string]: Stamp | undefined;
|
|
10
24
|
};
|
|
11
25
|
} = {};
|
|
26
|
+
const locks = new Map<string, Promise<void>>();
|
|
12
27
|
return {
|
|
13
28
|
read,
|
|
29
|
+
locks,
|
|
14
30
|
};
|
|
15
31
|
});
|
|
16
32
|
|
|
33
|
+
// `dir/./a.txt`, `dir/a.txt`, relative paths and symlinked paths of the
|
|
34
|
+
// same file must share one entry (and one lock) (upstream #20367).
|
|
35
|
+
export function normalize(file: string) {
|
|
36
|
+
const resolved = path.resolve(Instance.directory, file);
|
|
37
|
+
try {
|
|
38
|
+
return fs.realpathSync(resolved);
|
|
39
|
+
} catch {
|
|
40
|
+
// Not created yet: canonicalize the parent so the key does not change
|
|
41
|
+
// once the file exists (e.g. /tmp -> /private/tmp on macOS).
|
|
42
|
+
try {
|
|
43
|
+
return path.join(
|
|
44
|
+
fs.realpathSync(path.dirname(resolved)),
|
|
45
|
+
path.basename(resolved)
|
|
46
|
+
);
|
|
47
|
+
} catch {
|
|
48
|
+
return resolved;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function stamp(file: string): Stamp {
|
|
54
|
+
try {
|
|
55
|
+
const info = fs.statSync(file);
|
|
56
|
+
return { read: new Date(), mtime: info.mtimeMs, size: info.size };
|
|
57
|
+
} catch {
|
|
58
|
+
return { read: new Date(), mtime: undefined, size: undefined };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
17
62
|
export function read(sessionID: string, file: string) {
|
|
63
|
+
file = normalize(file);
|
|
18
64
|
log.info(() => ({ message: 'read', sessionID, file }));
|
|
19
65
|
const { read } = state();
|
|
20
66
|
read[sessionID] = read[sessionID] || {};
|
|
21
|
-
read[sessionID][file] =
|
|
67
|
+
read[sessionID][file] = stamp(file);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Forgets a deleted session's read stamps (issue #335 leak audit). */
|
|
71
|
+
export function clear(sessionID: string) {
|
|
72
|
+
delete state().read[sessionID];
|
|
22
73
|
}
|
|
23
74
|
|
|
24
75
|
export function get(sessionID: string, file: string) {
|
|
25
|
-
return state().read[sessionID]?.[file];
|
|
76
|
+
return state().read[sessionID]?.[normalize(file)]?.read;
|
|
26
77
|
}
|
|
27
78
|
|
|
28
79
|
export async function assert(sessionID: string, filepath: string) {
|
|
29
|
-
|
|
80
|
+
filepath = normalize(filepath);
|
|
81
|
+
const time = state().read[sessionID]?.[filepath];
|
|
30
82
|
if (!time)
|
|
31
83
|
throw new Error(
|
|
32
84
|
`You must read the file ${filepath} before overwriting it. Use the Read tool first`
|
|
33
85
|
);
|
|
34
|
-
const
|
|
35
|
-
if (
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
86
|
+
const next = stamp(filepath);
|
|
87
|
+
if (next.mtime === time.mtime && next.size === time.size) return;
|
|
88
|
+
throw new Error(
|
|
89
|
+
`File ${filepath} has been modified since it was last read.\nLast modification: ${new Date(next.mtime ?? next.read.getTime()).toISOString()}\nLast read: ${time.read.toISOString()}\n\nPlease read the file again before modifying it.`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Serializes read-modify-write cycles on one file. Tool calls of a single
|
|
94
|
+
// step run concurrently (and `batch` uses Promise.all), so without this two
|
|
95
|
+
// edits of the same file both read the old content and the last write wins,
|
|
96
|
+
// silently dropping the other edit (upstream 5cf126d489).
|
|
97
|
+
export async function withLock<T>(
|
|
98
|
+
filepath: string,
|
|
99
|
+
fn: () => Promise<T>
|
|
100
|
+
): Promise<T> {
|
|
101
|
+
filepath = normalize(filepath);
|
|
102
|
+
const { locks } = state();
|
|
103
|
+
const current = locks.get(filepath) ?? Promise.resolve();
|
|
104
|
+
let release: () => void = () => {};
|
|
105
|
+
const next = new Promise<void>((resolve) => {
|
|
106
|
+
release = resolve;
|
|
107
|
+
});
|
|
108
|
+
const chained = current.then(() => next);
|
|
109
|
+
locks.set(filepath, chained);
|
|
110
|
+
await current;
|
|
111
|
+
try {
|
|
112
|
+
return await fn();
|
|
113
|
+
} finally {
|
|
114
|
+
release();
|
|
115
|
+
if (locks.get(filepath) === chained) locks.delete(filepath);
|
|
39
116
|
}
|
|
40
117
|
}
|
|
41
118
|
}
|
package/src/index.js
CHANGED
|
@@ -495,14 +495,13 @@ async function runServerMode(
|
|
|
495
495
|
});
|
|
496
496
|
|
|
497
497
|
// Subscribe to all bus events and output in selected format
|
|
498
|
-
const { unsub: eventUnsub
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
});
|
|
498
|
+
const { unsub: eventUnsub } = createBusEventSubscription({
|
|
499
|
+
sessionID,
|
|
500
|
+
eventHandler,
|
|
501
|
+
onError: () => {
|
|
502
|
+
hasError = true;
|
|
503
|
+
},
|
|
504
|
+
});
|
|
506
505
|
unsub = eventUnsub;
|
|
507
506
|
|
|
508
507
|
// Send message to session with specified model
|
|
@@ -527,20 +526,26 @@ async function runServerMode(
|
|
|
527
526
|
temperature,
|
|
528
527
|
}),
|
|
529
528
|
}
|
|
530
|
-
)
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
529
|
+
)
|
|
530
|
+
// Read the reply to the end: it completes only after the prompt has
|
|
531
|
+
// returned, successful or not (issue #335).
|
|
532
|
+
.then((response) => response.text())
|
|
533
|
+
.catch((error) => {
|
|
534
|
+
hasError = true;
|
|
535
|
+
eventHandler.output({
|
|
536
|
+
type: 'error',
|
|
537
|
+
timestamp: Date.now(),
|
|
538
|
+
sessionID,
|
|
539
|
+
message: errorText(error),
|
|
540
|
+
error: error instanceof Error ? error.message : String(error),
|
|
541
|
+
});
|
|
538
542
|
});
|
|
539
|
-
});
|
|
540
543
|
|
|
541
|
-
// Wait for
|
|
542
|
-
//
|
|
543
|
-
|
|
544
|
+
// Wait for the request (its rejection must be observed, see
|
|
545
|
+
// runDirectMode, #290). `session.idle` is published before the reply is
|
|
546
|
+
// written, so the idle event has been handled by then; if the prompt
|
|
547
|
+
// failed before its loop started there is no idle event at all, and
|
|
548
|
+
// waiting for it hung the process (issue #335).
|
|
544
549
|
await requestPromise;
|
|
545
550
|
} finally {
|
|
546
551
|
// Always clean up resources
|
|
@@ -592,14 +597,13 @@ async function runDirectMode(
|
|
|
592
597
|
});
|
|
593
598
|
|
|
594
599
|
// Subscribe to all bus events and output in selected format
|
|
595
|
-
const { unsub: eventUnsub
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
});
|
|
600
|
+
const { unsub: eventUnsub } = createBusEventSubscription({
|
|
601
|
+
sessionID,
|
|
602
|
+
eventHandler,
|
|
603
|
+
onError: () => {
|
|
604
|
+
hasError = true;
|
|
605
|
+
},
|
|
606
|
+
});
|
|
603
607
|
unsub = eventUnsub;
|
|
604
608
|
|
|
605
609
|
// Send message to session directly
|
|
@@ -629,11 +633,13 @@ async function runDirectMode(
|
|
|
629
633
|
});
|
|
630
634
|
});
|
|
631
635
|
|
|
632
|
-
// Wait for
|
|
633
|
-
//
|
|
634
|
-
//
|
|
635
|
-
//
|
|
636
|
-
|
|
636
|
+
// Wait for the prompt itself. A fatal failure publishes `session.idle`
|
|
637
|
+
// while unwinding, so awaiting only the idle event let the process exit
|
|
638
|
+
// before the rejection handler above ran — no error record, exit code 0
|
|
639
|
+
// (#290). `session.idle` is published synchronously before `prompt`
|
|
640
|
+
// settles, so its output has been written by then; and a prompt that
|
|
641
|
+
// fails before its loop starts never goes idle, so also waiting for the
|
|
642
|
+
// idle event hung the process (issue #335).
|
|
637
643
|
await promptPromise;
|
|
638
644
|
} finally {
|
|
639
645
|
// Always clean up resources
|