@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 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.8",
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 || "/bin/bash",
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 || /\nCurrent shell:/.test(prompt)) {
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
+ }