zerg-ztc 0.1.10 → 0.1.12

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 (151) hide show
  1. package/bin/.gitkeep +0 -0
  2. package/bin/ztc-audio-darwin-arm64 +0 -0
  3. package/dist/App.d.ts.map +1 -1
  4. package/dist/App.js +63 -2
  5. package/dist/App.js.map +1 -1
  6. package/dist/agent/commands/dictation.d.ts +3 -0
  7. package/dist/agent/commands/dictation.d.ts.map +1 -0
  8. package/dist/agent/commands/dictation.js +10 -0
  9. package/dist/agent/commands/dictation.js.map +1 -0
  10. package/dist/agent/commands/index.d.ts.map +1 -1
  11. package/dist/agent/commands/index.js +2 -1
  12. package/dist/agent/commands/index.js.map +1 -1
  13. package/dist/agent/commands/types.d.ts +7 -0
  14. package/dist/agent/commands/types.d.ts.map +1 -1
  15. package/dist/components/InputArea.d.ts +1 -0
  16. package/dist/components/InputArea.d.ts.map +1 -1
  17. package/dist/components/InputArea.js +591 -43
  18. package/dist/components/InputArea.js.map +1 -1
  19. package/dist/components/SingleMessage.d.ts.map +1 -1
  20. package/dist/components/SingleMessage.js +157 -7
  21. package/dist/components/SingleMessage.js.map +1 -1
  22. package/dist/config/types.d.ts +6 -0
  23. package/dist/config/types.d.ts.map +1 -1
  24. package/dist/ui/views/status_bar.js +2 -2
  25. package/dist/ui/views/status_bar.js.map +1 -1
  26. package/dist/utils/dictation.d.ts +46 -0
  27. package/dist/utils/dictation.d.ts.map +1 -0
  28. package/dist/utils/dictation.js +409 -0
  29. package/dist/utils/dictation.js.map +1 -0
  30. package/dist/utils/dictation_native.d.ts +51 -0
  31. package/dist/utils/dictation_native.d.ts.map +1 -0
  32. package/dist/utils/dictation_native.js +236 -0
  33. package/dist/utils/dictation_native.js.map +1 -0
  34. package/dist/utils/path_format.d.ts +20 -0
  35. package/dist/utils/path_format.d.ts.map +1 -0
  36. package/dist/utils/path_format.js +90 -0
  37. package/dist/utils/path_format.js.map +1 -0
  38. package/dist/utils/table.d.ts +38 -0
  39. package/dist/utils/table.d.ts.map +1 -0
  40. package/dist/utils/table.js +133 -0
  41. package/dist/utils/table.js.map +1 -0
  42. package/dist/utils/tool_trace.d.ts +7 -2
  43. package/dist/utils/tool_trace.d.ts.map +1 -1
  44. package/dist/utils/tool_trace.js +156 -51
  45. package/dist/utils/tool_trace.js.map +1 -1
  46. package/package.json +5 -1
  47. package/src/App.tsx +0 -813
  48. package/src/agent/agent.ts +0 -534
  49. package/src/agent/backends/anthropic.ts +0 -86
  50. package/src/agent/backends/gemini.ts +0 -119
  51. package/src/agent/backends/inception.ts +0 -23
  52. package/src/agent/backends/index.ts +0 -17
  53. package/src/agent/backends/openai.ts +0 -23
  54. package/src/agent/backends/openai_compatible.ts +0 -143
  55. package/src/agent/backends/types.ts +0 -83
  56. package/src/agent/commands/clipboard.ts +0 -77
  57. package/src/agent/commands/config.ts +0 -204
  58. package/src/agent/commands/debug.ts +0 -23
  59. package/src/agent/commands/emulation.ts +0 -80
  60. package/src/agent/commands/execution.ts +0 -9
  61. package/src/agent/commands/help.ts +0 -20
  62. package/src/agent/commands/history.ts +0 -13
  63. package/src/agent/commands/index.ts +0 -46
  64. package/src/agent/commands/input_mode.ts +0 -22
  65. package/src/agent/commands/keybindings.ts +0 -40
  66. package/src/agent/commands/model.ts +0 -11
  67. package/src/agent/commands/models.ts +0 -116
  68. package/src/agent/commands/permissions.ts +0 -64
  69. package/src/agent/commands/retry.ts +0 -9
  70. package/src/agent/commands/shell.ts +0 -68
  71. package/src/agent/commands/skills.ts +0 -54
  72. package/src/agent/commands/status.ts +0 -19
  73. package/src/agent/commands/types.ts +0 -80
  74. package/src/agent/commands/update.ts +0 -32
  75. package/src/agent/factory.ts +0 -60
  76. package/src/agent/index.ts +0 -20
  77. package/src/agent/runtime/capabilities.ts +0 -7
  78. package/src/agent/runtime/memory.ts +0 -23
  79. package/src/agent/runtime/policy.ts +0 -48
  80. package/src/agent/runtime/session.ts +0 -18
  81. package/src/agent/runtime/tracing.ts +0 -23
  82. package/src/agent/tools/file.ts +0 -178
  83. package/src/agent/tools/index.ts +0 -52
  84. package/src/agent/tools/screenshot.ts +0 -821
  85. package/src/agent/tools/search.ts +0 -138
  86. package/src/agent/tools/shell.ts +0 -69
  87. package/src/agent/tools/skills.ts +0 -28
  88. package/src/agent/tools/types.ts +0 -14
  89. package/src/agent/tools/zerg.ts +0 -50
  90. package/src/cli.tsx +0 -163
  91. package/src/components/ActivityLine.tsx +0 -23
  92. package/src/components/FullScreen.tsx +0 -79
  93. package/src/components/Header.tsx +0 -27
  94. package/src/components/InputArea.tsx +0 -1096
  95. package/src/components/MessageList.tsx +0 -71
  96. package/src/components/SingleMessage.tsx +0 -59
  97. package/src/components/StatusBar.tsx +0 -55
  98. package/src/components/index.tsx +0 -8
  99. package/src/config/types.ts +0 -12
  100. package/src/config.ts +0 -186
  101. package/src/debug/logger.ts +0 -14
  102. package/src/emulation/README.md +0 -24
  103. package/src/emulation/catalog.ts +0 -82
  104. package/src/emulation/trace_style.ts +0 -8
  105. package/src/emulation/types.ts +0 -7
  106. package/src/skills/index.ts +0 -36
  107. package/src/skills/loader.ts +0 -135
  108. package/src/skills/registry.ts +0 -6
  109. package/src/skills/types.ts +0 -10
  110. package/src/types.ts +0 -84
  111. package/src/ui/README.md +0 -44
  112. package/src/ui/core/factory.ts +0 -9
  113. package/src/ui/core/index.ts +0 -4
  114. package/src/ui/core/input.ts +0 -38
  115. package/src/ui/core/input_segments.ts +0 -410
  116. package/src/ui/core/input_state.ts +0 -17
  117. package/src/ui/core/layout_yoga.ts +0 -122
  118. package/src/ui/core/style.ts +0 -38
  119. package/src/ui/core/types.ts +0 -54
  120. package/src/ui/ink/index.tsx +0 -1
  121. package/src/ui/ink/render.tsx +0 -60
  122. package/src/ui/views/activity_line.ts +0 -33
  123. package/src/ui/views/app.ts +0 -111
  124. package/src/ui/views/header.ts +0 -44
  125. package/src/ui/views/input_area.ts +0 -255
  126. package/src/ui/views/message_list.ts +0 -443
  127. package/src/ui/views/status_bar.ts +0 -114
  128. package/src/ui/vue/index.ts +0 -53
  129. package/src/ui/web/frame_render.tsx +0 -148
  130. package/src/ui/web/index.tsx +0 -1
  131. package/src/ui/web/render.tsx +0 -41
  132. package/src/utils/clipboard.ts +0 -39
  133. package/src/utils/clipboard_image.ts +0 -40
  134. package/src/utils/diff.ts +0 -52
  135. package/src/utils/image_preview.ts +0 -36
  136. package/src/utils/models.ts +0 -98
  137. package/src/utils/path_complete.ts +0 -173
  138. package/src/utils/shell.ts +0 -72
  139. package/src/utils/spinner_frames.ts +0 -1
  140. package/src/utils/spinner_verbs.ts +0 -23
  141. package/src/utils/tool_summary.ts +0 -56
  142. package/src/utils/tool_trace.ts +0 -216
  143. package/src/utils/update.ts +0 -44
  144. package/src/utils/version.ts +0 -15
  145. package/src/web/index.html +0 -352
  146. package/src/web/mirror-favicon.svg +0 -4
  147. package/src/web/mirror.html +0 -641
  148. package/src/web/mirror_hook.ts +0 -25
  149. package/src/web/mirror_server.ts +0 -204
  150. package/tsconfig.json +0 -22
  151. package/vite.config.ts +0 -363
