pi-mono-all 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENCE.md +7 -0
  3. package/node_modules/pi-common/package.json +22 -0
  4. package/node_modules/pi-common/src/auth-config.ts +290 -0
  5. package/node_modules/pi-common/src/auth.ts +63 -0
  6. package/node_modules/pi-common/src/cache.ts +60 -0
  7. package/node_modules/pi-common/src/errors.ts +47 -0
  8. package/node_modules/pi-common/src/http-client.ts +118 -0
  9. package/node_modules/pi-common/src/index.ts +7 -0
  10. package/node_modules/pi-common/src/rate-limiter.ts +32 -0
  11. package/node_modules/pi-common/src/tool-result.ts +27 -0
  12. package/node_modules/pi-mono-ask-user-question/CHANGELOG.md +185 -0
  13. package/node_modules/pi-mono-ask-user-question/README.md +226 -0
  14. package/node_modules/pi-mono-ask-user-question/index.ts +923 -0
  15. package/node_modules/pi-mono-ask-user-question/package.json +29 -0
  16. package/node_modules/pi-mono-auto-fix/CHANGELOG.md +59 -0
  17. package/node_modules/pi-mono-auto-fix/README.md +77 -0
  18. package/node_modules/pi-mono-auto-fix/index.ts +488 -0
  19. package/node_modules/pi-mono-auto-fix/package.json +23 -0
  20. package/node_modules/pi-mono-btw/CHANGELOG.md +180 -0
  21. package/node_modules/pi-mono-btw/README.md +24 -0
  22. package/node_modules/pi-mono-btw/index.ts +499 -0
  23. package/node_modules/pi-mono-btw/package.json +29 -0
  24. package/node_modules/pi-mono-clear/CHANGELOG.md +180 -0
  25. package/node_modules/pi-mono-clear/README.md +40 -0
  26. package/node_modules/pi-mono-clear/index.ts +45 -0
  27. package/node_modules/pi-mono-clear/package.json +29 -0
  28. package/node_modules/pi-mono-context/CHANGELOG.md +12 -0
  29. package/node_modules/pi-mono-context/README.md +74 -0
  30. package/node_modules/pi-mono-context/index.ts +641 -0
  31. package/node_modules/pi-mono-context/package.json +29 -0
  32. package/node_modules/pi-mono-context-guard/CHANGELOG.md +195 -0
  33. package/node_modules/pi-mono-context-guard/README.md +81 -0
  34. package/node_modules/pi-mono-context-guard/index.ts +212 -0
  35. package/node_modules/pi-mono-context-guard/package.json +23 -0
  36. package/node_modules/pi-mono-figma/CHANGELOG.md +59 -0
  37. package/node_modules/pi-mono-figma/README.md +236 -0
  38. package/node_modules/pi-mono-figma/__tests__/code-connect.test.ts +32 -0
  39. package/node_modules/pi-mono-figma/__tests__/figma-assets.test.ts +38 -0
  40. package/node_modules/pi-mono-figma/__tests__/figma-component-hints.test.ts +23 -0
  41. package/node_modules/pi-mono-figma/__tests__/figma-implementation-layout.test.ts +47 -0
  42. package/node_modules/pi-mono-figma/__tests__/figma-search.test.ts +51 -0
  43. package/node_modules/pi-mono-figma/__tests__/figma-summarizer.test.ts +65 -0
  44. package/node_modules/pi-mono-figma/__tests__/fixtures/complex-auto-layout.json +115 -0
  45. package/node_modules/pi-mono-figma/__tests__/fixtures/component-instance.json +50 -0
  46. package/node_modules/pi-mono-figma/__tests__/fixtures/hidden-and-vectors.json +28 -0
  47. package/node_modules/pi-mono-figma/__tests__/fixtures/variables-and-styles.json +40 -0
  48. package/node_modules/pi-mono-figma/docs/live-selection-bridge.md +16 -0
  49. package/node_modules/pi-mono-figma/index.ts +6 -0
  50. package/node_modules/pi-mono-figma/package.json +33 -0
  51. package/node_modules/pi-mono-figma/skills/figma/SKILL.md +143 -0
  52. package/node_modules/pi-mono-figma/src/code-connect.ts +110 -0
  53. package/node_modules/pi-mono-figma/src/figma-assets.ts +146 -0
  54. package/node_modules/pi-mono-figma/src/figma-cache.ts +6 -0
  55. package/node_modules/pi-mono-figma/src/figma-client.ts +471 -0
  56. package/node_modules/pi-mono-figma/src/figma-component-hints.ts +87 -0
  57. package/node_modules/pi-mono-figma/src/figma-implementation.ts +264 -0
  58. package/node_modules/pi-mono-figma/src/figma-schemas.ts +139 -0
  59. package/node_modules/pi-mono-figma/src/figma-search.ts +195 -0
  60. package/node_modules/pi-mono-figma/src/figma-summarizer.ts +673 -0
  61. package/node_modules/pi-mono-figma/src/figma-tokens.ts +57 -0
  62. package/node_modules/pi-mono-figma/src/figma-tools.ts +352 -0
  63. package/node_modules/pi-mono-linear/CHANGELOG.md +44 -0
  64. package/node_modules/pi-mono-linear/README.md +159 -0
  65. package/node_modules/pi-mono-linear/index.ts +6 -0
  66. package/node_modules/pi-mono-linear/package.json +30 -0
  67. package/node_modules/pi-mono-linear/skills/linear/SKILL.md +107 -0
  68. package/node_modules/pi-mono-linear/src/linear-client.ts +339 -0
  69. package/node_modules/pi-mono-linear/src/linear-queries.ts +101 -0
  70. package/node_modules/pi-mono-linear/src/linear-schemas.ts +90 -0
  71. package/node_modules/pi-mono-linear/src/linear-tools.ts +362 -0
  72. package/node_modules/pi-mono-loop/CHANGELOG.md +163 -0
  73. package/node_modules/pi-mono-loop/README.md +54 -0
  74. package/node_modules/pi-mono-loop/index.ts +291 -0
  75. package/node_modules/pi-mono-loop/package.json +26 -0
  76. package/node_modules/pi-mono-multi-edit/CHANGELOG.md +232 -0
  77. package/node_modules/pi-mono-multi-edit/README.md +244 -0
  78. package/node_modules/pi-mono-multi-edit/__tests__/classic.test.ts +277 -0
  79. package/node_modules/pi-mono-multi-edit/__tests__/diff.test.ts +77 -0
  80. package/node_modules/pi-mono-multi-edit/__tests__/patch.test.ts +287 -0
  81. package/node_modules/pi-mono-multi-edit/benchmark-edits.ts +966 -0
  82. package/node_modules/pi-mono-multi-edit/classic.ts +435 -0
  83. package/node_modules/pi-mono-multi-edit/diff.ts +143 -0
  84. package/node_modules/pi-mono-multi-edit/index.ts +266 -0
  85. package/node_modules/pi-mono-multi-edit/package.json +37 -0
  86. package/node_modules/pi-mono-multi-edit/patch.ts +463 -0
  87. package/node_modules/pi-mono-multi-edit/types.ts +53 -0
  88. package/node_modules/pi-mono-multi-edit/workspace.ts +85 -0
  89. package/node_modules/pi-mono-review/CHANGELOG.md +190 -0
  90. package/node_modules/pi-mono-review/README.md +30 -0
  91. package/node_modules/pi-mono-review/common.ts +930 -0
  92. package/node_modules/pi-mono-review/index.ts +8 -0
  93. package/node_modules/pi-mono-review/package.json +29 -0
  94. package/node_modules/pi-mono-review/review-tui.ts +194 -0
  95. package/node_modules/pi-mono-review/review.ts +119 -0
  96. package/node_modules/pi-mono-review/reviewer.ts +339 -0
  97. package/node_modules/pi-mono-sentinel/CHANGELOG.md +158 -0
  98. package/node_modules/pi-mono-sentinel/README.md +87 -0
  99. package/node_modules/pi-mono-sentinel/__tests__/output-scanner.test.ts +109 -0
  100. package/node_modules/pi-mono-sentinel/__tests__/permissions.test.ts +202 -0
  101. package/node_modules/pi-mono-sentinel/__tests__/whitelist.test.ts +59 -0
  102. package/node_modules/pi-mono-sentinel/guards/execution-tracker.ts +281 -0
  103. package/node_modules/pi-mono-sentinel/guards/output-scanner.ts +232 -0
  104. package/node_modules/pi-mono-sentinel/guards/permission-gate.ts +170 -0
  105. package/node_modules/pi-mono-sentinel/index.ts +43 -0
  106. package/node_modules/pi-mono-sentinel/package.json +26 -0
  107. package/node_modules/pi-mono-sentinel/patterns/permissions.ts +175 -0
  108. package/node_modules/pi-mono-sentinel/patterns/read-targets.ts +104 -0
  109. package/node_modules/pi-mono-sentinel/patterns/secrets.ts +143 -0
  110. package/node_modules/pi-mono-sentinel/session.ts +95 -0
  111. package/node_modules/pi-mono-sentinel/specs/2026/04/sentinel/001-permission-gate.md +145 -0
  112. package/node_modules/pi-mono-sentinel/types.ts +39 -0
  113. package/node_modules/pi-mono-sentinel/whitelist.ts +86 -0
  114. package/node_modules/pi-mono-simplify/CHANGELOG.md +163 -0
  115. package/node_modules/pi-mono-simplify/README.md +56 -0
  116. package/node_modules/pi-mono-simplify/index.ts +78 -0
  117. package/node_modules/pi-mono-simplify/package.json +29 -0
  118. package/node_modules/pi-mono-status-line/CHANGELOG.md +180 -0
  119. package/node_modules/pi-mono-status-line/README.md +96 -0
  120. package/node_modules/pi-mono-status-line/basic.ts +89 -0
  121. package/node_modules/pi-mono-status-line/expert.ts +689 -0
  122. package/node_modules/pi-mono-status-line/index.ts +54 -0
  123. package/node_modules/pi-mono-status-line/package.json +29 -0
  124. package/node_modules/pi-mono-team-mode/CHANGELOG.md +278 -0
  125. package/node_modules/pi-mono-team-mode/README.md +246 -0
  126. package/node_modules/pi-mono-team-mode/__tests__/agent-manager-transient.test.ts +75 -0
  127. package/node_modules/pi-mono-team-mode/__tests__/delegation-manager.test.ts +118 -0
  128. package/node_modules/pi-mono-team-mode/__tests__/formatters.test.ts +104 -0
  129. package/node_modules/pi-mono-team-mode/__tests__/model-config.test.ts +272 -0
  130. package/node_modules/pi-mono-team-mode/__tests__/notification-box.test.ts +34 -0
  131. package/node_modules/pi-mono-team-mode/__tests__/parallel-utils.test.ts +32 -0
  132. package/node_modules/pi-mono-team-mode/__tests__/pi-stream-parser.test.ts +64 -0
  133. package/node_modules/pi-mono-team-mode/__tests__/prompts.test.ts +106 -0
  134. package/node_modules/pi-mono-team-mode/__tests__/store.test.ts +164 -0
  135. package/node_modules/pi-mono-team-mode/__tests__/tasks.test.ts +267 -0
  136. package/node_modules/pi-mono-team-mode/__tests__/teammate-specs.test.ts +114 -0
  137. package/node_modules/pi-mono-team-mode/__tests__/widget.test.ts +41 -0
  138. package/node_modules/pi-mono-team-mode/__tests__/worktree.test.ts +78 -0
  139. package/node_modules/pi-mono-team-mode/core/chain-utils.ts +90 -0
  140. package/node_modules/pi-mono-team-mode/core/fs-utils.ts +44 -0
  141. package/node_modules/pi-mono-team-mode/core/model-config.ts +432 -0
  142. package/node_modules/pi-mono-team-mode/core/parallel-utils.ts +48 -0
  143. package/node_modules/pi-mono-team-mode/core/prompts.ts +158 -0
  144. package/node_modules/pi-mono-team-mode/core/store.ts +156 -0
  145. package/node_modules/pi-mono-team-mode/core/tasks.ts +99 -0
  146. package/node_modules/pi-mono-team-mode/core/teammate-specs.ts +124 -0
  147. package/node_modules/pi-mono-team-mode/core/types.ts +160 -0
  148. package/node_modules/pi-mono-team-mode/index.ts +825 -0
  149. package/node_modules/pi-mono-team-mode/managers/agent-manager.ts +654 -0
  150. package/node_modules/pi-mono-team-mode/managers/delegation-manager.ts +211 -0
  151. package/node_modules/pi-mono-team-mode/managers/task-manager.ts +238 -0
  152. package/node_modules/pi-mono-team-mode/managers/team-manager.ts +59 -0
  153. package/node_modules/pi-mono-team-mode/package.json +33 -0
  154. package/node_modules/pi-mono-team-mode/runtime/pi-stream-parser.ts +194 -0
  155. package/node_modules/pi-mono-team-mode/runtime/subprocess.ts +183 -0
  156. package/node_modules/pi-mono-team-mode/runtime/transient-session.ts +196 -0
  157. package/node_modules/pi-mono-team-mode/runtime/worktree.ts +90 -0
  158. package/node_modules/pi-mono-team-mode/ui/formatters.ts +149 -0
  159. package/node_modules/pi-mono-team-mode/ui/notification-box.ts +55 -0
  160. package/node_modules/pi-mono-team-mode/ui/widget.ts +94 -0
  161. package/package.json +76 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # pi-mono-all
