mirab 0.0.0-stage → 0.2.0

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 (105) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +226 -2
  3. package/package.json +58 -4
  4. package/src/agent/approved.ts +261 -0
  5. package/src/agent/define.ts +235 -0
  6. package/src/agent/guest.ts +138 -0
  7. package/src/agent/init.ts +150 -0
  8. package/src/agent/manifest.ts +141 -0
  9. package/src/agent/modules.ts +23 -0
  10. package/src/agent/runtime.ts +239 -0
  11. package/src/agent-profiles.ts +327 -0
  12. package/src/api/bootstrap-contract.ts +95 -0
  13. package/src/api/channel-contract.ts +19 -0
  14. package/src/api/client.ts +100 -0
  15. package/src/api/contract.ts +2454 -0
  16. package/src/api/events-contract.ts +103 -0
  17. package/src/api/import-contract.ts +22 -0
  18. package/src/api/orchestrator-contract.ts +73 -0
  19. package/src/api/runner-contract.ts +25 -0
  20. package/src/api/scopes.ts +41 -0
  21. package/src/api/sessions-contract.ts +71 -0
  22. package/src/api/settings-contract.ts +50 -0
  23. package/src/channel/bridge.ts +97 -0
  24. package/src/channel/local.ts +154 -0
  25. package/src/channel/state.ts +11 -0
  26. package/src/channel/wire.ts +104 -0
  27. package/src/dev-sync.ts +206 -0
  28. package/src/dev.ts +453 -0
  29. package/src/events-tail.ts +75 -0
  30. package/src/images.ts +498 -0
  31. package/src/index.ts +21 -0
  32. package/src/invocation.ts +34 -0
  33. package/src/isolation/anchored-fs.ts +284 -0
  34. package/src/isolation/colima.ts +159 -0
  35. package/src/isolation/container.ts +189 -0
  36. package/src/isolation/firecracker.ts +414 -0
  37. package/src/isolation/guest-protocol.json +1 -0
  38. package/src/isolation/host-user.ts +247 -0
  39. package/src/isolation/image.ts +336 -0
  40. package/src/isolation/index.ts +477 -0
  41. package/src/isolation/multipass.ts +541 -0
  42. package/src/isolation/network.ts +167 -0
  43. package/src/isolation/policy.ts +86 -0
  44. package/src/isolation/session.ts +385 -0
  45. package/src/isolation/sync.ts +329 -0
  46. package/src/isolation/vz-network-setup.ts +247 -0
  47. package/src/isolation/vz.ts +312 -0
  48. package/src/mirab-main.ts +536 -0
  49. package/src/mirab.ts +13 -0
  50. package/src/runner/define.ts +187 -0
  51. package/src/runner/image.ts +239 -0
  52. package/src/runner/index.ts +37 -0
  53. package/src/runner/serve.ts +491 -0
  54. package/src/runner/toolsets.ts +205 -0
  55. package/src/spawn-adapters/claude.ts +112 -0
  56. package/src/spawn-adapters/codex-logs.ts +130 -0
  57. package/src/spawn-adapters/codex-notify.ts +73 -0
  58. package/src/spawn-adapters/codex.ts +89 -0
  59. package/src/spawn-adapters/index.ts +40 -0
  60. package/src/spawn-adapters/types.ts +55 -0
  61. package/src/spawn-attach.ts +243 -0
  62. package/src/spawn-binary.ts +90 -0
  63. package/src/spawn-bridge.ts +504 -0
  64. package/src/spawn-bundle.ts +16 -0
  65. package/src/spawn-detach.ts +301 -0
  66. package/src/spawn-events.ts +35 -0
  67. package/src/spawn-hook.ts +86 -0
  68. package/src/spawn-input.ts +107 -0
  69. package/src/spawn-native.ts +49 -0
  70. package/src/spawn-nudge-worker.ts +61 -0
  71. package/src/spawn-nudge.ts +446 -0
  72. package/src/spawn-output.ts +45 -0
  73. package/src/spawn-queue.ts +135 -0
  74. package/src/spawn-quiet.ts +69 -0
  75. package/src/spawn-report.ts +114 -0
  76. package/src/spawn-sessions.ts +166 -0
  77. package/src/spawn-vm.ts +356 -0
  78. package/src/spawn-worker.ts +766 -0
  79. package/src/spawn.ts +468 -0
  80. package/src/station/config.ts +89 -0
  81. package/src/station/gateway.ts +94 -0
  82. package/src/station/vault.ts +96 -0
  83. package/src/test-cli.ts +90 -0
  84. package/src/tools.ts +3 -0
  85. package/types/agent/define.d.ts +76 -0
  86. package/types/agent/manifest.d.ts +53 -0
  87. package/types/agent/modules.d.ts +1 -0
  88. package/types/images.d.ts +124 -0
  89. package/types/index.d.ts +6 -0
  90. package/types/isolation/colima.d.ts +37 -0
  91. package/types/isolation/container.d.ts +25 -0
  92. package/types/isolation/firecracker.d.ts +55 -0
  93. package/types/isolation/host-user.d.ts +47 -0
  94. package/types/isolation/image.d.ts +158 -0
  95. package/types/isolation/index.d.ts +128 -0
  96. package/types/isolation/network.d.ts +33 -0
  97. package/types/isolation/policy.d.ts +29 -0
  98. package/types/isolation/session.d.ts +99 -0
  99. package/types/isolation/vz.d.ts +63 -0
  100. package/types/runner/define.d.ts +101 -0
  101. package/types/runner/image.d.ts +52 -0
  102. package/types/runner/index.d.ts +13 -0
  103. package/types/runner/serve.d.ts +69 -0
  104. package/types/runner/toolsets.d.ts +35 -0
  105. package/types/tools.d.ts +2 -0
