@link-assistant/agent 0.26.0 → 0.26.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/package.json +9 -4
- package/src/agent/agent.ts +8 -7
- package/src/project/instance.ts +1 -0
- package/src/project/project.ts +66 -0
- package/src/session/summary.ts +8 -7
- package/src/snapshot/index.ts +19 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@link-assistant/agent",
|
|
3
|
-
"version": "0.26.
|
|
3
|
+
"version": "0.26.1",
|
|
4
4
|
"description": "A minimal, public domain AI CLI agent compatible with OpenCode's JSON interface. Bun-only runtime.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -50,8 +50,6 @@
|
|
|
50
50
|
"LICENSE"
|
|
51
51
|
],
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@actions/core": "^1.11.1",
|
|
54
|
-
"@actions/github": "^6.0.1",
|
|
55
53
|
"@agentclientprotocol/sdk": "^0.5.1",
|
|
56
54
|
"@ai-sdk/amazon-bedrock": "^3.0.74",
|
|
57
55
|
"@ai-sdk/anthropic": "^2.0.58",
|
|
@@ -134,5 +132,12 @@
|
|
|
134
132
|
"prettier --check"
|
|
135
133
|
]
|
|
136
134
|
},
|
|
137
|
-
"license": "Unlicense"
|
|
135
|
+
"license": "Unlicense",
|
|
136
|
+
"allowScripts": {
|
|
137
|
+
"@parcel/watcher@2.5.1": true,
|
|
138
|
+
"tree-sitter-bash@0.25.1": true
|
|
139
|
+
},
|
|
140
|
+
"overrides": {
|
|
141
|
+
"undici": "^7.16.0"
|
|
142
|
+
}
|
|
138
143
|
}
|
package/src/agent/agent.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Config } from '../config/file-config';
|
|
2
2
|
import z from 'zod';
|
|
3
3
|
import { Provider } from '../provider/provider';
|
|
4
|
-
import { generateObject
|
|
4
|
+
import { generateObject } from 'ai';
|
|
5
5
|
import PROMPT_GENERATE from './generate.txt';
|
|
6
6
|
import { SystemPrompt } from '../session/system';
|
|
7
7
|
import { Instance } from '../project/instance';
|
|
@@ -138,13 +138,14 @@ export namespace Agent {
|
|
|
138
138
|
const existing = await list();
|
|
139
139
|
const result = await generateObject({
|
|
140
140
|
temperature: 0.3,
|
|
141
|
+
// The system prompt belongs in `system`, not in `prompt`: the AI SDK
|
|
142
|
+
// warns that a system message inside the prompt enables prompt
|
|
143
|
+
// injection, and providers are free to treat it as ordinary content.
|
|
144
|
+
system: system.map((content) => ({
|
|
145
|
+
role: 'system' as const,
|
|
146
|
+
content,
|
|
147
|
+
})),
|
|
141
148
|
prompt: [
|
|
142
|
-
...system.map(
|
|
143
|
-
(item): ModelMessage => ({
|
|
144
|
-
role: 'system',
|
|
145
|
-
content: item,
|
|
146
|
-
})
|
|
147
|
-
),
|
|
148
149
|
{
|
|
149
150
|
role: 'user',
|
|
150
151
|
content: `Create an agent configuration based on this request: \"${input.description}\".\n\nIMPORTANT: The following identifiers already exist and must NOT be used: ${existing.map((i) => i.name).join(', ')}\n Return ONLY the JSON object, no other text, do not wrap in backticks`,
|
package/src/project/instance.ts
CHANGED
|
@@ -58,6 +58,7 @@ export const Instance = {
|
|
|
58
58
|
async dispose() {
|
|
59
59
|
Log.Default.info('disposing instance', { directory: Instance.directory });
|
|
60
60
|
await State.dispose(Instance.directory);
|
|
61
|
+
await Project.prune();
|
|
61
62
|
},
|
|
62
63
|
async disposeAll() {
|
|
63
64
|
Log.Default.info('disposing all instances');
|
package/src/project/project.ts
CHANGED
|
@@ -4,9 +4,12 @@ import path from 'path';
|
|
|
4
4
|
import { $ } from 'bun';
|
|
5
5
|
import { Storage } from '../storage/storage';
|
|
6
6
|
import { Log } from '../util/log';
|
|
7
|
+
import fs from 'node:fs/promises';
|
|
8
|
+
import { Global } from '../global';
|
|
7
9
|
|
|
8
10
|
export namespace Project {
|
|
9
11
|
const log = Log.create({ service: 'project' });
|
|
12
|
+
const recentSnapshotAge = 15 * 60_000;
|
|
10
13
|
export const Info = z
|
|
11
14
|
.object({
|
|
12
15
|
id: z.string(),
|
|
@@ -24,6 +27,7 @@ export namespace Project {
|
|
|
24
27
|
|
|
25
28
|
export async function fromDirectory(directory: string) {
|
|
26
29
|
log.info(() => ({ message: 'fromDirectory', directory }));
|
|
30
|
+
await prune();
|
|
27
31
|
const matches = Filesystem.up({ targets: ['.git'], start: directory });
|
|
28
32
|
const git = await matches.next().then((x) => x.value);
|
|
29
33
|
await matches.return();
|
|
@@ -101,4 +105,66 @@ export namespace Project {
|
|
|
101
105
|
const keys = await Storage.list(['project']);
|
|
102
106
|
return await Promise.all(keys.map((x) => Storage.read<Info>(x)));
|
|
103
107
|
}
|
|
108
|
+
|
|
109
|
+
export async function prune() {
|
|
110
|
+
const root = path.join(Global.Path.data, 'snapshot');
|
|
111
|
+
const entries = await fs
|
|
112
|
+
.readdir(root, { withFileTypes: true })
|
|
113
|
+
.catch(() => []);
|
|
114
|
+
const snapshots: { id: string; path: string; modified: number }[] = [];
|
|
115
|
+
for (const entry of entries) {
|
|
116
|
+
if (!entry.isDirectory()) continue;
|
|
117
|
+
const snapshot = path.join(root, entry.name);
|
|
118
|
+
const modified = await fs
|
|
119
|
+
.stat(snapshot)
|
|
120
|
+
.then((stat) => stat.mtimeMs)
|
|
121
|
+
.catch(() => undefined);
|
|
122
|
+
if (modified === undefined) continue;
|
|
123
|
+
snapshots.push({ id: entry.name, path: snapshot, modified });
|
|
124
|
+
}
|
|
125
|
+
snapshots.sort((a, b) => b.modified - a.modified);
|
|
126
|
+
|
|
127
|
+
const newest = snapshots[0]?.id;
|
|
128
|
+
const recent = Date.now() - recentSnapshotAge;
|
|
129
|
+
for (const snapshot of snapshots) {
|
|
130
|
+
if (snapshot.id === newest || snapshot.modified >= recent) continue;
|
|
131
|
+
const project = await Storage.read<Info>(['project', snapshot.id]).catch(
|
|
132
|
+
() => undefined
|
|
133
|
+
);
|
|
134
|
+
const live = project
|
|
135
|
+
? await fs
|
|
136
|
+
.stat(project.worktree)
|
|
137
|
+
.then((stat) => stat.isDirectory())
|
|
138
|
+
.catch(() => false)
|
|
139
|
+
: false;
|
|
140
|
+
if (live) continue;
|
|
141
|
+
|
|
142
|
+
await remove(snapshot).catch((error) =>
|
|
143
|
+
log.warn(() => ({
|
|
144
|
+
message: 'failed to prune snapshot',
|
|
145
|
+
projectID: snapshot.id,
|
|
146
|
+
error: error instanceof Error ? error.message : String(error),
|
|
147
|
+
}))
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function remove(snapshot: { id: string; path: string }) {
|
|
153
|
+
await fs.rm(snapshot.path, { recursive: true, force: true });
|
|
154
|
+
for (const session of await Storage.list(['session', snapshot.id])) {
|
|
155
|
+
const sessionID = session.at(-1)!;
|
|
156
|
+
for (const message of await Storage.list(['message', sessionID])) {
|
|
157
|
+
const messageID = message.at(-1)!;
|
|
158
|
+
for (const part of await Storage.list(['part', messageID])) {
|
|
159
|
+
await Storage.remove(part);
|
|
160
|
+
}
|
|
161
|
+
await Storage.remove(message);
|
|
162
|
+
}
|
|
163
|
+
await Storage.remove(['session_diff', sessionID]);
|
|
164
|
+
await Storage.remove(['todo', sessionID]);
|
|
165
|
+
await Storage.remove(session);
|
|
166
|
+
}
|
|
167
|
+
await Storage.remove(['project', snapshot.id]);
|
|
168
|
+
log.info(() => ({ message: 'pruned snapshot', projectID: snapshot.id }));
|
|
169
|
+
}
|
|
104
170
|
}
|
package/src/session/summary.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Provider } from '../provider/provider';
|
|
|
2
2
|
import { fn } from '../util/fn';
|
|
3
3
|
import z from 'zod';
|
|
4
4
|
import { Session } from '.';
|
|
5
|
-
import { generateText
|
|
5
|
+
import { generateText } from 'ai';
|
|
6
6
|
import { MessageV2 } from './message-v2';
|
|
7
7
|
import { Identifier } from '../id/id';
|
|
8
8
|
import { Snapshot } from '../snapshot';
|
|
@@ -187,13 +187,14 @@ export namespace SessionSummary {
|
|
|
187
187
|
model.providerID,
|
|
188
188
|
{}
|
|
189
189
|
),
|
|
190
|
+
// The system prompt belongs in `system`, not in `messages`: the AI SDK
|
|
191
|
+
// warns that a system message inside `messages` enables prompt
|
|
192
|
+
// injection, and providers are free to treat it as ordinary content.
|
|
193
|
+
system: systemPrompts.map((content) => ({
|
|
194
|
+
role: 'system' as const,
|
|
195
|
+
content,
|
|
196
|
+
})),
|
|
190
197
|
messages: [
|
|
191
|
-
...systemPrompts.map(
|
|
192
|
-
(x): ModelMessage => ({
|
|
193
|
-
role: 'system',
|
|
194
|
-
content: x,
|
|
195
|
-
})
|
|
196
|
-
),
|
|
197
198
|
{
|
|
198
199
|
role: 'user' as const,
|
|
199
200
|
content: userContent,
|
package/src/snapshot/index.ts
CHANGED
|
@@ -30,6 +30,25 @@ export namespace Snapshot {
|
|
|
30
30
|
.nothrow();
|
|
31
31
|
log.info(() => ({ message: 'initialized' }));
|
|
32
32
|
}
|
|
33
|
+
const objects =
|
|
34
|
+
await $`git rev-parse --path-format=absolute --git-path objects`
|
|
35
|
+
.quiet()
|
|
36
|
+
.cwd(Instance.worktree)
|
|
37
|
+
.nothrow();
|
|
38
|
+
if (objects.exitCode === 0) {
|
|
39
|
+
const info = path.join(git, 'objects', 'info');
|
|
40
|
+
await fs.mkdir(info, { recursive: true });
|
|
41
|
+
await fs.writeFile(
|
|
42
|
+
path.join(info, 'alternates'),
|
|
43
|
+
`${objects.text().trim()}\n`
|
|
44
|
+
);
|
|
45
|
+
} else {
|
|
46
|
+
log.warn(() => ({
|
|
47
|
+
message: 'failed to resolve repository objects',
|
|
48
|
+
exitCode: objects.exitCode,
|
|
49
|
+
stderr: objects.stderr.toString(),
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
33
52
|
await $`git --git-dir ${git} --work-tree ${Instance.worktree} add .`
|
|
34
53
|
.quiet()
|
|
35
54
|
.cwd(Instance.directory)
|