@link-assistant/agent 0.0.8 → 0.0.11

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.
Files changed (104) hide show
  1. package/EXAMPLES.md +80 -1
  2. package/MODELS.md +72 -24
  3. package/README.md +95 -2
  4. package/TOOLS.md +20 -0
  5. package/package.json +36 -2
  6. package/src/agent/agent.ts +68 -54
  7. package/src/auth/claude-oauth.ts +426 -0
  8. package/src/auth/index.ts +28 -26
  9. package/src/auth/plugins.ts +876 -0
  10. package/src/bun/index.ts +53 -43
  11. package/src/bus/global.ts +5 -5
  12. package/src/bus/index.ts +59 -53
  13. package/src/cli/bootstrap.js +12 -12
  14. package/src/cli/bootstrap.ts +6 -6
  15. package/src/cli/cmd/agent.ts +97 -92
  16. package/src/cli/cmd/auth.ts +468 -0
  17. package/src/cli/cmd/cmd.ts +2 -2
  18. package/src/cli/cmd/export.ts +41 -41
  19. package/src/cli/cmd/mcp.ts +210 -53
  20. package/src/cli/cmd/models.ts +30 -29
  21. package/src/cli/cmd/run.ts +269 -213
  22. package/src/cli/cmd/stats.ts +185 -146
  23. package/src/cli/error.ts +17 -13
  24. package/src/cli/ui.ts +78 -0
  25. package/src/command/index.ts +26 -26
  26. package/src/config/config.ts +528 -288
  27. package/src/config/markdown.ts +15 -15
  28. package/src/file/ripgrep.ts +201 -169
  29. package/src/file/time.ts +21 -18
  30. package/src/file/watcher.ts +51 -42
  31. package/src/file.ts +1 -1
  32. package/src/flag/flag.ts +26 -11
  33. package/src/format/formatter.ts +206 -162
  34. package/src/format/index.ts +61 -61
  35. package/src/global/index.ts +21 -21
  36. package/src/id/id.ts +47 -33
  37. package/src/index.js +554 -332
  38. package/src/json-standard/index.ts +173 -0
  39. package/src/mcp/index.ts +135 -128
  40. package/src/patch/index.ts +336 -267
  41. package/src/project/bootstrap.ts +15 -15
  42. package/src/project/instance.ts +43 -36
  43. package/src/project/project.ts +47 -47
  44. package/src/project/state.ts +37 -33
  45. package/src/provider/models-macro.ts +5 -5
  46. package/src/provider/models.ts +32 -32
  47. package/src/provider/opencode.js +19 -19
  48. package/src/provider/provider.ts +518 -277
  49. package/src/provider/transform.ts +143 -102
  50. package/src/server/project.ts +21 -21
  51. package/src/server/server.ts +111 -105
  52. package/src/session/agent.js +66 -60
  53. package/src/session/compaction.ts +136 -111
  54. package/src/session/index.ts +189 -156
  55. package/src/session/message-v2.ts +312 -268
  56. package/src/session/message.ts +73 -57
  57. package/src/session/processor.ts +180 -166
  58. package/src/session/prompt.ts +678 -533
  59. package/src/session/retry.ts +26 -23
  60. package/src/session/revert.ts +76 -62
  61. package/src/session/status.ts +26 -26
  62. package/src/session/summary.ts +97 -76
  63. package/src/session/system.ts +77 -63
  64. package/src/session/todo.ts +22 -16
  65. package/src/snapshot/index.ts +92 -76
  66. package/src/storage/storage.ts +157 -120
  67. package/src/tool/bash.ts +116 -106
  68. package/src/tool/batch.ts +73 -59
  69. package/src/tool/codesearch.ts +60 -53
  70. package/src/tool/edit.ts +319 -263
  71. package/src/tool/glob.ts +32 -28
  72. package/src/tool/grep.ts +72 -53
  73. package/src/tool/invalid.ts +7 -7
  74. package/src/tool/ls.ts +77 -64
  75. package/src/tool/multiedit.ts +30 -21
  76. package/src/tool/patch.ts +121 -94
  77. package/src/tool/read.ts +140 -122
  78. package/src/tool/registry.ts +38 -38
  79. package/src/tool/task.ts +93 -60
  80. package/src/tool/todo.ts +16 -16
  81. package/src/tool/tool.ts +45 -36
  82. package/src/tool/webfetch.ts +97 -74
  83. package/src/tool/websearch.ts +78 -64
  84. package/src/tool/write.ts +21 -15
  85. package/src/util/binary.ts +27 -19
  86. package/src/util/context.ts +8 -8
  87. package/src/util/defer.ts +7 -5
  88. package/src/util/error.ts +24 -19
  89. package/src/util/eventloop.ts +16 -10
  90. package/src/util/filesystem.ts +37 -33
  91. package/src/util/fn.ts +11 -8
  92. package/src/util/iife.ts +1 -1
  93. package/src/util/keybind.ts +44 -44
  94. package/src/util/lazy.ts +7 -7
  95. package/src/util/locale.ts +20 -16
  96. package/src/util/lock.ts +43 -38
  97. package/src/util/log.ts +95 -85
  98. package/src/util/queue.ts +8 -8
  99. package/src/util/rpc.ts +35 -23
  100. package/src/util/scrap.ts +4 -4
  101. package/src/util/signal.ts +5 -5
  102. package/src/util/timeout.ts +6 -6
  103. package/src/util/token.ts +2 -2
  104. package/src/util/wildcard.ts +38 -27
