@glbgpt/cli 0.1.4 → 0.2.1

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 (117) hide show
  1. package/README.md +82 -40
  2. package/dist/src/coding/backgroundTasks.d.ts +34 -0
  3. package/dist/src/coding/backgroundTasks.js +154 -0
  4. package/dist/src/coding/backgroundTasks.js.map +1 -0
  5. package/dist/src/coding/backgroundTools.d.ts +9 -0
  6. package/dist/src/coding/backgroundTools.js +63 -0
  7. package/dist/src/coding/backgroundTools.js.map +1 -0
  8. package/dist/src/coding/bashTool.d.ts +27 -0
  9. package/dist/src/coding/bashTool.js +144 -0
  10. package/dist/src/coding/bashTool.js.map +1 -0
  11. package/dist/src/coding/dangerousCommands.d.ts +29 -0
  12. package/dist/src/coding/dangerousCommands.js +156 -0
  13. package/dist/src/coding/dangerousCommands.js.map +1 -0
  14. package/dist/src/coding/editDiff.d.ts +45 -0
  15. package/dist/src/coding/editDiff.js +156 -0
  16. package/dist/src/coding/editDiff.js.map +1 -0
  17. package/dist/src/coding/editTool.d.ts +14 -0
  18. package/dist/src/coding/editTool.js +154 -0
  19. package/dist/src/coding/editTool.js.map +1 -0
  20. package/dist/src/coding/index.d.ts +25 -0
  21. package/dist/src/coding/index.js +30 -0
  22. package/dist/src/coding/index.js.map +1 -0
  23. package/dist/src/coding/outputAccumulator.d.ts +34 -0
  24. package/dist/src/coding/outputAccumulator.js +111 -0
  25. package/dist/src/coding/outputAccumulator.js.map +1 -0
  26. package/dist/src/coding/permissionGate.d.ts +65 -0
  27. package/dist/src/coding/permissionGate.js +138 -0
  28. package/dist/src/coding/permissionGate.js.map +1 -0
  29. package/dist/src/coding/permissionStore.d.ts +36 -0
  30. package/dist/src/coding/permissionStore.js +56 -0
  31. package/dist/src/coding/permissionStore.js.map +1 -0
  32. package/dist/src/coding/permissions.d.ts +34 -0
  33. package/dist/src/coding/permissions.js +18 -0
  34. package/dist/src/coding/permissions.js.map +1 -0
  35. package/dist/src/coding/projectContext.d.ts +21 -0
  36. package/dist/src/coding/projectContext.js +118 -0
  37. package/dist/src/coding/projectContext.js.map +1 -0
  38. package/dist/src/coding/readState.d.ts +22 -0
  39. package/dist/src/coding/readState.js +30 -0
  40. package/dist/src/coding/readState.js.map +1 -0
  41. package/dist/src/coding/truncate.d.ts +39 -0
  42. package/dist/src/coding/truncate.js +83 -0
  43. package/dist/src/coding/truncate.js.map +1 -0
  44. package/dist/src/coding/writeTool.d.ts +14 -0
  45. package/dist/src/coding/writeTool.js +82 -0
  46. package/dist/src/coding/writeTool.js.map +1 -0
  47. package/dist/src/commands/execCommand.d.ts +13 -0
  48. package/dist/src/commands/execCommand.js +73 -6
  49. package/dist/src/commands/execCommand.js.map +1 -1
  50. package/dist/src/commands/updateCommand.d.ts +7 -0
  51. package/dist/src/commands/updateCommand.js +11 -0
  52. package/dist/src/commands/updateCommand.js.map +1 -0
  53. package/dist/src/core/catalog.d.ts +5 -0
  54. package/dist/src/core/catalog.js +31 -1
  55. package/dist/src/core/catalog.js.map +1 -1
  56. package/dist/src/core/compaction.d.ts +52 -0
  57. package/dist/src/core/compaction.js +151 -0
  58. package/dist/src/core/compaction.js.map +1 -0
  59. package/dist/src/core/contextPrune.js +8 -1
  60. package/dist/src/core/contextPrune.js.map +1 -1
  61. package/dist/src/core/runChat.d.ts +1 -1
  62. package/dist/src/core/runChat.js +1 -1
  63. package/dist/src/core/runChat.js.map +1 -1
  64. package/dist/src/core/session.d.ts +10 -1
  65. package/dist/src/core/session.js +11 -2
  66. package/dist/src/core/session.js.map +1 -1
  67. package/dist/src/core/turnLoop.d.ts +18 -2
  68. package/dist/src/core/turnLoop.js +79 -12
  69. package/dist/src/core/turnLoop.js.map +1 -1
  70. package/dist/src/core/updateCheck.d.ts +35 -0
  71. package/dist/src/core/updateCheck.js +71 -2
  72. package/dist/src/core/updateCheck.js.map +1 -1
  73. package/dist/src/core/updateRunner.d.ts +7 -0
  74. package/dist/src/core/updateRunner.js +26 -0
  75. package/dist/src/core/updateRunner.js.map +1 -0
  76. package/dist/src/exec/output.d.ts +3 -2
  77. package/dist/src/exec/output.js +17 -4
  78. package/dist/src/exec/output.js.map +1 -1
  79. package/dist/src/fs/index.d.ts +4 -1
  80. package/dist/src/fs/index.js +7 -2
  81. package/dist/src/fs/index.js.map +1 -1
  82. package/dist/src/fs/limits.d.ts +1 -1
  83. package/dist/src/fs/limits.js +3 -1
  84. package/dist/src/fs/limits.js.map +1 -1
  85. package/dist/src/fs/readFileTool.d.ts +2 -1
  86. package/dist/src/fs/readFileTool.js +4 -1
  87. package/dist/src/fs/readFileTool.js.map +1 -1
  88. package/dist/src/fs/workspaceGuard.d.ts +8 -0
  89. package/dist/src/fs/workspaceGuard.js +48 -0
  90. package/dist/src/fs/workspaceGuard.js.map +1 -1
  91. package/dist/src/index.js +44 -3
  92. package/dist/src/index.js.map +1 -1
  93. package/dist/src/mcp/server.js +17 -1
  94. package/dist/src/mcp/server.js.map +1 -1
  95. package/dist/src/tui/App.d.ts +10 -1
  96. package/dist/src/tui/App.js +114 -15
  97. package/dist/src/tui/App.js.map +1 -1
  98. package/dist/src/tui/UpdatePrompt.d.ts +17 -0
  99. package/dist/src/tui/UpdatePrompt.js +42 -0
  100. package/dist/src/tui/UpdatePrompt.js.map +1 -0
  101. package/dist/src/tui/approvalBridge.d.ts +21 -0
  102. package/dist/src/tui/approvalBridge.js +45 -0
  103. package/dist/src/tui/approvalBridge.js.map +1 -0
  104. package/dist/src/tui/chatController.d.ts +21 -0
  105. package/dist/src/tui/chatController.js +155 -16
  106. package/dist/src/tui/chatController.js.map +1 -1
  107. package/dist/src/tui/commands.js +16 -2
  108. package/dist/src/tui/commands.js.map +1 -1
  109. package/dist/src/tui/models.d.ts +4 -0
  110. package/dist/src/tui/models.js +8 -1
  111. package/dist/src/tui/models.js.map +1 -1
  112. package/dist/src/tui/run.d.ts +7 -0
  113. package/dist/src/tui/run.js +107 -22
  114. package/dist/src/tui/run.js.map +1 -1
  115. package/dist/src/web/webFetchTool.js +41 -5
  116. package/dist/src/web/webFetchTool.js.map +1 -1
  117. package/package.json +3 -2
