@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.
- package/EXAMPLES.md +383 -0
- package/LICENSE +24 -0
- package/MODELS.md +95 -0
- package/README.md +388 -0
- package/TOOLS.md +134 -0
- package/package.json +89 -0
- package/src/agent/agent.ts +150 -0
- package/src/agent/generate.txt +75 -0
- package/src/auth/index.ts +64 -0
- package/src/bun/index.ts +96 -0
- package/src/bus/global.ts +10 -0
- package/src/bus/index.ts +119 -0
- package/src/cli/bootstrap.js +41 -0
- package/src/cli/bootstrap.ts +17 -0
- package/src/cli/cmd/agent.ts +165 -0
- package/src/cli/cmd/cmd.ts +5 -0
- package/src/cli/cmd/export.ts +88 -0
- package/src/cli/cmd/mcp.ts +80 -0
- package/src/cli/cmd/models.ts +58 -0
- package/src/cli/cmd/run.ts +359 -0
- package/src/cli/cmd/stats.ts +276 -0
- package/src/cli/error.ts +27 -0
- package/src/command/index.ts +73 -0
- package/src/command/template/initialize.txt +10 -0
- package/src/config/config.ts +705 -0
- package/src/config/markdown.ts +41 -0
- package/src/file/ripgrep.ts +391 -0
- package/src/file/time.ts +38 -0
- package/src/file/watcher.ts +75 -0
- package/src/file.ts +6 -0
- package/src/flag/flag.ts +19 -0
- package/src/format/formatter.ts +248 -0
- package/src/format/index.ts +137 -0
- package/src/global/index.ts +52 -0
- package/src/id/id.ts +72 -0
- package/src/index.js +371 -0
- package/src/mcp/index.ts +289 -0
- package/src/patch/index.ts +622 -0
- package/src/project/bootstrap.ts +22 -0
- package/src/project/instance.ts +67 -0
- package/src/project/project.ts +105 -0
- package/src/project/state.ts +65 -0
- package/src/provider/models-macro.ts +11 -0
- package/src/provider/models.ts +98 -0
- package/src/provider/opencode.js +47 -0
- package/src/provider/provider.ts +636 -0
- package/src/provider/transform.ts +241 -0
- package/src/server/project.ts +48 -0
- package/src/server/server.ts +249 -0
- package/src/session/agent.js +204 -0
- package/src/session/compaction.ts +249 -0
- package/src/session/index.ts +380 -0
- package/src/session/message-v2.ts +758 -0
- package/src/session/message.ts +189 -0
- package/src/session/processor.ts +356 -0
- package/src/session/prompt/anthropic-20250930.txt +166 -0
- package/src/session/prompt/anthropic.txt +105 -0
- package/src/session/prompt/anthropic_spoof.txt +1 -0
- package/src/session/prompt/beast.txt +147 -0
- package/src/session/prompt/build-switch.txt +5 -0
- package/src/session/prompt/codex.txt +318 -0
- package/src/session/prompt/copilot-gpt-5.txt +143 -0
- package/src/session/prompt/gemini.txt +155 -0
- package/src/session/prompt/grok-code.txt +1 -0
- package/src/session/prompt/plan.txt +8 -0
- package/src/session/prompt/polaris.txt +107 -0
- package/src/session/prompt/qwen.txt +109 -0
- package/src/session/prompt/summarize-turn.txt +5 -0
- package/src/session/prompt/summarize.txt +10 -0
- package/src/session/prompt/title.txt +25 -0
- package/src/session/prompt.ts +1390 -0
- package/src/session/retry.ts +53 -0
- package/src/session/revert.ts +108 -0
- package/src/session/status.ts +75 -0
- package/src/session/summary.ts +179 -0
- package/src/session/system.ts +138 -0
- package/src/session/todo.ts +36 -0
- package/src/snapshot/index.ts +197 -0
- package/src/storage/storage.ts +226 -0
- package/src/tool/bash.ts +193 -0
- package/src/tool/bash.txt +121 -0
- package/src/tool/batch.ts +173 -0
- package/src/tool/batch.txt +28 -0
- package/src/tool/codesearch.ts +123 -0
- package/src/tool/codesearch.txt +12 -0
- package/src/tool/edit.ts +604 -0
- package/src/tool/edit.txt +10 -0
- package/src/tool/glob.ts +65 -0
- package/src/tool/glob.txt +6 -0
- package/src/tool/grep.ts +116 -0
- package/src/tool/grep.txt +8 -0
- package/src/tool/invalid.ts +17 -0
- package/src/tool/ls.ts +110 -0
- package/src/tool/ls.txt +1 -0
- package/src/tool/multiedit.ts +46 -0
- package/src/tool/multiedit.txt +41 -0
- package/src/tool/patch.ts +188 -0
- package/src/tool/patch.txt +1 -0
- package/src/tool/read.ts +201 -0
- package/src/tool/read.txt +12 -0
- package/src/tool/registry.ts +87 -0
- package/src/tool/task.ts +126 -0
- package/src/tool/task.txt +60 -0
- package/src/tool/todo.ts +39 -0
- package/src/tool/todoread.txt +14 -0
- package/src/tool/todowrite.txt +167 -0
- package/src/tool/tool.ts +66 -0
- package/src/tool/webfetch.ts +171 -0
- package/src/tool/webfetch.txt +14 -0
- package/src/tool/websearch.ts +133 -0
- package/src/tool/websearch.txt +11 -0
- package/src/tool/write.ts +33 -0
- package/src/tool/write.txt +8 -0
- package/src/util/binary.ts +41 -0
- package/src/util/context.ts +25 -0
- package/src/util/defer.ts +12 -0
- package/src/util/error.ts +54 -0
- package/src/util/eventloop.ts +20 -0
- package/src/util/filesystem.ts +69 -0
- package/src/util/fn.ts +11 -0
- package/src/util/iife.ts +3 -0
- package/src/util/keybind.ts +79 -0
- package/src/util/lazy.ts +11 -0
- package/src/util/locale.ts +39 -0
- package/src/util/lock.ts +98 -0
- package/src/util/log.ts +177 -0
- package/src/util/queue.ts +19 -0
- package/src/util/rpc.ts +42 -0
- package/src/util/scrap.ts +10 -0
- package/src/util/signal.ts +12 -0
- package/src/util/timeout.ts +14 -0
- package/src/util/token.ts +7 -0
- package/src/util/wildcard.ts +54 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function defer<T extends () => void | Promise<void>>(
|
|
2
|
+
fn: T,
|
|
3
|
+
): T extends () => Promise<void> ? { [Symbol.asyncDispose]: () => Promise<void> } : { [Symbol.dispose]: () => void } {
|
|
4
|
+
return {
|
|
5
|
+
[Symbol.dispose]() {
|
|
6
|
+
fn()
|
|
7
|
+
},
|
|
8
|
+
[Symbol.asyncDispose]() {
|
|
9
|
+
return Promise.resolve(fn())
|
|
10
|
+
},
|
|
11
|
+
} as any
|
|
12
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import z from "zod"
|
|
2
|
+
|
|
3
|
+
export abstract class NamedError extends Error {
|
|
4
|
+
abstract schema(): z.core.$ZodType
|
|
5
|
+
abstract toObject(): { name: string; data: any }
|
|
6
|
+
|
|
7
|
+
static create<Name extends string, Data extends z.core.$ZodType>(name: Name, data: Data) {
|
|
8
|
+
const schema = z
|
|
9
|
+
.object({
|
|
10
|
+
name: z.literal(name),
|
|
11
|
+
data,
|
|
12
|
+
})
|
|
13
|
+
.meta({
|
|
14
|
+
ref: name,
|
|
15
|
+
})
|
|
16
|
+
const result = class extends NamedError {
|
|
17
|
+
public static readonly Schema = schema
|
|
18
|
+
|
|
19
|
+
public override readonly name = name as Name
|
|
20
|
+
|
|
21
|
+
constructor(
|
|
22
|
+
public readonly data: z.input<Data>,
|
|
23
|
+
options?: ErrorOptions,
|
|
24
|
+
) {
|
|
25
|
+
super(name, options)
|
|
26
|
+
this.name = name
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static isInstance(input: any): input is InstanceType<typeof result> {
|
|
30
|
+
return typeof input === "object" && "name" in input && input.name === name
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
schema() {
|
|
34
|
+
return schema
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
toObject() {
|
|
38
|
+
return {
|
|
39
|
+
name: name,
|
|
40
|
+
data: this.data,
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
Object.defineProperty(result, "name", { value: name })
|
|
45
|
+
return result
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public static readonly Unknown = NamedError.create(
|
|
49
|
+
"UnknownError",
|
|
50
|
+
z.object({
|
|
51
|
+
message: z.string(),
|
|
52
|
+
}),
|
|
53
|
+
)
|
|
54
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Log } from "./log"
|
|
2
|
+
|
|
3
|
+
export namespace EventLoop {
|
|
4
|
+
export async function wait() {
|
|
5
|
+
return new Promise<void>((resolve) => {
|
|
6
|
+
const check = () => {
|
|
7
|
+
const active = [...(process as any)._getActiveHandles(), ...(process as any)._getActiveRequests()]
|
|
8
|
+
Log.Default.info("eventloop", {
|
|
9
|
+
active,
|
|
10
|
+
})
|
|
11
|
+
if ((process as any)._getActiveHandles().length === 0 && (process as any)._getActiveRequests().length === 0) {
|
|
12
|
+
resolve()
|
|
13
|
+
} else {
|
|
14
|
+
setImmediate(check)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
check()
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { exists } from "fs/promises"
|
|
2
|
+
import { dirname, join, relative } from "path"
|
|
3
|
+
|
|
4
|
+
export namespace Filesystem {
|
|
5
|
+
export function overlaps(a: string, b: string) {
|
|
6
|
+
const relA = relative(a, b)
|
|
7
|
+
const relB = relative(b, a)
|
|
8
|
+
return !relA || !relA.startsWith("..") || !relB || !relB.startsWith("..")
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function contains(parent: string, child: string) {
|
|
12
|
+
return !relative(parent, child).startsWith("..")
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function findUp(target: string, start: string, stop?: string) {
|
|
16
|
+
let current = start
|
|
17
|
+
const result = []
|
|
18
|
+
while (true) {
|
|
19
|
+
const search = join(current, target)
|
|
20
|
+
if (await exists(search)) result.push(search)
|
|
21
|
+
if (stop === current) break
|
|
22
|
+
const parent = dirname(current)
|
|
23
|
+
if (parent === current) break
|
|
24
|
+
current = parent
|
|
25
|
+
}
|
|
26
|
+
return result
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function* up(options: { targets: string[]; start: string; stop?: string }) {
|
|
30
|
+
const { targets, start, stop } = options
|
|
31
|
+
let current = start
|
|
32
|
+
while (true) {
|
|
33
|
+
for (const target of targets) {
|
|
34
|
+
const search = join(current, target)
|
|
35
|
+
if (await exists(search)) yield search
|
|
36
|
+
}
|
|
37
|
+
if (stop === current) break
|
|
38
|
+
const parent = dirname(current)
|
|
39
|
+
if (parent === current) break
|
|
40
|
+
current = parent
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function globUp(pattern: string, start: string, stop?: string) {
|
|
45
|
+
let current = start
|
|
46
|
+
const result = []
|
|
47
|
+
while (true) {
|
|
48
|
+
try {
|
|
49
|
+
const glob = new Bun.Glob(pattern)
|
|
50
|
+
for await (const match of glob.scan({
|
|
51
|
+
cwd: current,
|
|
52
|
+
absolute: true,
|
|
53
|
+
onlyFiles: true,
|
|
54
|
+
followSymlinks: true,
|
|
55
|
+
dot: true,
|
|
56
|
+
})) {
|
|
57
|
+
result.push(match)
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
// Skip invalid glob patterns
|
|
61
|
+
}
|
|
62
|
+
if (stop === current) break
|
|
63
|
+
const parent = dirname(current)
|
|
64
|
+
if (parent === current) break
|
|
65
|
+
current = parent
|
|
66
|
+
}
|
|
67
|
+
return result
|
|
68
|
+
}
|
|
69
|
+
}
|
package/src/util/fn.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { z } from "zod"
|
|
2
|
+
|
|
3
|
+
export function fn<T extends z.ZodType, Result>(schema: T, cb: (input: z.infer<T>) => Result) {
|
|
4
|
+
const result = (input: z.infer<T>) => {
|
|
5
|
+
const parsed = schema.parse(input)
|
|
6
|
+
return cb(parsed)
|
|
7
|
+
}
|
|
8
|
+
result.force = (input: z.infer<T>) => cb(input)
|
|
9
|
+
result.schema = schema
|
|
10
|
+
return result
|
|
11
|
+
}
|
package/src/util/iife.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { isDeepEqual } from "remeda"
|
|
2
|
+
|
|
3
|
+
export namespace Keybind {
|
|
4
|
+
export type Info = {
|
|
5
|
+
ctrl: boolean
|
|
6
|
+
meta: boolean
|
|
7
|
+
shift: boolean
|
|
8
|
+
leader: boolean
|
|
9
|
+
name: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function match(a: Info, b: Info): boolean {
|
|
13
|
+
return isDeepEqual(a, b)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function toString(info: Info): string {
|
|
17
|
+
const parts: string[] = []
|
|
18
|
+
|
|
19
|
+
if (info.ctrl) parts.push("ctrl")
|
|
20
|
+
if (info.meta) parts.push("alt")
|
|
21
|
+
if (info.shift) parts.push("shift")
|
|
22
|
+
if (info.name) {
|
|
23
|
+
if (info.name === "delete") parts.push("del")
|
|
24
|
+
else parts.push(info.name)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let result = parts.join("+")
|
|
28
|
+
|
|
29
|
+
if (info.leader) {
|
|
30
|
+
result = result ? `<leader> ${result}` : `<leader>`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return result
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function parse(key: string): Info[] {
|
|
37
|
+
if (key === "none") return []
|
|
38
|
+
|
|
39
|
+
return key.split(",").map((combo) => {
|
|
40
|
+
// Handle <leader> syntax by replacing with leader+
|
|
41
|
+
const normalized = combo.replace(/<leader>/g, "leader+")
|
|
42
|
+
const parts = normalized.toLowerCase().split("+")
|
|
43
|
+
const info: Info = {
|
|
44
|
+
ctrl: false,
|
|
45
|
+
meta: false,
|
|
46
|
+
shift: false,
|
|
47
|
+
leader: false,
|
|
48
|
+
name: "",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const part of parts) {
|
|
52
|
+
switch (part) {
|
|
53
|
+
case "ctrl":
|
|
54
|
+
info.ctrl = true
|
|
55
|
+
break
|
|
56
|
+
case "alt":
|
|
57
|
+
case "meta":
|
|
58
|
+
case "option":
|
|
59
|
+
info.meta = true
|
|
60
|
+
break
|
|
61
|
+
case "shift":
|
|
62
|
+
info.shift = true
|
|
63
|
+
break
|
|
64
|
+
case "leader":
|
|
65
|
+
info.leader = true
|
|
66
|
+
break
|
|
67
|
+
case "esc":
|
|
68
|
+
info.name = "escape"
|
|
69
|
+
break
|
|
70
|
+
default:
|
|
71
|
+
info.name = part
|
|
72
|
+
break
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return info
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
}
|
package/src/util/lazy.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export namespace Locale {
|
|
2
|
+
export function titlecase(str: string) {
|
|
3
|
+
return str.replace(/\b\w/g, (c) => c.toUpperCase())
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function time(input: number) {
|
|
7
|
+
const date = new Date(input)
|
|
8
|
+
return date.toLocaleTimeString()
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function number(num: number): string {
|
|
12
|
+
if (num >= 1000000) {
|
|
13
|
+
return (num / 1000000).toFixed(1) + "M"
|
|
14
|
+
} else if (num >= 1000) {
|
|
15
|
+
return (num / 1000).toFixed(1) + "K"
|
|
16
|
+
}
|
|
17
|
+
return num.toString()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function truncate(str: string, len: number): string {
|
|
21
|
+
if (str.length <= len) return str
|
|
22
|
+
return str.slice(0, len - 1) + "…"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function truncateMiddle(str: string, maxLength: number = 35): string {
|
|
26
|
+
if (str.length <= maxLength) return str
|
|
27
|
+
|
|
28
|
+
const ellipsis = "…"
|
|
29
|
+
const keepStart = Math.ceil((maxLength - ellipsis.length) / 2)
|
|
30
|
+
const keepEnd = Math.floor((maxLength - ellipsis.length) / 2)
|
|
31
|
+
|
|
32
|
+
return str.slice(0, keepStart) + ellipsis + str.slice(-keepEnd)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function pluralize(count: number, singular: string, plural: string): string {
|
|
36
|
+
const template = count === 1 ? singular : plural
|
|
37
|
+
return template.replace("{}", count.toString())
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/util/lock.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export namespace Lock {
|
|
2
|
+
const locks = new Map<
|
|
3
|
+
string,
|
|
4
|
+
{
|
|
5
|
+
readers: number
|
|
6
|
+
writer: boolean
|
|
7
|
+
waitingReaders: (() => void)[]
|
|
8
|
+
waitingWriters: (() => void)[]
|
|
9
|
+
}
|
|
10
|
+
>()
|
|
11
|
+
|
|
12
|
+
function get(key: string) {
|
|
13
|
+
if (!locks.has(key)) {
|
|
14
|
+
locks.set(key, {
|
|
15
|
+
readers: 0,
|
|
16
|
+
writer: false,
|
|
17
|
+
waitingReaders: [],
|
|
18
|
+
waitingWriters: [],
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
return locks.get(key)!
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function process(key: string) {
|
|
25
|
+
const lock = locks.get(key)
|
|
26
|
+
if (!lock || lock.writer || lock.readers > 0) return
|
|
27
|
+
|
|
28
|
+
// Prioritize writers to prevent starvation
|
|
29
|
+
if (lock.waitingWriters.length > 0) {
|
|
30
|
+
const nextWriter = lock.waitingWriters.shift()!
|
|
31
|
+
nextWriter()
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Wake up all waiting readers
|
|
36
|
+
while (lock.waitingReaders.length > 0) {
|
|
37
|
+
const nextReader = lock.waitingReaders.shift()!
|
|
38
|
+
nextReader()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Clean up empty locks
|
|
42
|
+
if (lock.readers === 0 && !lock.writer && lock.waitingReaders.length === 0 && lock.waitingWriters.length === 0) {
|
|
43
|
+
locks.delete(key)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function read(key: string): Promise<Disposable> {
|
|
48
|
+
const lock = get(key)
|
|
49
|
+
|
|
50
|
+
return new Promise((resolve) => {
|
|
51
|
+
if (!lock.writer && lock.waitingWriters.length === 0) {
|
|
52
|
+
lock.readers++
|
|
53
|
+
resolve({
|
|
54
|
+
[Symbol.dispose]: () => {
|
|
55
|
+
lock.readers--
|
|
56
|
+
process(key)
|
|
57
|
+
},
|
|
58
|
+
})
|
|
59
|
+
} else {
|
|
60
|
+
lock.waitingReaders.push(() => {
|
|
61
|
+
lock.readers++
|
|
62
|
+
resolve({
|
|
63
|
+
[Symbol.dispose]: () => {
|
|
64
|
+
lock.readers--
|
|
65
|
+
process(key)
|
|
66
|
+
},
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function write(key: string): Promise<Disposable> {
|
|
74
|
+
const lock = get(key)
|
|
75
|
+
|
|
76
|
+
return new Promise((resolve) => {
|
|
77
|
+
if (!lock.writer && lock.readers === 0) {
|
|
78
|
+
lock.writer = true
|
|
79
|
+
resolve({
|
|
80
|
+
[Symbol.dispose]: () => {
|
|
81
|
+
lock.writer = false
|
|
82
|
+
process(key)
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
} else {
|
|
86
|
+
lock.waitingWriters.push(() => {
|
|
87
|
+
lock.writer = true
|
|
88
|
+
resolve({
|
|
89
|
+
[Symbol.dispose]: () => {
|
|
90
|
+
lock.writer = false
|
|
91
|
+
process(key)
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
}
|
package/src/util/log.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import path from "path"
|
|
2
|
+
import fs from "fs/promises"
|
|
3
|
+
import { Global } from "../global"
|
|
4
|
+
import z from "zod"
|
|
5
|
+
|
|
6
|
+
export namespace Log {
|
|
7
|
+
export const Level = z.enum(["DEBUG", "INFO", "WARN", "ERROR"]).meta({ ref: "LogLevel", description: "Log level" })
|
|
8
|
+
export type Level = z.infer<typeof Level>
|
|
9
|
+
|
|
10
|
+
const levelPriority: Record<Level, number> = {
|
|
11
|
+
DEBUG: 0,
|
|
12
|
+
INFO: 1,
|
|
13
|
+
WARN: 2,
|
|
14
|
+
ERROR: 3,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let level: Level = "INFO"
|
|
18
|
+
|
|
19
|
+
function shouldLog(input: Level): boolean {
|
|
20
|
+
return levelPriority[input] >= levelPriority[level]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type Logger = {
|
|
24
|
+
debug(message?: any, extra?: Record<string, any>): void
|
|
25
|
+
info(message?: any, extra?: Record<string, any>): void
|
|
26
|
+
error(message?: any, extra?: Record<string, any>): void
|
|
27
|
+
warn(message?: any, extra?: Record<string, any>): void
|
|
28
|
+
tag(key: string, value: string): Logger
|
|
29
|
+
clone(): Logger
|
|
30
|
+
time(
|
|
31
|
+
message: string,
|
|
32
|
+
extra?: Record<string, any>,
|
|
33
|
+
): {
|
|
34
|
+
stop(): void
|
|
35
|
+
[Symbol.dispose](): void
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const loggers = new Map<string, Logger>()
|
|
40
|
+
|
|
41
|
+
export const Default = create({ service: "default" })
|
|
42
|
+
|
|
43
|
+
export interface Options {
|
|
44
|
+
print: boolean
|
|
45
|
+
dev?: boolean
|
|
46
|
+
level?: Level
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let logpath = ""
|
|
50
|
+
export function file() {
|
|
51
|
+
return logpath
|
|
52
|
+
}
|
|
53
|
+
let write = (msg: any) => Bun.stderr.write(msg)
|
|
54
|
+
|
|
55
|
+
export async function init(options: Options) {
|
|
56
|
+
if (options.level) level = options.level
|
|
57
|
+
cleanup(Global.Path.log)
|
|
58
|
+
if (options.print) return
|
|
59
|
+
logpath = path.join(
|
|
60
|
+
Global.Path.log,
|
|
61
|
+
options.dev ? "dev.log" : new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log",
|
|
62
|
+
)
|
|
63
|
+
const logfile = Bun.file(logpath)
|
|
64
|
+
await fs.truncate(logpath).catch(() => {})
|
|
65
|
+
const writer = logfile.writer()
|
|
66
|
+
write = async (msg: any) => {
|
|
67
|
+
const num = writer.write(msg)
|
|
68
|
+
writer.flush()
|
|
69
|
+
return num
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function cleanup(dir: string) {
|
|
74
|
+
const glob = new Bun.Glob("????-??-??T??????.log")
|
|
75
|
+
const files = await Array.fromAsync(
|
|
76
|
+
glob.scan({
|
|
77
|
+
cwd: dir,
|
|
78
|
+
absolute: true,
|
|
79
|
+
}),
|
|
80
|
+
)
|
|
81
|
+
if (files.length <= 5) return
|
|
82
|
+
|
|
83
|
+
const filesToDelete = files.slice(0, -10)
|
|
84
|
+
await Promise.all(filesToDelete.map((file) => fs.unlink(file).catch(() => {})))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function formatError(error: Error, depth = 0): string {
|
|
88
|
+
const result = error.message
|
|
89
|
+
return error.cause instanceof Error && depth < 10
|
|
90
|
+
? result + " Caused by: " + formatError(error.cause, depth + 1)
|
|
91
|
+
: result
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let last = Date.now()
|
|
95
|
+
export function create(tags?: Record<string, any>) {
|
|
96
|
+
tags = tags || {}
|
|
97
|
+
|
|
98
|
+
const service = tags["service"]
|
|
99
|
+
if (service && typeof service === "string") {
|
|
100
|
+
const cached = loggers.get(service)
|
|
101
|
+
if (cached) {
|
|
102
|
+
return cached
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function build(message: any, extra?: Record<string, any>) {
|
|
107
|
+
const prefix = Object.entries({
|
|
108
|
+
...tags,
|
|
109
|
+
...extra,
|
|
110
|
+
})
|
|
111
|
+
.filter(([_, value]) => value !== undefined && value !== null)
|
|
112
|
+
.map(([key, value]) => {
|
|
113
|
+
const prefix = `${key}=`
|
|
114
|
+
if (value instanceof Error) return prefix + formatError(value)
|
|
115
|
+
if (typeof value === "object") return prefix + JSON.stringify(value)
|
|
116
|
+
return prefix + value
|
|
117
|
+
})
|
|
118
|
+
.join(" ")
|
|
119
|
+
const next = new Date()
|
|
120
|
+
const diff = next.getTime() - last
|
|
121
|
+
last = next.getTime()
|
|
122
|
+
return [next.toISOString().split(".")[0], "+" + diff + "ms", prefix, message].filter(Boolean).join(" ") + "\n"
|
|
123
|
+
}
|
|
124
|
+
const result: Logger = {
|
|
125
|
+
debug(message?: any, extra?: Record<string, any>) {
|
|
126
|
+
if (shouldLog("DEBUG")) {
|
|
127
|
+
write("DEBUG " + build(message, extra))
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
info(message?: any, extra?: Record<string, any>) {
|
|
131
|
+
if (shouldLog("INFO")) {
|
|
132
|
+
write("INFO " + build(message, extra))
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
error(message?: any, extra?: Record<string, any>) {
|
|
136
|
+
if (shouldLog("ERROR")) {
|
|
137
|
+
write("ERROR " + build(message, extra))
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
warn(message?: any, extra?: Record<string, any>) {
|
|
141
|
+
if (shouldLog("WARN")) {
|
|
142
|
+
write("WARN " + build(message, extra))
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
tag(key: string, value: string) {
|
|
146
|
+
if (tags) tags[key] = value
|
|
147
|
+
return result
|
|
148
|
+
},
|
|
149
|
+
clone() {
|
|
150
|
+
return Log.create({ ...tags })
|
|
151
|
+
},
|
|
152
|
+
time(message: string, extra?: Record<string, any>) {
|
|
153
|
+
const now = Date.now()
|
|
154
|
+
result.info(message, { status: "started", ...extra })
|
|
155
|
+
function stop() {
|
|
156
|
+
result.info(message, {
|
|
157
|
+
status: "completed",
|
|
158
|
+
duration: Date.now() - now,
|
|
159
|
+
...extra,
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
stop,
|
|
164
|
+
[Symbol.dispose]() {
|
|
165
|
+
stop()
|
|
166
|
+
},
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (service && typeof service === "string") {
|
|
172
|
+
loggers.set(service, result)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return result
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export class AsyncQueue<T> implements AsyncIterable<T> {
|
|
2
|
+
private queue: T[] = []
|
|
3
|
+
private resolvers: ((value: T) => void)[] = []
|
|
4
|
+
|
|
5
|
+
push(item: T) {
|
|
6
|
+
const resolve = this.resolvers.shift()
|
|
7
|
+
if (resolve) resolve(item)
|
|
8
|
+
else this.queue.push(item)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async next(): Promise<T> {
|
|
12
|
+
if (this.queue.length > 0) return this.queue.shift()!
|
|
13
|
+
return new Promise((resolve) => this.resolvers.push(resolve))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async *[Symbol.asyncIterator]() {
|
|
17
|
+
while (true) yield await this.next()
|
|
18
|
+
}
|
|
19
|
+
}
|
package/src/util/rpc.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export namespace Rpc {
|
|
2
|
+
type Definition = {
|
|
3
|
+
[method: string]: (input: any) => any
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function listen(rpc: Definition) {
|
|
7
|
+
onmessage = async (evt) => {
|
|
8
|
+
const parsed = JSON.parse(evt.data)
|
|
9
|
+
if (parsed.type === "rpc.request") {
|
|
10
|
+
const result = await rpc[parsed.method](parsed.input)
|
|
11
|
+
postMessage(JSON.stringify({ type: "rpc.result", result, id: parsed.id }))
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function client<T extends Definition>(target: {
|
|
17
|
+
postMessage: (data: string) => void | null
|
|
18
|
+
onmessage: ((this: Worker, ev: MessageEvent<any>) => any) | null
|
|
19
|
+
}) {
|
|
20
|
+
const pending = new Map<number, (result: any) => void>()
|
|
21
|
+
let id = 0
|
|
22
|
+
target.onmessage = async (evt) => {
|
|
23
|
+
const parsed = JSON.parse(evt.data)
|
|
24
|
+
if (parsed.type === "rpc.result") {
|
|
25
|
+
const resolve = pending.get(parsed.id)
|
|
26
|
+
if (resolve) {
|
|
27
|
+
resolve(parsed.result)
|
|
28
|
+
pending.delete(parsed.id)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
call<Method extends keyof T>(method: Method, input: Parameters<T[Method]>[0]): Promise<ReturnType<T[Method]>> {
|
|
34
|
+
const requestId = id++
|
|
35
|
+
return new Promise((resolve) => {
|
|
36
|
+
pending.set(requestId, resolve)
|
|
37
|
+
target.postMessage(JSON.stringify({ type: "rpc.request", method, input, id: requestId }))
|
|
38
|
+
})
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
|
|
2
|
+
let timeout: NodeJS.Timeout
|
|
3
|
+
return Promise.race([
|
|
4
|
+
promise.then((result) => {
|
|
5
|
+
clearTimeout(timeout)
|
|
6
|
+
return result
|
|
7
|
+
}),
|
|
8
|
+
new Promise<never>((_, reject) => {
|
|
9
|
+
timeout = setTimeout(() => {
|
|
10
|
+
reject(new Error(`Operation timed out after ${ms}ms`))
|
|
11
|
+
}, ms)
|
|
12
|
+
}),
|
|
13
|
+
])
|
|
14
|
+
}
|