@@ -0,0 +1,173 @@
1
+ /**
2
+ * JSON Standard Format Handlers
3
+ *
4
+ * Provides adapters for different JSON output formats:
5
+ * - opencode: OpenCode format (default) - pretty-printed JSON events
6
+ * - claude: Claude CLI stream-json format - NDJSON (newline-delimited JSON)
7
+ */
8
+
9
+ import { EOL } from 'os';
10
+
11
+ export type JsonStandard = 'opencode' | 'claude';
12
+
13
+ /**
14
+ * OpenCode JSON event types
15
+ */
16
+ export interface OpenCodeEvent {
17
+ type: 'step_start' | 'step_finish' | 'text' | 'tool_use' | 'error';
18
+ timestamp: number;
19
+ sessionID: string;
20
+ part?: Record<string, unknown>;
21
+ error?: string | Record<string, unknown>;
22
+ }
23
+
24
+ /**
25
+ * Claude JSON event types (stream-json format)
26
+ */
27
+ export interface ClaudeEvent {
28
+ type: 'init' | 'message' | 'tool_use' | 'tool_result' | 'result';
29
+ timestamp?: string;
30
+ session_id?: string;
31
+ role?: 'assistant' | 'user';
32
+ content?: Array<{
33
+ type: 'text' | 'tool_use';
34
+ text?: string;
35
+ name?: string;
36
+ input?: unknown;
37
+ }>;
38
+ output?: string;
39
+ name?: string;
40
+ input?: unknown;
41
+ tool_use_id?: string;
42
+ status?: 'success' | 'error';
43
+ duration_ms?: number;
44
+ model?: string;
45
+ }
46
+
47
+ /**
48
+ * Serialize JSON output based on the selected standard
49
+ */
50
+ export function serializeOutput(
51
+ event: OpenCodeEvent | ClaudeEvent,
52
+ standard: JsonStandard
53
+ ): string {
54
+ if (standard === 'claude') {
55
+ // NDJSON format - compact, one line
56
+ return JSON.stringify(event) + EOL;
57
+ }
58
+ // OpenCode format - pretty-printed
59
+ return JSON.stringify(event, null, 2) + EOL;
60
+ }
61
+
62
+ /**
63
+ * Convert OpenCode event to Claude event format
64
+ */
65
+ export function convertOpenCodeToClaude(
66
+ event: OpenCodeEvent,
67
+ startTime: number
68
+ ): ClaudeEvent | null {
69
+ const timestamp = new Date(event.timestamp).toISOString();
70
+ const session_id = event.sessionID;
71
+
72
+ switch (event.type) {
73
+ case 'step_start':
74
+ return {
75
+ type: 'init',
76
+ timestamp,
77
+ session_id,
78
+ };
79
+
80
+ case 'text':
81
+ if (event.part && typeof event.part.text === 'string') {
82
+ return {
83
+ type: 'message',
84
+ timestamp,
85
+ session_id,
86
+ role: 'assistant',
87
+ content: [
88
+ {
89
+ type: 'text',
90
+ text: event.part.text,
91
+ },
92
+ ],
93
+ };
94
+ }
95
+ return null;
96
+
97
+ case 'tool_use':
98
+ if (event.part && event.part.state) {
99
+ const state = event.part.state as Record<string, unknown>;
100
+ const tool = state.tool as Record<string, unknown> | undefined;
101
+ return {
102
+ type: 'tool_use',
103
+ timestamp,
104
+ session_id,
105
+ name: (tool?.name as string) || 'unknown',
106
+ input: tool?.parameters || {},
107
+ tool_use_id: event.part.id as string,
108
+ };
109
+ }
110
+ return null;
111
+
112
+ case 'step_finish':
113
+ return {
114
+ type: 'result',
115
+ timestamp,
116
+ session_id,
117
+ status: 'success',
118
+ duration_ms: event.timestamp - startTime,
119
+ };
120
+
121
+ case 'error':
122
+ return {
123
+ type: 'result',
124
+ timestamp,
125
+ session_id,
126
+ status: 'error',
127
+ output:
128
+ typeof event.error === 'string'
129
+ ? event.error
130
+ : JSON.stringify(event.error),
131
+ };
132
+
133
+ default:
134
+ return null;
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Create an event output handler based on the selected standard
140
+ */
141
+ export function createEventHandler(standard: JsonStandard, sessionID: string) {
142
+ const startTime = Date.now();
143
+
144
+ return {
145
+ /**
146
+ * Format and output an event
147
+ */
148
+ output(event: OpenCodeEvent): void {
149
+ if (standard === 'claude') {
150
+ const claudeEvent = convertOpenCodeToClaude(event, startTime);
151
+ if (claudeEvent) {
152
+ process.stdout.write(serializeOutput(claudeEvent, standard));
153
+ }
154
+ } else {
155
+ process.stdout.write(serializeOutput(event, standard));
156
+ }
157
+ },
158
+
159
+ /**
160
+ * Get the start time for duration calculations
161
+ */
162
+ getStartTime(): number {
163
+ return startTime;
164
+ },
165
+ };
166
+ }
167
+
168
+ /**
169
+ * Validate JSON standard option
170
+ */
171
+ export function isValidJsonStandard(value: string): value is JsonStandard {
172
+ return value === 'opencode' || value === 'claude';
173
+ }
package/src/mcp/index.ts CHANGED
@@ -1,135 +1,135 @@
1
- import { experimental_createMCPClient } from "@ai-sdk/mcp"
2
- import { type Tool } from "ai"
3
- import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
4
- import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"
5
- import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
6
- import { Config } from "../config/config"
7
- import { Log } from "../util/log"
8
- import { NamedError } from "../util/error"
9
- import z from "zod/v4"
10
- import { Instance } from "../project/instance"
11
- import { withTimeout } from "../util/timeout"
1
+ import { experimental_createMCPClient } from '@ai-sdk/mcp';
2
+ import { type Tool } from 'ai';
3
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
4
+ import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
5
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
6
+ import { Config } from '../config/config';
7
+ import { Log } from '../util/log';
8
+ import { NamedError } from '../util/error';
9
+ import z from 'zod/v4';
10
+ import { Instance } from '../project/instance';
11
+ import { withTimeout } from '../util/timeout';
12
12
 
13
13
  export namespace MCP {
14
- const log = Log.create({ service: "mcp" })
14
+ const log = Log.create({ service: 'mcp' });
15
15
 
16
16
  export const Failed = NamedError.create(
17
- "MCPFailed",
17
+ 'MCPFailed',
18
18
  z.object({
19
19
  name: z.string(),
20
- }),
21
- )
20
+ })
21
+ );
22
22
 
