@yagni-app/code 1.0.0 → 1.0.1
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/README.md +42 -0
- package/dist/cli.js +231 -6
- package/dist/crashReport.d.ts +8 -0
- package/dist/crashReport.js +13 -1
- package/dist/doctor.d.ts +7 -0
- package/dist/doctor.js +33 -0
- package/dist/extension/askAdvisorTool.d.ts +7 -0
- package/dist/extension/askAdvisorTool.js +11 -3
- package/dist/extension/askYagniTool.js +2 -0
- package/dist/extension/branding.d.ts +15 -0
- package/dist/extension/branding.js +76 -0
- package/dist/extension/chipEditor.d.ts +22 -1
- package/dist/extension/chipEditor.js +58 -5
- package/dist/extension/condensedTools.d.ts +93 -0
- package/dist/extension/condensedTools.js +392 -0
- package/dist/extension/diffStat.d.ts +62 -0
- package/dist/extension/diffStat.js +158 -0
- package/dist/extension/footer.d.ts +2 -0
- package/dist/extension/footer.js +21 -8
- package/dist/extension/index.d.ts +6 -0
- package/dist/extension/index.js +70 -2
- package/dist/extension/permission/execPolicy.js +47 -0
- package/dist/extension/pipeline/invocation.d.ts +7 -0
- package/dist/extension/pipeline/invocation.js +7 -0
- package/dist/extension/pipeline/personas.js +4 -4
- package/dist/extension/pipeline/runner.d.ts +1 -0
- package/dist/extension/pipeline/runner.js +15 -3
- package/dist/extension/pipeline/sessionWorktree.d.ts +64 -0
- package/dist/extension/pipeline/sessionWorktree.js +225 -0
- package/dist/extension/scratchpad.d.ts +66 -0
- package/dist/extension/scratchpad.js +93 -0
- package/dist/extension/subagents.d.ts +10 -0
- package/dist/extension/subagents.js +18 -4
- package/dist/extension/todos.d.ts +1 -0
- package/dist/extension/todos.js +15 -0
- package/dist/extension/toolRuns.d.ts +92 -0
- package/dist/extension/toolRuns.js +201 -0
- package/dist/extension/webFetchTool.js +2 -0
- package/dist/extension/workingLine.d.ts +49 -0
- package/dist/extension/workingLine.js +116 -0
- package/dist/feedback.d.ts +77 -0
- package/dist/feedback.js +500 -0
- package/dist/goHeadless.d.ts +3 -0
- package/dist/goHeadless.js +13 -0
- package/dist/launch.d.ts +8 -0
- package/dist/launch.js +6 -0
- package/dist/otel.d.ts +150 -0
- package/dist/otel.js +291 -0
- package/dist/outputFormat.d.ts +83 -0
- package/dist/outputFormat.js +207 -0
- package/dist/paths.d.ts +10 -0
- package/dist/paths.js +13 -0
- package/dist/worktreeArgs.d.ts +43 -0
- package/dist/worktreeArgs.js +96 -0
- package/package.json +3 -2
package/dist/otel.d.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OTel export wiring (Updater pilot ask): bake the `pi-otel` extension into
|
|
3
|
+
* every launch so a workspace can stream session traces — token usage, cost,
|
|
4
|
+
* model tier, tool calls — to its own OTLP collector (e.g. Datadog) without
|
|
5
|
+
* anyone hand-installing an extension.
|
|
6
|
+
*
|
|
7
|
+
* Three deliberate policies, all enforced here rather than left to defaults:
|
|
8
|
+
*
|
|
9
|
+
* 1. GATED, not always-on. pi-otel ships `enabled: true` pointed at
|
|
10
|
+
* `localhost:4317`, so loading it unconditionally would have every session
|
|
11
|
+
* probing a collector nobody runs. We only pass the extension to pi when an
|
|
12
|
+
* OTLP endpoint is actually configured: `OTEL_EXPORTER_OTLP_ENDPOINT` in the
|
|
13
|
+
* environment, or `otel.endpoint` in the repo's committed `.pi/settings.json`
|
|
14
|
+
* (the file pi-otel itself reads, so teams can configure once per repo).
|
|
15
|
+
*
|
|
16
|
+
* 2. METADATA-ONLY, enforced. Cost, tokens, model, finish reasons and tool-call
|
|
17
|
+
* ids export; prompt and response text never do. `PI_OTEL_CAPTURE_CONTENT`
|
|
18
|
+
* is pinned in the child env, and env beats settings in pi-otel's own
|
|
19
|
+
* precedence — a repo settings file asking for "full" is overridden, not
|
|
20
|
+
* honored. Session content leaving the machine is a contract change, not a
|
|
21
|
+
* config knob.
|
|
22
|
+
*
|
|
23
|
+
* 3. Cost is the customer's SELL rate. pi-otel exports pi's `usage.cost.total`
|
|
24
|
+
* verbatim (as `pi.cost.usd`), and pi computes that from the catalog's
|
|
25
|
+
* tier rates (YAG-381) — so the collector sees contracted prices, and the
|
|
26
|
+
* exported model name is the opaque tier id, never the backing model.
|
|
27
|
+
*
|
|
28
|
+
* Everything here is fail-soft: telemetry must never block a launch, so a
|
|
29
|
+
* missing package or unreadable settings file resolves to "disabled".
|
|
30
|
+
*/
|
|
31
|
+
import { readFileSync } from "node:fs";
|
|
32
|
+
/**
|
|
33
|
+
* Env var carrying the resolved pi-otel entry path to /go stage children and
|
|
34
|
+
* subagents: they build their own pi argv inside pi-extension-yagni (which
|
|
35
|
+
* cannot import this package), so the path crosses over env. The extension
|
|
36
|
+
* reads the same literal in `pipeline/runner.ts`.
|
|
37
|
+
*/
|
|
38
|
+
export declare const OTEL_EXTENSION_PATH_ENV = "YAGNI_OTEL_EXTENSION_PATH";
|
|
39
|
+
/** The service name a collector sees unless the user set their own. */
|
|
40
|
+
export declare const DEFAULT_OTEL_SERVICE_NAME = "yagni-code";
|
|
41
|
+
export interface OtelLaunchConfig {
|
|
42
|
+
/** Absolute path to pi-otel's extension entry. */
|
|
43
|
+
extensionPath: string;
|
|
44
|
+
/**
|
|
45
|
+
* Where the enablement signal came from. "workspace" additionally carries
|
|
46
|
+
* protocol/headers/serviceName, delivered as env to the session (the other
|
|
47
|
+
* sources leave those to whatever the user/repo already configured).
|
|
48
|
+
*/
|
|
49
|
+
source: "env" | "workspace" | "project-settings";
|
|
50
|
+
/** The configured OTLP endpoint (for doctor display; env for workspace source). */
|
|
51
|
+
endpoint: string;
|
|
52
|
+
/** Workspace-configured OTLP protocol (workspace source only). */
|
|
53
|
+
protocol?: string;
|
|
54
|
+
/** Workspace-configured collector headers (workspace source only; carries secrets). */
|
|
55
|
+
headers?: Record<string, string>;
|
|
56
|
+
/** Workspace-configured service name (workspace source only). */
|
|
57
|
+
serviceName?: string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The `otel` block a workspace admin configures in the web app, as served on
|
|
61
|
+
* GET /api/yagni-code/models (decrypted headers included — this is the same
|
|
62
|
+
* trust boundary as the YAGNI_TOKEN that fetched it).
|
|
63
|
+
*/
|
|
64
|
+
export interface WorkspaceOtelConfig {
|
|
65
|
+
enabled: boolean;
|
|
66
|
+
endpoint: string;
|
|
67
|
+
protocol?: string;
|
|
68
|
+
serviceName?: string | null;
|
|
69
|
+
headers?: Record<string, string>;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Decide whether this launch exports OTel traces, and with which pi-otel entry.
|
|
73
|
+
* Returns undefined when export stays off: no endpoint configured anywhere,
|
|
74
|
+
* `PI_OTEL_DISABLED` set, or the pi-otel package unresolvable (never fatal).
|
|
75
|
+
*/
|
|
76
|
+
export declare function resolveOtelLaunch(opts: {
|
|
77
|
+
env: NodeJS.ProcessEnv;
|
|
78
|
+
cwd: string;
|
|
79
|
+
/** Injectable seams for tests. */
|
|
80
|
+
resolveExtension?: () => string;
|
|
81
|
+
readFile?: typeof readFileSync;
|
|
82
|
+
}): OtelLaunchConfig | undefined;
|
|
83
|
+
/**
|
|
84
|
+
* Absolute path to pi-otel's extension entry (its package main IS the pi
|
|
85
|
+
* extension entry, `dist/index.js`). Resolved ESM-native like `paths.ts` does
|
|
86
|
+
* for pi itself. Throws when the package is missing — callers treat that as
|
|
87
|
+
* "export off", not an error.
|
|
88
|
+
*/
|
|
89
|
+
export declare function resolveOtelExtensionPath(): string;
|
|
90
|
+
/**
|
|
91
|
+
* The env keys an OTel-exporting child must carry. `PI_OTEL_CAPTURE_CONTENT`
|
|
92
|
+
* is pinned unconditionally (policy 2 above); the service name only fills in
|
|
93
|
+
* when the user has not chosen their own.
|
|
94
|
+
*
|
|
95
|
+
* A "workspace"-sourced config additionally delivers the admin-set endpoint,
|
|
96
|
+
* protocol, and collector headers over the standard OTel env vars — but any
|
|
97
|
+
* of those the USER already set in their own environment wins (local env >
|
|
98
|
+
* workspace config), so an engineer can point one session at a scratch
|
|
99
|
+
* collector without an admin change.
|
|
100
|
+
*/
|
|
101
|
+
export declare function otelChildEnv(config: OtelLaunchConfig, baseEnv: NodeJS.ProcessEnv): Record<string, string>;
|
|
102
|
+
export interface WorkspaceOtelFetchDeps {
|
|
103
|
+
fetchImpl?: typeof fetch;
|
|
104
|
+
cacheDir?: string;
|
|
105
|
+
timeoutMs?: number;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Resolve the workspace's admin-set OTel config: one fail-soft GET of the
|
|
109
|
+
* catalog endpoint (which carries the additive `otel` block), cached on disk
|
|
110
|
+
* per profile so a slow or offline backend degrades to last-known config
|
|
111
|
+
* instead of a launch stall.
|
|
112
|
+
*
|
|
113
|
+
* Cache semantics matter for revocation: a SUCCESSFUL response without an
|
|
114
|
+
* otel block means the admin disabled or removed the config, so the cache is
|
|
115
|
+
* DELETED — only a network/HTTP failure falls back to it. Otherwise turning
|
|
116
|
+
* export off in the web app would leave every laptop exporting (with a stale
|
|
117
|
+
* collector key) until the cache happened to be overwritten.
|
|
118
|
+
*
|
|
119
|
+
* The cache lives under the credentials dir at mode 0600 — the same posture
|
|
120
|
+
* as the profile token files, which is the right comparison: the cached
|
|
121
|
+
* headers carry the collector key, the profile carries the YAGNI token.
|
|
122
|
+
*/
|
|
123
|
+
export declare function fetchWorkspaceOtel(creds: {
|
|
124
|
+
baseUrl: string;
|
|
125
|
+
token: string;
|
|
126
|
+
}, profileName: string, deps?: WorkspaceOtelFetchDeps): Promise<WorkspaceOtelConfig | null>;
|
|
127
|
+
/**
|
|
128
|
+
* Full launch-time resolution, all three sources in precedence order:
|
|
129
|
+
*
|
|
130
|
+
* 1. `PI_OTEL_DISABLED` — personal kill switch, beats everything.
|
|
131
|
+
* 2. env `OTEL_EXPORTER_OTLP_ENDPOINT` — the user's own setup, untouched.
|
|
132
|
+
* 3. workspace config — admin-set in the web app, fetched/cached.
|
|
133
|
+
* 4. repo `.pi/settings.json` — committed per-repo config.
|
|
134
|
+
*
|
|
135
|
+
* `creds` absent (not logged in — doctor on a fresh machine) skips source 3.
|
|
136
|
+
*/
|
|
137
|
+
export declare function resolveOtelLaunchWithWorkspace(opts: {
|
|
138
|
+
env: NodeJS.ProcessEnv;
|
|
139
|
+
cwd: string;
|
|
140
|
+
creds?: {
|
|
141
|
+
baseUrl: string;
|
|
142
|
+
token: string;
|
|
143
|
+
} | null;
|
|
144
|
+
profileName?: string;
|
|
145
|
+
resolveExtension?: () => string;
|
|
146
|
+
readFile?: typeof readFileSync;
|
|
147
|
+
fetchDeps?: WorkspaceOtelFetchDeps;
|
|
148
|
+
fetchWorkspace?: typeof fetchWorkspaceOtel;
|
|
149
|
+
}): Promise<OtelLaunchConfig | undefined>;
|
|
150
|
+
//# sourceMappingURL=otel.d.ts.map
|
package/dist/otel.js
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OTel export wiring (Updater pilot ask): bake the `pi-otel` extension into
|
|
3
|
+
* every launch so a workspace can stream session traces — token usage, cost,
|
|
4
|
+
* model tier, tool calls — to its own OTLP collector (e.g. Datadog) without
|
|
5
|
+
* anyone hand-installing an extension.
|
|
6
|
+
*
|
|
7
|
+
* Three deliberate policies, all enforced here rather than left to defaults:
|
|
8
|
+
*
|
|
9
|
+
* 1. GATED, not always-on. pi-otel ships `enabled: true` pointed at
|
|
10
|
+
* `localhost:4317`, so loading it unconditionally would have every session
|
|
11
|
+
* probing a collector nobody runs. We only pass the extension to pi when an
|
|
12
|
+
* OTLP endpoint is actually configured: `OTEL_EXPORTER_OTLP_ENDPOINT` in the
|
|
13
|
+
* environment, or `otel.endpoint` in the repo's committed `.pi/settings.json`
|
|
14
|
+
* (the file pi-otel itself reads, so teams can configure once per repo).
|
|
15
|
+
*
|
|
16
|
+
* 2. METADATA-ONLY, enforced. Cost, tokens, model, finish reasons and tool-call
|
|
17
|
+
* ids export; prompt and response text never do. `PI_OTEL_CAPTURE_CONTENT`
|
|
18
|
+
* is pinned in the child env, and env beats settings in pi-otel's own
|
|
19
|
+
* precedence — a repo settings file asking for "full" is overridden, not
|
|
20
|
+
* honored. Session content leaving the machine is a contract change, not a
|
|
21
|
+
* config knob.
|
|
22
|
+
*
|
|
23
|
+
* 3. Cost is the customer's SELL rate. pi-otel exports pi's `usage.cost.total`
|
|
24
|
+
* verbatim (as `pi.cost.usd`), and pi computes that from the catalog's
|
|
25
|
+
* tier rates (YAG-381) — so the collector sees contracted prices, and the
|
|
26
|
+
* exported model name is the opaque tier id, never the backing model.
|
|
27
|
+
*
|
|
28
|
+
* Everything here is fail-soft: telemetry must never block a launch, so a
|
|
29
|
+
* missing package or unreadable settings file resolves to "disabled".
|
|
30
|
+
*/
|
|
31
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
32
|
+
import { join } from "node:path";
|
|
33
|
+
import { fileURLToPath } from "node:url";
|
|
34
|
+
import { credentialsDir } from "./credentials.js";
|
|
35
|
+
/**
|
|
36
|
+
* Env var carrying the resolved pi-otel entry path to /go stage children and
|
|
37
|
+
* subagents: they build their own pi argv inside pi-extension-yagni (which
|
|
38
|
+
* cannot import this package), so the path crosses over env. The extension
|
|
39
|
+
* reads the same literal in `pipeline/runner.ts`.
|
|
40
|
+
*/
|
|
41
|
+
export const OTEL_EXTENSION_PATH_ENV = "YAGNI_OTEL_EXTENSION_PATH";
|
|
42
|
+
/** The service name a collector sees unless the user set their own. */
|
|
43
|
+
export const DEFAULT_OTEL_SERVICE_NAME = "yagni-code";
|
|
44
|
+
/**
|
|
45
|
+
* Read `otel.endpoint` from the repo's `.pi/settings.json`, the project-level
|
|
46
|
+
* file pi-otel itself resolves config from. Returns undefined on any problem —
|
|
47
|
+
* a malformed settings file must not block a launch (pi-otel will surface its
|
|
48
|
+
* own complaint in-session).
|
|
49
|
+
*/
|
|
50
|
+
function projectOtelEndpoint(cwd, readFile) {
|
|
51
|
+
try {
|
|
52
|
+
const raw = readFile(join(cwd, ".pi", "settings.json"), "utf8");
|
|
53
|
+
const parsed = JSON.parse(String(raw));
|
|
54
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
55
|
+
return undefined;
|
|
56
|
+
const otel = parsed.otel;
|
|
57
|
+
if (typeof otel !== "object" || otel === null)
|
|
58
|
+
return undefined;
|
|
59
|
+
const endpoint = otel.endpoint;
|
|
60
|
+
return typeof endpoint === "string" && endpoint.trim() !== "" ? endpoint.trim() : undefined;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Truthy per pi-otel's own convention for PI_OTEL_DISABLED. */
|
|
67
|
+
function envDisabled(env) {
|
|
68
|
+
return env.PI_OTEL_DISABLED === "1" || env.PI_OTEL_DISABLED === "true";
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Decide whether this launch exports OTel traces, and with which pi-otel entry.
|
|
72
|
+
* Returns undefined when export stays off: no endpoint configured anywhere,
|
|
73
|
+
* `PI_OTEL_DISABLED` set, or the pi-otel package unresolvable (never fatal).
|
|
74
|
+
*/
|
|
75
|
+
export function resolveOtelLaunch(opts) {
|
|
76
|
+
const { env, cwd } = opts;
|
|
77
|
+
if (envDisabled(env))
|
|
78
|
+
return undefined;
|
|
79
|
+
const envEndpoint = env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim();
|
|
80
|
+
const settingsEndpoint = envEndpoint
|
|
81
|
+
? undefined
|
|
82
|
+
: projectOtelEndpoint(cwd, opts.readFile ?? readFileSync);
|
|
83
|
+
if (!envEndpoint && !settingsEndpoint)
|
|
84
|
+
return undefined;
|
|
85
|
+
let extensionPath;
|
|
86
|
+
try {
|
|
87
|
+
extensionPath = (opts.resolveExtension ?? resolveOtelExtensionPath)();
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
return envEndpoint
|
|
93
|
+
? { extensionPath, source: "env", endpoint: envEndpoint }
|
|
94
|
+
: { extensionPath, source: "project-settings", endpoint: settingsEndpoint };
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Absolute path to pi-otel's extension entry (its package main IS the pi
|
|
98
|
+
* extension entry, `dist/index.js`). Resolved ESM-native like `paths.ts` does
|
|
99
|
+
* for pi itself. Throws when the package is missing — callers treat that as
|
|
100
|
+
* "export off", not an error.
|
|
101
|
+
*/
|
|
102
|
+
export function resolveOtelExtensionPath() {
|
|
103
|
+
const path = fileURLToPath(import.meta.resolve("pi-otel"));
|
|
104
|
+
if (!existsSync(path)) {
|
|
105
|
+
throw new Error(`pi-otel entry not found at ${path}`);
|
|
106
|
+
}
|
|
107
|
+
return path;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The env keys an OTel-exporting child must carry. `PI_OTEL_CAPTURE_CONTENT`
|
|
111
|
+
* is pinned unconditionally (policy 2 above); the service name only fills in
|
|
112
|
+
* when the user has not chosen their own.
|
|
113
|
+
*
|
|
114
|
+
* A "workspace"-sourced config additionally delivers the admin-set endpoint,
|
|
115
|
+
* protocol, and collector headers over the standard OTel env vars — but any
|
|
116
|
+
* of those the USER already set in their own environment wins (local env >
|
|
117
|
+
* workspace config), so an engineer can point one session at a scratch
|
|
118
|
+
* collector without an admin change.
|
|
119
|
+
*/
|
|
120
|
+
export function otelChildEnv(config, baseEnv) {
|
|
121
|
+
const workspace = {};
|
|
122
|
+
if (config.source === "workspace") {
|
|
123
|
+
if (!baseEnv.OTEL_EXPORTER_OTLP_ENDPOINT) {
|
|
124
|
+
workspace.OTEL_EXPORTER_OTLP_ENDPOINT = config.endpoint;
|
|
125
|
+
}
|
|
126
|
+
if (config.protocol && !baseEnv.OTEL_EXPORTER_OTLP_PROTOCOL) {
|
|
127
|
+
workspace.OTEL_EXPORTER_OTLP_PROTOCOL = config.protocol;
|
|
128
|
+
}
|
|
129
|
+
if (config.headers && Object.keys(config.headers).length > 0 && !baseEnv.OTEL_EXPORTER_OTLP_HEADERS) {
|
|
130
|
+
workspace.OTEL_EXPORTER_OTLP_HEADERS = Object.entries(config.headers)
|
|
131
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
132
|
+
.join(",");
|
|
133
|
+
}
|
|
134
|
+
if (config.serviceName && !baseEnv.OTEL_SERVICE_NAME) {
|
|
135
|
+
workspace.OTEL_SERVICE_NAME = config.serviceName;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
...workspace,
|
|
140
|
+
[OTEL_EXTENSION_PATH_ENV]: config.extensionPath,
|
|
141
|
+
PI_OTEL_CAPTURE_CONTENT: "metadata_only",
|
|
142
|
+
...(baseEnv.OTEL_SERVICE_NAME || workspace.OTEL_SERVICE_NAME
|
|
143
|
+
? {}
|
|
144
|
+
: { OTEL_SERVICE_NAME: DEFAULT_OTEL_SERVICE_NAME }),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
// ── Workspace-configured export (admin sets it once in the web app) ─────────
|
|
148
|
+
/** How long the launcher waits on the config fetch before falling back to the
|
|
149
|
+
* on-disk cache. Launch latency is user-facing; telemetry config is not worth
|
|
150
|
+
* more than this. */
|
|
151
|
+
const WORKSPACE_FETCH_TIMEOUT_MS = 1_500;
|
|
152
|
+
function workspaceCachePath(profileName, dir) {
|
|
153
|
+
// Profile names are already path-safe (they name profile JSON files).
|
|
154
|
+
return join(dir, "otel", `${profileName}.json`);
|
|
155
|
+
}
|
|
156
|
+
function parseWorkspaceOtel(raw) {
|
|
157
|
+
if (typeof raw !== "object" || raw === null)
|
|
158
|
+
return null;
|
|
159
|
+
const o = raw;
|
|
160
|
+
if (o.enabled !== true || typeof o.endpoint !== "string" || o.endpoint.trim() === "") {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
const headers = {};
|
|
164
|
+
if (typeof o.headers === "object" && o.headers !== null) {
|
|
165
|
+
for (const [k, v] of Object.entries(o.headers)) {
|
|
166
|
+
if (typeof v === "string")
|
|
167
|
+
headers[k] = v;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
enabled: true,
|
|
172
|
+
endpoint: o.endpoint.trim(),
|
|
173
|
+
...(typeof o.protocol === "string" ? { protocol: o.protocol } : {}),
|
|
174
|
+
...(typeof o.serviceName === "string" && o.serviceName ? { serviceName: o.serviceName } : {}),
|
|
175
|
+
headers,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Resolve the workspace's admin-set OTel config: one fail-soft GET of the
|
|
180
|
+
* catalog endpoint (which carries the additive `otel` block), cached on disk
|
|
181
|
+
* per profile so a slow or offline backend degrades to last-known config
|
|
182
|
+
* instead of a launch stall.
|
|
183
|
+
*
|
|
184
|
+
* Cache semantics matter for revocation: a SUCCESSFUL response without an
|
|
185
|
+
* otel block means the admin disabled or removed the config, so the cache is
|
|
186
|
+
* DELETED — only a network/HTTP failure falls back to it. Otherwise turning
|
|
187
|
+
* export off in the web app would leave every laptop exporting (with a stale
|
|
188
|
+
* collector key) until the cache happened to be overwritten.
|
|
189
|
+
*
|
|
190
|
+
* The cache lives under the credentials dir at mode 0600 — the same posture
|
|
191
|
+
* as the profile token files, which is the right comparison: the cached
|
|
192
|
+
* headers carry the collector key, the profile carries the YAGNI token.
|
|
193
|
+
*/
|
|
194
|
+
export async function fetchWorkspaceOtel(creds, profileName, deps = {}) {
|
|
195
|
+
const cacheDir = deps.cacheDir ?? credentialsDir();
|
|
196
|
+
const cachePath = workspaceCachePath(profileName, cacheDir);
|
|
197
|
+
const doFetch = deps.fetchImpl ?? fetch;
|
|
198
|
+
let body;
|
|
199
|
+
try {
|
|
200
|
+
const res = await doFetch(`${creds.baseUrl}/api/yagni-code/models`, {
|
|
201
|
+
method: "GET",
|
|
202
|
+
headers: { authorization: `Bearer ${creds.token}` },
|
|
203
|
+
signal: AbortSignal.timeout(deps.timeoutMs ?? WORKSPACE_FETCH_TIMEOUT_MS),
|
|
204
|
+
});
|
|
205
|
+
if (!res.ok)
|
|
206
|
+
throw new Error(`HTTP ${res.status}`);
|
|
207
|
+
body = await res.json();
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
// Network/HTTP failure → last-known config (or nothing). Fail-soft by
|
|
211
|
+
// design: the fallback is visible via `yagni doctor`, not a launch error.
|
|
212
|
+
return readWorkspaceOtelCache(cachePath);
|
|
213
|
+
}
|
|
214
|
+
const config = parseWorkspaceOtel(body?.otel);
|
|
215
|
+
try {
|
|
216
|
+
if (config) {
|
|
217
|
+
mkdirSync(join(cacheDir, "otel"), { recursive: true, mode: 0o700 });
|
|
218
|
+
writeFileSync(cachePath, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
rmSync(cachePath, { force: true });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
// Cache maintenance is best-effort; the fresh result still applies.
|
|
226
|
+
}
|
|
227
|
+
return config;
|
|
228
|
+
}
|
|
229
|
+
function readWorkspaceOtelCache(cachePath) {
|
|
230
|
+
try {
|
|
231
|
+
return parseWorkspaceOtel(JSON.parse(readFileSync(cachePath, "utf8")));
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Full launch-time resolution, all three sources in precedence order:
|
|
239
|
+
*
|
|
240
|
+
* 1. `PI_OTEL_DISABLED` — personal kill switch, beats everything.
|
|
241
|
+
* 2. env `OTEL_EXPORTER_OTLP_ENDPOINT` — the user's own setup, untouched.
|
|
242
|
+
* 3. workspace config — admin-set in the web app, fetched/cached.
|
|
243
|
+
* 4. repo `.pi/settings.json` — committed per-repo config.
|
|
244
|
+
*
|
|
245
|
+
* `creds` absent (not logged in — doctor on a fresh machine) skips source 3.
|
|
246
|
+
*/
|
|
247
|
+
export async function resolveOtelLaunchWithWorkspace(opts) {
|
|
248
|
+
const { env, cwd } = opts;
|
|
249
|
+
if (envDisabled(env))
|
|
250
|
+
return undefined;
|
|
251
|
+
// Source 2: the user's own env config short-circuits — no fetch needed.
|
|
252
|
+
if (env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim()) {
|
|
253
|
+
return resolveOtelLaunch({
|
|
254
|
+
env,
|
|
255
|
+
cwd,
|
|
256
|
+
...(opts.resolveExtension ? { resolveExtension: opts.resolveExtension } : {}),
|
|
257
|
+
...(opts.readFile ? { readFile: opts.readFile } : {}),
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
// Source 3: workspace config.
|
|
261
|
+
if (opts.creds?.token && opts.profileName) {
|
|
262
|
+
const workspace = await (opts.fetchWorkspace ?? fetchWorkspaceOtel)({ baseUrl: opts.creds.baseUrl, token: opts.creds.token }, opts.profileName, opts.fetchDeps ?? {});
|
|
263
|
+
if (workspace) {
|
|
264
|
+
let extensionPath;
|
|
265
|
+
try {
|
|
266
|
+
extensionPath = (opts.resolveExtension ?? resolveOtelExtensionPath)();
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
extensionPath,
|
|
273
|
+
source: "workspace",
|
|
274
|
+
endpoint: workspace.endpoint,
|
|
275
|
+
...(workspace.protocol ? { protocol: workspace.protocol } : {}),
|
|
276
|
+
...(workspace.serviceName ? { serviceName: workspace.serviceName } : {}),
|
|
277
|
+
...(workspace.headers && Object.keys(workspace.headers).length > 0
|
|
278
|
+
? { headers: workspace.headers }
|
|
279
|
+
: {}),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// Source 4: repo-committed settings.
|
|
284
|
+
return resolveOtelLaunch({
|
|
285
|
+
env,
|
|
286
|
+
cwd,
|
|
287
|
+
...(opts.resolveExtension ? { resolveExtension: opts.resolveExtension } : {}),
|
|
288
|
+
...(opts.readFile ? { readFile: opts.readFile } : {}),
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
//# sourceMappingURL=otel.js.map
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `--output-format` support for headless/print mode (YAG-593).
|
|
3
|
+
*
|
|
4
|
+
* The launcher parses `--output-format` out of argv (pi doesn't know about it),
|
|
5
|
+
* maps it to pi's `--mode json` internally, and post-processes pi's NDJSON
|
|
6
|
+
* event stream into a single JSON result object — mirroring Claude Code's
|
|
7
|
+
* `--output-format json` shape.
|
|
8
|
+
*/
|
|
9
|
+
export type OutputFormat = "text" | "json" | "stream-json";
|
|
10
|
+
/**
|
|
11
|
+
* Strip `--output-format <value>` (or `--output-format=<value>`) from argv.
|
|
12
|
+
* Returns the format and the remaining args (with the flag removed).
|
|
13
|
+
* Unknown values fall back to "text" with a stderr warning.
|
|
14
|
+
*/
|
|
15
|
+
export declare function parseOutputFormat(argv: string[], writeErr?: (line: string) => void): {
|
|
16
|
+
format: OutputFormat;
|
|
17
|
+
remainingArgs: string[];
|
|
18
|
+
};
|
|
19
|
+
export interface ToolUsed {
|
|
20
|
+
name: string;
|
|
21
|
+
is_error: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface GuardianReview {
|
|
24
|
+
outcome: string;
|
|
25
|
+
duration_ms?: number;
|
|
26
|
+
tier?: string;
|
|
27
|
+
/** True when the command was blocked (ask in headless or deny). */
|
|
28
|
+
blocked?: boolean;
|
|
29
|
+
}
|
|
30
|
+
export interface ResultObject {
|
|
31
|
+
type: "result";
|
|
32
|
+
subtype: "success" | "error_during_execution";
|
|
33
|
+
result: string;
|
|
34
|
+
is_error: boolean;
|
|
35
|
+
duration_ms: number;
|
|
36
|
+
num_turns: number;
|
|
37
|
+
session_id: string;
|
|
38
|
+
total_cost_usd: number;
|
|
39
|
+
usage: {
|
|
40
|
+
input_tokens: number;
|
|
41
|
+
output_tokens: number;
|
|
42
|
+
cache_read_tokens: number;
|
|
43
|
+
total_tokens: number;
|
|
44
|
+
};
|
|
45
|
+
tools_used: ToolUsed[];
|
|
46
|
+
guardian_reviews: GuardianReview[];
|
|
47
|
+
stop_reason: string | null;
|
|
48
|
+
}
|
|
49
|
+
interface ParsedEvent {
|
|
50
|
+
type: string;
|
|
51
|
+
[key: string]: unknown;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Parse raw NDJSON text (pi's --mode json stdout) into an array of event objects.
|
|
55
|
+
* Skips blank lines. Fail-soft: unparseable lines are skipped.
|
|
56
|
+
*/
|
|
57
|
+
export declare function parseJsonEvents(raw: string): ParsedEvent[];
|
|
58
|
+
/**
|
|
59
|
+
* Build the single JSON result object from collected pi events and Guardian
|
|
60
|
+
* events. Pure — no I/O.
|
|
61
|
+
*
|
|
62
|
+
* @param events NDJSON event objects from pi's --mode json stdout
|
|
63
|
+
* @param guardian Guardian review entries from the error sink
|
|
64
|
+
* @param durationMs Wall-clock time measured by the launcher
|
|
65
|
+
* @param verbose When true, returns the raw events array instead of a single object
|
|
66
|
+
*/
|
|
67
|
+
export declare function buildResultObject(opts: {
|
|
68
|
+
events: ParsedEvent[];
|
|
69
|
+
guardianEvents?: GuardianReview[];
|
|
70
|
+
durationMs: number;
|
|
71
|
+
verbose?: boolean;
|
|
72
|
+
}): ResultObject | ParsedEvent[];
|
|
73
|
+
/**
|
|
74
|
+
* Read Guardian review events from the error sink JSONL, filtered by
|
|
75
|
+
* YAGNI_SESSION_ID. The error sink is written by the extension during the
|
|
76
|
+
* session; this reads it post-run. Fail-soft: returns [] on any error.
|
|
77
|
+
*/
|
|
78
|
+
export declare function readGuardianEvents(yagniSessionId: string, opts?: {
|
|
79
|
+
homeDir?: string;
|
|
80
|
+
now?: Date;
|
|
81
|
+
}): GuardianReview[];
|
|
82
|
+
export {};
|
|
83
|
+
//# sourceMappingURL=outputFormat.d.ts.map
|