@@ -1,148 +0,0 @@
1
- import React, { useMemo } from 'react';
2
- import { LayoutNode, LayoutFrame, computeLayout } from '../core/index.js';
3
-
4
- type BadgeInfo = {
5
- type: 'paste' | 'file' | 'image';
6
- preview: string;
7
- full: string;
8
- path?: string;
9
- };
10
-
11
- interface FrameRendererProps {
12
- node: LayoutNode;
13
- cols: number;
14
- rows: number;
15
- cellWidth: number;
16
- cellHeight: number;
17
- debugBorders?: boolean;
18
- onBadgeHover?: (badge: BadgeInfo, x: number, y: number) => void;
19
- onBadgeLeave?: () => void;
20
- onBadgeClick?: (badge: BadgeInfo, x: number, y: number) => void;
21
- onBadgeDblClick?: (badge: BadgeInfo, x: number, y: number) => void;
22
- }
23
-
24
- function colorMap(color?: string): string {
25
- const map: Record<string, string> = {
26
- gray: '#9a9a9a',
27
- magenta: '#c084fc',
28
- yellow: '#facc15',
29
- green: '#34d399',
30
- red: '#f87171',
31
- cyan: '#67e8f9',
32
- blue: '#60a5fa',
33
- white: '#e5e7eb'
34
- };
35
- return map[color || ''] || color || '#e6e6e6';
36
- }
37
-
38
- function renderFrame(
39
- frame: LayoutFrame,
40
- keyPrefix: string,
41
- cellWidth: number,
42
- cellHeight: number,
43
- debugBorders: boolean,
44
- onBadgeHover?: (badge: BadgeInfo, x: number, y: number) => void,
45
- onBadgeLeave?: () => void,
46
- onBadgeClick?: (badge: BadgeInfo, x: number, y: number) => void,
47
- onBadgeDblClick?: (badge: BadgeInfo, x: number, y: number) => void,
48
- offsetX = 0,
49
- offsetY = 0,
50
- out: React.ReactElement[] = []
51
- ): React.ReactElement[] {
52
- const x = offsetX + frame.x;
53
- const y = offsetY + frame.y;
54
- const keyBase = `${keyPrefix}-${x}-${y}`;
55
-
56
- if (frame.node.type === 'text') {
57
- const style = frame.node.style || {};
58
- if (debugBorders) {
59
- out.push(
60
- <div
61
- key={`${keyBase}-debug`}
62
- style={{
63
- position: 'absolute',
64
- left: x * cellWidth,
65
- top: y * cellHeight,
66
- width: frame.width * cellWidth,
67
- height: frame.height * cellHeight,
68
- borderStyle: 'dashed',
69
- borderWidth: 1,
70
- borderColor: '#3a3a3a',
71
- pointerEvents: 'none'
72
- }}
73
- />
74
- );
75
- }
76
- const badge = style.badge;
77
- out.push(
78
- <span
79
- key={`${keyBase}-text`}
80
- style={{
81
- position: 'absolute',
82
- left: x * cellWidth,
83
- top: y * cellHeight,
84
- color: style.inverse ? '#111' : colorMap(style.color),
85
- fontWeight: style.bold ? 600 : undefined,
86
- opacity: style.dimColor ? 0.6 : undefined,
87
- background: style.inverse ? '#e6e6e6' : undefined,
88
- colorScheme: 'only light',
89
- cursor: badge ? 'pointer' : undefined
90
- }}
91
- title={badge?.preview || undefined}
92
- onMouseEnter={badge ? (evt) => onBadgeHover?.(badge, evt.clientX, evt.clientY) : undefined}
93
- onMouseLeave={badge ? () => onBadgeLeave?.() : undefined}
94
- onClick={badge ? (evt) => onBadgeClick?.(badge, evt.clientX, evt.clientY) : undefined}
95
- onDoubleClick={badge ? (evt) => onBadgeDblClick?.(badge, evt.clientX, evt.clientY) : undefined}
96
- >
97
- {frame.node.text}
98
- </span>
99
- );
100
- return out;
101
- }
102
-
103
- if (frame.node.type === 'box' && (frame.node.style?.borderStyle || debugBorders)) {
104
- out.push(
105
- <div
106
- key={`${keyBase}-border`}
107
- style={{
108
- position: 'absolute',
109
- left: x * cellWidth,
110
- top: y * cellHeight,
111
- width: frame.width * cellWidth,
112
- height: frame.height * cellHeight,
113
- borderStyle: frame.node.style?.borderStyle ? 'solid' : 'dashed',
114
- borderWidth: 1,
115
- borderColor: colorMap(frame.node.style?.borderColor || '#3a3a3a'),
116
- pointerEvents: 'none'
117
- }}
118
- />
119
- );
120
- }
121
-
122
- frame.children.forEach((child, idx) => {
123
- renderFrame(child, `${keyBase}-${idx}`, cellWidth, cellHeight, debugBorders, onBadgeHover, onBadgeLeave, onBadgeClick, onBadgeDblClick, x, y, out);
124
- });
125
-
126
- return out;
127
- }
128
-
129
- export const WebFrameRenderer: React.FC<FrameRendererProps> = ({
130
- node,
131
- cols,
132
- rows,
133
- cellWidth,
134
- cellHeight,
135
- debugBorders = false,
136
- onBadgeHover,
137
- onBadgeLeave,
138
- onBadgeClick,
139
- onBadgeDblClick
140
- }) => {
141
- const frame = useMemo(() => computeLayout(node, cols, rows), [node, cols, rows]);
142
- const elements = useMemo(
143
- () => renderFrame(frame, 'root', cellWidth, cellHeight, debugBorders, onBadgeHover, onBadgeLeave, onBadgeClick, onBadgeDblClick),
144
- [frame, cellWidth, cellHeight, debugBorders, onBadgeHover, onBadgeLeave, onBadgeClick, onBadgeDblClick]
145
- );
146
-
147
- return <>{elements}</>;
148
- };
@@ -1 +0,0 @@
1
- export { WebNode } from './render.js';
@@ -1,41 +0,0 @@
1
- import React from 'react';
2
- import { LayoutNode, TextNode } from '../core/types.js';
3
- import { toCssStyle } from '../core/style.js';
4
-
5
- const monoStyle: React.CSSProperties = {
6
- fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
7
- fontSize: 14,
8
- lineHeight: '16px',
9
- whiteSpace: 'pre'
10
- };
11
-
12
- function renderText(node: TextNode): React.ReactElement {
13
- const style: React.CSSProperties = {
14
- ...monoStyle,
15
- color: node.style?.color,
16
- fontWeight: node.style?.bold ? 600 : undefined,
17
- opacity: node.style?.dimColor ? 0.6 : undefined,
18
- background: node.style?.inverse ? '#111' : undefined,
19
- colorScheme: 'only light'
20
- };
21
- return <span style={style}>{node.text}</span>;
22
- }
23
-
24
- export function WebNode({ node }: { node: LayoutNode }): React.ReactElement {
25
- if (node.type === 'text') {
26
- return renderText(node);
27
- }
28
-
29
- const style = {
30
- ...monoStyle,
31
- ...toCssStyle(node.style)
32
- } as React.CSSProperties;
33
-
34
- return (
35
- <div style={style}>
36
- {node.children.map((child, index) => (
37
- <WebNode key={`${child.type}-${index}`} node={child} />
38
- ))}
39
- </div>
40
- );
41
- }
@@ -1,39 +0,0 @@
1
- import { spawn } from 'child_process';
2
-
3
- function runPipe(command: string, args: string[], input: string): Promise<void> {
4
- return new Promise((resolve, reject) => {
5
- const child = spawn(command, args, { stdio: ['pipe', 'ignore', 'pipe'] });
6
- let err = '';
7
- child.stderr.on('data', chunk => {
8
- err += chunk.toString();
9
- });
10
- child.on('error', reject);
11
- child.on('close', (code) => {
12
- if (code === 0) resolve();
13
- else reject(new Error(err || `Clipboard command failed (${command})`));
14
- });
15
- child.stdin.write(input);
16
- child.stdin.end();
17
- });
18
- }
19
-
20
- export async function writeClipboard(text: string): Promise<void> {
21
- const platform = process.platform;
22
- if (platform === 'darwin') {
23
- await runPipe('pbcopy', [], text);
24
- return;
25
- }
26
- if (platform === 'win32') {
27
- await runPipe('clip', [], text);
28
- return;
29
- }
30
-
31
- try {
32
- await runPipe('wl-copy', [], text);
33
- return;
34
- } catch {
35
- // fall through
36
- }
37
-
38
- await runPipe('xclip', ['-selection', 'clipboard'], text);
39
- }
@@ -1,40 +0,0 @@
1
- import { execFile } from 'child_process';
2
- import { promisify } from 'util';
3
- import { mkdir } from 'fs/promises';
4
- import { homedir } from 'os';
5
- import { join } from 'path';
6
- import { randomUUID } from 'crypto';
7
-
8
- const execFileAsync = promisify(execFile);
9
-
10
- async function hasCommand(command: string): Promise<boolean> {
11
- try {
12
- await execFileAsync('which', [command]);
13
- return true;
14
- } catch {
15
- return false;
16
- }
17
- }
18
-
19
- export async function saveClipboardImage(): Promise<string | null> {
20
- if (process.platform !== 'darwin') {
21
- return null;
22
- }
23
-
24
- const hasPngPaste = await hasCommand('pngpaste');
25
- if (!hasPngPaste) {
26
- return null;
27
- }
28
-
29
- const targetDir = join(homedir(), '.ztc', 'clipboard');
30
- await mkdir(targetDir, { recursive: true });
31
- const filename = `clipboard-${Date.now()}-${randomUUID().slice(0, 8)}.png`;
32
- const targetPath = join(targetDir, filename);
33
-
34
- try {
35
- await execFileAsync('pngpaste', [targetPath]);
36
- return targetPath;
37
- } catch {
38
- return null;
39
- }
40
- }
package/src/utils/diff.ts DELETED
@@ -1,52 +0,0 @@
1
- interface DiffLine {
2
- type: 'context' | 'add' | 'remove';
3
- text: string;
4
- }
5
-
6
- export function buildSimpleDiff(before: string, after: string, maxLines = 120, context = 2): string {
7
- const beforeLines = before.split('\n');
8
- const afterLines = after.split('\n');
9
- const maxLen = Math.max(beforeLines.length, afterLines.length);
10
-
11
- const changes = new Set<number>();
12
- for (let i = 0; i < maxLen; i += 1) {
13
- const b = beforeLines[i];
14
- const a = afterLines[i];
15
- if (b !== a) {
16
- changes.add(i);
17
- }
18
- }
19
-
20
- const include = new Set<number>();
21
- for (const idx of changes) {
22
- for (let i = Math.max(0, idx - context); i <= idx + context; i += 1) {
23
- include.add(i);
24
- }
25
- }
26
-
27
- const lines: DiffLine[] = [];
28
- for (let i = 0; i < maxLen; i += 1) {
29
- if (!include.has(i)) continue;
30
- const b = beforeLines[i];
31
- const a = afterLines[i];
32
- if (b === a) {
33
- lines.push({ type: 'context', text: b || '' });
34
- } else {
35
- if (b !== undefined) lines.push({ type: 'remove', text: b || '' });
36
- if (a !== undefined) lines.push({ type: 'add', text: a || '' });
37
- }
38
- }
39
-
40
- const clipped = lines.slice(0, maxLines);
41
- if (lines.length > maxLines) {
42
- clipped.push({ type: 'context', text: '... diff truncated ...' });
43
- }
44
-
45
- return clipped
46
- .map(line => {
47
- if (line.type === 'add') return `+ ${line.text}`;
48
- if (line.type === 'remove') return `- ${line.text}`;
49
- return ` ${line.text}`;
50
- })
51
- .join('\n');
52
- }
@@ -1,36 +0,0 @@
1
- import { execFile } from 'child_process';
2
- import { promisify } from 'util';
3
-
4
- const execFileAsync = promisify(execFile);
5
- const cache = new Map<string, string[]>();
6
-
7
- async function hasCommand(command: string): Promise<boolean> {
8
- try {
9
- await execFileAsync('which', [command]);
10
- return true;
11
- } catch {
12
- return false;
13
- }
14
- }
15
-
16
- export async function renderImagePreview(path: string, width = 40, height = 20): Promise<string[] | null> {
17
- const key = `${path}:${width}x${height}`;
18
- const cached = cache.get(key);
19
- if (cached) return cached;
20
- if (!(await hasCommand('chafa'))) {
21
- return null;
22
- }
23
- try {
24
- const { stdout } = await execFileAsync('chafa', [
25
- '--colors=none',
26
- '--symbols=block',
27
- `--size=${width}x${height}`,
28
- path
29
- ]);
30
- const lines = stdout.split('\n').filter(line => line.length > 0);
31
- cache.set(key, lines);
32
- return lines;
33
- } catch {
34
- return null;
35
- }
36
- }
@@ -1,98 +0,0 @@
1
- interface ListModelsOptions {
2
- provider: string;
3
- apiKey?: string;
4
- baseUrl?: string;
5
- }
6
-
7
- async function parseList(response: Response): Promise<string[]> {
8
- const data = await response.json() as Record<string, unknown>;
9
- const array =
10
- (Array.isArray(data.data) && data.data) ||
11
- (Array.isArray(data.models) && data.models) ||
12
- (Array.isArray(data.items) && data.items) ||
13
- [];
14
-
15
- const models = array
16
- .map(item => {
17
- if (typeof item === 'string') return item;
18
- if (item && typeof item === 'object') {
19
- const obj = item as Record<string, unknown>;
20
- if (typeof obj.id === 'string') return obj.id;
21
- if (typeof obj.name === 'string') return obj.name;
22
- }
23
- return null;
24
- })
25
- .filter((value): value is string => typeof value === 'string');
26
-
27
- return models;
28
- }
29
-
30
- export async function listModels(options: ListModelsOptions): Promise<string[]> {
31
- const provider = options.provider;
32
- const apiKey = options.apiKey || '';
33
-
34
- if (!apiKey) {
35
- throw new Error(`No API key configured for ${provider}`);
36
- }
37
-
38
- if (provider === 'anthropic') {
39
- const res = await fetch('https://api.anthropic.com/v1/models', {
40
- headers: {
41
- 'x-api-key': apiKey,
42
- 'anthropic-version': '2023-06-01'
43
- }
44
- });
45
- if (!res.ok) {
46
- throw new Error(`Anthropic error (${res.status})`);
47
- }
48
- return parseList(res);
49
- }
50
-
51
- if (provider === 'openai') {
52
- const res = await fetch('https://api.openai.com/v1/models', {
53
- headers: {
54
- Authorization: `Bearer ${apiKey}`
55
- }
56
- });
57
- if (!res.ok) {
58
- throw new Error(`OpenAI error (${res.status})`);
59
- }
60
- return parseList(res);
61
- }
62
-
63
- if (provider === 'openai_compatible') {
64
- const baseUrl = options.baseUrl || 'https://api.openai.com/v1';
65
- const res = await fetch(`${baseUrl.replace(/\/$/, '')}/models`, {
66
- headers: {
67
- Authorization: `Bearer ${apiKey}`
68
- }
69
- });
70
- if (!res.ok) {
71
- throw new Error(`OpenAI-compatible error (${res.status})`);
72
- }
73
- return parseList(res);
74
- }
75
-
76
- if (provider === 'gemini') {
77
- const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`);
78
- if (!res.ok) {
79
- throw new Error(`Gemini error (${res.status})`);
80
- }
81
- return parseList(res);
82
- }
83
-
84
- if (provider === 'inception') {
85
- const baseUrl = options.baseUrl || 'https://api.inception.ai/v1';
86
- const res = await fetch(`${baseUrl.replace(/\/$/, '')}/models`, {
87
- headers: {
88
- Authorization: `Bearer ${apiKey}`
89
- }
90
- });
91
- if (!res.ok) {
92
- throw new Error(`Inception error (${res.status})`);
93
- }
94
- return parseList(res);
95
- }
96
-
97
- throw new Error(`Unknown provider: ${provider}`);
98
- }
@@ -1,173 +0,0 @@
1
- import { readdir } from 'fs/promises';
2
- import { resolve, dirname, basename } from 'path';
3
- import { homedir } from 'os';
4
-
5
- /**
6
- * Get path completions for tab autocomplete
7
- */
8
- export async function getPathCompletions(
9
- partial: string,
10
- cwd: string
11
- ): Promise<string[]> {
12
- // Expand tilde
13
- let expandedPartial = partial;
14
- if (partial.startsWith('~/')) {
15
- expandedPartial = resolve(homedir(), partial.slice(2));
16
- } else if (partial === '~') {
17
- expandedPartial = homedir();
18
- }
19
-
20
- // Resolve the path relative to cwd
21
- const fullPath = resolve(cwd, expandedPartial);
22
-
23
- // Determine the directory to list and the prefix to match
24
- let dirToList: string;
25
- let prefix: string;
26
-
27
- try {
28
- const { stat } = await import('fs/promises');
29
- const stats = await stat(fullPath);
30
- if (stats.isDirectory()) {
31
- // If it's a directory, list its contents
32
- dirToList = fullPath;
33
- prefix = '';
34
- } else {
35
- // It's a file, no completions
36
- return [];
37
- }
38
- } catch {
39
- // Path doesn't exist, so complete the last component
40
- dirToList = dirname(fullPath);
41
- prefix = basename(fullPath).toLowerCase();
42
- }
43
-
44
- try {
45
- const entries = await readdir(dirToList, { withFileTypes: true });
46
- const matches = entries
47
- .filter(entry => entry.name.toLowerCase().startsWith(prefix))
48
- .map(entry => {
49
- const name = entry.name;
50
- const suffix = entry.isDirectory() ? '/' : '';
51
- return name + suffix;
52
- })
53
- .sort();
54
-
55
- return matches;
56
- } catch {
57
- return [];
58
- }
59
- }
60
-
61
- /**
62
- * Complete a partial path, returning the completed path or null if no unique completion
63
- */
64
- export async function completePath(
65
- partial: string,
66
- cwd: string
67
- ): Promise<{ completed: string; isDirectory: boolean; alternatives: string[] } | null> {
68
- // Handle empty partial - list cwd contents
69
- if (!partial || partial.trim() === '') {
70
- try {
71
- const entries = await readdir(cwd, { withFileTypes: true });
72
- const matches = entries.filter(e => !e.name.startsWith('.')).sort();
73
- if (matches.length === 0) return null;
74
- if (matches.length === 1) {
75
- const entry = matches[0];
76
- const suffix = entry.isDirectory() ? '/' : '';
77
- return { completed: entry.name + suffix, isDirectory: entry.isDirectory(), alternatives: [] };
78
- }
79
- const alternatives = matches.map(e => e.name + (e.isDirectory() ? '/' : ''));
80
- return { completed: '', isDirectory: false, alternatives };
81
- } catch {
82
- return null;
83
- }
84
- }
85
-
86
- // Expand tilde for internal processing
87
- let expandedPartial = partial;
88
- let tildePrefix = '';
89
- if (partial.startsWith('~/')) {
90
- expandedPartial = resolve(homedir(), partial.slice(2));
91
- tildePrefix = '~/';
92
- } else if (partial === '~') {
93
- return { completed: '~/', isDirectory: true, alternatives: [] };
94
- }
95
-
96
- const fullPath = resolve(cwd, expandedPartial);
97
- let dirToList: string;
98
- let prefix: string;
99
- let basePath: string;
100
-
101
- try {
102
- const { stat } = await import('fs/promises');
103
- const stats = await stat(fullPath);
104
- if (stats.isDirectory()) {
105
- // Already a complete directory, append / if not present
106
- if (!partial.endsWith('/')) {
107
- return { completed: partial + '/', isDirectory: true, alternatives: [] };
108
- }
109
- // List directory contents for alternatives
110
- dirToList = fullPath;
111
- prefix = '';
112
- basePath = partial;
113
- } else {
114
- // Complete file path
115
- return { completed: partial, isDirectory: false, alternatives: [] };
116
- }
117
- } catch {
118
- // Path doesn't exist, complete the last component
119
- dirToList = dirname(fullPath);
120
- prefix = basename(fullPath).toLowerCase();
121
-
122
- // Calculate basePath - the directory portion to preserve
123
- if (tildePrefix) {
124
- const afterTilde = expandedPartial.slice(homedir().length + 1);
125
- const dirPart = dirname(afterTilde);
126
- basePath = dirPart === '.' ? tildePrefix : tildePrefix + dirPart + '/';
127
- } else {
128
- const dirPart = dirname(partial);
129
- // Don't add ./ prefix for simple filenames
130
- basePath = (dirPart === '.' || dirPart === '') ? '' : dirPart + '/';
131
- }
132
- }
133
-
134
- try {
135
- const entries = await readdir(dirToList, { withFileTypes: true });
136
- const matches = entries
137
- .filter(entry => entry.name.toLowerCase().startsWith(prefix))
138
- .sort();
139
-
140
- if (matches.length === 0) {
141
- return null;
142
- }
143
-
144
- if (matches.length === 1) {
145
- const entry = matches[0];
146
- const suffix = entry.isDirectory() ? '/' : '';
147
- const completed = basePath + entry.name + suffix;
148
- return { completed, isDirectory: entry.isDirectory(), alternatives: [] };
149
- }
150
-
151
- // Find common prefix among all matches
152
- const names = matches.map(e => e.name);
153
- let commonPrefix = names[0];
154
- for (const name of names.slice(1)) {
155
- let i = 0;
156
- while (i < commonPrefix.length && i < name.length && commonPrefix[i] === name[i]) {
157
- i++;
158
- }
159
- commonPrefix = commonPrefix.slice(0, i);
160
- }
161
-
162
- const alternatives = matches.map(e => e.name + (e.isDirectory() ? '/' : ''));
163
- const completed = basePath + commonPrefix;
164
-
165
- return {
166
- completed: completed.length > partial.length ? completed : partial,
167
- isDirectory: false,
168
- alternatives
169
- };
170
- } catch {
171
- return null;
172
- }
173
- }