23
- type Client = Awaited<ReturnType<typeof experimental_createMCPClient>>
23
+ type Client = Awaited<ReturnType<typeof experimental_createMCPClient>>;
24
24
 
25
25
  export const Status = z
26
- .discriminatedUnion("status", [
26
+ .discriminatedUnion('status', [
27
27
  z
28
28
  .object({
29
- status: z.literal("connected"),
29
+ status: z.literal('connected'),
30
30
  })
31
31
  .meta({
32
- ref: "MCPStatusConnected",
32
+ ref: 'MCPStatusConnected',
33
33
  }),
34
34
  z
35
35
  .object({
36
- status: z.literal("disabled"),
36
+ status: z.literal('disabled'),
37
37
  })
38
38
  .meta({
39
- ref: "MCPStatusDisabled",
39
+ ref: 'MCPStatusDisabled',
40
40
  }),
41
41
  z
42
42
  .object({
43
- status: z.literal("failed"),
43
+ status: z.literal('failed'),
44
44
  error: z.string(),
45
45
  })
46
46
  .meta({
47
- ref: "MCPStatusFailed",
47
+ ref: 'MCPStatusFailed',
48
48
  }),
49
49
  ])
50
50
  .meta({
51
- ref: "MCPStatus",
52
- })
53
- export type Status = z.infer<typeof Status>
54
- type MCPClient = Awaited<ReturnType<typeof experimental_createMCPClient>>
51
+ ref: 'MCPStatus',
52
+ });
53
+ export type Status = z.infer<typeof Status>;
54
+ type MCPClient = Awaited<ReturnType<typeof experimental_createMCPClient>>;
55
55
 
