@link-assistant/agent 0.0.8

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 (133) hide show
  1. package/EXAMPLES.md +383 -0
  2. package/LICENSE +24 -0
  3. package/MODELS.md +95 -0
  4. package/README.md +388 -0
  5. package/TOOLS.md +134 -0
  6. package/package.json +89 -0
  7. package/src/agent/agent.ts +150 -0
  8. package/src/agent/generate.txt +75 -0
  9. package/src/auth/index.ts +64 -0
  10. package/src/bun/index.ts +96 -0
  11. package/src/bus/global.ts +10 -0
  12. package/src/bus/index.ts +119 -0
  13. package/src/cli/bootstrap.js +41 -0
  14. package/src/cli/bootstrap.ts +17 -0
  15. package/src/cli/cmd/agent.ts +165 -0
  16. package/src/cli/cmd/cmd.ts +5 -0
  17. package/src/cli/cmd/export.ts +88 -0
  18. package/src/cli/cmd/mcp.ts +80 -0
  19. package/src/cli/cmd/models.ts +58 -0
  20. package/src/cli/cmd/run.ts +359 -0
  21. package/src/cli/cmd/stats.ts +276 -0
  22. package/src/cli/error.ts +27 -0
  23. package/src/command/index.ts +73 -0
  24. package/src/command/template/initialize.txt +10 -0
  25. package/src/config/config.ts +705 -0
  26. package/src/config/markdown.ts +41 -0
  27. package/src/file/ripgrep.ts +391 -0
  28. package/src/file/time.ts +38 -0
  29. package/src/file/watcher.ts +75 -0
  30. package/src/file.ts +6 -0
  31. package/src/flag/flag.ts +19 -0
  32. package/src/format/formatter.ts +248 -0
  33. package/src/format/index.ts +137 -0
  34. package/src/global/index.ts +52 -0
  35. package/src/id/id.ts +72 -0
  36. package/src/index.js +371 -0
  37. package/src/mcp/index.ts +289 -0
  38. package/src/patch/index.ts +622 -0
  39. package/src/project/bootstrap.ts +22 -0
  40. package/src/project/instance.ts +67 -0
  41. package/src/project/project.ts +105 -0
  42. package/src/project/state.ts +65 -0
  43. package/src/provider/models-macro.ts +11 -0
  44. package/src/provider/models.ts +98 -0
  45. package/src/provider/opencode.js +47 -0
  46. package/src/provider/provider.ts +636 -0
  47. package/src/provider/transform.ts +241 -0
  48. package/src/server/project.ts +48 -0
  49. package/src/server/server.ts +249 -0
  50. package/src/session/agent.js +204 -0
  51. package/src/session/compaction.ts +249 -0
  52. package/src/session/index.ts +380 -0
  53. package/src/session/message-v2.ts +758 -0
  54. package/src/session/message.ts +189 -0
  55. package/src/session/processor.ts +356 -0
  56. package/src/session/prompt/anthropic-20250930.txt +166 -0
  57. package/src/session/prompt/anthropic.txt +105 -0
  58. package/src/session/prompt/anthropic_spoof.txt +1 -0
  59. package/src/session/prompt/beast.txt +147 -0
  60. package/src/session/prompt/build-switch.txt +5 -0
  61. package/src/session/prompt/codex.txt +318 -0
  62. package/src/session/prompt/copilot-gpt-5.txt +143 -0
  63. package/src/session/prompt/gemini.txt +155 -0
  64. package/src/session/prompt/grok-code.txt +1 -0
  65. package/src/session/prompt/plan.txt +8 -0
  66. package/src/session/prompt/polaris.txt +107 -0
  67. package/src/session/prompt/qwen.txt +109 -0
  68. package/src/session/prompt/summarize-turn.txt +5 -0
  69. package/src/session/prompt/summarize.txt +10 -0
  70. package/src/session/prompt/title.txt +25 -0
  71. package/src/session/prompt.ts +1390 -0
  72. package/src/session/retry.ts +53 -0
  73. package/src/session/revert.ts +108 -0
  74. package/src/session/status.ts +75 -0
  75. package/src/session/summary.ts +179 -0
  76. package/src/session/system.ts +138 -0
  77. package/src/session/todo.ts +36 -0
  78. package/src/snapshot/index.ts +197 -0
  79. package/src/storage/storage.ts +226 -0
  80. package/src/tool/bash.ts +193 -0
  81. package/src/tool/bash.txt +121 -0
  82. package/src/tool/batch.ts +173 -0
  83. package/src/tool/batch.txt +28 -0
  84. package/src/tool/codesearch.ts +123 -0
  85. package/src/tool/codesearch.txt +12 -0
  86. package/src/tool/edit.ts +604 -0
  87. package/src/tool/edit.txt +10 -0
  88. package/src/tool/glob.ts +65 -0
  89. package/src/tool/glob.txt +6 -0
  90. package/src/tool/grep.ts +116 -0
  91. package/src/tool/grep.txt +8 -0
  92. package/src/tool/invalid.ts +17 -0
  93. package/src/tool/ls.ts +110 -0
  94. package/src/tool/ls.txt +1 -0
  95. package/src/tool/multiedit.ts +46 -0
  96. package/src/tool/multiedit.txt +41 -0
  97. package/src/tool/patch.ts +188 -0
  98. package/src/tool/patch.txt +1 -0
  99. package/src/tool/read.ts +201 -0
  100. package/src/tool/read.txt +12 -0
  101. package/src/tool/registry.ts +87 -0
  102. package/src/tool/task.ts +126 -0
  103. package/src/tool/task.txt +60 -0
  104. package/src/tool/todo.ts +39 -0
  105. package/src/tool/todoread.txt +14 -0
  106. package/src/tool/todowrite.txt +167 -0
  107. package/src/tool/tool.ts +66 -0
  108. package/src/tool/webfetch.ts +171 -0
  109. package/src/tool/webfetch.txt +14 -0
  110. package/src/tool/websearch.ts +133 -0
  111. package/src/tool/websearch.txt +11 -0
  112. package/src/tool/write.ts +33 -0
  113. package/src/tool/write.txt +8 -0
  114. package/src/util/binary.ts +41 -0
  115. package/src/util/context.ts +25 -0
  116. package/src/util/defer.ts +12 -0
  117. package/src/util/error.ts +54 -0
  118. package/src/util/eventloop.ts +20 -0
  119. package/src/util/filesystem.ts +69 -0
  120. package/src/util/fn.ts +11 -0
  121. package/src/util/iife.ts +3 -0
  122. package/src/util/keybind.ts +79 -0
  123. package/src/util/lazy.ts +11 -0
  124. package/src/util/locale.ts +39 -0
  125. package/src/util/lock.ts +98 -0
  126. package/src/util/log.ts +177 -0
  127. package/src/util/queue.ts +19 -0
  128. package/src/util/rpc.ts +42 -0
  129. package/src/util/scrap.ts +10 -0
  130. package/src/util/signal.ts +12 -0
  131. package/src/util/timeout.ts +14 -0
  132. package/src/util/token.ts +7 -0
  133. package/src/util/wildcard.ts +54 -0