2
+
3
+ ## 1.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - Add the all-in-one pi package and bundle the shared pi-common workspace package into distributed packages.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies
12
+ - pi-mono-figma@0.2.1
13
+ - pi-mono-linear@0.2.1
package/LICENCE.md ADDED
@@ -0,0 +1,7 @@
1
+ Copyright 2026 Emanuel Casco
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "pi-common",
3
+ "version": "0.1.1",
4
+ "description": "Shared utilities for pi integration extensions",
5
+ "type": "module",
6
+ "private": true,
7
+ "exports": {
8
+ ".": "./src/index.ts",
9
+ "./auth": "./src/auth.ts",
10
+ "./auth-config": "./src/auth-config.ts",
11
+ "./http-client": "./src/http-client.ts",
12
+ "./rate-limiter": "./src/rate-limiter.ts",
13
+ "./cache": "./src/cache.ts",
14
+ "./errors": "./src/errors.ts",
15
+ "./tool-result": "./src/tool-result.ts"
16
+ },
17
+ "peerDependencies": {
18
+ "@mariozechner/pi-coding-agent": "*",
19
+ "@mariozechner/pi-tui": "*",
20
+ "@sinclair/typebox": "*"
21
+ }
22
+ }
@@ -0,0 +1,290 @@
1
+ import { execFile } from "node:child_process";
2
+ import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { dirname, resolve } from "node:path";
5
+ import { promisify } from "node:util";
6
+ import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@mariozechner/pi-coding-agent";
7
+ import { Key, matchesKey } from "@mariozechner/pi-tui";
8
+ import { Type } from "@sinclair/typebox";
9
+ import { ApiError } from "./errors.js";
10
+ import { MissingAuthTokenError, readAuthToken, setAuthTokenOverride, type ReadAuthTokenOptions } from "./auth.js";
11
+
12
+ const execFileAsync = promisify(execFile);
13
+
14
+ export interface AuthConfiguratorOptions extends ReadAuthTokenOptions {
15
+ service: string;
16
+ displayName: string;
17
+ commandName: string;
18
+ toolName: string;
19
+ tokenUrl?: string;
20
+ scopeInstructions: readonly string[];
21
+ }
22
+
23
+ interface ConfigureAuthParams {
24
+ force?: boolean;
25
+ }
26
+
27
+ const ConfigureAuthParamsSchema = Type.Object({
28
+ force: Type.Optional(Type.Boolean({ description: "Prompt even if a token is already configured. Defaults to false." })),
29
+ });
30
+
31
+ export function registerAuthConfigurator(pi: ExtensionAPI, options: AuthConfiguratorOptions): void {
32
+ pi.registerCommand(options.commandName, {
33
+ description: `Configure ${options.displayName} authentication token securely`,
34
+ handler: async (args, ctx) => {
35
+ const force = args.trim().split(/\s+/).includes("--force") || args.trim() === "force";
36
+ try {
37
+ const result = await configureAuthToken(ctx, options, { force });
38
+ ctx.ui.notify(result.message, "info");
39
+ } catch (error) {
40
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
41
+ }
42
+ },
43
+ });
44
+
45
+ pi.registerTool({
46
+ name: options.toolName,
47
+ label: `${options.displayName} Auth`,
48
+ description: `Securely prompt the user for a ${options.displayName} token and store it without exposing it to the model. Use only when auth is missing/expired/invalid, or when the user asks to update the token.`,
49
+ parameters: ConfigureAuthParamsSchema,
50
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
51
+ const result = await configureAuthToken(ctx, options, { force: params.force });
52
+ return {
53
+ content: [{ type: "text", text: result.message }],
54
+ details: {
55
+ service: options.service,
56
+ stored: result.stored,
57
+ authPath: options.authPath.join("."),
58
+ envName: options.envName,
59
+ envWasSet: result.envWasSet,
60
+ },
61
+ };
62
+ },
63
+ });
64
+ }
65
+
66
+ export async function runWithAuthRetry<T>(
67
+ ctx: ExtensionContext,
68
+ options: AuthConfiguratorOptions,
69
+ operation: () => Promise<T>,
70
+ ): Promise<T> {
71
+ try {
72
+ return await operation();
73
+ } catch (error) {
74
+ if (!isAuthError(error)) throw error;
75
+ if (!ctx.hasUI) throw error;
76
+ await configureAuthToken(ctx, options, { force: true });
77
+ return operation();
78
+ }
79
+ }
80
+
81
+ export async function configureAuthToken(
82
+ ctx: ExtensionContext | ExtensionCommandContext,
83
+ options: AuthConfiguratorOptions,
84
+ params: ConfigureAuthParams = {},
85
+ ): Promise<{ stored: boolean; message: string; envWasSet: boolean }> {
86
+ if (!params.force) {
87
+ try {
88
+ await readAuthToken(options);
89
+ return {
90
+ stored: false,
91
+ envWasSet: Boolean(process.env[options.envName]?.trim()),
92
+ message: `${options.displayName} token is already configured. Use /${options.commandName} --force to replace it.`,
93
+ };
94
+ } catch (error) {
95
+ if (!(error instanceof MissingAuthTokenError)) throw error;
96
+ }
97
+ }
98
+
99
+ if (!ctx.hasUI) {
100
+ throw new Error(`${options.displayName} auth setup requires interactive UI.`);
101
+ }
102
+
103
+ const token = await promptSecret(ctx, `${options.displayName} token`);
104
+ if (!token) throw new Error(`${options.displayName} token setup cancelled.`);
105
+
106
+ await writeAuthToken({ authFile: options.authFile, authPath: options.authPath, token });
107
+ setAuthTokenOverride(options, token);
108
+
109
+ const envWasSet = Boolean(process.env[options.envName]?.trim());
110
+ const envNote = envWasSet
111
+ ? ` ${options.envName} is set and normally takes precedence; this pi session will use the new token, but update your environment for future sessions.`
112
+ : "";
113
+
114
+ return {
115
+ stored: true,
116
+ envWasSet,
117
+ message: `${options.displayName} token stored in ~/.pi/agent/auth.json at ${options.authPath.join(".")}.${envNote}`,
118
+ };
119
+ }
120
+
121
+ export async function writeAuthToken(options: { authPath: readonly string[]; token: string; authFile?: string }): Promise<void> {
122
+ const authFile = options.authFile ?? resolve(homedir(), ".pi", "agent", "auth.json");
123
+ await mkdir(dirname(authFile), { recursive: true });
124
+ await safeChmod(dirname(authFile), 0o700);
125
+
126
+ let auth: unknown = {};
127
+ try {
128
+ auth = JSON.parse(await readFile(authFile, "utf8")) as unknown;
129
+ } catch (error) {
130
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
131
+ }
132
+
133
+ const next = auth && typeof auth === "object" ? (auth as Record<string, unknown>) : {};
134
+ setPath(next, options.authPath, options.token);
135
+ await writeFile(authFile, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
136
+ await safeChmod(authFile, 0o600);
137
+ }
138
+
139
+ export function isAuthError(error: unknown): boolean {
140
+ if (error instanceof MissingAuthTokenError) return true;
141
+ if (error instanceof ApiError && (error.status === 401 || error.status === 403)) return true;
142
+ const message = error instanceof Error ? error.message : String(error);
143
+ return /token expired|invalid token|missing token|no .*token|unauthorized|forbidden|authentication|api key/i.test(message);
144
+ }
145
+
146
+ async function promptSecret(ctx: ExtensionContext | ExtensionCommandContext, title: string): Promise<string | null> {
147
+ return ctx.ui.custom<string | null>((tui, _theme, _keybindings, done) => {
148
+ let value = "";
149
+ let cached: string[] | undefined;
150
+
151
+ function refresh(): void {
152
+ cached = undefined;
153
+ tui.requestRender();
154
+ }
155
+
156
+ function row(content: string, contentWidth: number): string {
157
+ const safe = content.length > contentWidth ? content.slice(0, contentWidth) : content;
158
+ return `│ ${safe.padEnd(contentWidth)} │`;
159
+ }
160
+
161
+ function maskedInput(contentWidth: number): string {
162
+ if (value.length === 0) return "> ▌";
163
+
164
+ const inputChromeWidth = 3; // "> " + cursor.
165
+ const availableMaskWidth = Math.max(1, contentWidth - inputChromeWidth);
166
+ const suffix = value.length > availableMaskWidth ? ` ${value.length} chars` : "";
167
+ const bulletCount = Math.max(1, Math.min(value.length, availableMaskWidth - suffix.length));
168
+ return `> ${"•".repeat(bulletCount)}${suffix}▌`;
169
+ }
170
+
171
+ return {
172
+ render(width: number): string[] {
173
+ if (cached) return cached;
174
+ const boxWidth = Math.max(4, Math.min(width, 48));
175
+ const contentWidth = Math.max(0, boxWidth - 4);
176
+ const border = `┌${"─".repeat(Math.max(0, boxWidth - 2))}┐`;
177
+ const bottom = `└${"─".repeat(Math.max(0, boxWidth - 2))}┘`;
178
+ const lines = [border, row(title, contentWidth), row(maskedInput(contentWidth), contentWidth), bottom];
179
+ cached = lines;
180
+ return cached;
181
+ },
182
+ handleInput(data: string): void {
183
+ if (matchesKey(data, Key.escape)) {
184
+ done(null);
185
+ return;
186
+ }
187
+ if (matchesKey(data, Key.enter)) {
188
+ done(value.trim() || null);
189
+ return;
190
+ }
191
+ if (matchesKey(data, Key.backspace)) {
192
+ value = value.slice(0, -1);
193
+ refresh();
194
+ return;
195
+ }
196
+ if (data === "\u0015") {
197
+ value = "";
198
+ refresh();
199
+ return;
200
+ }
201
+ if (data === "\u0016") {
202
+ void readClipboardText().then((text) => {
203
+ const sanitized = sanitizeSecretInput(text);
204
+ if (sanitized) {
205
+ value += sanitized;
206
+ refresh();
207
+ }
208
+ });
209
+ return;
210
+ }
211
+
212
+ const sanitized = sanitizeSecretInput(data);
213
+ if (sanitized) {
214
+ value += sanitized;
215
+ refresh();
216
+ }
217
+ },
218
+ invalidate(): void {
219
+ cached = undefined;
220
+ },
221
+ };
222
+ }, {
223
+ overlay: true,
224
+ overlayOptions: {
225
+ width: 48,
226
+ minWidth: 32,
227
+ maxHeight: 4,
228
+ margin: 1,
229
+ },
230
+ });
231
+ }
232
+
233
+ function sanitizeSecretInput(data: string): string {
234
+ if (!data) return "";
235
+
236
+ // Bracketed paste: ESC [ 200 ~ pasted text ESC [ 201 ~
237
+ if (data.includes("\u001b[200~") || data.includes("\u001b[201~")) {
238
+ return data.replace(/\u001b\[200~/g, "").replace(/\u001b\[201~/g, "").replace(/[\r\n\t]/g, "").trim();
239
+ }
240
+
241
+ // Ignore non-paste escape sequences such as arrows and modified keys.
242
+ if (data.startsWith("\u001b")) return "";
243
+
244
+ return data.replace(/[\x00-\x1f\x7f]/g, "").trim();
245
+ }
246
+
247
+ async function readClipboardText(): Promise<string> {
248
+ try {
249
+ if (process.platform === "darwin") return (await execFileAsync("pbpaste", [])).stdout;
250
+ if (process.platform === "win32") {
251
+ return (await execFileAsync("powershell.exe", ["-NoProfile", "-Command", "Get-Clipboard"])).stdout;
252
+ }
253
+
254
+ for (const [command, args] of [
255
+ ["wl-paste", ["--no-newline"]],
256
+ ["xclip", ["-selection", "clipboard", "-out"]],
257
+ ["xsel", ["--clipboard", "--output"]],
258
+ ] as const) {
259
+ try {
260
+ return (await execFileAsync(command, args)).stdout;
261
+ } catch {
262
+ // Try next clipboard provider.
263
+ }
264
+ }
265
+ } catch {
266
+ // Clipboard access is best-effort; normal terminal paste can still work.
267
+ }
268
+ return "";
269
+ }
270
+
271
+ function setPath(target: Record<string, unknown>, path: readonly string[], value: string): void {
272
+ let current = target;
273
+ for (const [index, segment] of path.entries()) {
274
+ if (index === path.length - 1) {
275
+ current[segment] = value;
276
+ return;
277
+ }
278
+ const next = current[segment];
279
+ if (!next || typeof next !== "object" || Array.isArray(next)) current[segment] = {};
280
+ current = current[segment] as Record<string, unknown>;
281
+ }
282
+ }
283
+
284
+ async function safeChmod(path: string, mode: number): Promise<void> {
285
+ try {
286
+ await chmod(path, mode);
287
+ } catch (error) {
288
+ if (process.platform !== "win32") throw error;
289
+ }
290
+ }
@@ -0,0 +1,63 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { resolve } from "node:path";
4
+
5
+ export interface ReadAuthTokenOptions {
6
+ envName: string;
7
+ authPath: readonly string[];
8
+ authFile?: string;
9
+ }
10
+
11
+ const authTokenOverrides = new Map<string, string>();
12
+
13
+ export function setAuthTokenOverride(options: ReadAuthTokenOptions, token: string): void {
14
+ authTokenOverrides.set(authTokenKey(options), token);
15
+ }
16
+
17
+ export function clearAuthTokenOverride(options: ReadAuthTokenOptions): void {
18
+ authTokenOverrides.delete(authTokenKey(options));
19
+ }
20
+
21
+ export class MissingAuthTokenError extends Error {
22
+ constructor(public readonly envName: string, public readonly authPath: readonly string[]) {
23
+ super(
24
+ `No auth token found. Set ${envName} or store it in ~/.pi/agent/auth.json at ${authPath.join(".")}`,
25
+ );
26
+ this.name = "MissingAuthTokenError";
27
+ }
28
+ }
29
+
30
+ export async function readAuthToken(options: ReadAuthTokenOptions): Promise<string> {
31
+ const override = authTokenOverrides.get(authTokenKey(options));
32
+ if (override) return override;
33
+
34
+ const envValue = process.env[options.envName]?.trim();
35
+ if (envValue) return envValue;
36
+
37
+ const authFile = options.authFile ?? resolve(homedir(), ".pi", "agent", "auth.json");
38
+ try {
39
+ const raw = await readFile(authFile, "utf8");
40
+ const parsed = JSON.parse(raw) as unknown;
41
+ const value = getPath(parsed, options.authPath);
42
+ if (typeof value === "string" && value.trim()) return value.trim();
43
+ } catch (error) {
44
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
45
+ throw error;
46
+ }
47
+ }
48
+
49
+ throw new MissingAuthTokenError(options.envName, options.authPath);
50
+ }
51
+
52
+ function getPath(value: unknown, path: readonly string[]): unknown {
53
+ let current = value;
54
+ for (const segment of path) {
55
+ if (!current || typeof current !== "object" || !(segment in current)) return undefined;
56
+ current = (current as Record<string, unknown>)[segment];
57
+ }
58
+ return current;
59
+ }
60
+
61
+ function authTokenKey(options: ReadAuthTokenOptions): string {
62
+ return `${options.envName}:${options.authPath.join(".")}:${options.authFile ?? "default"}`;
63
+ }
@@ -0,0 +1,60 @@
1
+ export interface CacheEntry<T> {
2
+ value: T;
3
+ expiresAt: number;
4
+ }
5
+
6
+ export interface TtlCacheOptions {
7
+ defaultTtlMs: number;
8
+ maxEntries?: number;
9
+ }
10
+
11
+ export class TtlCache<T> {
12
+ private readonly entries = new Map<string, CacheEntry<T>>();
13
+
14
+ constructor(private readonly options: TtlCacheOptions) {}
15
+
16
+ get(key: string): T | undefined {
17
+ const entry = this.entries.get(key);
18
+ if (!entry) return undefined;
19
+ if (entry.expiresAt <= Date.now()) {
20
+ this.entries.delete(key);
21
+ return undefined;
22
+ }
23
+ return entry.value;
24
+ }
25
+
26
+ set(key: string, value: T, ttlMs = this.options.defaultTtlMs): void {
27
+ this.entries.set(key, { value, expiresAt: Date.now() + ttlMs });
28
+ this.evictOverflow();
29
+ }
30
+
31
+ delete(key: string): void {
32
+ this.entries.delete(key);
33
+ }
34
+
35
+ clear(): void {
36
+ this.entries.clear();
37
+ }
38
+
39
+ getOrSet(key: string, load: () => Promise<T>, ttlMs = this.options.defaultTtlMs): Promise<T> {
40
+ const cached = this.get(key);
41
+ if (cached !== undefined) return Promise.resolve(cached);
42
+ return load().then((value) => {
43
+ this.set(key, value, ttlMs);
44
+ return value;
45
+ });
46
+ }
47
+
48
+ private evictOverflow(): void {
49
+ const maxEntries = this.options.maxEntries;
50
+ if (!maxEntries || this.entries.size <= maxEntries) return;
51
+ const overflow = this.entries.size - maxEntries;
52
+ for (const key of Array.from(this.entries.keys()).slice(0, overflow)) {
53
+ this.entries.delete(key);
54
+ }
55
+ }
56
+ }
57
+
58
+ export function createTtlCache<T>(options: TtlCacheOptions): TtlCache<T> {
59
+ return new TtlCache<T>(options);
60
+ }
@@ -0,0 +1,47 @@
1
+ export interface NormalizedApiError {
2
+ name: string;
3
+ message: string;
4
+ status?: number;
5
+ service?: string;
6
+ code?: string;
7
+ details?: unknown;
8
+ }
9
+
10
+ export class ApiError extends Error {
11
+ constructor(
12
+ message: string,
13
+ public readonly status?: number,
14
+ public readonly details?: unknown,
15
+ public readonly service?: string,
16
+ public readonly code?: string,
17
+ ) {
18
+ super(message);
19
+ this.name = "ApiError";
20
+ }
21
+ }
22
+
23
+ export function normalizeApiError(error: unknown, service?: string): NormalizedApiError {
24
+ if (error instanceof ApiError) {
25
+ return {
26
+ name: error.name,
27
+ message: error.message,
28
+ status: error.status,
29
+ service: error.service ?? service,
30
+ code: error.code,
31
+ details: error.details,
32
+ };
33
+ }
34
+
35
+ if (error instanceof Error) {
36
+ return { name: error.name, message: error.message, service };
37
+ }
38
+
39
+ return { name: "Error", message: String(error), service };
40
+ }
41
+
42
+ export function errorMessage(error: unknown, service?: string): string {
43
+ const normalized = normalizeApiError(error, service);
44
+ const prefix = normalized.service ? `${normalized.service} API error` : "API error";
45
+ const status = normalized.status ? ` (${normalized.status})` : "";
46
+ return `${prefix}${status}: ${normalized.message}`;
47
+ }
@@ -0,0 +1,118 @@
1
+ import { ApiError } from "./errors.js";
2
+
3
+ type HeadersInput = ConstructorParameters<typeof Headers>[0];
4
+
5
+ export interface HttpClientOptions {
6
+ baseUrl?: string;
7
+ timeoutMs?: number;
8
+ headers?: HeadersInput | (() => HeadersInput | Promise<HeadersInput>);
9
+ service?: string;
10
+ }
11
+
12
+ export interface RequestJsonOptions extends Omit<RequestInit, "body" | "headers"> {
13
+ body?: unknown;
14
+ headers?: HeadersInput;
15
+ timeoutMs?: number;
16
+ }
17
+
18
+ export interface HttpClient {
19
+ request<T = unknown>(path: string, options?: RequestJsonOptions): Promise<T>;
20
+ get<T = unknown>(path: string, options?: RequestJsonOptions): Promise<T>;
21
+ post<T = unknown>(path: string, body?: unknown, options?: RequestJsonOptions): Promise<T>;
22
+ download(url: string, options?: RequestJsonOptions): Promise<ArrayBuffer>;
23
+ }
24
+
25
+ export function createHttpClient(options: HttpClientOptions = {}): HttpClient {
26
+ async function mergedHeaders(extra?: HeadersInput): Promise<Headers> {
27
+ const headers = new Headers(
28
+ typeof options.headers === "function" ? await options.headers() : (options.headers ?? undefined),
29
+ );
30
+ if (extra) {
31
+ new Headers(extra).forEach((value, key) => headers.set(key, value));
32
+ }
33
+ return headers;
34
+ }
35
+
36
+ async function request<T = unknown>(path: string, requestOptions: RequestJsonOptions = {}): Promise<T> {
37
+ const url = buildUrl(options.baseUrl, path);
38
+ const { body, headers: extraHeaders, timeoutMs, ...initOptions } = requestOptions;
39
+ const headers = await mergedHeaders(extraHeaders);
40
+ const init: RequestInit = { ...initOptions, headers };
41
+
42
+ if (body !== undefined) {
43
+ if (!headers.has("content-type")) headers.set("content-type", "application/json");
44
+ init.body = typeof body === "string" ? body : JSON.stringify(body);
45
+ }
46
+
47
+ const response = await fetchWithTimeout(url, init, timeoutMs ?? options.timeoutMs);
48
+ return parseResponse<T>(response, options.service);
49
+ }
50
+
51
+ async function download(url: string, requestOptions: RequestJsonOptions = {}): Promise<ArrayBuffer> {
52
+ const { body: _body, headers: _headers, timeoutMs, ...initOptions } = requestOptions;
53
+ const response = await fetchWithTimeout(url, initOptions, timeoutMs ?? options.timeoutMs);
54
+ if (!response.ok) {
55
+ throw new ApiError(response.statusText || `HTTP ${response.status}`, response.status, await safeBody(response), options.service);
56
+ }
57
+ return response.arrayBuffer();
58
+ }
59
+
60
+ return {
61
+ request,
62
+ get: (path, requestOptions) => request(path, { ...requestOptions, method: "GET" }),
63
+ post: (path, body, requestOptions) => request(path, { ...requestOptions, method: "POST", body }),
64
+ download,
65
+ };
66
+ }
67
+
68
+ function buildUrl(baseUrl: string | undefined, path: string): string {
69
+ if (/^https?:\/\//i.test(path)) return path;
70
+ if (!baseUrl) return path;
71
+ return `${baseUrl.replace(/\/$/, "")}/${path.replace(/^\//, "")}`;
72
+ }
73
+
74
+ async function fetchWithTimeout(url: string, init: RequestInit, timeoutMs = 30_000): Promise<Response> {
75
+ const controller = new AbortController();
76
+ const timeout = setTimeout(() => controller.abort(new Error(`Request timed out after ${timeoutMs}ms`)), timeoutMs);
77
+ const upstream = init.signal;
78
+ const abort = () => controller.abort(upstream?.reason);
79
+ upstream?.addEventListener("abort", abort, { once: true });
80
+
81
+ try {
82
+ return await fetch(url, { ...init, signal: controller.signal });
83
+ } finally {
84
+ clearTimeout(timeout);
85
+ upstream?.removeEventListener("abort", abort);
86
+ }
87
+ }
88
+
89
+ async function parseResponse<T>(response: Response, service?: string): Promise<T> {
90
+ const body = await safeBody(response);
91
+ if (!response.ok) {
92
+ throw new ApiError(extractErrorMessage(body) ?? response.statusText ?? `HTTP ${response.status}`, response.status, body, service);
93
+ }
94
+ return body as T;
95
+ }
96
+
97
+ async function safeBody(response: Response): Promise<unknown> {
98
+ const text = await response.text();
99
+ if (!text) return undefined;
100
+ try {
101
+ return JSON.parse(text) as unknown;
102
+ } catch {
103
+ return text;
104
+ }
105
+ }
106
+
107
+ function extractErrorMessage(body: unknown): string | undefined {
108
+ if (!body || typeof body !== "object") return undefined;
109
+ const record = body as Record<string, unknown>;
110
+ if (typeof record.message === "string") return record.message;
111
+ if (typeof record.err === "string") return record.err;
112
+ const errors = record.errors;
113
+ if (Array.isArray(errors) && errors.length > 0) {
114
+ const first = errors[0] as Record<string, unknown>;
115
+ if (typeof first.message === "string") return first.message;
116
+ }
117
+ return undefined;
118
+ }
@@ -0,0 +1,7 @@
1
+ export * from "./auth.js";
2
+ export * from "./auth-config.js";
3
+ export * from "./http-client.js";
4
+ export * from "./rate-limiter.js";
5
+ export * from "./cache.js";
6
+ export * from "./errors.js";
7
+ export * from "./tool-result.js";
@@ -0,0 +1,32 @@
1
+ export interface RateLimiterOptions {
2
+ minIntervalMs: number;
3
+ }
4
+
5
+ export interface RateLimiter {
6
+ schedule<T>(operation: () => Promise<T>): Promise<T>;
7
+ }
8
+
9
+ export function createRateLimiter(options: RateLimiterOptions): RateLimiter {
10
+ let lastStart = 0;
11
+ let chain: Promise<unknown> = Promise.resolve();
12
+
13
+ return {
14
+ schedule<T>(operation: () => Promise<T>): Promise<T> {
15
+ const run = async (): Promise<T> => {
16
+ const now = Date.now();
17
+ const waitMs = Math.max(0, lastStart + options.minIntervalMs - now);
18
+ if (waitMs > 0) await sleep(waitMs);
19
+ lastStart = Date.now();
20
+ return operation();
21
+ };
22
+
23
+ const next = chain.then(run, run);
24
+ chain = next.catch(() => undefined);
25
+ return next;
26
+ },
27
+ };
28
+ }
29
+
30
+ function sleep(ms: number): Promise<void> {
31
+ return new Promise((resolve) => setTimeout(resolve, ms));
32
+ }
@@ -0,0 +1,27 @@
1
+ export interface ToolResultOptions {
2
+ maxChars?: number;
3
+ }
4
+
5
+ export interface ToolResult {
6
+ content: Array<{ type: "text"; text: string }>;
7
+ details: Record<string, unknown>;
8
+ }
9
+
10
+ const DEFAULT_MAX_CHARS = 40_000;
11
+
12
+ export function textToolResult(text: string, details: Record<string, unknown> = {}): ToolResult {
13
+ return { content: [{ type: "text", text }], details };
14
+ }
15
+
16
+ export function jsonToolResult(data: unknown, options: ToolResultOptions = {}): ToolResult {
17
+ const pretty = JSON.stringify(data, null, 2);
18
+ const maxChars = options.maxChars ?? DEFAULT_MAX_CHARS;
19
+ const truncated = pretty.length > maxChars;
20
+ const text = truncated
21
+ ? `${pretty.slice(0, maxChars)}\n\n[truncated ${pretty.length - maxChars} characters; narrow the query or request specific IDs]`
22
+ : pretty;
23
+ return {
24
+ content: [{ type: "text", text }],
25
+ details: { truncated, characters: pretty.length },
26
+ };
27
+ }