patchwork-os 0.2.0-beta.4 → 0.2.0-beta.5.canary.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/dist/analyticsConfig.d.ts +29 -0
- package/dist/analyticsConfig.js +89 -0
- package/dist/analyticsConfig.js.map +1 -0
- package/dist/analyticsSend.d.ts +17 -1
- package/dist/analyticsSend.js +63 -5
- package/dist/analyticsSend.js.map +1 -1
- package/dist/commands/analytics.d.ts +8 -0
- package/dist/commands/analytics.js +134 -0
- package/dist/commands/analytics.js.map +1 -0
- package/dist/index.js +85 -40
- package/dist/index.js.map +1 -1
- package/dist/recipeOrchestration.js +58 -8
- package/dist/recipeOrchestration.js.map +1 -1
- package/dist/recipeRoutes.js +95 -14
- package/dist/recipeRoutes.js.map +1 -1
- package/dist/server.js +36 -18
- package/dist/server.js.map +1 -1
- package/package.json +2 -1
- package/scripts/start-all.ps1 +209 -209
- package/scripts/start-orchestrator.ps1 +158 -158
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* On-disk config for the opt-in telemetry collector (endpoint + shared secret).
|
|
3
|
+
*
|
|
4
|
+
* Lives at ~/.claude/ide/analytics-config.json (respects CLAUDE_CONFIG_DIR).
|
|
5
|
+
* Mode 0600. Atomic writes via temp + rename.
|
|
6
|
+
*
|
|
7
|
+
* Resolution order in analyticsSend.ts:
|
|
8
|
+
* 1. process.env.PATCHWORK_ANALYTICS_ENDPOINT / _KEY (highest)
|
|
9
|
+
* 2. this file
|
|
10
|
+
* 3. upstream default (no key)
|
|
11
|
+
*
|
|
12
|
+
* Reason for the file: keeps the shared secret out of launchd plists
|
|
13
|
+
* (which are Time-Machine backed, iCloud synced, and survive reinstalls
|
|
14
|
+
* in an inconsistent way) and gives operators a single source of truth
|
|
15
|
+
* managed by the `patchwork analytics configure` CLI.
|
|
16
|
+
*/
|
|
17
|
+
export interface AnalyticsConfig {
|
|
18
|
+
endpoint?: string;
|
|
19
|
+
key?: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function configPath(): string;
|
|
22
|
+
/** Read the config file. Returns empty object on missing/invalid file. */
|
|
23
|
+
export declare function getAnalyticsConfig(): AnalyticsConfig;
|
|
24
|
+
/**
|
|
25
|
+
* Atomic write with mode 0600. Merges with existing config — pass
|
|
26
|
+
* `{ endpoint: undefined }` to clear a field explicitly.
|
|
27
|
+
*/
|
|
28
|
+
export declare function setAnalyticsConfig(update: AnalyticsConfig): void;
|
|
29
|
+
export declare function clearAnalyticsConfig(): void;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* On-disk config for the opt-in telemetry collector (endpoint + shared secret).
|
|
3
|
+
*
|
|
4
|
+
* Lives at ~/.claude/ide/analytics-config.json (respects CLAUDE_CONFIG_DIR).
|
|
5
|
+
* Mode 0600. Atomic writes via temp + rename.
|
|
6
|
+
*
|
|
7
|
+
* Resolution order in analyticsSend.ts:
|
|
8
|
+
* 1. process.env.PATCHWORK_ANALYTICS_ENDPOINT / _KEY (highest)
|
|
9
|
+
* 2. this file
|
|
10
|
+
* 3. upstream default (no key)
|
|
11
|
+
*
|
|
12
|
+
* Reason for the file: keeps the shared secret out of launchd plists
|
|
13
|
+
* (which are Time-Machine backed, iCloud synced, and survive reinstalls
|
|
14
|
+
* in an inconsistent way) and gives operators a single source of truth
|
|
15
|
+
* managed by the `patchwork analytics configure` CLI.
|
|
16
|
+
*/
|
|
17
|
+
import fs from "node:fs";
|
|
18
|
+
import os from "node:os";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
export function configPath() {
|
|
21
|
+
const claudeDir = process.env.CLAUDE_CONFIG_DIR ?? path.join(os.homedir(), ".claude");
|
|
22
|
+
return path.join(claudeDir, "ide", "analytics-config.json");
|
|
23
|
+
}
|
|
24
|
+
function isValidEndpoint(value) {
|
|
25
|
+
if (typeof value !== "string" || value.length === 0)
|
|
26
|
+
return false;
|
|
27
|
+
try {
|
|
28
|
+
const u = new URL(value);
|
|
29
|
+
return u.protocol === "http:" || u.protocol === "https:";
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Read the config file. Returns empty object on missing/invalid file. */
|
|
36
|
+
export function getAnalyticsConfig() {
|
|
37
|
+
try {
|
|
38
|
+
const raw = fs.readFileSync(configPath(), "utf-8");
|
|
39
|
+
const obj = JSON.parse(raw);
|
|
40
|
+
if (typeof obj !== "object" || obj === null)
|
|
41
|
+
return {};
|
|
42
|
+
const rec = obj;
|
|
43
|
+
const out = {};
|
|
44
|
+
if (isValidEndpoint(rec.endpoint))
|
|
45
|
+
out.endpoint = rec.endpoint;
|
|
46
|
+
if (typeof rec.key === "string" && rec.key.length > 0)
|
|
47
|
+
out.key = rec.key;
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Atomic write with mode 0600. Merges with existing config — pass
|
|
56
|
+
* `{ endpoint: undefined }` to clear a field explicitly.
|
|
57
|
+
*/
|
|
58
|
+
export function setAnalyticsConfig(update) {
|
|
59
|
+
const current = getAnalyticsConfig();
|
|
60
|
+
const next = { ...current };
|
|
61
|
+
if ("endpoint" in update) {
|
|
62
|
+
if (update.endpoint === undefined)
|
|
63
|
+
delete next.endpoint;
|
|
64
|
+
else if (isValidEndpoint(update.endpoint))
|
|
65
|
+
next.endpoint = update.endpoint;
|
|
66
|
+
else
|
|
67
|
+
throw new Error(`invalid endpoint (must be http(s) URL): ${update.endpoint}`);
|
|
68
|
+
}
|
|
69
|
+
if ("key" in update) {
|
|
70
|
+
if (update.key === undefined)
|
|
71
|
+
delete next.key;
|
|
72
|
+
else
|
|
73
|
+
next.key = update.key;
|
|
74
|
+
}
|
|
75
|
+
const p = configPath();
|
|
76
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
77
|
+
const tmp = `${p}.tmp.${process.pid}`;
|
|
78
|
+
fs.writeFileSync(tmp, JSON.stringify(next, null, 2) + "\n", { mode: 0o600 });
|
|
79
|
+
fs.renameSync(tmp, p);
|
|
80
|
+
}
|
|
81
|
+
export function clearAnalyticsConfig() {
|
|
82
|
+
try {
|
|
83
|
+
fs.unlinkSync(configPath());
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
/* file may not exist */
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=analyticsConfig.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyticsConfig.js","sourceRoot":"","sources":["../src/analyticsConfig.ts"],"names":[],"mappings":"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"}
|
package/dist/analyticsSend.d.ts
CHANGED
|
@@ -2,9 +2,25 @@
|
|
|
2
2
|
* Sends an anonymized analytics summary to the usage endpoint.
|
|
3
3
|
* - Fire-and-forget is NOT used: callers must await this with a timeout race.
|
|
4
4
|
* - All errors are caught and swallowed — telemetry must never affect bridge operation.
|
|
5
|
-
* - Endpoint
|
|
5
|
+
* - Endpoint defaults to the upstream collector. Operators may override via:
|
|
6
|
+
* 1. env: PATCHWORK_ANALYTICS_ENDPOINT (highest precedence; for CI/headless)
|
|
7
|
+
* 2. config file: ~/.claude/ide/analytics-config.json (preferred — managed by
|
|
8
|
+
* `patchwork analytics configure`, keeps secrets out of launchd plists)
|
|
9
|
+
* 3. default upstream collector
|
|
10
|
+
* Endpoint is resolved per-call (cheap fs read) so live config changes take
|
|
11
|
+
* effect on the next send without restart. Invalid values fall back through.
|
|
12
|
+
* Never read from the network or summary payload — preserves the
|
|
13
|
+
* redirect-attack property the original hardcoded constant was protecting.
|
|
6
14
|
*/
|
|
7
15
|
import type { AnalyticsSummary } from "./analyticsAggregator.js";
|
|
16
|
+
export declare function resolveAnalyticsTarget(): {
|
|
17
|
+
endpoint: string;
|
|
18
|
+
key: string | undefined;
|
|
19
|
+
source: {
|
|
20
|
+
endpoint: "env" | "config" | "default";
|
|
21
|
+
key: "env" | "config" | "none";
|
|
22
|
+
};
|
|
23
|
+
};
|
|
8
24
|
/**
|
|
9
25
|
* Sends the summary to the analytics endpoint.
|
|
10
26
|
* Resolves (never rejects) — all errors are swallowed silently.
|
package/dist/analyticsSend.js
CHANGED
|
@@ -2,11 +2,63 @@
|
|
|
2
2
|
* Sends an anonymized analytics summary to the usage endpoint.
|
|
3
3
|
* - Fire-and-forget is NOT used: callers must await this with a timeout race.
|
|
4
4
|
* - All errors are caught and swallowed — telemetry must never affect bridge operation.
|
|
5
|
-
* - Endpoint
|
|
5
|
+
* - Endpoint defaults to the upstream collector. Operators may override via:
|
|
6
|
+
* 1. env: PATCHWORK_ANALYTICS_ENDPOINT (highest precedence; for CI/headless)
|
|
7
|
+
* 2. config file: ~/.claude/ide/analytics-config.json (preferred — managed by
|
|
8
|
+
* `patchwork analytics configure`, keeps secrets out of launchd plists)
|
|
9
|
+
* 3. default upstream collector
|
|
10
|
+
* Endpoint is resolved per-call (cheap fs read) so live config changes take
|
|
11
|
+
* effect on the next send without restart. Invalid values fall back through.
|
|
12
|
+
* Never read from the network or summary payload — preserves the
|
|
13
|
+
* redirect-attack property the original hardcoded constant was protecting.
|
|
6
14
|
*/
|
|
15
|
+
import { getAnalyticsConfig } from "./analyticsConfig.js";
|
|
7
16
|
import { recordAnalyticsSent } from "./analyticsPrefs.js";
|
|
8
|
-
|
|
9
|
-
|
|
17
|
+
const DEFAULT_ENDPOINT = "https://analytics.claude-ide-bridge.dev/v1/usage";
|
|
18
|
+
function validUrl(value) {
|
|
19
|
+
if (!value)
|
|
20
|
+
return undefined;
|
|
21
|
+
try {
|
|
22
|
+
const u = new URL(value);
|
|
23
|
+
if (u.protocol !== "https:" && u.protocol !== "http:")
|
|
24
|
+
return undefined;
|
|
25
|
+
return u.toString();
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function resolveAnalyticsTarget() {
|
|
32
|
+
const envEndpoint = validUrl(process.env.PATCHWORK_ANALYTICS_ENDPOINT);
|
|
33
|
+
const envKey = process.env.PATCHWORK_ANALYTICS_KEY;
|
|
34
|
+
const cfg = getAnalyticsConfig();
|
|
35
|
+
const cfgEndpoint = validUrl(cfg.endpoint);
|
|
36
|
+
let endpoint = DEFAULT_ENDPOINT;
|
|
37
|
+
let endpointSource = "default";
|
|
38
|
+
if (envEndpoint) {
|
|
39
|
+
endpoint = envEndpoint;
|
|
40
|
+
endpointSource = "env";
|
|
41
|
+
}
|
|
42
|
+
else if (cfgEndpoint) {
|
|
43
|
+
endpoint = cfgEndpoint;
|
|
44
|
+
endpointSource = "config";
|
|
45
|
+
}
|
|
46
|
+
let key;
|
|
47
|
+
let keySource = "none";
|
|
48
|
+
if (envKey) {
|
|
49
|
+
key = envKey;
|
|
50
|
+
keySource = "env";
|
|
51
|
+
}
|
|
52
|
+
else if (cfg.key) {
|
|
53
|
+
key = cfg.key;
|
|
54
|
+
keySource = "config";
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
endpoint,
|
|
58
|
+
key,
|
|
59
|
+
source: { endpoint: endpointSource, key: keySource },
|
|
60
|
+
};
|
|
61
|
+
}
|
|
10
62
|
const SEND_TIMEOUT_MS = 3000;
|
|
11
63
|
/**
|
|
12
64
|
* Sends the summary to the analytics endpoint.
|
|
@@ -14,12 +66,18 @@ const SEND_TIMEOUT_MS = 3000;
|
|
|
14
66
|
*/
|
|
15
67
|
export async function sendAnalytics(summary) {
|
|
16
68
|
try {
|
|
69
|
+
const { endpoint, key } = resolveAnalyticsTarget();
|
|
17
70
|
const controller = new AbortController();
|
|
18
71
|
const timer = setTimeout(() => controller.abort(), SEND_TIMEOUT_MS);
|
|
19
72
|
try {
|
|
20
|
-
const
|
|
73
|
+
const headers = {
|
|
74
|
+
"Content-Type": "application/json",
|
|
75
|
+
};
|
|
76
|
+
if (key)
|
|
77
|
+
headers["X-Analytics-Key"] = key;
|
|
78
|
+
const res = await fetch(endpoint, {
|
|
21
79
|
method: "POST",
|
|
22
|
-
headers
|
|
80
|
+
headers,
|
|
23
81
|
body: JSON.stringify(summary),
|
|
24
82
|
signal: controller.signal,
|
|
25
83
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analyticsSend.js","sourceRoot":"","sources":["../src/analyticsSend.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"analyticsSend.js","sourceRoot":"","sources":["../src/analyticsSend.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `patchwork analytics` CLI — manage the self-hosted telemetry collector config.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the brittle pattern of putting endpoint + secret in a launchd plist.
|
|
5
|
+
* Config lives at ~/.claude/ide/analytics-config.json (mode 0600), read on every
|
|
6
|
+
* send. Env vars still win for headless / CI use.
|
|
7
|
+
*/
|
|
8
|
+
export declare function runAnalyticsCommand(argv: string[]): Promise<number>;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `patchwork analytics` CLI — manage the self-hosted telemetry collector config.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the brittle pattern of putting endpoint + secret in a launchd plist.
|
|
5
|
+
* Config lives at ~/.claude/ide/analytics-config.json (mode 0600), read on every
|
|
6
|
+
* send. Env vars still win for headless / CI use.
|
|
7
|
+
*/
|
|
8
|
+
import { clearAnalyticsConfig, configPath, getAnalyticsConfig, setAnalyticsConfig, } from "../analyticsConfig.js";
|
|
9
|
+
import { resolveAnalyticsTarget, sendAnalytics } from "../analyticsSend.js";
|
|
10
|
+
const USAGE = "Usage: patchwork analytics <subcommand>\n\n" +
|
|
11
|
+
" show Print active endpoint, key (masked), and source\n" +
|
|
12
|
+
" configure --endpoint URL [--key KEY] Write endpoint and/or key to the config file\n" +
|
|
13
|
+
" clear Remove the config file\n" +
|
|
14
|
+
" test Send a tiny synthetic payload and report HTTP status\n" +
|
|
15
|
+
"\n" +
|
|
16
|
+
"Resolution order: env (PATCHWORK_ANALYTICS_ENDPOINT/_KEY) > config file > upstream default.\n";
|
|
17
|
+
function maskKey(key) {
|
|
18
|
+
if (!key)
|
|
19
|
+
return "<unset>";
|
|
20
|
+
if (key.length <= 8)
|
|
21
|
+
return "***";
|
|
22
|
+
return `${key.slice(0, 4)}…${key.slice(-4)} (len=${key.length})`;
|
|
23
|
+
}
|
|
24
|
+
function parseFlags(args) {
|
|
25
|
+
const out = {};
|
|
26
|
+
for (let i = 0; i < args.length; i++) {
|
|
27
|
+
const a = args[i];
|
|
28
|
+
if (!a || !a.startsWith("--"))
|
|
29
|
+
continue;
|
|
30
|
+
const name = a.slice(2);
|
|
31
|
+
const next = args[i + 1];
|
|
32
|
+
if (next && !next.startsWith("--")) {
|
|
33
|
+
out[name] = next;
|
|
34
|
+
i++;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
out[name] = true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
export async function runAnalyticsCommand(argv) {
|
|
43
|
+
const sub = argv[0];
|
|
44
|
+
if (!sub || sub === "--help" || sub === "-h") {
|
|
45
|
+
process.stdout.write(USAGE);
|
|
46
|
+
return sub ? 0 : 1;
|
|
47
|
+
}
|
|
48
|
+
if (sub === "show") {
|
|
49
|
+
const target = resolveAnalyticsTarget();
|
|
50
|
+
const cfg = getAnalyticsConfig();
|
|
51
|
+
process.stdout.write(`Active endpoint: ${target.endpoint} (source: ${target.source.endpoint})\n` +
|
|
52
|
+
`Active key: ${maskKey(target.key)} (source: ${target.source.key})\n` +
|
|
53
|
+
`Config file: ${configPath()}\n` +
|
|
54
|
+
` endpoint: ${cfg.endpoint ?? "<unset>"}\n` +
|
|
55
|
+
` key: ${maskKey(cfg.key)}\n` +
|
|
56
|
+
`Env:\n` +
|
|
57
|
+
` PATCHWORK_ANALYTICS_ENDPOINT: ${process.env.PATCHWORK_ANALYTICS_ENDPOINT ?? "<unset>"}\n` +
|
|
58
|
+
` PATCHWORK_ANALYTICS_KEY: ${maskKey(process.env.PATCHWORK_ANALYTICS_KEY)}\n`);
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
if (sub === "configure") {
|
|
62
|
+
const flags = parseFlags(argv.slice(1));
|
|
63
|
+
const update = {};
|
|
64
|
+
if (typeof flags.endpoint === "string")
|
|
65
|
+
update.endpoint = flags.endpoint;
|
|
66
|
+
if (typeof flags.key === "string")
|
|
67
|
+
update.key = flags.key;
|
|
68
|
+
if (Object.keys(update).length === 0) {
|
|
69
|
+
process.stderr.write("configure requires at least --endpoint URL or --key KEY\n");
|
|
70
|
+
return 1;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
setAnalyticsConfig(update);
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
process.stderr.write(`configure failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
77
|
+
return 1;
|
|
78
|
+
}
|
|
79
|
+
process.stdout.write(`wrote ${configPath()}\n`);
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
if (sub === "clear") {
|
|
83
|
+
clearAnalyticsConfig();
|
|
84
|
+
process.stdout.write(`removed ${configPath()}\n`);
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
if (sub === "test") {
|
|
88
|
+
const target = resolveAnalyticsTarget();
|
|
89
|
+
process.stdout.write(`sending synthetic summary to ${target.endpoint} (key source: ${target.source.key})…\n`);
|
|
90
|
+
// Build minimal real-shaped summary
|
|
91
|
+
const summary = {
|
|
92
|
+
bridgeVersion: "cli-test",
|
|
93
|
+
sessionDurationMs: 1,
|
|
94
|
+
toolStats: [
|
|
95
|
+
{ tool: "getDiagnostics", calls: 1, errors: 0, p50Ms: 1, p95Ms: 1 },
|
|
96
|
+
],
|
|
97
|
+
};
|
|
98
|
+
// sendAnalytics swallows errors, so we shadow it with a direct fetch
|
|
99
|
+
// to surface the actual HTTP status to the operator.
|
|
100
|
+
try {
|
|
101
|
+
const controller = new AbortController();
|
|
102
|
+
const timer = setTimeout(() => controller.abort(), 5000);
|
|
103
|
+
const headers = {
|
|
104
|
+
"Content-Type": "application/json",
|
|
105
|
+
};
|
|
106
|
+
if (target.key)
|
|
107
|
+
headers["X-Analytics-Key"] = target.key;
|
|
108
|
+
const res = await fetch(target.endpoint, {
|
|
109
|
+
method: "POST",
|
|
110
|
+
headers,
|
|
111
|
+
body: JSON.stringify(summary),
|
|
112
|
+
signal: controller.signal,
|
|
113
|
+
});
|
|
114
|
+
clearTimeout(timer);
|
|
115
|
+
process.stdout.write(`HTTP ${res.status}\n`);
|
|
116
|
+
if (!res.ok) {
|
|
117
|
+
const body = await res.text().catch(() => "");
|
|
118
|
+
if (body)
|
|
119
|
+
process.stdout.write(`body: ${body.slice(0, 500)}\n`);
|
|
120
|
+
return 1;
|
|
121
|
+
}
|
|
122
|
+
// Also exercise the real send path so prefs.lastSentAt updates.
|
|
123
|
+
await sendAnalytics(summary);
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
process.stderr.write(`test failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
128
|
+
return 1;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
process.stderr.write(`unknown subcommand: ${sub}\n\n${USAGE}`);
|
|
132
|
+
return 1;
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=analytics.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analytics.js","sourceRoot":"","sources":["../../src/commands/analytics.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -134,6 +134,7 @@ const KNOWN_SUBCOMMANDS = [
|
|
|
134
134
|
"panic",
|
|
135
135
|
"halts",
|
|
136
136
|
"judgments",
|
|
137
|
+
"analytics",
|
|
137
138
|
];
|
|
138
139
|
const __invokedSubcommand = (() => {
|
|
139
140
|
const sub = process.argv[2];
|
|
@@ -193,8 +194,12 @@ if (process.argv[2] === "--help" ||
|
|
|
193
194
|
` recipe --help Full recipe subcommand index\n\n` +
|
|
194
195
|
`Diagnose\n` +
|
|
195
196
|
` halts [--window 1h|24h|overnight|7d] Morning summary of recent recipe halts\n` +
|
|
197
|
+
` judgments [--window ...] [--recipe N] Recent judge-step verdicts across runs\n` +
|
|
196
198
|
` traces export Bundle approval / recipe / decision traces\n` +
|
|
197
199
|
` print-token [--port N] Print the active bridge auth token\n\n` +
|
|
200
|
+
`Safety\n` +
|
|
201
|
+
` kill-switch <engage|release|status> Block / resume write-tier tools across bridges\n` +
|
|
202
|
+
` panic [--reason "..."] Shorthand for kill-switch engage\n\n` +
|
|
198
203
|
`Daemon (no subcommand)\n` +
|
|
199
204
|
` --workspace <dir> Start the bridge in foreground\n` +
|
|
200
205
|
` --watch Auto-restart supervisor\n` +
|
|
@@ -1763,11 +1768,22 @@ if (process.argv[2] === "kill-switch") {
|
|
|
1763
1768
|
// form is `kill-switch engage`; this alias matches it so shell history six
|
|
1764
1769
|
// months later still makes sense. Does not accept sub-verbs — just runs engage.
|
|
1765
1770
|
if (process.argv[2] === "panic") {
|
|
1771
|
+
const extra = process.argv.slice(3); // e.g. --reason "..." --force-local
|
|
1772
|
+
// Guard against `panic --help` engaging the kill switch — a real
|
|
1773
|
+
// footgun if you tab-completed the verb to confirm syntax before
|
|
1774
|
+
// committing to the action. `panic` is an alias, so we honor --help
|
|
1775
|
+
// here ourselves rather than forwarding to kill-switch engage.
|
|
1776
|
+
if (extra.includes("--help") || extra.includes("-h")) {
|
|
1777
|
+
console.log('Usage: patchwork panic [--reason "..."] [--force-local]\n\n' +
|
|
1778
|
+
" Alias for `patchwork kill-switch engage` — blocks all write-tier\n" +
|
|
1779
|
+
" tool calls across every running bridge. Use --reason to leave a\n" +
|
|
1780
|
+
" note in the audit trail. Release with `patchwork kill-switch release`.\n");
|
|
1781
|
+
process.exit(0);
|
|
1782
|
+
}
|
|
1766
1783
|
// Spawn self with kill-switch engage to reuse the full handler without
|
|
1767
1784
|
// duplicating 200+ LOC. Passes through any flags (--reason, --force-local).
|
|
1768
1785
|
import("node:child_process").then(({ spawnSync }) => {
|
|
1769
1786
|
const self = process.argv[1] ?? process.execPath;
|
|
1770
|
-
const extra = process.argv.slice(3); // e.g. --reason "..." --force-local
|
|
1771
1787
|
const result = spawnSync(process.execPath, [self, "kill-switch", "engage", ...extra], { stdio: "inherit" });
|
|
1772
1788
|
process.exit(result.status ?? 1);
|
|
1773
1789
|
});
|
|
@@ -1834,14 +1850,6 @@ if (process.argv[2] === "halts") {
|
|
|
1834
1850
|
process.stderr.write("No running bridge found. Start one with `patchwork start` (or `--driver subprocess`).\n");
|
|
1835
1851
|
process.exit(2);
|
|
1836
1852
|
}
|
|
1837
|
-
// Single-bridge default: query the first. Multi-bridge users will
|
|
1838
|
-
// typically have one orchestrator anyway; expanding to fan-out is a
|
|
1839
|
-
// follow-up if needed.
|
|
1840
|
-
const lock = liveLocks[0];
|
|
1841
|
-
if (!lock) {
|
|
1842
|
-
process.stderr.write("No running bridge found.\n");
|
|
1843
|
-
process.exit(2);
|
|
1844
|
-
}
|
|
1845
1853
|
const sinceMs = windowSinceMs(window);
|
|
1846
1854
|
const params = [];
|
|
1847
1855
|
if (sinceMs != null)
|
|
@@ -1849,20 +1857,35 @@ if (process.argv[2] === "halts") {
|
|
|
1849
1857
|
if (recipeFilter)
|
|
1850
1858
|
params.push(`recipe=${encodeURIComponent(recipeFilter)}`);
|
|
1851
1859
|
const qs = params.length > 0 ? `?${params.join("&")}` : "";
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1860
|
+
// Walk live bridges in order; first responsive one wins. See the
|
|
1861
|
+
// matching block in the `judgments` handler — findAllLiveBridges
|
|
1862
|
+
// can include stale entries when a recycled PID still answers
|
|
1863
|
+
// `kill(pid, 0)` but the lock points at a dead bridge.
|
|
1864
|
+
let res = null;
|
|
1865
|
+
let lastStatus = 0;
|
|
1866
|
+
for (const lock of liveLocks) {
|
|
1867
|
+
const controller = new AbortController();
|
|
1868
|
+
const timer = setTimeout(() => controller.abort(), 10_000);
|
|
1869
|
+
try {
|
|
1870
|
+
const candidate = await fetch(`http://127.0.0.1:${lock.port}/runs/halt-summary${qs}`, {
|
|
1871
|
+
headers: { Authorization: `Bearer ${lock.authToken}` },
|
|
1872
|
+
signal: controller.signal,
|
|
1873
|
+
});
|
|
1874
|
+
if (candidate.ok) {
|
|
1875
|
+
res = candidate;
|
|
1876
|
+
break;
|
|
1877
|
+
}
|
|
1878
|
+
lastStatus = candidate.status;
|
|
1879
|
+
}
|
|
1880
|
+
catch {
|
|
1881
|
+
/* unreachable lock — try next */
|
|
1882
|
+
}
|
|
1883
|
+
finally {
|
|
1884
|
+
clearTimeout(timer);
|
|
1885
|
+
}
|
|
1863
1886
|
}
|
|
1864
|
-
if (!res
|
|
1865
|
-
process.stderr.write(`
|
|
1887
|
+
if (!res) {
|
|
1888
|
+
process.stderr.write(`No live bridge served /runs/halt-summary (last status: ${lastStatus || "unreachable"}).\n`);
|
|
1866
1889
|
process.exit(1);
|
|
1867
1890
|
}
|
|
1868
1891
|
const summary = (await res.json());
|
|
@@ -1917,6 +1940,16 @@ if (process.argv[2] === "halts") {
|
|
|
1917
1940
|
}
|
|
1918
1941
|
})();
|
|
1919
1942
|
}
|
|
1943
|
+
// `patchwork analytics` — manage the self-hosted telemetry collector config.
|
|
1944
|
+
// Replaces the brittle "endpoint+secret in launchd plist" pattern with a
|
|
1945
|
+
// proper config file the bridge reads at startup.
|
|
1946
|
+
if (process.argv[2] === "analytics") {
|
|
1947
|
+
(async () => {
|
|
1948
|
+
const { runAnalyticsCommand } = await import("./commands/analytics.js");
|
|
1949
|
+
const code = await runAnalyticsCommand(process.argv.slice(3));
|
|
1950
|
+
process.exit(code);
|
|
1951
|
+
})();
|
|
1952
|
+
}
|
|
1920
1953
|
// `patchwork judgments` — PR3b sibling of `patchwork halts`. Same window
|
|
1921
1954
|
// + recipe filter shape; queries /runs/judge-summary and prints a
|
|
1922
1955
|
// per-verdict breakdown plus the 5 most-recent verdicts.
|
|
@@ -1974,11 +2007,6 @@ if (process.argv[2] === "judgments") {
|
|
|
1974
2007
|
process.stderr.write("No running bridge found. Start one with `patchwork start` (or `--driver subprocess`).\n");
|
|
1975
2008
|
process.exit(2);
|
|
1976
2009
|
}
|
|
1977
|
-
const lock = liveLocks[0];
|
|
1978
|
-
if (!lock) {
|
|
1979
|
-
process.stderr.write("No running bridge found.\n");
|
|
1980
|
-
process.exit(2);
|
|
1981
|
-
}
|
|
1982
2010
|
const sinceMs = windowSinceMs(window);
|
|
1983
2011
|
const params = [];
|
|
1984
2012
|
if (sinceMs != null)
|
|
@@ -1986,20 +2014,37 @@ if (process.argv[2] === "judgments") {
|
|
|
1986
2014
|
if (recipeFilter)
|
|
1987
2015
|
params.push(`recipe=${encodeURIComponent(recipeFilter)}`);
|
|
1988
2016
|
const qs = params.length > 0 ? `?${params.join("&")}` : "";
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2017
|
+
// Walk live bridges in order; the first responsive one wins.
|
|
2018
|
+
// findAllLiveBridges uses `kill(pid, 0)` for liveness, which
|
|
2019
|
+
// returns true for any recycled PID — so liveLocks can contain
|
|
2020
|
+
// stale entries from dead bridges. Previously we picked [0]
|
|
2021
|
+
// unconditionally and surfaced a confusing 404; now we try each
|
|
2022
|
+
// and only fall through to the error path when *all* fail.
|
|
2023
|
+
let res = null;
|
|
2024
|
+
let lastStatus = 0;
|
|
2025
|
+
for (const lock of liveLocks) {
|
|
2026
|
+
const controller = new AbortController();
|
|
2027
|
+
const timer = setTimeout(() => controller.abort(), 10_000);
|
|
2028
|
+
try {
|
|
2029
|
+
const candidate = await fetch(`http://127.0.0.1:${lock.port}/runs/judge-summary${qs}`, {
|
|
2030
|
+
headers: { Authorization: `Bearer ${lock.authToken}` },
|
|
2031
|
+
signal: controller.signal,
|
|
2032
|
+
});
|
|
2033
|
+
if (candidate.ok) {
|
|
2034
|
+
res = candidate;
|
|
2035
|
+
break;
|
|
2036
|
+
}
|
|
2037
|
+
lastStatus = candidate.status;
|
|
2038
|
+
}
|
|
2039
|
+
catch {
|
|
2040
|
+
/* unreachable lock — try next */
|
|
2041
|
+
}
|
|
2042
|
+
finally {
|
|
2043
|
+
clearTimeout(timer);
|
|
2044
|
+
}
|
|
2000
2045
|
}
|
|
2001
|
-
if (!res
|
|
2002
|
-
process.stderr.write(`
|
|
2046
|
+
if (!res) {
|
|
2047
|
+
process.stderr.write(`No live bridge served /runs/judge-summary (last status: ${lastStatus || "unreachable"}).\n`);
|
|
2003
2048
|
process.exit(1);
|
|
2004
2049
|
}
|
|
2005
2050
|
const summary = (await res.json());
|