@uipath/coder-tool 1.202.0-preview.140 → 1.202.0-preview.142
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/dist/index.js +1 -1
- package/dist/{tool-d4a6hxqe.js → tool-5mxjdn16.js} +5 -2
- package/dist/tool.js +1 -1
- package/package.json +5 -2
- package/src/commands/chat.spec.ts +0 -224
- package/src/commands/chat.ts +0 -156
- package/src/index.ts +0 -27
- package/src/providers/uipath.spec.ts +0 -296
- package/src/providers/uipath.ts +0 -195
- package/src/tool.spec.ts +0 -38
- package/src/tool.ts +0 -20
- package/templates/uipath-cli.md +0 -38
- package/tests/coding-agent.e2e.test.ts +0 -17
- package/tests/performance.e2e.test.ts +0 -32
- package/tsconfig.json +0 -9
- package/vitest.config.ts +0 -1
package/dist/index.js
CHANGED
|
@@ -5306,7 +5306,7 @@ var factorySlot = singleton("PackagerFactoryProvider");
|
|
|
5306
5306
|
var package_default = {
|
|
5307
5307
|
name: "@uipath/coder-tool",
|
|
5308
5308
|
license: "MIT",
|
|
5309
|
-
version: "1.202.0-preview.
|
|
5309
|
+
version: "1.202.0-preview.142",
|
|
5310
5310
|
description: "Interactive coding agent (Pi) with UiPath LLM Gateway and BYO provider support.",
|
|
5311
5311
|
publishConfig: {
|
|
5312
5312
|
registry: "https://npm.pkg.github.com/"
|
|
@@ -5319,6 +5319,9 @@ var package_default = {
|
|
|
5319
5319
|
bin: {
|
|
5320
5320
|
"coder-tool": "./dist/index.js"
|
|
5321
5321
|
},
|
|
5322
|
+
files: [
|
|
5323
|
+
"dist"
|
|
5324
|
+
],
|
|
5322
5325
|
scripts: {
|
|
5323
5326
|
build: "bun ../../tools/build-tool.ts",
|
|
5324
5327
|
package: "bun run build && bun pm pack",
|
|
@@ -6858,4 +6861,4 @@ var registerCommands = async (program2) => {
|
|
|
6858
6861
|
|
|
6859
6862
|
export { Command, setPreviewBuild, metadata, registerCommands };
|
|
6860
6863
|
|
|
6861
|
-
//# debugId=
|
|
6864
|
+
//# debugId=783DB046975C23A064756E2164756E21
|
package/dist/tool.js
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uipath/coder-tool",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "1.202.0-preview.
|
|
4
|
+
"version": "1.202.0-preview.142",
|
|
5
5
|
"description": "Interactive coding agent (Pi) with UiPath LLM Gateway and BYO provider support.",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"registry": "https://registry.npmjs.org/"
|
|
@@ -14,8 +14,11 @@
|
|
|
14
14
|
"bin": {
|
|
15
15
|
"coder-tool": "./dist/index.js"
|
|
16
16
|
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
17
20
|
"dependencies": {
|
|
18
21
|
"@earendil-works/pi-coding-agent": "^0.80.6"
|
|
19
22
|
},
|
|
20
|
-
"gitHead": "
|
|
23
|
+
"gitHead": "2f7204440851d62ecc9f2d2e202c863293821d90"
|
|
21
24
|
}
|
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
import { Command } from "commander";
|
|
2
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
-
|
|
4
|
-
vi.mock("@earendil-works/pi-coding-agent", () => ({
|
|
5
|
-
main: vi.fn(),
|
|
6
|
-
}));
|
|
7
|
-
|
|
8
|
-
vi.mock("../providers/uipath", () => ({
|
|
9
|
-
createUiPathProviderExtension: vi.fn(() => ({
|
|
10
|
-
name: "uipath-llm-gateway",
|
|
11
|
-
factory: vi.fn(),
|
|
12
|
-
})),
|
|
13
|
-
}));
|
|
14
|
-
|
|
15
|
-
let guideExists = true;
|
|
16
|
-
const GUIDE_TEXT = "# UiPath CLI (`uip`) — agent guidance\n(test copy)";
|
|
17
|
-
vi.mock("@uipath/filesystem", () => ({
|
|
18
|
-
getFileSystem: () => ({
|
|
19
|
-
exists: (path: string) =>
|
|
20
|
-
Promise.resolve(guideExists && path.includes("uipath-cli.md")),
|
|
21
|
-
readFile: () => Promise.resolve(GUIDE_TEXT),
|
|
22
|
-
}),
|
|
23
|
-
}));
|
|
24
|
-
|
|
25
|
-
import { main } from "@earendil-works/pi-coding-agent";
|
|
26
|
-
import { OutputFormatter } from "@uipath/common";
|
|
27
|
-
import { registerChatCommand, withAgentGuide } from "./chat";
|
|
28
|
-
|
|
29
|
-
function buildProgram(context?: { exit: (code: number) => void }): Command {
|
|
30
|
-
const program = new Command();
|
|
31
|
-
program.name("coder").exitOverride();
|
|
32
|
-
registerChatCommand(program, context, vi.fn());
|
|
33
|
-
return program;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function piArgs(): string[] {
|
|
37
|
-
return (vi.mocked(main).mock.calls[0]?.[0] ?? []) as string[];
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
describe("uip coder", () => {
|
|
41
|
-
beforeEach(() => {
|
|
42
|
-
vi.clearAllMocks();
|
|
43
|
-
guideExists = true;
|
|
44
|
-
process.exitCode = undefined;
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
afterEach(() => {
|
|
48
|
-
vi.restoreAllMocks();
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("launches pi with the UiPath agent guide appended to the system prompt", async () => {
|
|
52
|
-
const program = buildProgram();
|
|
53
|
-
|
|
54
|
-
await program.parseAsync([], { from: "user" });
|
|
55
|
-
|
|
56
|
-
expect(main).toHaveBeenCalledTimes(1);
|
|
57
|
-
const args = piArgs();
|
|
58
|
-
expect(args[0]).toBe("--append-system-prompt");
|
|
59
|
-
// The guide is passed as text, not a file path, so the injection
|
|
60
|
-
// does not depend on Pi's file-or-literal argument resolution.
|
|
61
|
-
expect(args[1]).toBe(GUIDE_TEXT);
|
|
62
|
-
expect(args).toHaveLength(2);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("forwards positional arguments and unknown options to pi", async () => {
|
|
66
|
-
const program = buildProgram();
|
|
67
|
-
|
|
68
|
-
await program.parseAsync(
|
|
69
|
-
["-p", "--provider", "uipath", "--model", "gpt-4o", "hello"],
|
|
70
|
-
{ from: "user" },
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
expect(piArgs().slice(2)).toEqual([
|
|
74
|
-
"-p",
|
|
75
|
-
"--provider",
|
|
76
|
-
"uipath",
|
|
77
|
-
"--model",
|
|
78
|
-
"gpt-4o",
|
|
79
|
-
"hello",
|
|
80
|
-
]);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("launches pi without the guide when the template is missing", async () => {
|
|
84
|
-
guideExists = false;
|
|
85
|
-
const program = buildProgram();
|
|
86
|
-
|
|
87
|
-
await program.parseAsync(["-p", "hi"], { from: "user" });
|
|
88
|
-
|
|
89
|
-
expect(piArgs()).toEqual(["-p", "hi"]);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("does not inject the guide before pi package-management subcommands", () => {
|
|
93
|
-
expect(withAgentGuide(["install", "some-ext"], "/guide.md")).toEqual([
|
|
94
|
-
"install",
|
|
95
|
-
"some-ext",
|
|
96
|
-
]);
|
|
97
|
-
expect(withAgentGuide(["-p", "hi"], "/guide.md")).toEqual([
|
|
98
|
-
"--append-system-prompt",
|
|
99
|
-
"/guide.md",
|
|
100
|
-
"-p",
|
|
101
|
-
"hi",
|
|
102
|
-
]);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("keeps the guide for prompts whose first word matches a subcommand name", () => {
|
|
106
|
-
// Pi routes package commands on args[0] only, so an unquoted prompt
|
|
107
|
-
// like `-p update the readme` is a prompt, not `pi update`.
|
|
108
|
-
expect(
|
|
109
|
-
withAgentGuide(["-p", "update", "the", "readme"], "/guide.md"),
|
|
110
|
-
).toEqual([
|
|
111
|
-
"--append-system-prompt",
|
|
112
|
-
"/guide.md",
|
|
113
|
-
"-p",
|
|
114
|
-
"update",
|
|
115
|
-
"the",
|
|
116
|
-
"readme",
|
|
117
|
-
]);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("passes the uipath provider extension to pi", async () => {
|
|
121
|
-
const program = buildProgram();
|
|
122
|
-
|
|
123
|
-
await program.parseAsync([], { from: "user" });
|
|
124
|
-
|
|
125
|
-
const [, options] = vi.mocked(main).mock.calls[0] ?? [];
|
|
126
|
-
const factories =
|
|
127
|
-
(options as { extensionFactories?: unknown[] })
|
|
128
|
-
?.extensionFactories ?? [];
|
|
129
|
-
expect(factories).toHaveLength(1);
|
|
130
|
-
expect(factories[0]).toMatchObject({ name: "uipath-llm-gateway" });
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("turns pi's non-zero process.exit into a failure envelope", async () => {
|
|
134
|
-
const errorSpy = vi
|
|
135
|
-
.spyOn(OutputFormatter, "error")
|
|
136
|
-
.mockImplementation(() => {});
|
|
137
|
-
vi.mocked(main).mockImplementation(async () => {
|
|
138
|
-
// Pi hard-exits on its own error paths instead of throwing.
|
|
139
|
-
process.exit(2);
|
|
140
|
-
});
|
|
141
|
-
const exit = vi.fn();
|
|
142
|
-
const program = buildProgram({ exit });
|
|
143
|
-
|
|
144
|
-
await program.parseAsync(["-p", "hi"], { from: "user" });
|
|
145
|
-
|
|
146
|
-
expect(errorSpy).toHaveBeenCalledWith(
|
|
147
|
-
expect.objectContaining({
|
|
148
|
-
Result: "Failure",
|
|
149
|
-
Message: expect.stringContaining("exited with code 2"),
|
|
150
|
-
}),
|
|
151
|
-
);
|
|
152
|
-
expect(exit).toHaveBeenCalledWith(2);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it("handles process.exit from a fire-and-forget shutdown without an unhandled rejection", async () => {
|
|
156
|
-
// Pi's interactive quit (Ctrl+C / Ctrl+D) calls process.exit(0) from
|
|
157
|
-
// a floating promise chain, not from main()'s awaited stack.
|
|
158
|
-
const rejections: unknown[] = [];
|
|
159
|
-
const onRejection = (reason: unknown) => rejections.push(reason);
|
|
160
|
-
process.on("unhandledRejection", onRejection);
|
|
161
|
-
vi.mocked(main).mockImplementation(
|
|
162
|
-
() =>
|
|
163
|
-
new Promise<void>(() => {
|
|
164
|
-
// never resolves — like an interactive session
|
|
165
|
-
setTimeout(() => {
|
|
166
|
-
void Promise.resolve().then(() => process.exit(0));
|
|
167
|
-
}, 0);
|
|
168
|
-
}),
|
|
169
|
-
);
|
|
170
|
-
const exit = vi.fn();
|
|
171
|
-
const program = buildProgram({ exit });
|
|
172
|
-
|
|
173
|
-
await program.parseAsync([], { from: "user" });
|
|
174
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
175
|
-
|
|
176
|
-
process.off("unhandledRejection", onRejection);
|
|
177
|
-
expect(rejections).toEqual([]);
|
|
178
|
-
expect(exit).not.toHaveBeenCalled();
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it("treats pi's process.exit(0) as success", async () => {
|
|
182
|
-
const errorSpy = vi
|
|
183
|
-
.spyOn(OutputFormatter, "error")
|
|
184
|
-
.mockImplementation(() => {});
|
|
185
|
-
vi.mocked(main).mockImplementation(async () => {
|
|
186
|
-
process.exit(0);
|
|
187
|
-
});
|
|
188
|
-
const exit = vi.fn();
|
|
189
|
-
const program = buildProgram({ exit });
|
|
190
|
-
|
|
191
|
-
await program.parseAsync(["-p", "hi"], { from: "user" });
|
|
192
|
-
|
|
193
|
-
expect(errorSpy).not.toHaveBeenCalled();
|
|
194
|
-
expect(exit).not.toHaveBeenCalled();
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
it("restores process.exit after pi finishes", async () => {
|
|
198
|
-
const originalExit = process.exit;
|
|
199
|
-
const program = buildProgram();
|
|
200
|
-
|
|
201
|
-
await program.parseAsync([], { from: "user" });
|
|
202
|
-
|
|
203
|
-
expect(process.exit).toBe(originalExit);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it("reports a failure envelope and exits 1 when pi fails to start", async () => {
|
|
207
|
-
const errorSpy = vi
|
|
208
|
-
.spyOn(OutputFormatter, "error")
|
|
209
|
-
.mockImplementation(() => {});
|
|
210
|
-
vi.mocked(main).mockRejectedValueOnce(new Error("pi exploded"));
|
|
211
|
-
const exit = vi.fn();
|
|
212
|
-
const program = buildProgram({ exit });
|
|
213
|
-
|
|
214
|
-
await program.parseAsync([], { from: "user" });
|
|
215
|
-
|
|
216
|
-
expect(errorSpy).toHaveBeenCalledWith(
|
|
217
|
-
expect.objectContaining({
|
|
218
|
-
Result: "Failure",
|
|
219
|
-
Message: expect.stringContaining("pi exploded"),
|
|
220
|
-
}),
|
|
221
|
-
);
|
|
222
|
-
expect(exit).toHaveBeenCalledWith(1);
|
|
223
|
-
});
|
|
224
|
-
});
|
package/src/commands/chat.ts
DELETED
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
import { fileURLToPath } from "node:url";
|
|
2
|
-
import {
|
|
3
|
-
type CommandContext,
|
|
4
|
-
OutputFormatter,
|
|
5
|
-
processContext,
|
|
6
|
-
RESULTS,
|
|
7
|
-
} from "@uipath/common";
|
|
8
|
-
import { getFileSystem } from "@uipath/filesystem";
|
|
9
|
-
import type { Command } from "commander";
|
|
10
|
-
import { createUiPathProviderExtension } from "../providers/uipath";
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Pi package-management subcommands. They must stay the first argv token,
|
|
14
|
-
* and they don't start an agent session, so no system prompt is injected.
|
|
15
|
-
*/
|
|
16
|
-
const PI_SUBCOMMANDS = new Set([
|
|
17
|
-
"install",
|
|
18
|
-
"remove",
|
|
19
|
-
"uninstall",
|
|
20
|
-
"update",
|
|
21
|
-
"list",
|
|
22
|
-
"config",
|
|
23
|
-
]);
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Load the bundled UiPath CLI guide appended to Pi's system prompt.
|
|
27
|
-
* Lives at dist/templates/ in the published package (copied by
|
|
28
|
-
* tools/build-tool.ts) and at the package root when running from source.
|
|
29
|
-
* Returns the guide TEXT (not the path) so the injection does not depend
|
|
30
|
-
* on Pi's file-or-literal resolution of `--append-system-prompt`.
|
|
31
|
-
*/
|
|
32
|
-
export async function resolveAgentGuide(): Promise<string | undefined> {
|
|
33
|
-
const fs = getFileSystem();
|
|
34
|
-
const candidates = [
|
|
35
|
-
new URL("./templates/uipath-cli.md", import.meta.url),
|
|
36
|
-
new URL("../../templates/uipath-cli.md", import.meta.url),
|
|
37
|
-
];
|
|
38
|
-
for (const url of candidates) {
|
|
39
|
-
const path = fileURLToPath(url);
|
|
40
|
-
if (await fs.exists(path)) {
|
|
41
|
-
const text = await fs.readFile(path, "utf-8");
|
|
42
|
-
return text ?? undefined;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return undefined;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Prepend `--append-system-prompt <guide>` so the agent knows how to drive
|
|
50
|
-
* the UiPath platform through `uip`. Skipped for Pi's package-management
|
|
51
|
-
* subcommands, which Pi routes on the first argv token only — a prompt like
|
|
52
|
-
* `-p update the readme` must still get the guide.
|
|
53
|
-
*/
|
|
54
|
-
export function withAgentGuide(
|
|
55
|
-
args: string[],
|
|
56
|
-
guideText: string | undefined,
|
|
57
|
-
): string[] {
|
|
58
|
-
if (!guideText) {
|
|
59
|
-
return args;
|
|
60
|
-
}
|
|
61
|
-
const first = args[0];
|
|
62
|
-
if (first !== undefined && PI_SUBCOMMANDS.has(first)) {
|
|
63
|
-
return args;
|
|
64
|
-
}
|
|
65
|
-
return ["--append-system-prompt", guideText, ...args];
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
type HardExit = (code: number) => void;
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Run Pi's CLI entry point with `process.exit` intercepted. Pi hard-exits
|
|
72
|
-
* on both success and failure (deliberately, so misbehaving extensions
|
|
73
|
-
* cannot keep one-shot commands alive); the interception turns that into a
|
|
74
|
-
* return value so failures can be reported through the CLI envelope. The
|
|
75
|
-
* hard exit is re-applied after the command finishes its bookkeeping.
|
|
76
|
-
*
|
|
77
|
-
* The interception resolves a promise instead of throwing: Pi's interactive
|
|
78
|
-
* quit paths (Ctrl+C / Ctrl+D / signal handlers) call `process.exit` from
|
|
79
|
-
* fire-and-forget shutdown chains, where a throw would surface as an
|
|
80
|
-
* unhandled rejection instead of a clean exit.
|
|
81
|
-
*/
|
|
82
|
-
async function runPi(args: string[], hardExit?: HardExit): Promise<number> {
|
|
83
|
-
const { main } = await import("@earendil-works/pi-coding-agent");
|
|
84
|
-
const originalExit = process.exit;
|
|
85
|
-
let requestedExitCode: number | undefined;
|
|
86
|
-
let resolveExit: (code: number) => void = () => {};
|
|
87
|
-
const exitRequested = new Promise<number>((resolve) => {
|
|
88
|
-
resolveExit = resolve;
|
|
89
|
-
});
|
|
90
|
-
process.exit = ((code?: number | string | null) => {
|
|
91
|
-
requestedExitCode =
|
|
92
|
-
typeof code === "number" ? code : (process.exitCode ?? 0);
|
|
93
|
-
resolveExit(requestedExitCode);
|
|
94
|
-
return undefined as never;
|
|
95
|
-
}) as typeof process.exit;
|
|
96
|
-
try {
|
|
97
|
-
const mainDone = main(args, {
|
|
98
|
-
extensionFactories: [createUiPathProviderExtension()],
|
|
99
|
-
}).then(() => requestedExitCode ?? 0);
|
|
100
|
-
// If main settles after an exit was already requested, that outcome
|
|
101
|
-
// has nowhere to go — swallow it so it cannot become an unhandled
|
|
102
|
-
// rejection. The raced result below already carries the exit code.
|
|
103
|
-
mainDone.catch(() => {});
|
|
104
|
-
return await Promise.race([mainDone, exitRequested]);
|
|
105
|
-
} finally {
|
|
106
|
-
process.exit = originalExit;
|
|
107
|
-
// Preserve Pi's lifecycle: force the exit once telemetry and output
|
|
108
|
-
// (which run in microtasks before I/O callbacks) have completed.
|
|
109
|
-
const exit: HardExit = hardExit ?? ((code) => originalExit(code));
|
|
110
|
-
setImmediate(() => exit(process.exitCode ?? requestedExitCode ?? 0));
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* `uip coder [args...]` — the UiPath coding agent, powered by Pi.
|
|
116
|
-
*
|
|
117
|
-
* All arguments are forwarded verbatim to Pi, so the full Pi CLI surface
|
|
118
|
-
* stays available: no args opens the interactive TUI; `-p "<prompt>"` runs
|
|
119
|
-
* one prompt and exits; `--list-models` shows every provider/model;
|
|
120
|
-
* `--provider uipath --model <id>` pins the UiPath LLM Gateway;
|
|
121
|
-
* `--continue` resumes the previous session. See `uip coder --help`
|
|
122
|
-
* (rendered by Pi) for everything else.
|
|
123
|
-
*/
|
|
124
|
-
export function registerChatCommand(
|
|
125
|
-
command: Command,
|
|
126
|
-
context: CommandContext = processContext,
|
|
127
|
-
hardExit?: HardExit,
|
|
128
|
-
): void {
|
|
129
|
-
command
|
|
130
|
-
.argument(
|
|
131
|
-
"[args...]",
|
|
132
|
-
"arguments forwarded to the Pi coding agent (see 'uip coder --help')",
|
|
133
|
-
)
|
|
134
|
-
.allowUnknownOption()
|
|
135
|
-
.allowExcessArguments(true)
|
|
136
|
-
.helpOption(false)
|
|
137
|
-
.trackedAction(
|
|
138
|
-
context,
|
|
139
|
-
async (_args: string[], _options: unknown, cmd: Command) => {
|
|
140
|
-
const guide = await resolveAgentGuide();
|
|
141
|
-
const exitCode = await runPi(
|
|
142
|
-
withAgentGuide(cmd.args, guide),
|
|
143
|
-
hardExit,
|
|
144
|
-
);
|
|
145
|
-
if (exitCode !== 0) {
|
|
146
|
-
OutputFormatter.error({
|
|
147
|
-
Result: RESULTS.Failure,
|
|
148
|
-
Message: `The coding agent exited with code ${exitCode}.`,
|
|
149
|
-
Instructions:
|
|
150
|
-
"Check the agent output above for the underlying error. Run 'uip coder --help' for usage, or 'uip login' if the uipath provider is missing.",
|
|
151
|
-
});
|
|
152
|
-
context.exit(exitCode);
|
|
153
|
-
}
|
|
154
|
-
},
|
|
155
|
-
);
|
|
156
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Standalone entry point for coder-tool
|
|
5
|
-
* This allows the tool to be run independently for testing
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
// Side-effect import: installs Command.prototype.trackedAction on this
|
|
9
|
-
// bundle's commander copy before registerCommands touches it.
|
|
10
|
-
import { setPreviewBuild } from "@uipath/common";
|
|
11
|
-
import { Command } from "commander";
|
|
12
|
-
import { metadata, registerCommands } from "./tool.js";
|
|
13
|
-
|
|
14
|
-
// The standalone bin is a dev/test surface — opt into the preview-gated
|
|
15
|
-
// command unconditionally. The CLI host gates on its own release channel.
|
|
16
|
-
setPreviewBuild(true);
|
|
17
|
-
|
|
18
|
-
const program = new Command();
|
|
19
|
-
|
|
20
|
-
program
|
|
21
|
-
.name(metadata.commandPrefix)
|
|
22
|
-
.description(metadata.description)
|
|
23
|
-
.version(metadata.version);
|
|
24
|
-
|
|
25
|
-
await registerCommands(program);
|
|
26
|
-
|
|
27
|
-
program.parse(process.argv);
|
|
@@ -1,296 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
|
|
3
|
-
vi.mock("@uipath/auth", () => ({
|
|
4
|
-
getLoginStatusAsync: vi.fn(),
|
|
5
|
-
}));
|
|
6
|
-
|
|
7
|
-
vi.mock("@uipath/common", async (importOriginal) => {
|
|
8
|
-
const actual = await importOriginal<typeof import("@uipath/common")>();
|
|
9
|
-
return {
|
|
10
|
-
...actual,
|
|
11
|
-
logger: { debug: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
12
|
-
};
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
import { getLoginStatusAsync } from "@uipath/auth";
|
|
16
|
-
import { createUiPathProviderExtension, UIPATH_PROVIDER_ID } from "./uipath";
|
|
17
|
-
|
|
18
|
-
type EventHandler = (event: unknown) => Promise<unknown>;
|
|
19
|
-
|
|
20
|
-
function buildFakePi() {
|
|
21
|
-
const handlers = new Map<string, EventHandler>();
|
|
22
|
-
return {
|
|
23
|
-
registerProvider: vi.fn(),
|
|
24
|
-
on: vi.fn((event: string, handler: EventHandler) => {
|
|
25
|
-
handlers.set(event, handler);
|
|
26
|
-
}),
|
|
27
|
-
handlers,
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function loggedInStatus(expiresInMs: number) {
|
|
32
|
-
return {
|
|
33
|
-
loginStatus: "Logged in" as const,
|
|
34
|
-
accessToken: "header.payload.signature",
|
|
35
|
-
baseUrl: "https://cloud.uipath.com",
|
|
36
|
-
organizationId: "org-id",
|
|
37
|
-
organizationName: "myorg",
|
|
38
|
-
tenantName: "mytenant",
|
|
39
|
-
expiration: new Date(Date.now() + expiresInMs),
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
describe("createUiPathProviderExtension", () => {
|
|
44
|
-
beforeEach(() => {
|
|
45
|
-
vi.resetAllMocks();
|
|
46
|
-
vi.unstubAllGlobals();
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("skips registration when not logged in", async () => {
|
|
50
|
-
vi.mocked(getLoginStatusAsync).mockResolvedValue({
|
|
51
|
-
loginStatus: "Not logged in",
|
|
52
|
-
});
|
|
53
|
-
const pi = buildFakePi();
|
|
54
|
-
|
|
55
|
-
await createUiPathProviderExtension().factory(pi as never);
|
|
56
|
-
|
|
57
|
-
expect(pi.registerProvider).not.toHaveBeenCalled();
|
|
58
|
-
expect(pi.on).not.toHaveBeenCalled();
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("registers discovered models against the agenthub_ gateway route", async () => {
|
|
62
|
-
vi.mocked(getLoginStatusAsync).mockResolvedValue(
|
|
63
|
-
loggedInStatus(60 * 60 * 1000),
|
|
64
|
-
);
|
|
65
|
-
vi.stubGlobal(
|
|
66
|
-
"fetch",
|
|
67
|
-
vi.fn().mockResolvedValue({
|
|
68
|
-
ok: true,
|
|
69
|
-
json: () =>
|
|
70
|
-
Promise.resolve({
|
|
71
|
-
supportedModels: [
|
|
72
|
-
{
|
|
73
|
-
model: "gpt-4o-2024-11-20",
|
|
74
|
-
contextWindow: 128000,
|
|
75
|
-
},
|
|
76
|
-
"gpt-4o-mini-2024-07-18",
|
|
77
|
-
],
|
|
78
|
-
}),
|
|
79
|
-
}),
|
|
80
|
-
);
|
|
81
|
-
const pi = buildFakePi();
|
|
82
|
-
|
|
83
|
-
await createUiPathProviderExtension().factory(pi as never);
|
|
84
|
-
|
|
85
|
-
expect(pi.registerProvider).toHaveBeenCalledWith(
|
|
86
|
-
UIPATH_PROVIDER_ID,
|
|
87
|
-
expect.objectContaining({
|
|
88
|
-
name: "UiPath LLM Gateway",
|
|
89
|
-
api: "openai-completions",
|
|
90
|
-
apiKey: "header.payload.signature",
|
|
91
|
-
baseUrl:
|
|
92
|
-
"https://cloud.uipath.com/org-id/mytenant/agenthub_/llm/openai",
|
|
93
|
-
}),
|
|
94
|
-
);
|
|
95
|
-
const config = vi.mocked(pi.registerProvider).mock.calls[0]?.[1] as {
|
|
96
|
-
models: Array<{ id: string; baseUrl: string }>;
|
|
97
|
-
};
|
|
98
|
-
expect(config.models).toHaveLength(2);
|
|
99
|
-
expect(config.models[0]?.baseUrl).toBe(
|
|
100
|
-
"https://cloud.uipath.com/org-id/mytenant/agenthub_/llm/openai/deployments/gpt-4o-2024-11-20",
|
|
101
|
-
);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it("accepts a bare-array discovery response", async () => {
|
|
105
|
-
vi.mocked(getLoginStatusAsync).mockResolvedValue(
|
|
106
|
-
loggedInStatus(60 * 60 * 1000),
|
|
107
|
-
);
|
|
108
|
-
vi.stubGlobal(
|
|
109
|
-
"fetch",
|
|
110
|
-
vi.fn().mockResolvedValue({
|
|
111
|
-
ok: true,
|
|
112
|
-
json: () => Promise.resolve(["gpt-4o-2024-11-20"]),
|
|
113
|
-
}),
|
|
114
|
-
);
|
|
115
|
-
const pi = buildFakePi();
|
|
116
|
-
|
|
117
|
-
await createUiPathProviderExtension().factory(pi as never);
|
|
118
|
-
|
|
119
|
-
const config = vi.mocked(pi.registerProvider).mock.calls[0]?.[1] as {
|
|
120
|
-
models: Array<{ id: string }>;
|
|
121
|
-
};
|
|
122
|
-
expect(config.models.map((m) => m.id)).toEqual(["gpt-4o-2024-11-20"]);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it("falls back to default models when discovery returns null", async () => {
|
|
126
|
-
vi.mocked(getLoginStatusAsync).mockResolvedValue(
|
|
127
|
-
loggedInStatus(60 * 60 * 1000),
|
|
128
|
-
);
|
|
129
|
-
vi.stubGlobal(
|
|
130
|
-
"fetch",
|
|
131
|
-
vi.fn().mockResolvedValue({
|
|
132
|
-
ok: true,
|
|
133
|
-
json: () => Promise.resolve(null),
|
|
134
|
-
}),
|
|
135
|
-
);
|
|
136
|
-
const pi = buildFakePi();
|
|
137
|
-
|
|
138
|
-
await createUiPathProviderExtension().factory(pi as never);
|
|
139
|
-
|
|
140
|
-
const config = vi.mocked(pi.registerProvider).mock.calls[0]?.[1] as {
|
|
141
|
-
models: Array<{ id: string }>;
|
|
142
|
-
};
|
|
143
|
-
expect(config.models.length).toBeGreaterThan(0);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it("passes a timeout signal to the discovery request", async () => {
|
|
147
|
-
vi.mocked(getLoginStatusAsync).mockResolvedValue(
|
|
148
|
-
loggedInStatus(60 * 60 * 1000),
|
|
149
|
-
);
|
|
150
|
-
const fetchMock = vi.fn().mockResolvedValue({ ok: false, status: 404 });
|
|
151
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
152
|
-
const pi = buildFakePi();
|
|
153
|
-
|
|
154
|
-
await createUiPathProviderExtension().factory(pi as never);
|
|
155
|
-
|
|
156
|
-
const init = fetchMock.mock.calls[0]?.[1] as { signal?: AbortSignal };
|
|
157
|
-
expect(init.signal).toBeInstanceOf(AbortSignal);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it("falls back to default models when discovery fails", async () => {
|
|
161
|
-
vi.mocked(getLoginStatusAsync).mockResolvedValue(
|
|
162
|
-
loggedInStatus(60 * 60 * 1000),
|
|
163
|
-
);
|
|
164
|
-
vi.stubGlobal(
|
|
165
|
-
"fetch",
|
|
166
|
-
vi.fn().mockResolvedValue({ ok: false, status: 404 }),
|
|
167
|
-
);
|
|
168
|
-
const pi = buildFakePi();
|
|
169
|
-
|
|
170
|
-
await createUiPathProviderExtension().factory(pi as never);
|
|
171
|
-
|
|
172
|
-
const config = vi.mocked(pi.registerProvider).mock.calls[0]?.[1] as {
|
|
173
|
-
models: Array<{ id: string }>;
|
|
174
|
-
};
|
|
175
|
-
expect(config.models.length).toBeGreaterThan(0);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it("encodes model ids when building deployment routes", async () => {
|
|
179
|
-
vi.mocked(getLoginStatusAsync).mockResolvedValue(
|
|
180
|
-
loggedInStatus(60 * 60 * 1000),
|
|
181
|
-
);
|
|
182
|
-
vi.stubGlobal(
|
|
183
|
-
"fetch",
|
|
184
|
-
vi.fn().mockResolvedValue({
|
|
185
|
-
ok: true,
|
|
186
|
-
json: () =>
|
|
187
|
-
Promise.resolve({ supportedModels: ["openai/gpt-4o"] }),
|
|
188
|
-
}),
|
|
189
|
-
);
|
|
190
|
-
const pi = buildFakePi();
|
|
191
|
-
|
|
192
|
-
await createUiPathProviderExtension().factory(pi as never);
|
|
193
|
-
|
|
194
|
-
const config = vi.mocked(pi.registerProvider).mock.calls[0]?.[1] as {
|
|
195
|
-
models: Array<{ id: string; baseUrl: string }>;
|
|
196
|
-
};
|
|
197
|
-
expect(config.models[0]?.id).toBe("openai/gpt-4o");
|
|
198
|
-
expect(config.models[0]?.baseUrl).toBe(
|
|
199
|
-
"https://cloud.uipath.com/org-id/mytenant/agenthub_/llm/openai/deployments/openai%2Fgpt-4o",
|
|
200
|
-
);
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it("skips registration when resolving the login status fails", async () => {
|
|
204
|
-
vi.mocked(getLoginStatusAsync).mockRejectedValue(
|
|
205
|
-
new Error("auth file unreadable"),
|
|
206
|
-
);
|
|
207
|
-
const pi = buildFakePi();
|
|
208
|
-
|
|
209
|
-
await createUiPathProviderExtension().factory(pi as never);
|
|
210
|
-
|
|
211
|
-
expect(pi.registerProvider).not.toHaveBeenCalled();
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it("falls back to default models when discovery returns an unusable shape", async () => {
|
|
215
|
-
vi.mocked(getLoginStatusAsync).mockResolvedValue(
|
|
216
|
-
loggedInStatus(60 * 60 * 1000),
|
|
217
|
-
);
|
|
218
|
-
vi.stubGlobal(
|
|
219
|
-
"fetch",
|
|
220
|
-
vi.fn().mockResolvedValue({
|
|
221
|
-
ok: true,
|
|
222
|
-
json: () => Promise.resolve({ unexpected: true }),
|
|
223
|
-
}),
|
|
224
|
-
);
|
|
225
|
-
const pi = buildFakePi();
|
|
226
|
-
|
|
227
|
-
await createUiPathProviderExtension().factory(pi as never);
|
|
228
|
-
|
|
229
|
-
const config = vi.mocked(pi.registerProvider).mock.calls[0]?.[1] as {
|
|
230
|
-
models: Array<{ id: string }>;
|
|
231
|
-
};
|
|
232
|
-
expect(config.models.length).toBeGreaterThan(0);
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
it("uses organizationName in the gateway path when organizationId is missing", async () => {
|
|
236
|
-
const status = loggedInStatus(60 * 60 * 1000);
|
|
237
|
-
vi.mocked(getLoginStatusAsync).mockResolvedValue({
|
|
238
|
-
...status,
|
|
239
|
-
organizationId: undefined,
|
|
240
|
-
});
|
|
241
|
-
vi.stubGlobal(
|
|
242
|
-
"fetch",
|
|
243
|
-
vi.fn().mockResolvedValue({ ok: false, status: 404 }),
|
|
244
|
-
);
|
|
245
|
-
const pi = buildFakePi();
|
|
246
|
-
|
|
247
|
-
await createUiPathProviderExtension().factory(pi as never);
|
|
248
|
-
|
|
249
|
-
const config = vi.mocked(pi.registerProvider).mock.calls[0]?.[1] as {
|
|
250
|
-
baseUrl: string;
|
|
251
|
-
};
|
|
252
|
-
expect(config.baseUrl).toBe(
|
|
253
|
-
"https://cloud.uipath.com/myorg/mytenant/agenthub_/llm/openai",
|
|
254
|
-
);
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
it("does not re-register on agent_start while the token is fresh", async () => {
|
|
258
|
-
vi.mocked(getLoginStatusAsync).mockResolvedValue(
|
|
259
|
-
loggedInStatus(60 * 60 * 1000),
|
|
260
|
-
);
|
|
261
|
-
vi.stubGlobal(
|
|
262
|
-
"fetch",
|
|
263
|
-
vi.fn().mockResolvedValue({ ok: false, status: 404 }),
|
|
264
|
-
);
|
|
265
|
-
const pi = buildFakePi();
|
|
266
|
-
|
|
267
|
-
await createUiPathProviderExtension().factory(pi as never);
|
|
268
|
-
expect(pi.registerProvider).toHaveBeenCalledTimes(1);
|
|
269
|
-
|
|
270
|
-
await pi.handlers.get("agent_start")?.({ type: "agent_start" });
|
|
271
|
-
|
|
272
|
-
expect(pi.registerProvider).toHaveBeenCalledTimes(1);
|
|
273
|
-
expect(getLoginStatusAsync).toHaveBeenCalledTimes(1);
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
it("re-registers with a fresh token on agent_start when close to expiry", async () => {
|
|
277
|
-
vi.mocked(getLoginStatusAsync).mockResolvedValue(
|
|
278
|
-
loggedInStatus(60 * 1000),
|
|
279
|
-
);
|
|
280
|
-
vi.stubGlobal(
|
|
281
|
-
"fetch",
|
|
282
|
-
vi.fn().mockResolvedValue({ ok: false, status: 404 }),
|
|
283
|
-
);
|
|
284
|
-
const pi = buildFakePi();
|
|
285
|
-
|
|
286
|
-
await createUiPathProviderExtension().factory(pi as never);
|
|
287
|
-
expect(pi.registerProvider).toHaveBeenCalledTimes(1);
|
|
288
|
-
|
|
289
|
-
const handler = pi.handlers.get("agent_start");
|
|
290
|
-
expect(handler).toBeDefined();
|
|
291
|
-
await handler?.({ type: "agent_start" });
|
|
292
|
-
|
|
293
|
-
expect(pi.registerProvider).toHaveBeenCalledTimes(2);
|
|
294
|
-
expect(getLoginStatusAsync).toHaveBeenCalledTimes(2);
|
|
295
|
-
});
|
|
296
|
-
});
|
package/src/providers/uipath.ts
DELETED
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ExtensionAPI,
|
|
3
|
-
ProviderModelConfig,
|
|
4
|
-
} from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import { getLoginStatusAsync, type LoginStatus } from "@uipath/auth";
|
|
6
|
-
import { catchError, logger } from "@uipath/common";
|
|
7
|
-
|
|
8
|
-
export const UIPATH_PROVIDER_ID = "uipath";
|
|
9
|
-
|
|
10
|
-
const TOKEN_VALIDITY_MINUTES = 30;
|
|
11
|
-
const REFRESH_THRESHOLD_MS = 5 * 60 * 1000;
|
|
12
|
-
const DEFAULT_CONTEXT_WINDOW = 128000;
|
|
13
|
-
const DEFAULT_MAX_TOKENS = 16384;
|
|
14
|
-
// Pi waits for extension factories before startup, so a stalled gateway
|
|
15
|
-
// must not block `uip coder` — cap discovery and fall back to the curated list.
|
|
16
|
-
const MODEL_DISCOVERY_TIMEOUT_MS = 5000;
|
|
17
|
-
|
|
18
|
-
/** Used when the LLM Gateway model discovery endpoint is unreachable. */
|
|
19
|
-
const FALLBACK_MODEL_IDS = ["gpt-4o-2024-11-20", "gpt-4o-mini-2024-07-18"];
|
|
20
|
-
|
|
21
|
-
interface DiscoveredModel {
|
|
22
|
-
id: string;
|
|
23
|
-
contextWindow?: number;
|
|
24
|
-
maxTokens?: number;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
interface ReadyLoginStatus {
|
|
28
|
-
accessToken: string;
|
|
29
|
-
gatewayBase: string;
|
|
30
|
-
expiration?: Date;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const asReadyStatus = (status: LoginStatus): ReadyLoginStatus | undefined => {
|
|
34
|
-
// Same org/tenant path segments as @uipath/agenthub-sdk's client factory.
|
|
35
|
-
const organization = status.organizationId ?? status.organizationName;
|
|
36
|
-
const tenant = status.tenantName ?? status.tenantId;
|
|
37
|
-
if (
|
|
38
|
-
status.loginStatus !== "Logged in" ||
|
|
39
|
-
!status.accessToken ||
|
|
40
|
-
!status.baseUrl ||
|
|
41
|
-
!organization ||
|
|
42
|
-
!tenant
|
|
43
|
-
) {
|
|
44
|
-
return undefined;
|
|
45
|
-
}
|
|
46
|
-
return {
|
|
47
|
-
accessToken: status.accessToken,
|
|
48
|
-
gatewayBase: `${status.baseUrl}/${organization}/${tenant}/agenthub_/llm`,
|
|
49
|
-
expiration: status.expiration,
|
|
50
|
-
};
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Model discovery via the gateway-normalized endpoint. The response shape is
|
|
55
|
-
* tolerated loosely (array of ids or objects) since the contract is evolving.
|
|
56
|
-
*/
|
|
57
|
-
const discoverModels = async (
|
|
58
|
-
ready: ReadyLoginStatus,
|
|
59
|
-
): Promise<DiscoveredModel[] | undefined> => {
|
|
60
|
-
const [error, response] = await catchError(
|
|
61
|
-
fetch(`${ready.gatewayBase}/api/chat/completions`, {
|
|
62
|
-
headers: { Authorization: `Bearer ${ready.accessToken}` },
|
|
63
|
-
signal: AbortSignal.timeout(MODEL_DISCOVERY_TIMEOUT_MS),
|
|
64
|
-
}),
|
|
65
|
-
);
|
|
66
|
-
if (error || !response.ok) {
|
|
67
|
-
logger.debug(
|
|
68
|
-
`[coder-tool] LLM Gateway model discovery failed: ${error?.message ?? response?.status}`,
|
|
69
|
-
);
|
|
70
|
-
return undefined;
|
|
71
|
-
}
|
|
72
|
-
const [parseError, parsed] = await catchError(
|
|
73
|
-
response.json() as Promise<unknown>,
|
|
74
|
-
);
|
|
75
|
-
if (parseError) {
|
|
76
|
-
return undefined;
|
|
77
|
-
}
|
|
78
|
-
const body =
|
|
79
|
-
parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)
|
|
80
|
-
? (parsed as Record<string, unknown>)
|
|
81
|
-
: undefined;
|
|
82
|
-
const rawModels = Array.isArray(parsed)
|
|
83
|
-
? parsed
|
|
84
|
-
: (body?.supportedModels ?? body?.models ?? body?.data);
|
|
85
|
-
if (!Array.isArray(rawModels)) {
|
|
86
|
-
return undefined;
|
|
87
|
-
}
|
|
88
|
-
const models = rawModels
|
|
89
|
-
.map((entry: unknown): DiscoveredModel | undefined => {
|
|
90
|
-
if (typeof entry === "string") {
|
|
91
|
-
return { id: entry };
|
|
92
|
-
}
|
|
93
|
-
if (entry && typeof entry === "object") {
|
|
94
|
-
const record = entry as Record<string, unknown>;
|
|
95
|
-
const id = record.model ?? record.name ?? record.id;
|
|
96
|
-
if (typeof id === "string" && id.length > 0) {
|
|
97
|
-
return {
|
|
98
|
-
id,
|
|
99
|
-
contextWindow:
|
|
100
|
-
typeof record.contextWindow === "number"
|
|
101
|
-
? record.contextWindow
|
|
102
|
-
: undefined,
|
|
103
|
-
maxTokens:
|
|
104
|
-
typeof record.maxTokens === "number"
|
|
105
|
-
? record.maxTokens
|
|
106
|
-
: undefined,
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
return undefined;
|
|
111
|
-
})
|
|
112
|
-
.filter((model): model is DiscoveredModel => model !== undefined);
|
|
113
|
-
return models.length > 0 ? models : undefined;
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
const toProviderModels = (
|
|
117
|
-
ready: ReadyLoginStatus,
|
|
118
|
-
models: DiscoveredModel[],
|
|
119
|
-
): ProviderModelConfig[] =>
|
|
120
|
-
models.map((model) => ({
|
|
121
|
-
id: model.id,
|
|
122
|
-
name: `${model.id} (UiPath)`,
|
|
123
|
-
// Azure-style route: the OpenAI client appends /chat/completions.
|
|
124
|
-
// The id is encoded so provider-qualified ids (e.g. "openai/gpt-4o")
|
|
125
|
-
// stay a single path segment.
|
|
126
|
-
baseUrl: `${ready.gatewayBase}/openai/deployments/${encodeURIComponent(model.id)}`,
|
|
127
|
-
reasoning: false,
|
|
128
|
-
input: ["text"],
|
|
129
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
130
|
-
contextWindow: model.contextWindow ?? DEFAULT_CONTEXT_WINDOW,
|
|
131
|
-
maxTokens: model.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
132
|
-
}));
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Registers the "uipath" provider backed by the LLM Gateway front door
|
|
136
|
-
* (agenthub_). Skipped silently when no `uip login` session exists so the
|
|
137
|
-
* other providers keep working.
|
|
138
|
-
*/
|
|
139
|
-
export const createUiPathProviderExtension = () => {
|
|
140
|
-
let cachedModels: DiscoveredModel[] | undefined;
|
|
141
|
-
let tokenExpiration: Date | undefined;
|
|
142
|
-
|
|
143
|
-
const register = async (pi: ExtensionAPI): Promise<boolean> => {
|
|
144
|
-
const [error, status] = await catchError(
|
|
145
|
-
getLoginStatusAsync({
|
|
146
|
-
ensureTokenValidityMinutes: TOKEN_VALIDITY_MINUTES,
|
|
147
|
-
}),
|
|
148
|
-
);
|
|
149
|
-
if (error) {
|
|
150
|
-
logger.debug(
|
|
151
|
-
`[coder-tool] Could not resolve UiPath login: ${error.message}`,
|
|
152
|
-
);
|
|
153
|
-
return false;
|
|
154
|
-
}
|
|
155
|
-
const ready = asReadyStatus(status);
|
|
156
|
-
if (!ready) {
|
|
157
|
-
logger.debug(
|
|
158
|
-
"[coder-tool] Not logged in to UiPath — 'uipath' provider not registered. Run 'uip login' to enable it.",
|
|
159
|
-
);
|
|
160
|
-
return false;
|
|
161
|
-
}
|
|
162
|
-
tokenExpiration = ready.expiration;
|
|
163
|
-
cachedModels ??= await discoverModels(ready);
|
|
164
|
-
const models = cachedModels ?? FALLBACK_MODEL_IDS.map((id) => ({ id }));
|
|
165
|
-
pi.registerProvider(UIPATH_PROVIDER_ID, {
|
|
166
|
-
name: "UiPath LLM Gateway",
|
|
167
|
-
baseUrl: `${ready.gatewayBase}/openai`,
|
|
168
|
-
apiKey: ready.accessToken,
|
|
169
|
-
api: "openai-completions",
|
|
170
|
-
models: toProviderModels(ready, models),
|
|
171
|
-
});
|
|
172
|
-
return true;
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
return {
|
|
176
|
-
name: "uipath-llm-gateway",
|
|
177
|
-
factory: async (pi: ExtensionAPI): Promise<void> => {
|
|
178
|
-
const registered = await register(pi);
|
|
179
|
-
if (!registered) {
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
// UiPath access tokens are short-lived; re-register with a fresh
|
|
183
|
-
// token when the current one is close to expiry.
|
|
184
|
-
pi.on("agent_start", async () => {
|
|
185
|
-
const expiresSoon =
|
|
186
|
-
tokenExpiration === undefined ||
|
|
187
|
-
tokenExpiration.getTime() - Date.now() <
|
|
188
|
-
REFRESH_THRESHOLD_MS;
|
|
189
|
-
if (expiresSoon) {
|
|
190
|
-
await register(pi);
|
|
191
|
-
}
|
|
192
|
-
});
|
|
193
|
-
},
|
|
194
|
-
};
|
|
195
|
-
};
|
package/src/tool.spec.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { setPreviewBuild } from "@uipath/common";
|
|
2
|
-
import { Command } from "commander";
|
|
3
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
4
|
-
import { metadata, registerCommands } from "./tool";
|
|
5
|
-
|
|
6
|
-
describe("coder-tool", () => {
|
|
7
|
-
afterEach(() => {
|
|
8
|
-
setPreviewBuild(false);
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
it("exposes the expected metadata", () => {
|
|
12
|
-
expect(metadata.name).toBe("coder-tool");
|
|
13
|
-
expect(metadata.commandPrefix).toBe("coder");
|
|
14
|
-
expect(metadata.version).toMatch(/^\d+\.\d+\.\d+/);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("registers a passthrough action on preview builds", async () => {
|
|
18
|
-
setPreviewBuild(true);
|
|
19
|
-
const program = new Command();
|
|
20
|
-
program.name("coder").exitOverride();
|
|
21
|
-
await registerCommands(program);
|
|
22
|
-
|
|
23
|
-
// The action lives on the tool command itself so `uip coder [args...]`
|
|
24
|
-
// forwards everything to Pi.
|
|
25
|
-
const usage = program.usage();
|
|
26
|
-
expect(usage).toContain("[args...]");
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("registers nothing on stable builds", async () => {
|
|
30
|
-
setPreviewBuild(false);
|
|
31
|
-
const program = new Command();
|
|
32
|
-
program.name("coder").exitOverride();
|
|
33
|
-
await registerCommands(program);
|
|
34
|
-
|
|
35
|
-
expect(program.usage()).not.toContain("[args...]");
|
|
36
|
-
expect(program.registeredArguments).toHaveLength(0);
|
|
37
|
-
});
|
|
38
|
-
});
|
package/src/tool.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { previewOnly } from "@uipath/common";
|
|
2
|
-
import type { Command } from "commander";
|
|
3
|
-
import pkg from "../package.json" with { type: "json" };
|
|
4
|
-
import { registerChatCommand } from "./commands/chat";
|
|
5
|
-
|
|
6
|
-
export const metadata = {
|
|
7
|
-
name: "coder-tool",
|
|
8
|
-
version: pkg.version,
|
|
9
|
-
description:
|
|
10
|
-
"Build UiPath automations with an AI agent (Pi) that drives uip — via UiPath LLM Gateway or your own model keys.",
|
|
11
|
-
commandPrefix: "coder",
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
export const registerCommands = async (program: Command): Promise<void> => {
|
|
15
|
-
// Preview builds only until the tool-permission posture for an
|
|
16
|
-
// autonomous agent in customer environments is settled (PR #3011
|
|
17
|
-
// review): Pi executes tool calls without per-call approval, so the
|
|
18
|
-
// command stays off stable builds for now.
|
|
19
|
-
previewOnly(() => registerChatCommand(program));
|
|
20
|
-
};
|
package/templates/uipath-cli.md
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
# UiPath CLI (`uip`) — agent guidance
|
|
2
|
-
|
|
3
|
-
You are running inside `uip coder`, the UiPath automation-building agent of the UiPath CLI. Your purpose is helping users build, ship, and operate **UiPath automations** — agent projects, processes, Orchestrator resources, platform data — not general-purpose software development. When a request is unrelated to UiPath work, help briefly but steer toward the automation task at hand.
|
|
4
|
-
|
|
5
|
-
The `uip` command line tool is available in this environment and is your primary way to work with the UiPath platform: build and run automations, manage agents and processes, and query platform resources. Prefer `uip` over raw REST calls — it handles auth, tenant routing, and pagination for you.
|
|
6
|
-
|
|
7
|
-
## Ground rules
|
|
8
|
-
|
|
9
|
-
- Discover before guessing: `uip --help` lists tools; `uip <tool> --help` and `uip <tool> <command> --help` list commands and options. Command surfaces evolve — trust `--help` over memory.
|
|
10
|
-
- Every command prints a JSON envelope: `{"Result": "Success", "Code": ..., "Data": ...}` on success (exit 0) or `{"Result": "Failure" | "AuthenticationError" | "ValidationError" | ..., "Message": ..., "Instructions": ...}` on error (exit 1-4). Parse `Data` for results; `Instructions` tells you how to recover.
|
|
11
|
-
- Check auth first when platform calls fail: `uip login status`. If expired or missing, ask the user to run `uip login` — never handle credentials yourself.
|
|
12
|
-
- Commands run against the logged-in org/tenant. `uip login status` shows which one.
|
|
13
|
-
- Be careful with destructive commands (delete, undeploy, job stop). Confirm with the user before running them against shared tenants.
|
|
14
|
-
|
|
15
|
-
## Common tools (run `uip <tool> --help` for the full surface)
|
|
16
|
-
|
|
17
|
-
| Tool | Purpose |
|
|
18
|
-
| --- | --- |
|
|
19
|
-
| `uip agent` | UiPath Agents: init, validate, refresh, review, debug, evaluate agent projects |
|
|
20
|
-
| `uip maestro` | Maestro process orchestration: BPMN, flows, deployments, instances |
|
|
21
|
-
| `uip or` | Orchestrator: folders, assets, queues, processes, jobs, buckets |
|
|
22
|
-
| `uip df` | Data Fabric: entities, records, queries, CSV import |
|
|
23
|
-
| `uip is` | Integration Service: connections, connectors, triggers |
|
|
24
|
-
| `uip context-grounding` | Context Grounding (ECS) indexes and semantic search |
|
|
25
|
-
| `uip solution` | Solution packages: init, publish, resync |
|
|
26
|
-
| `uip tm` | Test Manager: projects, test cases, requirements |
|
|
27
|
-
| `uip tasks` | Action Center tasks |
|
|
28
|
-
| `uip platform` | Cross-platform resources (orgs, tenants, services) |
|
|
29
|
-
|
|
30
|
-
Not every tool is installed by default — a missing tool auto-installs on first use, or run `uip tools install <name>`.
|
|
31
|
-
|
|
32
|
-
## Typical automation workflows
|
|
33
|
-
|
|
34
|
-
- Build and ship an agent: `uip agent init` → edit → `uip solution pack` → `uip solution publish` → `uip solution deploy`.
|
|
35
|
-
- Operate Orchestrator: `uip or jobs list --status Failed`, `uip or assets list`, `uip or queues items list`.
|
|
36
|
-
- Work with data: `uip df entities list`, `uip df records query <entity> --filter ...`, `uip df records import <entity> --file data.csv`.
|
|
37
|
-
|
|
38
|
-
When a task involves UiPath resources, reach for the matching `uip` tool before writing custom scripts.
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
// E2E coverage for scenarios/coder-tool/coding-agent.md.
|
|
2
|
-
//
|
|
3
|
-
// All scenarios need either a live LLM Gateway session (S3/S4), a BYO
|
|
4
|
-
// provider key (S5), or a packed-CLI harness run with network access
|
|
5
|
-
// (S1/S2/S6) — none of which the offline CI e2e lane provides today.
|
|
6
|
-
// They were verified manually against alpha (see the PR description);
|
|
7
|
-
// the placeholders below keep the mandatory scenario ↔ e2e mapping.
|
|
8
|
-
import { describe, it } from "vitest";
|
|
9
|
-
|
|
10
|
-
describe("coding-agent", () => {
|
|
11
|
-
it.skip("S1: forwards arguments to the pi coding agent", () => {});
|
|
12
|
-
it.skip("S2: lists models without a uipath provider when logged out", () => {});
|
|
13
|
-
it.skip("S3: registers the uipath provider when logged in", () => {});
|
|
14
|
-
it.skip("S4: completes a print-mode prompt through the uipath provider", () => {});
|
|
15
|
-
it.skip("S5: completes a print-mode prompt through a BYO provider", () => {});
|
|
16
|
-
it.skip("S6: reports a failure envelope when pi cannot start", () => {});
|
|
17
|
-
});
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { describe, it } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
type E2EManifest,
|
|
4
|
-
MANIFEST_PATH,
|
|
5
|
-
} from "../../../tests/e2e-shared/global-setup";
|
|
6
|
-
import {
|
|
7
|
-
assertHelpCommandDuration,
|
|
8
|
-
EXEC_TIMEOUT,
|
|
9
|
-
readManifest,
|
|
10
|
-
} from "../../../tests/e2e-shared/helpers";
|
|
11
|
-
|
|
12
|
-
const HELP_BUDGET_MS = 1_000;
|
|
13
|
-
|
|
14
|
-
const manifest = readManifest<E2EManifest>(MANIFEST_PATH);
|
|
15
|
-
const projectDir = manifest.sharedInstall.tempDir;
|
|
16
|
-
|
|
17
|
-
// Performance budgets are unreliable on Windows CI runners; skip there.
|
|
18
|
-
describe.skipIf(process.platform === "win32")("coder performance", () => {
|
|
19
|
-
it(
|
|
20
|
-
`uip coder -h completes within ${HELP_BUDGET_MS}ms`,
|
|
21
|
-
() => {
|
|
22
|
-
assertHelpCommandDuration({
|
|
23
|
-
projectDir,
|
|
24
|
-
packageName: "@uipath/coder-tool",
|
|
25
|
-
args: "coder -h",
|
|
26
|
-
thresholdMs: HELP_BUDGET_MS,
|
|
27
|
-
testFileUrl: import.meta.url,
|
|
28
|
-
});
|
|
29
|
-
},
|
|
30
|
-
EXEC_TIMEOUT,
|
|
31
|
-
);
|
|
32
|
-
});
|
package/tsconfig.json
DELETED
package/vitest.config.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { default } from "../../vitest.base.config";
|