python-codex 0.0.1__py3-none-any.whl → 0.1.0__py3-none-any.whl
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.
- pycodex/__init__.py +139 -2
- pycodex/agent.py +290 -0
- pycodex/cli.py +641 -0
- pycodex/collaboration.py +21 -0
- pycodex/context.py +580 -0
- pycodex/doctor.py +360 -0
- pycodex/model.py +533 -0
- pycodex/prompts/collaboration_default.md +11 -0
- pycodex/prompts/collaboration_plan.md +128 -0
- pycodex/prompts/default_base_instructions.md +275 -0
- pycodex/prompts/exec_tools.json +411 -0
- pycodex/prompts/models.json +847 -0
- pycodex/prompts/permissions/approval_policy/never.md +1 -0
- pycodex/prompts/permissions/approval_policy/on_failure.md +1 -0
- pycodex/prompts/permissions/approval_policy/on_request.md +57 -0
- pycodex/prompts/permissions/approval_policy/on_request_rule_request_permission.md +33 -0
- pycodex/prompts/permissions/approval_policy/unless_trusted.md +1 -0
- pycodex/prompts/permissions/sandbox_mode/danger_full_access.md +1 -0
- pycodex/prompts/permissions/sandbox_mode/read_only.md +1 -0
- pycodex/prompts/permissions/sandbox_mode/workspace_write.md +1 -0
- pycodex/prompts/subagent_tools.json +163 -0
- pycodex/protocol.py +347 -0
- pycodex/runtime.py +200 -0
- pycodex/runtime_services.py +408 -0
- pycodex/tools/__init__.py +58 -0
- pycodex/tools/agent_tool_schemas.py +70 -0
- pycodex/tools/apply_patch_tool.py +363 -0
- pycodex/tools/base_tool.py +168 -0
- pycodex/tools/close_agent_tool.py +55 -0
- pycodex/tools/code_mode_manager.py +519 -0
- pycodex/tools/exec_command_tool.py +96 -0
- pycodex/tools/exec_runtime.js +161 -0
- pycodex/tools/exec_tool.py +48 -0
- pycodex/tools/grep_files_tool.py +150 -0
- pycodex/tools/list_dir_tool.py +135 -0
- pycodex/tools/read_file_tool.py +217 -0
- pycodex/tools/request_permissions_tool.py +95 -0
- pycodex/tools/request_user_input_tool.py +167 -0
- pycodex/tools/resume_agent_tool.py +56 -0
- pycodex/tools/send_input_tool.py +106 -0
- pycodex/tools/shell_command_tool.py +107 -0
- pycodex/tools/shell_tool.py +112 -0
- pycodex/tools/spawn_agent_tool.py +97 -0
- pycodex/tools/unified_exec_manager.py +380 -0
- pycodex/tools/update_plan_tool.py +79 -0
- pycodex/tools/view_image_tool.py +111 -0
- pycodex/tools/wait_agent_tool.py +75 -0
- pycodex/tools/wait_tool.py +68 -0
- pycodex/tools/web_search_tool.py +30 -0
- pycodex/tools/write_stdin_tool.py +75 -0
- pycodex/utils/__init__.py +40 -0
- pycodex/utils/dotenv.py +64 -0
- pycodex/utils/get_env.py +218 -0
- pycodex/utils/random_ids.py +19 -0
- pycodex/utils/visualize.py +978 -0
- python_codex-0.1.0.dist-info/METADATA +267 -0
- python_codex-0.1.0.dist-info/RECORD +60 -0
- python_codex-0.1.0.dist-info/entry_points.txt +2 -0
- python_codex-0.1.0.dist-info/licenses/LICENSE +201 -0
- python_codex-0.0.1.dist-info/METADATA +0 -30
- python_codex-0.0.1.dist-info/RECORD +0 -4
- {python_codex-0.0.1.dist-info → python_codex-0.1.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
const readline = require('node:readline');
|
|
2
|
+
const { stdin, stdout } = require('node:process');
|
|
3
|
+
|
|
4
|
+
const pending = new Map();
|
|
5
|
+
let storedValues = {};
|
|
6
|
+
let toolMap = {};
|
|
7
|
+
let initialized = false;
|
|
8
|
+
|
|
9
|
+
function send(message) {
|
|
10
|
+
stdout.write(JSON.stringify(message) + '\n');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function stringifyValue(value) {
|
|
14
|
+
if (typeof value === 'string') {
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
if (value === undefined) {
|
|
18
|
+
return 'undefined';
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
return JSON.stringify(value);
|
|
22
|
+
} catch (_error) {
|
|
23
|
+
return String(value);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function text(value) {
|
|
28
|
+
send({ type: 'output_text', text: stringifyValue(value) });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function notify(value) {
|
|
32
|
+
text(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function image(value) {
|
|
36
|
+
if (typeof value === 'string') {
|
|
37
|
+
send({ type: 'output_image', image_url: value, detail: null });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (value && typeof value === 'object' && typeof value.image_url === 'string') {
|
|
41
|
+
send({
|
|
42
|
+
type: 'output_image',
|
|
43
|
+
image_url: value.image_url,
|
|
44
|
+
detail: value.detail ?? null,
|
|
45
|
+
});
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
throw new Error('image(...) expects an image URL string or { image_url, detail? }');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function store(key, value) {
|
|
52
|
+
storedValues[key] = value;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function load(key) {
|
|
56
|
+
return storedValues[key];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function exit() {
|
|
60
|
+
const error = new Error('__CODEX_EXIT__');
|
|
61
|
+
error.__codexExit = true;
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function yield_control() {
|
|
66
|
+
send({ type: 'yield' });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function createToolCaller(toolName) {
|
|
70
|
+
return function callTool(argumentsValue = {}) {
|
|
71
|
+
const id = `${toolName}_${Math.random().toString(16).slice(2)}`;
|
|
72
|
+
send({
|
|
73
|
+
type: 'tool_call',
|
|
74
|
+
id,
|
|
75
|
+
tool_name: toolName,
|
|
76
|
+
arguments: argumentsValue,
|
|
77
|
+
});
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
pending.set(id, { resolve, reject });
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function initialize(message) {
|
|
85
|
+
storedValues = message.stored_values || {};
|
|
86
|
+
toolMap = {};
|
|
87
|
+
for (const tool of message.tools || []) {
|
|
88
|
+
toolMap[tool.js_name] = createToolCaller(tool.tool_name);
|
|
89
|
+
}
|
|
90
|
+
const allTools = Object.freeze(
|
|
91
|
+
(message.tools || []).map((tool) => ({
|
|
92
|
+
name: tool.js_name,
|
|
93
|
+
description: tool.description,
|
|
94
|
+
}))
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
|
|
98
|
+
const fn = new AsyncFunction(
|
|
99
|
+
'tools',
|
|
100
|
+
'text',
|
|
101
|
+
'image',
|
|
102
|
+
'store',
|
|
103
|
+
'load',
|
|
104
|
+
'exit',
|
|
105
|
+
'notify',
|
|
106
|
+
'ALL_TOOLS',
|
|
107
|
+
'yield_control',
|
|
108
|
+
'console',
|
|
109
|
+
message.source
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
(async () => {
|
|
113
|
+
try {
|
|
114
|
+
await fn(
|
|
115
|
+
toolMap,
|
|
116
|
+
text,
|
|
117
|
+
image,
|
|
118
|
+
store,
|
|
119
|
+
load,
|
|
120
|
+
exit,
|
|
121
|
+
notify,
|
|
122
|
+
allTools,
|
|
123
|
+
yield_control,
|
|
124
|
+
undefined
|
|
125
|
+
);
|
|
126
|
+
send({ type: 'result', stored_values: storedValues, error_text: null });
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (error && error.__codexExit) {
|
|
129
|
+
send({ type: 'result', stored_values: storedValues, error_text: null });
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const errorText = error && error.stack ? error.stack : String(error);
|
|
133
|
+
send({ type: 'result', stored_values: storedValues, error_text: errorText });
|
|
134
|
+
}
|
|
135
|
+
})();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const rl = readline.createInterface({ input: stdin, crlfDelay: Infinity });
|
|
139
|
+
rl.on('line', (line) => {
|
|
140
|
+
if (!line.trim()) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const message = JSON.parse(line);
|
|
144
|
+
if (message.type === 'init' && !initialized) {
|
|
145
|
+
initialized = true;
|
|
146
|
+
initialize(message);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (message.type === 'tool_result') {
|
|
150
|
+
const entry = pending.get(message.id);
|
|
151
|
+
if (!entry) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
pending.delete(message.id);
|
|
155
|
+
if (message.ok) {
|
|
156
|
+
entry.resolve(message.result);
|
|
157
|
+
} else {
|
|
158
|
+
entry.reject(new Error(message.error || 'nested tool failed'));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""`exec` tool for the Python Codex prototype.
|
|
2
|
+
|
|
3
|
+
Original Codex mapping:
|
|
4
|
+
- Corresponds to the original Codex code-mode `exec` tool.
|
|
5
|
+
|
|
6
|
+
Expected behavior:
|
|
7
|
+
- Accept raw JavaScript source text as a freeform/custom tool payload.
|
|
8
|
+
- Run the script inside a background exec cell with helper functions and nested
|
|
9
|
+
tool access.
|
|
10
|
+
- Return either a completed result or a running `cell_id` status that can be
|
|
11
|
+
resumed via `wait`.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from ..protocol import JSONValue
|
|
17
|
+
from .base_tool import BaseTool, ToolContext
|
|
18
|
+
from .code_mode_manager import CodeModeManager
|
|
19
|
+
|
|
20
|
+
EXEC_FREEFORM_GRAMMAR = """start: pragma_source | plain_source
|
|
21
|
+
pragma_source: PRAGMA_LINE NEWLINE SOURCE
|
|
22
|
+
plain_source: SOURCE
|
|
23
|
+
|
|
24
|
+
PRAGMA_LINE: /[ \t]*\/\/ @exec:[^\r\n]*/
|
|
25
|
+
NEWLINE: /\r?\n/
|
|
26
|
+
SOURCE: /[\s\S]+/
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ExecTool(BaseTool):
|
|
31
|
+
name = "exec"
|
|
32
|
+
description = (
|
|
33
|
+
"Runs raw JavaScript in an isolated context. Send raw JavaScript "
|
|
34
|
+
"source text, not JSON, quoted strings, or markdown code fences."
|
|
35
|
+
)
|
|
36
|
+
tool_type = "custom"
|
|
37
|
+
format = {
|
|
38
|
+
"type": "grammar",
|
|
39
|
+
"syntax": "lark",
|
|
40
|
+
"definition": EXEC_FREEFORM_GRAMMAR,
|
|
41
|
+
}
|
|
42
|
+
supports_parallel = False
|
|
43
|
+
|
|
44
|
+
def __init__(self, manager: CodeModeManager) -> None:
|
|
45
|
+
self._manager = manager
|
|
46
|
+
|
|
47
|
+
async def run(self, context: ToolContext, args: JSONValue) -> JSONValue:
|
|
48
|
+
return await self._manager.exec(str(args), context)
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""`grep_files` tool for the Python Codex prototype.
|
|
2
|
+
|
|
3
|
+
Original Codex mapping:
|
|
4
|
+
- Corresponds to the original Codex `grep_files` tool.
|
|
5
|
+
|
|
6
|
+
Expected behavior:
|
|
7
|
+
- Search file contents with ripgrep and return only matching file paths.
|
|
8
|
+
- Support the original tool's `pattern`, `include`, `path`, and `limit`
|
|
9
|
+
parameters rather than delegating to a shell transcript.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import fnmatch
|
|
16
|
+
import re
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from ..protocol import JSONDict, JSONValue
|
|
20
|
+
from .base_tool import BaseTool, ToolContext
|
|
21
|
+
|
|
22
|
+
DEFAULT_LIMIT = 100
|
|
23
|
+
MAX_LIMIT = 2000
|
|
24
|
+
COMMAND_TIMEOUT_SECONDS = 30
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class GrepFilesTool(BaseTool):
|
|
28
|
+
name = "grep_files"
|
|
29
|
+
description = (
|
|
30
|
+
"Finds files whose contents match the pattern and lists them by "
|
|
31
|
+
"modification time."
|
|
32
|
+
)
|
|
33
|
+
input_schema = {
|
|
34
|
+
"type": "object",
|
|
35
|
+
"properties": {
|
|
36
|
+
"pattern": {"type": "string"},
|
|
37
|
+
"include": {"type": "string"},
|
|
38
|
+
"path": {"type": "string"},
|
|
39
|
+
"limit": {"type": "integer"},
|
|
40
|
+
},
|
|
41
|
+
"required": ["pattern"],
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
def __init__(self, cwd: str | Path | None = None) -> None:
|
|
45
|
+
self._working_directory = Path(cwd or Path.cwd()).resolve()
|
|
46
|
+
|
|
47
|
+
async def run(self, context: ToolContext, args: JSONDict) -> JSONValue:
|
|
48
|
+
del context
|
|
49
|
+
pattern = str(args.get("pattern", "")).strip()
|
|
50
|
+
include = str(args.get("include", "")).strip() or None
|
|
51
|
+
limit = min(int(args.get("limit", DEFAULT_LIMIT)), MAX_LIMIT)
|
|
52
|
+
path_arg = args.get("path")
|
|
53
|
+
search_path = self._resolve_path(path_arg)
|
|
54
|
+
|
|
55
|
+
if not pattern:
|
|
56
|
+
return "Error: `pattern` must not be empty."
|
|
57
|
+
if limit <= 0:
|
|
58
|
+
return "Error: `limit` must be greater than zero."
|
|
59
|
+
if not search_path.exists():
|
|
60
|
+
return f"Error: unable to access `{search_path}`."
|
|
61
|
+
|
|
62
|
+
command = [
|
|
63
|
+
"rg",
|
|
64
|
+
"--files-with-matches",
|
|
65
|
+
"--sortr=modified",
|
|
66
|
+
"--regexp",
|
|
67
|
+
pattern,
|
|
68
|
+
"--no-messages",
|
|
69
|
+
]
|
|
70
|
+
if include is not None:
|
|
71
|
+
command.extend(["--glob", include])
|
|
72
|
+
command.extend(["--", str(search_path)])
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
process = await asyncio.create_subprocess_exec(
|
|
76
|
+
*command,
|
|
77
|
+
cwd=str(self._working_directory),
|
|
78
|
+
stdout=asyncio.subprocess.PIPE,
|
|
79
|
+
stderr=asyncio.subprocess.PIPE,
|
|
80
|
+
)
|
|
81
|
+
except FileNotFoundError:
|
|
82
|
+
results = self._search_with_python(pattern, include, search_path, limit)
|
|
83
|
+
if not results:
|
|
84
|
+
return "No matches found."
|
|
85
|
+
return "\n".join(results)
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
stdout_bytes, stderr_bytes = await asyncio.wait_for(
|
|
89
|
+
process.communicate(),
|
|
90
|
+
timeout=COMMAND_TIMEOUT_SECONDS,
|
|
91
|
+
)
|
|
92
|
+
except asyncio.TimeoutError:
|
|
93
|
+
process.kill()
|
|
94
|
+
await process.communicate()
|
|
95
|
+
return "Error: rg timed out after 30 seconds."
|
|
96
|
+
|
|
97
|
+
if process.returncode == 1:
|
|
98
|
+
return "No matches found."
|
|
99
|
+
if process.returncode != 0:
|
|
100
|
+
stderr = stderr_bytes.decode("utf-8", errors="replace").strip()
|
|
101
|
+
return f"Error: rg failed: {stderr}"
|
|
102
|
+
|
|
103
|
+
results = [
|
|
104
|
+
line
|
|
105
|
+
for line in stdout_bytes.decode("utf-8", errors="replace").splitlines()
|
|
106
|
+
if line.strip()
|
|
107
|
+
][:limit]
|
|
108
|
+
if not results:
|
|
109
|
+
return "No matches found."
|
|
110
|
+
return "\n".join(results)
|
|
111
|
+
|
|
112
|
+
def _search_with_python(
|
|
113
|
+
self,
|
|
114
|
+
pattern: str,
|
|
115
|
+
include: str | None,
|
|
116
|
+
search_path: Path,
|
|
117
|
+
limit: int,
|
|
118
|
+
) -> list[str]:
|
|
119
|
+
regex = re.compile(pattern)
|
|
120
|
+
candidates: list[Path] = []
|
|
121
|
+
|
|
122
|
+
if search_path.is_file():
|
|
123
|
+
candidates = [search_path]
|
|
124
|
+
else:
|
|
125
|
+
candidates = [path for path in search_path.rglob("*") if path.is_file()]
|
|
126
|
+
|
|
127
|
+
if include is not None:
|
|
128
|
+
candidates = [
|
|
129
|
+
path for path in candidates if fnmatch.fnmatch(path.name, include)
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
matches = []
|
|
133
|
+
for path in candidates:
|
|
134
|
+
try:
|
|
135
|
+
text = path.read_text(errors="replace")
|
|
136
|
+
except OSError:
|
|
137
|
+
continue
|
|
138
|
+
if regex.search(text):
|
|
139
|
+
matches.append(path)
|
|
140
|
+
|
|
141
|
+
matches.sort(key=lambda path: path.stat().st_mtime, reverse=True)
|
|
142
|
+
return [str(path) for path in matches[:limit]]
|
|
143
|
+
|
|
144
|
+
def _resolve_path(self, path_arg) -> Path:
|
|
145
|
+
if path_arg in (None, ""):
|
|
146
|
+
return self._working_directory
|
|
147
|
+
path = Path(str(path_arg))
|
|
148
|
+
if not path.is_absolute():
|
|
149
|
+
path = self._working_directory / path
|
|
150
|
+
return path.resolve()
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""`list_dir` tool for the Python Codex prototype.
|
|
2
|
+
|
|
3
|
+
Original Codex mapping:
|
|
4
|
+
- Corresponds to the original Codex `list_dir` tool.
|
|
5
|
+
|
|
6
|
+
Expected behavior:
|
|
7
|
+
- List directory entries from an absolute path with offset/limit/depth controls.
|
|
8
|
+
- Produce a stable, human-readable directory tree slice instead of using shell
|
|
9
|
+
commands like `find` or `ls -R`.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from collections import deque
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from ..protocol import JSONDict, JSONValue
|
|
18
|
+
from .base_tool import BaseTool, ToolContext
|
|
19
|
+
|
|
20
|
+
MAX_ENTRY_LENGTH = 500
|
|
21
|
+
INDENTATION_SPACES = 2
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ListDirTool(BaseTool):
|
|
25
|
+
name = "list_dir"
|
|
26
|
+
description = (
|
|
27
|
+
"Lists entries in a local directory with 1-indexed entry numbers and "
|
|
28
|
+
"simple type labels."
|
|
29
|
+
)
|
|
30
|
+
input_schema = {
|
|
31
|
+
"type": "object",
|
|
32
|
+
"properties": {
|
|
33
|
+
"dir_path": {"type": "string"},
|
|
34
|
+
"offset": {"type": "integer"},
|
|
35
|
+
"limit": {"type": "integer"},
|
|
36
|
+
"depth": {"type": "integer"},
|
|
37
|
+
},
|
|
38
|
+
"required": ["dir_path"],
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async def run(self, context: ToolContext, args: JSONDict) -> JSONValue:
|
|
42
|
+
del context
|
|
43
|
+
dir_path = Path(str(args.get("dir_path", "")))
|
|
44
|
+
offset = int(args.get("offset", 1))
|
|
45
|
+
limit = int(args.get("limit", 25))
|
|
46
|
+
depth = int(args.get("depth", 2))
|
|
47
|
+
|
|
48
|
+
if not dir_path.is_absolute():
|
|
49
|
+
return "Error: `dir_path` must be an absolute path."
|
|
50
|
+
if offset <= 0:
|
|
51
|
+
return "Error: `offset` must be a 1-indexed entry number."
|
|
52
|
+
if limit <= 0:
|
|
53
|
+
return "Error: `limit` must be greater than zero."
|
|
54
|
+
if depth <= 0:
|
|
55
|
+
return "Error: `depth` must be greater than zero."
|
|
56
|
+
if not dir_path.exists():
|
|
57
|
+
return f"Error: `{dir_path}` does not exist."
|
|
58
|
+
if not dir_path.is_dir():
|
|
59
|
+
return f"Error: `{dir_path}` is not a directory."
|
|
60
|
+
|
|
61
|
+
entries = self._collect_entries(dir_path, depth)
|
|
62
|
+
if not entries:
|
|
63
|
+
return f"Absolute path: {dir_path}"
|
|
64
|
+
|
|
65
|
+
start_index = offset - 1
|
|
66
|
+
if start_index >= len(entries):
|
|
67
|
+
return "Error: `offset` exceeds directory entry count."
|
|
68
|
+
|
|
69
|
+
end_index = min(start_index + limit, len(entries))
|
|
70
|
+
selected = entries[start_index:end_index]
|
|
71
|
+
lines = [f"Absolute path: {dir_path}"]
|
|
72
|
+
lines.extend(self._format_entry_line(entry) for entry in selected)
|
|
73
|
+
if end_index < len(entries):
|
|
74
|
+
lines.append(f"More than {len(selected)} entries found")
|
|
75
|
+
return "\n".join(lines)
|
|
76
|
+
|
|
77
|
+
def _collect_entries(self, root: Path, depth: int) -> list[dict[str, object]]:
|
|
78
|
+
entries: list[dict[str, object]] = []
|
|
79
|
+
queue = deque([(root, Path(), depth)])
|
|
80
|
+
|
|
81
|
+
while queue:
|
|
82
|
+
current_dir, prefix, remaining_depth = queue.popleft()
|
|
83
|
+
dir_entries = []
|
|
84
|
+
for child in current_dir.iterdir():
|
|
85
|
+
relative_path = prefix / child.name if prefix.parts else Path(child.name)
|
|
86
|
+
kind = self._entry_kind(child)
|
|
87
|
+
dir_entries.append(
|
|
88
|
+
(
|
|
89
|
+
child,
|
|
90
|
+
relative_path,
|
|
91
|
+
{
|
|
92
|
+
"name": self._format_entry_name(relative_path),
|
|
93
|
+
"display_name": self._format_component(child.name),
|
|
94
|
+
"depth": len(prefix.parts),
|
|
95
|
+
"kind": kind,
|
|
96
|
+
},
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
dir_entries.sort(key=lambda item: item[2]["name"])
|
|
101
|
+
for child, relative_path, entry in dir_entries:
|
|
102
|
+
if entry["kind"] == "directory" and remaining_depth > 1:
|
|
103
|
+
queue.append((child, relative_path, remaining_depth - 1))
|
|
104
|
+
entries.append(entry)
|
|
105
|
+
|
|
106
|
+
entries.sort(key=lambda entry: entry["name"])
|
|
107
|
+
return entries
|
|
108
|
+
|
|
109
|
+
def _entry_kind(self, path: Path) -> str:
|
|
110
|
+
if path.is_symlink():
|
|
111
|
+
return "symlink"
|
|
112
|
+
if path.is_dir():
|
|
113
|
+
return "directory"
|
|
114
|
+
if path.is_file():
|
|
115
|
+
return "file"
|
|
116
|
+
return "other"
|
|
117
|
+
|
|
118
|
+
def _format_entry_name(self, path: Path) -> str:
|
|
119
|
+
text = path.as_posix()
|
|
120
|
+
return text[:MAX_ENTRY_LENGTH]
|
|
121
|
+
|
|
122
|
+
def _format_component(self, name: str) -> str:
|
|
123
|
+
return name[:MAX_ENTRY_LENGTH]
|
|
124
|
+
|
|
125
|
+
def _format_entry_line(self, entry: dict[str, object]) -> str:
|
|
126
|
+
indent = " " * (int(entry["depth"]) * INDENTATION_SPACES)
|
|
127
|
+
name = str(entry["display_name"])
|
|
128
|
+
kind = str(entry["kind"])
|
|
129
|
+
if kind == "directory":
|
|
130
|
+
name += "/"
|
|
131
|
+
elif kind == "symlink":
|
|
132
|
+
name += "@"
|
|
133
|
+
elif kind == "other":
|
|
134
|
+
name += "?"
|
|
135
|
+
return f"{indent}{name}"
|