package/README.md CHANGED
@@ -25,14 +25,15 @@ Run `glbgpt <command> --help` for the full flags of any command.
25
25
 
26
26
  ### Chat
27
27
 
28
- | Command | What it does |
29
- |---|---|
30
- | `glbgpt` | Start the interactive chat TUI. |
31
- | `glbgpt "<prompt>"` | Start the TUI with a first message. |
32
- | `glbgpt resume last` · `glbgpt resume <id>` | Resume a previous conversation. |
28
+ | Command | What it does |
29
+ | ------------------------------------------- | ----------------------------------- |
30
+ | `glbgpt` | Start the interactive chat TUI. |
31
+ | `glbgpt "<prompt>"` | Start the TUI with a first message. |
32
+ | `glbgpt resume last` · `glbgpt resume <id>` | Resume a previous conversation. |
33
33
 
34
34
  Inside the TUI, type `/` for commands: `/model` `/models` `/image` `/video` `/tasks` `/status`
35
- `/account` `/transactions` `/cost` `/login` `/logout` `/new` `/resume` `/help` `/exit`.
35
+ `/permissions` `/compact` `/account` `/transactions` `/cost` `/login` `/logout` `/new` `/resume`
36
+ `/help` `/exit`.
36
37
  Attach images with `@image` (last generation), `Ctrl+V` or `@clipboard`, or `@./photo.png`.