56
56
  const state = Instance.state(
57
57
  async () => {
58
- const cfg = await Config.get()
59
- const config = cfg.mcp ?? {}
60
- const clients: Record<string, Client> = {}
61
- const status: Record<string, Status> = {}
58
+ const cfg = await Config.get();
59
+ const config = cfg.mcp ?? {};
60
+ const clients: Record<string, Client> = {};
61
+ const status: Record<string, Status> = {};
62
62
 
63
63
  await Promise.all(
64
64
  Object.entries(config).map(async ([key, mcp]) => {
65
- const result = await create(key, mcp).catch(() => undefined)
66
- if (!result) return
65
+ const result = await create(key, mcp).catch(() => undefined);
66
+ if (!result) return;
67
67
 
68
- status[key] = result.status
68
+ status[key] = result.status;
69
69
 
70
70
  if (result.mcpClient) {
71
- clients[key] = result.mcpClient
71
+ clients[key] = result.mcpClient;
72
72
  }
73
- }),
74
- )
73
+ })
74
+ );
75
75
  return {
76
76
  status,
77
77
  clients,
78
- }
78
+ };
79
79
  },
80
80
  async (state) => {
81
81
  await Promise.all(
82
82
  Object.values(state.clients).map((client) =>
83
83
  client.close().catch((error) => {
84
- log.error("Failed to close MCP client", {
84
+ log.error('Failed to close MCP client', {
85
85
  error,
86
- })
87
- }),
88
- ),
89
- )
90
- },
91
- )
86
+ });
87
+ })
88
+ )
89
+ );
90
+ }
91
+ );
92
92
 
93
93
  export async function add(name: string, mcp: Config.Mcp) {
94
- const s = await state()
95
- const result = await create(name, mcp)
94
+ const s = await state();
95
+ const result = await create(name, mcp);
96
96
  if (!result) {
97
97
  const status = {
98
- status: "failed" as const,
99
- error: "unknown error",
100
- }
101
- s.status[name] = status
98
+ status: 'failed' as const,
99
+ error: 'unknown error',
100
+ };
101
+ s.status[name] = status;
102
102
  return {
103
103
  status,
104
- }
104
+ };
105
105
  }
106
106
  if (!result.mcpClient) {
107
- s.status[name] = result.status
107
+ s.status[name] = result.status;
108
108
  return {
109
109
  status: s.status,
110
- }
110
+ };
111
111
  }
112
- s.clients[name] = result.mcpClient
113
- s.status[name] = result.status
112
+ s.clients[name] = result.mcpClient;
113
+ s.status[name] = result.status;
114
114
 
115
115
  return {
116
116
  status: s.status,
117
- }
117
+ };
118
118
  }
119
119
 