@@ -0,0 +1,112 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises'
2
+ import { rmSync } from 'node:fs'
3
+ import { join } from 'node:path'
4
+ import {
5
+ event,
6
+ payload,
7
+ shellQuote,
8
+ nativeOptions,
9
+ type AgentAdapter,
10
+ } from './types'
11
+ // A new session sits at its prompt, so it starts idle. Claude's idle
12
+ // notification ("waiting for your input") is also ready for a prompt; every
13
+ // other notification (permission, elicitation) still needs the human.
14
+ const types = {
15
+ SessionStart: 'idle',
16
+ UserPromptSubmit: 'busy',
17
+ PreToolUse: 'tool_start',
18
+ PostToolUse: 'tool_end',
19
+ PostToolUseFailure: 'tool_end',
20
+ Notification: 'waiting',
21
+ PermissionRequest: 'waiting',
22
+ Stop: 'idle',
23
+ } as const
24
+ export const claudeAdapter: AgentAdapter = {
25
+ name: 'claude',
26
+ profileArgs({ model, effort }) {
27
+ if (effort && !['low', 'medium', 'high', 'xhigh', 'max'].includes(effort))
28
+ throw Error('Claude effort must be low, medium, high, xhigh or max')
29
+ return [
30
+ ...(model ? ['--model', model] : []),
31
+ ...(effort ? ['--effort', effort] : []),
32
+ ]
33
+ },
34
+ async prepare(context, args) {
35
+ // Modes explicitly disabling hooks must retain their native semantics.
36
+ if (nativeOptions(args).some((a) => ['--bare', '--safe-mode'].includes(a)))
37
+ return { args, cleanup() {} }
38
+ const directory = join(context.directory, context.id)
39
+ await mkdir(directory, { mode: 0o700 })
40
+ const file = join(directory, 'settings.json')
41
+ try {
42
+ context.onCleanup?.(() =>
43
+ rmSync(directory, { recursive: true, force: true }),
44
+ )
45
+ const command = [...context.command, 'hook', '--session', context.id]
46
+ .map(shellQuote)
47
+ .join(' ')
48
+ const hooks = Object.fromEntries(
49
+ Object.keys(types).map((name) => [
50
+ name,
51
+ [{ hooks: [{ type: 'command', command, timeout: 1 }] }],
52
+ ]),
53
+ )
54
+ await writeFile(file, JSON.stringify({ hooks }), {
55
+ mode: 0o600,
56
+ flag: 'wx',
57
+ })
58
+ // Installed Claude accepts only the last --settings. If the caller has
59
+ // one, additive plugin hooks preserve their entire settings source.
60
+ const extra: string[] = []
61
+ if (
62
+ nativeOptions(args).some(
63
+ (a) => a === '--settings' || a.startsWith('--settings='),
64
+ )
65
+ ) {
66
+ await mkdir(join(directory, '.claude-plugin'), { mode: 0o700 })
67
+ await mkdir(join(directory, 'hooks'), { mode: 0o700 })
68
+ await writeFile(
69
+ join(directory, '.claude-plugin', 'plugin.json'),
70
+ JSON.stringify({ name: 'mirab-session-observer', version: '1.0.0' }),
71
+ { mode: 0o600 },
72
+ )
73
+ await writeFile(
74
+ join(directory, 'hooks', 'hooks.json'),
75
+ JSON.stringify({ hooks }),
76
+ { mode: 0o600 },
77
+ )
78
+ extra.push('--plugin-dir', directory)
79
+ }
80
+ return {
81
+ args: ['--settings', file, ...extra, ...args],
82
+ cleanup: () => rmSync(directory, { recursive: true, force: true }),
83
+ }
84
+ } catch (error) {
85
+ rmSync(directory, { recursive: true, force: true })
86
+ throw error
87
+ }
88
+ },
89
+ translate(session, raw) {
90
+ const p = payload(raw)
91
+ if (
92
+ !p ||
93
+ typeof p.hook_event_name !== 'string' ||
94
+ typeof p.agent_id === 'string'
95
+ )
96
+ return null
97
+ const type =
98
+ p.hook_event_name === 'Notification' &&
99
+ p.notification_type === 'idle_prompt'
100
+ ? 'idle'
101
+ : types[p.hook_event_name as keyof typeof types]
102
+ return type
103
+ ? event(
104
+ session,
105
+ 'claude',
106
+ type,
107
+ raw,
108
+ typeof p.tool_name === 'string' ? p.tool_name : undefined,
109
+ )
110
+ : null
111
+ },
112
+ }
@@ -0,0 +1,130 @@
1
+ import { open, readdir, stat } from 'node:fs/promises'
2
+ import { basename, join } from 'node:path'
3
+ import type { AgentEvent } from '../spawn-events'
4
+ import { event, payload } from './types'
5
+ /** Best effort, bound only by a notify-proven thread ID; never guess by cwd. */
6
+ export class CodexLogs {
7
+ private path?: string
8
+ private offset = 0
9
+ private partial = ''
10
+ private tools = new Map<string, string>()
11
+ private timer?: ReturnType<typeof setInterval>
12
+ private reading = false
13
+ private closed = false
14
+ private bound = false
15
+ private notBefore = 0
16
+ constructor(
17
+ private readonly root: string,
18
+ private readonly session: string,
19
+ private readonly emit: (event: AgentEvent) => void,
20
+ ) {}
21
+ async bind(raw: unknown) {
22
+ this.notBefore = Date.now()
23
+ this.tools.clear()
24
+ const p = payload(raw),
25
+ id = p?.['thread-id']
26
+ if (
27
+ this.bound ||
28
+ typeof id !== 'string' ||
29
+ !/^[a-zA-Z0-9_-]{1,100}$/.test(id)
30
+ )
31
+ return
32
+ this.bound = true
33
+ try {
34
+ const entries = await readdir(this.root, { recursive: true })
35
+ const match = entries.find(
36
+ (path) =>
37
+ basename(path) === id + '.jsonl' ||
38
+ basename(path).endsWith('-' + id + '.jsonl'),
39
+ )
40
+ if (!match || this.closed) return
41
+ this.path = join(this.root, match)
42
+ // Old history cannot change the live state after a notify event.
43
+ this.offset = (await stat(this.path)).size
44
+ if (!this.closed)
45
+ this.timer = setInterval(() => {
46
+ void this.poll()
47
+ }, 250)
48
+ } catch {
49
+ /* Missing/paginated logs do not disable notify. */
50
+ }
51
+ }
52
+ async poll() {
53
+ if (this.reading || this.closed || !this.path) return
54
+ this.reading = true
55
+ let file: Awaited<ReturnType<typeof open>> | undefined
56
+ try {
57
+ file = await open(this.path, 'r')
58
+ const size = (await file.stat()).size
59
+ if (size < this.offset) {
60
+ this.offset = size
61
+ this.partial = ''
62
+ return
63
+ }
64
+ if (size === this.offset) return
65
+ if (size - this.offset > 1024 * 1024) {
66
+ this.offset = size
67
+ this.partial = ''
68
+ return
69
+ }
70
+ const buffer = Buffer.alloc(size - this.offset)
71
+ const { bytesRead } = await file.read(
72
+ buffer,
73
+ 0,
74
+ buffer.length,
75
+ this.offset,
76
+ )
77
+ this.offset += bytesRead
78
+ const lines = (
79
+ this.partial + buffer.subarray(0, bytesRead).toString('utf8')
80
+ ).split('\n')
81
+ this.partial = lines.pop() ?? ''
82
+ if (this.partial.length > 65536) this.partial = ''
83
+ for (const line of lines) {
84
+ if (line.length > 65536 || this.closed) continue
85
+ try {
86
+ const raw = JSON.parse(line),
87
+ p = payload(raw.payload)
88
+ const timestamp =
89
+ typeof raw.timestamp === 'string' ? Date.parse(raw.timestamp) : NaN
90
+ if (!p || !Number.isFinite(timestamp) || timestamp <= this.notBefore)
91
+ continue
92
+ if (raw.type === 'event_msg' && p.type === 'task_started')
93
+ this.emit(event(this.session, 'codex', 'busy', raw))
94
+ if (raw.type !== 'response_item') continue
95
+ if (
96
+ ['function_call', 'custom_tool_call'].includes(String(p.type)) &&
97
+ typeof p.name === 'string' &&
98
+ typeof p.call_id === 'string'
99
+ ) {
100
+ if (this.tools.size >= 1000) this.tools.clear()
101
+ this.tools.set(p.call_id, p.name)
102
+ this.emit(event(this.session, 'codex', 'tool_start', raw, p.name))
103
+ } else if (
104
+ ['function_call_output', 'custom_tool_call_output'].includes(
105
+ String(p.type),
106
+ ) &&
107
+ typeof p.call_id === 'string'
108
+ ) {
109
+ const tool = this.tools.get(p.call_id)
110
+ if (tool) {
111
+ this.tools.delete(p.call_id)
112
+ this.emit(event(this.session, 'codex', 'tool_end', raw, tool))
113
+ }
114
+ }
115
+ // Completion comes only from notify; delayed log records cannot unlock injection.
116
+ } catch {
117
+ /* Internal JSONL shapes are not a protocol. */
118
+ }
119
+ }
120
+ } catch {
121
+ } finally {
122
+ await file?.close().catch(() => {})
123
+ this.reading = false
124
+ }
125
+ }
126
+ close() {
127
+ this.closed = true
128
+ clearInterval(this.timer)
129
+ }
130
+ }
@@ -0,0 +1,73 @@
1
+ import { readFile } from 'node:fs/promises'
2
+ import { homedir } from 'node:os'
3
+ import { join } from 'node:path'
4
+ import { parse } from 'smol-toml'
5
+
6
+ function notifyCommand(value: unknown): string[] {
7
+ if (value === undefined) return []
8
+ if (!Array.isArray(value) || !value.every((arg) => typeof arg === 'string'))
9
+ throw new Error('Codex notify must be an array of strings')
10
+ return value
11
+ }
12
+ async function config(path: string) {
13
+ try {
14
+ return parse(await readFile(path, 'utf8'))
15
+ } catch (error) {
16
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
17
+ return {}
18
+ }
19
+ }
20
+ /** Resolve notification layers only; project config cannot set notify. */
21
+ export async function originalNotify(
22
+ args: string[],
23
+ environment = process.env,
24
+ ) {
25
+ const home =
26
+ environment.CODEX_HOME ?? join(environment.HOME ?? homedir(), '.codex')
27
+ const system = await config('/etc/codex/config.toml')
28
+ const user = await config(join(home, 'config.toml'))
29
+ let command = notifyCommand(user.notify ?? system.notify)
30
+ let profile = user.profile ?? system.profile
31
+ let explicitProfile: string | undefined
32
+ let override: string[] | undefined
33
+ const remaining: string[] = []
34
+ for (let i = 0; i < args.length; i++) {
35
+ const arg = args[i]
36
+ if (arg === '--') {
37
+ remaining.push(...args.slice(i))
38
+ break
39
+ }
40
+ if (arg === '-p' || arg === '--profile') {
41
+ explicitProfile = args[i + 1]
42
+ remaining.push(arg, args[++i])
43
+ continue
44
+ }
45
+ if (arg.startsWith('--profile=')) explicitProfile = arg.slice(10)
46
+ else if (arg.startsWith('-p') && arg.length > 2)
47
+ explicitProfile = arg.slice(2)
48
+ const separate = arg === '-c' || arg === '--config'
49
+ const value = separate
50
+ ? args[i + 1]
51
+ : arg.startsWith('--config=')
52
+ ? arg.slice(9)
53
+ : arg.startsWith('-c')
54
+ ? arg.slice(2)
55
+ : undefined
56
+ if (value && /^\s*notify\s*=/.test(value)) {
57
+ override = notifyCommand(parse(value).notify)
58
+ if (separate) i++
59
+ } else {
60
+ if (value && /^\s*profile\s*=/.test(value)) profile = parse(value).profile
61
+ remaining.push(arg)
62
+ }
63
+ }
64
+ profile = explicitProfile ?? profile
65
+ if (profile !== undefined) {
66
+ if (typeof profile !== 'string' || !/^[a-zA-Z0-9_-]+$/.test(profile))
67
+ throw new Error('Invalid Codex profile name')
68
+ const selected = await config(join(home, `${profile}.config.toml`))
69
+ if (selected.notify !== undefined) command = notifyCommand(selected.notify)
70
+ }
71
+ if (override !== undefined) command = override
72
+ return { command, args: remaining }
73
+ }
@@ -0,0 +1,89 @@
1
+ import { homedir } from 'node:os'
2
+ import { join } from 'node:path'
3
+ import { originalNotify } from './codex-notify'
4
+ import { CodexLogs } from './codex-logs'
5
+ import { event, payload, type AgentAdapter } from './types'
6
+ /** Codex settings every mirab session needs, because mirab types its prompts.
7
+ * - The startup "Update available" menu would swallow the first prompt (its
8
+ * Enter picks "Update now").
9
+ * - Paste-burst detection treats typed prompt text as a paste and the Enter
10
+ * that follows it as part of the paste, so the prompt sat in the composer
11
+ * unsent: 0 of 3 fresh sessions answered on 10 Oct, 3 of 3 with it off.
12
+ * Profile args come later on the command line, so a profile can still turn
13
+ * either back on. */
14
+ export const SESSION_CONFIG = [
15
+ '-c',
16
+ 'check_for_update_on_startup=false',
17
+ '-c',
18
+ 'tui.disable_paste_burst=true',
19
+ ]
20
+ export const codexAdapter: AgentAdapter = {
21
+ name: 'codex',
22
+ profileArgs({ model, effort }) {
23
+ return [
24
+ ...(model ? ['-m', model] : []),
25
+ ...(effort
26
+ ? ['-c', `model_reasoning_effort=${JSON.stringify(effort)}`]
27
+ : []),
28
+ ]
29
+ },
30
+ async prepare(context, args) {
31
+ const environment = context.environment ?? process.env
32
+ const original = await originalNotify(args, environment)
33
+ const command = [
34
+ ...context.command,
35
+ 'hook',
36
+ '--session',
37
+ context.id,
38
+ '--agent',
39
+ 'codex',
40
+ ...(original.command.length
41
+ ? ['--notify-command', JSON.stringify(original.command)]
42
+ : []),
43
+ ]
44
+ const logs = context.emit
45
+ ? new CodexLogs(
46
+ join(
47
+ environment.CODEX_HOME ??
48
+ join(environment.HOME ?? homedir(), '.codex'),
49
+ 'sessions',
50
+ ),
51
+ context.id,
52
+ context.emit,
53
+ )
54
+ : undefined
55
+ context.onCleanup?.(() => logs?.close())
56
+ // JSON array syntax is also a TOML array of quoted strings.
57
+ return {
58
+ args: [
59
+ '-c',
60
+ `notify=${JSON.stringify(command)}`,
61
+ ...SESSION_CONFIG,
62
+ ...original.args,
63
+ ],
64
+ cleanup: () => logs?.close(),
65
+ receive: (raw) => {
66
+ void logs?.bind(raw)
67
+ },
68
+ }
69
+ },
70
+ // Codex's notify fires only after a turn, and its SessionStart hook only on
71
+ // the first prompt, so nothing says a fresh TUI is ready; without this a
72
+ // mention to a new Codex session waits forever as "agent not idle". Codex
73
+ // draws its composer first and its trust dialog up to ~3 s later (10 Oct),
74
+ // so a fresh screen must stay quiet for 5 s. A busy Codex redraws its
75
+ // "Working (Ns)" timer every second, so 5 s of silence after an Enter means
76
+ // no turn is running. Its startup dialogs (trust, update) are for a person,
77
+ // never for a channel message.
78
+ quiet: {
79
+ startupMs: 5000,
80
+ busyMs: 5000,
81
+ dialogs: /Trustthisfolder|Updateavailable|Updatenow/i,
82
+ },
83
+ translate(session, raw) {
84
+ const p = payload(raw)
85
+ return p?.type === 'agent-turn-complete'
86
+ ? event(session, 'codex', 'idle', raw)
87
+ : null
88
+ },
89
+ }
@@ -0,0 +1,40 @@
1
+ import { basename } from 'node:path'
2
+ import { claudeAdapter } from './claude'
3
+ import { codexAdapter } from './codex'
4
+ import {
5
+ nativeOptions,
6
+ type AgentAdapter,
7
+ type RuntimeSettings,
8
+ type RuntimeFlags,
9
+ } from './types'
10
+ const adapters: AgentAdapter[] = [claudeAdapter, codexAdapter]
11
+ export function adapterFor(
12
+ agent: string,
13
+ args: string[],
14
+ ): AgentAdapter | undefined {
15
+ if (
16
+ basename(agent) === 'claude' &&
17
+ nativeOptions(args).some((a) => ['--bare', '--safe-mode'].includes(a))
18
+ )
19
+ return undefined
20
+ return adapters.find((adapter) => adapter.name === basename(agent))
21
+ }
22
+
23
+ // Kimi has no structured event observer yet. Its verified flags share the
24
+ // runtime registry; absent effort support is an error, never a guessed flag.
25
+ const runtimeFlags = new Map<string, RuntimeFlags>(
26
+ adapters.map((a) => [a.name, a.profileArgs]),
27
+ )
28
+ runtimeFlags.set('kimi', ({ model, effort }) => {
29
+ if (effort)
30
+ throw Error(
31
+ 'Kimi --help exposes no effort flag; omit effort and use native args',
32
+ )
33
+ return model ? ['--model', model] : []
34
+ })
35
+ export function profileArguments(
36
+ runtime: string,
37
+ settings: RuntimeSettings,
38
+ ): string[] {
39
+ return runtimeFlags.get(basename(runtime))?.(settings) ?? []
40
+ }
@@ -0,0 +1,55 @@
1
+ import type { AgentEvent } from '../spawn-events'
2
+ export type AdapterContext = {
3
+ id: string
4
+ directory: string
5
+ command: string[]
6
+ environment?: NodeJS.ProcessEnv
7
+ emit?: (event: AgentEvent) => void
8
+ onCleanup?: (cleanup: () => void) => void
9
+ }
10
+ export type RuntimeSettings = { model?: string; effort?: string }
11
+ export type RuntimeFlags = (settings: RuntimeSettings) => string[]
12
+ export type AgentAdapter = {
13
+ profileArgs: RuntimeFlags
14
+ name: string
15
+ prepare(
16
+ context: AdapterContext,
17
+ args: string[],
18
+ ): Promise<{
19
+ args: string[]
20
+ cleanup(): void
21
+ receive?: (raw: unknown) => void
22
+ }>
23
+ translate(session: string, raw: unknown): AgentEvent | null
24
+ /** For a runtime that reports only turn ends: terminal-quiet readiness
25
+ * (see QuietIdle). `dialogs` matches screen text with whitespace removed. */
26
+ quiet?: { startupMs: number; busyMs: number; dialogs?: RegExp }
27
+ }
28
+ export function event(
29
+ session: string,
30
+ agent: string,
31
+ type: AgentEvent['type'],
32
+ raw: unknown,
33
+ tool?: string,
34
+ ): AgentEvent {
35
+ return {
36
+ session,
37
+ agent,
38
+ type,
39
+ ...(tool ? { tool } : {}),
40
+ ts: new Date().toISOString(),
41
+ raw,
42
+ }
43
+ }
44
+ export function payload(raw: unknown): Record<string, unknown> | null {
45
+ return raw && typeof raw === 'object' && !Array.isArray(raw)
46
+ ? (raw as Record<string, unknown>)
47
+ : null
48
+ }
49
+ export const shellQuote = (value: string) =>
50
+ "'" + value.replaceAll("'", "'\\''") + "'"
51
+
52
+ export function nativeOptions(args: string[]) {
53
+ const separator = args.indexOf('--')
54
+ return separator < 0 ? args : args.slice(0, separator)
55
+ }