@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 CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  metadata,
5
5
  registerCommands,
6
6
  setPreviewBuild
7
- } from "./tool-d4a6hxqe.js";
7
+ } from "./tool-5mxjdn16.js";
8
8
  import"./tool-1de529jm.js";
9
9
 
10
10
  // src/index.ts
@@ -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.140",
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=A4018A6A18B8369264756E2164756E21
6864
+ //# debugId=783DB046975C23A064756E2164756E21
package/dist/tool.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  metadata,
3
3
  registerCommands
4
- } from "./tool-d4a6hxqe.js";
4
+ } from "./tool-5mxjdn16.js";
5
5
  import"./tool-1de529jm.js";
6
6
  export {
7
7
  metadata,
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.140",
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": "1861498d8a158a710a718c04614523ba59c93781"
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
- });
@@ -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
- });
@@ -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
- };
@@ -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
@@ -1,9 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "compilerOptions": {
4
- "outDir": "./dist",
5
- "types": ["node"]
6
- },
7
- "include": ["src/**/*", "vitest*.ts"],
8
- "exclude": ["node_modules", "dist"]
9
- }
package/vitest.config.ts DELETED
@@ -1 +0,0 @@
1
- export { default } from "../../vitest.base.config";