@docyrus/docyrus 0.0.76 → 0.0.78

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docyrus/docyrus",
3
- "version": "0.0.76",
3
+ "version": "0.0.78",
4
4
  "private": false,
5
5
  "description": "Docyrus API CLI",
6
6
  "main": "./main.js",
@@ -0,0 +1,322 @@
1
+ /**
2
+ * Docyrus authentication tool for the pi coding agent.
3
+ *
4
+ * Registers a single tool `authenticate_docyrus` that the agent can call to
5
+ * authenticate the Docyrus CLI in the current working directory.
6
+ *
7
+ * Two execution modes are supported, selected at call time:
8
+ *
9
+ * - Server desktop mode (DOCYRUS_DESKTOP_TOOLS=1):
10
+ * The execute handler routes the call to the desktop client via
11
+ * ctx.ui.input("authenticate_docyrus", paramsJson). The Electron host
12
+ * detects the tool name, runs the OAuth2 device flow on behalf of the
13
+ * user (in the same cwd) and replies with the authenticated profile.
14
+ *
15
+ * - TUI / non-desktop mode:
16
+ * Pi owns the active terminal, so we cannot run `docyrus auth login`
17
+ * inline. Instead the handler opens a new OS terminal window, runs
18
+ * `docyrus auth login` interactively, and polls `docyrus auth who --json`
19
+ * in the background until the user finishes (or times out).
20
+ */
21
+ import { spawn } from "node:child_process";
22
+ import { promisify } from "node:util";
23
+ import { execFile as execFileCb } from "node:child_process";
24
+ import { Type } from "typebox";
25
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
26
+
27
+ const execFile = promisify(execFileCb);
28
+
29
+ const TOOL_NAME = "authenticate_docyrus";
30
+ const DESKTOP_TIMEOUT_MS = 5 * 60_000;
31
+ const TUI_POLL_INTERVAL_MS = 2_000;
32
+ const TUI_TIMEOUT_MS = 10 * 60_000;
33
+
34
+ interface IAuthenticateInput {
35
+ scope?: string;
36
+ clientId?: string;
37
+ }
38
+
39
+ interface IAuthenticateResult {
40
+ authenticated: boolean;
41
+ apiBaseUrl?: string;
42
+ userId?: string;
43
+ email?: string;
44
+ tenantId?: string;
45
+ tenantName?: string;
46
+ tenantNo?: number;
47
+ scope?: string;
48
+ }
49
+
50
+ function toolResult(payload: unknown) {
51
+ const text = typeof payload === "string" ? payload : JSON.stringify(payload, null, 2);
52
+ return { content: [{ type: "text" as const, text }] };
53
+ }
54
+
55
+ function toolError(message: string) {
56
+ return { content: [{ type: "text" as const, text: message }], isError: true };
57
+ }
58
+
59
+ function readDocyrusCliEnvironment(env: NodeJS.ProcessEnv = process.env): {
60
+ executable: string;
61
+ entryPath: string;
62
+ scope: "local" | "global";
63
+ } {
64
+ const executable = env.DOCYRUS_CLI_EXECUTABLE?.trim();
65
+ const entryPath = env.DOCYRUS_CLI_ENTRY?.trim();
66
+ const rawScope = env.DOCYRUS_CLI_SCOPE?.trim();
67
+ const scope = rawScope === "global" ? "global" : "local";
68
+ if (!executable || !entryPath) {
69
+ throw new Error("Missing Docyrus CLI runtime env. Expected DOCYRUS_CLI_EXECUTABLE and DOCYRUS_CLI_ENTRY.");
70
+ }
71
+ return { executable, entryPath, scope };
72
+ }
73
+
74
+ function buildAuthLoginArgs(input: IAuthenticateInput, scope: "local" | "global"): string[] {
75
+ const args: string[] = [];
76
+ if (scope === "global") {
77
+ args.push("-g");
78
+ }
79
+ args.push("auth", "login");
80
+ if (input.scope?.trim()) {
81
+ args.push("--scope", input.scope.trim());
82
+ }
83
+ if (input.clientId?.trim()) {
84
+ args.push("--clientId", input.clientId.trim());
85
+ }
86
+ return args;
87
+ }
88
+
89
+ function buildAuthWhoArgs(scope: "local" | "global"): string[] {
90
+ const args: string[] = [];
91
+ if (scope === "global") {
92
+ args.push("-g");
93
+ }
94
+ args.push("auth", "who", "--json");
95
+ return args;
96
+ }
97
+
98
+ function shellQuote(value: string): string {
99
+ return `'${value.replace(/'/g, `'\\''`)}'`;
100
+ }
101
+
102
+ function spawnInteractiveTerminal(params: {
103
+ executable: string;
104
+ entryPath: string;
105
+ args: string[];
106
+ cwd: string;
107
+ }): void {
108
+ const platform = process.platform;
109
+ const fullCommand = [params.executable, params.entryPath, ...params.args]
110
+ .map(shellQuote)
111
+ .join(" ");
112
+
113
+ if (platform === "darwin") {
114
+ const script = `cd ${shellQuote(params.cwd)} && ${fullCommand}`;
115
+ const osa = `tell application "Terminal"
116
+ activate
117
+ do script ${JSON.stringify(script)}
118
+ end tell`;
119
+ spawn("osascript", ["-e", osa], { stdio: "ignore", detached: true }).unref();
120
+ return;
121
+ }
122
+
123
+ if (platform === "win32") {
124
+ const winCommand = `cd /d ${shellQuote(params.cwd)} && ${fullCommand}`;
125
+ spawn("cmd", ["/c", "start", "", "cmd", "/k", winCommand], {
126
+ stdio: "ignore",
127
+ detached: true,
128
+ windowsHide: false,
129
+ }).unref();
130
+ return;
131
+ }
132
+
133
+ const candidates = ["x-terminal-emulator", "gnome-terminal", "konsole", "xterm"];
134
+ const linuxCommand = `cd ${shellQuote(params.cwd)} && ${fullCommand}; echo; read -n1 -rsp 'Press any key to close...'`;
135
+ for (const candidate of candidates) {
136
+ try {
137
+ spawn(candidate, ["-e", "bash", "-c", linuxCommand], { stdio: "ignore", detached: true }).unref();
138
+ return;
139
+ }
140
+ catch {
141
+ // try next candidate
142
+ }
143
+ }
144
+ throw new Error("No supported terminal emulator was found. Run `docyrus auth login` manually in another terminal.");
145
+ }
146
+
147
+ async function readAuthWho(params: {
148
+ executable: string;
149
+ entryPath: string;
150
+ scope: "local" | "global";
151
+ cwd: string;
152
+ }): Promise<IAuthenticateResult | null> {
153
+ try {
154
+ const { stdout } = await execFile(
155
+ params.executable,
156
+ [params.entryPath, ...buildAuthWhoArgs(params.scope)],
157
+ { cwd: params.cwd },
158
+ );
159
+ const parsed = JSON.parse(stdout) as Record<string, unknown>;
160
+ const data = (parsed.data ?? parsed) as Record<string, unknown> | undefined;
161
+ const ctx = (parsed.context ?? null) as Record<string, unknown> | null;
162
+ if (!data || typeof data !== "object") {
163
+ return null;
164
+ }
165
+
166
+ return {
167
+ authenticated: true,
168
+ userId: typeof data.id === "string" ? data.id : undefined,
169
+ email: typeof data.email === "string" ? data.email : undefined,
170
+ tenantId: ctx && typeof ctx.tenantId === "string" ? ctx.tenantId : undefined,
171
+ tenantName: ctx && typeof ctx.tenantName === "string" ? ctx.tenantName : undefined,
172
+ tenantNo: ctx && typeof ctx.tenantNo === "number" ? ctx.tenantNo : undefined,
173
+ };
174
+ }
175
+ catch {
176
+ return null;
177
+ }
178
+ }
179
+
180
+ async function pollUntilAuthenticated(params: {
181
+ executable: string;
182
+ entryPath: string;
183
+ scope: "local" | "global";
184
+ cwd: string;
185
+ signal?: AbortSignal;
186
+ baseline: IAuthenticateResult | null;
187
+ }): Promise<IAuthenticateResult | null> {
188
+ const deadline = Date.now() + TUI_TIMEOUT_MS;
189
+ while (Date.now() < deadline) {
190
+ if (params.signal?.aborted) {
191
+ return null;
192
+ }
193
+ const profile = await readAuthWho(params);
194
+ if (profile && profile.userId
195
+ && (!params.baseline || params.baseline.userId !== profile.userId || params.baseline.tenantId !== profile.tenantId)) {
196
+ return profile;
197
+ }
198
+ await new Promise<void>((resolve) => {
199
+ const timer = setTimeout(resolve, TUI_POLL_INTERVAL_MS);
200
+ params.signal?.addEventListener("abort", () => {
201
+ clearTimeout(timer);
202
+ resolve();
203
+ }, { once: true });
204
+ });
205
+ }
206
+ return null;
207
+ }
208
+
209
+ async function executeDesktop(params: {
210
+ input: IAuthenticateInput;
211
+ ctx: { ui: { input: (title: string, placeholder?: string, opts?: { signal?: AbortSignal; timeout?: number }) => Promise<string | undefined> } };
212
+ signal: AbortSignal | undefined;
213
+ }) {
214
+ const paramsJson = JSON.stringify(params.input ?? {});
215
+ const resultJson = await params.ctx.ui.input(TOOL_NAME, paramsJson, {
216
+ signal: params.signal,
217
+ timeout: DESKTOP_TIMEOUT_MS,
218
+ });
219
+
220
+ if (!resultJson) {
221
+ return toolError("Desktop authentication was cancelled or timed out.");
222
+ }
223
+
224
+ try {
225
+ return toolResult(JSON.parse(resultJson));
226
+ }
227
+ catch {
228
+ return toolResult(resultJson);
229
+ }
230
+ }
231
+
232
+ async function executeTui(params: {
233
+ input: IAuthenticateInput;
234
+ ctx: {
235
+ cwd?: string;
236
+ hasUI: boolean;
237
+ ui: {
238
+ notify?: (message: string, level?: "info" | "warning" | "error") => void;
239
+ confirm?: (title: string, message: string, opts?: { signal?: AbortSignal; timeout?: number }) => Promise<boolean>;
240
+ };
241
+ };
242
+ signal: AbortSignal | undefined;
243
+ }) {
244
+ const env = readDocyrusCliEnvironment();
245
+ const cwd = params.ctx.cwd || process.cwd();
246
+ const baseline = await readAuthWho({
247
+ executable: env.executable,
248
+ entryPath: env.entryPath,
249
+ scope: env.scope,
250
+ cwd,
251
+ });
252
+
253
+ const launchArgs = buildAuthLoginArgs(params.input ?? {}, env.scope);
254
+ spawnInteractiveTerminal({
255
+ executable: env.executable,
256
+ entryPath: env.entryPath,
257
+ args: launchArgs,
258
+ cwd,
259
+ });
260
+
261
+ if (params.ctx.hasUI && params.ctx.ui.notify) {
262
+ params.ctx.ui.notify(
263
+ "Opened a new terminal to run `docyrus auth login`. Complete the device flow there; this tool will detect when login succeeds.",
264
+ "info",
265
+ );
266
+ }
267
+
268
+ const profile = await pollUntilAuthenticated({
269
+ executable: env.executable,
270
+ entryPath: env.entryPath,
271
+ scope: env.scope,
272
+ cwd,
273
+ signal: params.signal,
274
+ baseline,
275
+ });
276
+
277
+ if (!profile) {
278
+ return toolError("Timed out waiting for `docyrus auth login` to complete in the new terminal.");
279
+ }
280
+
281
+ return toolResult(profile);
282
+ }
283
+
284
+ export default function docyrusAuthTool(pi: ExtensionAPI): void {
285
+ pi.registerTool({
286
+ name: TOOL_NAME,
287
+ label: "Docyrus Authenticate",
288
+ description:
289
+ "Authenticate the Docyrus CLI in the current working directory. " +
290
+ "In the desktop app, the host runs the OAuth2 device flow automatically and returns the resulting profile. " +
291
+ "In a terminal session, a new terminal window is opened to run `docyrus auth login` interactively and the tool waits for it to finish.",
292
+ parameters: Type.Object({
293
+ scope: Type.Optional(Type.String({ description: "Optional OAuth2 scopes; defaults to the CLI's standard login scope." })),
294
+ clientId: Type.Optional(Type.String({ description: "Optional OAuth2 client id override." })),
295
+ }),
296
+ execute: async(
297
+ _toolCallId: string,
298
+ input: IAuthenticateInput,
299
+ signal: AbortSignal | undefined,
300
+ _onUpdate: unknown,
301
+ ctx: {
302
+ cwd?: string;
303
+ hasUI: boolean;
304
+ ui: {
305
+ input: (title: string, placeholder?: string, opts?: { signal?: AbortSignal; timeout?: number }) => Promise<string | undefined>;
306
+ notify?: (message: string, level?: "info" | "warning" | "error") => void;
307
+ confirm?: (title: string, message: string, opts?: { signal?: AbortSignal; timeout?: number }) => Promise<boolean>;
308
+ };
309
+ },
310
+ ) => {
311
+ try {
312
+ if (process.env.DOCYRUS_DESKTOP_TOOLS === "1") {
313
+ return await executeDesktop({ input: input ?? {}, ctx, signal });
314
+ }
315
+ return await executeTui({ input: input ?? {}, ctx, signal });
316
+ }
317
+ catch (error: unknown) {
318
+ return toolError(error instanceof Error ? error.message : String(error));
319
+ }
320
+ },
321
+ });
322
+ }
package/server-loader.js CHANGED
@@ -44601,6 +44601,21 @@ async function listAllTools(params) {
44601
44601
 
44602
44602
  // src/server/browserToolSchemas.ts
44603
44603
  var BROWSER_TOOL_PREFIX = "docyrus_browser_";
44604
+ var DESKTOP_AUTHENTICATE_TOOL_NAME = "authenticate_docyrus";
44605
+ var DESKTOP_TOOL_SCHEMAS = [
44606
+ {
44607
+ name: DESKTOP_AUTHENTICATE_TOOL_NAME,
44608
+ description: "Authenticate the Docyrus CLI in the current working directory. The desktop app runs the OAuth2 device flow on behalf of the user and returns the authenticated profile (user, tenant, scope).",
44609
+ source: "built-in",
44610
+ inputSchema: {
44611
+ type: "object",
44612
+ properties: {
44613
+ scope: { type: "string", description: "Optional OAuth2 scopes; defaults to the CLI's standard login scope." },
44614
+ clientId: { type: "string", description: "Optional OAuth2 client id override." }
44615
+ }
44616
+ }
44617
+ }
44618
+ ];
44604
44619
  var BROWSER_TOOL_SCHEMAS = [
44605
44620
  {
44606
44621
  name: "docyrus_browser_navigate",
@@ -44916,9 +44931,6 @@ async function waitForIdle(session, timeoutMs = 3e4) {
44916
44931
  }
44917
44932
  });
44918
44933
  }
44919
- function resolveServerSettingsRootPath(agentDir) {
44920
- return (0, import_node_path22.resolve)(agentDir, "..", "..");
44921
- }
44922
44934
  var SESSION_MODE_COMMANDS = {
44923
44935
  "read-only": "read-only",
44924
44936
  "end-read-only": "normal"
@@ -45347,7 +45359,7 @@ async function createAgentServer(params) {
45347
45359
  const pendingAskUserRequests = /* @__PURE__ */ new Map();
45348
45360
  const oauthFlowManager = new OAuthFlowManager();
45349
45361
  const app = new Hono2();
45350
- const settingsRootPath = resolveServerSettingsRootPath(context.agentDir);
45362
+ const settingsRootPath = context.settingsRootPath;
45351
45363
  function getModelIdsByProvider() {
45352
45364
  const values = {};
45353
45365
  for (const model of modelRegistry.getAll()) {
@@ -47391,14 +47403,15 @@ async function createAgentServer(params) {
47391
47403
  cwd: context.cwd
47392
47404
  });
47393
47405
  if (context.desktop) {
47394
- tools.push(...BROWSER_TOOL_SCHEMAS);
47406
+ tools.push(...BROWSER_TOOL_SCHEMAS, ...DESKTOP_TOOL_SCHEMAS);
47395
47407
  }
47396
47408
  const builtInCount = tools.filter((t) => t.source === "built-in").length;
47397
47409
  const mcpCount = tools.filter((t) => t.source !== "built-in").length;
47398
47410
  return c.json({
47399
47411
  tools,
47400
47412
  summary: { builtIn: builtInCount, mcp: mcpCount, total: tools.length },
47401
- clientSideToolPrefixes: context.desktop ? [BROWSER_TOOL_PREFIX] : []
47413
+ clientSideToolPrefixes: context.desktop ? [BROWSER_TOOL_PREFIX] : [],
47414
+ clientSideToolNames: context.desktop ? [DESKTOP_AUTHENTICATE_TOOL_NAME] : []
47402
47415
  });
47403
47416
  } catch (error48) {
47404
47417
  const message = error48 instanceof Error ? error48.message : String(error48);
@@ -47649,6 +47662,8 @@ async function createAgentServer(params) {
47649
47662
  process.stderr.write(` POST /api/extension-ui-response \u2014 submit extension UI response
47650
47663
  `);
47651
47664
  process.stderr.write(` Browser tools: docyrus_browser_* (client-side via extension_ui)
47665
+ `);
47666
+ process.stderr.write(` Auth tool: authenticate_docyrus (client-side via extension_ui)
47652
47667
  `);
47653
47668
  }
47654
47669
  process.stderr.write(` * /api/cli/** \u2014 proxy any docyrus CLI command
@@ -48144,6 +48159,7 @@ async function main() {
48144
48159
  const pi = await loadPiExports();
48145
48160
  const cwd = process.cwd();
48146
48161
  const agentDir = readRequiredEnv("PI_CODING_AGENT_DIR");
48162
+ const settingsRootPath = readRequiredEnv("DOCYRUS_SETTINGS_ROOT");
48147
48163
  const version2 = process.env.DOCYRUS_PI_VERSION || "dev";
48148
48164
  const resourceRoot = resolvePackagedPiResourceRoot();
48149
48165
  const packagedExtensionPaths = resolvePackagedExtensionPaths(resourceRoot, "server");
@@ -48251,6 +48267,7 @@ async function main() {
48251
48267
  cwd,
48252
48268
  profile: request.profile,
48253
48269
  agentDir,
48270
+ settingsRootPath,
48254
48271
  version: version2,
48255
48272
  sessionDir: request.sessionDir ?? null,
48256
48273
  thinkingLevel: request.thinking ?? null,