@oh-my-pi/pi-coding-agent 15.1.2 → 15.1.3
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/CHANGELOG.md +42 -0
- package/dist/types/cli/auth-broker-cli.d.ts +25 -0
- package/dist/types/cli/auth-gateway-cli.d.ts +18 -0
- package/dist/types/cli/grievances-cli.d.ts +12 -0
- package/dist/types/commands/auth-broker.d.ts +54 -0
- package/dist/types/commands/auth-gateway.d.ts +32 -0
- package/dist/types/commands/grievances.d.ts +1 -1
- package/dist/types/commit/agentic/tools/propose-commit.d.ts +9 -1
- package/dist/types/commit/agentic/tools/schemas.d.ts +9 -1
- package/dist/types/commit/agentic/tools/split-commit.d.ts +9 -1
- package/dist/types/config/model-registry.d.ts +3 -0
- package/dist/types/config/models-config-schema.d.ts +1 -0
- package/dist/types/config/settings-schema.d.ts +46 -0
- package/dist/types/discovery/agents.d.ts +12 -1
- package/dist/types/edit/renderer.d.ts +3 -0
- package/dist/types/eval/index.d.ts +0 -2
- package/dist/types/goals/tools/goal-tool.d.ts +10 -2
- package/dist/types/index.d.ts +0 -1
- package/dist/types/internal-urls/index.d.ts +1 -1
- package/dist/types/internal-urls/{pi-protocol.d.ts → omp-protocol.d.ts} +3 -3
- package/dist/types/internal-urls/types.d.ts +1 -1
- package/dist/types/modes/acp/acp-agent.d.ts +1 -0
- package/dist/types/modes/emoji-autocomplete.d.ts +16 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/prompt-action-autocomplete.d.ts +4 -0
- package/dist/types/plan-mode/approved-plan.d.ts +4 -0
- package/dist/types/sdk.d.ts +10 -3
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/auth-broker-config.d.ts +13 -0
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/tools/eval.d.ts +41 -7
- package/dist/types/tools/irc.d.ts +8 -2
- package/dist/types/tools/report-tool-issue.d.ts +118 -1
- package/dist/types/tools/resolve.d.ts +8 -2
- package/examples/custom-tools/README.md +3 -12
- package/examples/extensions/README.md +2 -15
- package/examples/extensions/api-demo.ts +1 -7
- package/package.json +7 -7
- package/src/autoresearch/tools/init-experiment.ts +11 -33
- package/src/autoresearch/tools/log-experiment.ts +10 -24
- package/src/autoresearch/tools/run-experiment.ts +1 -1
- package/src/autoresearch/tools/update-notes.ts +2 -9
- package/src/cli/auth-broker-cli.ts +746 -0
- package/src/cli/auth-gateway-cli.ts +342 -0
- package/src/cli/grievances-cli.ts +109 -16
- package/src/cli.ts +4 -2
- package/src/commands/auth-broker.ts +96 -0
- package/src/commands/auth-gateway.ts +61 -0
- package/src/commands/grievances.ts +13 -8
- package/src/commands/launch.ts +1 -1
- package/src/commit/agentic/agent.ts +2 -0
- package/src/commit/agentic/tools/analyze-file.ts +2 -2
- package/src/commit/agentic/tools/git-file-diff.ts +2 -2
- package/src/commit/agentic/tools/git-hunk.ts +3 -3
- package/src/commit/agentic/tools/git-overview.ts +2 -2
- package/src/commit/agentic/tools/propose-changelog.ts +1 -3
- package/src/commit/agentic/tools/recent-commits.ts +1 -1
- package/src/commit/agentic/tools/schemas.ts +1 -9
- package/src/config/model-equivalence.ts +279 -174
- package/src/config/model-registry.ts +37 -6
- package/src/config/model-resolver.ts +13 -8
- package/src/config/models-config-schema.ts +8 -0
- package/src/config/settings-schema.ts +52 -0
- package/src/cursor.ts +1 -1
- package/src/debug/log-formatting.ts +1 -1
- package/src/debug/log-viewer.ts +1 -1
- package/src/debug/profiler.ts +4 -0
- package/src/debug/raw-sse-buffer.ts +100 -59
- package/src/debug/raw-sse.ts +1 -1
- package/src/discovery/agents.ts +15 -4
- package/src/edit/modes/apply-patch.ts +1 -5
- package/src/edit/modes/patch.ts +5 -5
- package/src/edit/modes/replace.ts +5 -5
- package/src/edit/renderer.ts +2 -1
- package/src/edit/streaming.ts +1 -1
- package/src/eval/index.ts +0 -2
- package/src/eval/js/shared/runtime.ts +25 -0
- package/src/eval/py/kernel.ts +1 -1
- package/src/exa/researcher.ts +4 -4
- package/src/exa/search.ts +10 -22
- package/src/exa/websets.ts +33 -33
- package/src/goals/tools/goal-tool.ts +3 -3
- package/src/index.ts +0 -3
- package/src/internal-urls/docs-index.generated.ts +21 -18
- package/src/internal-urls/index.ts +1 -1
- package/src/internal-urls/{pi-protocol.ts → omp-protocol.ts} +10 -10
- package/src/internal-urls/router.ts +3 -3
- package/src/internal-urls/types.ts +1 -1
- package/src/lsp/types.ts +8 -11
- package/src/main.ts +3 -0
- package/src/mcp/tool-bridge.ts +3 -3
- package/src/modes/acp/acp-agent.ts +88 -25
- package/src/modes/components/bash-execution.ts +1 -1
- package/src/modes/components/diff.ts +1 -2
- package/src/modes/components/eval-execution.ts +1 -1
- package/src/modes/components/oauth-selector.ts +38 -2
- package/src/modes/components/tool-execution.ts +1 -2
- package/src/modes/controllers/command-controller.ts +95 -34
- package/src/modes/controllers/input-controller.ts +4 -3
- package/src/modes/data/emojis.json +1 -0
- package/src/modes/emoji-autocomplete.ts +285 -0
- package/src/modes/interactive-mode.ts +92 -19
- package/src/modes/print-mode.ts +3 -3
- package/src/modes/prompt-action-autocomplete.ts +14 -0
- package/src/plan-mode/approved-plan.ts +9 -0
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/prompts/system/ttsr-tool-reminder.md +5 -0
- package/src/prompts/tools/eval.md +25 -26
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/resolve.md +1 -1
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/web-search.md +1 -1
- package/src/sdk.ts +78 -7
- package/src/session/agent-session.ts +176 -77
- package/src/session/agent-storage.ts +7 -2
- package/src/session/auth-broker-config.ts +102 -0
- package/src/session/auth-storage.ts +7 -1
- package/src/session/streaming-output.ts +1 -1
- package/src/task/types.ts +10 -35
- package/src/tools/bash-interactive.ts +4 -1
- package/src/tools/bash-pty-selection.ts +2 -2
- package/src/tools/browser.ts +12 -20
- package/src/tools/eval.ts +77 -100
- package/src/tools/gh.ts +21 -45
- package/src/tools/hindsight-recall.ts +1 -1
- package/src/tools/hindsight-reflect.ts +2 -2
- package/src/tools/hindsight-retain.ts +3 -7
- package/src/tools/index.ts +8 -1
- package/src/tools/inspect-image.ts +4 -1
- package/src/tools/irc.ts +4 -12
- package/src/tools/job.ts +3 -11
- package/src/tools/report-tool-issue.ts +462 -17
- package/src/tools/resolve.ts +2 -7
- package/src/tools/todo-write.ts +8 -15
- package/src/utils/title-generator.ts +3 -0
- package/src/web/search/index.ts +6 -6
- package/dist/types/eval/parse.d.ts +0 -28
- package/dist/types/eval/sniff.d.ts +0 -11
- package/src/eval/eval.lark +0 -36
- package/src/eval/parse.ts +0 -407
- package/src/eval/sniff.ts +0 -28
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `omp auth-gateway` command handlers.
|
|
3
|
+
*
|
|
4
|
+
* Boots a forward-proxy server that lets less-trusted clients (the macOS
|
|
5
|
+
* usage widget, robomp containers, …) make provider API calls without ever
|
|
6
|
+
* seeing the access token. The gateway is itself a broker client and
|
|
7
|
+
* resolves credentials through the configured broker (via the same
|
|
8
|
+
* `OMP_AUTH_BROKER_URL` / `auth.broker.url` precedence used elsewhere).
|
|
9
|
+
*
|
|
10
|
+
* Sub-verbs:
|
|
11
|
+
* - `serve [--bind=…]` — boots the gateway against the configured broker.
|
|
12
|
+
* - `token` / `token --regenerate` — manages the gateway bearer token file.
|
|
13
|
+
* - `status` — prints the locally-stored gateway token and bind hint.
|
|
14
|
+
*/
|
|
15
|
+
import * as crypto from "node:crypto";
|
|
16
|
+
import * as fs from "node:fs/promises";
|
|
17
|
+
import * as path from "node:path";
|
|
18
|
+
import {
|
|
19
|
+
type Api,
|
|
20
|
+
AuthBrokerClient,
|
|
21
|
+
AuthStorage,
|
|
22
|
+
DEFAULT_AUTH_GATEWAY_BIND,
|
|
23
|
+
type GeneratedProvider,
|
|
24
|
+
getBundledModels,
|
|
25
|
+
getBundledProviders,
|
|
26
|
+
type Model,
|
|
27
|
+
RemoteAuthCredentialStore,
|
|
28
|
+
type SnapshotResponse,
|
|
29
|
+
startAuthGateway,
|
|
30
|
+
} from "@oh-my-pi/pi-ai";
|
|
31
|
+
import { getConfigRootDir, isEnoent, VERSION } from "@oh-my-pi/pi-utils";
|
|
32
|
+
import chalk from "chalk";
|
|
33
|
+
import { type AuthBrokerClientConfig, resolveAuthBrokerConfig } from "../session/auth-broker-config";
|
|
34
|
+
|
|
35
|
+
export type AuthGatewayAction = "serve" | "token" | "status";
|
|
36
|
+
|
|
37
|
+
export interface AuthGatewayCommandArgs {
|
|
38
|
+
action: AuthGatewayAction;
|
|
39
|
+
flags: {
|
|
40
|
+
json?: boolean;
|
|
41
|
+
bind?: string;
|
|
42
|
+
regenerate?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Disable bearer-token auth on inbound requests. Useful when the gateway
|
|
45
|
+
* is bound to loopback (the default `127.0.0.1:4000`) and you don't want
|
|
46
|
+
* to wire token-paste plumbing into every local client.
|
|
47
|
+
*/
|
|
48
|
+
noAuth?: boolean;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const ACTIONS: readonly AuthGatewayAction[] = ["serve", "token", "status"];
|
|
53
|
+
|
|
54
|
+
function getTokenFilePath(): string {
|
|
55
|
+
return path.join(getConfigRootDir(), "auth-gateway.token");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function readToken(): Promise<string | null> {
|
|
59
|
+
try {
|
|
60
|
+
const raw = await Bun.file(getTokenFilePath()).text();
|
|
61
|
+
const trimmed = raw.trim();
|
|
62
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
63
|
+
} catch (err) {
|
|
64
|
+
if (isEnoent(err)) return null;
|
|
65
|
+
throw err;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function writeToken(token: string): Promise<void> {
|
|
70
|
+
const file = getTokenFilePath();
|
|
71
|
+
await fs.mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
72
|
+
await fs.writeFile(file, token, { mode: 0o600 });
|
|
73
|
+
try {
|
|
74
|
+
await fs.chmod(file, 0o600);
|
|
75
|
+
} catch {
|
|
76
|
+
// Best-effort (e.g. Windows).
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Atomically create the token file, refusing to clobber an existing one.
|
|
82
|
+
* Returns `true` on success, `false` when the file already existed (so the
|
|
83
|
+
* caller re-reads it instead of racing another concurrent `ensureToken`).
|
|
84
|
+
*/
|
|
85
|
+
async function createTokenExclusive(token: string): Promise<boolean> {
|
|
86
|
+
const file = getTokenFilePath();
|
|
87
|
+
await fs.mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
88
|
+
try {
|
|
89
|
+
// `wx` = O_CREAT | O_EXCL — fails with EEXIST if the file is already there.
|
|
90
|
+
await fs.writeFile(file, token, { flag: "wx", mode: 0o600 });
|
|
91
|
+
} catch (err) {
|
|
92
|
+
if ((err as NodeJS.ErrnoException).code === "EEXIST") return false;
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
await fs.chmod(file, 0o600);
|
|
97
|
+
} catch {
|
|
98
|
+
// Best-effort (e.g. Windows).
|
|
99
|
+
}
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function generateToken(): string {
|
|
104
|
+
return crypto.randomBytes(32).toString("base64url");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function ensureToken(): Promise<string> {
|
|
108
|
+
const existing = await readToken();
|
|
109
|
+
if (existing) return existing;
|
|
110
|
+
const token = generateToken();
|
|
111
|
+
if (await createTokenExclusive(token)) return token;
|
|
112
|
+
// Another concurrent invocation won the create race; read what they wrote.
|
|
113
|
+
const fromRace = await readToken();
|
|
114
|
+
if (fromRace) return fromRace;
|
|
115
|
+
// File existed-then-disappeared between EEXIST and read; last resort, write
|
|
116
|
+
// our generated token unconditionally so callers don't see an empty string.
|
|
117
|
+
await writeToken(token);
|
|
118
|
+
return token;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function createBrokerClient(brokerConfig: AuthBrokerClientConfig): AuthBrokerClient {
|
|
122
|
+
return new AuthBrokerClient({ url: brokerConfig.url, token: brokerConfig.token });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function fetchBrokerSnapshot(client: AuthBrokerClient): Promise<SnapshotResponse> {
|
|
126
|
+
const result = await client.fetchSnapshot();
|
|
127
|
+
if (result.status !== 200) throw new Error("Auth broker returned no initial snapshot");
|
|
128
|
+
return result.snapshot;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function runServe(flags: AuthGatewayCommandArgs["flags"]): Promise<void> {
|
|
132
|
+
const brokerConfig = await resolveAuthBrokerConfig();
|
|
133
|
+
if (!brokerConfig) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
"`omp auth-gateway serve` requires OMP_AUTH_BROKER_URL (or `auth.broker.url`/`auth.broker.token` in config.yml). The gateway is itself a broker client.",
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
const bind = flags.bind ?? DEFAULT_AUTH_GATEWAY_BIND;
|
|
139
|
+
const gatewayToken = flags.noAuth ? null : await ensureToken();
|
|
140
|
+
|
|
141
|
+
// Build a broker-backed AuthStorage — same pattern as discoverAuthStorage()
|
|
142
|
+
// in sdk.ts. The gateway never touches local SQLite.
|
|
143
|
+
const client = createBrokerClient(brokerConfig);
|
|
144
|
+
const initialSnapshot = await fetchBrokerSnapshot(client);
|
|
145
|
+
const store = new RemoteAuthCredentialStore({ client, initialSnapshot });
|
|
146
|
+
// Refresh + usage both flow through the store's broker hooks automatically —
|
|
147
|
+
// `RemoteAuthCredentialStore.refreshOAuthCredential` and `.fetchUsageReports`.
|
|
148
|
+
// AuthStorage discovers them when no explicit option overrides them, so the
|
|
149
|
+
// gateway only needs to construct the store and pass it in.
|
|
150
|
+
const storage = new AuthStorage(store, {
|
|
151
|
+
sourceLabel: `broker ${brokerConfig.url}`,
|
|
152
|
+
});
|
|
153
|
+
await storage.reload();
|
|
154
|
+
|
|
155
|
+
// Build the model resolver + catalog from pi-ai's bundled metadata, scoped
|
|
156
|
+
// to providers we hold credentials for. Format handlers ask `resolveModel`
|
|
157
|
+
// to translate a client-requested `model` field into a pi-ai `Model<Api>`
|
|
158
|
+
// before dispatch; `listModels` powers `/v1/models`.
|
|
159
|
+
const snapshot = storage.exportSnapshot();
|
|
160
|
+
const providersWithCreds = new Set<string>();
|
|
161
|
+
for (const entry of snapshot.credentials) providersWithCreds.add(entry.provider);
|
|
162
|
+
const modelById = new Map<string, Model<Api>>();
|
|
163
|
+
for (const provider of getBundledProviders()) {
|
|
164
|
+
if (!providersWithCreds.has(provider)) continue;
|
|
165
|
+
for (const model of getBundledModels(provider as GeneratedProvider)) {
|
|
166
|
+
// First-write-wins so a canonical model id collisions across providers
|
|
167
|
+
// stick to the provider listed first by getBundledProviders.
|
|
168
|
+
if (!modelById.has(model.id)) modelById.set(model.id, model);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const handle = startAuthGateway({
|
|
173
|
+
storage,
|
|
174
|
+
bind,
|
|
175
|
+
bearerTokens: gatewayToken ? [gatewayToken] : [],
|
|
176
|
+
version: VERSION,
|
|
177
|
+
resolveModel: (id: string) => modelById.get(id),
|
|
178
|
+
listModels: () => modelById.values(),
|
|
179
|
+
});
|
|
180
|
+
process.stdout.write(`auth-gateway listening on ${handle.url}\n`);
|
|
181
|
+
if (gatewayToken) {
|
|
182
|
+
process.stdout.write(`bearer token: ${getTokenFilePath()} (chmod 0600)\n`);
|
|
183
|
+
} else {
|
|
184
|
+
process.stdout.write(`auth: disabled (--no-auth) — any client can call this gateway\n`);
|
|
185
|
+
}
|
|
186
|
+
process.stdout.write(`upstream broker: ${brokerConfig.url}\n`);
|
|
187
|
+
|
|
188
|
+
const stopped = Promise.withResolvers<void>();
|
|
189
|
+
let shutdownStarted = false;
|
|
190
|
+
const stop = async (signal: NodeJS.Signals): Promise<void> => {
|
|
191
|
+
if (shutdownStarted) return;
|
|
192
|
+
shutdownStarted = true;
|
|
193
|
+
process.stdout.write(`\nReceived ${signal}, shutting down...\n`);
|
|
194
|
+
let closeError: unknown;
|
|
195
|
+
try {
|
|
196
|
+
await handle.close();
|
|
197
|
+
} catch (error) {
|
|
198
|
+
closeError = error;
|
|
199
|
+
} finally {
|
|
200
|
+
storage.close();
|
|
201
|
+
}
|
|
202
|
+
if (closeError) {
|
|
203
|
+
stopped.reject(closeError);
|
|
204
|
+
} else {
|
|
205
|
+
stopped.resolve();
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
const onSigint = (): void => {
|
|
209
|
+
void stop("SIGINT");
|
|
210
|
+
};
|
|
211
|
+
const onSigterm = (): void => {
|
|
212
|
+
void stop("SIGTERM");
|
|
213
|
+
};
|
|
214
|
+
process.once("SIGINT", onSigint);
|
|
215
|
+
process.once("SIGTERM", onSigterm);
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
await stopped.promise;
|
|
219
|
+
} finally {
|
|
220
|
+
process.off("SIGINT", onSigint);
|
|
221
|
+
process.off("SIGTERM", onSigterm);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function runToken(flags: AuthGatewayCommandArgs["flags"]): Promise<void> {
|
|
226
|
+
if (flags.regenerate) {
|
|
227
|
+
const next = generateToken();
|
|
228
|
+
await writeToken(next);
|
|
229
|
+
if (flags.json) {
|
|
230
|
+
process.stdout.write(`${JSON.stringify({ token: next, path: getTokenFilePath() })}\n`);
|
|
231
|
+
} else {
|
|
232
|
+
process.stdout.write(`${next}\n`);
|
|
233
|
+
}
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const token = await ensureToken();
|
|
237
|
+
if (flags.json) {
|
|
238
|
+
process.stdout.write(`${JSON.stringify({ token, path: getTokenFilePath() })}\n`);
|
|
239
|
+
} else {
|
|
240
|
+
process.stdout.write(`${token}\n`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function runStatus(flags: AuthGatewayCommandArgs["flags"]): Promise<void> {
|
|
245
|
+
const token = await readToken();
|
|
246
|
+
const brokerConfig = await resolveAuthBrokerConfig();
|
|
247
|
+
const tokenFile = getTokenFilePath();
|
|
248
|
+
if (!brokerConfig) {
|
|
249
|
+
const status = {
|
|
250
|
+
ready: false,
|
|
251
|
+
reason: "not_configured",
|
|
252
|
+
tokenFile,
|
|
253
|
+
tokenPresent: token !== null,
|
|
254
|
+
broker: null,
|
|
255
|
+
brokerConfigured: false,
|
|
256
|
+
brokerAuthenticated: false,
|
|
257
|
+
};
|
|
258
|
+
if (flags.json) {
|
|
259
|
+
process.stdout.write(`${JSON.stringify(status)}\n`);
|
|
260
|
+
} else {
|
|
261
|
+
process.stdout.write(`${chalk.yellow("No broker configured.")} Set OMP_AUTH_BROKER_URL.\n`);
|
|
262
|
+
process.stdout.write(
|
|
263
|
+
`token: ${status.tokenPresent ? chalk.green("present") : chalk.red("missing")} at ${status.tokenFile}\n`,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
process.exitCode = 1;
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
const snapshot = await fetchBrokerSnapshot(createBrokerClient(brokerConfig));
|
|
272
|
+
const tokenPresent = token !== null;
|
|
273
|
+
const status = {
|
|
274
|
+
ready: tokenPresent,
|
|
275
|
+
reason: tokenPresent ? null : "token_missing",
|
|
276
|
+
tokenFile,
|
|
277
|
+
tokenPresent,
|
|
278
|
+
broker: brokerConfig.url,
|
|
279
|
+
brokerConfigured: true,
|
|
280
|
+
brokerAuthenticated: true,
|
|
281
|
+
credentialCount: snapshot.credentials.length,
|
|
282
|
+
};
|
|
283
|
+
if (flags.json) {
|
|
284
|
+
process.stdout.write(`${JSON.stringify(status)}\n`);
|
|
285
|
+
} else {
|
|
286
|
+
const brokerLine = `upstream broker: ${brokerConfig.url} (${snapshot.credentials.length} credential${
|
|
287
|
+
snapshot.credentials.length === 1 ? "" : "s"
|
|
288
|
+
})`;
|
|
289
|
+
process.stdout.write(`${tokenPresent ? chalk.green("ready") : chalk.yellow("not ready")} ${brokerLine}\n`);
|
|
290
|
+
process.stdout.write(
|
|
291
|
+
`token: ${tokenPresent ? chalk.green("present") : chalk.red("missing")} at ${status.tokenFile}\n`,
|
|
292
|
+
);
|
|
293
|
+
if (!tokenPresent) {
|
|
294
|
+
process.stdout.write(
|
|
295
|
+
"Run `omp auth-gateway token` or `omp auth-gateway serve` to create a bearer token.\n",
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (!tokenPresent) process.exitCode = 1;
|
|
300
|
+
} catch (error) {
|
|
301
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
302
|
+
const status = {
|
|
303
|
+
ready: false,
|
|
304
|
+
reason: "broker_unavailable",
|
|
305
|
+
tokenFile,
|
|
306
|
+
tokenPresent: token !== null,
|
|
307
|
+
broker: brokerConfig.url,
|
|
308
|
+
brokerConfigured: true,
|
|
309
|
+
brokerAuthenticated: false,
|
|
310
|
+
error: message,
|
|
311
|
+
};
|
|
312
|
+
if (flags.json) {
|
|
313
|
+
process.stdout.write(`${JSON.stringify(status)}\n`);
|
|
314
|
+
} else {
|
|
315
|
+
process.stdout.write(`${chalk.red("FAILED")} upstream broker: ${brokerConfig.url}: ${message}\n`);
|
|
316
|
+
process.stdout.write(
|
|
317
|
+
`token: ${status.tokenPresent ? chalk.green("present") : chalk.red("missing")} at ${status.tokenFile}\n`,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
process.exitCode = 1;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export async function runAuthGatewayCommand(cmd: AuthGatewayCommandArgs): Promise<void> {
|
|
325
|
+
switch (cmd.action) {
|
|
326
|
+
case "serve":
|
|
327
|
+
await runServe(cmd.flags);
|
|
328
|
+
return;
|
|
329
|
+
case "token":
|
|
330
|
+
await runToken(cmd.flags);
|
|
331
|
+
return;
|
|
332
|
+
case "status":
|
|
333
|
+
await runStatus(cmd.flags);
|
|
334
|
+
return;
|
|
335
|
+
default: {
|
|
336
|
+
const _exhaustive: never = cmd.action;
|
|
337
|
+
throw new Error(`Unknown auth-gateway action: ${String(_exhaustive)}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export { ACTIONS as AUTH_GATEWAY_ACTIONS };
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CLI handler for `omp grievances` — view reported tool issues
|
|
2
|
+
* CLI handler for `omp grievances` — view, clean, and manually push reported tool issues.
|
|
3
3
|
*/
|
|
4
|
-
import { Database } from "bun:sqlite";
|
|
5
4
|
import chalk from "chalk";
|
|
6
|
-
import {
|
|
5
|
+
import { Settings } from "../config/settings";
|
|
6
|
+
import { flushGrievances, openAutoQaDb } from "../tools/report-tool-issue";
|
|
7
7
|
|
|
8
8
|
interface GrievanceRow {
|
|
9
9
|
id: number;
|
|
@@ -30,20 +30,12 @@ export interface CleanGrievancesOptions {
|
|
|
30
30
|
json?: boolean;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
// readwrite, or create flags to be explicit. Use the default constructor
|
|
37
|
-
// (readwrite + create) for write mode and only pass `readonly: true` when
|
|
38
|
-
// listing.
|
|
39
|
-
return readonly ? new Database(getAutoQaDbPath(), { readonly: true }) : new Database(getAutoQaDbPath());
|
|
40
|
-
} catch {
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
33
|
+
export interface PushGrievancesOptions {
|
|
34
|
+
/** Emit the {@link FlushResult} as JSON instead of a status line. */
|
|
35
|
+
json?: boolean;
|
|
43
36
|
}
|
|
44
|
-
|
|
45
37
|
export async function listGrievances(options: ListGrievancesOptions): Promise<void> {
|
|
46
|
-
const db =
|
|
38
|
+
const db = openAutoQaDb();
|
|
47
39
|
if (!db) {
|
|
48
40
|
if (options.json) {
|
|
49
41
|
console.log("[]");
|
|
@@ -112,7 +104,7 @@ export async function cleanGrievances(options: CleanGrievancesOptions): Promise<
|
|
|
112
104
|
return;
|
|
113
105
|
}
|
|
114
106
|
|
|
115
|
-
const db =
|
|
107
|
+
const db = openAutoQaDb();
|
|
116
108
|
if (!db) {
|
|
117
109
|
if (options.json) {
|
|
118
110
|
console.log(JSON.stringify({ deleted: 0 }));
|
|
@@ -161,3 +153,104 @@ export async function cleanGrievances(options: CleanGrievancesOptions): Promise<
|
|
|
161
153
|
db.close();
|
|
162
154
|
}
|
|
163
155
|
}
|
|
156
|
+
|
|
157
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
158
|
+
// Manual push (`omp grievances push`)
|
|
159
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Single-line ANSI progress reporter. `update(done)` rewrites the line via
|
|
163
|
+
* `\r`; `finish()` newlines out so subsequent log lines land cleanly. On a
|
|
164
|
+
* non-TTY stdout (CI, pipes) both calls no-op so log files don't fill with
|
|
165
|
+
* carriage-return noise.
|
|
166
|
+
*/
|
|
167
|
+
interface ProgressBar {
|
|
168
|
+
update(done: number): void;
|
|
169
|
+
finish(): void;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function makeProgressBar(total: number, width = 30): ProgressBar {
|
|
173
|
+
const isTty = !!process.stdout.isTTY;
|
|
174
|
+
if (!isTty || total === 0) {
|
|
175
|
+
return { update: () => undefined, finish: () => undefined };
|
|
176
|
+
}
|
|
177
|
+
const render = (done: number): void => {
|
|
178
|
+
const ratio = Math.min(1, done / total);
|
|
179
|
+
const filled = Math.round(ratio * width);
|
|
180
|
+
const bar = `${"█".repeat(filled)}${"░".repeat(width - filled)}`;
|
|
181
|
+
const pct = `${Math.floor(ratio * 100)
|
|
182
|
+
.toString()
|
|
183
|
+
.padStart(3, " ")}%`;
|
|
184
|
+
process.stdout.write(`\r${chalk.cyan("Pushing")} [${bar}] ${pct} ${done}/${total}`);
|
|
185
|
+
};
|
|
186
|
+
render(0);
|
|
187
|
+
return {
|
|
188
|
+
update: render,
|
|
189
|
+
finish: () => process.stdout.write("\n"),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Manually drain every unpushed grievance to the configured backend,
|
|
195
|
+
* ignoring the user-facing consent gate (manual push is the user's
|
|
196
|
+
* explicit "yes ship these now" intent).
|
|
197
|
+
*
|
|
198
|
+
* Requires endpoint configuration (default `qa.omp.sh/v1/grievances`).
|
|
199
|
+
*/
|
|
200
|
+
export async function pushGrievances(options: PushGrievancesOptions): Promise<void> {
|
|
201
|
+
const db = openAutoQaDb();
|
|
202
|
+
if (!db) {
|
|
203
|
+
if (options.json) {
|
|
204
|
+
console.log(JSON.stringify({ pushed: 0, ok: false, skipped: true, reason: "no_db" }));
|
|
205
|
+
} else {
|
|
206
|
+
console.log(chalk.dim("No grievances database found — nothing to push."));
|
|
207
|
+
}
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const settings = await Settings.init();
|
|
211
|
+
let bar: ProgressBar = { update: () => undefined, finish: () => undefined };
|
|
212
|
+
let total = 0;
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
const result = await flushGrievances(db, settings, {
|
|
216
|
+
bypassConsent: true,
|
|
217
|
+
onStart: t => {
|
|
218
|
+
total = t;
|
|
219
|
+
if (!options.json) bar = makeProgressBar(t);
|
|
220
|
+
},
|
|
221
|
+
onProgress: pushed => bar.update(pushed),
|
|
222
|
+
});
|
|
223
|
+
bar.finish();
|
|
224
|
+
|
|
225
|
+
if (options.json) {
|
|
226
|
+
console.log(JSON.stringify(result));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (result.skipped) {
|
|
231
|
+
console.log(
|
|
232
|
+
chalk.yellow(
|
|
233
|
+
"Push skipped — no endpoint configured. Set `dev.autoqaPush.endpoint` or `PI_AUTO_QA_PUSH_URL`.",
|
|
234
|
+
),
|
|
235
|
+
);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (total === 0) {
|
|
239
|
+
console.log(chalk.dim("Nothing to push — all grievances are already shipped."));
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (result.ok) {
|
|
243
|
+
console.log(chalk.green(`Pushed ${result.pushed}/${total} grievance${result.pushed === 1 ? "" : "s"}.`));
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const remaining = total - result.pushed;
|
|
247
|
+
console.log(
|
|
248
|
+
chalk.red(
|
|
249
|
+
`Push failed after ${result.pushed}/${total}; ${remaining} grievance${remaining === 1 ? "" : "s"} remain unpushed.`,
|
|
250
|
+
),
|
|
251
|
+
);
|
|
252
|
+
process.exitCode = 1;
|
|
253
|
+
} finally {
|
|
254
|
+
db.close();
|
|
255
|
+
}
|
|
256
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -18,7 +18,7 @@ procmgr.scrubProcessEnv();
|
|
|
18
18
|
* CLI entry point — registers all commands explicitly and delegates to the
|
|
19
19
|
* lightweight CLI runner from pi-utils.
|
|
20
20
|
*/
|
|
21
|
-
import { type CommandEntry, run } from "@oh-my-pi/pi-utils/cli";
|
|
21
|
+
import { type CliConfig, type CommandEntry, run } from "@oh-my-pi/pi-utils/cli";
|
|
22
22
|
|
|
23
23
|
if (Bun.semver.order(Bun.version, MIN_BUN_VERSION) < 0) {
|
|
24
24
|
process.stderr.write(
|
|
@@ -32,6 +32,8 @@ process.title = APP_NAME;
|
|
|
32
32
|
const commands: CommandEntry[] = [
|
|
33
33
|
{ name: "launch", load: () => import("./commands/launch").then(m => m.default) },
|
|
34
34
|
{ name: "acp", load: () => import("./commands/acp").then(m => m.default) },
|
|
35
|
+
{ name: "auth-broker", load: () => import("./commands/auth-broker").then(m => m.default) },
|
|
36
|
+
{ name: "auth-gateway", load: () => import("./commands/auth-gateway").then(m => m.default) },
|
|
35
37
|
{ name: "agents", load: () => import("./commands/agents").then(m => m.default) },
|
|
36
38
|
{ name: "commit", load: () => import("./commands/commit").then(m => m.default) },
|
|
37
39
|
{ name: "config", load: () => import("./commands/config").then(m => m.default) },
|
|
@@ -47,7 +49,7 @@ const commands: CommandEntry[] = [
|
|
|
47
49
|
{ name: "search", load: () => import("./commands/web-search").then(m => m.default), aliases: ["q"] },
|
|
48
50
|
];
|
|
49
51
|
|
|
50
|
-
async function showHelp(config:
|
|
52
|
+
async function showHelp(config: CliConfig): Promise<void> {
|
|
51
53
|
const { renderRootHelp } = await import("@oh-my-pi/pi-utils/cli");
|
|
52
54
|
const { getExtraHelpText } = await import("./cli/args");
|
|
53
55
|
renderRootHelp(config);
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `omp auth-broker` — manage the omp credential vault.
|
|
3
|
+
*/
|
|
4
|
+
import { Args, Command, Flags, renderCommandHelp } from "@oh-my-pi/pi-utils/cli";
|
|
5
|
+
import {
|
|
6
|
+
AUTH_BROKER_ACTIONS,
|
|
7
|
+
type AuthBrokerAction,
|
|
8
|
+
type AuthBrokerCommandArgs,
|
|
9
|
+
runAuthBrokerCommand,
|
|
10
|
+
} from "../cli/auth-broker-cli";
|
|
11
|
+
import { initTheme } from "../modes/theme/theme";
|
|
12
|
+
|
|
13
|
+
export default class AuthBroker extends Command {
|
|
14
|
+
static description = "Manage the omp auth-broker (credential vault)";
|
|
15
|
+
|
|
16
|
+
static args = {
|
|
17
|
+
action: Args.string({
|
|
18
|
+
description: "Sub-command",
|
|
19
|
+
required: false,
|
|
20
|
+
options: [...AUTH_BROKER_ACTIONS],
|
|
21
|
+
}),
|
|
22
|
+
// Second positional: provider id (login/logout) or filesystem path (import).
|
|
23
|
+
source: Args.string({
|
|
24
|
+
description: "OAuth provider id (login/logout) or path (import)",
|
|
25
|
+
required: false,
|
|
26
|
+
}),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
static flags = {
|
|
30
|
+
json: Flags.boolean({ description: "Output JSON" }),
|
|
31
|
+
bind: Flags.string({ description: "Bind address for `serve` (host:port)", char: "b" }),
|
|
32
|
+
regenerate: Flags.boolean({ description: "Regenerate the bearer token" }),
|
|
33
|
+
via: Flags.string({
|
|
34
|
+
description: "SSH user@host for remote login (login --via=user@host)",
|
|
35
|
+
}),
|
|
36
|
+
provider: Flags.string({
|
|
37
|
+
description: "Override provider id for `import` (e.g. when JSON `type` is unrecognized)",
|
|
38
|
+
}),
|
|
39
|
+
"include-disabled": Flags.boolean({
|
|
40
|
+
description: "Import credentials whose JSON has `disabled: true` (import)",
|
|
41
|
+
}),
|
|
42
|
+
"from-local": Flags.boolean({
|
|
43
|
+
description: "migrate source: local SQLite + env vars (required for `migrate`)",
|
|
44
|
+
}),
|
|
45
|
+
"include-env": Flags.boolean({
|
|
46
|
+
description: "Capture env-var API keys for providers not yet on broker (migrate)",
|
|
47
|
+
}),
|
|
48
|
+
"include-oauth": Flags.boolean({
|
|
49
|
+
description: "Also upload OAuth from local SQLite during migrate (default skips them)",
|
|
50
|
+
}),
|
|
51
|
+
"dry-run": Flags.boolean({ description: "Print actions without executing (import / login --via / migrate)" }),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
static examples = [
|
|
55
|
+
"# Boot the broker against the local SQLite store\n omp auth-broker serve",
|
|
56
|
+
"# Boot on a non-default port\n omp auth-broker serve --bind=127.0.0.1:9000",
|
|
57
|
+
"# Print the bearer token\n omp auth-broker token",
|
|
58
|
+
"# Rotate the bearer token\n omp auth-broker token --regenerate",
|
|
59
|
+
"# Local login (run on the broker host)\n omp auth-broker login anthropic",
|
|
60
|
+
"# Remote login over SSH tunnel\n omp auth-broker login anthropic --via=user@broker",
|
|
61
|
+
"# Import a CLIProxyAPI auth dump\n omp auth-broker import ~/.cliproxy/auth",
|
|
62
|
+
"# Import a single CLIProxyAPI JSON, overriding the provider mapping\n omp auth-broker import ~/.cliproxy/auth/claude-foo.json --provider anthropic",
|
|
63
|
+
"# Preview a migration from local store + env vars to the configured broker\n omp auth-broker migrate --from-local --include-env --dry-run",
|
|
64
|
+
"# Apply the migration\n omp auth-broker migrate --from-local --include-env",
|
|
65
|
+
"# Health-check the configured remote broker\n omp auth-broker status",
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
async run(): Promise<void> {
|
|
69
|
+
const { args, flags } = await this.parse(AuthBroker);
|
|
70
|
+
if (!args.action) {
|
|
71
|
+
renderCommandHelp("omp", "auth-broker", AuthBroker);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const action = args.action as AuthBrokerAction;
|
|
75
|
+
const cmd: AuthBrokerCommandArgs = {
|
|
76
|
+
action,
|
|
77
|
+
flags: {
|
|
78
|
+
json: flags.json,
|
|
79
|
+
bind: flags.bind,
|
|
80
|
+
regenerate: flags.regenerate,
|
|
81
|
+
via: flags.via,
|
|
82
|
+
// `login`/`logout` reuse the legacy `provider` slot; `import` keeps `source` separate
|
|
83
|
+
// so `provider` flag (used as an override) is unambiguous.
|
|
84
|
+
provider: action === "import" ? flags.provider : (args.source ?? flags.provider),
|
|
85
|
+
source: args.source,
|
|
86
|
+
includeDisabled: flags["include-disabled"],
|
|
87
|
+
fromLocal: flags["from-local"],
|
|
88
|
+
includeEnv: flags["include-env"],
|
|
89
|
+
includeOauth: flags["include-oauth"],
|
|
90
|
+
dryRun: flags["dry-run"],
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
await initTheme();
|
|
94
|
+
await runAuthBrokerCommand(cmd);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `omp auth-gateway` — run a forward proxy that injects auth from the broker.
|
|
3
|
+
*/
|
|
4
|
+
import { Args, Command, Flags, renderCommandHelp } from "@oh-my-pi/pi-utils/cli";
|
|
5
|
+
import {
|
|
6
|
+
AUTH_GATEWAY_ACTIONS,
|
|
7
|
+
type AuthGatewayAction,
|
|
8
|
+
type AuthGatewayCommandArgs,
|
|
9
|
+
runAuthGatewayCommand,
|
|
10
|
+
} from "../cli/auth-gateway-cli";
|
|
11
|
+
import { initTheme } from "../modes/theme/theme";
|
|
12
|
+
|
|
13
|
+
export default class AuthGateway extends Command {
|
|
14
|
+
static description = "Run an auth-gateway forward proxy backed by the configured broker";
|
|
15
|
+
|
|
16
|
+
static args = {
|
|
17
|
+
action: Args.string({
|
|
18
|
+
description: "Sub-command",
|
|
19
|
+
required: false,
|
|
20
|
+
options: [...AUTH_GATEWAY_ACTIONS],
|
|
21
|
+
}),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
static flags = {
|
|
25
|
+
json: Flags.boolean({ description: "Output JSON (token/status)" }),
|
|
26
|
+
bind: Flags.string({ description: "Bind address for `serve` (host:port)", char: "b" }),
|
|
27
|
+
regenerate: Flags.boolean({ description: "Regenerate the gateway bearer token (token)" }),
|
|
28
|
+
"no-auth": Flags.boolean({
|
|
29
|
+
description:
|
|
30
|
+
"Disable inbound bearer-token auth (serve). Useful when bound to loopback — any caller is allowed.",
|
|
31
|
+
}),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
static examples = [
|
|
35
|
+
"# Boot the gateway against the configured broker\n omp auth-gateway serve",
|
|
36
|
+
"# Boot on a non-default port\n omp auth-gateway serve --bind=127.0.0.1:4000",
|
|
37
|
+
"# Print the gateway bearer token (creates one on first run)\n omp auth-gateway token",
|
|
38
|
+
"# Rotate the gateway bearer token\n omp auth-gateway token --regenerate",
|
|
39
|
+
"# Run on loopback without any bearer (anyone on this host can call)\n omp auth-gateway serve --no-auth",
|
|
40
|
+
"# Show local gateway + broker config status\n omp auth-gateway status",
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
async run(): Promise<void> {
|
|
44
|
+
const { args, flags } = await this.parse(AuthGateway);
|
|
45
|
+
if (!args.action) {
|
|
46
|
+
renderCommandHelp("omp", "auth-gateway", AuthGateway);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const cmd: AuthGatewayCommandArgs = {
|
|
50
|
+
action: args.action as AuthGatewayAction,
|
|
51
|
+
flags: {
|
|
52
|
+
json: flags.json,
|
|
53
|
+
bind: flags.bind,
|
|
54
|
+
regenerate: flags.regenerate,
|
|
55
|
+
noAuth: flags["no-auth"],
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
await initTheme();
|
|
59
|
+
await runAuthGatewayCommand(cmd);
|
|
60
|
+
}
|
|
61
|
+
}
|