37
38
  Multi-line: end a line with `\` then Enter (or Ctrl+J); plain Enter sends; Esc interrupts.
38
39
  On tool-capable models the assistant can also read files under the directory you started
@@ -40,22 +41,49 @@ On tool-capable models the assistant can also read files under the directory you
40
41
  web when current information matters (a small per-search credit charge applies, at most a
41
42
  few searches per message), and fetch public web pages by URL (free).
42
43
 
44
+ #### Coding tools
45
+
46
+ On tool-capable models the assistant can also **run shell commands, create files, and edit
47
+ files** in your workspace — every command and file change asks for your approval first
48
+ (`[y]` allow once, `[a]` allow similar actions for this session, `[p]` always allow for this
49
+ project, `[n]` deny). Diffs are shown before an edit runs. Project instructions in
50
+ `AGENTS.md` or `CLAUDE.md` (from the repository root down to the current directory) are
51
+ picked up automatically (`--no-project-context` to skip). Long-running commands (dev
52
+ servers, watchers) can run in the background — the assistant starts them detached, polls
53
+ their output, and they are stopped when the CLI exits.
54
+
55
+ Long conversations compact automatically: when the history approaches the model's context
56
+ limit the older part is summarized (one model call) and the conversation continues
57
+ seamlessly — `/compact` triggers it manually. The full history always stays on disk.
58
+
59
+ Permission modes — switch with `/permissions <mode>` or start with `--permission-mode <mode>`:
60
+
61
+ | Mode | Behavior |
62
+ | -------------- | ------------------------------------------------------------------------------ |
63
+ | `default` | Ask before every command and file write. |
64
+ | `accept-edits` | File writes run without asking; commands still ask. |
65
+ | `yolo` | Nothing asks (also `--yolo`). Dangerous — use only in disposable environments. |
66
+
67
+ Dangerous commands (`sudo`, forced/recursive `rm`, ...) always ask individually and can never
68
+ be remembered for the session; writes inside `.git/` always ask.
69
+
43
70
  ### Account
44
71
 
45
- | Command | What it does |
46
- |---|---|
72
+ | Command | What it does |
73
+ | -------------------------------------- | --------------------------------------------------------------------------------------- |
47
74
  | `glbgpt login [--no-browser] [--code]` | Sign in. `--no-browser` prints the URL; `--code` pastes a one-time code (headless/SSH). |
48
- | `glbgpt logout` | Sign out and remove stored credentials. |
49
- | `glbgpt whoami` | Show the signed-in account. |
50
- | `glbgpt account` | Membership and credit balance. |
51
- | `glbgpt account transactions` | Recent spend (authoritative billing). |
75
+ | `glbgpt logout` | Sign out and remove stored credentials. |
76
+ | `glbgpt whoami` | Show the signed-in account. |
77
+ | `glbgpt account` | Membership and credit balance. |
78
+ | `glbgpt account transactions` | Recent spend (authoritative billing). |
79
+ | `glbgpt update` | Update the CLI to the latest version (runs `npm i -g` for you). |
52
80
 
53
81
  ### Images and video
54
82
 
55
- | Command | What it does |
56
- |---|---|
83
+ | Command | What it does |
84
+ | ------------------------- | ----------------------------------------- |
57
85
  | `glbgpt image "<prompt>"` | Generate an image (asks before spending). |
58
- | `glbgpt video "<prompt>"` | Generate a video (asks before spending). |
86
+ | `glbgpt video "<prompt>"` | Generate a video (asks before spending). |
59
87
 
60
88
  Common flags: `-m/--model`, `--ar 16:9`, `--res 2K`, `--seed N`, `--negative-prompt "..."`,
61
89
  `-i ./ref.png` (reference image), `--duration N` and `--audio/--no-audio` (video),
@@ -64,14 +92,14 @@ task id without waiting).
64
92
 
65
93
  ### Models and tasks
66
94
 
67
- | Command | What it does |
68
- |---|---|
69
- | `glbgpt model list [chat\|image\|video]` | List available models. |
70
- | `glbgpt model show <model>` | Parameters and limits for one model. |
71
- | `glbgpt task list` | Recent generation tasks. |
72
- | `glbgpt task show last` · `glbgpt task show <id>` | A task's status. |
73
- | `glbgpt task wait last` | Block until a task finishes. |
74
- | `glbgpt task download last` | Download a finished result. |
95
+ | Command | What it does |
96
+ | ------------------------------------------------- | ------------------------------------ |
97
+ | `glbgpt model list [chat\|image\|video]` | List available models. |
98
+ | `glbgpt model show <model>` | Parameters and limits for one model. |
99
+ | `glbgpt task list` | Recent generation tasks. |
100
+ | `glbgpt task show last` · `glbgpt task show <id>` | A task's status. |
101
+ | `glbgpt task wait last` | Block until a task finishes. |
102
+ | `glbgpt task download last` | Download a finished result. |
75
103
 
76
104
  ### exec — non-interactive
77
105
 
@@ -94,19 +122,27 @@ On tool-capable models the assistant can also read files under the current direc
94
122
  search the web / fetch pages, and generate images/videos: exec waits for in-flight generations
95
123
  before exiting and reports each as a `media_result` event (`--jsonl`) or a stderr line.
96
124
 
125
+ Coding tools are **off by default in exec** (no one is present to approve): the model sees the
126
+ tools but every command/write is refused with a message naming the enable flag. Opt in with
127
+ `--allow-write` (create/edit files), `--allow-bash` (shell commands), or `--yolo` (both):
128
+
129
+ ```bash
130
+ glbgpt exec "fix the failing test in src/utils.test.ts and run the tests" --allow-write --allow-bash
131
+ ```
132
+
97
133
  ### mcp — use from other agents
98
134
 
99
135
  `glbgpt mcp` runs a [Model Context Protocol](https://modelcontextprotocol.io) server over stdio,
100
136
  so agents like Claude Code, Codex, or Cursor can chat and generate media through your GlobalGPT
101
137
  account. Sign in once with `glbgpt login` first.
102
138
 
103
- | Tool | What it does |
104
- |---|---|
105
- | `glbgpt_chat` | One chat turn: question in, answer out (system prompt, vision, JSON schema, and — on tool-capable models — live web search/fetch supported). |
106
- | `glbgpt_generate_image` | Generate an image; usually returns the finished result (URL + local file) in one call. |
107
- | `glbgpt_generate_video` | Generate a video; returns a `task_id` to check. |
108
- | `glbgpt_check_task` | Check a generation task. Waits server-side — the agent can just call it again immediately until done. |
109
- | `glbgpt_list_models` | List available chat/image/video models and what parameters each supports. |
139
+ | Tool | What it does |
140
+ | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
141
+ | `glbgpt_chat` | One chat turn: question in, answer out (system prompt, vision, JSON schema, and — on tool-capable models — live web search/fetch supported). |
142
+ | `glbgpt_generate_image` | Generate an image; usually returns the finished result (URL + local file) in one call. |
143
+ | `glbgpt_generate_video` | Generate a video; returns a `task_id` to check. |
144
+ | `glbgpt_check_task` | Check a generation task. Waits server-side — the agent can just call it again immediately until done. |
145
+ | `glbgpt_list_models` | List available chat/image/video models and what parameters each supports. |
110
146
 
111
147
  Register it:
112
148
 
@@ -182,27 +218,33 @@ re-points the link and leaves nothing to lose.
182
218
 
183
219
  **Which surface should you use?**
184
220
 
185
- | | Use it when |
186
- |---|---|
187
- | **Skill** | Your agent runs shell commands (Claude Code, Cursor, Codex). Zero config — install once and ask for an image. |
188
- | **MCP** | Your agent speaks MCP but cannot run shell commands, or you want the session credit budget enforced (`--max-credits`). |
189
- | **CLI / exec** | You are scripting it yourself, in CI, or piping output between tools. |
221
+ | | Use it when |
222
+ | -------------- | ---------------------------------------------------------------------------------------------------------------------- |
223
+ | **Skill** | Your agent runs shell commands (Claude Code, Cursor, Codex). Zero config — install once and ask for an image. |
224
+ | **MCP** | Your agent speaks MCP but cannot run shell commands, or you want the session credit budget enforced (`--max-credits`). |
225
+ | **CLI / exec** | You are scripting it yourself, in CI, or piping output between tools. |
190
226
 
191
227
  ### config
192
228
 
193
- | Command | What it does |
194
- |---|---|
195
- | `glbgpt config list` | Show persisted settings. |
196
- | `glbgpt config get <key>` · `set <key> <value>` · `unset <key>` | Manage settings. |
229
+ | Command | What it does |
230
+ | --------------------------------------------------------------- | ------------------------ |
231
+ | `glbgpt config list` | Show persisted settings. |
232
+ | `glbgpt config get <key>` · `set <key> <value>` · `unset <key>` | Manage settings. |
197
233
 
198
234
  Keys: `lang`, `max-credits` (unattended media budget), `context-pruning` (set `false` to
199
235
  keep old tool results in long chats), `update-check` (set `false` to disable the startup
200
- notice about newer versions). One-shot overrides via env vars (`GLBGPT_LANG`,
236
+ update prompt and notices about newer versions). One-shot overrides via env vars (`GLBGPT_LANG`,
201
237
  `GLBGPT_TOKEN`, `GLBGPT_MAX_CREDITS`, `GLBGPT_CONTEXT_PRUNING`, `GLBGPT_UPDATE_CHECK`)
202
238
  always win.
203
239
 
204
240
  ## Notes
205
241
 
242
+ **Updates.** When a newer version exists, an interactive start shows a chooser (Enter
243
+ updates on the spot; "Skip until next version" mutes the chooser for that release), and
244
+ `exec`/MCP print a one-line notice on stderr so agent hosts can relay it. The check reads
245
+ a local cache refreshed in the background — it never blocks startup or touches the network
246
+ on the hot path. `glbgpt update` upgrades any time; `update-check false` disables it all.
247
+
206
248
  **Usage telemetry.** The CLI reports anonymous usage events (session starts, turn latency,
207
249
  tool-call counts, error categories) to the GlobalGPT backend to help us improve the product.
208
250
  It never collects prompt text, file contents, or file paths. Set `GLBGPT_DISABLE_TELEMETRY=1`
@@ -0,0 +1,34 @@
1
+ export declare class BackgroundBashTasks {
2
+ private readonly spillDir;
3
+ private readonly tasks;
4
+ private seq;
5
+ constructor(spillDir?: string);
6
+ /** Ids of tasks still running (for surfaces that want to report before exit). */
7
+ running(): Array<{
8
+ id: string;
9
+ command: string;
10
+ }>;
11
+ start(command: string, cwd: string): {
12
+ taskId: string;
13
+ outputPath: string;
14
+ } | {
15
+ error: string;
16
+ };
17
+ /** New output since the last read (tail-capped), plus liveness/exit status. */
18
+ readNew(taskId: string): Promise<{
19
+ output: string;
20
+ truncatedBytes: number;
21
+ status: 'running' | 'exited' | 'killed';
22
+ exitCode: number | null;
23
+ } | {
24
+ error: string;
25
+ }>;
26
+ kill(taskId: string): {
27
+ ok: true;
28
+ status: string;
29
+ } | {
30
+ error: string;
31
+ };
32
+ /** Kill every still-running task (CLI exit path). Synchronous — safe in an exit handler. */
33
+ killAll(): void;
34
+ }
@@ -0,0 +1,154 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { createWriteStream, promises as fs, existsSync } from 'node:fs';
3
+ import { randomBytes } from 'node:crypto';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { cutToBytesFromEnd } from './truncate.js';
7
+ /**
8
+ * Registry for background bash tasks (`bash` with run_in_background, P1 2026-08-21).
9
+ *
10
+ * Every task's stdout+stderr streams to a spill file from byte 0 (background tasks
11
+ * are long-lived by definition — memory holds nothing). `readNew` returns the bytes
12
+ * appended since the caller's last read (tail-capped per read), so the model polls
13
+ * incrementally like `tail -f`. Tasks die with the CLI (process-group kill on exit).
14
+ */
15
+ const MAX_CONCURRENT_TASKS = 5;
16
+ const MAX_READ_BYTES = 50 * 1024;
17
+ export class BackgroundBashTasks {
18
+ spillDir;
19
+ tasks = new Map();
20
+ seq = 0;
21
+ constructor(spillDir = os.tmpdir()) {
22
+ this.spillDir = spillDir;
23
+ // Belt and suspenders: kill everything if the process exits without cleanup.
24
+ process.once('exit', () => this.killAll());
25
+ }
26
+ /** Ids of tasks still running (for surfaces that want to report before exit). */
27
+ running() {
28
+ return [...this.tasks.values()].filter((t) => !t.exited).map((t) => ({ id: t.id, command: t.command }));
29
+ }
30
+ start(command, cwd) {
31
+ const live = [...this.tasks.values()].filter((t) => !t.exited).length;
32
+ if (live >= MAX_CONCURRENT_TASKS) {
33
+ return {
34
+ error: `Too many background tasks (${live} running, max ${MAX_CONCURRENT_TASKS}). Kill one with bash_kill first.`,
35
+ };
36
+ }
37
+ const shell = process.platform === 'win32' ? (process.env.ComSpec ?? 'cmd.exe') : pickShell();
38
+ const id = `bg-${++this.seq}`;
39
+ const outputPath = path.join(this.spillDir, `glbgpt-bgtask-${randomBytes(6).toString('hex')}.log`);
40
+ const out = createWriteStream(outputPath, { flags: 'w' });
41
+ const child = spawn(shell, ['-c', command], {
42
+ cwd,
43
+ detached: process.platform !== 'win32',
44
+ env: process.env,
45
+ stdio: ['ignore', 'pipe', 'pipe'],
46
+ windowsHide: true,
47
+ });
48
+ child.stdout?.pipe(out, { end: false });
49
+ child.stderr?.pipe(out, { end: false });
50
+ const task = {
51
+ id,
52
+ command,
53
+ pid: child.pid,
54
+ outputPath,
55
+ cursor: 0,
56
+ startedAt: Date.now(),
57
+ exitCode: null,
58
+ exited: false,
59
+ killed: false,
60
+ };
61
+ child.on('error', () => {
62
+ task.exited = true;
63
+ out.end();
64
+ });
65
+ child.on('close', (code) => {
66
+ task.exited = true;
67
+ task.exitCode = code;
68
+ out.end();
69
+ });
70
+ this.tasks.set(id, task);
71
+ return { taskId: id, outputPath };
72
+ }
73
+ /** New output since the last read (tail-capped), plus liveness/exit status. */
74
+ async readNew(taskId) {
75
+ const task = this.tasks.get(taskId);
76
+ if (!task)
77
+ return { error: `Unknown background task: ${taskId}` };
78
+ let data = '';
79
+ let truncatedBytes = 0;
80
+ try {
81
+ const stat = await fs.stat(task.outputPath);
82
+ if (stat.size > task.cursor) {
83
+ const newBytes = stat.size - task.cursor;
84
+ const fh = await fs.open(task.outputPath, 'r');
85
+ try {
86
+ const readFrom = newBytes > MAX_READ_BYTES ? stat.size - MAX_READ_BYTES : task.cursor;
87
+ truncatedBytes = readFrom - task.cursor;
88
+ const buf = Buffer.alloc(stat.size - readFrom);
89
+ await fh.read(buf, 0, buf.length, readFrom);
90
+ data =
91
+ truncatedBytes > 0
92
+ ? cutToBytesFromEnd(buf.toString('utf8'), MAX_READ_BYTES)
93
+ : buf.toString('utf8');
94
+ }
95
+ finally {
96
+ await fh.close();
97
+ }
98
+ task.cursor = stat.size;
99
+ }
100
+ }
101
+ catch {
102
+ // Spill unreadable (already cleaned?) — report status only.
103
+ }
104
+ return {
105
+ output: data,
106
+ truncatedBytes,
107
+ status: task.killed ? 'killed' : task.exited ? 'exited' : 'running',
108
+ exitCode: task.exitCode,
109
+ };
110
+ }
111
+ kill(taskId) {
112
+ const task = this.tasks.get(taskId);
113
+ if (!task)
114
+ return { error: `Unknown background task: ${taskId}` };
115
+ if (task.exited)
116
+ return { ok: true, status: `already exited with code ${task.exitCode}` };
117
+ task.killed = true;
118
+ killTree(task.pid);
119
+ return { ok: true, status: 'killed' };
120
+ }
121
+ /** Kill every still-running task (CLI exit path). Synchronous — safe in an exit handler. */
122
+ killAll() {
123
+ for (const task of this.tasks.values()) {
124
+ if (!task.exited) {
125
+ task.killed = true;
126
+ killTree(task.pid);
127
+ }
128
+ }
129
+ }
130
+ }
131
+ function pickShell() {
132
+ return existsSync('/bin/bash') ? '/bin/bash' : 'sh';
133
+ }
134
+ function killTree(pid) {
135
+ if (!pid)
136
+ return;
137
+ try {
138
+ if (process.platform === 'win32') {
139
+ spawn('taskkill', ['/F', '/T', '/PID', String(pid)], { windowsHide: true });
140
+ }
141
+ else {
142
+ process.kill(-pid, 'SIGKILL');
143
+ }
144
+ }
145
+ catch {
146
+ try {
147
+ process.kill(pid, 'SIGKILL');
148
+ }
149
+ catch {
150
+ // Already gone.
151
+ }
152
+ }
153
+ }
154
+ //# sourceMappingURL=backgroundTasks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backgroundTasks.js","sourceRoot":"","sources":["../../../src/coding/backgroundTasks.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import type { ClientTool } from '../core/types.js';
2
+ import type { BackgroundBashTasks } from './backgroundTasks.js';
3
+ /**
4
+ * Companion tools for background bash tasks: poll new output, stop a task.
5
+ * Neither is gated by the permission gate — the task itself was approved when
6
+ * it was started, and observing/stopping your own task needs no new authority.
7
+ */
8
+ export declare function createBashOutputTool(tasks: BackgroundBashTasks): ClientTool;
9
+ export declare function createBashKillTool(tasks: BackgroundBashTasks): ClientTool;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Companion tools for background bash tasks: poll new output, stop a task.
3
+ * Neither is gated by the permission gate — the task itself was approved when
4
+ * it was started, and observing/stopping your own task needs no new authority.
5
+ */
6
+ export function createBashOutputTool(tasks) {
7
+ return {
8
+ name: 'bash_output',
9
+ readOnly: true,
10
+ description: 'Read NEW output from a background bash task since the last bash_output call (like tail -f), ' +
11
+ 'plus its status (running/exited/killed and exit code). Poll this after starting a command ' +
12
+ 'with run_in_background.',
13
+ parameters: {
14
+ type: 'object',
15
+ properties: {
16
+ task_id: { type: 'string', description: 'Background task id from the bash tool result.' },
17
+ },
18
+ required: ['task_id'],
19
+ },
20
+ async execute(args) {
21
+ if (typeof args.task_id !== 'string' || !args.task_id.trim()) {
22
+ return { error: 'Missing required "task_id" argument.' };
23
+ }
24
+ const r = await tasks.readNew(args.task_id.trim());
25
+ if ('error' in r)
26
+ return { error: r.error };
27
+ const parts = [];
28
+ if (r.truncatedBytes > 0)
29
+ parts.push(`[${r.truncatedBytes} bytes of new output skipped — showing the tail]`);
30
+ parts.push(r.output.length > 0 ? r.output : '(no new output)');
31
+ parts.push(r.status === 'running'
32
+ ? 'Status: running'
33
+ : r.status === 'killed'
34
+ ? 'Status: killed'
35
+ : `Status: exited with code ${r.exitCode}`);
36
+ return { output: parts.join('\n') };
37
+ },
38
+ };
39
+ }
40
+ export function createBashKillTool(tasks) {
41
+ return {
42
+ name: 'bash_kill',
43
+ readOnly: false,
44
+ description: 'Stop a background bash task (kills its whole process group).',
45
+ parameters: {
46
+ type: 'object',
47
+ properties: {
48
+ task_id: { type: 'string', description: 'Background task id from the bash tool result.' },
49
+ },
50
+ required: ['task_id'],
51
+ },
52
+ async execute(args) {
53
+ if (typeof args.task_id !== 'string' || !args.task_id.trim()) {
54
+ return { error: 'Missing required "task_id" argument.' };
55
+ }
56
+ const r = tasks.kill(args.task_id.trim());
57
+ if ('error' in r)
58
+ return { error: r.error };
59
+ return { output: `Task ${args.task_id}: ${r.status}.` };
60
+ },
61
+ };
62
+ }
63
+ //# sourceMappingURL=backgroundTools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backgroundTools.js","sourceRoot":"","sources":["../../../src/coding/backgroundTools.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AAEH,MAAM,UAAU,oBAAoB,CAAC,KAA0B;IAC7D,OAAO;QACL,IAAI,EAAE,aAAa;QACnB,QAAQ,EAAE,IAAI;QACd,WAAW,EACT,8FAA8F;YAC9F,4FAA4F;YAC5F,yBAAyB;QAC3B,UAAU,EAAE;YACV,IAAI,EAAE,QAAQ;YACd,UAAU,EAAE;gBACV,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,+CAA+C,EAAE;aAC1F;YACD,QAAQ,EAAE,CAAC,SAAS,CAAC;SACtB;QACD,KAAK,CAAC,OAAO,CAAC,IAAI;YAChB,IAAI,OAAO,IAAI,CAAC,OAAO,KAAK,QAAQ,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC;gBAC7D,OAAO,EAAE,KAAK,EAAE,sCAAsC,EAAE,CAAC;YAC3D,CAAC;YACD,MAAM,CAAC,GAAG,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;YACnD,IAAI,OAAO,IAAI,CAAC;gBAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC;YAC5C,MAAM,KAAK,GAAa,EAAE,CAAC;YAC3B,IAAI,CAAC,CAAC,cAAc,GAAG,CAAC;gBACtB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,cAAc,kDAAkD,CAAC,CAAC;YACrF,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC;YAC/D,KAAK,CAAC,IAAI,CACR,CAAC,CAAC,MAAM,KAAK,SAAS;gBACpB,CAAC,CAAC,iBAAiB;gBACnB,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,QAAQ;oBACrB,CAAC,CAAC,gBAAgB;oBAClB,CAAC,CAAC,4BAA4B,CAAC,CAAC,QAAQ,EAAE,CAC/C,CAAC;YACF,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACtC,CAAC;KACF,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,KAA0B;IAC3D,OAAO;QACL,IAAI,EAAE,WAAW;QACjB,QAAQ,EAAE,KAAK;QACf,WAAW,EAAE,8DAA8D;QAC3E,UAAU,EAAE;YACV,IAAI,EAAE,QAAQ;YACd,UAAU,EAAE;gBACV,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,+CAA+C,EAAE;aAC1F;YACD,QAAQ,EAAE,CAAC,SAAS,CAAC;SACtB;QACD,KAAK,CAAC,OAAO,CAAC,IAAI;YAChB,IAAI,OAAO,IAAI,CAAC,OAAO,KAAK,QAAQ,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC;gBAC7D,OAAO,EAAE,KAAK,EAAE,sCAAsC,EAAE,CAAC;YAC3D,CAAC;YACD,MAAM,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;YAC1C,IAAI,OAAO,IAAI,CAAC;gBAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC;YAC5C,OAAO,EAAE,MAAM,EAAE,QAAQ,IAAI,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,GAAG,EAAE,CAAC;QAC1D,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,27 @@
1
+ import type { ClientTool } from '../core/types.js';
2
+ import type { PermissionGate } from './permissions.js';
3
+ import type { BackgroundBashTasks } from './backgroundTasks.js';
4
+ export declare const BASH_DEFAULT_TIMEOUT_S = 120;
5
+ export declare const BASH_MAX_TIMEOUT_S = 600;
6
+ /**
7
+ * The `bash` tool: run a shell command in the workspace directory.
8
+ *
9
+ * Execution contract (2026-08-21 design, pi lineage):
10
+ * - New process per call; no shell state persists. Commands run in the session cwd —
11
+ * there is NO cwd parameter and `cd` does not carry over (prompt guidance says to use
12
+ * absolute paths or `cd` within a single command).
13
+ * - stdout+stderr merged in arrival order; tail-truncated at 2000 lines / 50KB with the
14
+ * full output spilled to a temp file (footnote names the path).
15
+ * - Non-zero exit is an error result but the collected output is still attached — the
16
+ * model needs the output to react.
17
+ * - Timeout (default 120s, cap 600s) kills the whole process GROUP (detached spawn), so
18
+ * children die with the shell.
19
+ */
20
+ export declare function createBashTool(opts: {
21
+ cwd: string;
22
+ gate: PermissionGate;
23
+ /** Background task registry; absent = run_in_background rejected. */
24
+ background?: BackgroundBashTasks;
25
+ /** Test hook: where spill files go. */
26
+ spillDir?: string;
27
+ }): ClientTool;
@@ -0,0 +1,144 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { OutputAccumulator } from './outputAccumulator.js';
4
+ export const BASH_DEFAULT_TIMEOUT_S = 120;
5
+ export const BASH_MAX_TIMEOUT_S = 600;
6
+ /**
7
+ * The `bash` tool: run a shell command in the workspace directory.
8
+ *
9
+ * Execution contract (2026-08-21 design, pi lineage):
10
+ * - New process per call; no shell state persists. Commands run in the session cwd —
11
+ * there is NO cwd parameter and `cd` does not carry over (prompt guidance says to use
12
+ * absolute paths or `cd` within a single command).
13
+ * - stdout+stderr merged in arrival order; tail-truncated at 2000 lines / 50KB with the
14
+ * full output spilled to a temp file (footnote names the path).
15
+ * - Non-zero exit is an error result but the collected output is still attached — the
16
+ * model needs the output to react.
17
+ * - Timeout (default 120s, cap 600s) kills the whole process GROUP (detached spawn), so
18
+ * children die with the shell.
19
+ */
20
+ export function createBashTool(opts) {
21
+ return {
22
+ name: 'bash',
23
+ description: 'Execute a bash command in the workspace directory. Returns stdout and stderr combined. ' +
24
+ 'Output is truncated to the last 2000 lines or 50KB (whichever is hit first); when truncated, ' +
25
+ 'the full output is saved to a temp file whose path is included in the result. ' +
26
+ `Optional timeout in seconds (default ${BASH_DEFAULT_TIMEOUT_S}, max ${BASH_MAX_TIMEOUT_S}). ` +
27
+ 'Shell state does not persist between calls; the working directory is always the workspace root. ' +
28
+ 'For long-running commands (dev servers, watchers) set run_in_background: true — the result gives ' +
29
+ 'a task id to poll with bash_output and stop with bash_kill.',
30
+ parameters: {
31
+ type: 'object',
32
+ properties: {
33
+ command: { type: 'string', description: 'Bash command to execute' },
34
+ timeout_seconds: {
35
+ type: 'number',
36
+ description: `Timeout in seconds (default ${BASH_DEFAULT_TIMEOUT_S}, max ${BASH_MAX_TIMEOUT_S})`,
37
+ },
38
+ run_in_background: {
39
+ type: 'boolean',
40
+ description: 'Run detached and return a task id immediately (for dev servers / long jobs). ' +
41
+ 'Poll with bash_output; stop with bash_kill. No timeout applies.',
42
+ },
43
+ },
44
+ required: ['command'],
45
+ },
46
+ readOnly: false,
47
+ async execute(args) {
48
+ const command = typeof args.command === 'string' ? args.command.trim() : '';
49
+ if (!command)
50
+ return { error: 'command is required' };
51
+ const rawTimeout = args.timeout_seconds;
52
+ let timeoutS = BASH_DEFAULT_TIMEOUT_S;
53
+ if (rawTimeout !== undefined && rawTimeout !== null) {
54
+ const n = Number(rawTimeout);
55
+ if (!Number.isFinite(n) || n <= 0) {
56
+ return { error: 'Invalid timeout_seconds: must be a positive number of seconds' };
57
+ }
58
+ timeoutS = Math.min(n, BASH_MAX_TIMEOUT_S);
59
+ }
60
+ const verdict = await opts.gate.check({ kind: 'bash', command });
61
+ if (!verdict.ok)
62
+ return { error: verdict.message };
63
+ if (args.run_in_background === true) {
64
+ if (!opts.background)
65
+ return { error: 'Background tasks are not available here.' };
66
+ const started = opts.background.start(command, opts.cwd);
67
+ if ('error' in started)
68
+ return { error: started.error };
69
+ return {
70
+ output: `Started background task ${started.taskId}. ` +
71
+ 'Poll new output with bash_output; stop it with bash_kill. It dies when the CLI exits.',
72
+ task_id: started.taskId,
73
+ };
74
+ }
75
+ return runBash(command, opts.cwd, timeoutS, opts.spillDir);
76
+ },
77
+ };
78
+ }
79
+ function pickShell() {
80
+ if (process.platform === 'win32')
81
+ return process.env.ComSpec ?? 'cmd.exe';
82
+ if (existsSync('/bin/bash'))
83
+ return '/bin/bash';
84
+ return 'sh';
85
+ }
86
+ async function runBash(command, cwd, timeoutS, spillDir) {
87
+ if (!existsSync(cwd)) {
88
+ return { error: `Working directory does not exist: ${cwd}` };
89
+ }
90
+ const shell = pickShell();
91
+ const detached = process.platform !== 'win32';
92
+ const child = spawn(shell, ['-c', command], {
93
+ cwd,
94
+ detached,
95
+ env: process.env,
96
+ stdio: ['ignore', 'pipe', 'pipe'],
97
+ windowsHide: true,
98
+ });
99
+ const acc = new OutputAccumulator(undefined, undefined, spillDir);
100
+ child.stdout?.on('data', (b) => acc.append(b));
101
+ child.stderr?.on('data', (b) => acc.append(b));
102
+ let timedOut = false;
103
+ const timer = setTimeout(() => {
104
+ timedOut = true;
105
+ killTree(child.pid);
106
+ }, timeoutS * 1000);
107
+ const exitCode = await new Promise((resolve) => {
108
+ child.on('error', () => resolve(null));
109
+ // 'close' (not 'exit') so stdio has fully flushed before we snapshot the output.
110
+ child.on('close', (code) => resolve(code));
111
+ });
112
+ clearTimeout(timer);
113
+ const { text } = await acc.finalize();
114
+ const output = text.length > 0 ? text : '(no output)';
115
+ if (timedOut) {
116
+ return { error: `${output}\n\nCommand timed out after ${timeoutS} seconds` };
117
+ }
118
+ if (exitCode !== 0 && exitCode !== null) {
119
+ return { error: `${output}\n\nCommand exited with code ${exitCode}` };
120
+ }
121
+ return { output };
122
+ }
123
+ /** Kill the whole process group (detached spawn) so children die with the shell. */
124
+ function killTree(pid) {
125
+ if (!pid)
126
+ return;
127
+ try {
128
+ if (process.platform === 'win32') {
129
+ spawn('taskkill', ['/F', '/T', '/PID', String(pid)], { windowsHide: true });
130
+ }
131
+ else {
132
+ process.kill(-pid, 'SIGKILL');
133
+ }
134
+ }
135
+ catch {
136
+ try {
137
+ process.kill(pid, 'SIGKILL');
138
+ }
139
+ catch {
140
+ // Already gone.
141
+ }
142
+ }
143
+ }
144
+ //# sourceMappingURL=bashTool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bashTool.js","sourceRoot":"","sources":["../../../src/coding/bashTool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAErC,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAI3D,MAAM,CAAC,MAAM,sBAAsB,GAAG,GAAG,CAAC;AAC1C,MAAM,CAAC,MAAM,kBAAkB,GAAG,GAAG,CAAC;AAEtC;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,cAAc,CAAC,IAO9B;IACC,OAAO;QACL,IAAI,EAAE,MAAM;QACZ,WAAW,EACT,yFAAyF;YACzF,+FAA+F;YAC/F,gFAAgF;YAChF,wCAAwC,sBAAsB,SAAS,kBAAkB,KAAK;YAC9F,kGAAkG;YAClG,mGAAmG;YACnG,6DAA6D;QAC/D,UAAU,EAAE;YACV,IAAI,EAAE,QAAQ;YACd,UAAU,EAAE;gBACV,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,yBAAyB,EAAE;gBACnE,eAAe,EAAE;oBACf,IAAI,EAAE,QAAQ;oBACd,WAAW,EAAE,+BAA+B,sBAAsB,SAAS,kBAAkB,GAAG;iBACjG;gBACD,iBAAiB,EAAE;oBACjB,IAAI,EAAE,SAAS;oBACf,WAAW,EACT,+EAA+E;wBAC/E,iEAAiE;iBACpE;aACF;YACD,QAAQ,EAAE,CAAC,SAAS,CAAC;SACtB;QACD,QAAQ,EAAE,KAAK;QACf,KAAK,CAAC,OAAO,CAAC,IAA6B;YACzC,MAAM,OAAO,GAAG,OAAO,IAAI,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC5E,IAAI,CAAC,OAAO;gBAAE,OAAO,EAAE,KAAK,EAAE,qBAAqB,EAAE,CAAC;YAEtD,MAAM,UAAU,GAAG,IAAI,CAAC,eAAe,CAAC;YACxC,IAAI,QAAQ,GAAG,sBAAsB,CAAC;YACtC,IAAI,UAAU,KAAK,SAAS,IAAI,UAAU,KAAK,IAAI,EAAE,CAAC;gBACpD,MAAM,CAAC,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC;gBAC7B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;oBAClC,OAAO,EAAE,KAAK,EAAE,+DAA+D,EAAE,CAAC;gBACpF,CAAC;gBACD,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,kBAAkB,CAAC,CAAC;YAC7C,CAAC;YAED,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC;YACjE,IAAI,CAAC,OAAO,CAAC,EAAE;gBAAE,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC;YAEnD,IAAI,IAAI,CAAC,iBAAiB,KAAK,IAAI,EAAE,CAAC;gBACpC,IAAI,CAAC,IAAI,CAAC,UAAU;oBAAE,OAAO,EAAE,KAAK,EAAE,0CAA0C,EAAE,CAAC;gBACnF,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;gBACzD,IAAI,OAAO,IAAI,OAAO;oBAAE,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC;gBACxD,OAAO;oBACL,MAAM,EACJ,2BAA2B,OAAO,CAAC,MAAM,IAAI;wBAC7C,uFAAuF;oBACzF,OAAO,EAAE,OAAO,CAAC,MAAM;iBACxB,CAAC;YACJ,CAAC;YAED,OAAO,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC7D,CAAC;KACF,CAAC;AACJ,CAAC;AAED,SAAS,SAAS;IAChB,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO;QAAE,OAAO,OAAO,CAAC,GAAG,CAAC,OAAO,IAAI,SAAS,CAAC;IAC1E,IAAI,UAAU,CAAC,WAAW,CAAC;QAAE,OAAO,WAAW,CAAC;IAChD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,KAAK,UAAU,OAAO,CACpB,OAAe,EACf,GAAW,EACX,QAAgB,EAChB,QAAiB;IAEjB,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACrB,OAAO,EAAE,KAAK,EAAE,qCAAqC,GAAG,EAAE,EAAE,CAAC;IAC/D,CAAC;IACD,MAAM,KAAK,GAAG,SAAS,EAAE,CAAC;IAC1B,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC;IAC9C,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,EAAE;QAC1C,GAAG;QACH,QAAQ;QACR,GAAG,EAAE,OAAO,CAAC,GAAG;QAChB,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC;QACjC,WAAW,EAAE,IAAI;KAClB,CAAC,CAAC;IAEH,MAAM,GAAG,GAAG,IAAI,iBAAiB,CAAC,SAAS,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC;IAClE,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,CAAS,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IACvD,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,CAAS,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IAEvD,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;QAC5B,QAAQ,GAAG,IAAI,CAAC;QAChB,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACtB,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC,CAAC;IAEpB,MAAM,QAAQ,GAAkB,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC5D,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;QACvC,iFAAiF;QACjF,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;IACH,YAAY,CAAC,KAAK,CAAC,CAAC;IAEpB,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,GAAG,CAAC,QAAQ,EAAE,CAAC;IACtC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,aAAa,CAAC;IAEtD,IAAI,QAAQ,EAAE,CAAC;QACb,OAAO,EAAE,KAAK,EAAE,GAAG,MAAM,+BAA+B,QAAQ,UAAU,EAAE,CAAC;IAC/E,CAAC;IACD,IAAI,QAAQ,KAAK,CAAC,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QACxC,OAAO,EAAE,KAAK,EAAE,GAAG,MAAM,gCAAgC,QAAQ,EAAE,EAAE,CAAC;IACxE,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,CAAC;AACpB,CAAC;AAED,oFAAoF;AACpF,SAAS,QAAQ,CAAC,GAAuB;IACvC,IAAI,CAAC,GAAG;QAAE,OAAO;IACjB,IAAI,CAAC;QACH,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;YACjC,KAAK,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC;QAC9E,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;QAChC,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,IAAI,CAAC;YACH,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;QAC/B,CAAC;QAAC,MAAM,CAAC;YACP,gBAAgB;QAClB,CAAC;IACH,CAAC;AACH,CAAC"}