@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/agent",
3
- "version": "0.25.3",
3
+ "version": "0.25.4",
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",
@@ -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 { createRequire } from 'module';
50
- import { readFileSync } from 'fs';
51
- import { dirname, join } from 'path';
52
- import { fileURLToPath } from 'url';
53
-
54
- const require = createRequire(import.meta.url);
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 = require('path').resolve(
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 = require('path').resolve(
235
+ const resolvedPath = resolvePath(
246
236
  process.cwd(),
247
237
  argv['append-system-message-file']
248
238
  );
@@ -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: 'agent-cli-1.0.0',
230
+ version: VERSION,
230
231
  projectID: Instance.project.id,
231
232
  directory: input.directory,
232
233
  parentID: input.parentID,
@@ -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
- if (modelID.includes('gpt-5')) return [PROMPT_CODEX];
31
- if (
32
- modelID.includes('gpt-') ||
33
- modelID.includes('o1') ||
34
- modelID.includes('o3')
35
- )
36
- return [PROMPT_BEAST];
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 [PROMPT_ANTHROPIC_SPOOF.trim(), PROMPT_SUMMARIZE];
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();