@howaboua/pi-codex-conversion 1.0.8 → 1.0.9-dev.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/README.md +37 -0
- package/package.json +7 -2
- package/src/adapter/runtime-shell.ts +13 -0
- package/src/index.ts +2 -1
- package/src/prompt/build-system-prompt.ts +4 -1
- package/src/shell/bash.ts +261 -0
- package/src/shell/parse-command.ts +616 -0
- package/src/shell/parse.ts +370 -93
- package/src/shell/summary.ts +19 -51
- package/src/shell/tokenize.ts +28 -5
- package/src/tools/exec-session-manager.ts +61 -5
package/README.md
CHANGED
|
@@ -80,6 +80,43 @@ Alternative Git install:
|
|
|
80
80
|
pi install git:github.com/IgorWarzocha/pi-codex-conversion
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
+
## Publishing
|
|
84
|
+
|
|
85
|
+
This package is already configured for public npm publishes via:
|
|
86
|
+
|
|
87
|
+
- `publishConfig.access = "public"`
|
|
88
|
+
- `prepublishOnly` / `prepack` checks
|
|
89
|
+
|
|
90
|
+
Useful commands:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npm run publish:dry-run
|
|
94
|
+
npm run publish:dev
|
|
95
|
+
npm run release:dev
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
What they do:
|
|
99
|
+
|
|
100
|
+
- `npm run publish:dry-run` — inspect what would be published
|
|
101
|
+
- `npm run publish:dev` — publish the current version under the `dev` dist-tag
|
|
102
|
+
- `npm run release:dev` — bump the package to the next `-dev.N` prerelease and publish it under the `dev` dist-tag
|
|
103
|
+
|
|
104
|
+
Typical flow:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
npm login
|
|
108
|
+
npm run publish:dry-run
|
|
109
|
+
npm run release:dev
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
For modern npm auth, just run `npm login` and complete the browser flow when prompted.
|
|
113
|
+
|
|
114
|
+
After publishing, install the dev build with:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
pi install npm:@howaboua/pi-codex-conversion@dev
|
|
118
|
+
```
|
|
119
|
+
|
|
83
120
|
## Prompt behavior
|
|
84
121
|
|
|
85
122
|
The adapter does not build a standalone replacement prompt anymore. Instead it:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@howaboua/pi-codex-conversion",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9-dev.0",
|
|
4
4
|
"description": "Codex-oriented tool and prompt adapter for pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -41,6 +41,9 @@
|
|
|
41
41
|
"typecheck": "tsc -p tsconfig.json",
|
|
42
42
|
"test": "tsx --test tests/**/*.test.ts",
|
|
43
43
|
"check": "npm run typecheck && npm run test",
|
|
44
|
+
"publish:dry-run": "npm publish --dry-run",
|
|
45
|
+
"publish:dev": "npm publish --tag dev",
|
|
46
|
+
"release:dev": "npm version prerelease --preid dev && npm publish --tag dev",
|
|
44
47
|
"prepack": "npm run check",
|
|
45
48
|
"prepublishOnly": "npm run check"
|
|
46
49
|
},
|
|
@@ -57,6 +60,8 @@
|
|
|
57
60
|
"typescript": "^5.9.3"
|
|
58
61
|
},
|
|
59
62
|
"dependencies": {
|
|
60
|
-
"node-pty": "^1.1.0"
|
|
63
|
+
"node-pty": "^1.1.0",
|
|
64
|
+
"tree-sitter-bash": "^0.25.1",
|
|
65
|
+
"web-tree-sitter": "^0.26.7"
|
|
61
66
|
}
|
|
62
67
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const CODEX_FALLBACK_SHELL = "/bin/bash";
|
|
2
|
+
|
|
3
|
+
export function isFishShell(shell: string | undefined): boolean {
|
|
4
|
+
const name = shell?.replace(/\\/g, "/").split("/").pop()?.toLowerCase();
|
|
5
|
+
return name === "fish";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function getCodexRuntimeShell(shell: string | undefined): string {
|
|
9
|
+
if (!shell) {
|
|
10
|
+
return CODEX_FALLBACK_SHELL;
|
|
11
|
+
}
|
|
12
|
+
return isFishShell(shell) ? CODEX_FALLBACK_SHELL : shell;
|
|
13
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { getCodexRuntimeShell } from "./adapter/runtime-shell.ts";
|
|
2
3
|
import { CORE_ADAPTER_TOOL_NAMES, DEFAULT_TOOL_NAMES, STATUS_KEY, STATUS_TEXT, VIEW_IMAGE_TOOL_NAME, WEB_SEARCH_TOOL_NAME } from "./adapter/tool-set.ts";
|
|
3
4
|
import { registerApplyPatchTool } from "./tools/apply-patch-tool.ts";
|
|
4
5
|
import { isCodexLikeContext, isOpenAICodexContext } from "./adapter/codex-model.ts";
|
|
@@ -81,7 +82,7 @@ export default function codexConversion(pi: ExtensionAPI) {
|
|
|
81
82
|
return {
|
|
82
83
|
systemPrompt: buildCodexSystemPrompt(event.systemPrompt, {
|
|
83
84
|
skills: state.promptSkills,
|
|
84
|
-
shell: process.env.SHELL
|
|
85
|
+
shell: getCodexRuntimeShell(process.env.SHELL),
|
|
85
86
|
}),
|
|
86
87
|
};
|
|
87
88
|
});
|
|
@@ -21,9 +21,12 @@ function insertBeforeTrailingContext(prompt: string, section: string): string {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
function injectShell(prompt: string, shell?: string): string {
|
|
24
|
-
if (!shell
|
|
24
|
+
if (!shell) {
|
|
25
25
|
return prompt;
|
|
26
26
|
}
|
|
27
|
+
if (/\nCurrent shell:/.test(prompt)) {
|
|
28
|
+
return prompt.replace(/(^Current shell:) .*$/m, `$1 ${shell}`);
|
|
29
|
+
}
|
|
27
30
|
return insertBeforeTrailingContext(prompt, `Current shell: ${shell}`);
|
|
28
31
|
}
|
|
29
32
|
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { Language, Parser, type Node, type Tree } from "web-tree-sitter";
|
|
3
|
+
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
|
|
6
|
+
const parser = await createBashParser();
|
|
7
|
+
|
|
8
|
+
export function hasBashAstSupport(): boolean {
|
|
9
|
+
return parser !== undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function extractBashCommand(command: string[]): [shell: string, script: string] | undefined {
|
|
13
|
+
if (command.length !== 3) return undefined;
|
|
14
|
+
const [shell, flag, script] = command;
|
|
15
|
+
if (flag !== "-lc" && flag !== "-c") return undefined;
|
|
16
|
+
const shellName = shell.replace(/\\/g, "/").split("/").pop()?.toLowerCase();
|
|
17
|
+
if (shellName !== "bash" && shellName !== "zsh" && shellName !== "sh") return undefined;
|
|
18
|
+
return [shell, script];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function tryParseShell(shellLcArg: string): Tree | undefined {
|
|
22
|
+
return parser?.parse(shellLcArg) ?? undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function tryParseWordOnlyCommandsSequence(tree: Tree, src: string): string[][] | undefined {
|
|
26
|
+
if (tree.rootNode.hasError) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const allowedKinds = new Set([
|
|
31
|
+
"program",
|
|
32
|
+
"list",
|
|
33
|
+
"pipeline",
|
|
34
|
+
"command",
|
|
35
|
+
"command_name",
|
|
36
|
+
"word",
|
|
37
|
+
"string",
|
|
38
|
+
"string_content",
|
|
39
|
+
"raw_string",
|
|
40
|
+
"number",
|
|
41
|
+
"concatenation",
|
|
42
|
+
]);
|
|
43
|
+
const allowedPunctuation = new Set(["&&", "||", ";", "|", '"', "'"]);
|
|
44
|
+
|
|
45
|
+
const commandNodes: Node[] = [];
|
|
46
|
+
const stack: Node[] = [tree.rootNode];
|
|
47
|
+
while (stack.length > 0) {
|
|
48
|
+
const node = stack.pop();
|
|
49
|
+
if (!node) continue;
|
|
50
|
+
if (node.isNamed) {
|
|
51
|
+
if (!allowedKinds.has(node.type)) return undefined;
|
|
52
|
+
if (node.type === "command") commandNodes.push(node);
|
|
53
|
+
} else {
|
|
54
|
+
if ([...node.type].some((char) => "&;|".includes(char)) && !allowedPunctuation.has(node.type)) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
if (!allowedPunctuation.has(node.type) && node.type.trim().length > 0) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
for (const child of node.children) {
|
|
62
|
+
stack.push(child);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
commandNodes.sort((left, right) => left.startIndex - right.startIndex);
|
|
67
|
+
const commands: string[][] = [];
|
|
68
|
+
for (const node of commandNodes) {
|
|
69
|
+
const parsed = parsePlainCommandFromNode(node, src);
|
|
70
|
+
if (!parsed) return undefined;
|
|
71
|
+
commands.push(parsed);
|
|
72
|
+
}
|
|
73
|
+
return commands;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function parseShellLcPlainCommands(command: string[]): string[][] | undefined {
|
|
77
|
+
const bash = extractBashCommand(command);
|
|
78
|
+
if (!bash) return undefined;
|
|
79
|
+
const [, script] = bash;
|
|
80
|
+
const tree = tryParseShell(script);
|
|
81
|
+
if (!tree) return undefined;
|
|
82
|
+
return tryParseWordOnlyCommandsSequence(tree, script);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function parseShellLcSingleCommandPrefix(command: string[]): string[] | undefined {
|
|
86
|
+
const bash = extractBashCommand(command);
|
|
87
|
+
if (!bash) return undefined;
|
|
88
|
+
const [, script] = bash;
|
|
89
|
+
const tree = tryParseShell(script);
|
|
90
|
+
if (!tree || tree.rootNode.hasError) return undefined;
|
|
91
|
+
if (!hasNamedDescendantKind(tree.rootNode, "heredoc_redirect")) return undefined;
|
|
92
|
+
|
|
93
|
+
const commandNode = findSingleCommandNode(tree.rootNode);
|
|
94
|
+
if (!commandNode) return undefined;
|
|
95
|
+
return parseHeredocCommandWords(commandNode, script);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function createBashParser(): Promise<Parser | undefined> {
|
|
99
|
+
try {
|
|
100
|
+
await Parser.init();
|
|
101
|
+
const language = await Language.load(require.resolve("tree-sitter-bash/tree-sitter-bash.wasm"));
|
|
102
|
+
const parser = new Parser();
|
|
103
|
+
parser.setLanguage(language);
|
|
104
|
+
return parser;
|
|
105
|
+
} catch {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function parsePlainCommandFromNode(command: Node, src: string): string[] | undefined {
|
|
111
|
+
if (command.type !== "command") return undefined;
|
|
112
|
+
|
|
113
|
+
const words: string[] = [];
|
|
114
|
+
for (const child of command.namedChildren) {
|
|
115
|
+
switch (child.type) {
|
|
116
|
+
case "command_name": {
|
|
117
|
+
const wordNode = child.namedChild(0);
|
|
118
|
+
if (!wordNode || wordNode.type !== "word") return undefined;
|
|
119
|
+
words.push(textOf(wordNode, src));
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
case "word":
|
|
123
|
+
case "number":
|
|
124
|
+
words.push(textOf(child, src));
|
|
125
|
+
break;
|
|
126
|
+
case "string": {
|
|
127
|
+
const parsed = parseDoubleQuotedString(child, src);
|
|
128
|
+
if (parsed === undefined) return undefined;
|
|
129
|
+
words.push(parsed);
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
case "raw_string": {
|
|
133
|
+
const parsed = parseRawString(child, src);
|
|
134
|
+
if (parsed === undefined) return undefined;
|
|
135
|
+
words.push(parsed);
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
case "concatenation": {
|
|
139
|
+
let concatenated = "";
|
|
140
|
+
for (const part of child.namedChildren) {
|
|
141
|
+
switch (part.type) {
|
|
142
|
+
case "word":
|
|
143
|
+
case "number":
|
|
144
|
+
concatenated += textOf(part, src);
|
|
145
|
+
break;
|
|
146
|
+
case "string": {
|
|
147
|
+
const parsed = parseDoubleQuotedString(part, src);
|
|
148
|
+
if (parsed === undefined) return undefined;
|
|
149
|
+
concatenated += parsed;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
case "raw_string": {
|
|
153
|
+
const parsed = parseRawString(part, src);
|
|
154
|
+
if (parsed === undefined) return undefined;
|
|
155
|
+
concatenated += parsed;
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
default:
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (concatenated.length === 0) return undefined;
|
|
163
|
+
words.push(concatenated);
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
default:
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return words;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function parseHeredocCommandWords(command: Node, src: string): string[] | undefined {
|
|
175
|
+
if (command.type !== "command") return undefined;
|
|
176
|
+
const words: string[] = [];
|
|
177
|
+
for (const child of command.namedChildren) {
|
|
178
|
+
switch (child.type) {
|
|
179
|
+
case "command_name": {
|
|
180
|
+
const wordNode = child.namedChild(0);
|
|
181
|
+
if (!wordNode || !(wordNode.type === "word" || wordNode.type === "number") || !isLiteralWordOrNumber(wordNode)) return undefined;
|
|
182
|
+
words.push(textOf(wordNode, src));
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
case "word":
|
|
186
|
+
case "number":
|
|
187
|
+
if (!isLiteralWordOrNumber(child)) return undefined;
|
|
188
|
+
words.push(textOf(child, src));
|
|
189
|
+
break;
|
|
190
|
+
case "variable_assignment":
|
|
191
|
+
case "comment":
|
|
192
|
+
break;
|
|
193
|
+
case "heredoc_body":
|
|
194
|
+
case "simple_heredoc_body":
|
|
195
|
+
case "heredoc_redirect":
|
|
196
|
+
case "herestring_redirect":
|
|
197
|
+
case "file_redirect":
|
|
198
|
+
case "redirected_statement":
|
|
199
|
+
break;
|
|
200
|
+
default:
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return words.length > 0 ? words : undefined;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function isLiteralWordOrNumber(node: Node): boolean {
|
|
208
|
+
if (node.type !== "word" && node.type !== "number") return false;
|
|
209
|
+
return node.namedChildren.length === 0;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function findSingleCommandNode(root: Node): Node | undefined {
|
|
213
|
+
const stack: Node[] = [root];
|
|
214
|
+
let singleCommand: Node | undefined;
|
|
215
|
+
while (stack.length > 0) {
|
|
216
|
+
const node = stack.pop();
|
|
217
|
+
if (!node) continue;
|
|
218
|
+
if (node.type === "command") {
|
|
219
|
+
if (singleCommand) return undefined;
|
|
220
|
+
singleCommand = node;
|
|
221
|
+
}
|
|
222
|
+
for (const child of node.namedChildren) {
|
|
223
|
+
stack.push(child);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return singleCommand;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function hasNamedDescendantKind(node: Node, kind: string): boolean {
|
|
230
|
+
const stack: Node[] = [node];
|
|
231
|
+
while (stack.length > 0) {
|
|
232
|
+
const current = stack.pop();
|
|
233
|
+
if (!current) continue;
|
|
234
|
+
if (current.type === kind) return true;
|
|
235
|
+
for (const child of current.namedChildren) {
|
|
236
|
+
stack.push(child);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function parseDoubleQuotedString(node: Node, src: string): string | undefined {
|
|
243
|
+
if (node.type !== "string") return undefined;
|
|
244
|
+
for (const part of node.namedChildren) {
|
|
245
|
+
if (part.type !== "string_content") return undefined;
|
|
246
|
+
}
|
|
247
|
+
const raw = textOf(node, src);
|
|
248
|
+
if (!raw.startsWith('"') || !raw.endsWith('"')) return undefined;
|
|
249
|
+
return raw.slice(1, -1);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function parseRawString(node: Node, src: string): string | undefined {
|
|
253
|
+
if (node.type !== "raw_string") return undefined;
|
|
254
|
+
const raw = textOf(node, src);
|
|
255
|
+
if (!raw.startsWith("'") || !raw.endsWith("'")) return undefined;
|
|
256
|
+
return raw.slice(1, -1);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function textOf(node: Node, _src: string): string {
|
|
260
|
+
return node.text;
|
|
261
|
+
}
|