@link-assistant/agent 0.25.3 → 0.25.4
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 +1 -1
- package/src/branding.ts +47 -0
- package/src/index.js +8 -18
- 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/package.json
CHANGED
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
|
);
|
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();
|