@link-assistant/agent 0.8.20 → 0.8.22

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/README.md CHANGED
@@ -20,11 +20,11 @@
20
20
 
21
21
  > This is the JavaScript/Bun implementation. See also the [Rust implementation](../rust/README.md).
22
22
 
23
- This is an MVP implementation of an OpenCode-compatible CLI agent, focused on maximum efficiency and unrestricted execution. We reproduce OpenCode's `run --format json --model opencode/grok-code` mode with:
23
+ This is an MVP implementation of an OpenCode-compatible CLI agent, focused on maximum efficiency and unrestricted execution. We reproduce OpenCode's `run --format json --model opencode/kimi-k2.5-free` mode with:
24
24
 
25
- - ✅ **JSON Input/Output**: Compatible with `opencode run --format json --model opencode/grok-code`
25
+ - ✅ **JSON Input/Output**: Compatible with `opencode run --format json --model opencode/kimi-k2.5-free`
26
26
  - ✅ **Plain Text Input**: Also accepts plain text messages (auto-converted to JSON format)
27
- - ✅ **Flexible Model Selection**: Defaults to free OpenCode Zen Grok Code Fast 1, supports [OpenCode Zen](https://opencode.ai/docs/zen/), [Claude OAuth](../docs/claude-oauth.md), [Groq](../docs/groq.md), and [OpenRouter](../docs/openrouter.md) providers
27
+ - ✅ **Flexible Model Selection**: Defaults to free OpenCode Zen Kimi K2.5, supports [OpenCode Zen](https://opencode.ai/docs/zen/), [Claude OAuth](../docs/claude-oauth.md), [Groq](../docs/groq.md), and [OpenRouter](../docs/openrouter.md) providers
28
28
  - ✅ **No Restrictions**: Fully unrestricted file system and command execution access (no sandbox)
29
29
  - ✅ **Minimal Footprint**: Built with Bun.sh for maximum efficiency
30
30
  - ✅ **Full Tool Support**: 13 tools including websearch, codesearch, batch - all enabled by default
@@ -169,7 +169,7 @@ echo '{"message":"hi"}' | agent
169
169
  **With custom model:**
170
170
 
171
171
  ```bash
172
- echo "hi" | agent --model opencode/grok-code
172
+ echo "hi" | agent --model opencode/kimi-k2.5-free
173
173
  ```
174
174
 
175
175
  ### More Examples
@@ -190,12 +190,14 @@ echo '{"message":"run command","tools":[{"name":"bash","params":{"command":"ls -
190
190
  **Using different models:**
191
191
 
192
192
  ```bash
193
- # Default model (free Grok Code Fast 1)
193
+ # Default model (free Kimi K2.5)
194
194
  echo "hi" | agent
195
195
 
196
- # Other free models
197
- echo "hi" | agent --model opencode/big-pickle
196
+ # Other free models (in order of recommendation)
197
+ echo "hi" | agent --model opencode/minimax-m2.1-free
198
198
  echo "hi" | agent --model opencode/gpt-5-nano
199
+ echo "hi" | agent --model opencode/glm-4.7-free
200
+ echo "hi" | agent --model opencode/big-pickle
199
201
 
200
202
  # Premium models (OpenCode Zen subscription)
201
203
  echo "hi" | agent --model opencode/sonnet # Claude Sonnet 4.5
@@ -279,7 +281,7 @@ agent [options]
279
281
 
280
282
  Options:
281
283
  --model Model to use in format providerID/modelID
282
- Default: opencode/grok-code
284
+ Default: opencode/kimi-k2.5-free
283
285
  --json-standard JSON output format standard
284
286
  Choices: "opencode" (default), "claude" (experimental)
285
287
  --use-existing-claude-oauth Use existing Claude OAuth credentials
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/agent",
3
- "version": "0.8.20",
3
+ "version": "0.8.22",
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",
package/src/index.js CHANGED
@@ -147,7 +147,7 @@ async function parseModelConfig(argv) {
147
147
  // Parse model argument (handle model IDs with slashes like groq/qwen/qwen3-32b)
148
148
  const modelParts = argv.model.split('/');
149
149
  let providerID = modelParts[0] || 'opencode';
150
- let modelID = modelParts.slice(1).join('/') || 'grok-code';
150
+ let modelID = modelParts.slice(1).join('/') || 'kimi-k2.5-free';
151
151
 
152
152
  // Handle --use-existing-claude-oauth option
153
153
  // This reads OAuth credentials from ~/.claude/.credentials.json (Claude Code CLI)
@@ -175,7 +175,7 @@ async function parseModelConfig(argv) {
175
175
 
176
176
  // If user specified a model, use it with claude-oauth provider
177
177
  // If not, use claude-oauth/claude-sonnet-4-5 as default
178
- if (providerID === 'opencode' && modelID === 'grok-code') {
178
+ if (providerID === 'opencode' && modelID === 'kimi-k2.5-free') {
179
179
  providerID = 'claude-oauth';
180
180
  modelID = 'claude-sonnet-4-5';
181
181
  } else if (!['claude-oauth', 'anthropic'].includes(providerID)) {
@@ -575,7 +575,7 @@ async function main() {
575
575
  .option('model', {
576
576
  type: 'string',
577
577
  description: 'Model to use in format providerID/modelID',
578
- default: 'opencode/grok-code',
578
+ default: 'opencode/kimi-k2.5-free',
579
579
  })
580
580
  .option('json-standard', {
581
581
  type: 'string',
@@ -1005,7 +1005,13 @@ export namespace Provider {
1005
1005
  priority = priority.filter((m) => m !== 'claude-haiku-4.5');
1006
1006
  }
1007
1007
  if (providerID === 'opencode' || providerID === 'local') {
1008
- priority = ['gpt-5-nano'];
1008
+ priority = [
1009
+ 'kimi-k2.5-free',
1010
+ 'minimax-m2.1-free',
1011
+ 'gpt-5-nano',
1012
+ 'glm-4.7-free',
1013
+ 'big-pickle',
1014
+ ];
1009
1015
  }
1010
1016
  for (const item of priority) {
1011
1017
  for (const model of Object.keys(provider.info.models)) {
@@ -1015,10 +1021,13 @@ export namespace Provider {
1015
1021
  }
1016
1022
 
1017
1023
  const priority = [
1018
- 'grok-code',
1024
+ 'kimi-k2.5-free',
1025
+ 'minimax-m2.1-free',
1026
+ 'gpt-5-nano',
1027
+ 'glm-4.7-free',
1028
+ 'big-pickle',
1019
1029
  'gpt-5',
1020
1030
  'claude-sonnet-4',
1021
- 'big-pickle',
1022
1031
  'gemini-3-pro',
1023
1032
  ];
1024
1033
  export function sort(models: ModelsDev.Model[]) {
@@ -136,6 +136,26 @@ export namespace SessionCompaction {
136
136
  model: model.info,
137
137
  abort: input.abort,
138
138
  });
139
+ // Pre-convert messages to ModelMessage format (async in AI SDK 6.0+)
140
+ const modelMessages = await MessageV2.toModelMessage(
141
+ input.messages.filter((m) => {
142
+ if (m.info.role !== 'assistant' || m.info.error === undefined) {
143
+ return true;
144
+ }
145
+ if (
146
+ MessageV2.AbortedError.isInstance(m.info.error) &&
147
+ m.parts.some(
148
+ (part) => part.type !== 'step-start' && part.type !== 'reasoning'
149
+ )
150
+ ) {
151
+ return true;
152
+ }
153
+
154
+ return false;
155
+ })
156
+ );
157
+ // Defensive check: ensure modelMessages is iterable (AI SDK 6.0.1 compatibility fix)
158
+ const safeModelMessages = Array.isArray(modelMessages) ? modelMessages : [];
139
159
  const result = await processor.process(() =>
140
160
  streamText({
141
161
  onError(error) {
@@ -166,24 +186,7 @@ export namespace SessionCompaction {
166
186
  content: x,
167
187
  })
168
188
  ),
169
- ...MessageV2.toModelMessage(
170
- input.messages.filter((m) => {
171
- if (m.info.role !== 'assistant' || m.info.error === undefined) {
172
- return true;
173
- }
174
- if (
175
- MessageV2.AbortedError.isInstance(m.info.error) &&
176
- m.parts.some(
177
- (part) =>
178
- part.type !== 'step-start' && part.type !== 'reasoning'
179
- )
180
- ) {
181
- return true;
182
- }
183
-
184
- return false;
185
- })
186
- ),
189
+ ...safeModelMessages,
187
190
  {
188
191
  role: 'user',
189
192
  content: [
@@ -601,12 +601,12 @@ export namespace MessageV2 {
601
601
  throw new Error('unknown message type');
602
602
  }
603
603
 
604
- export function toModelMessage(
604
+ export async function toModelMessage(
605
605
  input: {
606
606
  info: Info;
607
607
  parts: Part[];
608
608
  }[]
609
- ): ModelMessage[] {
609
+ ): Promise<ModelMessage[]> {
610
610
  const result: UIMessage[] = [];
611
611
 
612
612
  for (const msg of input) {
@@ -723,7 +723,7 @@ export namespace MessageV2 {
723
723
  }
724
724
  }
725
725
 
726
- return convertToModelMessages(result);
726
+ return await convertToModelMessages(result);
727
727
  }
728
728
 
729
729
  export const stream = fn(
@@ -533,6 +533,29 @@ export namespace SessionPrompt {
533
533
  });
534
534
  }
535
535
 
536
+ // Pre-convert messages to ModelMessage format (async in AI SDK 6.0+)
537
+ const modelMessages = await MessageV2.toModelMessage(
538
+ msgs.filter((m) => {
539
+ if (m.info.role !== 'assistant' || m.info.error === undefined) {
540
+ return true;
541
+ }
542
+ if (
543
+ MessageV2.AbortedError.isInstance(m.info.error) &&
544
+ m.parts.some(
545
+ (part) => part.type !== 'step-start' && part.type !== 'reasoning'
546
+ )
547
+ ) {
548
+ return true;
549
+ }
550
+
551
+ return false;
552
+ })
553
+ );
554
+ // Defensive check: ensure modelMessages is iterable (AI SDK 6.0.1 compatibility fix)
555
+ const safeModelMessages = Array.isArray(modelMessages)
556
+ ? modelMessages
557
+ : [];
558
+
536
559
  // Verbose logging: output request details for debugging
537
560
  if (Flag.OPENCODE_VERBOSE) {
538
561
  const systemTokens = system.reduce(
@@ -676,24 +699,7 @@ export namespace SessionPrompt {
676
699
  content: x,
677
700
  })
678
701
  ),
679
- ...MessageV2.toModelMessage(
680
- msgs.filter((m) => {
681
- if (m.info.role !== 'assistant' || m.info.error === undefined) {
682
- return true;
683
- }
684
- if (
685
- MessageV2.AbortedError.isInstance(m.info.error) &&
686
- m.parts.some(
687
- (part) =>
688
- part.type !== 'step-start' && part.type !== 'reasoning'
689
- )
690
- ) {
691
- return true;
692
- }
693
-
694
- return false;
695
- })
696
- ),
702
+ ...safeModelMessages,
697
703
  ],
698
704
  tools: model.info?.tool_call === false ? undefined : tools,
699
705
  model: wrapLanguageModel({
@@ -1565,6 +1571,37 @@ export namespace SessionPrompt {
1565
1571
  thinkingBudget: 0,
1566
1572
  };
1567
1573
  }
1574
+ // Pre-convert messages to ModelMessage format (async in AI SDK 6.0+)
1575
+ const titleModelMessages = await MessageV2.toModelMessage([
1576
+ {
1577
+ info: {
1578
+ id: Identifier.ascending('message'),
1579
+ role: 'user',
1580
+ sessionID: input.session.id,
1581
+ time: {
1582
+ created: Date.now(),
1583
+ },
1584
+ agent:
1585
+ input.message.info.role === 'user'
1586
+ ? input.message.info.agent
1587
+ : 'build',
1588
+ model: {
1589
+ providerID: input.providerID,
1590
+ modelID: input.modelID,
1591
+ },
1592
+ },
1593
+ parts: input.message.parts,
1594
+ },
1595
+ ]);
1596
+ // Defensive check: ensure titleModelMessages is iterable (AI SDK 6.0.1 compatibility fix)
1597
+ const safeTitleMessages = Array.isArray(titleModelMessages)
1598
+ ? titleModelMessages
1599
+ : [];
1600
+ // Defensive check: ensure SystemPrompt.title returns iterable (fix for issue #155)
1601
+ const titleSystemMessages = SystemPrompt.title(small.providerID);
1602
+ const safeTitleSystemMessages = Array.isArray(titleSystemMessages)
1603
+ ? titleSystemMessages
1604
+ : [];
1568
1605
  await generateText({
1569
1606
  maxOutputTokens: small.info?.reasoning ? 1500 : 20,
1570
1607
  providerOptions: ProviderTransform.providerOptions(
@@ -1573,7 +1610,7 @@ export namespace SessionPrompt {
1573
1610
  options
1574
1611
  ),
1575
1612
  messages: [
1576
- ...SystemPrompt.title(small.providerID).map(
1613
+ ...safeTitleSystemMessages.map(
1577
1614
  (x): ModelMessage => ({
1578
1615
  role: 'system',
1579
1616
  content: x,
@@ -1585,27 +1622,7 @@ export namespace SessionPrompt {
1585
1622
  The following is the text to summarize:
1586
1623
  `,
1587
1624
  },
1588
- ...MessageV2.toModelMessage([
1589
- {
1590
- info: {
1591
- id: Identifier.ascending('message'),
1592
- role: 'user',
1593
- sessionID: input.session.id,
1594
- time: {
1595
- created: Date.now(),
1596
- },
1597
- agent:
1598
- input.message.info.role === 'user'
1599
- ? input.message.info.agent
1600
- : 'build',
1601
- model: {
1602
- providerID: input.providerID,
1603
- modelID: input.modelID,
1604
- },
1605
- },
1606
- parts: input.message.parts,
1607
- },
1608
- ]),
1625
+ ...safeTitleMessages,
1609
1626
  ],
