@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/agent",
3
- "version": "0.26.0",
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
  }
@@ -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, type ModelMessage } from 'ai';
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`,
@@ -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');
@@ -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
  }
@@ -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, type ModelMessage } from 'ai';
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,
@@ -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)