@abacus-ai/cli 1.106.25007 → 2.0.0-canary.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.
- package/.oxlintrc.json +8 -0
- package/dist/index.mjs +12603 -0
- package/package.json +7 -39
- package/resources/abacus.ico +0 -0
- package/resources/entitlements.plist +9 -0
- package/src/__e2e__/README.md +196 -0
- package/src/__e2e__/agent-interactions.e2e.test.tsx +61 -0
- package/src/__e2e__/cli-commands.e2e.test.tsx +77 -0
- package/src/__e2e__/conversation-throttle.e2e.test.ts +453 -0
- package/src/__e2e__/conversation.e2e.test.tsx +56 -0
- package/src/__e2e__/diff-preview.e2e.test.tsx +3399 -0
- package/src/__e2e__/file-creation.e2e.test.tsx +149 -0
- package/src/__e2e__/helpers/test-helpers.ts +450 -0
- package/src/__e2e__/keyboard-navigation.e2e.test.tsx +34 -0
- package/src/__e2e__/llm-models.e2e.test.ts +402 -0
- package/src/__e2e__/mcp/mcp-callback-flow.e2e.test.tsx +71 -0
- package/src/__e2e__/mcp/mcp-full-app-ui.e2e.test.tsx +167 -0
- package/src/__e2e__/mcp/mcp-ui-rendering.e2e.test.tsx +185 -0
- package/src/__e2e__/repl.e2e.test.tsx +78 -0
- package/src/__e2e__/shell-compatibility.e2e.test.tsx +76 -0
- package/src/__e2e__/theme-mcp.e2e.test.tsx +98 -0
- package/src/__e2e__/tool-permissions.e2e.test.tsx +66 -0
- package/src/args.ts +22 -0
- package/src/components/__tests__/react-compiler.test.tsx +78 -0
- package/src/components/__tests__/status-indicator.test.tsx +403 -0
- package/src/components/composer/__tests__/bash-runner.test.tsx +263 -0
- package/src/components/composer/agent-mode-indicator.tsx +63 -0
- package/src/components/composer/bash-runner.tsx +54 -0
- package/src/components/composer/commands/default-commands.tsx +615 -0
- package/src/components/composer/commands/handler.tsx +59 -0
- package/src/components/composer/commands/picker.tsx +273 -0
- package/src/components/composer/commands/registry.ts +233 -0
- package/src/components/composer/commands/types.ts +33 -0
- package/src/components/composer/context.tsx +88 -0
- package/src/components/composer/file-mention-picker.tsx +83 -0
- package/src/components/composer/help.tsx +44 -0
- package/src/components/composer/index.tsx +1006 -0
- package/src/components/composer/mentions.ts +57 -0
- package/src/components/composer/message-queue.tsx +70 -0
- package/src/components/composer/mode-panel.tsx +35 -0
- package/src/components/composer/modes/__tests__/bash-handler.test.tsx +755 -0
- package/src/components/composer/modes/__tests__/bash-renderer.test.tsx +1108 -0
- package/src/components/composer/modes/bash-handler.tsx +132 -0
- package/src/components/composer/modes/bash-renderer.tsx +175 -0
- package/src/components/composer/modes/default-handlers.tsx +33 -0
- package/src/components/composer/modes/index.ts +41 -0
- package/src/components/composer/modes/types.ts +21 -0
- package/src/components/composer/persistent-shell.ts +283 -0
- package/src/components/composer/process.ts +65 -0
- package/src/components/composer/types.ts +9 -0
- package/src/components/composer/use-mention-search.ts +68 -0
- package/src/components/error-boundry.tsx +60 -0
- package/src/components/exit-message.tsx +29 -0
- package/src/components/expanded-view.tsx +74 -0
- package/src/components/file-completion.tsx +127 -0
- package/src/components/header.tsx +47 -0
- package/src/components/logo.tsx +37 -0
- package/src/components/segments.tsx +356 -0
- package/src/components/status-indicator.tsx +306 -0
- package/src/components/tool-group-summary.tsx +263 -0
- package/src/components/tool-permissions/ask-user-question-permission-ui.tsx +312 -0
- package/src/components/tool-permissions/diff-preview.tsx +355 -0
- package/src/components/tool-permissions/index.ts +5 -0
- package/src/components/tool-permissions/permission-options.tsx +375 -0
- package/src/components/tool-permissions/permission-preview-header.tsx +57 -0
- package/src/components/tool-permissions/tool-permission-ui.tsx +398 -0
- package/src/components/tools/agent/ask-user-question.tsx +101 -0
- package/src/components/tools/agent/enter-plan-mode.tsx +49 -0
- package/src/components/tools/agent/exit-plan-mode.tsx +75 -0
- package/src/components/tools/agent/handoff-to-main.tsx +27 -0
- package/src/components/tools/agent/subagent.tsx +37 -0
- package/src/components/tools/agent/todo-write.tsx +104 -0
- package/src/components/tools/browser/close-tab.tsx +58 -0
- package/src/components/tools/browser/computer.tsx +70 -0
- package/src/components/tools/browser/get-interactive-elements.tsx +54 -0
- package/src/components/tools/browser/get-tab-content.tsx +51 -0
- package/src/components/tools/browser/navigate-to.tsx +59 -0
- package/src/components/tools/browser/new-tab.tsx +60 -0
- package/src/components/tools/browser/perform-action.tsx +63 -0
- package/src/components/tools/browser/refresh-tab.tsx +43 -0
- package/src/components/tools/browser/switch-tab.tsx +58 -0
- package/src/components/tools/filesystem/delete-file.tsx +104 -0
- package/src/components/tools/filesystem/edit.tsx +220 -0
- package/src/components/tools/filesystem/list-dir.tsx +78 -0
- package/src/components/tools/filesystem/read-file.tsx +180 -0
- package/src/components/tools/filesystem/upload-image.tsx +76 -0
- package/src/components/tools/ide/ide-diagnostics.tsx +62 -0
- package/src/components/tools/index.ts +91 -0
- package/src/components/tools/mcp/mcp-tool.tsx +158 -0
- package/src/components/tools/search/fetch-url.tsx +73 -0
- package/src/components/tools/search/file-search.tsx +78 -0
- package/src/components/tools/search/grep.tsx +90 -0
- package/src/components/tools/search/semantic-search.tsx +66 -0
- package/src/components/tools/search/web-search.tsx +71 -0
- package/src/components/tools/shared/index.tsx +48 -0
- package/src/components/tools/shared/zod-coercion.ts +35 -0
- package/src/components/tools/terminal/bash-tool-output.tsx +174 -0
- package/src/components/tools/terminal/get-terminal-output.tsx +85 -0
- package/src/components/tools/terminal/run-in-terminal.tsx +106 -0
- package/src/components/tools/types.ts +16 -0
- package/src/components/tools.tsx +66 -0
- package/src/components/ui/__tests__/divider.test.tsx +61 -0
- package/src/components/ui/__tests__/gradient.test.tsx +125 -0
- package/src/components/ui/__tests__/input.test.tsx +166 -0
- package/src/components/ui/__tests__/select.test.tsx +273 -0
- package/src/components/ui/__tests__/shimmer.test.tsx +99 -0
- package/src/components/ui/blinking-indicator.tsx +25 -0
- package/src/components/ui/divider.tsx +162 -0
- package/src/components/ui/gradient.tsx +56 -0
- package/src/components/ui/input.tsx +228 -0
- package/src/components/ui/select.tsx +151 -0
- package/src/components/ui/shimmer.tsx +84 -0
- package/src/context/agent-mode.tsx +95 -0
- package/src/context/extension-file.tsx +136 -0
- package/src/context/network-activity.tsx +45 -0
- package/src/context/notification.tsx +62 -0
- package/src/context/shell-size.tsx +49 -0
- package/src/context/shell-title.tsx +38 -0
- package/src/entrypoints/print-mode.ts +312 -0
- package/src/entrypoints/repl.tsx +401 -0
- package/src/hooks/use-agent.ts +15 -0
- package/src/hooks/use-api-client.ts +1 -0
- package/src/hooks/use-available-height.ts +8 -0
- package/src/hooks/use-cleanup.ts +29 -0
- package/src/hooks/use-interrupt-manager.ts +242 -0
- package/src/hooks/use-models.ts +22 -0
- package/src/index.ts +217 -0
- package/src/lib/__tests__/ansi.test.ts +255 -0
- package/src/lib/__tests__/cli.test.ts +122 -0
- package/src/lib/__tests__/commands.test.ts +325 -0
- package/src/lib/__tests__/constants.test.ts +15 -0
- package/src/lib/__tests__/focusables.test.ts +25 -0
- package/src/lib/__tests__/fs.test.ts +231 -0
- package/src/lib/__tests__/markdown.test.tsx +348 -0
- package/src/lib/__tests__/mcpCommandHandler.test.ts +173 -0
- package/src/lib/__tests__/mcpManagement.test.ts +38 -0
- package/src/lib/__tests__/path-paste.test.ts +144 -0
- package/src/lib/__tests__/path.test.ts +300 -0
- package/src/lib/__tests__/queries.test.ts +39 -0
- package/src/lib/__tests__/standaloneMcpService.test.ts +71 -0
- package/src/lib/__tests__/text-buffer.test.ts +328 -0
- package/src/lib/__tests__/text-utils.test.ts +32 -0
- package/src/lib/__tests__/timing.test.ts +78 -0
- package/src/lib/__tests__/utils.test.ts +238 -0
- package/src/lib/__tests__/vim-buffer-actions.test.ts +154 -0
- package/src/lib/ansi.ts +150 -0
- package/src/lib/cli-push-server.ts +112 -0
- package/src/lib/cli.ts +44 -0
- package/src/lib/clipboard.ts +226 -0
- package/src/lib/command-utils.ts +93 -0
- package/src/lib/commands.ts +270 -0
- package/src/lib/constants.ts +3 -0
- package/src/lib/extension-connection.ts +181 -0
- package/src/lib/focusables.ts +7 -0
- package/src/lib/fs.ts +533 -0
- package/src/lib/markdown/code-block.tsx +63 -0
- package/src/lib/markdown/index.ts +4 -0
- package/src/lib/markdown/link.tsx +19 -0
- package/src/lib/markdown/markdown.tsx +372 -0
- package/src/lib/markdown/types.ts +15 -0
- package/src/lib/mcpCommandHandler.ts +121 -0
- package/src/lib/mcpManagement.ts +44 -0
- package/src/lib/path-paste.ts +185 -0
- package/src/lib/path.ts +179 -0
- package/src/lib/queries.ts +15 -0
- package/src/lib/standaloneMcpService.ts +688 -0
- package/src/lib/status-utils.ts +237 -0
- package/src/lib/test-utils.tsx +72 -0
- package/src/lib/text-buffer.ts +2415 -0
- package/src/lib/text-utils.ts +272 -0
- package/src/lib/timing.ts +63 -0
- package/src/lib/types.ts +295 -0
- package/src/lib/utils.ts +182 -0
- package/src/lib/vim-buffer-actions.ts +732 -0
- package/src/providers/agent.tsx +1075 -0
- package/src/providers/api-client.tsx +43 -0
- package/src/services/logger.ts +85 -0
- package/src/terminal/detection.ts +187 -0
- package/src/terminal/exit.ts +279 -0
- package/src/terminal/notification.ts +83 -0
- package/src/terminal/progress.ts +201 -0
- package/src/terminal/setup.ts +797 -0
- package/src/terminal/suspend.ts +58 -0
- package/src/terminal/types.ts +51 -0
- package/src/theme/context.tsx +57 -0
- package/src/theme/index.ts +4 -0
- package/src/theme/themed.tsx +35 -0
- package/src/theme/themes.json +546 -0
- package/src/theme/types.ts +110 -0
- package/src/tools/types.ts +59 -0
- package/src/tools/utils/__tests__/zod-coercion.test.ts +33 -0
- package/src/tools/utils/tool-ui-components.tsx +631 -0
- package/src/tools/utils/zod-coercion.ts +35 -0
- package/tsconfig.json +11 -0
- package/tsconfig.node.json +29 -0
- package/tsconfig.test.json +27 -0
- package/tsdown.config.ts +17 -0
- package/vitest.config.ts +76 -0
- package/README.md +0 -28
- package/dist/index.js +0 -26
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
|
|
5
|
+
import type { TextBufferState } from "../text-buffer.js";
|
|
6
|
+
|
|
7
|
+
import { handleVimAction, assumeExhaustive } from "../vim-buffer-actions.js";
|
|
8
|
+
|
|
9
|
+
describe.concurrent("vim-buffer-actions", () => {
|
|
10
|
+
const createState = (
|
|
11
|
+
lines: string[] = ["hello world"],
|
|
12
|
+
cursorRow = 0,
|
|
13
|
+
cursorCol = 0,
|
|
14
|
+
): TextBufferState => ({
|
|
15
|
+
lines,
|
|
16
|
+
cursorRow,
|
|
17
|
+
cursorCol,
|
|
18
|
+
preferredCol: null,
|
|
19
|
+
undoStack: [],
|
|
20
|
+
redoStack: [],
|
|
21
|
+
clipboard: null,
|
|
22
|
+
selectionAnchor: null,
|
|
23
|
+
viewportWidth: 80,
|
|
24
|
+
viewportHeight: 24,
|
|
25
|
+
visualLayout: {
|
|
26
|
+
visualLines: lines,
|
|
27
|
+
logicalToVisualMap: lines.map((_, i) => [[i, 0]]),
|
|
28
|
+
visualToLogicalMap: lines.map((_, i) => [i, 0]),
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe.concurrent("assumeExhaustive", () => {
|
|
33
|
+
it("should be a function", () => {
|
|
34
|
+
expect(typeof assumeExhaustive).toBe("function");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("should not throw when called", () => {
|
|
38
|
+
expect(() => assumeExhaustive(null as never)).not.toThrow();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe.concurrent("handleVimAction", () => {
|
|
43
|
+
it("should handle vim_move_left", () => {
|
|
44
|
+
const state = createState(["hello"], 0, 5);
|
|
45
|
+
const newState = handleVimAction(state, { type: "vim_move_left", payload: { count: 1 } });
|
|
46
|
+
expect(newState.cursorCol).toBe(4);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("should handle vim_move_right", () => {
|
|
50
|
+
const state = createState(["hello"], 0, 0);
|
|
51
|
+
const newState = handleVimAction(state, { type: "vim_move_right", payload: { count: 1 } });
|
|
52
|
+
expect(newState.cursorCol).toBe(1);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("should handle vim_move_to_line_start", () => {
|
|
56
|
+
const state = createState(["hello world"], 0, 5);
|
|
57
|
+
const newState = handleVimAction(state, { type: "vim_move_to_line_start" });
|
|
58
|
+
expect(newState.cursorCol).toBe(0);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should handle vim_move_to_line_end", () => {
|
|
62
|
+
// TODO: IMPROVEMENT - Test should verify exact position. For "hello" (5 chars),
|
|
63
|
+
// cursor should be at position 4 (last character), not just > 0.
|
|
64
|
+
// Current implementation: lineLength > 0 ? lineLength - 1 : 0
|
|
65
|
+
const state = createState(["hello"], 0, 0);
|
|
66
|
+
const newState = handleVimAction(state, { type: "vim_move_to_line_end" });
|
|
67
|
+
expect(newState.cursorCol).toBeGreaterThan(0);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should handle vim_move_to_first_nonwhitespace", () => {
|
|
71
|
+
const state = createState([" hello"], 0, 0);
|
|
72
|
+
const newState = handleVimAction(state, { type: "vim_move_to_first_nonwhitespace" });
|
|
73
|
+
expect(newState.cursorCol).toBe(3);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("should handle vim_move_to_first_line", () => {
|
|
77
|
+
const state = createState(["line1", "line2", "line3"], 2, 0);
|
|
78
|
+
const newState = handleVimAction(state, { type: "vim_move_to_first_line" });
|
|
79
|
+
expect(newState.cursorRow).toBe(0);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should handle vim_move_to_last_line", () => {
|
|
83
|
+
const state = createState(["line1", "line2", "line3"], 0, 0);
|
|
84
|
+
const newState = handleVimAction(state, { type: "vim_move_to_last_line" });
|
|
85
|
+
expect(newState.cursorRow).toBe(2);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("should handle vim_move_to_line", () => {
|
|
89
|
+
// TODO: CLARIFICATION - The API is confusing: lineNumber is 1-indexed (user-facing)
|
|
90
|
+
// but internally converted to 0-indexed. Consider documenting this clearly or
|
|
91
|
+
// using a more explicit parameter name like `lineNumberOneIndexed`.
|
|
92
|
+
// Current behavior: lineNumber 2 -> row index 1 (second line)
|
|
93
|
+
const state = createState(["line1", "line2", "line3"], 0, 0);
|
|
94
|
+
const newState = handleVimAction(state, {
|
|
95
|
+
type: "vim_move_to_line",
|
|
96
|
+
payload: { lineNumber: 2 },
|
|
97
|
+
});
|
|
98
|
+
expect(newState.cursorRow).toBe(1); // 0-indexed
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("should handle vim_delete_char", () => {
|
|
102
|
+
const state = createState(["hello"], 0, 2);
|
|
103
|
+
const newState = handleVimAction(state, { type: "vim_delete_char", payload: { count: 1 } });
|
|
104
|
+
expect(newState.lines[0].length).toBeLessThan(state.lines[0].length);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("should handle vim_move_word_forward", () => {
|
|
108
|
+
const state = createState(["hello world test"], 0, 0);
|
|
109
|
+
const newState = handleVimAction(state, {
|
|
110
|
+
type: "vim_move_word_forward",
|
|
111
|
+
payload: { count: 1 },
|
|
112
|
+
});
|
|
113
|
+
expect(newState.cursorCol).toBeGreaterThan(0);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("should handle vim_move_word_backward", () => {
|
|
117
|
+
const state = createState(["hello world"], 0, 11);
|
|
118
|
+
const newState = handleVimAction(state, {
|
|
119
|
+
type: "vim_move_word_backward",
|
|
120
|
+
payload: { count: 1 },
|
|
121
|
+
});
|
|
122
|
+
expect(newState.cursorCol).toBeLessThan(11);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("should handle vim_delete_word_forward", () => {
|
|
126
|
+
const state = createState(["hello world"], 0, 0);
|
|
127
|
+
const newState = handleVimAction(state, {
|
|
128
|
+
type: "vim_delete_word_forward",
|
|
129
|
+
payload: { count: 1 },
|
|
130
|
+
});
|
|
131
|
+
expect(newState.lines[0]).not.toContain("hello");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("should handle vim_delete_to_end_of_line", () => {
|
|
135
|
+
const state = createState(["hello world"], 0, 5);
|
|
136
|
+
const newState = handleVimAction(state, { type: "vim_delete_to_end_of_line" });
|
|
137
|
+
expect(newState.lines[0]).toBe("hello");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("should handle vim_open_line_below", () => {
|
|
141
|
+
const state = createState(["line1"], 0, 5);
|
|
142
|
+
const newState = handleVimAction(state, { type: "vim_open_line_below" });
|
|
143
|
+
expect(newState.lines.length).toBe(2);
|
|
144
|
+
expect(newState.cursorRow).toBe(1);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("should handle vim_open_line_above", () => {
|
|
148
|
+
const state = createState(["line1"], 0, 5);
|
|
149
|
+
const newState = handleVimAction(state, { type: "vim_open_line_above" });
|
|
150
|
+
expect(newState.lines.length).toBe(2);
|
|
151
|
+
expect(newState.cursorRow).toBe(0);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
});
|
package/src/lib/ansi.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
export enum ANSI {
|
|
2
|
+
ESC = "\x1b",
|
|
3
|
+
BEL = "\x07",
|
|
4
|
+
ST = "\x1b\\",
|
|
5
|
+
ENABLE_FOCUS_REPORTING = `${ANSI.ESC}[?1004h`,
|
|
6
|
+
DISABLE_FOCUS_REPORTING = `${ANSI.ESC}[?1004l`,
|
|
7
|
+
FOCUS_IN = `${ANSI.ESC}[I`,
|
|
8
|
+
FOCUS_OUT = `${ANSI.ESC}[O`,
|
|
9
|
+
SHOW_CURSOR = `${ANSI.ESC}[?25h`,
|
|
10
|
+
HIDE_CURSOR = `${ANSI.ESC}[?25l`,
|
|
11
|
+
SET_TITLE = `${ANSI.ESC}]2;`,
|
|
12
|
+
RESET_SGR = `${ANSI.ESC}[0m`,
|
|
13
|
+
SOFT_RESET = `${ANSI.ESC}[!p`,
|
|
14
|
+
HARD_RESET = `${ANSI.ESC}c`,
|
|
15
|
+
ENABLE_BRACKETED_PASTE = `${ANSI.ESC}[?2004h`,
|
|
16
|
+
DISABLE_BRACKETED_PASTE = `${ANSI.ESC}[?2004l`,
|
|
17
|
+
PASTE_MODE_PREFIX = `${ANSI.ESC}[200~`,
|
|
18
|
+
PASTE_MODE_SUFFIX = `${ANSI.ESC}[201~`,
|
|
19
|
+
DIM = `${ANSI.ESC}[2m`,
|
|
20
|
+
BOLD = `${ANSI.ESC}[1m`,
|
|
21
|
+
RESET_BOLD = `${ANSI.ESC}[22m`,
|
|
22
|
+
ITALIC = `${ANSI.ESC}[3m`,
|
|
23
|
+
RESET_ITALIC = `${ANSI.ESC}[23m`,
|
|
24
|
+
STRIKETHROUGH = `${ANSI.ESC}[9m`,
|
|
25
|
+
RESET_STRIKETHROUGH = `${ANSI.ESC}[29m`,
|
|
26
|
+
UNDERLINE = `${ANSI.ESC}[4m`,
|
|
27
|
+
RESET_UNDERLINE = `${ANSI.ESC}[24m`,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* OSC 9 - iTerm2 notification
|
|
32
|
+
* Format: ESC ] 9 ; message BEL
|
|
33
|
+
*/
|
|
34
|
+
export function osc9(message: string): string {
|
|
35
|
+
return `${ANSI.ESC}]9;\n\n${message}${ANSI.BEL}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* OSC 777 - Ghostty/urxvt notification
|
|
40
|
+
* Format: ESC ] 777 ; notify ; title ; body BEL
|
|
41
|
+
*/
|
|
42
|
+
export function osc777(title: string, body: string): string {
|
|
43
|
+
return `${ANSI.ESC}]777;notify;${title};${body}${ANSI.BEL}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* OSC 99 - Kitty desktop notification
|
|
48
|
+
* Format: ESC ] 99 ; metadata ; payload ST
|
|
49
|
+
*/
|
|
50
|
+
export function osc99(title: string, body: string): string {
|
|
51
|
+
const notificationId = Math.floor(Math.random() * 10000);
|
|
52
|
+
const titlePart = `${ANSI.ESC}]99;i=${notificationId}:d=0:p=title;${title}${ANSI.ST}`;
|
|
53
|
+
const bodyPart = `${ANSI.ESC}]99;i=${notificationId}:p=body;${body}${ANSI.ST}`;
|
|
54
|
+
const donePart = `${ANSI.ESC}]99;i=${notificationId}:d=1:a=focus;${ANSI.ST}`;
|
|
55
|
+
return titlePart + bodyPart + donePart;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type ProgressState = 0 | 1 | 2 | 3 | 4;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* OSC 9;4 - Progress bar (ConEmu/Ghostty/iTerm2 style)
|
|
62
|
+
* Format: ESC ] 9 ; 4 ; state ; progress [BEL/ST]
|
|
63
|
+
* @param state - 0 (hide), 1 (default), 2 (error), 3 (indeterminate), 4 (warning)
|
|
64
|
+
* @param progress - Progress percentage (0-100), required when state is 1, 2, or 4
|
|
65
|
+
*/
|
|
66
|
+
export function osc9Progress(state: ProgressState, progress?: number): string {
|
|
67
|
+
const p = progress !== undefined ? Math.max(0, Math.min(100, Math.round(progress))) : 0;
|
|
68
|
+
const payload = `9;4;${state};${p}`;
|
|
69
|
+
// Emit both terminators for maximum compatibility (iTerm2 prefers BEL, Ghostty prefers ST)
|
|
70
|
+
return `${ANSI.ESC}]${payload}${ANSI.BEL}${ANSI.ESC}]${payload}${ANSI.ST}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* OSC 1337 - iTerm2 Progress indicator
|
|
75
|
+
*/
|
|
76
|
+
function sanitizeItermValue(value: string): string {
|
|
77
|
+
return value
|
|
78
|
+
.replaceAll("\x1b", "")
|
|
79
|
+
.replaceAll("\x07", "")
|
|
80
|
+
.replaceAll("\n", " ")
|
|
81
|
+
.replaceAll("\r", " ")
|
|
82
|
+
.replaceAll(";", ":")
|
|
83
|
+
.trim()
|
|
84
|
+
.slice(0, 120);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function itermOsc(payload: string): string {
|
|
88
|
+
// Emit both terminators for maximum compatibility: BEL and ST.
|
|
89
|
+
return `${ANSI.ESC}]${payload}${ANSI.BEL}${ANSI.ESC}]${payload}${ANSI.ST}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function osc1337ProgressClear(): string {
|
|
93
|
+
return itermOsc("1337;Progress=");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function osc1337ProgressState(state: 0 | 1 | 2): string {
|
|
97
|
+
return itermOsc(`1337;Progress=${state}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function osc1337ProgressPercent(percent: number, message?: string): string {
|
|
101
|
+
const progressValue = Math.max(0, Math.min(100, Math.round(percent)));
|
|
102
|
+
const msg = message ? sanitizeItermValue(message) : "";
|
|
103
|
+
|
|
104
|
+
if (msg) {
|
|
105
|
+
return (
|
|
106
|
+
itermOsc(`1337;Progress=${progressValue};${msg}`) +
|
|
107
|
+
itermOsc(`1337;Progress=message=${msg};percent=${progressValue}`)
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return itermOsc(`1337;Progress=${progressValue}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Creates a regex pattern that matches ANSI escape sequences
|
|
116
|
+
* @param options.onlyFirst - If true, matches only the first occurrence
|
|
117
|
+
* @returns Regular expression for matching ANSI sequences
|
|
118
|
+
*/
|
|
119
|
+
export function ansiRegex({ onlyFirst = false } = {}): RegExp {
|
|
120
|
+
// Valid string terminator sequences are BEL, ESC\, and 0x9c
|
|
121
|
+
const ST = "(?:\\u0007|\\u001B\\u005C|\\u009C)";
|
|
122
|
+
|
|
123
|
+
// OSC sequences only: ESC ] ... ST (non-greedy until the first ST)
|
|
124
|
+
const osc = `(?:\\u001B\\][\\s\\S]*?${ST})`;
|
|
125
|
+
|
|
126
|
+
// CSI and related: ESC/C1, optional intermediates, optional params (supports ; and :) then final byte
|
|
127
|
+
const csi = "[\\u001B\\u009B][[\\]()#;?]*(?:\\d{1,4}(?:[;:]\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]";
|
|
128
|
+
|
|
129
|
+
const pattern = `${osc}|${csi}`;
|
|
130
|
+
|
|
131
|
+
return new RegExp(pattern, onlyFirst ? undefined : "g");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const stripAnsiRegex = ansiRegex();
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Strip ANSI escape codes from a string
|
|
138
|
+
* @param string - The string to strip ANSI codes from
|
|
139
|
+
* @returns The string with ANSI codes removed
|
|
140
|
+
*/
|
|
141
|
+
export function stripAnsi(string: string): string {
|
|
142
|
+
if (typeof string !== "string") {
|
|
143
|
+
throw new TypeError(`Expected a \`string\`, got \`${typeof string}\``);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Even though the regex is global, we don't need to reset the `.lastIndex`
|
|
147
|
+
// because unlike `.exec()` and `.test()`, `.replace()` does it automatically
|
|
148
|
+
// and doing it manually has a performance penalty.
|
|
149
|
+
return string.replace(stripAnsiRegex, "");
|
|
150
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { SocketServer } from "@codellm/comms";
|
|
2
|
+
|
|
3
|
+
import { cliContract, createSocketServer } from "@codellm/comms";
|
|
4
|
+
import { product } from "@codellm/product";
|
|
5
|
+
import { implement } from "@orpc/server";
|
|
6
|
+
import { StandardRPCHandler } from "@orpc/server/standard";
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
import * as os from "node:os";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
|
|
11
|
+
export interface EditorState {
|
|
12
|
+
file: string | undefined;
|
|
13
|
+
startLine: number | undefined;
|
|
14
|
+
endLine: number | undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type ToolDecisionValue = "accept" | "reject" | "allowAlways";
|
|
18
|
+
|
|
19
|
+
export class CliPushServer {
|
|
20
|
+
private socketServer: SocketServer | undefined;
|
|
21
|
+
readonly socketPath: string;
|
|
22
|
+
private editorState: EditorState = {
|
|
23
|
+
file: undefined,
|
|
24
|
+
startLine: undefined,
|
|
25
|
+
endLine: undefined,
|
|
26
|
+
};
|
|
27
|
+
private onEditorStateCallback: ((state: EditorState) => void) | null = null;
|
|
28
|
+
private pendingDecisions = new Map<
|
|
29
|
+
string,
|
|
30
|
+
{ resolve: (d: ToolDecisionValue) => void; cleanup: () => void }
|
|
31
|
+
>();
|
|
32
|
+
|
|
33
|
+
constructor() {
|
|
34
|
+
const socketDir = path.join(os.homedir(), product.configDirName, "cli");
|
|
35
|
+
this.socketPath = path.join(socketDir, `${process.pid}.sock`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getEditorState(): EditorState {
|
|
39
|
+
return { ...this.editorState };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
onEditorChange(callback: (state: EditorState) => void): void {
|
|
43
|
+
this.onEditorStateCallback = callback;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
waitForDecision(toolId: string, signal?: AbortSignal): Promise<ToolDecisionValue> {
|
|
47
|
+
return new Promise<ToolDecisionValue>((resolve, reject) => {
|
|
48
|
+
if (signal?.aborted) {
|
|
49
|
+
reject(new Error("Aborted"));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const cleanup = () => {
|
|
54
|
+
signal?.removeEventListener("abort", onAbort);
|
|
55
|
+
this.pendingDecisions.delete(toolId);
|
|
56
|
+
};
|
|
57
|
+
const onAbort = () => {
|
|
58
|
+
cleanup();
|
|
59
|
+
reject(new Error("Aborted"));
|
|
60
|
+
};
|
|
61
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
62
|
+
|
|
63
|
+
this.pendingDecisions.set(toolId, {
|
|
64
|
+
resolve: (d) => {
|
|
65
|
+
cleanup();
|
|
66
|
+
resolve(d);
|
|
67
|
+
},
|
|
68
|
+
cleanup,
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async start(): Promise<void> {
|
|
74
|
+
const socketDir = path.dirname(this.socketPath);
|
|
75
|
+
if (!fs.existsSync(socketDir)) {
|
|
76
|
+
fs.mkdirSync(socketDir, { recursive: true });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const cc = implement(cliContract);
|
|
80
|
+
|
|
81
|
+
type EditorStateInput = { file: string | null; startLine?: number; endLine?: number };
|
|
82
|
+
type ToolDecisionInput = { toolId: string; decision: "accept" | "reject" | "allowAlways" };
|
|
83
|
+
|
|
84
|
+
const router = cc.router({
|
|
85
|
+
editorStateChanged: cc.editorStateChanged.handler(
|
|
86
|
+
async ({ input }: { input: EditorStateInput }) => {
|
|
87
|
+
this.editorState = {
|
|
88
|
+
file: input.file ?? undefined,
|
|
89
|
+
startLine: input.startLine,
|
|
90
|
+
endLine: input.endLine,
|
|
91
|
+
};
|
|
92
|
+
this.onEditorStateCallback?.(this.editorState);
|
|
93
|
+
return { ok: true };
|
|
94
|
+
},
|
|
95
|
+
),
|
|
96
|
+
toolDecision: cc.toolDecision.handler(async ({ input }: { input: ToolDecisionInput }) => {
|
|
97
|
+
const pending = this.pendingDecisions.get(input.toolId);
|
|
98
|
+
if (pending) pending.resolve(input.decision);
|
|
99
|
+
return { ok: true };
|
|
100
|
+
}),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
this.socketServer = await createSocketServer(this.socketPath, new StandardRPCHandler(router));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
dispose(): void {
|
|
107
|
+
for (const [, entry] of this.pendingDecisions) entry.cleanup();
|
|
108
|
+
this.pendingDecisions.clear();
|
|
109
|
+
this.socketServer?.dispose();
|
|
110
|
+
this.socketServer = undefined;
|
|
111
|
+
}
|
|
112
|
+
}
|
package/src/lib/cli.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
|
|
3
|
+
export async function* runCli(command: string, args: string[]): AsyncGenerator<string> {
|
|
4
|
+
const proc = spawn(command, args, {
|
|
5
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
6
|
+
cwd: process.cwd(),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
const streams = [proc.stdout, proc.stderr];
|
|
10
|
+
|
|
11
|
+
const readers = streams.map((stream) => stream[Symbol.asyncIterator]());
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
while (true) {
|
|
15
|
+
const results = await Promise.all(readers.map((reader) => reader.next()));
|
|
16
|
+
let doneCount = 0;
|
|
17
|
+
for (let i = 0; i < results.length; i++) {
|
|
18
|
+
const { value, done } = results[i];
|
|
19
|
+
if (done) {
|
|
20
|
+
doneCount++;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (value) {
|
|
24
|
+
yield value.toString();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (doneCount === readers.length) {
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
} finally {
|
|
32
|
+
// Attempt graceful shutdown
|
|
33
|
+
proc.kill("SIGTERM");
|
|
34
|
+
|
|
35
|
+
const timeout = setTimeout(() => {
|
|
36
|
+
// Force termination if still running
|
|
37
|
+
if (!proc.killed) {
|
|
38
|
+
proc.kill("SIGKILL");
|
|
39
|
+
}
|
|
40
|
+
}, 5000);
|
|
41
|
+
|
|
42
|
+
proc.on("close", () => clearTimeout(timeout));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
// ── macOS screenshot detection ────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
const MACOS_TEMP_SCREENSHOT_PATTERN =
|
|
8
|
+
/\/var\/folders\/.*\/T\/TemporaryItems\/.*screencaptureui.*\//i;
|
|
9
|
+
const SCREENSHOTS_DIR = ".abacusai/screenshots";
|
|
10
|
+
|
|
11
|
+
export function isMacOSTempScreenshot(filePath: string): boolean {
|
|
12
|
+
return MACOS_TEMP_SCREENSHOT_PATTERN.test(filePath);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function copyTempScreenshotToProject(tempPath: string): string | null {
|
|
16
|
+
try {
|
|
17
|
+
const unescaped = tempPath.replace(/\\ /g, " ");
|
|
18
|
+
const cleaned = stripFileProtocol(unescaped);
|
|
19
|
+
|
|
20
|
+
if (!existsSync(cleaned)) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const screenshotsDir = path.resolve(process.cwd(), SCREENSHOTS_DIR);
|
|
25
|
+
if (!existsSync(screenshotsDir)) {
|
|
26
|
+
mkdirSync(screenshotsDir, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const filename = path.basename(cleaned);
|
|
30
|
+
const destPath = path.join(screenshotsDir, filename);
|
|
31
|
+
|
|
32
|
+
copyFileSync(cleaned, destPath);
|
|
33
|
+
return destPath;
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Strips the file:// protocol prefix from a path if present.
|
|
41
|
+
* Also percent-decodes URI-encoded characters (e.g. %20 → space).
|
|
42
|
+
*/
|
|
43
|
+
export function stripFileProtocol(text: string): string {
|
|
44
|
+
if (text.startsWith("file://")) {
|
|
45
|
+
try {
|
|
46
|
+
return decodeURIComponent(text.slice("file://".length));
|
|
47
|
+
} catch {
|
|
48
|
+
return text.slice("file://".length);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return text;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── Linux clipboard tool detection ───────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
let linuxClipboardTool: "wl-paste" | "xclip" | null | undefined;
|
|
57
|
+
|
|
58
|
+
function detectLinuxClipboardTool(): "wl-paste" | "xclip" | null {
|
|
59
|
+
if (linuxClipboardTool !== undefined) return linuxClipboardTool;
|
|
60
|
+
try {
|
|
61
|
+
execFileSync("which", ["wl-paste"], { stdio: "ignore" });
|
|
62
|
+
linuxClipboardTool = "wl-paste";
|
|
63
|
+
return "wl-paste";
|
|
64
|
+
} catch {
|
|
65
|
+
// not found
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
execFileSync("which", ["xclip"], { stdio: "ignore" });
|
|
69
|
+
linuxClipboardTool = "xclip";
|
|
70
|
+
return "xclip";
|
|
71
|
+
} catch {
|
|
72
|
+
// not found
|
|
73
|
+
}
|
|
74
|
+
linuxClipboardTool = null;
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── Clipboard image detection ─────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
export async function clipboardHasImage(): Promise<boolean> {
|
|
81
|
+
try {
|
|
82
|
+
if (process.platform === "darwin") {
|
|
83
|
+
const result = execFileSync("osascript", ["-e", "clipboard info"], {
|
|
84
|
+
encoding: "utf8",
|
|
85
|
+
timeout: 2000,
|
|
86
|
+
});
|
|
87
|
+
return /PNGf|JPEG|TIFF|GIF/i.test(result);
|
|
88
|
+
}
|
|
89
|
+
if (process.platform === "win32") {
|
|
90
|
+
const result = execFileSync(
|
|
91
|
+
"powershell",
|
|
92
|
+
[
|
|
93
|
+
"-NoProfile",
|
|
94
|
+
"-Command",
|
|
95
|
+
"Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.Clipboard]::ContainsImage()",
|
|
96
|
+
],
|
|
97
|
+
{ encoding: "utf8", timeout: 2000 },
|
|
98
|
+
);
|
|
99
|
+
return result.trim().toLowerCase() === "true";
|
|
100
|
+
}
|
|
101
|
+
// Linux
|
|
102
|
+
const tool = detectLinuxClipboardTool();
|
|
103
|
+
if (tool === "wl-paste") {
|
|
104
|
+
const result = execFileSync("wl-paste", ["--list-types"], {
|
|
105
|
+
encoding: "utf8",
|
|
106
|
+
timeout: 2000,
|
|
107
|
+
});
|
|
108
|
+
return result.includes("image/");
|
|
109
|
+
}
|
|
110
|
+
if (tool === "xclip") {
|
|
111
|
+
const result = execFileSync("xclip", ["-selection", "clipboard", "-t", "TARGETS", "-o"], {
|
|
112
|
+
encoding: "utf8",
|
|
113
|
+
timeout: 2000,
|
|
114
|
+
});
|
|
115
|
+
return result.includes("image/");
|
|
116
|
+
}
|
|
117
|
+
} catch {
|
|
118
|
+
// ignore — clipboard tools may not be available
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── Save clipboard image to disk ──────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
export async function saveClipboardImage(cwd: string): Promise<string | null> {
|
|
126
|
+
try {
|
|
127
|
+
const screenshotsDir = path.resolve(cwd, SCREENSHOTS_DIR);
|
|
128
|
+
if (!existsSync(screenshotsDir)) {
|
|
129
|
+
mkdirSync(screenshotsDir, { recursive: true });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const timestamp = Date.now();
|
|
133
|
+
const destPath = path.join(screenshotsDir, `clipboard-${timestamp}.png`);
|
|
134
|
+
|
|
135
|
+
if (process.platform === "darwin") {
|
|
136
|
+
// Try PNG first, then JPEG
|
|
137
|
+
const script = `
|
|
138
|
+
set destFile to "${destPath}"
|
|
139
|
+
try
|
|
140
|
+
set imgData to (clipboard info for «class PNGf»)
|
|
141
|
+
set fileRef to open for access POSIX file destFile with write permission
|
|
142
|
+
set eof of fileRef to 0
|
|
143
|
+
write (clipboard as «class PNGf») to fileRef
|
|
144
|
+
close access fileRef
|
|
145
|
+
on error
|
|
146
|
+
try
|
|
147
|
+
set imgData to (clipboard info for «class JPEG»)
|
|
148
|
+
set fileRef to open for access POSIX file destFile with write permission
|
|
149
|
+
set eof of fileRef to 0
|
|
150
|
+
write (clipboard as «class JPEG») to fileRef
|
|
151
|
+
close access fileRef
|
|
152
|
+
end try
|
|
153
|
+
end try
|
|
154
|
+
`;
|
|
155
|
+
execFileSync("osascript", ["-e", script], { timeout: 5000 });
|
|
156
|
+
} else if (process.platform === "win32") {
|
|
157
|
+
execFileSync(
|
|
158
|
+
"powershell",
|
|
159
|
+
[
|
|
160
|
+
"-NoProfile",
|
|
161
|
+
"-Command",
|
|
162
|
+
`Add-Type -Assembly System.Windows.Forms; [System.Windows.Forms.Clipboard]::GetImage().Save("${destPath}", [System.Drawing.Imaging.ImageFormat]::Png)`,
|
|
163
|
+
],
|
|
164
|
+
{ timeout: 5000 },
|
|
165
|
+
);
|
|
166
|
+
} else {
|
|
167
|
+
const tool = detectLinuxClipboardTool();
|
|
168
|
+
if (tool === "wl-paste") {
|
|
169
|
+
const imgData = execFileSync("wl-paste", ["--no-newline", "--type", "image/png"], {
|
|
170
|
+
timeout: 5000,
|
|
171
|
+
});
|
|
172
|
+
writeFileSync(destPath, imgData);
|
|
173
|
+
} else if (tool === "xclip") {
|
|
174
|
+
const imgData = execFileSync(
|
|
175
|
+
"xclip",
|
|
176
|
+
["-selection", "clipboard", "-t", "image/png", "-o"],
|
|
177
|
+
{ timeout: 5000 },
|
|
178
|
+
);
|
|
179
|
+
writeFileSync(destPath, imgData);
|
|
180
|
+
} else {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Verify file exists and has content (prevents empty-file false positives)
|
|
186
|
+
try {
|
|
187
|
+
const stat = statSync(destPath);
|
|
188
|
+
if (stat.size === 0) return null;
|
|
189
|
+
} catch {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return destPath;
|
|
194
|
+
} catch {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ── Read clipboard text ───────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
export async function readClipboardText(): Promise<string | null> {
|
|
202
|
+
try {
|
|
203
|
+
if (process.platform === "darwin") {
|
|
204
|
+
return execFileSync("pbpaste", { encoding: "utf8", timeout: 2000 });
|
|
205
|
+
}
|
|
206
|
+
if (process.platform === "win32") {
|
|
207
|
+
return execFileSync("powershell", ["-NoProfile", "-Command", "Get-Clipboard"], {
|
|
208
|
+
encoding: "utf8",
|
|
209
|
+
timeout: 2000,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
const tool = detectLinuxClipboardTool();
|
|
213
|
+
if (tool === "wl-paste") {
|
|
214
|
+
return execFileSync("wl-paste", ["--no-newline"], { encoding: "utf8", timeout: 2000 });
|
|
215
|
+
}
|
|
216
|
+
if (tool === "xclip") {
|
|
217
|
+
return execFileSync("xclip", ["-selection", "clipboard", "-o"], {
|
|
218
|
+
encoding: "utf8",
|
|
219
|
+
timeout: 2000,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
} catch {
|
|
223
|
+
// ignore
|
|
224
|
+
}
|
|
225
|
+
return null;
|
|
226
|
+
}
|