@@ -0,0 +1,241 @@
1
+ import type { ModelMessage } from "ai"
2
+ import { unique } from "remeda"
3
+ import type { JSONSchema } from "zod/v4/core"
4
+
5
+ export namespace ProviderTransform {
6
+ function normalizeMessages(msgs: ModelMessage[], providerID: string, modelID: string): ModelMessage[] {
7
+ if (modelID.includes("claude")) {
8
+ return msgs.map((msg) => {
9
+ if ((msg.role === "assistant" || msg.role === "tool") && Array.isArray(msg.content)) {
10
+ msg.content = msg.content.map((part) => {
11
+ if ((part.type === "tool-call" || part.type === "tool-result") && "toolCallId" in part) {
12
+ return {
13
+ ...part,
14
+ toolCallId: part.toolCallId.replace(/[^a-zA-Z0-9_-]/g, "_"),
15
+ }
16
+ }
17
+ return part
18
+ })
19
+ }
20
+ return msg
21
+ })
22
+ }
23
+ if (providerID === "mistral" || modelID.toLowerCase().includes("mistral")) {
24
+ const result: ModelMessage[] = []
25
+ for (let i = 0; i < msgs.length; i++) {
26
+ const msg = msgs[i]
27
+ const prevMsg = msgs[i - 1]
28
+ const nextMsg = msgs[i + 1]
29
+
30
+ if ((msg.role === "assistant" || msg.role === "tool") && Array.isArray(msg.content)) {
31
+ msg.content = msg.content.map((part) => {
32
+ if ((part.type === "tool-call" || part.type === "tool-result") && "toolCallId" in part) {
33
+ // Mistral requires alphanumeric tool call IDs with exactly 9 characters
34
+ const normalizedId = part.toolCallId
35
+ .replace(/[^a-zA-Z0-9]/g, "") // Remove non-alphanumeric characters
36
+ .substring(0, 9) // Take first 9 characters
37
+ .padEnd(9, "0") // Pad with zeros if less than 9 characters
38
+
39
+ return {
40
+ ...part,
41
+ toolCallId: normalizedId,
42
+ }
43
+ }
44
+ return part
45
+ })
46
+ }
47
+
48
+ result.push(msg)
49
+
50
+ // Fix message sequence: tool messages cannot be followed by user messages
51
+ if (msg.role === "tool" && nextMsg?.role === "user") {
52
+ result.push({
53
+ role: "assistant",
54
+ content: [
55
+ {
56
+ type: "text",
57
+ text: "Done.",
58
+ },
59
+ ],
60
+ })
61
+ }
62
+ }
63
+ return result
64
+ }
65
+
66
+ return msgs
67
+ }
68
+
69
+ function applyCaching(msgs: ModelMessage[], providerID: string): ModelMessage[] {
70
+ const system = msgs.filter((msg) => msg.role === "system").slice(0, 2)
71
+ const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
72
+
73
+ const providerOptions = {
74
+ anthropic: {
75
+ cacheControl: { type: "ephemeral" },
76
+ },
77
+ openrouter: {
78
+ cache_control: { type: "ephemeral" },
79
+ },
80
+ bedrock: {
81
+ cachePoint: { type: "ephemeral" },
82
+ },
83
+ openaiCompatible: {
84
+ cache_control: { type: "ephemeral" },
85
+ },
86
+ }
87
+
88
+ for (const msg of unique([...system, ...final])) {
89
+ const shouldUseContentOptions = providerID !== "anthropic" && Array.isArray(msg.content) && msg.content.length > 0
90
+
91
+ if (shouldUseContentOptions) {
92
+ const lastContent = msg.content[msg.content.length - 1]
93
+ if (lastContent && typeof lastContent === "object") {
94
+ lastContent.providerOptions = {
95
+ ...lastContent.providerOptions,
96
+ ...providerOptions,
97
+ }
98
+ continue
99
+ }
100
+ }
101
+
102
+ msg.providerOptions = {
103
+ ...msg.providerOptions,
104
+ ...providerOptions,
105
+ }
106
+ }
107
+
108
+ return msgs
109
+ }
110
+
111
+ export function message(msgs: ModelMessage[], providerID: string, modelID: string) {
112
+ msgs = normalizeMessages(msgs, providerID, modelID)
113
+ if (providerID === "anthropic" || modelID.includes("anthropic") || modelID.includes("claude")) {
114
+ msgs = applyCaching(msgs, providerID)
115
+ }
116
+
117
+ return msgs
118
+ }
119
+
120
+ export function temperature(_providerID: string, modelID: string) {
121
+ if (modelID.toLowerCase().includes("qwen")) return 0.55
122
+ if (modelID.toLowerCase().includes("claude")) return undefined
123
+ if (modelID.toLowerCase().includes("gemini-3-pro")) return 1.0
124
+ return 0
125
+ }
126
+
127
+ export function topP(_providerID: string, modelID: string) {
128
+ if (modelID.toLowerCase().includes("qwen")) return 1
129
+ return undefined
130
+ }
131
+
132
+ export function options(
133
+ providerID: string,
134
+ modelID: string,
135
+ npm: string,
136
+ sessionID: string,
137
+ ): Record<string, any> | undefined {
138
+ const result: Record<string, any> = {}
139
+
140
+ if (providerID === "openai") {
141
+ result["promptCacheKey"] = sessionID
142
+ }
143
+
144
+ if (modelID.includes("gpt-5") && !modelID.includes("gpt-5-chat")) {
145
+ if (modelID.includes("codex")) {
146
+ result["store"] = false
147
+ }
148
+
149
+ if (!modelID.includes("codex") && !modelID.includes("gpt-5-pro")) {
150
+ result["reasoningEffort"] = "medium"
151
+ }
152
+
153
+ if (modelID.endsWith("gpt-5.1") && providerID !== "azure") {
154
+ result["textVerbosity"] = "low"
155
+ }
156
+
157
+ if (providerID === "opencode") {
158
+ result["promptCacheKey"] = sessionID
159
+ result["include"] = ["reasoning.encrypted_content"]
160
+ result["reasoningSummary"] = "auto"
161
+ }
162
+ }
163
+ return result
164
+ }
165
+
166
+ export function providerOptions(npm: string | undefined, providerID: string, options: { [x: string]: any }) {
167
+ switch (npm) {
168
+ case "@ai-sdk/openai":
169
+ case "@ai-sdk/azure":
170
+ return {
171
+ ["openai" as string]: options,
172
+ }
173
+ case "@ai-sdk/amazon-bedrock":
174
+ return {
175
+ ["bedrock" as string]: options,
176
+ }
177
+ case "@ai-sdk/anthropic":
178
+ return {
179
+ ["anthropic" as string]: options,
180
+ }
181
+ case "@ai-sdk/gateway":
182
+ return {
183
+ ["gateway" as string]: options,
184
+ }
185
+ default:
186
+ return {
187
+ [providerID]: options,
188
+ }
189
+ }
190
+ }
191
+
192
+ export function maxOutputTokens(
193
+ npm: string,
194
+ options: Record<string, any>,
195
+ modelLimit: number,
196
+ globalLimit: number,
197
+ ): number {
198
+ const modelCap = modelLimit || globalLimit
199
+ const standardLimit = Math.min(modelCap, globalLimit)
200
+
201
+ if (npm === "@ai-sdk/anthropic") {
202
+ const thinking = options?.["thinking"]
203
+ const budgetTokens = typeof thinking?.["budgetTokens"] === "number" ? thinking["budgetTokens"] : 0
204
+ const enabled = thinking?.["type"] === "enabled"
205
+ if (enabled && budgetTokens > 0) {
206
+ // Return text tokens so that text + thinking <= model cap, preferring 32k text when possible.
207
+ if (budgetTokens + standardLimit <= modelCap) {
208
+ return standardLimit
209
+ }
210
+ return modelCap - budgetTokens
211
+ }
212
+ }
213
+
214
+ return standardLimit
215
+ }
216
+
217
+ export function schema(_providerID: string, _modelID: string, schema: JSONSchema.BaseSchema) {
218
+ /*
219
+ if (["openai", "azure"].includes(providerID)) {
220
+ if (schema.type === "object" && schema.properties) {
221
+ for (const [key, value] of Object.entries(schema.properties)) {
222
+ if (schema.required?.includes(key)) continue
223
+ schema.properties[key] = {
224
+ anyOf: [
225
+ value as JSONSchema.JSONSchema,
226
+ {
227
+ type: "null",
228
+ },
229
+ ],
230
+ }
231
+ }
232
+ }
233
+ }
234
+
235
+ if (providerID === "google") {
236
+ }
237
+ */
238
+
239
+ return schema
240
+ }
241
+ }
@@ -0,0 +1,48 @@
1
+ import { Hono } from "hono"
2
+ import { describeRoute } from "hono-openapi"
3
+ import { resolver } from "hono-openapi"
4
+ import { Instance } from "../project/instance"
5
+ import { Project } from "../project/project"
6
+
7
+ export const ProjectRoute = new Hono()
8
+ .get(
9
+ "/",
10
+ describeRoute({
11
+ description: "List all projects",
12
+ operationId: "project.list",
13
+ responses: {
14
+ 200: {
15
+ description: "List of projects",
16
+ content: {
17
+ "application/json": {
18
+ schema: resolver(Project.Info.array()),
19
+ },
20
+ },
21
+ },
22
+ },
23
+ }),
24
+ async (c) => {
25
+ const projects = await Project.list()
26
+ return c.json(projects)
27
+ },
28
+ )
29
+ .get(
30
+ "/current",
31
+ describeRoute({
32
+ description: "Get the current project",
33
+ operationId: "project.current",
34
+ responses: {
35
+ 200: {
36
+ description: "Current project",
37
+ content: {
38
+ "application/json": {
39
+ schema: resolver(Project.Info),
40
+ },
41
+ },
42
+ },
43
+ },
44
+ }),
45
+ async (c) => {
46
+ return c.json(Instance.project)
47
+ },
48
+ )
@@ -0,0 +1,249 @@
1
+ import { Log } from "../util/log"
2
+ import { describeRoute, validator, resolver } from "hono-openapi"
3
+ import { Hono } from "hono"
4
+ import { cors } from "hono/cors"
5
+ import { stream } from "hono/streaming"
6
+ import { Session } from "../session"
7
+ import z from "zod"
8
+ import { NamedError } from "../util/error"
9
+ import { MessageV2 } from "../session/message-v2"
10
+ import { Instance } from "../project/instance"
11
+ import { SessionPrompt } from "../session/prompt"
12
+ import { Storage } from "../storage/storage"
13
+ import type { ContentfulStatusCode } from "hono/utils/http-status"
14
+ import { lazy } from "../util/lazy"
15
+
16
+ const ERRORS = {
17
+ 400: {
18
+ description: "Bad request",
19
+ content: {
20
+ "application/json": {
21
+ schema: resolver(
22
+ z
23
+ .object({
24
+ data: z.any(),
25
+ errors: z.array(z.record(z.string(), z.any())),
26
+ success: z.literal(false),
27
+ })
28
+ .meta({
29
+ ref: "BadRequestError",
30
+ }),
31
+ ),
32
+ },
33
+ },
34
+ },
35
+ 404: {
36
+ description: "Not found",
37
+ content: {
38
+ "application/json": {
39
+ schema: resolver(Storage.NotFoundError.Schema),
40
+ },
41
+ },
42
+ },
43
+ } as const
44
+
45
+ function errors(...codes: number[]) {
46
+ return Object.fromEntries(codes.map((code) => [code, ERRORS[code as keyof typeof ERRORS]]))
47
+ }
48
+
49
+ export namespace Server {
50
+ const log = Log.create({ service: "server" })
51
+
52
+ const app = new Hono()
53
+ export const App = lazy(() =>
54
+ app
55
+ .onError((err, c) => {
56
+ log.error("failed", {
57
+ error: err,
58
+ })
59
+ if (err instanceof NamedError) {
60
+ let status: ContentfulStatusCode
61
+ if (err instanceof Storage.NotFoundError) status = 404
62
+ else status = 500
63
+ return c.json(err.toObject(), { status })
64
+ }
65
+ const message = err instanceof Error && err.stack ? err.stack : err.toString()
66
+ return c.json(new NamedError.Unknown({ message }).toObject(), {
67
+ status: 500,
68
+ })
69
+ })
70
+ .use(async (c, next) => {
71
+ log.info("request", {
72
+ method: c.req.method,
73
+ path: c.req.path,
74
+ })
75
+ const timer = log.time("request", {
76
+ method: c.req.method,
77
+ path: c.req.path,
78
+ })
79
+ await next()
80
+ timer.stop()
81
+ })
82
+ .use(cors())
83
+ .get("/health", (c) => {
84
+ return c.json({ status: "ok" })
85
+ })
86
+ .post(
87
+ "/session",
88
+ describeRoute({
89
+ description: "Create a new session",
90
+ operationId: "session.create",
91
+ responses: {
92
+ 200: {
93
+ description: "Created session",
94
+ content: {
95
+ "application/json": {
96
+ schema: resolver(Session.Info),
97
+ },
98
+ },
99
+ },
100
+ },
101
+ }),
102
+ validator("json", z.object({ title: z.string().optional() }).optional()),
103
+ async (c) => {
104
+ const body = c.req.valid("json") || {}
105
+ const session = await Session.create({
106
+ title: body.title,
107
+ })
108
+ return c.json(session)
109
+ },
110
+ )
111
+ .get(
112
+ "/session",
113
+ describeRoute({
114
+ description: "List all sessions",
115
+ operationId: "session.list",
116
+ responses: {
117
+ 200: {
118
+ description: "List of sessions",
119
+ content: {
120
+ "application/json": {
121
+ schema: resolver(Session.Info.array()),
122
+ },
123
+ },
124
+ },
125
+ },
126
+ }),
127
+ async (c) => {
128
+ const sessions = await Session.list()
129
+ return c.json(sessions)
130
+ },
131
+ )
132
+ .get(
133
+ "/session/:id",
134
+ describeRoute({
135
+ description: "Get a session",
136
+ operationId: "session.get",
137
+ responses: {
138
+ 200: {
139
+ description: "Session info",
140
+ content: {
141
+ "application/json": {
142
+ schema: resolver(Session.Info),
143
+ },
144
+ },
145
+ },
146
+ ...errors(404),
147
+ },
148
+ }),
149
+ validator(
150
+ "param",
151
+ z.object({
152
+ id: z.string(),
153
+ }),
154
+ ),
155
+ async (c) => {
156
+ const session = await Session.get(c.req.valid("param").id)
157
+ return c.json(session)
158
+ },
159
+ )
160
+ .post(
161
+ "/session/:id/message",
162
+ describeRoute({
163
+ description: "Create and send a new message to a session",
164
+ operationId: "session.prompt",
165
+ responses: {
166
+ 200: {
167
+ description: "Created message",
168
+ content: {
169
+ "application/json": {
170
+ schema: resolver(
171
+ z.object({
172
+ info: MessageV2.Assistant,
173
+ parts: MessageV2.Part.array(),
174
+ }),
175
+ ),
176
+ },
177
+ },
178
+ },
179
+ ...errors(400, 404),
180
+ },
181
+ }),
182
+ validator(
183
+ "param",
184
+ z.object({
185
+ id: z.string().meta({ description: "Session ID" }),
186
+ }),
187
+ ),
188
+ validator("json", SessionPrompt.PromptInput.omit({ sessionID: true })),
189
+ async (c) => {
190
+ c.status(200)
191
+ c.header("Content-Type", "application/json")
192
+ return stream(c, async (stream) => {
193
+ const sessionID = c.req.valid("param").id
194
+ const body = c.req.valid("json")
195
+ const msg = await SessionPrompt.prompt({ ...body, sessionID })
196
+ stream.write(JSON.stringify(msg))
197
+ })
198
+ },
199
+ )
200
+ .get(
201
+ "/session/:id/message",
202
+ describeRoute({
203
+ description: "List messages for a session",
204
+ operationId: "session.messages",
205
+ responses: {
206
+ 200: {
207
+ description: "List of messages",
208
+ content: {
209
+ "application/json": {
210
+ schema: resolver(MessageV2.WithParts.array()),
211
+ },
212
+ },
213
+ },
214
+ ...errors(400, 404),
215
+ },
216
+ }),
217
+ validator(
218
+ "param",
219
+ z.object({
220
+ id: z.string().meta({ description: "Session ID" }),
221
+ }),
222
+ ),
223
+ validator(
224
+ "query",
225
+ z.object({
226
+ limit: z.coerce.number().optional(),
227
+ }),
228
+ ),
229
+ async (c) => {
230
+ const query = c.req.valid("query")
231
+ const messages = await Session.messages({
232
+ sessionID: c.req.valid("param").id,
233
+ limit: query.limit,
234
+ })
235
+ return c.json(messages)
236
+ },
237
+ ),
238
+ )
239
+
240
+ export function listen(opts: { port: number; hostname: string }) {
241
+ const server = Bun.serve({
242
+ port: opts.port,
243
+ hostname: opts.hostname,
244
+ idleTimeout: 0,
245
+ fetch: App().fetch,
246
+ })
247
+ return server
248
+ }
249
+ }