@jpcasabiancai/cayenne 0.5.6 → 0.5.8
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/cli.js +866 -421
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -31,7 +31,7 @@ var init_cloud = __esm({
|
|
|
31
31
|
CLOUD_SUPABASE_ANON_KEY = assertPublishable(
|
|
32
32
|
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im5kcXJrcXh3a3p4Z25oa3ZjdW5yIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODg3MTMyNzYsImV4cCI6MjEwNDI4OTI3Nn0.Dlsxw91E1QYZ4eoRDjP80Rf4feY77Z-T-fiIwUDY2k0"
|
|
33
33
|
);
|
|
34
|
-
CLOUD_APP_ORIGIN = "https://cayenne
|
|
34
|
+
CLOUD_APP_ORIGIN = "https://app.cayenne.sh";
|
|
35
35
|
}
|
|
36
36
|
});
|
|
37
37
|
|
|
@@ -1325,7 +1325,7 @@ var init_local = __esm({
|
|
|
1325
1325
|
|
|
1326
1326
|
// ../../packages/shared/src/protocol/runner.ts
|
|
1327
1327
|
import { z as z14 } from "zod";
|
|
1328
|
-
var ProviderModel, ProviderCapability, UNAVAILABLE, RunnerCapabilities, CODEX_TOOL_NAMES;
|
|
1328
|
+
var ProviderModel, ProviderAuthMode, ProviderCapability, UNAVAILABLE, RunnerCapabilities, CODEX_TOOL_NAMES;
|
|
1329
1329
|
var init_runner = __esm({
|
|
1330
1330
|
"../../packages/shared/src/protocol/runner.ts"() {
|
|
1331
1331
|
"use strict";
|
|
@@ -1338,13 +1338,21 @@ var init_runner = __esm({
|
|
|
1338
1338
|
/** From the app-server's `supportedReasoningEfforts`; empty means the control is hidden. */
|
|
1339
1339
|
efforts: z14.array(Effort).default([]),
|
|
1340
1340
|
/** The effort a new chat on this model opens at, sent back on `session.start`. */
|
|
1341
|
-
defaultEffort: Effort.optional()
|
|
1341
|
+
defaultEffort: Effort.optional(),
|
|
1342
|
+
/** One line about the model in the provider's own words (Claude's `ModelInfo.description`). */
|
|
1343
|
+
description: z14.string().min(1).max(200).optional()
|
|
1342
1344
|
});
|
|
1345
|
+
ProviderAuthMode = z14.enum(["subscription", "api", "third_party"]);
|
|
1343
1346
|
ProviderCapability = z14.object({
|
|
1344
1347
|
available: z14.boolean(),
|
|
1345
1348
|
reason: z14.enum(["not_installed", "not_logged_in", "error"]).optional(),
|
|
1346
1349
|
version: z14.string().max(50).optional(),
|
|
1347
|
-
models: z14.array(ProviderModel).max(50).default([])
|
|
1350
|
+
models: z14.array(ProviderModel).max(50).default([]),
|
|
1351
|
+
/**
|
|
1352
|
+
* How the provider is signed in, when the runner can tell without opening a chat (Claude's
|
|
1353
|
+
* `accountInfo`, protocol §9). Absent means unknown. Optional so an older row still parses.
|
|
1354
|
+
*/
|
|
1355
|
+
authMode: ProviderAuthMode.optional()
|
|
1348
1356
|
});
|
|
1349
1357
|
UNAVAILABLE = { available: false, reason: "not_installed", models: [] };
|
|
1350
1358
|
RunnerCapabilities = z14.object({
|
|
@@ -2624,13 +2632,200 @@ var init_version = __esm({
|
|
|
2624
2632
|
}
|
|
2625
2633
|
});
|
|
2626
2634
|
|
|
2635
|
+
// src/providers/env.ts
|
|
2636
|
+
function definedEnv(env) {
|
|
2637
|
+
const out = {};
|
|
2638
|
+
for (const [key, value] of Object.entries(env)) if (value !== void 0) out[key] = value;
|
|
2639
|
+
return out;
|
|
2640
|
+
}
|
|
2641
|
+
var init_env = __esm({
|
|
2642
|
+
"src/providers/env.ts"() {
|
|
2643
|
+
"use strict";
|
|
2644
|
+
}
|
|
2645
|
+
});
|
|
2646
|
+
|
|
2647
|
+
// src/claude/auth-mode.ts
|
|
2648
|
+
function authModeFrom(info) {
|
|
2649
|
+
if (info.apiProvider && info.apiProvider !== "firstParty") return "third_party";
|
|
2650
|
+
if (info.apiKeySource && info.apiKeySource !== "none") return "api";
|
|
2651
|
+
if (info.subscriptionType) return "subscription";
|
|
2652
|
+
return null;
|
|
2653
|
+
}
|
|
2654
|
+
var init_auth_mode = __esm({
|
|
2655
|
+
"src/claude/auth-mode.ts"() {
|
|
2656
|
+
"use strict";
|
|
2657
|
+
}
|
|
2658
|
+
});
|
|
2659
|
+
|
|
2660
|
+
// src/claude/pushable.ts
|
|
2661
|
+
var Pushable;
|
|
2662
|
+
var init_pushable = __esm({
|
|
2663
|
+
"src/claude/pushable.ts"() {
|
|
2664
|
+
"use strict";
|
|
2665
|
+
Pushable = class {
|
|
2666
|
+
queue = [];
|
|
2667
|
+
waiters = [];
|
|
2668
|
+
ended = false;
|
|
2669
|
+
push(value) {
|
|
2670
|
+
if (this.ended) throw new Error("Pushable already ended");
|
|
2671
|
+
const waiter = this.waiters.shift();
|
|
2672
|
+
if (waiter) waiter({ value, done: false });
|
|
2673
|
+
else this.queue.push(value);
|
|
2674
|
+
}
|
|
2675
|
+
end() {
|
|
2676
|
+
this.ended = true;
|
|
2677
|
+
for (const waiter of this.waiters.splice(0)) waiter({ value: void 0, done: true });
|
|
2678
|
+
}
|
|
2679
|
+
[Symbol.asyncIterator]() {
|
|
2680
|
+
return {
|
|
2681
|
+
next: () => {
|
|
2682
|
+
const value = this.queue.shift();
|
|
2683
|
+
if (value !== void 0) return Promise.resolve({ value, done: false });
|
|
2684
|
+
if (this.ended) return Promise.resolve({ value: void 0, done: true });
|
|
2685
|
+
return new Promise((resolve6) => this.waiters.push(resolve6));
|
|
2686
|
+
}
|
|
2687
|
+
};
|
|
2688
|
+
}
|
|
2689
|
+
};
|
|
2690
|
+
}
|
|
2691
|
+
});
|
|
2692
|
+
|
|
2693
|
+
// src/claude/detect.ts
|
|
2694
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
2695
|
+
import { createRequire as createRequire2 } from "module";
|
|
2696
|
+
import { dirname as dirname3, join as join8 } from "path";
|
|
2697
|
+
import {
|
|
2698
|
+
query as sdkQuery
|
|
2699
|
+
} from "@anthropic-ai/claude-agent-sdk";
|
|
2700
|
+
function claudeCodeVersion(resolve6 = createRequire2(import.meta.url).resolve) {
|
|
2701
|
+
try {
|
|
2702
|
+
const entry = resolve6("@anthropic-ai/claude-agent-sdk");
|
|
2703
|
+
const pkg = JSON.parse(readFileSync4(join8(dirname3(entry), "package.json"), "utf8"));
|
|
2704
|
+
return typeof pkg.claudeCodeVersion === "string" && pkg.claudeCodeVersion ? pkg.claudeCodeVersion.slice(0, MAX_VERSION) : void 0;
|
|
2705
|
+
} catch {
|
|
2706
|
+
return void 0;
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
async function probeClaude(opts) {
|
|
2710
|
+
const queryFn = opts.queryFn ?? sdkQuery;
|
|
2711
|
+
const prompt = new Pushable();
|
|
2712
|
+
const abortController = new AbortController();
|
|
2713
|
+
let timer;
|
|
2714
|
+
let q;
|
|
2715
|
+
try {
|
|
2716
|
+
q = queryFn({
|
|
2717
|
+
prompt,
|
|
2718
|
+
options: {
|
|
2719
|
+
cwd: opts.cwd,
|
|
2720
|
+
settingSources: opts.settingSources,
|
|
2721
|
+
strictMcpConfig: true,
|
|
2722
|
+
mcpServers: {},
|
|
2723
|
+
settings: { disableAllHooks: true },
|
|
2724
|
+
abortController,
|
|
2725
|
+
stderr: () => {
|
|
2726
|
+
},
|
|
2727
|
+
env: definedEnv({ ...process.env, ...opts.env })
|
|
2728
|
+
}
|
|
2729
|
+
});
|
|
2730
|
+
const init = q.initializationResult();
|
|
2731
|
+
init.catch(() => {
|
|
2732
|
+
});
|
|
2733
|
+
const timeoutMs = opts.timeoutMs ?? PROBE_TIMEOUT_MS;
|
|
2734
|
+
const timeout = new Promise((_, reject) => {
|
|
2735
|
+
timer = setTimeout(() => {
|
|
2736
|
+
abortController.abort();
|
|
2737
|
+
reject(new Error(`Claude probe timed out after ${timeoutMs} ms`));
|
|
2738
|
+
}, timeoutMs);
|
|
2739
|
+
});
|
|
2740
|
+
const res = await Promise.race([init, timeout]);
|
|
2741
|
+
return { account: res.account ?? {}, models: res.models ?? [] };
|
|
2742
|
+
} finally {
|
|
2743
|
+
clearTimeout(timer);
|
|
2744
|
+
prompt.end();
|
|
2745
|
+
try {
|
|
2746
|
+
q?.close();
|
|
2747
|
+
} catch {
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
function toProviderModel(m, id, isDefault) {
|
|
2752
|
+
const levels = m.supportedEffortLevels ?? [];
|
|
2753
|
+
const efforts = levels.filter((e) => Effort.options.includes(e));
|
|
2754
|
+
const description = (m.description ?? "").slice(0, MAX_NAME);
|
|
2755
|
+
return {
|
|
2756
|
+
id: id.slice(0, MAX_NAME),
|
|
2757
|
+
// `||`, not `??`: an empty display name would fail `label.min(1)` and take the whole
|
|
2758
|
+
// capability with it.
|
|
2759
|
+
label: (m.displayName || id).slice(0, MAX_NAME),
|
|
2760
|
+
...isDefault ? { default: true } : {},
|
|
2761
|
+
...description ? { description } : {},
|
|
2762
|
+
efforts
|
|
2763
|
+
};
|
|
2764
|
+
}
|
|
2765
|
+
function toProviderModels(models) {
|
|
2766
|
+
const fallback = models.find((m) => m.value === "default");
|
|
2767
|
+
const resolved = fallback?.resolvedModel;
|
|
2768
|
+
const rows = models.filter((m) => m.value !== "default");
|
|
2769
|
+
const match = resolved ? rows.findIndex((m) => (m.resolvedModel ?? m.value) === resolved) : -1;
|
|
2770
|
+
const mapped = rows.map((m, i) => toProviderModel(m, m.value ?? "", i === match));
|
|
2771
|
+
if (fallback && resolved && match === -1) {
|
|
2772
|
+
mapped.unshift(toProviderModel(fallback, resolved, true));
|
|
2773
|
+
}
|
|
2774
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2775
|
+
return mapped.filter((m) => !seen.has(m.id) && seen.add(m.id)).slice(0, MAX_MODELS).filter((m) => ProviderModel.safeParse(m).success);
|
|
2776
|
+
}
|
|
2777
|
+
async function detectClaude(deps) {
|
|
2778
|
+
let version;
|
|
2779
|
+
try {
|
|
2780
|
+
version = deps.version();
|
|
2781
|
+
} catch {
|
|
2782
|
+
version = void 0;
|
|
2783
|
+
}
|
|
2784
|
+
const withVersion = version ? { version } : {};
|
|
2785
|
+
let res;
|
|
2786
|
+
try {
|
|
2787
|
+
res = await deps.probe();
|
|
2788
|
+
} catch (err) {
|
|
2789
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2790
|
+
if (/native binary not found/i.test(message)) {
|
|
2791
|
+
return { available: false, reason: "not_installed", ...withVersion, models: [] };
|
|
2792
|
+
}
|
|
2793
|
+
return { available: true, ...withVersion, models: [] };
|
|
2794
|
+
}
|
|
2795
|
+
const account = res.account ?? {};
|
|
2796
|
+
const authMode = authModeFrom(account);
|
|
2797
|
+
if (authMode === null && account.tokenSource === "none") {
|
|
2798
|
+
return { available: false, reason: "not_logged_in", ...withVersion, models: [] };
|
|
2799
|
+
}
|
|
2800
|
+
return {
|
|
2801
|
+
available: true,
|
|
2802
|
+
...withVersion,
|
|
2803
|
+
models: toProviderModels(res.models ?? []),
|
|
2804
|
+
...authMode ? { authMode } : {}
|
|
2805
|
+
};
|
|
2806
|
+
}
|
|
2807
|
+
var PROBE_TIMEOUT_MS, MAX_MODELS, MAX_NAME, MAX_VERSION;
|
|
2808
|
+
var init_detect = __esm({
|
|
2809
|
+
"src/claude/detect.ts"() {
|
|
2810
|
+
"use strict";
|
|
2811
|
+
init_src();
|
|
2812
|
+
init_env();
|
|
2813
|
+
init_auth_mode();
|
|
2814
|
+
init_pushable();
|
|
2815
|
+
PROBE_TIMEOUT_MS = 15e3;
|
|
2816
|
+
MAX_MODELS = 50;
|
|
2817
|
+
MAX_NAME = 200;
|
|
2818
|
+
MAX_VERSION = 50;
|
|
2819
|
+
}
|
|
2820
|
+
});
|
|
2821
|
+
|
|
2627
2822
|
// src/exec-path.ts
|
|
2628
2823
|
import { accessSync, constants, statSync as statSync3 } from "fs";
|
|
2629
|
-
import { delimiter, isAbsolute as isAbsolute5, join as
|
|
2824
|
+
import { delimiter, isAbsolute as isAbsolute5, join as join9 } from "path";
|
|
2630
2825
|
function findOnPath(name, path) {
|
|
2631
2826
|
for (const dir of (path ?? "").split(delimiter)) {
|
|
2632
2827
|
if (!isAbsolute5(dir)) continue;
|
|
2633
|
-
const candidate =
|
|
2828
|
+
const candidate = join9(dir, name);
|
|
2634
2829
|
try {
|
|
2635
2830
|
if (!statSync3(candidate).isFile()) continue;
|
|
2636
2831
|
accessSync(candidate, constants.X_OK);
|
|
@@ -2729,13 +2924,16 @@ var init_rpc = __esm({
|
|
|
2729
2924
|
}
|
|
2730
2925
|
}
|
|
2731
2926
|
dispatch(line) {
|
|
2732
|
-
let
|
|
2927
|
+
let parsed;
|
|
2733
2928
|
try {
|
|
2734
|
-
|
|
2929
|
+
parsed = JSON.parse(line);
|
|
2735
2930
|
} catch {
|
|
2736
2931
|
return;
|
|
2737
2932
|
}
|
|
2933
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return;
|
|
2934
|
+
const frame = parsed;
|
|
2738
2935
|
if (frame.id !== void 0 && frame.method === void 0) {
|
|
2936
|
+
if (typeof frame.id !== "number") return;
|
|
2739
2937
|
const p = this.pending.get(frame.id);
|
|
2740
2938
|
if (!p) return;
|
|
2741
2939
|
this.pending.delete(frame.id);
|
|
@@ -2748,6 +2946,7 @@ var init_rpc = __esm({
|
|
|
2748
2946
|
void (async () => {
|
|
2749
2947
|
try {
|
|
2750
2948
|
const result = await this.request_?.({
|
|
2949
|
+
id,
|
|
2751
2950
|
method: frame.method,
|
|
2752
2951
|
params: frame.params
|
|
2753
2952
|
});
|
|
@@ -2755,7 +2954,11 @@ var init_rpc = __esm({
|
|
|
2755
2954
|
} catch (err) {
|
|
2756
2955
|
this.write({
|
|
2757
2956
|
id,
|
|
2758
|
-
error: {
|
|
2957
|
+
error: {
|
|
2958
|
+
// A handler that knows the JSON-RPC code for its refusal says so; else "internal".
|
|
2959
|
+
code: err instanceof RpcError ? err.code : -32603,
|
|
2960
|
+
message: err instanceof Error ? err.message : String(err)
|
|
2961
|
+
}
|
|
2759
2962
|
});
|
|
2760
2963
|
}
|
|
2761
2964
|
})();
|
|
@@ -2770,6 +2973,17 @@ var init_rpc = __esm({
|
|
|
2770
2973
|
// src/codex/detect.ts
|
|
2771
2974
|
import { execFile as execFile2, spawn } from "child_process";
|
|
2772
2975
|
import { promisify } from "util";
|
|
2976
|
+
async function allModels(request) {
|
|
2977
|
+
const models = [];
|
|
2978
|
+
let cursor;
|
|
2979
|
+
do {
|
|
2980
|
+
const page = await request(cursor ? { cursor } : {});
|
|
2981
|
+
const data = page.data ?? [];
|
|
2982
|
+
models.push(...data);
|
|
2983
|
+
cursor = data.length > 0 && page.nextCursor ? page.nextCursor : void 0;
|
|
2984
|
+
} while (cursor && models.length < MAX_MODELS2);
|
|
2985
|
+
return models;
|
|
2986
|
+
}
|
|
2773
2987
|
function findCodexOnPath(env) {
|
|
2774
2988
|
return findOnPath("codex", env.PATH);
|
|
2775
2989
|
}
|
|
@@ -2778,7 +2992,7 @@ function resolveCodexPath(cfg, env, onPath) {
|
|
|
2778
2992
|
}
|
|
2779
2993
|
async function runCodex(path, args) {
|
|
2780
2994
|
try {
|
|
2781
|
-
const { stdout, stderr } = await exec(path, args, { timeout:
|
|
2995
|
+
const { stdout, stderr } = await exec(path, args, { timeout: PROBE_TIMEOUT_MS2 });
|
|
2782
2996
|
return { code: 0, stdout, stderr };
|
|
2783
2997
|
} catch (err) {
|
|
2784
2998
|
const e = err;
|
|
@@ -2798,30 +3012,29 @@ async function listCodexModels(path) {
|
|
|
2798
3012
|
clientInfo: { name: "cayenne-runner", title: "Cayenne", version: "0.0.1" }
|
|
2799
3013
|
});
|
|
2800
3014
|
rpc.notify("initialized", {});
|
|
2801
|
-
|
|
2802
|
-
return res.data ?? [];
|
|
3015
|
+
return await allModels((params) => rpc.request("model/list", params));
|
|
2803
3016
|
} finally {
|
|
2804
3017
|
clearTimeout(timer);
|
|
2805
3018
|
rpc.close();
|
|
2806
3019
|
child.kill();
|
|
2807
3020
|
}
|
|
2808
3021
|
}
|
|
2809
|
-
function
|
|
2810
|
-
const efforts = (m.supportedReasoningEfforts ?? []).map((e) => e.
|
|
3022
|
+
function toProviderModel2(m) {
|
|
3023
|
+
const efforts = (m.supportedReasoningEfforts ?? []).map((e) => e.reasoningEffort).filter((e) => Effort.options.includes(e));
|
|
2811
3024
|
const fallback = Effort.options.includes(m.defaultReasoningEffort) ? m.defaultReasoningEffort : void 0;
|
|
2812
3025
|
return {
|
|
2813
|
-
id: (m.id ?? "").slice(0,
|
|
3026
|
+
id: (m.id ?? "").slice(0, MAX_NAME2),
|
|
2814
3027
|
// `||`, not `??`: an app-server that sends `displayName: ''` would otherwise publish an
|
|
2815
3028
|
// empty label, which `ProviderModel.label.min(1)` rejects — taking the whole capability
|
|
2816
3029
|
// with it.
|
|
2817
|
-
label: (m.displayName || m.id || "").slice(0,
|
|
3030
|
+
label: (m.displayName || m.id || "").slice(0, MAX_NAME2),
|
|
2818
3031
|
...m.isDefault ? { default: true } : {},
|
|
2819
3032
|
efforts,
|
|
2820
3033
|
...fallback ? { defaultEffort: fallback } : {}
|
|
2821
3034
|
};
|
|
2822
3035
|
}
|
|
2823
|
-
function
|
|
2824
|
-
return models.filter((m) => m.hidden !== true).slice(0,
|
|
3036
|
+
function toProviderModels2(models) {
|
|
3037
|
+
return models.filter((m) => m.hidden !== true).slice(0, MAX_MODELS2).map(toProviderModel2).filter((m) => ProviderModel.safeParse(m).success);
|
|
2825
3038
|
}
|
|
2826
3039
|
async function detectCodex(deps) {
|
|
2827
3040
|
if (!deps.path) return { available: false, reason: "not_installed", models: [] };
|
|
@@ -2829,33 +3042,37 @@ async function detectCodex(deps) {
|
|
|
2829
3042
|
try {
|
|
2830
3043
|
const probe = await deps.run(deps.path, ["--version"]);
|
|
2831
3044
|
if (probe.code !== 0) return { available: false, reason: "not_installed", models: [] };
|
|
2832
|
-
version = probe.stdout.trim().split(/\s+/).at(-1)?.slice(0,
|
|
3045
|
+
version = probe.stdout.trim().split(/\s+/).at(-1)?.slice(0, MAX_VERSION2);
|
|
2833
3046
|
const login2 = await deps.run(deps.path, ["login", "status"]);
|
|
2834
3047
|
if (login2.code !== 0) {
|
|
2835
3048
|
return { available: false, reason: "not_logged_in", version, models: [] };
|
|
2836
3049
|
}
|
|
2837
3050
|
const models = await deps.listModels(deps.path);
|
|
2838
|
-
return { available: true, version, models:
|
|
3051
|
+
return { available: true, version, models: toProviderModels2(models) };
|
|
2839
3052
|
} catch {
|
|
2840
3053
|
return { available: false, reason: "error", ...version ? { version } : {}, models: [] };
|
|
2841
3054
|
}
|
|
2842
3055
|
}
|
|
2843
3056
|
async function detectProviders(deps) {
|
|
2844
|
-
|
|
3057
|
+
const [claude, codex] = await Promise.all([
|
|
3058
|
+
deps.claude().catch(() => ({ available: true, models: [] })),
|
|
3059
|
+
detectCodex(deps)
|
|
3060
|
+
]);
|
|
3061
|
+
return { claude, codex };
|
|
2845
3062
|
}
|
|
2846
|
-
var exec,
|
|
2847
|
-
var
|
|
3063
|
+
var exec, PROBE_TIMEOUT_MS2, LIST_TIMEOUT_MS, MAX_MODELS2, MAX_NAME2, MAX_VERSION2;
|
|
3064
|
+
var init_detect2 = __esm({
|
|
2848
3065
|
"src/codex/detect.ts"() {
|
|
2849
3066
|
"use strict";
|
|
2850
3067
|
init_src();
|
|
2851
3068
|
init_exec_path();
|
|
2852
3069
|
init_rpc();
|
|
2853
3070
|
exec = promisify(execFile2);
|
|
2854
|
-
|
|
3071
|
+
PROBE_TIMEOUT_MS2 = 5e3;
|
|
2855
3072
|
LIST_TIMEOUT_MS = 1e4;
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
3073
|
+
MAX_MODELS2 = 50;
|
|
3074
|
+
MAX_NAME2 = 200;
|
|
3075
|
+
MAX_VERSION2 = 50;
|
|
2859
3076
|
}
|
|
2860
3077
|
});
|
|
2861
3078
|
|
|
@@ -3499,7 +3716,7 @@ var init_source = __esm({
|
|
|
3499
3716
|
|
|
3500
3717
|
// src/paths.ts
|
|
3501
3718
|
import { lstatSync, realpathSync as realpathSync5 } from "fs";
|
|
3502
|
-
import { dirname as
|
|
3719
|
+
import { dirname as dirname4, isAbsolute as isAbsolute6, join as join10, relative as relative2, sep as sep2 } from "path";
|
|
3503
3720
|
function assertInsideRoots(target, roots) {
|
|
3504
3721
|
return resolveInsideRoots(target, roots).real;
|
|
3505
3722
|
}
|
|
@@ -3519,8 +3736,8 @@ function assertInsideRootsOrAncestor(target, roots) {
|
|
|
3519
3736
|
assertRoots(roots);
|
|
3520
3737
|
const absolute = isAbsolute6(target) ? target : `${process.cwd()}${sep2}${target}`;
|
|
3521
3738
|
let walked = walk(target, absolute);
|
|
3522
|
-
if (walked.tail.includes("..")) walked = walk(target,
|
|
3523
|
-
const checked = walked.tail.length === 0 ? walked.real :
|
|
3739
|
+
if (walked.tail.includes("..")) walked = walk(target, join10(walked.real, ...walked.tail));
|
|
3740
|
+
const checked = walked.tail.length === 0 ? walked.real : join10(walked.real, ...walked.tail);
|
|
3524
3741
|
if (!insideAny(checked, roots)) throw new Error(`ROOT_NOT_ALLOWED: ${target}`);
|
|
3525
3742
|
return checked;
|
|
3526
3743
|
}
|
|
@@ -3537,7 +3754,7 @@ function walk(target, absolute) {
|
|
|
3537
3754
|
tail.push(segment);
|
|
3538
3755
|
continue;
|
|
3539
3756
|
}
|
|
3540
|
-
const next = segment === ".." ?
|
|
3757
|
+
const next = segment === ".." ? dirname4(real) : join10(real, segment);
|
|
3541
3758
|
if (!lstatSync(next, { throwIfNoEntry: false })) {
|
|
3542
3759
|
tail.push(segment);
|
|
3543
3760
|
continue;
|
|
@@ -3585,8 +3802,8 @@ var init_paths = __esm({
|
|
|
3585
3802
|
|
|
3586
3803
|
// src/git/introspect.ts
|
|
3587
3804
|
import { execFile as execFile3 } from "child_process";
|
|
3588
|
-
import { readFileSync as
|
|
3589
|
-
import { delimiter as delimiter2, dirname as
|
|
3805
|
+
import { readFileSync as readFileSync5, realpathSync as realpathSync6 } from "fs";
|
|
3806
|
+
import { delimiter as delimiter2, dirname as dirname5, isAbsolute as isAbsolute7, join as join11, resolve as resolve2 } from "path";
|
|
3590
3807
|
import { promisify as promisify2 } from "util";
|
|
3591
3808
|
function parseGitHubRemote(url) {
|
|
3592
3809
|
const trimmed = url.trim();
|
|
@@ -3672,10 +3889,10 @@ function insideRoot(path, root) {
|
|
|
3672
3889
|
}
|
|
3673
3890
|
}
|
|
3674
3891
|
function isLinkedWorktree(gitDir, commonDir, repoRoot) {
|
|
3675
|
-
if (gitDir === commonDir ||
|
|
3892
|
+
if (gitDir === commonDir || dirname5(gitDir) !== join11(commonDir, "worktrees")) return false;
|
|
3676
3893
|
try {
|
|
3677
|
-
const back =
|
|
3678
|
-
const mine = canonical(
|
|
3894
|
+
const back = readFileSync5(join11(gitDir, "gitdir"), "utf8").trim();
|
|
3895
|
+
const mine = canonical(join11(repoRoot, ".git"), repoRoot);
|
|
3679
3896
|
return mine !== null && canonical(back, gitDir) === mine;
|
|
3680
3897
|
} catch {
|
|
3681
3898
|
return false;
|
|
@@ -3683,7 +3900,7 @@ function isLinkedWorktree(gitDir, commonDir, repoRoot) {
|
|
|
3683
3900
|
}
|
|
3684
3901
|
async function introspectRepo(cwd, run, root, { names = true } = {}) {
|
|
3685
3902
|
try {
|
|
3686
|
-
const ceiling =
|
|
3903
|
+
const ceiling = dirname5(root);
|
|
3687
3904
|
if (ceiling.includes(delimiter2)) return { ...NONE };
|
|
3688
3905
|
const line = async (args) => {
|
|
3689
3906
|
const { code, stdout } = await run(args, cwd, ceiling);
|
|
@@ -3780,9 +3997,9 @@ var init_introspect = __esm({
|
|
|
3780
3997
|
});
|
|
3781
3998
|
|
|
3782
3999
|
// src/repos/credentials.ts
|
|
3783
|
-
import { chmodSync as chmodSync4, mkdirSync as mkdirSync4, readFileSync as
|
|
4000
|
+
import { chmodSync as chmodSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync6, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
3784
4001
|
import { devNull, homedir as homedir4 } from "os";
|
|
3785
|
-
import { delimiter as delimiter3, isAbsolute as isAbsolute8, join as
|
|
4002
|
+
import { delimiter as delimiter3, isAbsolute as isAbsolute8, join as join12 } from "path";
|
|
3786
4003
|
function gitWrapper(git, gitHome) {
|
|
3787
4004
|
if (git === null || !isAbsolute8(git)) {
|
|
3788
4005
|
return `#!/bin/sh
|
|
@@ -3797,7 +4014,7 @@ exec ${shellQuote(git)} "$@"
|
|
|
3797
4014
|
`;
|
|
3798
4015
|
}
|
|
3799
4016
|
function repositoryGitHome(runDir) {
|
|
3800
|
-
return
|
|
4017
|
+
return join12(runDir, GIT_HOME);
|
|
3801
4018
|
}
|
|
3802
4019
|
function gitCredentialEnv(token, readOnlyText, opts = {}) {
|
|
3803
4020
|
const env = {
|
|
@@ -3818,7 +4035,7 @@ function gitCredentialEnv(token, readOnlyText, opts = {}) {
|
|
|
3818
4035
|
CAYENNE_READ_ONLY: readOnlyText
|
|
3819
4036
|
};
|
|
3820
4037
|
if (token !== null) env.GH_TOKEN = token;
|
|
3821
|
-
if (opts.runDir !== void 0) env.GIT_SSH_COMMAND = shellQuote(
|
|
4038
|
+
if (opts.runDir !== void 0) env.GIT_SSH_COMMAND = shellQuote(join12(opts.runDir, NO_SSH));
|
|
3822
4039
|
if (opts.home !== void 0) env.HOME = opts.home;
|
|
3823
4040
|
return env;
|
|
3824
4041
|
}
|
|
@@ -3830,14 +4047,14 @@ function repositoryChatEnv(input) {
|
|
|
3830
4047
|
SSH_AUTH_SOCK: void 0,
|
|
3831
4048
|
GIT_CONFIG_PARAMETERS: void 0,
|
|
3832
4049
|
...Object.fromEntries(TRACE_AND_TLS_VARS.map((key) => [key, void 0])),
|
|
3833
|
-
GH_CONFIG_DIR:
|
|
4050
|
+
GH_CONFIG_DIR: join12(input.runDir, GH_CONFIG),
|
|
3834
4051
|
GH_PROMPT_DISABLED: "1",
|
|
3835
4052
|
// A replace ref another chat planted in the shared clone must not change what a sha reads as.
|
|
3836
4053
|
GIT_NO_REPLACE_OBJECTS: "1",
|
|
3837
4054
|
// Only git runs with HOME at the git home. Go, SwiftPM and python-requests read `$NETRC` first:
|
|
3838
4055
|
// this one never exists. curl reads the first `.curlrc` of `$CURL_HOME`, then `$HOME`: the git
|
|
3839
4056
|
// home's is empty, so the owner's (`netrc`, `user = …`) never applies.
|
|
3840
|
-
NETRC:
|
|
4057
|
+
NETRC: join12(repositoryGitHome(input.runDir), NETRC_NAME),
|
|
3841
4058
|
CURL_HOME: repositoryGitHome(input.runDir),
|
|
3842
4059
|
// With it and CLAUDE_CONFIG_DIR both set, the CLI names its project dir by it rather than by
|
|
3843
4060
|
// the cwd's slug: every chat's tool outputs would share one dir the worktree bound lets it read.
|
|
@@ -3852,8 +4069,8 @@ function repositoryChatEnv(input) {
|
|
|
3852
4069
|
};
|
|
3853
4070
|
if (input.token === null) env.GH_TOKEN = void 0;
|
|
3854
4071
|
const bins = [
|
|
3855
|
-
...input.token === null ? [
|
|
3856
|
-
|
|
4072
|
+
...input.token === null ? [join12(input.runDir, READONLY_BIN)] : [],
|
|
4073
|
+
join12(input.runDir, GIT_BIN)
|
|
3857
4074
|
];
|
|
3858
4075
|
env.PATH = [...bins, ...input.path ? [input.path] : []].join(delimiter3);
|
|
3859
4076
|
for (const key of Object.keys(input.inherited ?? process.env)) {
|
|
@@ -3864,17 +4081,17 @@ function repositoryChatEnv(input) {
|
|
|
3864
4081
|
}
|
|
3865
4082
|
function prepareRunDir(runDir, opts) {
|
|
3866
4083
|
mkdirSync4(runDir, { recursive: true, mode: 448 });
|
|
3867
|
-
freshDir(
|
|
4084
|
+
freshDir(join12(runDir, GH_CONFIG));
|
|
3868
4085
|
freshDir(repositoryGitHome(runDir));
|
|
3869
|
-
writeFileSync4(
|
|
3870
|
-
chmodSync4(
|
|
3871
|
-
const bin =
|
|
4086
|
+
writeFileSync4(join12(repositoryGitHome(runDir), CURLRC_NAME), "", { mode: 384, flag: "wx" });
|
|
4087
|
+
chmodSync4(join12(repositoryGitHome(runDir), CURLRC_NAME), 384);
|
|
4088
|
+
const bin = join12(runDir, READONLY_BIN);
|
|
3872
4089
|
freshDir(bin);
|
|
3873
|
-
writeShim(
|
|
3874
|
-
writeShim(
|
|
3875
|
-
const gitBin =
|
|
4090
|
+
writeShim(join12(bin, "gh"), GH_SHIM);
|
|
4091
|
+
writeShim(join12(runDir, NO_SSH), NO_SSH_SHIM);
|
|
4092
|
+
const gitBin = join12(runDir, GIT_BIN);
|
|
3876
4093
|
freshDir(gitBin);
|
|
3877
|
-
writeShim(
|
|
4094
|
+
writeShim(join12(gitBin, "git"), gitWrapper(opts.git, repositoryGitHome(runDir)));
|
|
3878
4095
|
}
|
|
3879
4096
|
function freshDir(path) {
|
|
3880
4097
|
rmSync3(path, { recursive: true, force: true });
|
|
@@ -3882,12 +4099,12 @@ function freshDir(path) {
|
|
|
3882
4099
|
chmodSync4(path, 448);
|
|
3883
4100
|
}
|
|
3884
4101
|
function githubNetrcFiles(home = homedir4(), env = process.env) {
|
|
3885
|
-
const candidates = [env.NETRC,
|
|
4102
|
+
const candidates = [env.NETRC, join12(home, ".netrc")].filter(
|
|
3886
4103
|
(path) => typeof path === "string" && path !== ""
|
|
3887
4104
|
);
|
|
3888
4105
|
return [...new Set(candidates)].filter((path) => {
|
|
3889
4106
|
try {
|
|
3890
|
-
return netrcCoversGitHub(
|
|
4107
|
+
return netrcCoversGitHub(readFileSync6(path, "utf8"));
|
|
3891
4108
|
} catch {
|
|
3892
4109
|
return false;
|
|
3893
4110
|
}
|
|
@@ -3968,13 +4185,13 @@ exit 1
|
|
|
3968
4185
|
|
|
3969
4186
|
// src/git/commands.ts
|
|
3970
4187
|
import { lstatSync as lstatSync2, realpathSync as realpathSync7, statSync as statSync4 } from "fs";
|
|
3971
|
-
import { dirname as
|
|
4188
|
+
import { dirname as dirname6, isAbsolute as isAbsolute9, join as join13, relative as relative3, resolve as resolve3, sep as sep3 } from "path";
|
|
3972
4189
|
async function scopeRepo(cwd, run, root) {
|
|
3973
4190
|
const identity = await introspectRepo(cwd, run, root, { names: false });
|
|
3974
4191
|
if (identity.repo_root === null) {
|
|
3975
4192
|
throw new Error(`VALIDATION: ${cwd} is not inside a git repository under an allowed root`);
|
|
3976
4193
|
}
|
|
3977
|
-
return { repoRoot: identity.repo_root, ceiling:
|
|
4194
|
+
return { repoRoot: identity.repo_root, ceiling: dirname6(root) };
|
|
3978
4195
|
}
|
|
3979
4196
|
function fold(letter) {
|
|
3980
4197
|
switch (letter) {
|
|
@@ -4134,13 +4351,13 @@ async function gitDirs(scope, run) {
|
|
|
4134
4351
|
}
|
|
4135
4352
|
}
|
|
4136
4353
|
function isGitDirShape(dir) {
|
|
4137
|
-
const head = lstatSync2(
|
|
4354
|
+
const head = lstatSync2(join13(dir, "HEAD"), { throwIfNoEntry: false });
|
|
4138
4355
|
if (!head || !(head.isFile() || head.isSymbolicLink())) return false;
|
|
4139
|
-
const commondir = lstatSync2(
|
|
4356
|
+
const commondir = lstatSync2(join13(dir, "commondir"), { throwIfNoEntry: false });
|
|
4140
4357
|
if (commondir?.isFile()) return true;
|
|
4141
4358
|
const isDir = (name) => {
|
|
4142
4359
|
try {
|
|
4143
|
-
return statSync4(
|
|
4360
|
+
return statSync4(join13(dir, name), { throwIfNoEntry: false })?.isDirectory() === true;
|
|
4144
4361
|
} catch {
|
|
4145
4362
|
return false;
|
|
4146
4363
|
}
|
|
@@ -4161,13 +4378,13 @@ function enclosesHome(repoRoot, home) {
|
|
|
4161
4378
|
} catch {
|
|
4162
4379
|
return false;
|
|
4163
4380
|
}
|
|
4164
|
-
for (let cur = real; ; cur =
|
|
4381
|
+
for (let cur = real; ; cur = dirname6(cur)) {
|
|
4165
4382
|
try {
|
|
4166
4383
|
const st = statSync4(cur, { bigint: true, throwIfNoEntry: false });
|
|
4167
4384
|
if (st && st.dev === root.dev && st.ino === root.ino) return true;
|
|
4168
4385
|
} catch {
|
|
4169
4386
|
}
|
|
4170
|
-
if (
|
|
4387
|
+
if (dirname6(cur) === cur) return false;
|
|
4171
4388
|
}
|
|
4172
4389
|
}
|
|
4173
4390
|
function protectedTest(paths, exempt = []) {
|
|
@@ -4195,10 +4412,10 @@ function protectedTest(paths, exempt = []) {
|
|
|
4195
4412
|
return (path) => {
|
|
4196
4413
|
if (exempts.length === 0 && entries2.some((e) => within(path, e.real))) return true;
|
|
4197
4414
|
let exempted = false;
|
|
4198
|
-
for (let cur = path; ; cur =
|
|
4415
|
+
for (let cur = path; ; cur = dirname6(cur)) {
|
|
4199
4416
|
if (!exempted && isOneOf(cur, entries2)) return true;
|
|
4200
4417
|
if (exempts.length > 0 && isOneOf(cur, exempts)) exempted = true;
|
|
4201
|
-
if (
|
|
4418
|
+
if (dirname6(cur) === cur) return false;
|
|
4202
4419
|
}
|
|
4203
4420
|
};
|
|
4204
4421
|
}
|
|
@@ -4215,7 +4432,7 @@ function identities(paths) {
|
|
|
4215
4432
|
return out;
|
|
4216
4433
|
}
|
|
4217
4434
|
function insideOtherGitDir(real, repoRoot) {
|
|
4218
|
-
for (let dir =
|
|
4435
|
+
for (let dir = dirname6(real); within(dir, repoRoot) && dir !== repoRoot; dir = dirname6(dir)) {
|
|
4219
4436
|
if (isGitDirShape(dir)) return true;
|
|
4220
4437
|
}
|
|
4221
4438
|
return false;
|
|
@@ -4225,7 +4442,7 @@ async function gitDiff(scope, run, opts, guard = {}) {
|
|
|
4225
4442
|
const guarded = protectedTest(guard.protectedPaths ?? [], guard.exemptPaths ?? []);
|
|
4226
4443
|
const isProtected = (p) => guarded?.(p) === true;
|
|
4227
4444
|
if (opts.path) {
|
|
4228
|
-
const named =
|
|
4445
|
+
const named = join13(scope.repoRoot, opts.path);
|
|
4229
4446
|
let resolved = null;
|
|
4230
4447
|
try {
|
|
4231
4448
|
resolved = realpathSync7.native(named);
|
|
@@ -4255,7 +4472,7 @@ async function gitDiff(scope, run, opts, guard = {}) {
|
|
|
4255
4472
|
`VALIDATION: untracked files are not diffed in a repository at or above the home directory (${scope.repoRoot})`
|
|
4256
4473
|
);
|
|
4257
4474
|
}
|
|
4258
|
-
const absolute =
|
|
4475
|
+
const absolute = join13(scope.repoRoot, opts.path);
|
|
4259
4476
|
if (!lstatSync2(absolute, { throwIfNoEntry: false })) {
|
|
4260
4477
|
try {
|
|
4261
4478
|
assertInsideRootsOrAncestor(absolute, [scope.repoRoot]);
|
|
@@ -4298,7 +4515,7 @@ async function gitDiff(scope, run, opts, guard = {}) {
|
|
|
4298
4515
|
}
|
|
4299
4516
|
const diff = splitDiff(first.stdout);
|
|
4300
4517
|
if (guarded === null) return diff;
|
|
4301
|
-
const keep = (p) => p === void 0 || !isProtected(
|
|
4518
|
+
const keep = (p) => p === void 0 || !isProtected(join13(scope.repoRoot, p));
|
|
4302
4519
|
return { ...diff, files: diff.files.filter((f) => keep(f.path) && keep(f.oldPath)) };
|
|
4303
4520
|
}
|
|
4304
4521
|
function parseBranches(raw, head) {
|
|
@@ -4367,16 +4584,16 @@ import {
|
|
|
4367
4584
|
lstatSync as lstatSync3,
|
|
4368
4585
|
mkdirSync as mkdirSync5,
|
|
4369
4586
|
openSync,
|
|
4370
|
-
readFileSync as
|
|
4587
|
+
readFileSync as readFileSync7,
|
|
4371
4588
|
realpathSync as realpathSync8,
|
|
4372
4589
|
statSync as statSync5,
|
|
4373
4590
|
writeSync
|
|
4374
4591
|
} from "fs";
|
|
4375
4592
|
import { readdir } from "fs/promises";
|
|
4376
|
-
import { dirname as
|
|
4593
|
+
import { dirname as dirname7, isAbsolute as isAbsolute10, join as join14, relative as relative4, sep as sep4 } from "path";
|
|
4377
4594
|
import { setImmediate } from "timers/promises";
|
|
4378
4595
|
function worktreePathFor(repoRoot, name) {
|
|
4379
|
-
return
|
|
4596
|
+
return join14(repoRoot, WORKTREES_DIR, name);
|
|
4380
4597
|
}
|
|
4381
4598
|
async function listedWorktrees(scope, run) {
|
|
4382
4599
|
const r = await run(["worktree", "list", "--porcelain"], scope.repoRoot, scope.ceiling);
|
|
@@ -4399,7 +4616,7 @@ async function excludeCayenneDir(scope, run) {
|
|
|
4399
4616
|
);
|
|
4400
4617
|
const common = r.stdout.trim();
|
|
4401
4618
|
if (r.code !== 0 || !isAbsolute10(common)) return;
|
|
4402
|
-
const dotGit =
|
|
4619
|
+
const dotGit = join14(scope.repoRoot, ".git");
|
|
4403
4620
|
let dotGitIsRealDir;
|
|
4404
4621
|
try {
|
|
4405
4622
|
dotGitIsRealDir = lstatSync3(dotGit).isDirectory();
|
|
@@ -4416,16 +4633,16 @@ async function excludeCayenneDir(scope, run) {
|
|
|
4416
4633
|
return;
|
|
4417
4634
|
}
|
|
4418
4635
|
if (commonReal !== dotGitReal) return;
|
|
4419
|
-
const exclude =
|
|
4636
|
+
const exclude = join14(commonReal, "info", "exclude");
|
|
4420
4637
|
let checkedExclude;
|
|
4421
4638
|
try {
|
|
4422
4639
|
checkedExclude = assertInsideRootsOrAncestor(exclude, [scope.repoRoot]);
|
|
4423
4640
|
} catch {
|
|
4424
4641
|
return;
|
|
4425
4642
|
}
|
|
4426
|
-
const existing = existsSync4(checkedExclude) ?
|
|
4643
|
+
const existing = existsSync4(checkedExclude) ? readFileSync7(checkedExclude, "utf8") : "";
|
|
4427
4644
|
if (existing.split("\n").includes(".cayenne/")) return;
|
|
4428
|
-
mkdirSync5(
|
|
4645
|
+
mkdirSync5(dirname7(checkedExclude), { recursive: true });
|
|
4429
4646
|
const toAppend = `${existing.endsWith("\n") || existing === "" ? "" : "\n"}.cayenne/
|
|
4430
4647
|
`;
|
|
4431
4648
|
let fd;
|
|
@@ -4451,7 +4668,7 @@ function asNotADirectory(path, err) {
|
|
|
4451
4668
|
return err instanceof Error ? err : new Error(String(err));
|
|
4452
4669
|
}
|
|
4453
4670
|
function canonicalWorktreesDir(scope) {
|
|
4454
|
-
const dir =
|
|
4671
|
+
const dir = join14(scope.repoRoot, WORKTREES_DIR);
|
|
4455
4672
|
try {
|
|
4456
4673
|
assertInsideRootsOrAncestor(dir, [scope.repoRoot]);
|
|
4457
4674
|
} catch (err) {
|
|
@@ -4474,7 +4691,7 @@ async function createWorktree(scope, run, name, base) {
|
|
|
4474
4691
|
throw new Error(`VALIDATION: ${name} is not a worktree name`);
|
|
4475
4692
|
}
|
|
4476
4693
|
const worktreesDir = canonicalWorktreesDir(scope);
|
|
4477
|
-
const path =
|
|
4694
|
+
const path = join14(worktreesDir, name);
|
|
4478
4695
|
if (lstatSync3(path, { throwIfNoEntry: false })) {
|
|
4479
4696
|
try {
|
|
4480
4697
|
resolveInsideRoots(path, [scope.repoRoot]);
|
|
@@ -4523,7 +4740,7 @@ async function findNestedRepository(top, limits = NESTED_WALK_LIMITS) {
|
|
|
4523
4740
|
const lower = entry.name.toLowerCase();
|
|
4524
4741
|
if (lower === ".git" && !(depth === 0 && entry.name === ".git")) return "repository";
|
|
4525
4742
|
if (lower === "head") hasHead = true;
|
|
4526
|
-
const path =
|
|
4743
|
+
const path = join14(dir, entry.name);
|
|
4527
4744
|
let isDir;
|
|
4528
4745
|
try {
|
|
4529
4746
|
isDir = entry.isDirectory() || // Some filesystems report no type; lstat decides, still without following.
|
|
@@ -4559,7 +4776,7 @@ async function worktreeIdentity(scope, run) {
|
|
|
4559
4776
|
}
|
|
4560
4777
|
}
|
|
4561
4778
|
async function removeWorktree(scope, run, worktreePath, force) {
|
|
4562
|
-
const worktreesDir =
|
|
4779
|
+
const worktreesDir = join14(scope.repoRoot, WORKTREES_DIR);
|
|
4563
4780
|
let base;
|
|
4564
4781
|
try {
|
|
4565
4782
|
base = realpathSync8.native(worktreesDir);
|
|
@@ -4664,10 +4881,10 @@ var init_ops = __esm({
|
|
|
4664
4881
|
|
|
4665
4882
|
// src/git/worktree-owners.ts
|
|
4666
4883
|
import { createHash, randomBytes } from "crypto";
|
|
4667
|
-
import { mkdirSync as mkdirSync6, readFileSync as
|
|
4668
|
-
import { join as
|
|
4884
|
+
import { mkdirSync as mkdirSync6, readFileSync as readFileSync8, renameSync as renameSync2, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
4885
|
+
import { join as join15 } from "path";
|
|
4669
4886
|
function createWorktreeOwners(dir) {
|
|
4670
|
-
const fileFor = (path) =>
|
|
4887
|
+
const fileFor = (path) => join15(dir, `${createHash("sha256").update(path).digest("hex")}.json`);
|
|
4671
4888
|
return {
|
|
4672
4889
|
record(path, rec) {
|
|
4673
4890
|
const body = {
|
|
@@ -4710,7 +4927,7 @@ function writeRecordFile(dir, file, body) {
|
|
|
4710
4927
|
}
|
|
4711
4928
|
function readRecordFile(file) {
|
|
4712
4929
|
try {
|
|
4713
|
-
const parsed = JSON.parse(
|
|
4930
|
+
const parsed = JSON.parse(readFileSync8(file, "utf8"));
|
|
4714
4931
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
4715
4932
|
} catch {
|
|
4716
4933
|
return null;
|
|
@@ -4724,9 +4941,9 @@ var init_worktree_owners = __esm({
|
|
|
4724
4941
|
|
|
4725
4942
|
// src/git/session-worktrees.ts
|
|
4726
4943
|
import { rmSync as rmSync5 } from "fs";
|
|
4727
|
-
import { join as
|
|
4944
|
+
import { join as join16 } from "path";
|
|
4728
4945
|
function createSessionWorktrees(dir) {
|
|
4729
|
-
const fileFor = (sessionId) => UUID2.test(sessionId) ?
|
|
4946
|
+
const fileFor = (sessionId) => UUID2.test(sessionId) ? join16(dir, `${sessionId}.json`) : null;
|
|
4730
4947
|
return {
|
|
4731
4948
|
record(rec) {
|
|
4732
4949
|
const file = fileFor(rec.sessionId);
|
|
@@ -5546,16 +5763,16 @@ import {
|
|
|
5546
5763
|
mkdirSync as mkdirSync7,
|
|
5547
5764
|
openSync as openSync2,
|
|
5548
5765
|
readdirSync,
|
|
5549
|
-
readFileSync as
|
|
5766
|
+
readFileSync as readFileSync9,
|
|
5550
5767
|
realpathSync as realpathSync9,
|
|
5551
5768
|
renameSync as renameSync3,
|
|
5552
5769
|
rmSync as rmSync6,
|
|
5553
5770
|
statSync as statSync6,
|
|
5554
5771
|
writeSync as writeSync2
|
|
5555
5772
|
} from "fs";
|
|
5556
|
-
import { dirname as
|
|
5773
|
+
import { dirname as dirname8, join as join17 } from "path";
|
|
5557
5774
|
function managedReposRoot(profileDir2) {
|
|
5558
|
-
return
|
|
5775
|
+
return join17(profileDir2, "repos");
|
|
5559
5776
|
}
|
|
5560
5777
|
function githubRemoteUrl(repo) {
|
|
5561
5778
|
return `https://github.com/${repo.owner}/${repo.name}.git`;
|
|
@@ -5590,9 +5807,9 @@ function createManagedRepos(deps) {
|
|
|
5590
5807
|
}
|
|
5591
5808
|
};
|
|
5592
5809
|
async function cloneInto(repo, env) {
|
|
5593
|
-
const ownerDir =
|
|
5594
|
-
const path =
|
|
5595
|
-
const tmp =
|
|
5810
|
+
const ownerDir = join17(root, repo.owner);
|
|
5811
|
+
const path = join17(ownerDir, repo.name);
|
|
5812
|
+
const tmp = join17(ownerDir, `.${repo.name}.clone-${randomBytes2(4).toString("hex")}`);
|
|
5596
5813
|
inflight.add(tmp);
|
|
5597
5814
|
try {
|
|
5598
5815
|
mkdirSync7(ownerDir, { recursive: true, mode: 448 });
|
|
@@ -5637,7 +5854,7 @@ function createManagedRepos(deps) {
|
|
|
5637
5854
|
const listed = await run(
|
|
5638
5855
|
["config", "--local", "--no-includes", "--list", "-z"],
|
|
5639
5856
|
path,
|
|
5640
|
-
|
|
5857
|
+
dirname8(path)
|
|
5641
5858
|
);
|
|
5642
5859
|
const entries2 = listed.code === 0 ? parseConfigList(listed.stdout) : [];
|
|
5643
5860
|
const mark = entries2.filter((e) => e.section === "cayenne" && e.subsection === null && e.name === "repositoryid").at(-1)?.value;
|
|
@@ -5662,8 +5879,8 @@ function createManagedRepos(deps) {
|
|
|
5662
5879
|
}
|
|
5663
5880
|
async function sync(repo, tok) {
|
|
5664
5881
|
const slug = `${repo.owner}/${repo.name}`;
|
|
5665
|
-
const ownerDir =
|
|
5666
|
-
const path =
|
|
5882
|
+
const ownerDir = join17(root, repo.owner);
|
|
5883
|
+
const path = join17(ownerDir, repo.name);
|
|
5667
5884
|
assertRealDirs(ownerDir, path);
|
|
5668
5885
|
const env = gitCredentialEnv(tok.token, "", { home: deps.gitHome });
|
|
5669
5886
|
const run = makeGitRun({ timeoutMs: GIT_SLOW_TIMEOUT_MS, env });
|
|
@@ -5703,9 +5920,9 @@ function createManagedRepos(deps) {
|
|
|
5703
5920
|
return { clonePath: real, defaultBranch: branch, baseSha };
|
|
5704
5921
|
}
|
|
5705
5922
|
function excludePlans(path) {
|
|
5706
|
-
const gitDir =
|
|
5707
|
-
const info =
|
|
5708
|
-
const file =
|
|
5923
|
+
const gitDir = join17(path, ".git");
|
|
5924
|
+
const info = join17(gitDir, "info");
|
|
5925
|
+
const file = join17(info, "exclude");
|
|
5709
5926
|
try {
|
|
5710
5927
|
if (!lstatSync4(gitDir).isDirectory()) throw new Error(".git is not a directory");
|
|
5711
5928
|
const infoStat = lstatSync4(info, { throwIfNoEntry: false });
|
|
@@ -5721,7 +5938,7 @@ function createManagedRepos(deps) {
|
|
|
5721
5938
|
420
|
|
5722
5939
|
);
|
|
5723
5940
|
try {
|
|
5724
|
-
const text =
|
|
5941
|
+
const text = readFileSync9(fd, "utf8");
|
|
5725
5942
|
const lines = text.split("\n");
|
|
5726
5943
|
const isOld = (line) => line.replace(/\r$/, "") === OLD_PLANS_EXCLUDE_LINE;
|
|
5727
5944
|
const has = lines.some((line) => line.replace(/\r$/, "") === PLANS_EXCLUDE_LINE);
|
|
@@ -5762,9 +5979,9 @@ function createManagedRepos(deps) {
|
|
|
5762
5979
|
}
|
|
5763
5980
|
async function markOf(path) {
|
|
5764
5981
|
const { code, stdout } = await scanRun(
|
|
5765
|
-
["config", "--file",
|
|
5982
|
+
["config", "--file", join17(path, ".git", "config"), "--get", ID_KEY],
|
|
5766
5983
|
path,
|
|
5767
|
-
|
|
5984
|
+
dirname8(path)
|
|
5768
5985
|
);
|
|
5769
5986
|
const text = stdout.trim();
|
|
5770
5987
|
if (code !== 0 || !ID_TEXT.test(text)) return null;
|
|
@@ -5791,8 +6008,8 @@ function createManagedRepos(deps) {
|
|
|
5791
6008
|
});
|
|
5792
6009
|
},
|
|
5793
6010
|
verifyConfig(repo) {
|
|
5794
|
-
const ownerDir =
|
|
5795
|
-
const path =
|
|
6011
|
+
const ownerDir = join17(root, repo.owner);
|
|
6012
|
+
const path = join17(ownerDir, repo.name);
|
|
5796
6013
|
const check = async () => {
|
|
5797
6014
|
assertRealDirs(ownerDir, path);
|
|
5798
6015
|
await checkConfig(repo, path, scanRun);
|
|
@@ -5813,12 +6030,12 @@ function createManagedRepos(deps) {
|
|
|
5813
6030
|
const candidates = [];
|
|
5814
6031
|
for (const owner of entries(root)) {
|
|
5815
6032
|
if (!owner.isDirectory()) continue;
|
|
5816
|
-
const ownerDir =
|
|
6033
|
+
const ownerDir = join17(root, owner.name);
|
|
5817
6034
|
for (const entry of entries(ownerDir)) {
|
|
5818
|
-
const path =
|
|
6035
|
+
const path = join17(ownerDir, entry.name);
|
|
5819
6036
|
if (TEMP_CLONE.test(entry.name)) {
|
|
5820
6037
|
if (inflight.has(path)) continue;
|
|
5821
|
-
if (!present(
|
|
6038
|
+
if (!present(join17(path, ".cayenne"))) {
|
|
5822
6039
|
rmSync6(path, { recursive: true, force: true });
|
|
5823
6040
|
deps.log.info({ path }, "removed an interrupted managed clone");
|
|
5824
6041
|
continue;
|
|
@@ -5894,7 +6111,7 @@ function keyText(e) {
|
|
|
5894
6111
|
return `${e.section}.${subsection}.${e.name}`;
|
|
5895
6112
|
}
|
|
5896
6113
|
function recency(path) {
|
|
5897
|
-
for (const file of [
|
|
6114
|
+
for (const file of [join17(path, ".git", "FETCH_HEAD"), join17(path, ".git")]) {
|
|
5898
6115
|
try {
|
|
5899
6116
|
const stat = statSync6(file, { throwIfNoEntry: false });
|
|
5900
6117
|
if (stat) return stat.mtimeMs;
|
|
@@ -6023,7 +6240,7 @@ var init_managed = __esm({
|
|
|
6023
6240
|
|
|
6024
6241
|
// src/repos/sweep.ts
|
|
6025
6242
|
import { lstatSync as lstatSync5, realpathSync as realpathSync10 } from "fs";
|
|
6026
|
-
import { isAbsolute as isAbsolute12, join as
|
|
6243
|
+
import { isAbsolute as isAbsolute12, join as join18, relative as relative5, sep as sep7 } from "path";
|
|
6027
6244
|
function createRepoWorktreeSweep(deps) {
|
|
6028
6245
|
const { log } = deps;
|
|
6029
6246
|
const said3 = /* @__PURE__ */ new Set();
|
|
@@ -6038,8 +6255,8 @@ function createRepoWorktreeSweep(deps) {
|
|
|
6038
6255
|
return null;
|
|
6039
6256
|
}
|
|
6040
6257
|
const leaf = worktreeName(sessionId);
|
|
6041
|
-
const repoRoot =
|
|
6042
|
-
if (leaf === null || rec.path !==
|
|
6258
|
+
const repoRoot = join18(deps.managedRoot, owner, name);
|
|
6259
|
+
if (leaf === null || rec.path !== join18(repoRoot, WORKTREES_DIR, leaf)) return null;
|
|
6043
6260
|
return { sessionId, path: rec.path, repoRoot, record: rec };
|
|
6044
6261
|
}
|
|
6045
6262
|
async function readRows(ids) {
|
|
@@ -6481,7 +6698,7 @@ var init_roster = __esm({
|
|
|
6481
6698
|
|
|
6482
6699
|
// src/sessions/attachments.ts
|
|
6483
6700
|
import { readdirSync as readdirSync2, rmSync as rmSync7 } from "fs";
|
|
6484
|
-
import { join as
|
|
6701
|
+
import { join as join19 } from "path";
|
|
6485
6702
|
async function loadAttachments(db, scope, attachments) {
|
|
6486
6703
|
const prefix = `${scope.workspaceId.toLowerCase()}/${scope.sessionId.toLowerCase()}/`;
|
|
6487
6704
|
const named = attachments.map(({ storagePath }, i) => {
|
|
@@ -6518,13 +6735,13 @@ async function loadAttachments(db, scope, attachments) {
|
|
|
6518
6735
|
return loaded;
|
|
6519
6736
|
}
|
|
6520
6737
|
function attachmentsTempDir(home, runnerId, sessionId) {
|
|
6521
|
-
return
|
|
6738
|
+
return join19(home, "tmp", "attachments", runnerId, sessionId);
|
|
6522
6739
|
}
|
|
6523
6740
|
function assertStagingInsideHome(home, dir) {
|
|
6524
6741
|
return assertInsideRootsOrAncestor(dir, [home]);
|
|
6525
6742
|
}
|
|
6526
6743
|
function clearAttachmentsTemp(home, runnerId, heldBy) {
|
|
6527
|
-
clearRunnerStaging(home,
|
|
6744
|
+
clearRunnerStaging(home, join19(home, "tmp", "attachments"), runnerId, heldBy);
|
|
6528
6745
|
}
|
|
6529
6746
|
function clearRunnerStaging(home, dir, runnerId, heldBy = (id) => holderOf({ root: home, runnerId: id }) !== null) {
|
|
6530
6747
|
const proved = assertStagingInsideHome(home, dir);
|
|
@@ -6535,9 +6752,9 @@ function clearRunnerStaging(home, dir, runnerId, heldBy = (id) => holderOf({ roo
|
|
|
6535
6752
|
return;
|
|
6536
6753
|
}
|
|
6537
6754
|
for (const name of names) {
|
|
6538
|
-
assertStagingInsideHome(home,
|
|
6755
|
+
assertStagingInsideHome(home, join19(proved, name));
|
|
6539
6756
|
if (name !== runnerId && heldBy(name)) continue;
|
|
6540
|
-
rmSync7(
|
|
6757
|
+
rmSync7(join19(proved, name), { recursive: true, force: true });
|
|
6541
6758
|
}
|
|
6542
6759
|
}
|
|
6543
6760
|
var ATTACHMENTS_BUCKET, TYPE_OF_EXT, refuse;
|
|
@@ -6557,7 +6774,7 @@ var init_attachments2 = __esm({
|
|
|
6557
6774
|
|
|
6558
6775
|
// src/fs/list.ts
|
|
6559
6776
|
import { readdirSync as readdirSync3, realpathSync as realpathSync11, statSync as statSync7 } from "fs";
|
|
6560
|
-
import { join as
|
|
6777
|
+
import { join as join20, relative as relative6, sep as sep8 } from "path";
|
|
6561
6778
|
function order(a, b) {
|
|
6562
6779
|
const aDir = a.kind === "dir" ? 0 : 1;
|
|
6563
6780
|
const bDir = b.kind === "dir" ? 0 : 1;
|
|
@@ -6598,7 +6815,7 @@ function listDirectory(dir, root, depth) {
|
|
|
6598
6815
|
return;
|
|
6599
6816
|
}
|
|
6600
6817
|
const level = dirents.map((dirent) => {
|
|
6601
|
-
const path =
|
|
6818
|
+
const path = join20(current, dirent.name);
|
|
6602
6819
|
const link = dirent.isSymbolicLink();
|
|
6603
6820
|
const resolved = link ? symlinkKind(path, root) : null;
|
|
6604
6821
|
const isDir = link ? resolved?.kind === "dir" : dirent.isDirectory();
|
|
@@ -7043,7 +7260,7 @@ var init_broadcast = __esm({
|
|
|
7043
7260
|
|
|
7044
7261
|
// src/documents/paths.ts
|
|
7045
7262
|
import { realpathSync as realpathSync12 } from "fs";
|
|
7046
|
-
import { isAbsolute as isAbsolute13, join as
|
|
7263
|
+
import { isAbsolute as isAbsolute13, join as join21, relative as relative7, sep as sep10 } from "path";
|
|
7047
7264
|
function contained(dir, candidate) {
|
|
7048
7265
|
const rel = relative7(dir, candidate);
|
|
7049
7266
|
return rel !== "" && rel !== ".." && !rel.startsWith(`..${sep10}`) && !isAbsolute13(rel);
|
|
@@ -7080,7 +7297,7 @@ var init_paths2 = __esm({
|
|
|
7080
7297
|
"src/documents/paths.ts"() {
|
|
7081
7298
|
"use strict";
|
|
7082
7299
|
init_src();
|
|
7083
|
-
docsDir = (cwd) =>
|
|
7300
|
+
docsDir = (cwd) => join21(cwd, ...DOCUMENTS_DIR.split("/"));
|
|
7084
7301
|
}
|
|
7085
7302
|
});
|
|
7086
7303
|
|
|
@@ -7160,10 +7377,10 @@ async function syncDocuments(deps, pass) {
|
|
|
7160
7377
|
if (error2) throw new Error(error2.message);
|
|
7161
7378
|
const row = data;
|
|
7162
7379
|
if (!row?.id) throw new Error("the insert returned no row");
|
|
7163
|
-
const
|
|
7164
|
-
if (
|
|
7380
|
+
const failed5 = await publish2(row, doc);
|
|
7381
|
+
if (failed5 !== null) {
|
|
7165
7382
|
log.warn(
|
|
7166
|
-
{ err:
|
|
7383
|
+
{ err: failed5, relPath: doc.relPath, sessionId: deps.sessionId },
|
|
7167
7384
|
"could not upload a document; removing the row so the next pass retries it"
|
|
7168
7385
|
);
|
|
7169
7386
|
await takeBack(row.id, doc.relPath);
|
|
@@ -7176,10 +7393,10 @@ async function syncDocuments(deps, pass) {
|
|
|
7176
7393
|
const restated = before.status !== "present" || before.title !== doc.title || before.kind !== doc.kind || before.size_bytes !== doc.size;
|
|
7177
7394
|
if (!rewritten && !restated) continue;
|
|
7178
7395
|
if (rewritten) {
|
|
7179
|
-
const
|
|
7180
|
-
if (
|
|
7396
|
+
const failed5 = await upload(before.storage_path, doc);
|
|
7397
|
+
if (failed5 !== null) {
|
|
7181
7398
|
log.warn(
|
|
7182
|
-
{ err:
|
|
7399
|
+
{ err: failed5, relPath: doc.relPath, sessionId: deps.sessionId },
|
|
7183
7400
|
"could not overwrite a document; its row still describes the bytes in the bucket"
|
|
7184
7401
|
);
|
|
7185
7402
|
continue;
|
|
@@ -7257,10 +7474,10 @@ import {
|
|
|
7257
7474
|
lstatSync as lstatSync6,
|
|
7258
7475
|
opendirSync,
|
|
7259
7476
|
openSync as openSync3,
|
|
7260
|
-
readFileSync as
|
|
7477
|
+
readFileSync as readFileSync10
|
|
7261
7478
|
} from "fs";
|
|
7262
7479
|
import { open } from "fs/promises";
|
|
7263
|
-
import { join as
|
|
7480
|
+
import { join as join22 } from "path";
|
|
7264
7481
|
function isNew(baseline, relPath, contentHash) {
|
|
7265
7482
|
const before = baseline.hashes.get(relPath);
|
|
7266
7483
|
if (before === void 0) return baseline.complete;
|
|
@@ -7347,7 +7564,7 @@ function walk2(st, dir, rel, depth) {
|
|
|
7347
7564
|
for (const entry of entries2) {
|
|
7348
7565
|
if (st.stop) return;
|
|
7349
7566
|
if (entry.name.startsWith(".")) continue;
|
|
7350
|
-
const path =
|
|
7567
|
+
const path = join22(dir, entry.name);
|
|
7351
7568
|
const relPath = rel === "" ? entry.name : `${rel}/${entry.name}`;
|
|
7352
7569
|
if (entry.isSymbolicLink()) {
|
|
7353
7570
|
st.log.warn({ path, relPath }, `document skipped: ${SYMLINK_SKIPPED}`);
|
|
@@ -7458,7 +7675,7 @@ function baselineOf(root, log) {
|
|
|
7458
7675
|
continue;
|
|
7459
7676
|
}
|
|
7460
7677
|
try {
|
|
7461
|
-
hashes.set(file.relPath, sha256(
|
|
7678
|
+
hashes.set(file.relPath, sha256(readFileSync10(fd)));
|
|
7462
7679
|
} catch {
|
|
7463
7680
|
hashes.set(file.relPath, UNREADABLE);
|
|
7464
7681
|
} finally {
|
|
@@ -7580,31 +7797,6 @@ var init_service = __esm({
|
|
|
7580
7797
|
}
|
|
7581
7798
|
});
|
|
7582
7799
|
|
|
7583
|
-
// src/providers/env.ts
|
|
7584
|
-
function definedEnv(env) {
|
|
7585
|
-
const out = {};
|
|
7586
|
-
for (const [key, value] of Object.entries(env)) if (value !== void 0) out[key] = value;
|
|
7587
|
-
return out;
|
|
7588
|
-
}
|
|
7589
|
-
var init_env = __esm({
|
|
7590
|
-
"src/providers/env.ts"() {
|
|
7591
|
-
"use strict";
|
|
7592
|
-
}
|
|
7593
|
-
});
|
|
7594
|
-
|
|
7595
|
-
// src/claude/auth-mode.ts
|
|
7596
|
-
function authModeFrom(info) {
|
|
7597
|
-
if (info.apiProvider && info.apiProvider !== "firstParty") return "third_party";
|
|
7598
|
-
if (info.apiKeySource && info.apiKeySource !== "none") return "api";
|
|
7599
|
-
if (info.subscriptionType) return "subscription";
|
|
7600
|
-
return null;
|
|
7601
|
-
}
|
|
7602
|
-
var init_auth_mode = __esm({
|
|
7603
|
-
"src/claude/auth-mode.ts"() {
|
|
7604
|
-
"use strict";
|
|
7605
|
-
}
|
|
7606
|
-
});
|
|
7607
|
-
|
|
7608
7800
|
// src/claude/normalize.ts
|
|
7609
7801
|
function normalizeSdkMessage(msg) {
|
|
7610
7802
|
switch (msg.type) {
|
|
@@ -7708,43 +7900,10 @@ var init_normalize = __esm({
|
|
|
7708
7900
|
}
|
|
7709
7901
|
});
|
|
7710
7902
|
|
|
7711
|
-
// src/claude/pushable.ts
|
|
7712
|
-
var Pushable;
|
|
7713
|
-
var init_pushable = __esm({
|
|
7714
|
-
"src/claude/pushable.ts"() {
|
|
7715
|
-
"use strict";
|
|
7716
|
-
Pushable = class {
|
|
7717
|
-
queue = [];
|
|
7718
|
-
waiters = [];
|
|
7719
|
-
ended = false;
|
|
7720
|
-
push(value) {
|
|
7721
|
-
if (this.ended) throw new Error("Pushable already ended");
|
|
7722
|
-
const waiter = this.waiters.shift();
|
|
7723
|
-
if (waiter) waiter({ value, done: false });
|
|
7724
|
-
else this.queue.push(value);
|
|
7725
|
-
}
|
|
7726
|
-
end() {
|
|
7727
|
-
this.ended = true;
|
|
7728
|
-
for (const waiter of this.waiters.splice(0)) waiter({ value: void 0, done: true });
|
|
7729
|
-
}
|
|
7730
|
-
[Symbol.asyncIterator]() {
|
|
7731
|
-
return {
|
|
7732
|
-
next: () => {
|
|
7733
|
-
const value = this.queue.shift();
|
|
7734
|
-
if (value !== void 0) return Promise.resolve({ value, done: false });
|
|
7735
|
-
if (this.ended) return Promise.resolve({ value: void 0, done: true });
|
|
7736
|
-
return new Promise((resolve6) => this.waiters.push(resolve6));
|
|
7737
|
-
}
|
|
7738
|
-
};
|
|
7739
|
-
}
|
|
7740
|
-
};
|
|
7741
|
-
}
|
|
7742
|
-
});
|
|
7743
|
-
|
|
7744
7903
|
// src/claude/session.ts
|
|
7745
7904
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
7746
7905
|
import { EventEmitter } from "events";
|
|
7747
|
-
import { query as
|
|
7906
|
+
import { query as sdkQuery2 } from "@anthropic-ai/claude-agent-sdk";
|
|
7748
7907
|
async function askGuard(guard, toolName2, input) {
|
|
7749
7908
|
try {
|
|
7750
7909
|
const refusal = await guard(toolName2, input);
|
|
@@ -7805,7 +7964,7 @@ var init_session2 = __esm({
|
|
|
7805
7964
|
stopAsked = false;
|
|
7806
7965
|
start() {
|
|
7807
7966
|
if (this.q) throw new Error("VALIDATION: session already started");
|
|
7808
|
-
const queryFn = this.opts.queryFn ??
|
|
7967
|
+
const queryFn = this.opts.queryFn ?? sdkQuery2;
|
|
7809
7968
|
const guard = this.opts.toolGuard;
|
|
7810
7969
|
const canUseTool = async (toolName2, input, options) => {
|
|
7811
7970
|
if (guard) {
|
|
@@ -8047,15 +8206,37 @@ ${this.stderrTail.join("\n")}` : "";
|
|
|
8047
8206
|
});
|
|
8048
8207
|
|
|
8049
8208
|
// src/codex/normalize.ts
|
|
8209
|
+
function moveTarget(kind) {
|
|
8210
|
+
const named = [kind.move_path, kind.movePath].filter((target) => target != null);
|
|
8211
|
+
if (named.length === 0) return void 0;
|
|
8212
|
+
const [first] = named;
|
|
8213
|
+
return typeof first === "string" && named.every((target) => target === first) ? first : "";
|
|
8214
|
+
}
|
|
8215
|
+
function fileChanges(item) {
|
|
8216
|
+
if (!Array.isArray(item.changes)) return [];
|
|
8217
|
+
return item.changes.map((entry) => {
|
|
8218
|
+
const change = isRecord(entry) ? entry : {};
|
|
8219
|
+
const kind = isRecord(change.kind) ? change.kind : {};
|
|
8220
|
+
const movePath = moveTarget(kind);
|
|
8221
|
+
return {
|
|
8222
|
+
path: typeof change.path === "string" ? change.path : "",
|
|
8223
|
+
...typeof kind.type === "string" ? { kind: kind.type } : {},
|
|
8224
|
+
...typeof change.diff === "string" ? { diff: change.diff } : {},
|
|
8225
|
+
...movePath === void 0 ? {} : { movePath }
|
|
8226
|
+
};
|
|
8227
|
+
});
|
|
8228
|
+
}
|
|
8050
8229
|
function normalizeCodexItem(item, phase) {
|
|
8051
8230
|
const base = { providerUuid: item.id, parentToolUseId: null, raw: item };
|
|
8052
|
-
if (item.type === "
|
|
8231
|
+
if (item.type === "agentMessage") {
|
|
8053
8232
|
if (phase !== "completed") return null;
|
|
8054
8233
|
return { role: "assistant", content: [{ type: "text", text: item.text ?? "" }], ...base };
|
|
8055
8234
|
}
|
|
8056
8235
|
if (item.type === "reasoning") {
|
|
8057
8236
|
if (phase !== "completed") return null;
|
|
8058
|
-
|
|
8237
|
+
const text = (Array.isArray(item.summary) ? item.summary : []).join("\n\n");
|
|
8238
|
+
if (!text) return null;
|
|
8239
|
+
return { role: "assistant", content: [{ type: "thinking", text }], ...base };
|
|
8059
8240
|
}
|
|
8060
8241
|
const name = toolName(item);
|
|
8061
8242
|
if (!name) return null;
|
|
@@ -8063,12 +8244,11 @@ function normalizeCodexItem(item, phase) {
|
|
|
8063
8244
|
const block2 = { type: "tool_use", id: item.id, name, input: toolInput(item) };
|
|
8064
8245
|
return { role: "assistant", content: [block2], ...base };
|
|
8065
8246
|
}
|
|
8066
|
-
const isError2 = item.status === "failed" || item.exit_code !== void 0 && item.exit_code !== 0;
|
|
8067
8247
|
const block = {
|
|
8068
8248
|
type: "tool_result",
|
|
8069
8249
|
toolUseId: item.id,
|
|
8070
8250
|
content: resultText(item),
|
|
8071
|
-
isError:
|
|
8251
|
+
isError: failed4(item)
|
|
8072
8252
|
};
|
|
8073
8253
|
return { role: "tool", content: [block], ...base };
|
|
8074
8254
|
}
|
|
@@ -8089,20 +8269,21 @@ function deltaEvents(_itemId, kind, text, first) {
|
|
|
8089
8269
|
function stopEvents() {
|
|
8090
8270
|
return [{ type: "content_block_stop", index: 0 }, { type: "message_stop" }];
|
|
8091
8271
|
}
|
|
8092
|
-
var toolName, toolInput, resultText;
|
|
8272
|
+
var isRecord, toolName, toolInput, resultText, failed4;
|
|
8093
8273
|
var init_normalize2 = __esm({
|
|
8094
8274
|
"src/codex/normalize.ts"() {
|
|
8095
8275
|
"use strict";
|
|
8096
8276
|
init_src();
|
|
8277
|
+
isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
8097
8278
|
toolName = (item) => {
|
|
8098
8279
|
switch (item.type) {
|
|
8099
|
-
case "
|
|
8280
|
+
case "commandExecution":
|
|
8100
8281
|
return CODEX_TOOL_NAMES.command;
|
|
8101
|
-
case "
|
|
8282
|
+
case "fileChange":
|
|
8102
8283
|
return CODEX_TOOL_NAMES.fileChange;
|
|
8103
|
-
case "
|
|
8284
|
+
case "webSearch":
|
|
8104
8285
|
return CODEX_TOOL_NAMES.webSearch;
|
|
8105
|
-
case "
|
|
8286
|
+
case "mcpToolCall":
|
|
8106
8287
|
return `codex.mcp.${(item.server ?? "server").replace(/[^A-Za-z0-9_.-]/g, "")}.${(item.tool ?? "tool").replace(/[^A-Za-z0-9_.-]/g, "")}`.slice(
|
|
8107
8288
|
0,
|
|
8108
8289
|
64
|
|
@@ -8113,21 +8294,25 @@ var init_normalize2 = __esm({
|
|
|
8113
8294
|
};
|
|
8114
8295
|
toolInput = (item) => {
|
|
8115
8296
|
switch (item.type) {
|
|
8116
|
-
case "
|
|
8297
|
+
case "commandExecution":
|
|
8117
8298
|
return { command: item.command ?? "", ...item.cwd ? { cwd: item.cwd } : {} };
|
|
8118
|
-
case "
|
|
8119
|
-
return { changes: item
|
|
8120
|
-
case "
|
|
8299
|
+
case "fileChange":
|
|
8300
|
+
return { changes: fileChanges(item) };
|
|
8301
|
+
case "webSearch":
|
|
8121
8302
|
return { query: item.query ?? "" };
|
|
8122
8303
|
default:
|
|
8123
|
-
|
|
8304
|
+
if (isRecord(item.arguments)) return item.arguments;
|
|
8305
|
+
return item.arguments === void 0 ? {} : { arguments: item.arguments };
|
|
8124
8306
|
}
|
|
8125
8307
|
};
|
|
8126
8308
|
resultText = (item) => {
|
|
8127
|
-
if (item.type === "
|
|
8128
|
-
if (item.type === "
|
|
8309
|
+
if (item.type === "commandExecution") return item.aggregatedOutput ?? "";
|
|
8310
|
+
if (item.type === "fileChange") return JSON.stringify(fileChanges(item));
|
|
8311
|
+
if (item.type === "webSearch") return JSON.stringify(item.action ?? null);
|
|
8312
|
+
if (item.error?.message) return item.error.message;
|
|
8129
8313
|
return typeof item.result === "string" ? item.result : JSON.stringify(item.result ?? null);
|
|
8130
8314
|
};
|
|
8315
|
+
failed4 = (item) => item.status === "failed" || item.status === "declined" || Boolean(item.error) || typeof item.exitCode === "number" && item.exitCode !== 0;
|
|
8131
8316
|
}
|
|
8132
8317
|
});
|
|
8133
8318
|
|
|
@@ -8142,6 +8327,9 @@ function sandboxFor(mode, cwd) {
|
|
|
8142
8327
|
return { type: "workspaceWrite", writableRoots: [cwd], networkAccess: false };
|
|
8143
8328
|
}
|
|
8144
8329
|
}
|
|
8330
|
+
function sandboxMode(policy) {
|
|
8331
|
+
return SANDBOX_MODES[policy.type];
|
|
8332
|
+
}
|
|
8145
8333
|
function autoDecide(mode, kind) {
|
|
8146
8334
|
if (mode === "bypassPermissions") return "accept";
|
|
8147
8335
|
if (mode === "dontAsk") return "decline";
|
|
@@ -8149,9 +8337,15 @@ function autoDecide(mode, kind) {
|
|
|
8149
8337
|
if (mode === "acceptEdits" && kind === "file_change") return "accept";
|
|
8150
8338
|
return "ask";
|
|
8151
8339
|
}
|
|
8340
|
+
var SANDBOX_MODES;
|
|
8152
8341
|
var init_policy = __esm({
|
|
8153
8342
|
"src/codex/policy.ts"() {
|
|
8154
8343
|
"use strict";
|
|
8344
|
+
SANDBOX_MODES = {
|
|
8345
|
+
readOnly: "read-only",
|
|
8346
|
+
workspaceWrite: "workspace-write",
|
|
8347
|
+
dangerFullAccess: "danger-full-access"
|
|
8348
|
+
};
|
|
8155
8349
|
}
|
|
8156
8350
|
});
|
|
8157
8351
|
|
|
@@ -8160,7 +8354,8 @@ import { spawn as spawn2 } from "child_process";
|
|
|
8160
8354
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
8161
8355
|
import { EventEmitter as EventEmitter2 } from "events";
|
|
8162
8356
|
import { chmodSync as chmodSync5, lstatSync as lstatSync7, mkdirSync as mkdirSync8, rmdirSync, rmSync as rmSync8, writeFileSync as writeFileSync6 } from "fs";
|
|
8163
|
-
import { dirname as
|
|
8357
|
+
import { dirname as dirname9, join as join23 } from "path";
|
|
8358
|
+
import { z as z18 } from "zod";
|
|
8164
8359
|
function newStop() {
|
|
8165
8360
|
let resolve6;
|
|
8166
8361
|
let reject;
|
|
@@ -8174,10 +8369,35 @@ function newStop() {
|
|
|
8174
8369
|
function removeFiles(files) {
|
|
8175
8370
|
for (const file of files) rmSync8(file, { force: true });
|
|
8176
8371
|
}
|
|
8372
|
+
function readQuestions(params) {
|
|
8373
|
+
const asked = AskedQuestions.safeParse(params);
|
|
8374
|
+
if (!asked.success) return null;
|
|
8375
|
+
const { questions } = asked.data;
|
|
8376
|
+
const ids = questions.map((q) => q.id);
|
|
8377
|
+
if (new Set(ids).size !== ids.length || questions.some((q) => q.isSecret)) return null;
|
|
8378
|
+
const input = CodexQuestions.safeParse({
|
|
8379
|
+
questions: questions.map((q) => ({
|
|
8380
|
+
text: q.question,
|
|
8381
|
+
...q.options?.length ? { options: q.options.map((o) => o.label) } : {}
|
|
8382
|
+
}))
|
|
8383
|
+
});
|
|
8384
|
+
return input.success ? { ids, input: input.data } : null;
|
|
8385
|
+
}
|
|
8177
8386
|
function scrubSecrets(text) {
|
|
8178
8387
|
return SECRET_PATTERNS.reduce((acc, [pattern, to]) => acc.replace(pattern, to), text);
|
|
8179
8388
|
}
|
|
8180
|
-
|
|
8389
|
+
function failureText(error) {
|
|
8390
|
+
const message = error?.message;
|
|
8391
|
+
if (typeof message !== "string" || !message.trim()) return null;
|
|
8392
|
+
let text = message;
|
|
8393
|
+
try {
|
|
8394
|
+
const inner = JSON.parse(message)?.error?.message;
|
|
8395
|
+
if (typeof inner === "string" && inner.trim()) text = inner;
|
|
8396
|
+
} catch {
|
|
8397
|
+
}
|
|
8398
|
+
return scrubSecrets(text).trim().slice(0, TURN_ERROR_MAX);
|
|
8399
|
+
}
|
|
8400
|
+
var decided, NO_ANSWERS, REFUSALS, METHOD_NOT_FOUND, AskedQuestions, STDERR_TAIL_MAX, STOP_WAIT_MS, TURN_ERROR_MAX, SIGKILL_AFTER_MS, SECRET_PATTERNS, APPROVAL_METHODS, CodexSession;
|
|
8181
8401
|
var init_session3 = __esm({
|
|
8182
8402
|
"src/codex/session.ts"() {
|
|
8183
8403
|
"use strict";
|
|
@@ -8187,8 +8407,27 @@ var init_session3 = __esm({
|
|
|
8187
8407
|
init_normalize2();
|
|
8188
8408
|
init_policy();
|
|
8189
8409
|
init_rpc();
|
|
8410
|
+
decided = (decision) => ({ decision });
|
|
8411
|
+
NO_ANSWERS = { answers: {} };
|
|
8412
|
+
REFUSALS = {
|
|
8413
|
+
"item/permissions/requestApproval": { permissions: {} },
|
|
8414
|
+
"mcpServer/elicitation/request": { action: "decline" },
|
|
8415
|
+
"item/tool/call": { contentItems: [], success: false }
|
|
8416
|
+
};
|
|
8417
|
+
METHOD_NOT_FOUND = -32601;
|
|
8418
|
+
AskedQuestions = z18.object({
|
|
8419
|
+
questions: z18.array(
|
|
8420
|
+
z18.object({
|
|
8421
|
+
id: z18.string().min(1),
|
|
8422
|
+
question: z18.string(),
|
|
8423
|
+
isSecret: z18.boolean().nullish(),
|
|
8424
|
+
options: z18.array(z18.object({ label: z18.string() })).nullish()
|
|
8425
|
+
})
|
|
8426
|
+
)
|
|
8427
|
+
});
|
|
8190
8428
|
STDERR_TAIL_MAX = 2 * 1024;
|
|
8191
8429
|
STOP_WAIT_MS = 2e3;
|
|
8430
|
+
TURN_ERROR_MAX = 500;
|
|
8192
8431
|
SIGKILL_AFTER_MS = 5e3;
|
|
8193
8432
|
SECRET_PATTERNS = [
|
|
8194
8433
|
[
|
|
@@ -8234,8 +8473,17 @@ var init_session3 = __esm({
|
|
|
8234
8473
|
resolvers = /* @__PURE__ */ new Map();
|
|
8235
8474
|
/** Monotonic, so no two held requests can ever share a local id. */
|
|
8236
8475
|
approvalSeq = 0;
|
|
8237
|
-
/**
|
|
8238
|
-
|
|
8476
|
+
/**
|
|
8477
|
+
* The app-server's id of each question a pending `codex.user_input` asked, in the order the
|
|
8478
|
+
* card shows them: an answer goes back under its question's id.
|
|
8479
|
+
*/
|
|
8480
|
+
questionIds = /* @__PURE__ */ new Map();
|
|
8481
|
+
/**
|
|
8482
|
+
* The files of each `fileChange` item in progress, by item id. `item/fileChange/requestApproval`
|
|
8483
|
+
* names the item and not its files: those came with `item/started`, which the app-server sends
|
|
8484
|
+
* first (live 2026-10-13), and with any `item/fileChange/patchUpdated` after.
|
|
8485
|
+
*/
|
|
8486
|
+
fileChanges = /* @__PURE__ */ new Map();
|
|
8239
8487
|
start() {
|
|
8240
8488
|
if (this.child) throw new Error("VALIDATION: session already started");
|
|
8241
8489
|
const spawnFn = this.opts.spawnFn ?? ((p, a, env) => spawn2(p, a, { stdio: "pipe", env }));
|
|
@@ -8255,13 +8503,19 @@ var init_session3 = __esm({
|
|
|
8255
8503
|
${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
|
|
8256
8504
|
const error = this.stopping || code === 0 ? null : `PROVIDER_ERROR: codex exited with ${code}${tail}`;
|
|
8257
8505
|
this.removeAllFiles();
|
|
8506
|
+
this.withdrawAll();
|
|
8258
8507
|
this.emit("exit", { error });
|
|
8259
8508
|
});
|
|
8260
8509
|
const rpc = new JsonRpc(child.stdin, child.stdout);
|
|
8261
8510
|
this.rpc = rpc;
|
|
8262
|
-
rpc.onNotification(
|
|
8263
|
-
|
|
8511
|
+
rpc.onNotification(
|
|
8512
|
+
(n) => this.onNotification(n.method, n.params ?? {})
|
|
8513
|
+
);
|
|
8514
|
+
rpc.onRequest(
|
|
8515
|
+
(r) => this.onRequest(r.id, r.method, r.params ?? {})
|
|
8516
|
+
);
|
|
8264
8517
|
void this.handshake().catch((err) => {
|
|
8518
|
+
this.withdrawAll();
|
|
8265
8519
|
this.emit("exit", {
|
|
8266
8520
|
error: `PROVIDER_ERROR: ${err instanceof Error ? err.message : String(err)}`
|
|
8267
8521
|
});
|
|
@@ -8277,17 +8531,24 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
|
|
|
8277
8531
|
const params = {
|
|
8278
8532
|
cwd: this.opts.cwd,
|
|
8279
8533
|
approvalPolicy: "on-request",
|
|
8280
|
-
|
|
8534
|
+
// The mode word: opening a thread refuses a policy object (`unknown variant `type``, live
|
|
8535
|
+
// 2026-10-13). The policy itself, with its writable root, goes with every `turn/start`.
|
|
8536
|
+
sandbox: sandboxMode(this.sandbox),
|
|
8281
8537
|
...this.pendingModel ? { model: this.pendingModel } : {}
|
|
8282
8538
|
};
|
|
8283
8539
|
const res = this.opts.resume ? await rpc.request("thread/resume", {
|
|
8284
8540
|
threadId: this.opts.resume,
|
|
8285
8541
|
...params
|
|
8286
|
-
}) : await rpc.request("thread/start",
|
|
8287
|
-
|
|
8542
|
+
}) : await rpc.request("thread/start", {
|
|
8543
|
+
...params,
|
|
8544
|
+
...this.opts.ephemeral ? { ephemeral: true } : {}
|
|
8545
|
+
});
|
|
8546
|
+
const threadId = res?.thread?.id;
|
|
8547
|
+
if (!threadId) throw new Error("codex app-server opened no thread");
|
|
8548
|
+
this.providerSessionId = threadId;
|
|
8288
8549
|
this.emit("init", {
|
|
8289
|
-
providerSessionId:
|
|
8290
|
-
model: res
|
|
8550
|
+
providerSessionId: threadId,
|
|
8551
|
+
model: res?.model ?? this.pendingModel ?? "",
|
|
8291
8552
|
tools: [],
|
|
8292
8553
|
slashCommands: [],
|
|
8293
8554
|
permissionMode: this.mode
|
|
@@ -8325,7 +8586,7 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
|
|
|
8325
8586
|
assertStagingInsideHome(home, dir);
|
|
8326
8587
|
mkdirSync8(dir, { recursive: true, mode: 448 });
|
|
8327
8588
|
assertStagingInsideHome(home, dir);
|
|
8328
|
-
for (const path of [
|
|
8589
|
+
for (const path of [dirname9(dir), dir]) {
|
|
8329
8590
|
const st = lstatSync7(path);
|
|
8330
8591
|
if (st.isSymbolicLink() || !st.isDirectory()) {
|
|
8331
8592
|
throw new Error(`${path} is not a plain directory`);
|
|
@@ -8333,7 +8594,7 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
|
|
|
8333
8594
|
}
|
|
8334
8595
|
chmodSync5(dir, 448);
|
|
8335
8596
|
for (const image of images) {
|
|
8336
|
-
const path =
|
|
8597
|
+
const path = join23(dir, `${randomUUID4()}.${ATTACHMENT_MEDIA_TYPES[image.mediaType]}`);
|
|
8337
8598
|
writeFileSync6(path, image.data, { mode: 384, flag: "wx" });
|
|
8338
8599
|
written.push(path);
|
|
8339
8600
|
}
|
|
@@ -8353,6 +8614,7 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
|
|
|
8353
8614
|
if (!turn) return;
|
|
8354
8615
|
this.turn = null;
|
|
8355
8616
|
removeFiles(turn.files);
|
|
8617
|
+
this.fileChanges.clear();
|
|
8356
8618
|
turn.stop?.resolve();
|
|
8357
8619
|
}
|
|
8358
8620
|
removeAllFiles() {
|
|
@@ -8399,7 +8661,11 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
|
|
|
8399
8661
|
isError: true,
|
|
8400
8662
|
durationMs: 0,
|
|
8401
8663
|
numTurns: 1,
|
|
8402
|
-
|
|
8664
|
+
// The app-server's own words, broadcast like a failed turn's: scrubbed and kept short.
|
|
8665
|
+
stopReason: scrubSecrets(err instanceof Error ? err.message : String(err)).slice(
|
|
8666
|
+
0,
|
|
8667
|
+
TURN_ERROR_MAX
|
|
8668
|
+
)
|
|
8403
8669
|
});
|
|
8404
8670
|
});
|
|
8405
8671
|
}
|
|
@@ -8469,6 +8735,10 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
|
|
|
8469
8735
|
const item = params.item;
|
|
8470
8736
|
if (!item) return;
|
|
8471
8737
|
const phase = method === "item/started" ? "started" : "completed";
|
|
8738
|
+
if (item.type === "fileChange") {
|
|
8739
|
+
if (phase === "started") this.setFiles(item.id, fileChanges(item));
|
|
8740
|
+
else this.fileChanges.delete(item.id);
|
|
8741
|
+
}
|
|
8472
8742
|
if (phase === "completed" && this.streaming.delete(item.id)) {
|
|
8473
8743
|
for (const event of stopEvents())
|
|
8474
8744
|
this.emit("stream", { event, parentToolUseId: null, uuid: item.id });
|
|
@@ -8477,15 +8747,32 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
|
|
|
8477
8747
|
if (row) this.emit("message", row);
|
|
8478
8748
|
return;
|
|
8479
8749
|
}
|
|
8480
|
-
case "
|
|
8481
|
-
|
|
8750
|
+
case "item/fileChange/patchUpdated":
|
|
8751
|
+
if (typeof params.itemId === "string" && this.fileChanges.has(params.itemId)) {
|
|
8752
|
+
this.setFiles(params.itemId, fileChanges(params));
|
|
8753
|
+
}
|
|
8754
|
+
return;
|
|
8755
|
+
case "serverRequest/resolved":
|
|
8756
|
+
for (const [id, entry] of [...this.approvals]) {
|
|
8757
|
+
if (entry.rpcId === params.requestId) this.withdraw(id);
|
|
8758
|
+
}
|
|
8759
|
+
return;
|
|
8760
|
+
case "turn/completed": {
|
|
8482
8761
|
this.endTurn();
|
|
8483
|
-
const
|
|
8762
|
+
const ended = turn?.id ?? params.turnId;
|
|
8763
|
+
for (const [id, entry] of [...this.approvals]) {
|
|
8764
|
+
if (typeof ended !== "string" || entry.turnId === null || entry.turnId === ended) {
|
|
8765
|
+
this.withdraw(id);
|
|
8766
|
+
}
|
|
8767
|
+
}
|
|
8768
|
+
const status = turn?.status ?? params.status ?? "completed";
|
|
8769
|
+
const isError2 = status === "failed";
|
|
8484
8770
|
this.emit("result", {
|
|
8485
|
-
isError:
|
|
8771
|
+
isError: isError2,
|
|
8486
8772
|
durationMs: turn?.durationMs ?? params.durationMs ?? 0,
|
|
8487
8773
|
numTurns: 1,
|
|
8488
|
-
|
|
8774
|
+
// A failed turn says why, when the app-server did.
|
|
8775
|
+
stopReason: isError2 && failureText(turn?.error) || status
|
|
8489
8776
|
});
|
|
8490
8777
|
this.flush();
|
|
8491
8778
|
return;
|
|
@@ -8494,6 +8781,45 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
|
|
|
8494
8781
|
return;
|
|
8495
8782
|
}
|
|
8496
8783
|
}
|
|
8784
|
+
/**
|
|
8785
|
+
* The files of a `fileChange` item, as last told. A card already up for that item shows the
|
|
8786
|
+
* files as they were, and the roots check passed those: it is withdrawn and the approval
|
|
8787
|
+
* declined, since the patch an answer would now allow is not the one the owner is reading.
|
|
8788
|
+
*/
|
|
8789
|
+
setFiles(itemId, changes) {
|
|
8790
|
+
const before = this.fileChanges.get(itemId);
|
|
8791
|
+
this.fileChanges.set(itemId, changes);
|
|
8792
|
+
if (before === void 0 || JSON.stringify(before) === JSON.stringify(changes)) return;
|
|
8793
|
+
for (const [id, entry] of [...this.approvals]) {
|
|
8794
|
+
if (entry.kind === "file_change" && entry.itemId === itemId) this.withdraw(id);
|
|
8795
|
+
}
|
|
8796
|
+
}
|
|
8797
|
+
/**
|
|
8798
|
+
* Take back a held request no answer can reach any more: nobody answered in time, its files
|
|
8799
|
+
* changed under the card, the app-server settled it itself, its turn ended, the session is
|
|
8800
|
+
* stopping, or the app-server is gone. The request is answered with its own refusal (an approval
|
|
8801
|
+
* declines; a question's response is its answers, of which there are none), and
|
|
8802
|
+
* `permissionTimeout` has the manager expire the card's row, after which an answer to it is
|
|
8803
|
+
* refused. One place, so no way of withdrawing can leave behind what another clears.
|
|
8804
|
+
*/
|
|
8805
|
+
withdraw(id) {
|
|
8806
|
+
const entry = this.approvals.get(id);
|
|
8807
|
+
if (!entry) return;
|
|
8808
|
+
clearTimeout(entry.timer);
|
|
8809
|
+
this.approvals.delete(id);
|
|
8810
|
+
this.questionIds.delete(id);
|
|
8811
|
+
this.resolvers.get(id)?.(entry.refusal);
|
|
8812
|
+
this.resolvers.delete(id);
|
|
8813
|
+
this.emit("permissionTimeout", { id });
|
|
8814
|
+
}
|
|
8815
|
+
/**
|
|
8816
|
+
* Every held request, when nothing is left to answer: the session is stopping, the app-server
|
|
8817
|
+
* has exited, or it never opened a thread. The refusals are written if there is still a pipe to
|
|
8818
|
+
* write them to; into one that is gone they are dropped, without a throw (`rpc.test.ts`).
|
|
8819
|
+
*/
|
|
8820
|
+
withdrawAll() {
|
|
8821
|
+
for (const id of [...this.approvals.keys()]) this.withdraw(id);
|
|
8822
|
+
}
|
|
8497
8823
|
delta(params, kind, field) {
|
|
8498
8824
|
const itemId = params.itemId ?? "";
|
|
8499
8825
|
const text = params[field] ?? "";
|
|
@@ -8509,27 +8835,43 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
|
|
|
8509
8835
|
* The reply is held until a human answers or the timer fires, which is what makes a Codex chat
|
|
8510
8836
|
* behave like a Claude one — the SDK's `canUseTool` blocks the same way.
|
|
8511
8837
|
*/
|
|
8512
|
-
onRequest(method, params) {
|
|
8513
|
-
|
|
8514
|
-
|
|
8515
|
-
|
|
8516
|
-
|
|
8517
|
-
|
|
8518
|
-
|
|
8519
|
-
|
|
8520
|
-
|
|
8521
|
-
|
|
8838
|
+
onRequest(rpcId, method, params) {
|
|
8839
|
+
const from = { rpcId, turnId: typeof params.turnId === "string" ? params.turnId : null };
|
|
8840
|
+
if (method === "item/tool/requestUserInput") return this.askQuestion(from, params);
|
|
8841
|
+
const kind = Object.hasOwn(APPROVAL_METHODS, method) ? APPROVAL_METHODS[method] : void 0;
|
|
8842
|
+
if (!kind) {
|
|
8843
|
+
if (Object.hasOwn(REFUSALS, method)) return REFUSALS[method];
|
|
8844
|
+
throw new RpcError(METHOD_NOT_FOUND, `cayenne does not handle ${method}`);
|
|
8845
|
+
}
|
|
8846
|
+
const itemId = typeof params.itemId === "string" ? params.itemId : null;
|
|
8847
|
+
const reason = params.reason ? { reason: params.reason } : {};
|
|
8848
|
+
let toolName2;
|
|
8849
|
+
let input;
|
|
8850
|
+
if (kind === "command") {
|
|
8851
|
+
toolName2 = CODEX_TOOL_NAMES.command;
|
|
8852
|
+
const { command, cwd } = params;
|
|
8853
|
+
if ((params.kind ?? "command") !== "command" || params.networkApprovalContext != null || typeof command !== "string" || command.trim() === "" || cwd != null && typeof cwd !== "string") {
|
|
8854
|
+
return decided("decline");
|
|
8855
|
+
}
|
|
8856
|
+
input = { command, cwd: cwd ?? this.opts.cwd, ...reason };
|
|
8857
|
+
} else {
|
|
8858
|
+
toolName2 = CODEX_TOOL_NAMES.fileChange;
|
|
8859
|
+
if (params.grantRoot != null) return decided("decline");
|
|
8860
|
+
const changes = (itemId ? this.fileChanges.get(itemId) : void 0) ?? [];
|
|
8861
|
+
if (changes.length === 0) return decided("decline");
|
|
8862
|
+
input = { changes, ...reason };
|
|
8863
|
+
}
|
|
8522
8864
|
try {
|
|
8523
|
-
this.opts.checkInput
|
|
8865
|
+
this.opts.checkInput(toolName2, input);
|
|
8524
8866
|
} catch {
|
|
8525
|
-
return "decline";
|
|
8867
|
+
return decided("decline");
|
|
8526
8868
|
}
|
|
8527
8869
|
const decision = autoDecide(this.mode, kind);
|
|
8528
|
-
if (decision !== "ask") return decision;
|
|
8529
|
-
return this.hold(this.nextApprovalId(kind), kind, "decline", {
|
|
8870
|
+
if (decision !== "ask") return decided(decision);
|
|
8871
|
+
return this.hold(this.nextApprovalId(kind), kind, decided("decline"), from, {
|
|
8530
8872
|
toolName: toolName2,
|
|
8531
8873
|
input,
|
|
8532
|
-
toolUseId:
|
|
8874
|
+
toolUseId: itemId
|
|
8533
8875
|
});
|
|
8534
8876
|
}
|
|
8535
8877
|
/**
|
|
@@ -8539,15 +8881,20 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
|
|
|
8539
8881
|
* same answer RPC. The shape is validated here, before a row exists — a malformed or oversized
|
|
8540
8882
|
* ask is cancelled rather than rendered, because the questions are text the model produced and
|
|
8541
8883
|
* the card is the one surface where a human is asked to trust what they read.
|
|
8542
|
-
|
|
8543
|
-
|
|
8544
|
-
|
|
8545
|
-
|
|
8884
|
+
*
|
|
8885
|
+
* The app-server's question (`{ id, header, question, options: { label, description }[] }`) is
|
|
8886
|
+
* not the one Cayenne stores (`CodexQuestions`: `{ text, options: string[] }`), and its answer is
|
|
8887
|
+
* keyed by question id where `CodexAnswers` is a list in order. The adapter translates both
|
|
8888
|
+
* ways, so the row, the card and the answer RPC are what they were.
|
|
8889
|
+
*/
|
|
8890
|
+
askQuestion(from, params) {
|
|
8891
|
+
const asked = readQuestions(params);
|
|
8892
|
+
if (!asked) return Promise.resolve(NO_ANSWERS);
|
|
8546
8893
|
const id = this.nextApprovalId("question");
|
|
8547
|
-
this.
|
|
8548
|
-
return this.hold(id, "question",
|
|
8894
|
+
this.questionIds.set(id, asked.ids);
|
|
8895
|
+
return this.hold(id, "question", NO_ANSWERS, from, {
|
|
8549
8896
|
toolName: CODEX_TOOL_NAMES.userInput,
|
|
8550
|
-
input:
|
|
8897
|
+
input: asked.input,
|
|
8551
8898
|
toolUseId: null
|
|
8552
8899
|
});
|
|
8553
8900
|
}
|
|
@@ -8564,20 +8911,21 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
|
|
|
8564
8911
|
this.approvalSeq += 1;
|
|
8565
8912
|
return `${kind}-${this.approvalSeq}`;
|
|
8566
8913
|
}
|
|
8567
|
-
/**
|
|
8568
|
-
|
|
8914
|
+
/**
|
|
8915
|
+
* Park a server request until a human answers it, the timer gives up on them, or it is
|
|
8916
|
+
* withdrawn (`withdraw`).
|
|
8917
|
+
*/
|
|
8918
|
+
hold(id, kind, onTimeout, from, req) {
|
|
8569
8919
|
if (this.approvals.has(id) || this.resolvers.has(id)) {
|
|
8570
8920
|
throw new Error(`VALIDATION: approval id ${id} is already held`);
|
|
8571
8921
|
}
|
|
8572
8922
|
return new Promise((resolve6) => {
|
|
8573
|
-
const timer = setTimeout(
|
|
8574
|
-
this.
|
|
8575
|
-
this.
|
|
8576
|
-
|
|
8577
|
-
|
|
8578
|
-
|
|
8579
|
-
}, this.opts.permissionTimeoutMs ?? PERMISSION_TIMEOUT_MS);
|
|
8580
|
-
this.approvals.set(id, { rpcId: 0, kind, refusal: onTimeout, timer });
|
|
8923
|
+
const timer = setTimeout(
|
|
8924
|
+
() => this.withdraw(id),
|
|
8925
|
+
this.opts.permissionTimeoutMs ?? PERMISSION_TIMEOUT_MS
|
|
8926
|
+
);
|
|
8927
|
+
const itemId = kind === "question" ? null : req.toolUseId;
|
|
8928
|
+
this.approvals.set(id, { ...from, kind, refusal: onTimeout, itemId, timer });
|
|
8581
8929
|
this.resolvers.set(id, resolve6);
|
|
8582
8930
|
this.emit("permission", { id, ...req, suggestions: null });
|
|
8583
8931
|
});
|
|
@@ -8588,20 +8936,24 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
|
|
|
8588
8936
|
if (!entry || !resolve6) return false;
|
|
8589
8937
|
let reply;
|
|
8590
8938
|
if (entry.kind === "question") {
|
|
8591
|
-
if (decision.behavior === "deny") reply =
|
|
8939
|
+
if (decision.behavior === "deny") reply = NO_ANSWERS;
|
|
8592
8940
|
else {
|
|
8941
|
+
const ids = this.questionIds.get(id) ?? [];
|
|
8593
8942
|
const answers = CodexAnswers.safeParse(decision.updatedInput ?? {});
|
|
8594
|
-
if (!answers.success || answers.data.answers.length !==
|
|
8595
|
-
|
|
8596
|
-
|
|
8943
|
+
if (!answers.success || answers.data.answers.length !== ids.length) return false;
|
|
8944
|
+
reply = {
|
|
8945
|
+
answers: Object.fromEntries(
|
|
8946
|
+
ids.map((questionId, n) => [questionId, { answers: [answers.data.answers[n] ?? ""] }])
|
|
8947
|
+
)
|
|
8948
|
+
};
|
|
8597
8949
|
}
|
|
8598
8950
|
} else {
|
|
8599
|
-
reply = decision.behavior === "allow" ? "accept" : "decline";
|
|
8951
|
+
reply = decided(decision.behavior === "allow" ? "accept" : "decline");
|
|
8600
8952
|
}
|
|
8601
8953
|
clearTimeout(entry.timer);
|
|
8602
8954
|
this.approvals.delete(id);
|
|
8603
8955
|
this.resolvers.delete(id);
|
|
8604
|
-
this.
|
|
8956
|
+
this.questionIds.delete(id);
|
|
8605
8957
|
resolve6(reply);
|
|
8606
8958
|
return true;
|
|
8607
8959
|
}
|
|
@@ -8657,13 +9009,7 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
|
|
|
8657
9009
|
}
|
|
8658
9010
|
async stop() {
|
|
8659
9011
|
this.stopping = true;
|
|
8660
|
-
|
|
8661
|
-
clearTimeout(entry.timer);
|
|
8662
|
-
this.approvals.delete(id);
|
|
8663
|
-
this.resolvers.get(id)?.(entry.refusal);
|
|
8664
|
-
this.resolvers.delete(id);
|
|
8665
|
-
this.emit("permissionTimeout", { id });
|
|
8666
|
-
}
|
|
9012
|
+
this.withdrawAll();
|
|
8667
9013
|
const stoppable = this.turn?.started && this.turn.id;
|
|
8668
9014
|
await (stoppable ? this.interrupt().catch(() => void 0) : void 0);
|
|
8669
9015
|
this.removeAllFiles();
|
|
@@ -8715,9 +9061,9 @@ var init_types = __esm({
|
|
|
8715
9061
|
|
|
8716
9062
|
// src/skills/paths.ts
|
|
8717
9063
|
import { realpathSync as realpathSync13 } from "fs";
|
|
8718
|
-
import { isAbsolute as isAbsolute14, join as
|
|
9064
|
+
import { isAbsolute as isAbsolute14, join as join24, relative as relative8, sep as sep11 } from "path";
|
|
8719
9065
|
function clearSessionSkills(home, runnerId, heldBy) {
|
|
8720
|
-
clearRunnerStaging(home,
|
|
9066
|
+
clearRunnerStaging(home, join24(home, "session-skills"), runnerId, heldBy);
|
|
8721
9067
|
}
|
|
8722
9068
|
function assertInsideScopeDir(path, scopeDir) {
|
|
8723
9069
|
let dirNow;
|
|
@@ -8742,8 +9088,8 @@ var init_paths3 = __esm({
|
|
|
8742
9088
|
"use strict";
|
|
8743
9089
|
init_paths();
|
|
8744
9090
|
init_attachments2();
|
|
8745
|
-
sessionSkillsRoot = (home, runnerId) =>
|
|
8746
|
-
sessionSkillsDir = (home, runnerId, sessionId) =>
|
|
9091
|
+
sessionSkillsRoot = (home, runnerId) => join24(home, "session-skills", runnerId);
|
|
9092
|
+
sessionSkillsDir = (home, runnerId, sessionId) => join24(sessionSkillsRoot(home, runnerId), sessionId);
|
|
8747
9093
|
}
|
|
8748
9094
|
});
|
|
8749
9095
|
|
|
@@ -8882,7 +9228,7 @@ import {
|
|
|
8882
9228
|
realpathSync as realpathSync14
|
|
8883
9229
|
} from "fs";
|
|
8884
9230
|
import { open as open2, opendir } from "fs/promises";
|
|
8885
|
-
import { isAbsolute as isAbsolute15, join as
|
|
9231
|
+
import { isAbsolute as isAbsolute15, join as join25, relative as relative9, sep as sep12 } from "path";
|
|
8886
9232
|
function inside2(root, candidate) {
|
|
8887
9233
|
const rel = relative9(root, candidate);
|
|
8888
9234
|
return rel === "" || !rel.startsWith(`..${sep12}`) && rel !== ".." && !isAbsolute15(rel);
|
|
@@ -8911,7 +9257,7 @@ function scopeRoots(opts) {
|
|
|
8911
9257
|
const isGitRepo = opts.isGitRepo ?? defaultIsGitRepo;
|
|
8912
9258
|
const home = canonical2(opts.home);
|
|
8913
9259
|
const homes = protectedHomes(opts);
|
|
8914
|
-
const found = [{ scope: "personal", dir:
|
|
9260
|
+
const found = [{ scope: "personal", dir: join25(home, "skills"), base: home }];
|
|
8915
9261
|
let skillsRepo;
|
|
8916
9262
|
if (opts.skillsRepo !== void 0) {
|
|
8917
9263
|
try {
|
|
@@ -8932,7 +9278,7 @@ function scopeRoots(opts) {
|
|
|
8932
9278
|
continue;
|
|
8933
9279
|
}
|
|
8934
9280
|
if (!isGitRepo(real)) continue;
|
|
8935
|
-
const dir =
|
|
9281
|
+
const dir = join25(real, ".cayenne", "skills");
|
|
8936
9282
|
const shared = skillsRepo !== void 0 && real === skillsRepo ? "workspace" : "project";
|
|
8937
9283
|
if (overlapsHome(canonical2(dir), homes, shared, log)) continue;
|
|
8938
9284
|
if (skillsRepo !== void 0 && real === skillsRepo) {
|
|
@@ -8994,7 +9340,7 @@ async function scanSkills(opts) {
|
|
|
8994
9340
|
} catch (err) {
|
|
8995
9341
|
log.warn(
|
|
8996
9342
|
{
|
|
8997
|
-
path:
|
|
9343
|
+
path: join25(root.dirReal, entry.name),
|
|
8998
9344
|
scope: root.scope,
|
|
8999
9345
|
err: err instanceof Error ? err.message : String(err)
|
|
9000
9346
|
},
|
|
@@ -9039,13 +9385,13 @@ async function readAtMost2(fh, limit) {
|
|
|
9039
9385
|
return buffer.subarray(0, filled);
|
|
9040
9386
|
}
|
|
9041
9387
|
async function readEntry(root, entry, log, budget) {
|
|
9042
|
-
const path =
|
|
9388
|
+
const path = join25(root.dirReal, entry.name);
|
|
9043
9389
|
const skip = (reason) => {
|
|
9044
9390
|
log.warn({ path, scope: root.scope }, `skill skipped: ${reason}`);
|
|
9045
9391
|
return null;
|
|
9046
9392
|
};
|
|
9047
9393
|
if (entry.isDirectory()) {
|
|
9048
|
-
const nested = existsSync5(
|
|
9394
|
+
const nested = existsSync5(join25(path, "SKILL.md"));
|
|
9049
9395
|
return skip(
|
|
9050
9396
|
nested ? `a directory holding SKILL.md: Cayenne's store is flat, so move it to ${entry.name}.md beside this directory` : "a directory, and the scan does not recurse"
|
|
9051
9397
|
);
|
|
@@ -9205,7 +9551,7 @@ var init_scan2 = __esm({
|
|
|
9205
9551
|
SKILL_SCAN_MAX_PER_DIR = 500;
|
|
9206
9552
|
SKILL_SCAN_MAX_SKILLS = 500;
|
|
9207
9553
|
SKILL_SCAN_MAX_BYTES = 32 * 1024 * 1024;
|
|
9208
|
-
defaultIsGitRepo = (dir) => lstatSync8(
|
|
9554
|
+
defaultIsGitRepo = (dir) => lstatSync8(join25(dir, ".git"), { throwIfNoEntry: false }) !== void 0;
|
|
9209
9555
|
summariesOf = (scan) => scan.map((s) => s.summary);
|
|
9210
9556
|
FRONTMATTER = /^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/;
|
|
9211
9557
|
KEY = /^([A-Za-z_][A-Za-z0-9_-]*):[ \t]*(.*)$/;
|
|
@@ -9224,7 +9570,7 @@ import {
|
|
|
9224
9570
|
rmSync as rmSync9
|
|
9225
9571
|
} from "fs";
|
|
9226
9572
|
import { open as open3, writeFile } from "fs/promises";
|
|
9227
|
-
import { dirname as
|
|
9573
|
+
import { dirname as dirname10, isAbsolute as isAbsolute16, join as join26, relative as relative10, sep as sep13 } from "path";
|
|
9228
9574
|
function isSkillRefusal(err) {
|
|
9229
9575
|
const msg = err instanceof Error ? err.message : String(err);
|
|
9230
9576
|
return msg.startsWith("ROOT_NOT_ALLOWED") || msg.startsWith(BUDGET) || msg.includes("is not a plain directory") || msg.includes("is not part of the session skills plugin");
|
|
@@ -9271,7 +9617,7 @@ async function materialiseSkills(scan, t) {
|
|
|
9271
9617
|
}
|
|
9272
9618
|
const path = plainDirectory(t.home, sessionSkillsDir(t.home, t.runnerId, t.sessionId));
|
|
9273
9619
|
await writeFile(
|
|
9274
|
-
|
|
9620
|
+
join26(plainDirectory(t.home, join26(path, ".claude-plugin")), "plugin.json"),
|
|
9275
9621
|
`${JSON.stringify(
|
|
9276
9622
|
{
|
|
9277
9623
|
name: SKILL_PLUGIN_NAME,
|
|
@@ -9301,13 +9647,13 @@ async function materialiseSkills(scan, t) {
|
|
|
9301
9647
|
);
|
|
9302
9648
|
}
|
|
9303
9649
|
try {
|
|
9304
|
-
const dir = plainDirectory(t.home,
|
|
9305
|
-
await writeFile(
|
|
9650
|
+
const dir = plainDirectory(t.home, join26(path, "skills", slug));
|
|
9651
|
+
await writeFile(join26(dir, "SKILL.md"), bytes, { mode: 384, flag: "wx" });
|
|
9306
9652
|
slugs.push(slug);
|
|
9307
9653
|
} catch (err) {
|
|
9308
9654
|
note(log, err, { slug, scope, path: skill.path }, "skill not materialised for this session");
|
|
9309
9655
|
try {
|
|
9310
|
-
rmSync9(assertStagingInsideHome(t.home,
|
|
9656
|
+
rmSync9(assertStagingInsideHome(t.home, join26(path, "skills", slug)), {
|
|
9311
9657
|
recursive: true,
|
|
9312
9658
|
force: true
|
|
9313
9659
|
});
|
|
@@ -9371,20 +9717,20 @@ function assertSealed(path, slugs) {
|
|
|
9371
9717
|
const want = new Set(expected);
|
|
9372
9718
|
for (const entry of entries2) {
|
|
9373
9719
|
const ok = kind === "dir" ? entry.isDirectory() : entry.isFile();
|
|
9374
|
-
if (!ok || !want.has(entry.name)) throw stray(
|
|
9720
|
+
if (!ok || !want.has(entry.name)) throw stray(join26(dir, entry.name));
|
|
9375
9721
|
}
|
|
9376
|
-
if (entries2.length !== want.size) throw stray(
|
|
9722
|
+
if (entries2.length !== want.size) throw stray(join26(dir, [...want].join("|")));
|
|
9377
9723
|
};
|
|
9378
9724
|
named(path, [".claude-plugin", "skills"], "dir");
|
|
9379
|
-
named(
|
|
9380
|
-
named(
|
|
9381
|
-
for (const slug of slugs) named(
|
|
9725
|
+
named(join26(path, ".claude-plugin"), ["plugin.json"], "file");
|
|
9726
|
+
named(join26(path, "skills"), slugs, "dir");
|
|
9727
|
+
for (const slug of slugs) named(join26(path, "skills", slug), ["SKILL.md"], "file");
|
|
9382
9728
|
}
|
|
9383
9729
|
function plainDirectory(home, dir) {
|
|
9384
9730
|
const intended = assertStagingInsideHome(home, dir);
|
|
9385
9731
|
mkdirSync9(intended, { recursive: true, mode: 448 });
|
|
9386
9732
|
const proved = assertStagingInsideHome(home, intended);
|
|
9387
|
-
for (const p of [
|
|
9733
|
+
for (const p of [dirname10(proved), proved]) {
|
|
9388
9734
|
const st = lstatSync9(p);
|
|
9389
9735
|
if (st.isSymbolicLink() || !st.isDirectory()) throw new Error(`${p} is not a plain directory`);
|
|
9390
9736
|
}
|
|
@@ -9941,13 +10287,13 @@ var init_secret_files = __esm({
|
|
|
9941
10287
|
|
|
9942
10288
|
// src/sessions/task-sessions.ts
|
|
9943
10289
|
import { existsSync as existsSync6 } from "fs";
|
|
9944
|
-
import { join as
|
|
10290
|
+
import { join as join27 } from "path";
|
|
9945
10291
|
function createTaskSessions(dir) {
|
|
9946
10292
|
const fileFor = (sessionId) => {
|
|
9947
10293
|
if (!UUID3.test(sessionId)) {
|
|
9948
10294
|
throw new Error(`VALIDATION: ${sessionId.slice(0, 40)} is not a session id`);
|
|
9949
10295
|
}
|
|
9950
|
-
return
|
|
10296
|
+
return join27(dir, `${sessionId.toLowerCase()}.json`);
|
|
9951
10297
|
};
|
|
9952
10298
|
return {
|
|
9953
10299
|
mark(sessionId) {
|
|
@@ -9968,10 +10314,10 @@ var init_task_sessions = __esm({
|
|
|
9968
10314
|
});
|
|
9969
10315
|
|
|
9970
10316
|
// src/sessions/user-guards.ts
|
|
9971
|
-
import { closeSync as closeSync4, constants as constants4, fstatSync, openSync as openSync4, readFileSync as
|
|
9972
|
-
import { isAbsolute as isAbsolute20, join as
|
|
10317
|
+
import { closeSync as closeSync4, constants as constants4, fstatSync, openSync as openSync4, readFileSync as readFileSync11 } from "fs";
|
|
10318
|
+
import { isAbsolute as isAbsolute20, join as join28 } from "path";
|
|
9973
10319
|
function readUserGuards(configDir) {
|
|
9974
|
-
const settingsFile =
|
|
10320
|
+
const settingsFile = join28(configDir, "settings.json");
|
|
9975
10321
|
const none = { settingsFile, preToolUseHooks: 0, denyRules: 0 };
|
|
9976
10322
|
if (!isAbsolute20(configDir)) return none;
|
|
9977
10323
|
const text = readSmallFile(settingsFile);
|
|
@@ -10006,7 +10352,7 @@ function readSmallFile(path) {
|
|
|
10006
10352
|
try {
|
|
10007
10353
|
const stats = fstatSync(fd);
|
|
10008
10354
|
if (!stats.isFile() || stats.size > USER_SETTINGS_MAX_BYTES) return null;
|
|
10009
|
-
return
|
|
10355
|
+
return readFileSync11(fd, "utf8");
|
|
10010
10356
|
} catch {
|
|
10011
10357
|
return null;
|
|
10012
10358
|
} finally {
|
|
@@ -10040,7 +10386,7 @@ var init_user_guards = __esm({
|
|
|
10040
10386
|
// src/sessions/manager.ts
|
|
10041
10387
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
10042
10388
|
import { realpathSync as realpathSync17 } from "fs";
|
|
10043
|
-
import { isAbsolute as isAbsolute21, join as
|
|
10389
|
+
import { isAbsolute as isAbsolute21, join as join29, relative as relative14, sep as sep17 } from "path";
|
|
10044
10390
|
import { isDeepStrictEqual } from "util";
|
|
10045
10391
|
function releaseCreds(creds) {
|
|
10046
10392
|
if (creds?.state === "ok") creds.release();
|
|
@@ -10071,7 +10417,7 @@ function assertRepoOwner(ownerId, createdBy) {
|
|
|
10071
10417
|
}
|
|
10072
10418
|
function narrowToSession(suggestions) {
|
|
10073
10419
|
if (!suggestions) return suggestions;
|
|
10074
|
-
return suggestions.map((s) =>
|
|
10420
|
+
return suggestions.map((s) => isRecord2(s) ? { ...s, destination: "session" } : s);
|
|
10075
10421
|
}
|
|
10076
10422
|
function shrink(event) {
|
|
10077
10423
|
switch (event.type) {
|
|
@@ -10089,7 +10435,7 @@ function shrink(event) {
|
|
|
10089
10435
|
return null;
|
|
10090
10436
|
}
|
|
10091
10437
|
}
|
|
10092
|
-
var READ_ONLY_TEXT, REFRESH_WITHIN_MS, betweenTurns, UNREADABLE_MARK_CREDS, DRAINED_AFTER_MS, roundUsd, UNIQUE_VIOLATION, ROW_WRITE_TIMEOUT_MS, ORPHANED_STATUSES, RESTARTABLE_STATUSES, isAtOrBelow3, DENY_MESSAGE_MAX, EXIT_ERROR_MAX, INITIAL_PROMPT_MAX, TASK_SKILL_LOOKUP_MS, TASK_REFUSED_MODES, OWNER_DRIVEN_KINDS, REPO_OWNER_ONLY, REPO_NO_DUPLICATE, PROVIDER_SESSION_ID, REPO_WORKTREE_GONE, TASK_ASK_TOOLS, MEMORY_PREAMBLE, MEMORY_SOURCE, wrapMemory, PATH_INPUT_TOOLS, PATH_INPUT_KEYS,
|
|
10438
|
+
var READ_ONLY_TEXT, REFRESH_WITHIN_MS, betweenTurns, UNREADABLE_MARK_CREDS, DRAINED_AFTER_MS, roundUsd, UNIQUE_VIOLATION, ROW_WRITE_TIMEOUT_MS, ORPHANED_STATUSES, RESTARTABLE_STATUSES, isAtOrBelow3, DENY_MESSAGE_MAX, EXIT_ERROR_MAX, INITIAL_PROMPT_MAX, TASK_SKILL_LOOKUP_MS, TASK_REFUSED_MODES, OWNER_DRIVEN_KINDS, REPO_OWNER_ONLY, REPO_NO_DUPLICATE, PROVIDER_SESSION_ID, REPO_WORKTREE_GONE, TASK_ASK_TOOLS, MEMORY_PREAMBLE, MEMORY_SOURCE, wrapMemory, PATH_INPUT_TOOLS, PATH_INPUT_KEYS, isRecord2, errorCode, SessionManager;
|
|
10093
10439
|
var init_manager = __esm({
|
|
10094
10440
|
"src/sessions/manager.ts"() {
|
|
10095
10441
|
"use strict";
|
|
@@ -10176,7 +10522,7 @@ var init_manager = __esm({
|
|
|
10176
10522
|
"LS"
|
|
10177
10523
|
]);
|
|
10178
10524
|
PATH_INPUT_KEYS = ["file_path", "path", "cwd", "notebook_path"];
|
|
10179
|
-
|
|
10525
|
+
isRecord2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
10180
10526
|
errorCode = (message) => message.startsWith("ROOT_NOT_ALLOWED") ? "ROOT_NOT_ALLOWED" : "VALIDATION";
|
|
10181
10527
|
SessionManager = class {
|
|
10182
10528
|
constructor(deps) {
|
|
@@ -11173,6 +11519,7 @@ var init_manager = __esm({
|
|
|
11173
11519
|
turns.drainedAt = Date.now();
|
|
11174
11520
|
turns.early = true;
|
|
11175
11521
|
turns.chased = false;
|
|
11522
|
+
turns.ended += 1;
|
|
11176
11523
|
}
|
|
11177
11524
|
/**
|
|
11178
11525
|
* Put rows on this runner that claim a turn in progress back to `idle`, for the chats that have
|
|
@@ -11429,7 +11776,7 @@ var init_manager = __esm({
|
|
|
11429
11776
|
const id = sessionId === void 0 ? null : this.live.get(sessionId)?.session.providerSessionId;
|
|
11430
11777
|
if (projectDir === null || typeof id !== "string" || !PROVIDER_SESSION_ID.test(id))
|
|
11431
11778
|
return [];
|
|
11432
|
-
return [
|
|
11779
|
+
return [join29(projectDir, id, "tool-results")];
|
|
11433
11780
|
};
|
|
11434
11781
|
const policy2 = this.settingsPolicy(createdBy, cwd, permissionMode, []);
|
|
11435
11782
|
this.warnBypassWithoutUserGuards(createdBy, permissionMode, policy2, sessionId);
|
|
@@ -11477,7 +11824,7 @@ var init_manager = __esm({
|
|
|
11477
11824
|
canonical3 = realpathSync17.native(configDir);
|
|
11478
11825
|
} catch {
|
|
11479
11826
|
}
|
|
11480
|
-
return
|
|
11827
|
+
return join29(canonical3, "projects", claudeProjectDirName(cwd));
|
|
11481
11828
|
}
|
|
11482
11829
|
/** Whether one of `roots` contains the managed root, as an allowed root such as `~` can. */
|
|
11483
11830
|
reachesManagedRoot(roots) {
|
|
@@ -11508,7 +11855,7 @@ var init_manager = __esm({
|
|
|
11508
11855
|
return parsed.data;
|
|
11509
11856
|
}
|
|
11510
11857
|
taskSessions() {
|
|
11511
|
-
return this.deps.taskSessions ?? createTaskSessions(
|
|
11858
|
+
return this.deps.taskSessions ?? createTaskSessions(join29(this.deps.cayenneHome ?? cayenneHome(), "task-sessions"));
|
|
11512
11859
|
}
|
|
11513
11860
|
markTaskSession(sessionId) {
|
|
11514
11861
|
try {
|
|
@@ -11690,15 +12037,31 @@ var init_manager = __esm({
|
|
|
11690
12037
|
/**
|
|
11691
12038
|
* A tool's path as the provider opens it: relative means relative to the session's cwd. Every
|
|
11692
12039
|
* judge of a tool's path goes through here (R28's `checkUpdatedInput`, a suggested directory,
|
|
11693
|
-
* Codex's
|
|
11694
|
-
* (surrounding whitespace, a leading `~`, a non-NFC spelling)
|
|
11695
|
-
* written, `~/.ssh/id_rsa` was `<cwd>/~/.ssh/id_rsa`, inside
|
|
11696
|
-
* 2026-09-29).
|
|
12040
|
+
* and Codex's paths by way of `codexPath`, which admits no relative one). A value the Claude CLI
|
|
12041
|
+
* would rewrite before it opens it (surrounding whitespace, a leading `~`, a non-NFC spelling)
|
|
12042
|
+
* is refused, not judged: judged as written, `~/.ssh/id_rsa` was `<cwd>/~/.ssh/id_rsa`, inside
|
|
12043
|
+
* any root (security re-review 2026-09-29).
|
|
11697
12044
|
*/
|
|
11698
12045
|
fromCwd(cwd, value) {
|
|
11699
12046
|
if (!isPlainToolPath(value)) throw new Error(notPlainPathRefusal(value));
|
|
11700
12047
|
return isAbsolute21(value) ? value : `${cwd}${sep17}${value}`;
|
|
11701
12048
|
}
|
|
12049
|
+
/**
|
|
12050
|
+
* A path Codex named, for the roots check: absolute, or refused. The runner cannot prove what
|
|
12051
|
+
* directory the app-server would resolve a relative one against: a turn's cwd is the
|
|
12052
|
+
* app-server's, and a command can move it. Every live run against codex-cli 0.162 named
|
|
12053
|
+
* absolute paths only, so a relative one is an unknown shape, refused exactly as an
|
|
12054
|
+
* out-of-roots path is. An absolute value still passes through `fromCwd`, which refuses one
|
|
12055
|
+
* that is not a plain path.
|
|
12056
|
+
*/
|
|
12057
|
+
codexPath(cwd, value) {
|
|
12058
|
+
if (!isAbsolute21(value)) {
|
|
12059
|
+
throw new Error(
|
|
12060
|
+
`ROOT_NOT_ALLOWED: ${JSON.stringify(value)} is not an absolute path; a Codex change or command names every path by its absolute path`
|
|
12061
|
+
);
|
|
12062
|
+
}
|
|
12063
|
+
return this.fromCwd(cwd, value);
|
|
12064
|
+
}
|
|
11702
12065
|
/**
|
|
11703
12066
|
* §2.9: a path in `updatedInput` passes the same boundary the session's `cwd` did, and the
|
|
11704
12067
|
* SDK receives the canonical path the guard proved rather than the spelling typed (S2). The
|
|
@@ -11721,6 +12084,11 @@ var init_manager = __esm({
|
|
|
11721
12084
|
* not admit is denied before a row exists: no answer the owner could give would make it
|
|
11722
12085
|
* acceptable, exactly as R28 reasons for the Claude path.
|
|
11723
12086
|
*
|
|
12087
|
+
* A change writes its `path` and, when it moves the file, its `movePath`: both are read here,
|
|
12088
|
+
* each as a path of its own, so the card and the auto-accept gate judge the same set. A command
|
|
12089
|
+
* must name its `cwd`: absent, blank or not a string, the command is refused, not passed.
|
|
12090
|
+
* Every path, and that `cwd`, must be absolute (`codexPath`).
|
|
12091
|
+
*
|
|
11724
12092
|
* When the roots reach the managed root (D50: an allowed root such as `~`), a file change is
|
|
11725
12093
|
* also held to the managed `.git` guard Claude sessions get: a change under a managed clone's
|
|
11726
12094
|
* git dir, or to a worktree's `.git` file, is refused with the same text. In `acceptEdits` or
|
|
@@ -11733,19 +12101,30 @@ var init_manager = __esm({
|
|
|
11733
12101
|
throw new Error("VALIDATION: codex file change carries no changes to check");
|
|
11734
12102
|
}
|
|
11735
12103
|
const guarded = this.reachesManagedRoot(roots);
|
|
12104
|
+
const check = (path) => {
|
|
12105
|
+
assertInsideRootsOrAncestor(this.codexPath(cwd, path), roots);
|
|
12106
|
+
if (guarded && writesManagedGit(path, this.deps.managedRoot, cwd)) {
|
|
12107
|
+
throw new Error(MANAGED_GIT_REFUSAL);
|
|
12108
|
+
}
|
|
12109
|
+
};
|
|
11736
12110
|
for (const change of changes) {
|
|
11737
|
-
if (!
|
|
12111
|
+
if (!isRecord2(change) || typeof change.path !== "string" || change.path.trim() === "") {
|
|
11738
12112
|
throw new Error("VALIDATION: codex file change entry names no path to check");
|
|
11739
12113
|
}
|
|
11740
|
-
|
|
11741
|
-
if (
|
|
11742
|
-
|
|
12114
|
+
check(change.path);
|
|
12115
|
+
if (change.movePath === void 0) continue;
|
|
12116
|
+
if (typeof change.movePath !== "string" || change.movePath.trim() === "") {
|
|
12117
|
+
throw new Error("VALIDATION: codex file change entry names no move target to check");
|
|
11743
12118
|
}
|
|
12119
|
+
check(change.movePath);
|
|
11744
12120
|
}
|
|
11745
12121
|
return;
|
|
11746
12122
|
}
|
|
11747
|
-
if (toolName2 === CODEX_TOOL_NAMES.command
|
|
11748
|
-
|
|
12123
|
+
if (toolName2 === CODEX_TOOL_NAMES.command) {
|
|
12124
|
+
if (typeof input.cwd !== "string" || input.cwd.trim() === "") {
|
|
12125
|
+
throw new Error("VALIDATION: codex command names no cwd to check");
|
|
12126
|
+
}
|
|
12127
|
+
assertInsideRoots(this.codexPath(cwd, input.cwd), roots);
|
|
11749
12128
|
}
|
|
11750
12129
|
}
|
|
11751
12130
|
/**
|
|
@@ -11818,7 +12197,7 @@ var init_manager = __esm({
|
|
|
11818
12197
|
checkUpdatedPermissions(live, entry, updates) {
|
|
11819
12198
|
if (!updates) return void 0;
|
|
11820
12199
|
return updates.map((update) => {
|
|
11821
|
-
if (!
|
|
12200
|
+
if (!isRecord2(update))
|
|
11822
12201
|
throw new Error("VALIDATION: updatedPermissions entry is not an object");
|
|
11823
12202
|
if (update.destination !== "session") {
|
|
11824
12203
|
throw new Error(
|
|
@@ -12004,9 +12383,19 @@ var init_manager = __esm({
|
|
|
12004
12383
|
status: "starting",
|
|
12005
12384
|
echoed: /* @__PURE__ */ new Set(),
|
|
12006
12385
|
permissions: /* @__PURE__ */ new Map(),
|
|
12386
|
+
permissionsInFlight: /* @__PURE__ */ new Set(),
|
|
12387
|
+
permissionsWithdrawn: /* @__PURE__ */ new Set(),
|
|
12007
12388
|
cwd: opts.cwd,
|
|
12008
12389
|
stopping: false,
|
|
12009
|
-
turns: {
|
|
12390
|
+
turns: {
|
|
12391
|
+
owed: 0,
|
|
12392
|
+
begun: false,
|
|
12393
|
+
drainedAt: null,
|
|
12394
|
+
stops: 0,
|
|
12395
|
+
early: false,
|
|
12396
|
+
chased: false,
|
|
12397
|
+
ended: 0
|
|
12398
|
+
},
|
|
12010
12399
|
dropping: null,
|
|
12011
12400
|
inflight: /* @__PURE__ */ new Set(),
|
|
12012
12401
|
writes: Promise.resolve(),
|
|
@@ -12089,7 +12478,7 @@ var init_manager = __esm({
|
|
|
12089
12478
|
*/
|
|
12090
12479
|
watchWorkingCopy(sessionId, live) {
|
|
12091
12480
|
const factory = this.deps.watch ?? ((cwd, onChange2, onError2) => watchChanges(cwd, onChange2, CHANGES_DEBOUNCE_MS, onError2));
|
|
12092
|
-
let
|
|
12481
|
+
let failed5 = false;
|
|
12093
12482
|
let built = null;
|
|
12094
12483
|
const closeFailed = (watcher) => {
|
|
12095
12484
|
if (live.watcher === watcher) live.watcher = null;
|
|
@@ -12098,8 +12487,8 @@ var init_manager = __esm({
|
|
|
12098
12487
|
);
|
|
12099
12488
|
};
|
|
12100
12489
|
const onError = (err) => {
|
|
12101
|
-
if (
|
|
12102
|
-
|
|
12490
|
+
if (failed5) return;
|
|
12491
|
+
failed5 = true;
|
|
12103
12492
|
this.deps.log.warn(
|
|
12104
12493
|
{ err, sessionId },
|
|
12105
12494
|
"working-copy watcher error; live change refresh stopped"
|
|
@@ -12107,7 +12496,7 @@ var init_manager = __esm({
|
|
|
12107
12496
|
if (built) closeFailed(built);
|
|
12108
12497
|
};
|
|
12109
12498
|
const onChange = () => {
|
|
12110
|
-
if (
|
|
12499
|
+
if (failed5 || this.live.get(sessionId) !== live) return;
|
|
12111
12500
|
this.bumpChanges(sessionId, live);
|
|
12112
12501
|
};
|
|
12113
12502
|
try {
|
|
@@ -12116,7 +12505,7 @@ var init_manager = __esm({
|
|
|
12116
12505
|
this.deps.log.warn({ err, sessionId }, "working-copy watcher failed to start");
|
|
12117
12506
|
return null;
|
|
12118
12507
|
}
|
|
12119
|
-
if (
|
|
12508
|
+
if (failed5) {
|
|
12120
12509
|
closeFailed(built);
|
|
12121
12510
|
return null;
|
|
12122
12511
|
}
|
|
@@ -12218,89 +12607,95 @@ var init_manager = __esm({
|
|
|
12218
12607
|
);
|
|
12219
12608
|
})();
|
|
12220
12609
|
});
|
|
12610
|
+
const raise = async (rawReq) => {
|
|
12611
|
+
const req = { ...rawReq, suggestions: narrowToSession(rawReq.suggestions) };
|
|
12612
|
+
try {
|
|
12613
|
+
this.checkUpdatedInput(live, req.toolName, req.input);
|
|
12614
|
+
this.checkCodexInput(live.cwd, live.roots, req.toolName, req.input);
|
|
12615
|
+
} catch (err) {
|
|
12616
|
+
const message = this.cap(err instanceof Error ? err.message : String(err));
|
|
12617
|
+
session.respondPermission(req.id, { behavior: "deny", message, interrupt: false });
|
|
12618
|
+
this.deps.log.warn(
|
|
12619
|
+
{ sessionId, tool: req.toolName, err: message },
|
|
12620
|
+
"permission request refused before a row existed"
|
|
12621
|
+
);
|
|
12622
|
+
await this.broadcast(sessionId, { type: "error", code: errorCode(message), message });
|
|
12623
|
+
return;
|
|
12624
|
+
}
|
|
12625
|
+
const input = this.secrets.redact(req.input);
|
|
12626
|
+
const suggestions = this.secrets.redact(req.suggestions);
|
|
12627
|
+
const { data, error } = await this.deps.db.from("permission_requests").insert({
|
|
12628
|
+
session_id: sessionId,
|
|
12629
|
+
workspace_id: this.deps.workspaceId,
|
|
12630
|
+
tool_use_id: req.toolUseId,
|
|
12631
|
+
tool_name: req.toolName,
|
|
12632
|
+
input,
|
|
12633
|
+
suggestions
|
|
12634
|
+
}).select("*").single();
|
|
12635
|
+
if (error || !data) {
|
|
12636
|
+
const message = this.cap(
|
|
12637
|
+
`VALIDATION: could not persist the permission request for ${req.toolName}: ${error?.message ?? "no row"}`
|
|
12638
|
+
);
|
|
12639
|
+
session.respondPermission(req.id, { behavior: "deny", message, interrupt: false });
|
|
12640
|
+
await this.broadcast(sessionId, { type: "error", code: "VALIDATION", message });
|
|
12641
|
+
throw new Error(message);
|
|
12642
|
+
}
|
|
12643
|
+
if (live.permissionsWithdrawn.has(req.id)) {
|
|
12644
|
+
await this.expirePermission(sessionId, data.id);
|
|
12645
|
+
return;
|
|
12646
|
+
}
|
|
12647
|
+
live.permissions.set(data.id, {
|
|
12648
|
+
localId: req.id,
|
|
12649
|
+
toolName: req.toolName,
|
|
12650
|
+
input,
|
|
12651
|
+
suggestions
|
|
12652
|
+
});
|
|
12653
|
+
await this.setStatus(sessionId, "awaiting_permission");
|
|
12654
|
+
if (!live.permissions.has(data.id)) return;
|
|
12655
|
+
try {
|
|
12656
|
+
await this.broadcast(sessionId, {
|
|
12657
|
+
type: "permission.request",
|
|
12658
|
+
request: data
|
|
12659
|
+
});
|
|
12660
|
+
} catch (err) {
|
|
12661
|
+
if (!(err instanceof Error) || !err.message.startsWith("VALIDATION:")) throw err;
|
|
12662
|
+
this.deps.log.warn(
|
|
12663
|
+
{ sessionId, requestId: data.id, err: err.message },
|
|
12664
|
+
"permission request too large to broadcast; clients reconcile from the row"
|
|
12665
|
+
);
|
|
12666
|
+
await this.broadcast(sessionId, {
|
|
12667
|
+
type: "error",
|
|
12668
|
+
code: "VALIDATION",
|
|
12669
|
+
message: err.message
|
|
12670
|
+
});
|
|
12671
|
+
}
|
|
12672
|
+
};
|
|
12221
12673
|
session.on(
|
|
12222
12674
|
"permission",
|
|
12223
12675
|
(rawReq) => guard(async () => {
|
|
12224
|
-
|
|
12225
|
-
try {
|
|
12226
|
-
this.checkUpdatedInput(live, req.toolName, req.input);
|
|
12227
|
-
this.checkCodexInput(live.cwd, live.roots, req.toolName, req.input);
|
|
12228
|
-
} catch (err) {
|
|
12229
|
-
const message = this.cap(err instanceof Error ? err.message : String(err));
|
|
12230
|
-
session.respondPermission(req.id, { behavior: "deny", message, interrupt: false });
|
|
12231
|
-
this.deps.log.warn(
|
|
12232
|
-
{ sessionId, tool: req.toolName, err: message },
|
|
12233
|
-
"permission request refused before a row existed"
|
|
12234
|
-
);
|
|
12235
|
-
await this.broadcast(sessionId, { type: "error", code: errorCode(message), message });
|
|
12236
|
-
return;
|
|
12237
|
-
}
|
|
12238
|
-
const input = this.secrets.redact(req.input);
|
|
12239
|
-
const suggestions = this.secrets.redact(req.suggestions);
|
|
12240
|
-
const { data, error } = await this.deps.db.from("permission_requests").insert({
|
|
12241
|
-
session_id: sessionId,
|
|
12242
|
-
workspace_id: this.deps.workspaceId,
|
|
12243
|
-
tool_use_id: req.toolUseId,
|
|
12244
|
-
tool_name: req.toolName,
|
|
12245
|
-
input,
|
|
12246
|
-
suggestions
|
|
12247
|
-
}).select("*").single();
|
|
12248
|
-
if (error || !data) {
|
|
12249
|
-
const message = this.cap(
|
|
12250
|
-
`VALIDATION: could not persist the permission request for ${req.toolName}: ${error?.message ?? "no row"}`
|
|
12251
|
-
);
|
|
12252
|
-
session.respondPermission(req.id, { behavior: "deny", message, interrupt: false });
|
|
12253
|
-
await this.broadcast(sessionId, { type: "error", code: "VALIDATION", message });
|
|
12254
|
-
throw new Error(message);
|
|
12255
|
-
}
|
|
12256
|
-
live.permissions.set(data.id, {
|
|
12257
|
-
localId: req.id,
|
|
12258
|
-
toolName: req.toolName,
|
|
12259
|
-
input,
|
|
12260
|
-
suggestions
|
|
12261
|
-
});
|
|
12262
|
-
await this.setStatus(sessionId, "awaiting_permission");
|
|
12676
|
+
live.permissionsInFlight.add(rawReq.id);
|
|
12263
12677
|
try {
|
|
12264
|
-
await
|
|
12265
|
-
|
|
12266
|
-
|
|
12267
|
-
|
|
12268
|
-
} catch (err) {
|
|
12269
|
-
if (!(err instanceof Error) || !err.message.startsWith("VALIDATION:")) throw err;
|
|
12270
|
-
this.deps.log.warn(
|
|
12271
|
-
{ sessionId, requestId: data.id, err: err.message },
|
|
12272
|
-
"permission request too large to broadcast; clients reconcile from the row"
|
|
12273
|
-
);
|
|
12274
|
-
await this.broadcast(sessionId, {
|
|
12275
|
-
type: "error",
|
|
12276
|
-
code: "VALIDATION",
|
|
12277
|
-
message: err.message
|
|
12278
|
-
});
|
|
12678
|
+
await raise(rawReq);
|
|
12679
|
+
} finally {
|
|
12680
|
+
live.permissionsInFlight.delete(rawReq.id);
|
|
12681
|
+
live.permissionsWithdrawn.delete(rawReq.id);
|
|
12279
12682
|
}
|
|
12280
12683
|
})()
|
|
12281
12684
|
);
|
|
12282
12685
|
session.on(
|
|
12283
12686
|
"permissionTimeout",
|
|
12284
12687
|
({ id }) => guard(async () => {
|
|
12688
|
+
const ended = live.turns.ended;
|
|
12285
12689
|
const requestId = [...live.permissions.entries()].find(
|
|
12286
12690
|
([, entry]) => entry.localId === id
|
|
12287
12691
|
)?.[0];
|
|
12288
|
-
if (!requestId)
|
|
12289
|
-
|
|
12290
|
-
|
|
12291
|
-
if (error) {
|
|
12292
|
-
this.deps.log.warn(
|
|
12293
|
-
{ requestId, err: error.message },
|
|
12294
|
-
"could not expire permission request"
|
|
12295
|
-
);
|
|
12296
|
-
} else if (data && data.length > 0) {
|
|
12297
|
-
await this.broadcast(sessionId, {
|
|
12298
|
-
type: "permission.resolved",
|
|
12299
|
-
requestId,
|
|
12300
|
-
status: "expired"
|
|
12301
|
-
});
|
|
12692
|
+
if (!requestId) {
|
|
12693
|
+
if (live.permissionsInFlight.has(id)) live.permissionsWithdrawn.add(id);
|
|
12694
|
+
return;
|
|
12302
12695
|
}
|
|
12303
|
-
|
|
12696
|
+
live.permissions.delete(requestId);
|
|
12697
|
+
await this.expirePermission(sessionId, requestId);
|
|
12698
|
+
if (live.permissions.size === 0 && !live.stopping && live.status === "awaiting_permission" && live.turns.ended === ended) {
|
|
12304
12699
|
await this.setStatus(sessionId, "running");
|
|
12305
12700
|
}
|
|
12306
12701
|
})()
|
|
@@ -12343,6 +12738,26 @@ var init_manager = __esm({
|
|
|
12343
12738
|
})();
|
|
12344
12739
|
});
|
|
12345
12740
|
}
|
|
12741
|
+
/**
|
|
12742
|
+
* Expire a request the session settled itself, and say so when this write is what settled the
|
|
12743
|
+
* row. `permissions_expire_by_runner` is the one transition the runner may write. Filtered on
|
|
12744
|
+
* status = 'pending' so an owner answer in the same instant wins: the row is then terminal and
|
|
12745
|
+
* this matches nothing rather than failing. A write that fails is logged, never thrown.
|
|
12746
|
+
*/
|
|
12747
|
+
async expirePermission(sessionId, requestId) {
|
|
12748
|
+
const { data, error } = await this.deps.db.from("permission_requests").update({ status: "expired" }).eq("id", requestId).eq("status", "pending").select("id");
|
|
12749
|
+
if (error) {
|
|
12750
|
+
this.deps.log.warn({ requestId, err: error.message }, "could not expire permission request");
|
|
12751
|
+
return;
|
|
12752
|
+
}
|
|
12753
|
+
if (data && data.length > 0) {
|
|
12754
|
+
await this.broadcast(sessionId, {
|
|
12755
|
+
type: "permission.resolved",
|
|
12756
|
+
requestId,
|
|
12757
|
+
status: "expired"
|
|
12758
|
+
});
|
|
12759
|
+
}
|
|
12760
|
+
}
|
|
12346
12761
|
/**
|
|
12347
12762
|
* Forget `live` and release its channel — once, and only if it is still the current entry.
|
|
12348
12763
|
*
|
|
@@ -12584,10 +12999,10 @@ var init_manager = __esm({
|
|
|
12584
12999
|
});
|
|
12585
13000
|
|
|
12586
13001
|
// src/sessions/repo-sessions.ts
|
|
12587
|
-
import { readdirSync as readdirSync5, readFileSync as
|
|
12588
|
-
import { join as
|
|
13002
|
+
import { readdirSync as readdirSync5, readFileSync as readFileSync12 } from "fs";
|
|
13003
|
+
import { join as join30 } from "path";
|
|
12589
13004
|
function createRepoSessions(dir) {
|
|
12590
|
-
const fileFor = (sessionId) => UUID4.test(sessionId) ?
|
|
13005
|
+
const fileFor = (sessionId) => UUID4.test(sessionId) ? join30(dir, `${sessionId.toLowerCase()}.json`) : null;
|
|
12591
13006
|
return {
|
|
12592
13007
|
mark(sessionId, mark) {
|
|
12593
13008
|
const file = fileFor(sessionId);
|
|
@@ -12607,7 +13022,7 @@ function createRepoSessions(dir) {
|
|
|
12607
13022
|
if (file === null) return null;
|
|
12608
13023
|
let text;
|
|
12609
13024
|
try {
|
|
12610
|
-
text =
|
|
13025
|
+
text = readFileSync12(file, "utf8");
|
|
12611
13026
|
} catch (err) {
|
|
12612
13027
|
return isMissing(err) ? null : "unreadable";
|
|
12613
13028
|
}
|
|
@@ -12991,9 +13406,9 @@ function createTerminalChannels({
|
|
|
12991
13406
|
const inflight = joining.get(terminalId);
|
|
12992
13407
|
if (inflight) return inflight;
|
|
12993
13408
|
if (joined.has(terminalId)) return;
|
|
12994
|
-
const
|
|
12995
|
-
joining.set(terminalId,
|
|
12996
|
-
return
|
|
13409
|
+
const join33 = attachOne(terminalId).finally(() => joining.delete(terminalId));
|
|
13410
|
+
joining.set(terminalId, join33);
|
|
13411
|
+
return join33;
|
|
12997
13412
|
},
|
|
12998
13413
|
flush,
|
|
12999
13414
|
detach,
|
|
@@ -13054,9 +13469,9 @@ var init_channels = __esm({
|
|
|
13054
13469
|
});
|
|
13055
13470
|
|
|
13056
13471
|
// src/terminal/env.ts
|
|
13057
|
-
import { readFileSync as
|
|
13472
|
+
import { readFileSync as readFileSync13, realpathSync as realpathSync19 } from "fs";
|
|
13058
13473
|
import { lstat } from "fs/promises";
|
|
13059
|
-
import { dirname as
|
|
13474
|
+
import { dirname as dirname11, isAbsolute as isAbsolute22, relative as relative15 } from "path";
|
|
13060
13475
|
function terminalEnv(base, { runnerOwner, claudeConfigDir }) {
|
|
13061
13476
|
const env = {};
|
|
13062
13477
|
for (const [key, value] of Object.entries(base)) {
|
|
@@ -13115,7 +13530,7 @@ async function rootOwnedPath(fs, real) {
|
|
|
13115
13530
|
return false;
|
|
13116
13531
|
}
|
|
13117
13532
|
if (st.isSymbolicLink() || st.uid !== 0 || (st.mode & 18) !== 0) return false;
|
|
13118
|
-
const parent =
|
|
13533
|
+
const parent = dirname11(path);
|
|
13119
13534
|
if (parent === path) return true;
|
|
13120
13535
|
path = parent;
|
|
13121
13536
|
}
|
|
@@ -13154,7 +13569,7 @@ var init_env2 = __esm({
|
|
|
13154
13569
|
nodeShellFs = {
|
|
13155
13570
|
readEtcShells: () => {
|
|
13156
13571
|
try {
|
|
13157
|
-
return
|
|
13572
|
+
return readFileSync13("/etc/shells", "utf8");
|
|
13158
13573
|
} catch {
|
|
13159
13574
|
return "";
|
|
13160
13575
|
}
|
|
@@ -14200,7 +14615,7 @@ __export(start_exports, {
|
|
|
14200
14615
|
start: () => start
|
|
14201
14616
|
});
|
|
14202
14617
|
import { homedir as homedir6, hostname } from "os";
|
|
14203
|
-
import { join as
|
|
14618
|
+
import { join as join31 } from "path";
|
|
14204
14619
|
function protectedPaths(claudeConfigDir, home = homedir6()) {
|
|
14205
14620
|
const inHome = [
|
|
14206
14621
|
".claude",
|
|
@@ -14208,12 +14623,12 @@ function protectedPaths(claudeConfigDir, home = homedir6()) {
|
|
|
14208
14623
|
".codex",
|
|
14209
14624
|
".ssh",
|
|
14210
14625
|
".aws",
|
|
14211
|
-
|
|
14626
|
+
join31(".config", "gh"),
|
|
14212
14627
|
".gnupg",
|
|
14213
14628
|
".git-credentials",
|
|
14214
14629
|
".npmrc",
|
|
14215
14630
|
".netrc"
|
|
14216
|
-
].map((p) =>
|
|
14631
|
+
].map((p) => join31(home, p));
|
|
14217
14632
|
return [cayenneHome(), ...claudeConfigDir ? [claudeConfigDir] : [], ...inHome];
|
|
14218
14633
|
}
|
|
14219
14634
|
function withRepoClone(repoClone) {
|
|
@@ -14229,6 +14644,16 @@ function logCodex(codex) {
|
|
|
14229
14644
|
`codex available on ${codex.models.map((m) => m.id).join(", ") || "its default model"}`
|
|
14230
14645
|
);
|
|
14231
14646
|
}
|
|
14647
|
+
function logClaude(claude) {
|
|
14648
|
+
if (!claude.available) {
|
|
14649
|
+
logger.info({ reason: claude.reason }, `claude ${claude.reason ?? "unavailable"}`);
|
|
14650
|
+
return;
|
|
14651
|
+
}
|
|
14652
|
+
logger.info(
|
|
14653
|
+
{ version: claude.version, models: claude.models.map((m) => m.id), authMode: claude.authMode },
|
|
14654
|
+
`claude available on ${claude.models.map((m) => m.id).join(", ") || "the curated model list"}`
|
|
14655
|
+
);
|
|
14656
|
+
}
|
|
14232
14657
|
function holdLock(root, record) {
|
|
14233
14658
|
let held;
|
|
14234
14659
|
return {
|
|
@@ -14321,7 +14746,6 @@ async function compose(cfg, lock, host, preflight) {
|
|
|
14321
14746
|
);
|
|
14322
14747
|
});
|
|
14323
14748
|
const codexPath = resolveCodexPath(cfg, process.env, () => findCodexOnPath(process.env));
|
|
14324
|
-
const detect = () => detectProviders({ path: codexPath, run: runCodex, listModels: listCodexModels });
|
|
14325
14749
|
let providers = {
|
|
14326
14750
|
claude: { available: true, models: [] },
|
|
14327
14751
|
codex: { available: false, reason: "not_installed", models: [] }
|
|
@@ -14329,9 +14753,23 @@ async function compose(cfg, lock, host, preflight) {
|
|
|
14329
14753
|
const pty = await loadPty();
|
|
14330
14754
|
const memberTerminalIds = memberTerminalIdsOf(cfg);
|
|
14331
14755
|
const kinds2 = handledKinds({ terminal: pty !== null });
|
|
14332
|
-
const runDir =
|
|
14756
|
+
const runDir = join31(cayenneDir(), "run");
|
|
14333
14757
|
prepareRunDir(runDir, { git: findOnPath("git", process.env.PATH) });
|
|
14334
14758
|
const gitHome = repositoryGitHome(runDir);
|
|
14759
|
+
const claudeProbeEnv = claudeEnv(claudeConfigDir);
|
|
14760
|
+
const detect = () => detectProviders({
|
|
14761
|
+
path: codexPath,
|
|
14762
|
+
run: runCodex,
|
|
14763
|
+
listModels: listCodexModels,
|
|
14764
|
+
claude: () => detectClaude({
|
|
14765
|
+
version: claudeCodeVersion,
|
|
14766
|
+
probe: () => probeClaude({
|
|
14767
|
+
env: claudeProbeEnv,
|
|
14768
|
+
cwd: runDir,
|
|
14769
|
+
settingSources: cfg.loadUserSettings ? ["user"] : []
|
|
14770
|
+
})
|
|
14771
|
+
})
|
|
14772
|
+
});
|
|
14335
14773
|
for (const path of githubNetrcFiles()) {
|
|
14336
14774
|
logger.warn(
|
|
14337
14775
|
{ path },
|
|
@@ -14411,6 +14849,7 @@ async function compose(cfg, lock, host, preflight) {
|
|
|
14411
14849
|
});
|
|
14412
14850
|
const registered = capabilities();
|
|
14413
14851
|
let loggedCodex = null;
|
|
14852
|
+
let loggedClaude = null;
|
|
14414
14853
|
const refresh = async () => {
|
|
14415
14854
|
providers = await detect();
|
|
14416
14855
|
const line = JSON.stringify(providers.codex);
|
|
@@ -14418,6 +14857,11 @@ async function compose(cfg, lock, host, preflight) {
|
|
|
14418
14857
|
loggedCodex = line;
|
|
14419
14858
|
logCodex(providers.codex);
|
|
14420
14859
|
}
|
|
14860
|
+
const claudeLine = JSON.stringify(providers.claude);
|
|
14861
|
+
if (claudeLine !== loggedClaude) {
|
|
14862
|
+
loggedClaude = claudeLine;
|
|
14863
|
+
logClaude(providers.claude);
|
|
14864
|
+
}
|
|
14421
14865
|
return capabilities();
|
|
14422
14866
|
};
|
|
14423
14867
|
let identity;
|
|
@@ -14478,15 +14922,15 @@ async function compose(cfg, lock, host, preflight) {
|
|
|
14478
14922
|
const trustedRoots = trustedRepoRoots(cfg.trustRepoSettings);
|
|
14479
14923
|
if (!cfg.loadUserSettings) {
|
|
14480
14924
|
const guards = readUserGuards(sessionClaudeConfigDir(claudeEnv(claudeConfigDir)));
|
|
14481
|
-
const warning = userGuardsWarning(guards,
|
|
14925
|
+
const warning = userGuardsWarning(guards, join31(cayenneDir(), "config.json"));
|
|
14482
14926
|
if (warning) {
|
|
14483
14927
|
const { settingsFile, preToolUseHooks, denyRules } = guards;
|
|
14484
14928
|
logger.warn({ settingsFile, preToolUseHooks, denyRules }, warning);
|
|
14485
14929
|
}
|
|
14486
14930
|
}
|
|
14487
|
-
const repoSessions = createRepoSessions(
|
|
14488
|
-
const worktreeOwners = createWorktreeOwners(
|
|
14489
|
-
const sessionWorktrees = createSessionWorktrees(
|
|
14931
|
+
const repoSessions = createRepoSessions(join31(cayenneHome(), "repo-sessions"));
|
|
14932
|
+
const worktreeOwners = createWorktreeOwners(join31(cayenneHome(), "worktrees"));
|
|
14933
|
+
const sessionWorktrees = createSessionWorktrees(join31(cayenneHome(), "session-worktrees"));
|
|
14490
14934
|
const git = createGitOps({
|
|
14491
14935
|
protectedPaths: protectedPaths(claudeConfigDir),
|
|
14492
14936
|
home: homedir6(),
|
|
@@ -14514,7 +14958,7 @@ async function compose(cfg, lock, host, preflight) {
|
|
|
14514
14958
|
worktreeOwners,
|
|
14515
14959
|
sessionWorktrees,
|
|
14516
14960
|
// And which sessions were started from a task, so their secret-file guard outlives a restart.
|
|
14517
|
-
taskSessions: createTaskSessions(
|
|
14961
|
+
taskSessions: createTaskSessions(join31(cayenneHome(), "task-sessions")),
|
|
14518
14962
|
// The paths no `git.diff` returns are paths no task session reads (protocol §2.8).
|
|
14519
14963
|
protectedPaths: protectedPaths(claudeConfigDir),
|
|
14520
14964
|
// D50: where repository chats run, which of them are, and their credentials' run dir. A
|
|
@@ -14873,9 +15317,10 @@ var init_start = __esm({
|
|
|
14873
15317
|
"src/start.ts"() {
|
|
14874
15318
|
"use strict";
|
|
14875
15319
|
init_src();
|
|
15320
|
+
init_detect();
|
|
14876
15321
|
init_claude_dir();
|
|
14877
15322
|
init_cloud();
|
|
14878
|
-
|
|
15323
|
+
init_detect2();
|
|
14879
15324
|
init_dispatcher();
|
|
14880
15325
|
init_source();
|
|
14881
15326
|
init_config();
|
|
@@ -14918,9 +15363,9 @@ var init_start = __esm({
|
|
|
14918
15363
|
});
|
|
14919
15364
|
|
|
14920
15365
|
// src/cli.ts
|
|
14921
|
-
import { existsSync as existsSync7, mkdirSync as mkdirSync10, readFileSync as
|
|
15366
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync10, readFileSync as readFileSync14, realpathSync as realpathSync20, rmSync as rmSync10, writeFileSync as writeFileSync7 } from "fs";
|
|
14922
15367
|
import { homedir as homedir7, hostname as hostname2, userInfo } from "os";
|
|
14923
|
-
import { dirname as
|
|
15368
|
+
import { dirname as dirname12, join as join32 } from "path";
|
|
14924
15369
|
import { Command } from "commander";
|
|
14925
15370
|
import prompts from "prompts";
|
|
14926
15371
|
|
|
@@ -15418,14 +15863,14 @@ var configForWrite = () => {
|
|
|
15418
15863
|
});
|
|
15419
15864
|
return cfg;
|
|
15420
15865
|
};
|
|
15421
|
-
var WEB_URL = "https://cayenne
|
|
15866
|
+
var WEB_URL = "https://app.cayenne.sh";
|
|
15422
15867
|
var PKG = "@jpcasabiancai/cayenne";
|
|
15423
15868
|
var service = () => {
|
|
15424
15869
|
const exec4 = nodeExec();
|
|
15425
15870
|
return { exec: exec4, launchd: makeLaunchd(exec4, userInfo().uid), label: plistLabel(currentProfile()) };
|
|
15426
15871
|
};
|
|
15427
15872
|
var writePlistFile = (path, xml2) => {
|
|
15428
|
-
mkdirSync10(
|
|
15873
|
+
mkdirSync10(dirname12(path), { recursive: true });
|
|
15429
15874
|
writeFileSync7(path, xml2, { mode: 420 });
|
|
15430
15875
|
};
|
|
15431
15876
|
var requireMac = () => {
|
|
@@ -15574,7 +16019,7 @@ program.command("doctor").description("Check the install and say how to fix what
|
|
|
15574
16019
|
if (checks.some((c) => !c.ok)) process.exitCode = 1;
|
|
15575
16020
|
});
|
|
15576
16021
|
program.command("logs").description("Show the service log").option("-f, --follow", "keep streaming").action(async (opts) => {
|
|
15577
|
-
const file =
|
|
16022
|
+
const file = join32(cayenneDir(), "logs", "runner.log");
|
|
15578
16023
|
if (!existsSync7(file)) throw new Error(`no log yet at ${file}. Run \`cayenne up\` first.`);
|
|
15579
16024
|
const print = (chunk) => {
|
|
15580
16025
|
for (const line of chunk.split("\n")) {
|
|
@@ -15587,12 +16032,12 @@ program.command("logs").description("Show the service log").option("-f, --follow
|
|
|
15587
16032
|
}
|
|
15588
16033
|
}
|
|
15589
16034
|
};
|
|
15590
|
-
print(
|
|
16035
|
+
print(readFileSync14(file, "utf8"));
|
|
15591
16036
|
if (opts.follow) {
|
|
15592
16037
|
const { watch: watch2 } = await import("fs");
|
|
15593
|
-
let size =
|
|
16038
|
+
let size = readFileSync14(file).length;
|
|
15594
16039
|
watch2(file, () => {
|
|
15595
|
-
const buf =
|
|
16040
|
+
const buf = readFileSync14(file);
|
|
15596
16041
|
if (buf.length > size) print(buf.subarray(size).toString("utf8"));
|
|
15597
16042
|
size = buf.length;
|
|
15598
16043
|
});
|
|
@@ -15620,7 +16065,7 @@ program.command("update").description("Re-pin the installed app and restart the
|
|
|
15620
16065
|
cliPath: record.cliPath,
|
|
15621
16066
|
profile: currentProfile(),
|
|
15622
16067
|
pathEnv: record.pathEnv,
|
|
15623
|
-
logDir:
|
|
16068
|
+
logDir: join32(dir, "logs")
|
|
15624
16069
|
})
|
|
15625
16070
|
);
|
|
15626
16071
|
await launchd.bootout(label);
|
|
@@ -15641,7 +16086,7 @@ program.command("uninstall").description("Stop the service and remove the launch
|
|
|
15641
16086
|
if (dir === "/" || dir === home || home.startsWith(`${dir}/`)) {
|
|
15642
16087
|
throw new Error(`refusing to purge ${dir}: it is the home directory or an ancestor of it`);
|
|
15643
16088
|
}
|
|
15644
|
-
if (!existsSync7(
|
|
16089
|
+
if (!existsSync7(join32(dir, "config.json")) && !existsSync7(join32(dir, "app", "install.json"))) {
|
|
15645
16090
|
throw new Error(`refusing to purge ${dir}: it does not look like a Cayenne profile`);
|
|
15646
16091
|
}
|
|
15647
16092
|
if (!opts.yes) {
|