@thegitai/cli 1.0.0-beta.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 (101) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +30 -0
  3. package/dist/bin/ai.js +438 -0
  4. package/dist/parsers/tree-sitter-c-sharp.wasm +0 -0
  5. package/dist/parsers/tree-sitter-c.wasm +0 -0
  6. package/dist/parsers/tree-sitter-cpp.wasm +0 -0
  7. package/dist/parsers/tree-sitter-css.wasm +0 -0
  8. package/dist/parsers/tree-sitter-go.wasm +0 -0
  9. package/dist/parsers/tree-sitter-html.wasm +0 -0
  10. package/dist/parsers/tree-sitter-java.wasm +0 -0
  11. package/dist/parsers/tree-sitter-javascript.wasm +0 -0
  12. package/dist/parsers/tree-sitter-objc.wasm +0 -0
  13. package/dist/parsers/tree-sitter-php.wasm +0 -0
  14. package/dist/parsers/tree-sitter-python.wasm +0 -0
  15. package/dist/parsers/tree-sitter-ruby.wasm +0 -0
  16. package/dist/parsers/tree-sitter-rust.wasm +0 -0
  17. package/dist/parsers/tree-sitter-tsx.wasm +0 -0
  18. package/dist/parsers/tree-sitter-typescript.wasm +0 -0
  19. package/dist/src/agent-mode.js +142 -0
  20. package/dist/src/api/auth.js +81 -0
  21. package/dist/src/api/browser-login.js +184 -0
  22. package/dist/src/api/chat.js +346 -0
  23. package/dist/src/api/contracts.js +1 -0
  24. package/dist/src/api/http.js +44 -0
  25. package/dist/src/api/index.js +11 -0
  26. package/dist/src/api/models.js +110 -0
  27. package/dist/src/api/sessions.js +72 -0
  28. package/dist/src/artifact-policy.js +207 -0
  29. package/dist/src/client-state.js +14 -0
  30. package/dist/src/core/clipboard.js +208 -0
  31. package/dist/src/core/open-url.js +32 -0
  32. package/dist/src/edit-journal.js +133 -0
  33. package/dist/src/executor.js +924 -0
  34. package/dist/src/extractors/cpp.js +18 -0
  35. package/dist/src/extractors/csharp.js +16 -0
  36. package/dist/src/extractors/css.js +12 -0
  37. package/dist/src/extractors/go.js +27 -0
  38. package/dist/src/extractors/index.js +52 -0
  39. package/dist/src/extractors/java.js +14 -0
  40. package/dist/src/extractors/javascript.js +33 -0
  41. package/dist/src/extractors/objc.js +14 -0
  42. package/dist/src/extractors/php.js +20 -0
  43. package/dist/src/extractors/python.js +11 -0
  44. package/dist/src/extractors/ruby.js +13 -0
  45. package/dist/src/extractors/rust.js +17 -0
  46. package/dist/src/extractors/utils.js +58 -0
  47. package/dist/src/help-text.js +125 -0
  48. package/dist/src/markdown-renderer.js +112 -0
  49. package/dist/src/patcher.js +279 -0
  50. package/dist/src/project-index.js +221 -0
  51. package/dist/src/repo-map-languages.js +100 -0
  52. package/dist/src/runtime-mode.js +35 -0
  53. package/dist/src/scanner.js +362 -0
  54. package/dist/src/secret-preview.js +137 -0
  55. package/dist/src/session-exit.js +17 -0
  56. package/dist/src/session-safety.js +1012 -0
  57. package/dist/src/session-store.js +266 -0
  58. package/dist/src/session.js +93 -0
  59. package/dist/src/tool-executor.js +188 -0
  60. package/dist/src/tools/code-intel.js +472 -0
  61. package/dist/src/tools/delete-file.js +27 -0
  62. package/dist/src/tools/exec-utils.js +17 -0
  63. package/dist/src/tools/find-symbol.js +70 -0
  64. package/dist/src/tools/get-diagnostics.js +22 -0
  65. package/dist/src/tools/grep-code.js +331 -0
  66. package/dist/src/tools/hover-symbol.js +95 -0
  67. package/dist/src/tools/index.js +73 -0
  68. package/dist/src/tools/list-checkpoints.js +11 -0
  69. package/dist/src/tools/list-directories.js +16 -0
  70. package/dist/src/tools/list-files.js +13 -0
  71. package/dist/src/tools/list-session-edits.js +9 -0
  72. package/dist/src/tools/list-symbols.js +55 -0
  73. package/dist/src/tools/patch-file.js +88 -0
  74. package/dist/src/tools/path-listing.js +83 -0
  75. package/dist/src/tools/read-document.js +111 -0
  76. package/dist/src/tools/read-file.js +109 -0
  77. package/dist/src/tools/restore-checkpoint.js +100 -0
  78. package/dist/src/tools/ripgrep.js +29 -0
  79. package/dist/src/tools/run-command.js +94 -0
  80. package/dist/src/tools/run-node-script.js +210 -0
  81. package/dist/src/tools/search-code.js +37 -0
  82. package/dist/src/tools/shell-diagnostics.js +707 -0
  83. package/dist/src/tools/signature-help.js +118 -0
  84. package/dist/src/tools/str-replace.js +193 -0
  85. package/dist/src/tools/types.js +1 -0
  86. package/dist/src/tools/undo-edit.js +202 -0
  87. package/dist/src/tools/write-file.js +59 -0
  88. package/dist/src/tree-sitter-runtime.js +135 -0
  89. package/dist/src/types.js +1 -0
  90. package/dist/src/ui/paste-collapse.js +22 -0
  91. package/dist/src/ui/prompt-history-store.js +96 -0
  92. package/dist/src/ui/repl.js +2238 -0
  93. package/dist/src/ui/tui/bridge.js +175 -0
  94. package/dist/src/ui/tui/build-frame.js +718 -0
  95. package/dist/src/ui/tui/markdown-render.js +455 -0
  96. package/dist/src/ui/tui/shell-input.js +488 -0
  97. package/dist/src/ui/tui/text.js +30 -0
  98. package/dist/src/ui/tui/types.js +1 -0
  99. package/dist/src/usage.js +47 -0
  100. package/dist/src/utils.js +38 -0
  101. package/package.json +38 -0
