@link-assistant/agent 0.25.3 → 0.25.5
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/LICENSE +24 -0
- package/package.json +10 -12
- package/src/branding.ts +47 -0
- package/src/index.js +8 -18
- package/src/provider/retry-fetch.ts +9 -2
- package/src/session/index.ts +2 -1
- package/src/session/system.ts +97 -16
- package/src/tool/bash.ts +2 -1
- package/src/version.ts +25 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
This is free and unencumbered software released into the public domain.
|
|
2
|
+
|
|
3
|
+
Anyone is free to copy, modify, publish, use, compile, sell, or
|
|
4
|
+
distribute this software, either in source code form or as a compiled
|
|
5
|
+
binary, for any purpose, commercial or non-commercial, and by any
|
|
6
|
+
means.
|
|
7
|
+
|
|
8
|
+
In jurisdictions that recognize copyright laws, the author or authors
|
|
9
|
+
of this software dedicate any and all copyright interest in the
|
|
10
|
+
software to the public domain. We make this dedication for the benefit
|
|
11
|
+
of the public at large and to the detriment of our heirs and
|
|
12
|
+
successors. We intend this dedication to be an overt act of
|
|
13
|
+
relinquishment in perpetuity of all present and future rights to this
|
|
14
|
+
software under copyright law.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
19
|
+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
20
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
21
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
|
23
|
+
|
|
24
|
+
For more information, please refer to <https://unlicense.org>
|
package/package.json
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@link-assistant/agent",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.5",
|
|
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",
|
|
7
7
|
"bin": {
|
|
8
|
-
"agent": "
|
|
8
|
+
"agent": "src/index.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"dev": "bun run src/index.js",
|
|
12
|
-
"test": "bun test ./tests/*.js ./tests/*.ts",
|
|
12
|
+
"test": "bun test --timeout 30000 ./tests/*.js ./tests/*.ts",
|
|
13
13
|
"test:integration": "bun test ./tests/integration/basic.js",
|
|
14
|
-
"lint": "eslint .",
|
|
15
|
-
"lint:
|
|
16
|
-
"
|
|
17
|
-
"format
|
|
14
|
+
"lint": "eslint . && npm run lint:scripts",
|
|
15
|
+
"lint:scripts": "cd ../scripts && eslint . --config ./eslint.config.mjs",
|
|
16
|
+
"lint:fix": "eslint . --fix && cd ../scripts && eslint . --config ./eslint.config.mjs --fix",
|
|
17
|
+
"format": "prettier --write . ../scripts",
|
|
18
|
+
"format:check": "prettier --check . ../scripts",
|
|
18
19
|
"check:file-size": "node ../scripts/check-file-size.mjs",
|
|
19
20
|
"check": "npm run lint && npm run format:check && npm run check:file-size",
|
|
20
21
|
"prepare": "cd .. && husky || true",
|
|
@@ -28,7 +29,7 @@
|
|
|
28
29
|
},
|
|
29
30
|
"repository": {
|
|
30
31
|
"type": "git",
|
|
31
|
-
"url": "https://github.com/link-assistant/agent"
|
|
32
|
+
"url": "git+https://github.com/link-assistant/agent.git"
|
|
32
33
|
},
|
|
33
34
|
"keywords": [
|
|
34
35
|
"ai",
|
|
@@ -46,10 +47,7 @@
|
|
|
46
47
|
"src/",
|
|
47
48
|
"package.json",
|
|
48
49
|
"README.md",
|
|
49
|
-
"
|
|
50
|
-
"../TOOLS.md",
|
|
51
|
-
"../EXAMPLES.md",
|
|
52
|
-
"../LICENSE"
|
|
50
|
+
"LICENSE"
|
|
53
51
|
],
|
|
54
52
|
"dependencies": {
|
|
55
53
|
"@actions/core": "^1.11.1",
|
package/src/branding.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for the product identity that is exposed to models
|
|
3
|
+
* and to users.
|
|
4
|
+
*
|
|
5
|
+
* Several system prompts in `src/session/prompt/*.txt` are kept byte-identical
|
|
6
|
+
* to their upstream (opencode) originals so they can be re-synced without
|
|
7
|
+
* conflicts. Instead of editing those files, the identity is substituted at
|
|
8
|
+
* render time by {@link applyBranding}, so a rename is one edit here rather
|
|
9
|
+
* than one edit per prompt file. See issue #285.
|
|
10
|
+
*/
|
|
11
|
+
export namespace Branding {
|
|
12
|
+
/** Product name as it should appear in prose. */
|
|
13
|
+
export const NAME = 'Agent';
|
|
14
|
+
/** Product name as it appears in a shell (the binary name). */
|
|
15
|
+
export const BINARY = 'agent';
|
|
16
|
+
/** Canonical source repository. */
|
|
17
|
+
export const REPO_URL = 'https://github.com/link-assistant/agent';
|
|
18
|
+
/** Where users should report issues with this tool. */
|
|
19
|
+
export const ISSUES_URL = 'https://github.com/link-assistant/agent/issues';
|
|
20
|
+
/** Where documentation about this tool lives. */
|
|
21
|
+
export const DOCS_URL = 'https://github.com/link-assistant/agent#readme';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Ordered replacements. URLs must be replaced before the bare product name,
|
|
25
|
+
* otherwise the name substitution would corrupt the URLs.
|
|
26
|
+
*/
|
|
27
|
+
const REPLACEMENTS: [RegExp, string][] = [
|
|
28
|
+
[/https:\/\/github\.com\/sst\/opencode\/issues/gi, ISSUES_URL],
|
|
29
|
+
[/https:\/\/github\.com\/sst\/opencode/gi, REPO_URL],
|
|
30
|
+
[/https:\/\/opencode\.ai\/docs/gi, DOCS_URL],
|
|
31
|
+
[/https:\/\/opencode\.ai/gi, DOCS_URL],
|
|
32
|
+
[/sst\/opencode/gi, 'link-assistant/agent'],
|
|
33
|
+
[/opencode/gi, NAME],
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Replace every upstream product reference in `text` with this product's
|
|
38
|
+
* identity. Safe to call on text that contains no references.
|
|
39
|
+
*/
|
|
40
|
+
export function apply(text: string): string {
|
|
41
|
+
let result = text;
|
|
42
|
+
for (const [pattern, replacement] of REPLACEMENTS) {
|
|
43
|
+
result = result.replace(pattern, replacement);
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/index.js
CHANGED
|
@@ -46,22 +46,12 @@ import {
|
|
|
46
46
|
outputInput,
|
|
47
47
|
} from './cli/output.ts';
|
|
48
48
|
import stripAnsi from 'strip-ansi';
|
|
49
|
-
import {
|
|
50
|
-
import {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
let pkg;
|
|
56
|
-
try {
|
|
57
|
-
pkg = require('../package.json');
|
|
58
|
-
} catch (_e) {
|
|
59
|
-
// Fallback: read package.json directly
|
|
60
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
61
|
-
const pkgPath = join(__dirname, '../package.json');
|
|
62
|
-
const pkgContent = readFileSync(pkgPath, 'utf8');
|
|
63
|
-
pkg = JSON.parse(pkgContent);
|
|
64
|
-
}
|
|
49
|
+
import { resolve as resolvePath } from 'path';
|
|
50
|
+
import { VERSION } from './version.ts';
|
|
51
|
+
|
|
52
|
+
// Version comes from the shared module so that `--version`, the process log
|
|
53
|
+
// and stored session records can never disagree (#285).
|
|
54
|
+
const pkg = { version: VERSION };
|
|
65
55
|
|
|
66
56
|
// Track if any errors occurred during execution
|
|
67
57
|
let hasError = false;
|
|
@@ -226,7 +216,7 @@ async function readSystemMessages(argv) {
|
|
|
226
216
|
let appendSystemMessage = argv['append-system-message'];
|
|
227
217
|
|
|
228
218
|
if (argv['system-message-file']) {
|
|
229
|
-
const resolvedPath =
|
|
219
|
+
const resolvedPath = resolvePath(
|
|
230
220
|
process.cwd(),
|
|
231
221
|
argv['system-message-file']
|
|
232
222
|
);
|
|
@@ -242,7 +232,7 @@ async function readSystemMessages(argv) {
|
|
|
242
232
|
}
|
|
243
233
|
|
|
244
234
|
if (argv['append-system-message-file']) {
|
|
245
|
-
const resolvedPath =
|
|
235
|
+
const resolvedPath = resolvePath(
|
|
246
236
|
process.cwd(),
|
|
247
237
|
argv['append-system-message-file']
|
|
248
238
|
);
|
|
@@ -183,9 +183,16 @@ export namespace RetryFetch {
|
|
|
183
183
|
return;
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
// The timer is deliberately NOT unref'd. Every caller awaits this
|
|
187
|
+
// promise, so an unref'd timer is the only pending work while a retry
|
|
188
|
+
// wait is in flight: the runtime is then free to stop waiting on it,
|
|
189
|
+
// which drops the retry instead of performing it. On Bun for Windows
|
|
190
|
+
// this showed up as `tests/retry-fetch.ts > retries on 429 and succeeds
|
|
191
|
+
// on second attempt` never resuming, burning the whole job timeout.
|
|
192
|
+
// See https://github.com/link-assistant/agent/issues/287.
|
|
193
|
+
// The abort path below clears the timer, so a cancelled wait still lets
|
|
194
|
+
// the process exit immediately (the concern behind #213).
|
|
186
195
|
const timeout = setTimeout(resolve, ms);
|
|
187
|
-
// Prevent sleep timer from keeping event loop alive (#213)
|
|
188
|
-
if (timeout.unref) timeout.unref();
|
|
189
196
|
|
|
190
197
|
if (signal) {
|
|
191
198
|
const abortHandler = () => {
|
package/src/session/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { SessionPrompt } from './prompt';
|
|
|
14
14
|
import { fn } from '../util/fn';
|
|
15
15
|
import { Command } from '../command';
|
|
16
16
|
import { Snapshot } from '../snapshot';
|
|
17
|
+
import { VERSION } from '../version';
|
|
17
18
|
|
|
18
19
|
export namespace Session {
|
|
19
20
|
const log = Log.create({ service: 'session' });
|
|
@@ -226,7 +227,7 @@ export namespace Session {
|
|
|
226
227
|
}) {
|
|
227
228
|
const result: Info = {
|
|
228
229
|
id: Identifier.descending('session', input.id),
|
|
229
|
-
version:
|
|
230
|
+
version: VERSION,
|
|
230
231
|
projectID: Instance.project.id,
|
|
231
232
|
directory: input.directory,
|
|
232
233
|
parentID: input.parentID,
|
package/src/session/system.ts
CHANGED
|
@@ -18,27 +18,105 @@ import PROMPT_SUMMARIZE from './prompt/summarize.txt';
|
|
|
18
18
|
import PROMPT_TITLE from './prompt/title.txt';
|
|
19
19
|
import PROMPT_CODEX from './prompt/codex.txt';
|
|
20
20
|
import PROMPT_GROK_CODE from './prompt/grok-code.txt';
|
|
21
|
+
import { Branding } from '../branding';
|
|
22
|
+
import { Log } from '../util/log';
|
|
21
23
|
|
|
22
24
|
export namespace SystemPrompt {
|
|
25
|
+
const log = Log.create({ service: 'system-prompt' });
|
|
26
|
+
|
|
23
27
|
export function header(providerID: string) {
|
|
24
28
|
if (providerID.includes('anthropic'))
|
|
25
29
|
return [PROMPT_ANTHROPIC_SPOOF.trim()];
|
|
26
30
|
return [];
|
|
27
31
|
}
|
|
28
32
|
|
|
33
|
+
/** Identifiers of the selectable system prompts. */
|
|
34
|
+
export type PromptID =
|
|
35
|
+
| 'anthropic'
|
|
36
|
+
| 'anthropic-without-todo'
|
|
37
|
+
| 'beast'
|
|
38
|
+
| 'codex'
|
|
39
|
+
| 'gemini'
|
|
40
|
+
| 'grok-code'
|
|
41
|
+
| 'polaris';
|
|
42
|
+
|
|
43
|
+
const RAW: Record<PromptID, string> = {
|
|
44
|
+
anthropic: PROMPT_ANTHROPIC,
|
|
45
|
+
'anthropic-without-todo': PROMPT_ANTHROPIC_WITHOUT_TODO,
|
|
46
|
+
beast: PROMPT_BEAST,
|
|
47
|
+
codex: PROMPT_CODEX,
|
|
48
|
+
gemini: PROMPT_GEMINI,
|
|
49
|
+
'grok-code': PROMPT_GROK_CODE,
|
|
50
|
+
polaris: PROMPT_POLARIS,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The prompt used for any model that is not matched by an explicit rule.
|
|
55
|
+
* It is the full prompt (with todo/task-tracking discipline); models that
|
|
56
|
+
* genuinely break on todo tools must opt out explicitly via the
|
|
57
|
+
* `AGENT_SYSTEM_PROMPT` environment variable. See issue #285.
|
|
58
|
+
*/
|
|
59
|
+
export const DEFAULT_PROMPT_ID: PromptID = 'anthropic';
|
|
60
|
+
|
|
61
|
+
const RULES: { matches: (modelID: string) => boolean; id: PromptID }[] = [
|
|
62
|
+
{ matches: (m) => m.includes('gpt-5'), id: 'codex' },
|
|
63
|
+
{
|
|
64
|
+
matches: (m) =>
|
|
65
|
+
m.includes('gpt-') || m.includes('o1') || m.includes('o3'),
|
|
66
|
+
id: 'beast',
|
|
67
|
+
},
|
|
68
|
+
{ matches: (m) => m.includes('gemini-'), id: 'gemini' },
|
|
69
|
+
{ matches: (m) => m.includes('claude'), id: 'anthropic' },
|
|
70
|
+
{ matches: (m) => m.includes('polaris-alpha'), id: 'polaris' },
|
|
71
|
+
{ matches: (m) => m.includes('grok-code'), id: 'grok-code' },
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
export function isPromptID(value: string): value is PromptID {
|
|
75
|
+
return value in RAW;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Resolve which prompt a model gets, and why.
|
|
80
|
+
*
|
|
81
|
+
* Resolution order:
|
|
82
|
+
* 1. explicit override via the `AGENT_SYSTEM_PROMPT` environment variable;
|
|
83
|
+
* 2. an explicit rule matching the model id;
|
|
84
|
+
* 3. {@link DEFAULT_PROMPT_ID}.
|
|
85
|
+
*/
|
|
86
|
+
export function resolve(modelID: string): { id: PromptID; reason: string } {
|
|
87
|
+
const override = process.env['AGENT_SYSTEM_PROMPT']?.trim();
|
|
88
|
+
if (override) {
|
|
89
|
+
if (isPromptID(override))
|
|
90
|
+
return { id: override, reason: 'AGENT_SYSTEM_PROMPT override' };
|
|
91
|
+
log.warn(() => ({
|
|
92
|
+
message: 'unknown AGENT_SYSTEM_PROMPT value, ignoring',
|
|
93
|
+
value: override,
|
|
94
|
+
known: Object.keys(RAW),
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
for (const rule of RULES) {
|
|
98
|
+
if (rule.matches(modelID))
|
|
99
|
+
return { id: rule.id, reason: `matched model id ${modelID}` };
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
id: DEFAULT_PROMPT_ID,
|
|
103
|
+
reason: `default for unknown model ${modelID}`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Get a prompt by id, with the product identity substituted in (#285). */
|
|
108
|
+
export function text(id: PromptID): string {
|
|
109
|
+
return Branding.apply(RAW[id]);
|
|
110
|
+
}
|
|
111
|
+
|
|
29
112
|
export function provider(modelID: string) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
modelID
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
if (modelID.includes('gemini-')) return [PROMPT_GEMINI];
|
|
38
|
-
if (modelID.includes('claude')) return [PROMPT_ANTHROPIC];
|
|
39
|
-
if (modelID.includes('polaris-alpha')) return [PROMPT_POLARIS];
|
|
40
|
-
if (modelID.includes('grok-code')) return [PROMPT_GROK_CODE];
|
|
41
|
-
return [PROMPT_ANTHROPIC_WITHOUT_TODO];
|
|
113
|
+
const resolved = resolve(modelID);
|
|
114
|
+
log.info(() => ({
|
|
115
|
+
message: `system prompt: ${resolved.id} (${resolved.reason})`,
|
|
116
|
+
prompt: resolved.id,
|
|
117
|
+
model: modelID,
|
|
118
|
+
}));
|
|
119
|
+
return [text(resolved.id)];
|
|
42
120
|
}
|
|
43
121
|
|
|
44
122
|
export async function environment() {
|
|
@@ -149,18 +227,21 @@ export namespace SystemPrompt {
|
|
|
149
227
|
export function summarize(providerID: string) {
|
|
150
228
|
switch (providerID) {
|
|
151
229
|
case 'anthropic':
|
|
152
|
-
return [
|
|
230
|
+
return [
|
|
231
|
+
PROMPT_ANTHROPIC_SPOOF.trim(),
|
|
232
|
+
Branding.apply(PROMPT_SUMMARIZE),
|
|
233
|
+
];
|
|
153
234
|
default:
|
|
154
|
-
return [PROMPT_SUMMARIZE];
|
|
235
|
+
return [Branding.apply(PROMPT_SUMMARIZE)];
|
|
155
236
|
}
|
|
156
237
|
}
|
|
157
238
|
|
|
158
239
|
export function title(providerID: string) {
|
|
159
240
|
switch (providerID) {
|
|
160
241
|
case 'anthropic':
|
|
161
|
-
return [PROMPT_ANTHROPIC_SPOOF.trim(), PROMPT_TITLE];
|
|
242
|
+
return [PROMPT_ANTHROPIC_SPOOF.trim(), Branding.apply(PROMPT_TITLE)];
|
|
162
243
|
default:
|
|
163
|
-
return [PROMPT_TITLE];
|
|
244
|
+
return [Branding.apply(PROMPT_TITLE)];
|
|
164
245
|
}
|
|
165
246
|
}
|
|
166
247
|
}
|
package/src/tool/bash.ts
CHANGED
|
@@ -2,6 +2,7 @@ import z from 'zod';
|
|
|
2
2
|
import { spawn } from 'child_process';
|
|
3
3
|
import { Tool } from './tool';
|
|
4
4
|
import DESCRIPTION from './bash.txt';
|
|
5
|
+
import { Branding } from '../branding';
|
|
5
6
|
import { Log } from '../util/log';
|
|
6
7
|
import { Instance } from '../project/instance';
|
|
7
8
|
import { lazy } from '../util/lazy';
|
|
@@ -53,7 +54,7 @@ const parser = lazy(async () => {
|
|
|
53
54
|
});
|
|
54
55
|
|
|
55
56
|
export const BashTool = Tool.define('bash', {
|
|
56
|
-
description: DESCRIPTION,
|
|
57
|
+
description: Branding.apply(DESCRIPTION),
|
|
57
58
|
parameters: z.object({
|
|
58
59
|
command: z.string().describe('The command to execute'),
|
|
59
60
|
timeout: z.number().describe('Optional timeout in milliseconds').optional(),
|
package/src/version.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for the running version.
|
|
3
|
+
*
|
|
4
|
+
* The version is read from the package manifest, so `--version`, the process
|
|
5
|
+
* log and stored session records can never disagree. See issue #285.
|
|
6
|
+
*/
|
|
7
|
+
import { createRequire } from 'module';
|
|
8
|
+
import { readFileSync } from 'fs';
|
|
9
|
+
import { dirname, join } from 'path';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
|
|
12
|
+
function read(): string {
|
|
13
|
+
try {
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
return require('../package.json').version;
|
|
16
|
+
} catch {
|
|
17
|
+
// Fallback: read package.json directly (e.g. when `require` of JSON is
|
|
18
|
+
// unavailable in the current runtime configuration).
|
|
19
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
return JSON.parse(readFileSync(join(here, '../package.json'), 'utf8'))
|
|
21
|
+
.version;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const VERSION: string = read();
|