1610
1627
  headers: small.info?.headers ?? {},
1611
1628
  model: small.language,
@@ -133,6 +133,8 @@ export namespace SessionSummary {
133
133
  .findLast((m) => m.info.role === 'assistant')
134
134
  ?.parts.findLast((p) => p.type === 'text')?.text;
135
135
  if (!summary || diffs.length > 0) {
136
+ // Pre-convert messages to ModelMessage format (async in AI SDK 6.0+)
137
+ const modelMessages = await MessageV2.toModelMessage(messages);
136
138
  const result = await generateText({
137
139
  model: small.language,
138
140
  maxOutputTokens: 100,
@@ -142,7 +144,7 @@ export namespace SessionSummary {
142
144
  content: `
143
145
  Summarize the following conversation into 2 sentences MAX explaining what the assistant did and why. Do not explain the user's input. Do not speak in the third person about the assistant.
144
146
  <conversation>
145
- ${JSON.stringify(MessageV2.toModelMessage(messages))}
147
+ ${JSON.stringify(modelMessages)}
146
148
  </conversation>
147
149
  `,
148
150
  },
package/src/tool/task.ts CHANGED
@@ -99,7 +99,7 @@ export const TaskTool = Tool.define('task', async () => {
99
99
 
100
100
  const model = agent.model ??
101
101
  parentModel ?? {
102
- modelID: 'grok-code',
102
+ modelID: 'kimi-k2.5-free',
103
103
  providerID: 'opencode',
104
104
  };
105
105