120
120
  async function create(key: string, mcp: Config.Mcp) {
121
121
  if (mcp.enabled === false) {
122
- log.info("mcp server disabled", { key })
123
- return
122
+ log.info('mcp server disabled', { key });
123
+ return;
124
124
  }
125
- log.info("found", { key, type: mcp.type })
126
- let mcpClient: MCPClient | undefined
127
- let status: Status | undefined = undefined
125
+ log.info('found', { key, type: mcp.type });
126
+ let mcpClient: MCPClient | undefined;
127
+ let status: Status | undefined = undefined;
128
128
 
129
- if (mcp.type === "remote") {
129
+ if (mcp.type === 'remote') {
130
130
  const transports = [
131
131
  {
132
- name: "StreamableHTTP",
132
+ name: 'StreamableHTTP',
133
133
  transport: new StreamableHTTPClientTransport(new URL(mcp.url), {
134
134
  requestInit: {
135
135
  headers: mcp.headers,
@@ -137,153 +137,160 @@ export namespace MCP {
137
137
  }),
138
138
  },
139
139
  {
140
- name: "SSE",
140
+ name: 'SSE',
141
141
  transport: new SSEClientTransport(new URL(mcp.url), {
142
142
  requestInit: {
143
143
  headers: mcp.headers,
144
144
  },
145
145
  }),
146
146
  },
147
- ]
148
- let lastError: Error | undefined
147
+ ];
148
+ let lastError: Error | undefined;
149
149
  for (const { name, transport } of transports) {
150
150
  const result = await experimental_createMCPClient({
151
- name: "opencode",
151
+ name: 'opencode',
152
152
  transport,
153
153
  })
154
154
  .then((client) => {
155
- log.info("connected", { key, transport: name })
156
- mcpClient = client
157
- status = { status: "connected" }
158
- return true
155
+ log.info('connected', { key, transport: name });
156
+ mcpClient = client;
157
+ status = { status: 'connected' };
158
+ return true;
159
159
  })
160
160
  .catch((error) => {
161
- lastError = error instanceof Error ? error : new Error(String(error))
162
- log.debug("transport connection failed", {
161
+ lastError =
162
+ error instanceof Error ? error : new Error(String(error));
163
+ log.debug('transport connection failed', {
163
164
  key,
164
165
  transport: name,
165
166
  url: mcp.url,
166
167
  error: lastError.message,
167
- })
168
+ });
168
169
  status = {
169
- status: "failed" as const,
170
+ status: 'failed' as const,
170
171
  error: lastError.message,
171
- }
172
- return false
173
- })
174
- if (result) break
172
+ };
173
+ return false;
174
+ });
175
+ if (result) break;
175
176
  }
176
177
  }
177
178
 
178
- if (mcp.type === "local") {
179
- const [cmd, ...args] = mcp.command
179
+ if (mcp.type === 'local') {
180
+ const [cmd, ...args] = mcp.command;
180
181
  await experimental_createMCPClient({
181
- name: "opencode",
182
+ name: 'opencode',
182
183
  transport: new StdioClientTransport({
183
- stderr: "ignore",
184
+ stderr: 'ignore',
184
185
  command: cmd,
185
186
  args,
186
187
  env: {
187
188
  ...process.env,
188
- ...(cmd === "opencode" ? { BUN_BE_BUN: "1" } : {}),
189
+ ...(cmd === 'opencode' ? { BUN_BE_BUN: '1' } : {}),
189
190
  ...mcp.environment,
190
191
  },
191
192
  }),
192
193
  })
193
194
  .then((client) => {
194
- mcpClient = client
195
+ mcpClient = client;
195
196
  status = {
196
- status: "connected",
197
- }
197
+ status: 'connected',
198
+ };
198
199
  })
199
200
  .catch((error) => {
200
- log.error("local mcp startup failed", {
201
+ log.error('local mcp startup failed', {
201
202
  key,
202
203
  command: mcp.command,
203
204
  error: error instanceof Error ? error.message : String(error),
204
- })
205
+ });
205
206
  status = {
206
- status: "failed" as const,
207
+ status: 'failed' as const,
207
208
  error: error instanceof Error ? error.message : String(error),
208
- }
209
- })
209
+ };
210
+ });
210
211
  }