@@ -0,0 +1,110 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { getClientStateDir } from '../client-state.js';
4
+ import { failureMessage, normalizeServerUrl, readJsonResponse, } from './http.js';
5
+ function sanitizeModelInfo(raw) {
6
+ if (!raw || typeof raw !== 'object') {
7
+ return null;
8
+ }
9
+ const value = raw;
10
+ const id = Number(value.id);
11
+ const label = String(value.label ?? '').trim();
12
+ if (!Number.isInteger(id) || id <= 0 || !label) {
13
+ return null;
14
+ }
15
+ return { id, label };
16
+ }
17
+ export function getModelsCachePath(env = process.env) {
18
+ return path.join(getClientStateDir(env), 'models.json');
19
+ }
20
+ export function readCachedServerModels(env = process.env) {
21
+ const filePath = getModelsCachePath(env);
22
+ if (!existsSync(filePath))
23
+ return null;
24
+ try {
25
+ const parsed = JSON.parse(readFileSync(filePath, 'utf8'));
26
+ const serverUrl = String(parsed.serverUrl ?? '').trim();
27
+ const selectedModelId = Number(parsed.selectedModelId);
28
+ const models = Array.isArray(parsed.models)
29
+ ? parsed.models.map(sanitizeModelInfo).filter(Boolean)
30
+ : [];
31
+ if (!serverUrl ||
32
+ !Number.isInteger(selectedModelId) ||
33
+ selectedModelId <= 0 ||
34
+ models.length === 0) {
35
+ return null;
36
+ }
37
+ return {
38
+ serverUrl,
39
+ selectedModelId,
40
+ models,
41
+ updatedAt: String(parsed.updatedAt ?? ''),
42
+ };
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
48
+ export function writeCachedServerModels(cache, env = process.env) {
49
+ const filePath = getModelsCachePath(env);
50
+ mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
51
+ writeFileSync(filePath, `${JSON.stringify(cache, null, 2)}\n`, {
52
+ encoding: 'utf8',
53
+ mode: 0o600,
54
+ });
55
+ }
56
+ export async function fetchServerModels({ config, fetchImpl = globalThis.fetch, }) {
57
+ const response = await fetchImpl(`${normalizeServerUrl(config.serverUrl)}/v1/models`, {
58
+ headers: {
59
+ authorization: `Bearer ${config.token}`,
60
+ },
61
+ });
62
+ const data = (await readJsonResponse(response));
63
+ if (!response.ok) {
64
+ throw new Error(failureMessage(data, response.status));
65
+ }
66
+ const models = Array.isArray(data?.models)
67
+ ? data.models.map(sanitizeModelInfo).filter(Boolean)
68
+ : [];
69
+ if (models.length === 0) {
70
+ throw new Error('Server returned an invalid model list.');
71
+ }
72
+ return {
73
+ models,
74
+ };
75
+ }
76
+ export function selectServerModel({ requestedModelId, cached, serverModels, }) {
77
+ const supportedIds = new Set(serverModels.models.map((model) => model.id));
78
+ const requested = requestedModelId == null ? null : Number(requestedModelId);
79
+ if (requested != null) {
80
+ if (supportedIds.has(requested)) {
81
+ return requested;
82
+ }
83
+ throw new Error(`Server does not support model "${requested}".`);
84
+ }
85
+ for (const candidate of [cached?.selectedModelId, serverModels.models[0]?.id]) {
86
+ const id = Number(candidate);
87
+ if (Number.isInteger(id) && supportedIds.has(id)) {
88
+ return id;
89
+ }
90
+ }
91
+ throw new Error('Server did not advertise a usable model.');
92
+ }
93
+ export function validateServerModel(modelId, serverModels) {
94
+ const requested = Number(modelId);
95
+ if (!Number.isInteger(requested) || requested <= 0) {
96
+ throw new Error('Model id is required.');
97
+ }
98
+ if (!serverModels.models.some((model) => model.id === requested)) {
99
+ throw new Error(`Server does not support model "${requested}".`);
100
+ }
101
+ return requested;
102
+ }
103
+ export function updateSelectedModelCache({ config, selectedModelId, serverModels, env = process.env, }) {
104
+ writeCachedServerModels({
105
+ serverUrl: normalizeServerUrl(config.serverUrl),
106
+ selectedModelId,
107
+ models: serverModels.models,
108
+ updatedAt: new Date().toISOString(),
109
+ }, env);
110
+ }
@@ -0,0 +1,72 @@
1
+ import { createSessionSafetyState } from '../session-safety.js';
2
+ import { getSessionProjectKey, snapshotFromSession, } from '../session-store.js';
3
+ import { authorizedJson } from './http.js';
4
+ function sessionListPath(rootDir) {
5
+ const query = new URLSearchParams({
6
+ projectKey: getSessionProjectKey(rootDir),
7
+ });
8
+ return `/v1/sessions?${query}`;
9
+ }
10
+ function sessionPath(rootDir, identifier) {
11
+ const query = new URLSearchParams({
12
+ projectKey: getSessionProjectKey(rootDir),
13
+ });
14
+ return `/v1/sessions/${encodeURIComponent(identifier)}?${query}`;
15
+ }
16
+ function metadataOnlySnapshot(snapshot) {
17
+ const providerSelection = snapshot.serverState?.providerSelection &&
18
+ typeof snapshot.serverState.providerSelection === 'object' &&
19
+ !Array.isArray(snapshot.serverState.providerSelection)
20
+ ? { providerSelection: snapshot.serverState.providerSelection }
21
+ : {};
22
+ return {
23
+ ...snapshot,
24
+ history: [],
25
+ clientState: {
26
+ editCounter: 0,
27
+ editJournal: [],
28
+ stickyFilePaths: [],
29
+ safety: createSessionSafetyState(),
30
+ },
31
+ serverState: providerSelection,
32
+ };
33
+ }
34
+ export function createServerSessionClient({ config, fetchImpl = globalThis.fetch, }) {
35
+ return {
36
+ async list(rootDir) {
37
+ const data = (await authorizedJson({
38
+ config,
39
+ path: sessionListPath(rootDir),
40
+ fetchImpl,
41
+ }));
42
+ return Array.isArray(data?.sessions) ? data.sessions : [];
43
+ },
44
+ async load(rootDir, identifier) {
45
+ const data = (await authorizedJson({
46
+ config,
47
+ path: sessionPath(rootDir, identifier),
48
+ fetchImpl,
49
+ }));
50
+ const snapshot = data?.session;
51
+ if (!snapshot?.id) {
52
+ throw new Error('Server returned an invalid session.');
53
+ }
54
+ return snapshot;
55
+ },
56
+ async save(session) {
57
+ const snapshot = snapshotFromSession(session);
58
+ const body = {
59
+ messageCount: snapshot.history.length,
60
+ session: metadataOnlySnapshot(snapshot),
61
+ };
62
+ await authorizedJson({
63
+ config,
64
+ path: sessionPath(snapshot.rootDir, snapshot.id),
65
+ method: 'PUT',
66
+ body,
67
+ fetchImpl,
68
+ });
69
+ return snapshot;
70
+ },
71
+ };
72
+ }
@@ -0,0 +1,207 @@
1
+ import path from 'node:path';
2
+ function createArtifactNameSet(values) {
3
+ const lookup = Object.fromEntries(values.map((value) => [value, true]));
4
+ return {
5
+ has: (value) => lookup[value] === true,
6
+ *[Symbol.iterator]() {
7
+ yield* values;
8
+ },
9
+ };
10
+ }
11
+ export const ARTIFACT_IGNORE_FILES = createArtifactNameSet([
12
+ 'package-lock.json',
13
+ 'yarn.lock',
14
+ 'pnpm-lock.yaml',
15
+ 'bun.lockb',
16
+ 'Cargo.lock.bak',
17
+ ]);
18
+ export const BINARY_ARTIFACT_EXTENSIONS = createArtifactNameSet([
19
+ '.png',
20
+ '.jpg',
21
+ '.jpeg',
22
+ '.gif',
23
+ '.ico',
24
+ '.webp',
25
+ '.avif',
26
+ '.woff',
27
+ '.woff2',
28
+ '.ttf',
29
+ '.eot',
30
+ '.mp4',
31
+ '.mp3',
32
+ '.wav',
33
+ '.ogg',
34
+ '.zip',
35
+ '.tar',
36
+ '.gz',
37
+ '.bz2',
38
+ '.7z',
39
+ '.pdf',
40
+ '.exe',
41
+ '.dll',
42
+ '.so',
43
+ '.dylib',
44
+ '.bin',
45
+ '.dat',
46
+ '.db',
47
+ '.sqlite',
48
+ '.map',
49
+ '.min.js',
50
+ '.min.css',
51
+ ]);
52
+ const REPO_METADATA_DIRS = [
53
+ '.git',
54
+ '.thegitai',
55
+ '.thegitai-debug',
56
+ ];
57
+ const EDITOR_METADATA_DIRS = [
58
+ '.idea',
59
+ '.vscode',
60
+ '.fleet',
61
+ '.zed',
62
+ '.history',
63
+ '.vs',
64
+ ];
65
+ const PACKAGE_DEPENDENCY_DIRS = [
66
+ 'node_modules',
67
+ 'vendor',
68
+ '.venv',
69
+ 'venv',
70
+ 'env',
71
+ ];
72
+ const GENERATED_OUTPUT_DIRS = [
73
+ '.next',
74
+ '.nuxt',
75
+ '.output',
76
+ '.vercel',
77
+ '.vite',
78
+ '.svelte-kit',
79
+ '.angular',
80
+ '.astro',
81
+ '.expo',
82
+ '.parcel-cache',
83
+ '.serverless',
84
+ '.build',
85
+ 'DerivedData',
86
+ 'dist',
87
+ 'build',
88
+ 'out',
89
+ 'target',
90
+ 'coverage',
91
+ 'tmp',
92
+ 'temp',
93
+ 'obj',
94
+ ];
95
+ const TOOL_CACHE_DIRS = [
96
+ '.turbo',
97
+ '.yarn',
98
+ '.pnpm-store',
99
+ '.npm',
100
+ '.gradle',
101
+ '.terraform',
102
+ '.mypy_cache',
103
+ '.pytest_cache',
104
+ '.ruff_cache',
105
+ '.sass-cache',
106
+ '.cache',
107
+ '.tox',
108
+ '.nox',
109
+ '.dart_tool',
110
+ '__pycache__',
111
+ ];
112
+ export const ARTIFACT_IGNORE_DIRS = createArtifactNameSet([
113
+ ...REPO_METADATA_DIRS,
114
+ ...EDITOR_METADATA_DIRS,
115
+ ...PACKAGE_DEPENDENCY_DIRS,
116
+ ...GENERATED_OUTPUT_DIRS,
117
+ ...TOOL_CACHE_DIRS,
118
+ ]);
119
+ export const ARTIFACT_INSPECT_BLOCK_DIRS = ARTIFACT_IGNORE_DIRS;
120
+ export const ARTIFACT_IGNORE_PATH_PREFIXES = [
121
+ 'storage/framework/cache',
122
+ 'storage/framework/sessions',
123
+ 'storage/framework/views',
124
+ 'storage/logs',
125
+ 'bootstrap/cache',
126
+ 'var/cache',
127
+ 'var/log',
128
+ ];
129
+ export const ARTIFACT_FALLBACK_IGNORE_GLOBS = [
130
+ ...[...ARTIFACT_IGNORE_DIRS].flatMap((dir) => [
131
+ `${dir}/**`,
132
+ `**/${dir}/**`,
133
+ ]),
134
+ '.env',
135
+ '.env.*',
136
+ '**/*.lock',
137
+ '**/package-lock.json',
138
+ '**/yarn.lock',
139
+ '**/pnpm-lock.yaml',
140
+ ...ARTIFACT_IGNORE_PATH_PREFIXES.map((prefix) => `${prefix}/**`),
141
+ ];
142
+ const SENSITIVE_BASENAME_PATTERNS = [
143
+ /^\.env(?:\..+)?$/i,
144
+ /^\.?npmrc$/i,
145
+ /^\.?pypirc$/i,
146
+ /^credentials(?:\..*)?$/i,
147
+ /^secrets?(?:\..*)?$/i,
148
+ ];
149
+ const SENSITIVE_PATH_PATTERNS = [
150
+ /(^|[/\\])\.aws[/\\]credentials$/i,
151
+ /(^|[/\\])\.config[/\\]gcloud[/\\]/i,
152
+ /(^|[/\\])credentials?([._-]|$)/i,
153
+ /(^|[/\\])secrets?([._-]|$)/i,
154
+ /(^|[/\\])private[-_]?key([._-]|$)/i,
155
+ /\.(?:pem|key|p12|pfx)$/i,
156
+ ];
157
+ export function normalizeArtifactPath(relPath) {
158
+ return String(relPath ?? '')
159
+ .split(/[\\/]+/)
160
+ .filter(Boolean)
161
+ .join('/');
162
+ }
163
+ export function getIgnoredArtifactDir(relPath) {
164
+ const parts = normalizeArtifactPath(relPath).split('/').filter(Boolean);
165
+ return parts.find((part) => ARTIFACT_IGNORE_DIRS.has(part)) ?? null;
166
+ }
167
+ export function getBlockedArtifactInspectDir(relPath) {
168
+ const parts = normalizeArtifactPath(relPath).split('/').filter(Boolean);
169
+ return parts.find((part) => ARTIFACT_INSPECT_BLOCK_DIRS.has(part)) ?? null;
170
+ }
171
+ export function matchesArtifactIgnorePrefix(relPath) {
172
+ const normalized = normalizeArtifactPath(relPath);
173
+ for (const prefix of ARTIFACT_IGNORE_PATH_PREFIXES) {
174
+ if (normalized === prefix || normalized.startsWith(`${prefix}/`)) {
175
+ return true;
176
+ }
177
+ }
178
+ return false;
179
+ }
180
+ export function shouldIgnoreArtifactPath(relPath) {
181
+ return (getIgnoredArtifactDir(relPath) !== null ||
182
+ matchesArtifactIgnorePrefix(relPath));
183
+ }
184
+ export function isSensitiveProjectPath(value) {
185
+ const normalized = normalizeArtifactPath(String(value ?? '').trim());
186
+ if (!normalized)
187
+ return false;
188
+ const base = path.posix.basename(normalized);
189
+ return (SENSITIVE_BASENAME_PATTERNS.some((pattern) => pattern.test(base)) ||
190
+ SENSITIVE_PATH_PATTERNS.some((pattern) => pattern.test(normalized)));
191
+ }
192
+ export function relativeProjectPath(rootDir, absPath) {
193
+ const relPath = path.relative(path.resolve(rootDir), path.resolve(absPath));
194
+ if (!relPath ||
195
+ relPath === '.' ||
196
+ relPath.startsWith('..') ||
197
+ path.isAbsolute(relPath)) {
198
+ return null;
199
+ }
200
+ return relPath;
201
+ }
202
+ export function normalizeProjectRelativePath(rootDir, targetPath) {
203
+ const absPath = path.isAbsolute(targetPath)
204
+ ? targetPath
205
+ : path.join(rootDir, targetPath);
206
+ return relativeProjectPath(rootDir, absPath);
207
+ }
@@ -0,0 +1,14 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ const STORE_DIR = 'thegitai';
4
+ export function getClientStateDir(env = process.env) {
5
+ const configured = String(env.THEGITAI_STATE_DIR ?? env.THEGITAI_SESSION_DIR ?? '').trim();
6
+ if (configured) {
7
+ return path.resolve(configured);
8
+ }
9
+ if (process.platform === 'win32') {
10
+ return path.join(env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'), STORE_DIR);
11
+ }
12
+ const stateHome = String(env.XDG_STATE_HOME ?? '').trim();
13
+ return path.join(stateHome || path.join(os.homedir(), '.local', 'state'), STORE_DIR);
14
+ }
@@ -0,0 +1,208 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ const MAX_IMAGE_SIZE_BYTES = 10 * 1024 * 1024;
3
+ const MIME_BY_EXT = {
4
+ '.png': 'image/png',
5
+ '.jpg': 'image/jpeg',
6
+ '.jpeg': 'image/jpeg',
7
+ '.gif': 'image/gif',
8
+ '.webp': 'image/webp',
9
+ };
10
+ const SUPPORTED_MIME_TYPES = new Set(Object.values(MIME_BY_EXT));
11
+ export class ClipboardError extends Error {
12
+ code;
13
+ constructor(message, code) {
14
+ super(message);
15
+ this.code = code;
16
+ this.name = 'ClipboardError';
17
+ }
18
+ }
19
+ export function isSupportedImageMimeType(mime) {
20
+ return SUPPORTED_MIME_TYPES.has(mime);
21
+ }
22
+ function whichSync(cmd) {
23
+ try {
24
+ execFileSync('which', [cmd], { stdio: 'ignore', timeout: 2000 });
25
+ return true;
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ }
31
+ function isWayland() {
32
+ return Boolean(process.env.WAYLAND_DISPLAY ||
33
+ process.env.XDG_SESSION_TYPE === 'wayland');
34
+ }
35
+ function isMaxBufferError(err) {
36
+ return (err?.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER' ||
37
+ /maxBuffer/.test(String(err?.message ?? '')));
38
+ }
39
+ function tryReadWithXclip() {
40
+ if (!whichSync('xclip'))
41
+ return null;
42
+ try {
43
+ const buf = execFileSync('xclip', ['-selection', 'clipboard', '-t', 'image/png', '-o'], {
44
+ timeout: 5000,
45
+ maxBuffer: MAX_IMAGE_SIZE_BYTES,
46
+ stdio: ['ignore', 'pipe', 'ignore'],
47
+ });
48
+ if (buf.length) {
49
+ return { base64Data: buf.toString('base64'), mimeType: 'image/png' };
50
+ }
51
+ }
52
+ catch (err) {
53
+ if (isMaxBufferError(err)) {
54
+ throw new ClipboardError('Clipboard image exceeds 10MB size limit.', 'READ_FAILED');
55
+ }
56
+ }
57
+ return null;
58
+ }
59
+ function tryReadWithWlPaste() {
60
+ if (!whichSync('wl-paste'))
61
+ return null;
62
+ try {
63
+ const buf = execFileSync('wl-paste', ['--type', 'image/png'], {
64
+ timeout: 5000,
65
+ maxBuffer: MAX_IMAGE_SIZE_BYTES,
66
+ stdio: ['ignore', 'pipe', 'ignore'],
67
+ });
68
+ if (buf.length) {
69
+ return { base64Data: buf.toString('base64'), mimeType: 'image/png' };
70
+ }
71
+ }
72
+ catch (err) {
73
+ if (isMaxBufferError(err)) {
74
+ throw new ClipboardError('Clipboard image exceeds 10MB size limit.', 'READ_FAILED');
75
+ }
76
+ }
77
+ return null;
78
+ }
79
+ function readClipboardLinux() {
80
+ if (isWayland()) {
81
+ const wlResult = tryReadWithWlPaste();
82
+ if (wlResult)
83
+ return wlResult;
84
+ }
85
+ const xclipResult = tryReadWithXclip();
86
+ if (xclipResult)
87
+ return xclipResult;
88
+ if (!isWayland() && !whichSync('xclip')) {
89
+ throw new ClipboardError('xclip is required for clipboard image paste on X11. Install xclip.', 'NO_TOOL');
90
+ }
91
+ if (isWayland() && !whichSync('wl-paste') && !whichSync('xclip')) {
92
+ throw new ClipboardError('wl-paste or xclip is required for clipboard image paste. Install wl-clipboard or xclip.', 'NO_TOOL');
93
+ }
94
+ throw new ClipboardError('Clipboard contains no image data.', 'NO_IMAGE');
95
+ }
96
+ function readClipboardDarwin() {
97
+ if (whichSync('pngpaste')) {
98
+ try {
99
+ const buf = execFileSync('pngpaste', ['-'], {
100
+ timeout: 5000,
101
+ maxBuffer: MAX_IMAGE_SIZE_BYTES,
102
+ stdio: ['ignore', 'pipe', 'ignore'],
103
+ });
104
+ if (!buf.length) {
105
+ throw new ClipboardError('Clipboard contains no image data.', 'NO_IMAGE');
106
+ }
107
+ return { base64Data: buf.toString('base64'), mimeType: 'image/png' };
108
+ }
109
+ catch (err) {
110
+ if (err instanceof ClipboardError)
111
+ throw err;
112
+ if (isMaxBufferError(err)) {
113
+ throw new ClipboardError('Clipboard image exceeds 10MB size limit.', 'READ_FAILED');
114
+ }
115
+ throw new ClipboardError('Clipboard contains no image data.', 'NO_IMAGE');
116
+ }
117
+ }
118
+ throw new ClipboardError('pngpaste is required for clipboard image paste on macOS. Install via: brew install pngpaste', 'NO_TOOL');
119
+ }
120
+ export function readClipboardImage(platform = process.platform) {
121
+ switch (platform) {
122
+ case 'linux':
123
+ return readClipboardLinux();
124
+ case 'darwin':
125
+ return readClipboardDarwin();
126
+ default:
127
+ throw new ClipboardError(`Clipboard image paste is not supported on ${platform}.`, 'NO_TOOL');
128
+ }
129
+ }
130
+ export function readClipboardText(platform = process.platform) {
131
+ try {
132
+ if (platform === 'darwin') {
133
+ return normalizeClipboardTextOutput(execFileSync('pbpaste', [], {
134
+ encoding: 'utf-8',
135
+ timeout: 2000,
136
+ stdio: ['ignore', 'pipe', 'ignore'],
137
+ }));
138
+ }
139
+ if (platform === 'win32') {
140
+ return normalizeClipboardTextOutput(execFileSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', 'Get-Clipboard -Raw'], {
141
+ encoding: 'utf-8',
142
+ timeout: 2000,
143
+ stdio: ['ignore', 'pipe', 'ignore'],
144
+ }));
145
+ }
146
+ if (platform === 'linux') {
147
+ if (isWayland() && whichSync('wl-paste')) {
148
+ return normalizeClipboardTextOutput(execFileSync('wl-paste', ['--type', 'text/plain'], {
149
+ encoding: 'utf-8',
150
+ timeout: 2000,
151
+ stdio: ['ignore', 'pipe', 'ignore'],
152
+ }));
153
+ }
154
+ if (whichSync('xclip')) {
155
+ return normalizeClipboardTextOutput(execFileSync('xclip', ['-selection', 'clipboard', '-o'], {
156
+ encoding: 'utf-8',
157
+ timeout: 2000,
158
+ stdio: ['ignore', 'pipe', 'ignore'],
159
+ }));
160
+ }
161
+ }
162
+ }
163
+ catch {
164
+ return '';
165
+ }
166
+ return '';
167
+ }
168
+ function normalizeClipboardTextOutput(text) {
169
+ return text.replace(/\r?\n$/, '');
170
+ }
171
+ function tryWriteClipboardCommand(cmd, args, text) {
172
+ if (!whichSync(cmd))
173
+ return false;
174
+ try {
175
+ execFileSync(cmd, args, {
176
+ input: text,
177
+ timeout: 2000,
178
+ stdio: ['pipe', 'ignore', 'ignore'],
179
+ });
180
+ return true;
181
+ }
182
+ catch (error) {
183
+ throw new ClipboardError(`Failed to write clipboard with ${cmd}: ${error.message}`, 'WRITE_FAILED');
184
+ }
185
+ }
186
+ export function writeClipboardText(text, platform = process.platform) {
187
+ const value = String(text ?? '');
188
+ if (platform === 'darwin') {
189
+ if (tryWriteClipboardCommand('pbcopy', [], value))
190
+ return;
191
+ throw new ClipboardError('pbcopy is required to copy text on macOS.', 'NO_TOOL');
192
+ }
193
+ if (platform === 'win32') {
194
+ if (tryWriteClipboardCommand('clip.exe', [], value))
195
+ return;
196
+ throw new ClipboardError('clip.exe is required to copy text on Windows.', 'NO_TOOL');
197
+ }
198
+ if (platform === 'linux') {
199
+ if (isWayland() && tryWriteClipboardCommand('wl-copy', [], value))
200
+ return;
201
+ if (tryWriteClipboardCommand('xclip', ['-selection', 'clipboard'], value))
202
+ return;
203
+ if (tryWriteClipboardCommand('xsel', ['--clipboard', '--input'], value))
204
+ return;
205
+ throw new ClipboardError('wl-copy, xclip, or xsel is required to copy text. Install wl-clipboard, xclip, or xsel.', 'NO_TOOL');
206
+ }
207
+ throw new ClipboardError(`Clipboard text copy is not supported on ${platform}.`, 'NO_TOOL');
208
+ }
@@ -0,0 +1,32 @@
1
+ import { spawn } from 'node:child_process';
2
+ export function resolveOpenUrlCommand(url, platform = process.platform) {
3
+ const parsed = new URL(url);
4
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
5
+ throw new Error('Only http:// and https:// links can be opened.');
6
+ }
7
+ const href = parsed.href;
8
+ if (platform === 'darwin') {
9
+ return { command: 'open', args: [href] };
10
+ }
11
+ if (platform === 'win32') {
12
+ return { command: 'rundll32', args: ['url.dll,FileProtocolHandler', href] };
13
+ }
14
+ return { command: 'xdg-open', args: [href] };
15
+ }
16
+ export async function openUrl(url, platform = process.platform) {
17
+ const { command, args } = resolveOpenUrlCommand(url, platform);
18
+ return await new Promise((resolve) => {
19
+ const child = spawn(command, args, {
20
+ stdio: 'ignore',
21
+ });
22
+ let settled = false;
23
+ const finish = (opened) => {
24
+ if (settled)
25
+ return;
26
+ settled = true;
27
+ resolve(opened);
28
+ };
29
+ child.once('error', () => finish(false));
30
+ child.once('exit', (code) => finish(code === 0));
31
+ });
32
+ }