@link-assistant/agent 0.26.15 → 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 +49 -7
- package/package.json +51 -42
- package/src/agent/agent.ts +12 -7
- package/src/auth/azure-login.ts +241 -0
- package/src/auth/plugins.ts +9 -0
- package/src/auth/provider-logins.ts +804 -0
- package/src/cli/argv.ts +1 -1
- package/src/cli/cmd/auth.ts +4 -0
- package/src/cli/cmd/models.ts +68 -13
- 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 +27 -8
- 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 +42 -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/env-template.ts +115 -0
- package/src/provider/extra-providers.ts +347 -0
- package/src/provider/free-tier-error.ts +1 -1
- package/src/provider/kilo-deprecation.ts +30 -0
- package/src/provider/kilo.ts +44 -7
- package/src/provider/model-discovery.ts +690 -0
- package/src/provider/models.ts +11 -1
- package/src/provider/parity-loaders.ts +595 -0
- package/src/provider/provider.ts +309 -203
- package/src/provider/retry-fetch.ts +5 -0
- package/src/provider/transform.ts +168 -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
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
} from './json-standard/index.ts';
|
|
27
27
|
import { McpCommand } from './cli/cmd/mcp.ts';
|
|
28
28
|
import { AuthCommand } from './cli/cmd/auth.ts';
|
|
29
|
+
import { ModelsCommand } from './cli/cmd/models.ts';
|
|
29
30
|
import { FormatError } from './cli/error.ts';
|
|
30
31
|
import { UI } from './cli/ui.ts';
|
|
31
32
|
import { errorText } from './util/error-text.ts';
|
|
@@ -494,14 +495,13 @@ async function runServerMode(
|
|
|
494
495
|
});
|
|
495
496
|
|
|
496
497
|
// Subscribe to all bus events and output in selected format
|
|
497
|
-
const { unsub: eventUnsub
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
});
|
|
498
|
+
const { unsub: eventUnsub } = createBusEventSubscription({
|
|
499
|
+
sessionID,
|
|
500
|
+
eventHandler,
|
|
501
|
+
onError: () => {
|
|
502
|
+
hasError = true;
|
|
503
|
+
},
|
|
504
|
+
});
|
|
505
505
|
unsub = eventUnsub;
|
|
506
506
|
|
|
507
507
|
// Send message to session with specified model
|
|
@@ -526,20 +526,26 @@ async function runServerMode(
|
|
|
526
526
|
temperature,
|
|
527
527
|
}),
|
|
528
528
|
}
|
|
529
|
-
)
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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
|
+
});
|
|
537
542
|
});
|
|
538
|
-
});
|
|
539
543
|
|
|
540
|
-
// Wait for
|
|
541
|
-
//
|
|
542
|
-
|
|
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).
|
|
543
549
|
await requestPromise;
|
|
544
550
|
} finally {
|
|
545
551
|
// Always clean up resources
|
|
@@ -591,14 +597,13 @@ async function runDirectMode(
|
|
|
591
597
|
});
|
|
592
598
|
|
|
593
599
|
// Subscribe to all bus events and output in selected format
|
|
594
|
-
const { unsub: eventUnsub
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
});
|
|
600
|
+
const { unsub: eventUnsub } = createBusEventSubscription({
|
|
601
|
+
sessionID,
|
|
602
|
+
eventHandler,
|
|
603
|
+
onError: () => {
|
|
604
|
+
hasError = true;
|
|
605
|
+
},
|
|
606
|
+
});
|
|
602
607
|
unsub = eventUnsub;
|
|
603
608
|
|
|
604
609
|
// Send message to session directly
|
|
@@ -628,11 +633,13 @@ async function runDirectMode(
|
|
|
628
633
|
});
|
|
629
634
|
});
|
|
630
635
|
|
|
631
|
-
// Wait for
|
|
632
|
-
//
|
|
633
|
-
//
|
|
634
|
-
//
|
|
635
|
-
|
|
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).
|
|
636
643
|
await promptPromise;
|
|
637
644
|
} finally {
|
|
638
645
|
// Always clean up resources
|
|
@@ -654,6 +661,8 @@ async function main() {
|
|
|
654
661
|
.command(McpCommand)
|
|
655
662
|
// Auth subcommand
|
|
656
663
|
.command(AuthCommand)
|
|
664
|
+
// Models subcommand (list, --discover, --refresh, --free)
|
|
665
|
+
.command(ModelsCommand)
|
|
657
666
|
// Default command for agent mode (when no subcommand specified)
|
|
658
667
|
.command({
|
|
659
668
|
command: '$0',
|