211
212
 
212
213
  if (!status) {
213
214
  status = {
214
- status: "failed" as const,
215
- error: "Unknown error",
216
- }
215
+ status: 'failed' as const,
216
+ error: 'Unknown error',
217
+ };
217
218
  }
218
219
 
219
220
  if (!mcpClient) {
220
221
  return {
221
222
  mcpClient: undefined,
222
223
  status,
223
- }
224
+ };
224
225
  }
225
226
 
226
- const result = await withTimeout(mcpClient.tools(), mcp.timeout ?? 5000).catch((err) => {
227
- log.error("failed to get tools from client", { key, error: err })
228
- return undefined
229
- })
227
+ const result = await withTimeout(
228
+ mcpClient.tools(),
229
+ mcp.timeout ?? 5000
230
+ ).catch((err) => {
231
+ log.error('failed to get tools from client', { key, error: err });
232
+ return undefined;
233
+ });
230
234
  if (!result) {
231
235
  await mcpClient.close().catch((error) => {
232
- log.error("Failed to close MCP client", {
236
+ log.error('Failed to close MCP client', {
233
237
  error,
234
- })
235
- })
238
+ });
239
+ });
236
240
  status = {
237
- status: "failed",
238
- error: "Failed to get tools",
239
- }
241
+ status: 'failed',
242
+ error: 'Failed to get tools',
243
+ };
240
244
  return {
241
245
  mcpClient: undefined,
242
246
  status: {
243
- status: "failed" as const,
244
- error: "Failed to get tools",
247
+ status: 'failed' as const,
248
+ error: 'Failed to get tools',
245
249
  },
246
- }
250
+ };
247
251
  }
248
252
 
249
- log.info("create() successfully created client", { key, toolCount: Object.keys(result).length })
253
+ log.info('create() successfully created client', {
254
+ key,
255
+ toolCount: Object.keys(result).length,
256
+ });
250
257
  return {
251
258
  mcpClient,
252
259
  status,
253
- }
260
+ };
254
261
  }
255
262
 
256
263
  export async function status() {
257
- return state().then((state) => state.status)
264
+ return state().then((state) => state.status);
258
265
  }
259
266
 
260
267
  export async function clients() {
261
- return state().then((state) => state.clients)
268
+ return state().then((state) => state.clients);
262
269
  }
263
270
 
264
271
  export async function tools() {
265
- const result: Record<string, Tool> = {}
266
- const s = await state()
267
- const clientsSnapshot = await clients()
272
+ const result: Record<string, Tool> = {};
273
+ const s = await state();
274
+ const clientsSnapshot = await clients();
268
275
  for (const [clientName, client] of Object.entries(clientsSnapshot)) {
269
276
  const tools = await client.tools().catch((e) => {
270
- log.error("failed to get tools", { clientName, error: e.message })
277
+ log.error('failed to get tools', { clientName, error: e.message });
271
278
  const failedStatus = {
272
- status: "failed" as const,
279
+ status: 'failed' as const,
273
280
  error: e instanceof Error ? e.message : String(e),
274
- }
275
- s.status[clientName] = failedStatus
276
- delete s.clients[clientName]
277
- })
281
+ };
282
+ s.status[clientName] = failedStatus;
283
+ delete s.clients[clientName];
284
+ });
278
285
  if (!tools) {
279
- continue
286
+ continue;
280
287
  }
281
288
  for (const [toolName, tool] of Object.entries(tools)) {
282
- const sanitizedClientName = clientName.replace(/[^a-zA-Z0-9_-]/g, "_")
283
- const sanitizedToolName = toolName.replace(/[^a-zA-Z0-9_-]/g, "_")
284
- result[sanitizedClientName + "_" + sanitizedToolName] = tool
289
+ const sanitizedClientName = clientName.replace(/[^a-zA-Z0-9_-]/g, '_');
290
+ const sanitizedToolName = toolName.replace(/[^a-zA-Z0-9_-]/g, '_');
291
+ result[sanitizedClientName + '_' + sanitizedToolName] = tool;
285
292
  }
286
293
  }
287
- return result
294
+ return result;
288
295
  }
289
296
  }