@mrciphersmith/keryx 0.2.153 → 0.2.154
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -0
- package/dist/cli.js +2471 -103
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -51578,7 +51578,7 @@ import path47 from "path";
|
|
|
51578
51578
|
// package.json
|
|
51579
51579
|
var package_default = {
|
|
51580
51580
|
name: "@mrciphersmith/keryx",
|
|
51581
|
-
version: "0.2.
|
|
51581
|
+
version: "0.2.154",
|
|
51582
51582
|
description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
|
|
51583
51583
|
private: false,
|
|
51584
51584
|
publishConfig: {
|
|
@@ -51623,7 +51623,7 @@ var package_default = {
|
|
|
51623
51623
|
typecheck: "tsc --noEmit",
|
|
51624
51624
|
"typecheck:scripts": "tsc --project tsconfig.scripts.json --noEmit",
|
|
51625
51625
|
test: "bun test",
|
|
51626
|
-
"test:core": "bun test src/cli src/core src/shell-source-audits.test.ts src/assets/ src/capability/ src/commands/ src/contracts/ src/ctx/ src/eval/ src/flow/ src/forgetting/ src/gdgraph/ src/gdskills/ src/health/ src/job/ src/lib/ src/mcp/ src/memory/ src/metrics/ src/retention/ src/review/ src/sac/ src/security/ src/standard/ src/sync/ src/testing/ src/wiki/",
|
|
51626
|
+
"test:core": "bun test src/cli src/core src/shell-source-audits.test.ts src/acp/ src/assets/ src/capability/ src/commands/ src/contracts/ src/ctx/ src/eval/ src/flow/ src/forgetting/ src/gdgraph/ src/gdskills/ src/health/ src/job/ src/lib/ src/mcp/ src/memory/ src/metrics/ src/retention/ src/review/ src/sac/ src/security/ src/standard/ src/sync/ src/testing/ src/trigger/ src/wiki/",
|
|
51627
51627
|
"test:client:terminal": "bun test src/tui/ src/commands/shell",
|
|
51628
51628
|
"test:client:streaming": "bun test src/harness/provider/",
|
|
51629
51629
|
"test:client:cancel-resume": "bun test src/harness/run/ src/harness/resume/ src/harness/session/ src/session/ src/bus/ src/commands/sessions",
|
|
@@ -73546,6 +73546,20 @@ var COMMAND_DESCRIPTORS = [
|
|
|
73546
73546
|
"writes the memory provenance record"
|
|
73547
73547
|
]
|
|
73548
73548
|
},
|
|
73549
|
+
{
|
|
73550
|
+
module: "trigger",
|
|
73551
|
+
command: "trigger run",
|
|
73552
|
+
summary: `Perform exactly one pass of a declared trigger's action (.metaproject/triggers.json). Only "reconcile" ` + '(-> sync --apply) and "rebuild" (-> gdgraph build) run today; "open-flow"/"flow-next" refuse cleanly.',
|
|
73553
|
+
intent: ["\u0437\u0430\u043F\u0443\u0441\u0442\u0438 \u0442\u0440\u0438\u0433\u0433\u0435\u0440", "run trigger", "fire trigger", "trigger run"],
|
|
73554
|
+
args: [{ name: "<name>", type: "string", required: true, desc: "the trigger's name, as declared in .metaproject/triggers.json" }],
|
|
73555
|
+
json: false,
|
|
73556
|
+
read: false,
|
|
73557
|
+
sideEffects: [
|
|
73558
|
+
'reconcile: writes graph/wiki/memory via "keryx sync --apply"',
|
|
73559
|
+
'rebuild: writes the code graph via "keryx gdgraph build"',
|
|
73560
|
+
"briefly writes .metaproject/data/trigger/.run.lock while the action runs"
|
|
73561
|
+
]
|
|
73562
|
+
},
|
|
73549
73563
|
{
|
|
73550
73564
|
module: "testing",
|
|
73551
73565
|
command: "test analyze",
|
|
@@ -111598,12 +111612,1364 @@ Shell:
|
|
|
111598
111612
|
`);
|
|
111599
111613
|
}
|
|
111600
111614
|
|
|
111601
|
-
// src/commands/
|
|
111615
|
+
// src/commands/acp.ts
|
|
111616
|
+
import { randomUUID as randomUUID37 } from "crypto";
|
|
111617
|
+
|
|
111618
|
+
// src/acp/server.ts
|
|
111619
|
+
init_agent();
|
|
111602
111620
|
import { randomUUID as randomUUID36 } from "crypto";
|
|
111603
111621
|
|
|
111622
|
+
// src/acp/agent-io.ts
|
|
111623
|
+
import { randomUUID as randomUUID35 } from "crypto";
|
|
111624
|
+
|
|
111625
|
+
// src/acp/permission.ts
|
|
111626
|
+
var ACP_PERMISSION_OPTION_IDS = {
|
|
111627
|
+
allowOnce: "allow_once",
|
|
111628
|
+
allowAlways: "allow_always",
|
|
111629
|
+
rejectOnce: "reject_once",
|
|
111630
|
+
rejectAlways: "reject_always"
|
|
111631
|
+
};
|
|
111632
|
+
var OPTION_ALLOW_ONCE = Object.freeze({
|
|
111633
|
+
optionId: ACP_PERMISSION_OPTION_IDS.allowOnce,
|
|
111634
|
+
name: "Allow once",
|
|
111635
|
+
kind: "allow_once"
|
|
111636
|
+
});
|
|
111637
|
+
var OPTION_ALLOW_ALWAYS = Object.freeze({
|
|
111638
|
+
optionId: ACP_PERMISSION_OPTION_IDS.allowAlways,
|
|
111639
|
+
name: "Always allow",
|
|
111640
|
+
kind: "allow_always"
|
|
111641
|
+
});
|
|
111642
|
+
var OPTION_REJECT_ONCE = Object.freeze({
|
|
111643
|
+
optionId: ACP_PERMISSION_OPTION_IDS.rejectOnce,
|
|
111644
|
+
name: "Reject",
|
|
111645
|
+
kind: "reject_once"
|
|
111646
|
+
});
|
|
111647
|
+
var OPTION_REJECT_ALWAYS = Object.freeze({
|
|
111648
|
+
optionId: ACP_PERMISSION_OPTION_IDS.rejectAlways,
|
|
111649
|
+
name: "Always reject",
|
|
111650
|
+
kind: "reject_always"
|
|
111651
|
+
});
|
|
111652
|
+
function permissionIsEscalated(meta) {
|
|
111653
|
+
return meta?.destructive === true || meta?.credentials === true || meta?.publishLease === true || meta?.untrustedOrigin === true;
|
|
111654
|
+
}
|
|
111655
|
+
function permissionOptionsFor(meta) {
|
|
111656
|
+
return permissionIsEscalated(meta) ? [OPTION_ALLOW_ONCE, OPTION_REJECT_ONCE, OPTION_REJECT_ALWAYS] : [OPTION_ALLOW_ONCE, OPTION_ALLOW_ALWAYS, OPTION_REJECT_ONCE, OPTION_REJECT_ALWAYS];
|
|
111657
|
+
}
|
|
111658
|
+
function kindOf(optionId, options) {
|
|
111659
|
+
return options.find((option) => option.optionId === optionId)?.kind;
|
|
111660
|
+
}
|
|
111661
|
+
function approvalFromPermissionResponse(response, options, fingerprint) {
|
|
111662
|
+
const raw = response.outcome;
|
|
111663
|
+
if (typeof raw !== "object" || raw === null) {
|
|
111664
|
+
return false;
|
|
111665
|
+
}
|
|
111666
|
+
const outcome = raw;
|
|
111667
|
+
if (outcome.outcome !== "selected") {
|
|
111668
|
+
return false;
|
|
111669
|
+
}
|
|
111670
|
+
switch (kindOf(outcome.optionId, options)) {
|
|
111671
|
+
case "allow_once":
|
|
111672
|
+
return true;
|
|
111673
|
+
case "allow_always":
|
|
111674
|
+
return fingerprint === undefined ? true : { approved: true, fingerprint };
|
|
111675
|
+
default:
|
|
111676
|
+
return false;
|
|
111677
|
+
}
|
|
111678
|
+
}
|
|
111679
|
+
|
|
111680
|
+
// src/acp/jsonrpc.ts
|
|
111681
|
+
var JSONRPC_VERSION = "2.0";
|
|
111682
|
+
var JSON_RPC_ERROR_CODES = {
|
|
111683
|
+
parseError: -32700,
|
|
111684
|
+
invalidRequest: -32600,
|
|
111685
|
+
methodNotFound: -32601,
|
|
111686
|
+
invalidParams: -32602,
|
|
111687
|
+
internalError: -32603,
|
|
111688
|
+
requestCancelled: -32800,
|
|
111689
|
+
authRequired: -32000,
|
|
111690
|
+
resourceNotFound: -32002
|
|
111691
|
+
};
|
|
111692
|
+
|
|
111693
|
+
class AcpError extends Error {
|
|
111694
|
+
code;
|
|
111695
|
+
data;
|
|
111696
|
+
constructor(code, message, data) {
|
|
111697
|
+
super(message);
|
|
111698
|
+
this.name = "AcpError";
|
|
111699
|
+
this.code = code;
|
|
111700
|
+
this.data = data;
|
|
111701
|
+
}
|
|
111702
|
+
toErrorObject() {
|
|
111703
|
+
return jsonRpcError(this.code, this.message, this.data);
|
|
111704
|
+
}
|
|
111705
|
+
static from(code, message, data) {
|
|
111706
|
+
return new AcpError(code, message, data);
|
|
111707
|
+
}
|
|
111708
|
+
}
|
|
111709
|
+
function jsonRpcError(code, message, data) {
|
|
111710
|
+
return data === undefined ? { code, message } : { code, message, data };
|
|
111711
|
+
}
|
|
111712
|
+
function parseError(data) {
|
|
111713
|
+
return jsonRpcError(JSON_RPC_ERROR_CODES.parseError, "Parse error", data);
|
|
111714
|
+
}
|
|
111715
|
+
function invalidRequest(data) {
|
|
111716
|
+
return jsonRpcError(JSON_RPC_ERROR_CODES.invalidRequest, "Invalid request", data);
|
|
111717
|
+
}
|
|
111718
|
+
function methodNotFound(method, data) {
|
|
111719
|
+
return jsonRpcError(JSON_RPC_ERROR_CODES.methodNotFound, `Method not found: ${method}`, data);
|
|
111720
|
+
}
|
|
111721
|
+
function invalidParams(message, data) {
|
|
111722
|
+
return jsonRpcError(JSON_RPC_ERROR_CODES.invalidParams, message, data);
|
|
111723
|
+
}
|
|
111724
|
+
function internalError(message, data) {
|
|
111725
|
+
return jsonRpcError(JSON_RPC_ERROR_CODES.internalError, message, data);
|
|
111726
|
+
}
|
|
111727
|
+
function successResponse(id, result) {
|
|
111728
|
+
return { jsonrpc: JSONRPC_VERSION, id, result };
|
|
111729
|
+
}
|
|
111730
|
+
function errorResponse(id, error) {
|
|
111731
|
+
return { jsonrpc: JSONRPC_VERSION, id, error };
|
|
111732
|
+
}
|
|
111733
|
+
function requestMessage(id, method, params) {
|
|
111734
|
+
return params === undefined ? { jsonrpc: JSONRPC_VERSION, id, method } : { jsonrpc: JSONRPC_VERSION, id, method, params };
|
|
111735
|
+
}
|
|
111736
|
+
function notificationMessage(method, params) {
|
|
111737
|
+
return params === undefined ? { jsonrpc: JSONRPC_VERSION, method } : { jsonrpc: JSONRPC_VERSION, method, params };
|
|
111738
|
+
}
|
|
111739
|
+
function isPlainObject10(value) {
|
|
111740
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
111741
|
+
}
|
|
111742
|
+
function isJsonRpcId(value) {
|
|
111743
|
+
return value === null || typeof value === "string" || typeof value === "number";
|
|
111744
|
+
}
|
|
111745
|
+
function recoverId(value) {
|
|
111746
|
+
if (!isPlainObject10(value)) {
|
|
111747
|
+
return null;
|
|
111748
|
+
}
|
|
111749
|
+
const id = value["id"];
|
|
111750
|
+
return isJsonRpcId(id) ? id : null;
|
|
111751
|
+
}
|
|
111752
|
+
function classifyJsonRpcMessage(value) {
|
|
111753
|
+
if (!isPlainObject10(value)) {
|
|
111754
|
+
return {
|
|
111755
|
+
kind: "invalid",
|
|
111756
|
+
id: null,
|
|
111757
|
+
error: invalidRequest({ reason: "message is not a JSON object" })
|
|
111758
|
+
};
|
|
111759
|
+
}
|
|
111760
|
+
if (value["jsonrpc"] !== JSONRPC_VERSION) {
|
|
111761
|
+
return {
|
|
111762
|
+
kind: "invalid",
|
|
111763
|
+
id: recoverId(value),
|
|
111764
|
+
error: invalidRequest({
|
|
111765
|
+
reason: `jsonrpc must be "${JSONRPC_VERSION}"`,
|
|
111766
|
+
received: value["jsonrpc"]
|
|
111767
|
+
})
|
|
111768
|
+
};
|
|
111769
|
+
}
|
|
111770
|
+
const hasMethod = "method" in value;
|
|
111771
|
+
const hasResult = "result" in value;
|
|
111772
|
+
const hasError = "error" in value;
|
|
111773
|
+
if (hasMethod) {
|
|
111774
|
+
const method = value["method"];
|
|
111775
|
+
if (typeof method !== "string") {
|
|
111776
|
+
return {
|
|
111777
|
+
kind: "invalid",
|
|
111778
|
+
id: recoverId(value),
|
|
111779
|
+
error: invalidRequest({ reason: "method must be a string", received: method })
|
|
111780
|
+
};
|
|
111781
|
+
}
|
|
111782
|
+
if (!("id" in value)) {
|
|
111783
|
+
const notification = "params" in value ? { jsonrpc: JSONRPC_VERSION, method, params: value["params"] } : { jsonrpc: JSONRPC_VERSION, method };
|
|
111784
|
+
return { kind: "notification", message: notification };
|
|
111785
|
+
}
|
|
111786
|
+
const id = value["id"];
|
|
111787
|
+
if (!isJsonRpcId(id)) {
|
|
111788
|
+
return {
|
|
111789
|
+
kind: "invalid",
|
|
111790
|
+
id: null,
|
|
111791
|
+
error: invalidRequest({ reason: "id must be a string, number or null", received: id })
|
|
111792
|
+
};
|
|
111793
|
+
}
|
|
111794
|
+
const request = "params" in value ? { jsonrpc: JSONRPC_VERSION, id, method, params: value["params"] } : { jsonrpc: JSONRPC_VERSION, id, method };
|
|
111795
|
+
return { kind: "request", message: request };
|
|
111796
|
+
}
|
|
111797
|
+
if (hasResult || hasError) {
|
|
111798
|
+
const id = value["id"];
|
|
111799
|
+
if (!isJsonRpcId(id)) {
|
|
111800
|
+
return {
|
|
111801
|
+
kind: "invalid",
|
|
111802
|
+
id: null,
|
|
111803
|
+
error: invalidRequest({ reason: "response id must be a string, number or null" })
|
|
111804
|
+
};
|
|
111805
|
+
}
|
|
111806
|
+
if (hasError) {
|
|
111807
|
+
const raw = value["error"];
|
|
111808
|
+
const code = isPlainObject10(raw) ? raw["code"] : undefined;
|
|
111809
|
+
const message = isPlainObject10(raw) ? raw["message"] : undefined;
|
|
111810
|
+
if (typeof code !== "number" || typeof message !== "string") {
|
|
111811
|
+
return {
|
|
111812
|
+
kind: "invalid",
|
|
111813
|
+
id,
|
|
111814
|
+
error: invalidRequest({ reason: "error must carry a numeric code and a string message" })
|
|
111815
|
+
};
|
|
111816
|
+
}
|
|
111817
|
+
const data = isPlainObject10(raw) ? raw["data"] : undefined;
|
|
111818
|
+
return { kind: "response", message: errorResponse(id, jsonRpcError(code, message, data)) };
|
|
111819
|
+
}
|
|
111820
|
+
return { kind: "response", message: successResponse(id, value["result"]) };
|
|
111821
|
+
}
|
|
111822
|
+
return {
|
|
111823
|
+
kind: "invalid",
|
|
111824
|
+
id: recoverId(value),
|
|
111825
|
+
error: invalidRequest({ reason: "message carries neither method, result nor error" })
|
|
111826
|
+
};
|
|
111827
|
+
}
|
|
111828
|
+
|
|
111829
|
+
// src/acp/protocol.ts
|
|
111830
|
+
var ACP_PROTOCOL_VERSION = 1;
|
|
111831
|
+
var ACP_SUPPORTED_PROTOCOL_VERSIONS = [ACP_PROTOCOL_VERSION];
|
|
111832
|
+
var ACP_AGENT_METHODS = {
|
|
111833
|
+
initialize: "initialize",
|
|
111834
|
+
authenticate: "authenticate",
|
|
111835
|
+
logout: "logout",
|
|
111836
|
+
sessionNew: "session/new",
|
|
111837
|
+
sessionLoad: "session/load",
|
|
111838
|
+
sessionResume: "session/resume",
|
|
111839
|
+
sessionPrompt: "session/prompt",
|
|
111840
|
+
sessionCancel: "session/cancel",
|
|
111841
|
+
sessionClose: "session/close",
|
|
111842
|
+
sessionDelete: "session/delete",
|
|
111843
|
+
sessionList: "session/list",
|
|
111844
|
+
sessionSetMode: "session/set_mode",
|
|
111845
|
+
sessionSetConfigOption: "session/set_config_option"
|
|
111846
|
+
};
|
|
111847
|
+
var ACP_CLIENT_METHODS = {
|
|
111848
|
+
fsReadTextFile: "fs/read_text_file",
|
|
111849
|
+
fsWriteTextFile: "fs/write_text_file",
|
|
111850
|
+
sessionRequestPermission: "session/request_permission",
|
|
111851
|
+
sessionUpdate: "session/update",
|
|
111852
|
+
elicitationCreate: "elicitation/create",
|
|
111853
|
+
elicitationComplete: "elicitation/complete",
|
|
111854
|
+
terminalCreate: "terminal/create",
|
|
111855
|
+
terminalOutput: "terminal/output",
|
|
111856
|
+
terminalKill: "terminal/kill",
|
|
111857
|
+
terminalRelease: "terminal/release",
|
|
111858
|
+
terminalWaitForExit: "terminal/wait_for_exit"
|
|
111859
|
+
};
|
|
111860
|
+
var ACP_IMPLEMENTED_AGENT_METHODS = [
|
|
111861
|
+
ACP_AGENT_METHODS.initialize,
|
|
111862
|
+
ACP_AGENT_METHODS.sessionNew,
|
|
111863
|
+
ACP_AGENT_METHODS.sessionLoad,
|
|
111864
|
+
ACP_AGENT_METHODS.sessionList,
|
|
111865
|
+
ACP_AGENT_METHODS.sessionPrompt,
|
|
111866
|
+
ACP_AGENT_METHODS.sessionCancel
|
|
111867
|
+
];
|
|
111868
|
+
var ACP_REFUSED_AGENT_METHODS = new Map([
|
|
111869
|
+
[
|
|
111870
|
+
ACP_AGENT_METHODS.authenticate,
|
|
111871
|
+
{
|
|
111872
|
+
code: JSON_RPC_ERROR_CODES.methodNotFound,
|
|
111873
|
+
reason: "keryx advertises no authMethods; there is nothing to authenticate against"
|
|
111874
|
+
}
|
|
111875
|
+
],
|
|
111876
|
+
[
|
|
111877
|
+
ACP_AGENT_METHODS.logout,
|
|
111878
|
+
{
|
|
111879
|
+
code: JSON_RPC_ERROR_CODES.methodNotFound,
|
|
111880
|
+
reason: "keryx does not advertise agentCapabilities.auth.logout"
|
|
111881
|
+
}
|
|
111882
|
+
],
|
|
111883
|
+
[
|
|
111884
|
+
ACP_AGENT_METHODS.sessionResume,
|
|
111885
|
+
{
|
|
111886
|
+
code: JSON_RPC_ERROR_CODES.methodNotFound,
|
|
111887
|
+
reason: "keryx does not advertise sessionCapabilities.resume; use session/load, which replays history"
|
|
111888
|
+
}
|
|
111889
|
+
],
|
|
111890
|
+
[
|
|
111891
|
+
ACP_AGENT_METHODS.sessionClose,
|
|
111892
|
+
{
|
|
111893
|
+
code: JSON_RPC_ERROR_CODES.methodNotFound,
|
|
111894
|
+
reason: "keryx does not advertise sessionCapabilities.close; sessions are durable on disk"
|
|
111895
|
+
}
|
|
111896
|
+
],
|
|
111897
|
+
[
|
|
111898
|
+
ACP_AGENT_METHODS.sessionDelete,
|
|
111899
|
+
{
|
|
111900
|
+
code: JSON_RPC_ERROR_CODES.methodNotFound,
|
|
111901
|
+
reason: "keryx does not advertise sessionCapabilities.delete; session retention is an operator decision, not a client one"
|
|
111902
|
+
}
|
|
111903
|
+
],
|
|
111904
|
+
[
|
|
111905
|
+
ACP_AGENT_METHODS.sessionSetMode,
|
|
111906
|
+
{
|
|
111907
|
+
code: JSON_RPC_ERROR_CODES.methodNotFound,
|
|
111908
|
+
reason: "keryx returns no modes from session/new, so there is no mode to set"
|
|
111909
|
+
}
|
|
111910
|
+
],
|
|
111911
|
+
[
|
|
111912
|
+
ACP_AGENT_METHODS.sessionSetConfigOption,
|
|
111913
|
+
{
|
|
111914
|
+
code: JSON_RPC_ERROR_CODES.methodNotFound,
|
|
111915
|
+
reason: "keryx returns no configOptions from session/new, so there is no option to set"
|
|
111916
|
+
}
|
|
111917
|
+
]
|
|
111918
|
+
]);
|
|
111919
|
+
function refusalError(method) {
|
|
111920
|
+
const refusal = ACP_REFUSED_AGENT_METHODS.get(method);
|
|
111921
|
+
if (refusal === undefined) {
|
|
111922
|
+
return;
|
|
111923
|
+
}
|
|
111924
|
+
return methodNotFound(method, {
|
|
111925
|
+
reason: refusal.reason,
|
|
111926
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
111927
|
+
implemented: ACP_IMPLEMENTED_AGENT_METHODS
|
|
111928
|
+
});
|
|
111929
|
+
}
|
|
111930
|
+
function unsupportedVersionError(requested, reason) {
|
|
111931
|
+
return invalidParams(`Unsupported ACP protocol version: ${reason}`, {
|
|
111932
|
+
requested,
|
|
111933
|
+
supported: ACP_SUPPORTED_PROTOCOL_VERSIONS,
|
|
111934
|
+
latest: ACP_PROTOCOL_VERSION
|
|
111935
|
+
});
|
|
111936
|
+
}
|
|
111937
|
+
function negotiateProtocolVersion(requested) {
|
|
111938
|
+
if (typeof requested !== "number" || !Number.isInteger(requested)) {
|
|
111939
|
+
return {
|
|
111940
|
+
kind: "refuse",
|
|
111941
|
+
error: unsupportedVersionError(requested, "protocolVersion must be an integer")
|
|
111942
|
+
};
|
|
111943
|
+
}
|
|
111944
|
+
if (requested < 0 || requested > 65535) {
|
|
111945
|
+
return {
|
|
111946
|
+
kind: "refuse",
|
|
111947
|
+
error: unsupportedVersionError(requested, "protocolVersion is outside the uint16 range")
|
|
111948
|
+
};
|
|
111949
|
+
}
|
|
111950
|
+
if (ACP_SUPPORTED_PROTOCOL_VERSIONS.includes(requested)) {
|
|
111951
|
+
return { kind: "exact", version: requested };
|
|
111952
|
+
}
|
|
111953
|
+
if (requested > ACP_PROTOCOL_VERSION) {
|
|
111954
|
+
return { kind: "offer", version: ACP_PROTOCOL_VERSION, requested };
|
|
111955
|
+
}
|
|
111956
|
+
return {
|
|
111957
|
+
kind: "refuse",
|
|
111958
|
+
error: unsupportedVersionError(requested, `keryx serves ${ACP_SUPPORTED_PROTOCOL_VERSIONS.join(", ")} and cannot speak anything older`)
|
|
111959
|
+
};
|
|
111960
|
+
}
|
|
111961
|
+
function textBlock(text) {
|
|
111962
|
+
return { type: "text", text };
|
|
111963
|
+
}
|
|
111964
|
+
var KERYX_AGENT_CAPABILITIES = Object.freeze({
|
|
111965
|
+
loadSession: true,
|
|
111966
|
+
promptCapabilities: Object.freeze({ image: false, audio: false, embeddedContext: true }),
|
|
111967
|
+
mcpCapabilities: Object.freeze({ http: false, sse: false }),
|
|
111968
|
+
sessionCapabilities: Object.freeze({ list: Object.freeze({}) }),
|
|
111969
|
+
auth: Object.freeze({})
|
|
111970
|
+
});
|
|
111971
|
+
var KERYX_AUTH_METHODS = Object.freeze([]);
|
|
111972
|
+
function requireObjectParams(params, method) {
|
|
111973
|
+
if (typeof params !== "object" || params === null || Array.isArray(params)) {
|
|
111974
|
+
const error = invalidParams(`${method}: params must be an object`, { received: params });
|
|
111975
|
+
throw new AcpError(error.code, error.message, error.data);
|
|
111976
|
+
}
|
|
111977
|
+
return params;
|
|
111978
|
+
}
|
|
111979
|
+
function requireStringField(params, field, method) {
|
|
111980
|
+
const value = params[field];
|
|
111981
|
+
if (typeof value !== "string" || value === "") {
|
|
111982
|
+
const error = invalidParams(`${method}: ${field} must be a non-empty string`, {
|
|
111983
|
+
field,
|
|
111984
|
+
received: value
|
|
111985
|
+
});
|
|
111986
|
+
throw new AcpError(error.code, error.message, error.data);
|
|
111987
|
+
}
|
|
111988
|
+
return value;
|
|
111989
|
+
}
|
|
111990
|
+
|
|
111991
|
+
// src/acp/agent-io.ts
|
|
111992
|
+
var TOOL_KIND_BY_NAME = {
|
|
111993
|
+
get_cwd: "read",
|
|
111994
|
+
list_dir: "read",
|
|
111995
|
+
read_file: "read",
|
|
111996
|
+
search_code: "search",
|
|
111997
|
+
apply_patch: "edit",
|
|
111998
|
+
shell_exec: "execute",
|
|
111999
|
+
web_fetch: "fetch",
|
|
112000
|
+
web_search: "search"
|
|
112001
|
+
};
|
|
112002
|
+
function toolKindFor(name) {
|
|
112003
|
+
return TOOL_KIND_BY_NAME[name] ?? "other";
|
|
112004
|
+
}
|
|
112005
|
+
function tryParseJson(text) {
|
|
112006
|
+
try {
|
|
112007
|
+
return JSON.parse(text);
|
|
112008
|
+
} catch {
|
|
112009
|
+
return text;
|
|
112010
|
+
}
|
|
112011
|
+
}
|
|
112012
|
+
function createAcpAgentIo(sessionId, send, askPermission) {
|
|
112013
|
+
const pendingByName = new Map;
|
|
112014
|
+
const emit = (update) => send(sessionId, update);
|
|
112015
|
+
const openToolCall = (name, input, claimed) => {
|
|
112016
|
+
const toolCallId = randomUUID35();
|
|
112017
|
+
const queue = pendingByName.get(name) ?? [];
|
|
112018
|
+
queue.push({ toolCallId, input, claimed });
|
|
112019
|
+
pendingByName.set(name, queue);
|
|
112020
|
+
emit({
|
|
112021
|
+
sessionUpdate: "tool_call",
|
|
112022
|
+
toolCallId,
|
|
112023
|
+
title: name,
|
|
112024
|
+
name,
|
|
112025
|
+
kind: toolKindFor(name),
|
|
112026
|
+
status: "in_progress",
|
|
112027
|
+
rawInput: tryParseJson(input)
|
|
112028
|
+
});
|
|
112029
|
+
return toolCallId;
|
|
112030
|
+
};
|
|
112031
|
+
const requestApproval = async (tool, input, meta) => {
|
|
112032
|
+
if (askPermission === undefined) {
|
|
112033
|
+
return false;
|
|
112034
|
+
}
|
|
112035
|
+
const pending = pendingByName.get(tool)?.find((entry) => entry.input === input);
|
|
112036
|
+
const toolCallId = pending?.toolCallId ?? openToolCall(tool, input, false);
|
|
112037
|
+
const options = permissionOptionsFor(meta);
|
|
112038
|
+
const response = await askPermission({
|
|
112039
|
+
toolCall: {
|
|
112040
|
+
toolCallId,
|
|
112041
|
+
title: tool,
|
|
112042
|
+
name: tool,
|
|
112043
|
+
kind: toolKindFor(tool),
|
|
112044
|
+
status: "pending",
|
|
112045
|
+
rawInput: tryParseJson(input)
|
|
112046
|
+
},
|
|
112047
|
+
options
|
|
112048
|
+
});
|
|
112049
|
+
if (response === undefined) {
|
|
112050
|
+
return false;
|
|
112051
|
+
}
|
|
112052
|
+
return approvalFromPermissionResponse(response, options, meta?.fingerprint);
|
|
112053
|
+
};
|
|
112054
|
+
return {
|
|
112055
|
+
...askPermission !== undefined ? { requestApproval } : {},
|
|
112056
|
+
write: (text) => {
|
|
112057
|
+
if (text.length === 0) {
|
|
112058
|
+
return;
|
|
112059
|
+
}
|
|
112060
|
+
emit({ sessionUpdate: "agent_message_chunk", content: textBlock(text) });
|
|
112061
|
+
},
|
|
112062
|
+
onReasoningDelta: (delta) => {
|
|
112063
|
+
if (delta.redacted === true || delta.text === undefined || delta.text.length === 0) {
|
|
112064
|
+
return;
|
|
112065
|
+
}
|
|
112066
|
+
emit({ sessionUpdate: "agent_thought_chunk", content: textBlock(delta.text) });
|
|
112067
|
+
},
|
|
112068
|
+
onToolCall: (name, input) => {
|
|
112069
|
+
const announcedAhead = pendingByName.get(name)?.find((entry) => !entry.claimed && entry.input === input);
|
|
112070
|
+
if (announcedAhead !== undefined) {
|
|
112071
|
+
announcedAhead.claimed = true;
|
|
112072
|
+
return;
|
|
112073
|
+
}
|
|
112074
|
+
openToolCall(name, input, true);
|
|
112075
|
+
},
|
|
112076
|
+
onToolResult: (name, result) => {
|
|
112077
|
+
const queue = pendingByName.get(name);
|
|
112078
|
+
const index = queue?.findIndex((entry) => entry.claimed) ?? -1;
|
|
112079
|
+
const pending = queue?.splice(index >= 0 ? index : 0, 1)[0];
|
|
112080
|
+
const toolCallId = pending?.toolCallId;
|
|
112081
|
+
emit({
|
|
112082
|
+
sessionUpdate: "tool_call_update",
|
|
112083
|
+
toolCallId: toolCallId ?? randomUUID35(),
|
|
112084
|
+
status: result.isError ? "failed" : "completed",
|
|
112085
|
+
content: [{ type: "content", content: textBlock(result.output) }],
|
|
112086
|
+
rawOutput: result.output
|
|
112087
|
+
});
|
|
112088
|
+
},
|
|
112089
|
+
onSystem: (text) => {
|
|
112090
|
+
if (text.length === 0) {
|
|
112091
|
+
return;
|
|
112092
|
+
}
|
|
112093
|
+
emit({ sessionUpdate: "agent_message_chunk", content: textBlock(text) });
|
|
112094
|
+
}
|
|
112095
|
+
};
|
|
112096
|
+
}
|
|
112097
|
+
|
|
112098
|
+
// src/acp/capability-tools.ts
|
|
112099
|
+
function acpAwareReadFileTool(base, root, clientCapabilities, readViaClient) {
|
|
112100
|
+
if (clientCapabilities?.fs?.readTextFile !== true) {
|
|
112101
|
+
return base;
|
|
112102
|
+
}
|
|
112103
|
+
return {
|
|
112104
|
+
definition: base.definition,
|
|
112105
|
+
invoke: async (input, ctx) => {
|
|
112106
|
+
const requested = typeof input.path === "string" ? input.path : "";
|
|
112107
|
+
if (requested.length === 0) {
|
|
112108
|
+
return base.invoke(input, ctx);
|
|
112109
|
+
}
|
|
112110
|
+
const target = confineToRoot(root, requested);
|
|
112111
|
+
if (target === null) {
|
|
112112
|
+
return base.invoke(input, ctx);
|
|
112113
|
+
}
|
|
112114
|
+
const startLine = typeof input.start_line === "number" ? input.start_line : undefined;
|
|
112115
|
+
const viaClient = await readViaClient(target, startLine);
|
|
112116
|
+
if (viaClient !== undefined) {
|
|
112117
|
+
return { output: viaClient, isError: false };
|
|
112118
|
+
}
|
|
112119
|
+
return base.invoke(input, ctx);
|
|
112120
|
+
}
|
|
112121
|
+
};
|
|
112122
|
+
}
|
|
112123
|
+
|
|
112124
|
+
// src/acp/client-requests.ts
|
|
112125
|
+
class AcpClientRequests {
|
|
112126
|
+
options;
|
|
112127
|
+
pending = new Map;
|
|
112128
|
+
closedReason;
|
|
112129
|
+
constructor(options) {
|
|
112130
|
+
this.options = options;
|
|
112131
|
+
}
|
|
112132
|
+
get pendingCount() {
|
|
112133
|
+
return this.pending.size;
|
|
112134
|
+
}
|
|
112135
|
+
request(method, params, onId) {
|
|
112136
|
+
if (this.closedReason !== undefined) {
|
|
112137
|
+
return Promise.resolve({ kind: "closed", reason: this.closedReason });
|
|
112138
|
+
}
|
|
112139
|
+
const id = `acp-agent-${this.options.idSeq()}`;
|
|
112140
|
+
onId?.(id);
|
|
112141
|
+
return new Promise((resolve) => {
|
|
112142
|
+
this.pending.set(id, resolve);
|
|
112143
|
+
try {
|
|
112144
|
+
this.options.send(requestMessage(id, method, params));
|
|
112145
|
+
} catch (error) {
|
|
112146
|
+
this.pending.delete(id);
|
|
112147
|
+
resolve({
|
|
112148
|
+
kind: "closed",
|
|
112149
|
+
reason: `the request could not be written: ${error instanceof Error ? error.message : String(error)}`
|
|
112150
|
+
});
|
|
112151
|
+
}
|
|
112152
|
+
});
|
|
112153
|
+
}
|
|
112154
|
+
cancel(id, reason) {
|
|
112155
|
+
const settle = this.pending.get(id);
|
|
112156
|
+
if (settle === undefined) {
|
|
112157
|
+
return false;
|
|
112158
|
+
}
|
|
112159
|
+
this.pending.delete(id);
|
|
112160
|
+
settle({ kind: "closed", reason });
|
|
112161
|
+
return true;
|
|
112162
|
+
}
|
|
112163
|
+
resolve(message) {
|
|
112164
|
+
const key = idKey(message.id);
|
|
112165
|
+
if (key === undefined) {
|
|
112166
|
+
return false;
|
|
112167
|
+
}
|
|
112168
|
+
const settle = this.pending.get(key);
|
|
112169
|
+
if (settle === undefined) {
|
|
112170
|
+
return false;
|
|
112171
|
+
}
|
|
112172
|
+
this.pending.delete(key);
|
|
112173
|
+
if ("error" in message) {
|
|
112174
|
+
settle({ kind: "error", error: message.error });
|
|
112175
|
+
return true;
|
|
112176
|
+
}
|
|
112177
|
+
settle({ kind: "result", result: message.result });
|
|
112178
|
+
return true;
|
|
112179
|
+
}
|
|
112180
|
+
close(reason) {
|
|
112181
|
+
this.closedReason ??= reason;
|
|
112182
|
+
const outstanding = [...this.pending.values()];
|
|
112183
|
+
this.pending.clear();
|
|
112184
|
+
for (const settle of outstanding) {
|
|
112185
|
+
settle({ kind: "closed", reason });
|
|
112186
|
+
}
|
|
112187
|
+
}
|
|
112188
|
+
}
|
|
112189
|
+
function idKey(id) {
|
|
112190
|
+
if (typeof id === "string") {
|
|
112191
|
+
return id;
|
|
112192
|
+
}
|
|
112193
|
+
if (typeof id === "number") {
|
|
112194
|
+
return String(id);
|
|
112195
|
+
}
|
|
112196
|
+
return;
|
|
112197
|
+
}
|
|
112198
|
+
|
|
112199
|
+
// src/acp/framing.ts
|
|
112200
|
+
var ACP_MESSAGE_DELIMITER = `
|
|
112201
|
+
`;
|
|
112202
|
+
var ACP_DEFAULT_MAX_LINE_BYTES = 64 * 1024 * 1024;
|
|
112203
|
+
|
|
112204
|
+
class AcpFramingError extends Error {
|
|
112205
|
+
bufferedBytes;
|
|
112206
|
+
constructor(message, bufferedBytes) {
|
|
112207
|
+
super(message);
|
|
112208
|
+
this.name = "AcpFramingError";
|
|
112209
|
+
this.bufferedBytes = bufferedBytes;
|
|
112210
|
+
}
|
|
112211
|
+
}
|
|
112212
|
+
function encodeAcpMessage(message) {
|
|
112213
|
+
const json = JSON.stringify(message);
|
|
112214
|
+
if (json === undefined) {
|
|
112215
|
+
throw new AcpFramingError("message is not JSON-serialisable", 0);
|
|
112216
|
+
}
|
|
112217
|
+
if (json.includes(`
|
|
112218
|
+
`) || json.includes("\r")) {
|
|
112219
|
+
throw new AcpFramingError("encoded message contains an embedded newline", json.length);
|
|
112220
|
+
}
|
|
112221
|
+
return `${json}${ACP_MESSAGE_DELIMITER}`;
|
|
112222
|
+
}
|
|
112223
|
+
|
|
112224
|
+
class AcpLineFramer {
|
|
112225
|
+
buffer = "";
|
|
112226
|
+
decoder = new TextDecoder("utf-8");
|
|
112227
|
+
maxLineBytes;
|
|
112228
|
+
constructor(options = {}) {
|
|
112229
|
+
this.maxLineBytes = options.maxLineBytes ?? ACP_DEFAULT_MAX_LINE_BYTES;
|
|
112230
|
+
}
|
|
112231
|
+
get buffered() {
|
|
112232
|
+
return this.buffer.length;
|
|
112233
|
+
}
|
|
112234
|
+
push(chunk) {
|
|
112235
|
+
const text = typeof chunk === "string" ? chunk : this.decoder.decode(chunk, { stream: true });
|
|
112236
|
+
if (text === "") {
|
|
112237
|
+
this.assertWithinLimit();
|
|
112238
|
+
return [];
|
|
112239
|
+
}
|
|
112240
|
+
this.buffer += text;
|
|
112241
|
+
const parts = this.buffer.split(ACP_MESSAGE_DELIMITER);
|
|
112242
|
+
this.buffer = parts.pop() ?? "";
|
|
112243
|
+
this.assertWithinLimit();
|
|
112244
|
+
const lines = [];
|
|
112245
|
+
for (const part of parts) {
|
|
112246
|
+
const line = normaliseLine(part);
|
|
112247
|
+
if (line !== "") {
|
|
112248
|
+
lines.push(line);
|
|
112249
|
+
}
|
|
112250
|
+
}
|
|
112251
|
+
return lines;
|
|
112252
|
+
}
|
|
112253
|
+
flush() {
|
|
112254
|
+
this.buffer += this.decoder.decode();
|
|
112255
|
+
const line = normaliseLine(this.buffer);
|
|
112256
|
+
this.buffer = "";
|
|
112257
|
+
return line === "" ? [] : [line];
|
|
112258
|
+
}
|
|
112259
|
+
assertWithinLimit() {
|
|
112260
|
+
if (this.buffer.length > this.maxLineBytes) {
|
|
112261
|
+
const buffered = this.buffer.length;
|
|
112262
|
+
this.buffer = "";
|
|
112263
|
+
throw new AcpFramingError(`no message delimiter within ${this.maxLineBytes} bytes; dropped the buffer`, buffered);
|
|
112264
|
+
}
|
|
112265
|
+
}
|
|
112266
|
+
}
|
|
112267
|
+
function normaliseLine(part) {
|
|
112268
|
+
return part.replace(/\r$/, "").trim();
|
|
112269
|
+
}
|
|
112270
|
+
function decodeAcpLine(line) {
|
|
112271
|
+
try {
|
|
112272
|
+
return { ok: true, value: JSON.parse(line) };
|
|
112273
|
+
} catch (error) {
|
|
112274
|
+
return {
|
|
112275
|
+
ok: false,
|
|
112276
|
+
error: parseError({
|
|
112277
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
112278
|
+
})
|
|
112279
|
+
};
|
|
112280
|
+
}
|
|
112281
|
+
}
|
|
112282
|
+
|
|
112283
|
+
// src/acp/dispatch.ts
|
|
112284
|
+
class AcpDispatcher {
|
|
112285
|
+
requests = new Map;
|
|
112286
|
+
notifications = new Map;
|
|
112287
|
+
options;
|
|
112288
|
+
constructor(handlers = {}, options = {}) {
|
|
112289
|
+
for (const [method, handler] of Object.entries(handlers.requests ?? {})) {
|
|
112290
|
+
this.requests.set(method, handler);
|
|
112291
|
+
}
|
|
112292
|
+
for (const [method, handler] of Object.entries(handlers.notifications ?? {})) {
|
|
112293
|
+
this.notifications.set(method, handler);
|
|
112294
|
+
}
|
|
112295
|
+
this.options = options;
|
|
112296
|
+
}
|
|
112297
|
+
onRequest(method, handler) {
|
|
112298
|
+
this.requests.set(method, handler);
|
|
112299
|
+
return this;
|
|
112300
|
+
}
|
|
112301
|
+
onNotification(method, handler) {
|
|
112302
|
+
this.notifications.set(method, handler);
|
|
112303
|
+
return this;
|
|
112304
|
+
}
|
|
112305
|
+
registeredRequestMethods() {
|
|
112306
|
+
return [...this.requests.keys()].sort();
|
|
112307
|
+
}
|
|
112308
|
+
registeredNotificationMethods() {
|
|
112309
|
+
return [...this.notifications.keys()].sort();
|
|
112310
|
+
}
|
|
112311
|
+
async handleLine(line) {
|
|
112312
|
+
const decoded = decodeAcpLine(line);
|
|
112313
|
+
if (!decoded.ok) {
|
|
112314
|
+
return { kind: "reply", message: errorResponse(null, decoded.error) };
|
|
112315
|
+
}
|
|
112316
|
+
return this.handleMessage(decoded.value);
|
|
112317
|
+
}
|
|
112318
|
+
async handleMessage(value) {
|
|
112319
|
+
const classified = classifyJsonRpcMessage(value);
|
|
112320
|
+
switch (classified.kind) {
|
|
112321
|
+
case "invalid":
|
|
112322
|
+
return { kind: "reply", message: errorResponse(classified.id, classified.error) };
|
|
112323
|
+
case "response":
|
|
112324
|
+
return { kind: "incoming-response", message: classified.message };
|
|
112325
|
+
case "notification":
|
|
112326
|
+
await this.handleNotification(classified.message);
|
|
112327
|
+
return { kind: "none" };
|
|
112328
|
+
case "request":
|
|
112329
|
+
return { kind: "reply", message: await this.handleRequest(classified.message) };
|
|
112330
|
+
}
|
|
112331
|
+
}
|
|
112332
|
+
async handleRequest(message) {
|
|
112333
|
+
const handler = this.requests.get(message.method);
|
|
112334
|
+
if (handler === undefined) {
|
|
112335
|
+
return errorResponse(message.id, this.unknownMethodError(message.method));
|
|
112336
|
+
}
|
|
112337
|
+
try {
|
|
112338
|
+
const result = await handler(message.params, { method: message.method, id: message.id });
|
|
112339
|
+
return successResponse(message.id, result === undefined ? {} : result);
|
|
112340
|
+
} catch (error) {
|
|
112341
|
+
this.options.onHandlerError?.(message.method, error);
|
|
112342
|
+
if (error instanceof AcpError) {
|
|
112343
|
+
return errorResponse(message.id, error.toErrorObject());
|
|
112344
|
+
}
|
|
112345
|
+
return errorResponse(message.id, internalError(error instanceof Error ? error.message : String(error), {
|
|
112346
|
+
method: message.method
|
|
112347
|
+
}));
|
|
112348
|
+
}
|
|
112349
|
+
}
|
|
112350
|
+
async handleNotification(message) {
|
|
112351
|
+
const handler = this.notifications.get(message.method);
|
|
112352
|
+
if (handler === undefined) {
|
|
112353
|
+
this.options.onUnhandledNotification?.(message.method, message.params);
|
|
112354
|
+
return;
|
|
112355
|
+
}
|
|
112356
|
+
try {
|
|
112357
|
+
await handler(message.params, { method: message.method });
|
|
112358
|
+
} catch (error) {
|
|
112359
|
+
this.options.onHandlerError?.(message.method, error);
|
|
112360
|
+
}
|
|
112361
|
+
}
|
|
112362
|
+
unknownMethodError(method) {
|
|
112363
|
+
return refusalError(method) ?? methodNotFound(method, {
|
|
112364
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
112365
|
+
implemented: ACP_IMPLEMENTED_AGENT_METHODS
|
|
112366
|
+
});
|
|
112367
|
+
}
|
|
112368
|
+
}
|
|
112369
|
+
|
|
112370
|
+
// src/acp/prompt-content.ts
|
|
112371
|
+
function isContentBlock(value) {
|
|
112372
|
+
return typeof value === "object" && value !== null && typeof value.type === "string";
|
|
112373
|
+
}
|
|
112374
|
+
function renderAcpPromptContent(blocks) {
|
|
112375
|
+
const parts = [];
|
|
112376
|
+
for (const block of blocks) {
|
|
112377
|
+
if (!isContentBlock(block)) {
|
|
112378
|
+
continue;
|
|
112379
|
+
}
|
|
112380
|
+
switch (block.type) {
|
|
112381
|
+
case "text":
|
|
112382
|
+
parts.push(block.text);
|
|
112383
|
+
break;
|
|
112384
|
+
case "resource": {
|
|
112385
|
+
const resource = block.resource;
|
|
112386
|
+
if ("text" in resource) {
|
|
112387
|
+
parts.push(resource.text);
|
|
112388
|
+
} else {
|
|
112389
|
+
parts.push(`[embedded resource omitted (binary, ${resource.mimeType ?? "unknown type"}): ${resource.uri}]`);
|
|
112390
|
+
}
|
|
112391
|
+
break;
|
|
112392
|
+
}
|
|
112393
|
+
case "resource_link":
|
|
112394
|
+
parts.push(`[resource link: ${block.uri}]`);
|
|
112395
|
+
break;
|
|
112396
|
+
case "image":
|
|
112397
|
+
parts.push("[image content omitted \u2014 keryx does not accept image prompt input (promptCapabilities.image: false)]");
|
|
112398
|
+
break;
|
|
112399
|
+
case "audio":
|
|
112400
|
+
parts.push("[audio content omitted \u2014 keryx does not accept audio prompt input (promptCapabilities.audio: false)]");
|
|
112401
|
+
break;
|
|
112402
|
+
default:
|
|
112403
|
+
break;
|
|
112404
|
+
}
|
|
112405
|
+
}
|
|
112406
|
+
return parts.join(`
|
|
112407
|
+
|
|
112408
|
+
`);
|
|
112409
|
+
}
|
|
112410
|
+
|
|
112411
|
+
// src/acp/session.ts
|
|
112412
|
+
class AcpSessionTranscriptUnreadableError extends Error {
|
|
112413
|
+
sessionId;
|
|
112414
|
+
cause;
|
|
112415
|
+
constructor(sessionId, cause) {
|
|
112416
|
+
super(`session ${sessionId}: ${cause.message}`);
|
|
112417
|
+
this.sessionId = sessionId;
|
|
112418
|
+
this.cause = cause;
|
|
112419
|
+
this.name = "AcpSessionTranscriptUnreadableError";
|
|
112420
|
+
}
|
|
112421
|
+
}
|
|
112422
|
+
|
|
112423
|
+
class AcpSessionRegistry {
|
|
112424
|
+
options;
|
|
112425
|
+
sessions = new Map;
|
|
112426
|
+
constructor(options) {
|
|
112427
|
+
this.options = options;
|
|
112428
|
+
}
|
|
112429
|
+
create(cwd, clientCapabilities) {
|
|
112430
|
+
const resolvedRoot = resolveProjectRoot2(cwd);
|
|
112431
|
+
const handle = createSession({
|
|
112432
|
+
cwd: resolvedRoot,
|
|
112433
|
+
provider: this.options.providerId,
|
|
112434
|
+
model: this.options.modelId,
|
|
112435
|
+
...this.options.dataDir !== undefined ? { dataDir: this.options.dataDir } : {}
|
|
112436
|
+
});
|
|
112437
|
+
const state = {
|
|
112438
|
+
sessionId: handle.summary.id,
|
|
112439
|
+
requestedCwd: cwd,
|
|
112440
|
+
resolvedRoot,
|
|
112441
|
+
handle,
|
|
112442
|
+
history: [],
|
|
112443
|
+
clientCapabilities
|
|
112444
|
+
};
|
|
112445
|
+
this.sessions.set(state.sessionId, state);
|
|
112446
|
+
return state;
|
|
112447
|
+
}
|
|
112448
|
+
get(sessionId) {
|
|
112449
|
+
return this.sessions.get(sessionId);
|
|
112450
|
+
}
|
|
112451
|
+
load(sessionId, cwd, clientCapabilities) {
|
|
112452
|
+
const resolvedRoot = resolveProjectRoot2(cwd);
|
|
112453
|
+
const known = listSessions(cwd, this.options.dataDir).some((summary) => summary.id === sessionId);
|
|
112454
|
+
if (!known) {
|
|
112455
|
+
return;
|
|
112456
|
+
}
|
|
112457
|
+
let opened;
|
|
112458
|
+
try {
|
|
112459
|
+
opened = openSession({
|
|
112460
|
+
cwd,
|
|
112461
|
+
resumeId: sessionId,
|
|
112462
|
+
...this.options.dataDir !== undefined ? { dataDir: this.options.dataDir } : {},
|
|
112463
|
+
provider: this.options.providerId,
|
|
112464
|
+
model: this.options.modelId
|
|
112465
|
+
});
|
|
112466
|
+
} catch (cause) {
|
|
112467
|
+
if (cause instanceof TranscriptUnreadableError) {
|
|
112468
|
+
throw new AcpSessionTranscriptUnreadableError(sessionId, cause);
|
|
112469
|
+
}
|
|
112470
|
+
throw cause;
|
|
112471
|
+
}
|
|
112472
|
+
const state = {
|
|
112473
|
+
sessionId: opened.handle.summary.id,
|
|
112474
|
+
requestedCwd: cwd,
|
|
112475
|
+
resolvedRoot,
|
|
112476
|
+
handle: opened.handle,
|
|
112477
|
+
history: opened.history,
|
|
112478
|
+
clientCapabilities
|
|
112479
|
+
};
|
|
112480
|
+
this.sessions.set(state.sessionId, state);
|
|
112481
|
+
return state;
|
|
112482
|
+
}
|
|
112483
|
+
updateHandle(sessionId, handle) {
|
|
112484
|
+
const state = this.sessions.get(sessionId);
|
|
112485
|
+
if (state === undefined) {
|
|
112486
|
+
return;
|
|
112487
|
+
}
|
|
112488
|
+
this.sessions.set(sessionId, { ...state, handle });
|
|
112489
|
+
}
|
|
112490
|
+
}
|
|
112491
|
+
|
|
112492
|
+
// src/acp/server.ts
|
|
112493
|
+
var DEFAULT_AGENT_INFO = { name: "keryx", version: "0" };
|
|
112494
|
+
function finishReasonToStopReason(finishReason) {
|
|
112495
|
+
switch (finishReason) {
|
|
112496
|
+
case "budget":
|
|
112497
|
+
return "max_tokens";
|
|
112498
|
+
case "tool-call-budget":
|
|
112499
|
+
return "max_turn_requests";
|
|
112500
|
+
case "no-progress":
|
|
112501
|
+
case undefined:
|
|
112502
|
+
return "end_turn";
|
|
112503
|
+
}
|
|
112504
|
+
}
|
|
112505
|
+
function refuse3(error) {
|
|
112506
|
+
throw new AcpError(error.code, error.message, error.data);
|
|
112507
|
+
}
|
|
112508
|
+
async function runAcpServer(options) {
|
|
112509
|
+
let initialized = false;
|
|
112510
|
+
let clientCapabilities;
|
|
112511
|
+
const idSeq = options.idSeq ?? (() => randomUUID36());
|
|
112512
|
+
const registry = new AcpSessionRegistry({
|
|
112513
|
+
providerId: options.providerId,
|
|
112514
|
+
modelId: options.modelId,
|
|
112515
|
+
...options.dataDir !== undefined ? { dataDir: options.dataDir } : {}
|
|
112516
|
+
});
|
|
112517
|
+
const writeMessage = (message) => {
|
|
112518
|
+
options.write(encodeAcpMessage(message));
|
|
112519
|
+
};
|
|
112520
|
+
const sendUpdate = (sessionId, update) => {
|
|
112521
|
+
const notification = { sessionId, update };
|
|
112522
|
+
writeMessage(notificationMessage(ACP_CLIENT_METHODS.sessionUpdate, notification));
|
|
112523
|
+
};
|
|
112524
|
+
const clientRequests = new AcpClientRequests({
|
|
112525
|
+
send: (message) => {
|
|
112526
|
+
options.write(encodeAcpMessage(message));
|
|
112527
|
+
},
|
|
112528
|
+
idSeq
|
|
112529
|
+
});
|
|
112530
|
+
let clientAnswersPermissions = true;
|
|
112531
|
+
const activeTurns = new Map;
|
|
112532
|
+
function refuseIfBusy(method, sessionId) {
|
|
112533
|
+
if (!activeTurns.has(sessionId)) {
|
|
112534
|
+
return;
|
|
112535
|
+
}
|
|
112536
|
+
refuse3(invalidRequest({
|
|
112537
|
+
reason: `${method}: a turn is already running for session ${sessionId}; ` + "wait for its session/prompt response \u2014 send session/cancel first if you do not want to wait for it to finish, " + "then prompt again once the cancelled turn has replied",
|
|
112538
|
+
method,
|
|
112539
|
+
sessionId,
|
|
112540
|
+
condition: "session-busy"
|
|
112541
|
+
}));
|
|
112542
|
+
}
|
|
112543
|
+
const askPermissionFor = (sessionId, turn) => {
|
|
112544
|
+
return async (ask) => {
|
|
112545
|
+
if (!clientAnswersPermissions) {
|
|
112546
|
+
options.logError(`acp: ${ACP_CLIENT_METHODS.sessionRequestPermission} not asked for ${ask.toolCall.name ?? "a tool call"}: ` + "this client already refused the method; the call is denied");
|
|
112547
|
+
return;
|
|
112548
|
+
}
|
|
112549
|
+
const params = {
|
|
112550
|
+
sessionId,
|
|
112551
|
+
toolCall: ask.toolCall,
|
|
112552
|
+
options: [...ask.options]
|
|
112553
|
+
};
|
|
112554
|
+
let requestId;
|
|
112555
|
+
const outcome = await clientRequests.request(ACP_CLIENT_METHODS.sessionRequestPermission, params, (id) => {
|
|
112556
|
+
requestId = id;
|
|
112557
|
+
turn.permissionRequestIds.add(id);
|
|
112558
|
+
});
|
|
112559
|
+
if (requestId !== undefined) {
|
|
112560
|
+
turn.permissionRequestIds.delete(requestId);
|
|
112561
|
+
}
|
|
112562
|
+
if (outcome.kind === "error") {
|
|
112563
|
+
clientAnswersPermissions = false;
|
|
112564
|
+
options.logError(`acp: ${ACP_CLIENT_METHODS.sessionRequestPermission} refused by the client ` + `(${outcome.error.code}: ${outcome.error.message}); the call is denied`);
|
|
112565
|
+
return;
|
|
112566
|
+
}
|
|
112567
|
+
if (outcome.kind === "closed") {
|
|
112568
|
+
options.logError(`acp: ${ACP_CLIENT_METHODS.sessionRequestPermission} went unanswered (${outcome.reason}); the call is denied`);
|
|
112569
|
+
return;
|
|
112570
|
+
}
|
|
112571
|
+
const result = outcome.result;
|
|
112572
|
+
if (typeof result !== "object" || result === null || Array.isArray(result)) {
|
|
112573
|
+
options.logError(`acp: ${ACP_CLIENT_METHODS.sessionRequestPermission} answered with a non-object result; the call is denied`);
|
|
112574
|
+
return;
|
|
112575
|
+
}
|
|
112576
|
+
return result;
|
|
112577
|
+
};
|
|
112578
|
+
};
|
|
112579
|
+
function requireInitialized(method) {
|
|
112580
|
+
if (!initialized) {
|
|
112581
|
+
refuse3(invalidRequest({ reason: `${method}: call initialize before any other request`, method }));
|
|
112582
|
+
}
|
|
112583
|
+
}
|
|
112584
|
+
function handleInitialize(params) {
|
|
112585
|
+
const obj = requireObjectParams(params, ACP_AGENT_METHODS.initialize);
|
|
112586
|
+
const negotiation = negotiateProtocolVersion(obj["protocolVersion"]);
|
|
112587
|
+
if (negotiation.kind === "refuse") {
|
|
112588
|
+
refuse3(negotiation.error);
|
|
112589
|
+
}
|
|
112590
|
+
initialized = true;
|
|
112591
|
+
const rawCaps = obj["clientCapabilities"];
|
|
112592
|
+
clientCapabilities = typeof rawCaps === "object" && rawCaps !== null && !Array.isArray(rawCaps) ? rawCaps : undefined;
|
|
112593
|
+
return {
|
|
112594
|
+
protocolVersion: negotiation.version,
|
|
112595
|
+
agentCapabilities: KERYX_AGENT_CAPABILITIES,
|
|
112596
|
+
authMethods: KERYX_AUTH_METHODS,
|
|
112597
|
+
agentInfo: options.agentInfo ?? DEFAULT_AGENT_INFO
|
|
112598
|
+
};
|
|
112599
|
+
}
|
|
112600
|
+
function handleSessionNew(params) {
|
|
112601
|
+
requireInitialized(ACP_AGENT_METHODS.sessionNew);
|
|
112602
|
+
const obj = requireObjectParams(params, ACP_AGENT_METHODS.sessionNew);
|
|
112603
|
+
const cwd = requireStringField(obj, "cwd", ACP_AGENT_METHODS.sessionNew);
|
|
112604
|
+
const mcpServers = obj["mcpServers"];
|
|
112605
|
+
if (mcpServers !== undefined && !Array.isArray(mcpServers)) {
|
|
112606
|
+
refuse3(invalidParams(`${ACP_AGENT_METHODS.sessionNew}: mcpServers must be an array`, { received: mcpServers }));
|
|
112607
|
+
}
|
|
112608
|
+
if (Array.isArray(mcpServers) && mcpServers.length > 0) {
|
|
112609
|
+
refuse3(invalidParams(`${ACP_AGENT_METHODS.sessionNew}: keryx has no in-memory MCP server registration seam for a single ` + "session; configure servers with `keryx mcp`/`keryx integrate` (project-wide) and omit mcpServers, " + "or send an empty list", { requestedServers: mcpServers.length }));
|
|
112610
|
+
}
|
|
112611
|
+
const state = registry.create(cwd, clientCapabilities);
|
|
112612
|
+
return { sessionId: state.sessionId };
|
|
112613
|
+
}
|
|
112614
|
+
async function handleSessionPrompt(params) {
|
|
112615
|
+
requireInitialized(ACP_AGENT_METHODS.sessionPrompt);
|
|
112616
|
+
const obj = requireObjectParams(params, ACP_AGENT_METHODS.sessionPrompt);
|
|
112617
|
+
const sessionId = requireStringField(obj, "sessionId", ACP_AGENT_METHODS.sessionPrompt);
|
|
112618
|
+
const promptField = obj["prompt"];
|
|
112619
|
+
if (!Array.isArray(promptField)) {
|
|
112620
|
+
refuse3(invalidParams(`${ACP_AGENT_METHODS.sessionPrompt}: prompt must be an array of content blocks`, {
|
|
112621
|
+
received: promptField
|
|
112622
|
+
}));
|
|
112623
|
+
}
|
|
112624
|
+
const state = registry.get(sessionId);
|
|
112625
|
+
if (state === undefined) {
|
|
112626
|
+
refuse3({
|
|
112627
|
+
code: JSON_RPC_ERROR_CODES.resourceNotFound,
|
|
112628
|
+
message: `Unknown ACP session: ${sessionId}`,
|
|
112629
|
+
data: { sessionId }
|
|
112630
|
+
});
|
|
112631
|
+
}
|
|
112632
|
+
refuseIfBusy(ACP_AGENT_METHODS.sessionPrompt, sessionId);
|
|
112633
|
+
const userLine = renderAcpPromptContent(promptField);
|
|
112634
|
+
const readViaClient = async (path, line) => {
|
|
112635
|
+
const params = {
|
|
112636
|
+
sessionId,
|
|
112637
|
+
path,
|
|
112638
|
+
...line !== undefined ? { line } : {}
|
|
112639
|
+
};
|
|
112640
|
+
const outcome = await clientRequests.request(ACP_CLIENT_METHODS.fsReadTextFile, params);
|
|
112641
|
+
if (outcome.kind !== "result") {
|
|
112642
|
+
return;
|
|
112643
|
+
}
|
|
112644
|
+
const result = outcome.result;
|
|
112645
|
+
if (typeof result !== "object" || result === null) {
|
|
112646
|
+
return;
|
|
112647
|
+
}
|
|
112648
|
+
const content = result.content;
|
|
112649
|
+
return typeof content === "string" ? content : undefined;
|
|
112650
|
+
};
|
|
112651
|
+
const tools = [
|
|
112652
|
+
...builtinReadOnlyTools(state.resolvedRoot).map((tool) => tool.definition.name === "read_file" ? acpAwareReadFileTool(tool, state.resolvedRoot, state.clientCapabilities, readViaClient) : tool),
|
|
112653
|
+
shellExecTool(state.resolvedRoot),
|
|
112654
|
+
applyPatchTool(state.resolvedRoot)
|
|
112655
|
+
];
|
|
112656
|
+
const toolNames = tools.map((tool) => tool.definition.name);
|
|
112657
|
+
const deps = {
|
|
112658
|
+
provider: options.provider,
|
|
112659
|
+
providerId: options.providerId,
|
|
112660
|
+
modelId: options.modelId,
|
|
112661
|
+
tools,
|
|
112662
|
+
systemInstruction: buildAgentSystemInstruction(undefined, {
|
|
112663
|
+
providerId: options.providerId,
|
|
112664
|
+
modelId: options.modelId,
|
|
112665
|
+
toolNames
|
|
112666
|
+
}),
|
|
112667
|
+
idSeq
|
|
112668
|
+
};
|
|
112669
|
+
const turn = {
|
|
112670
|
+
controller: new AbortController,
|
|
112671
|
+
cancelled: false,
|
|
112672
|
+
permissionRequestIds: new Set
|
|
112673
|
+
};
|
|
112674
|
+
activeTurns.set(sessionId, turn);
|
|
112675
|
+
try {
|
|
112676
|
+
const io = createAcpAgentIo(sessionId, sendUpdate, askPermissionFor(sessionId, turn));
|
|
112677
|
+
const result = await runAgentTurn(io, deps, state.history, userLine, { signal: turn.controller.signal });
|
|
112678
|
+
const updatedHandle = persistHistory(state.handle, state.history, {
|
|
112679
|
+
provider: options.providerId,
|
|
112680
|
+
model: options.modelId
|
|
112681
|
+
});
|
|
112682
|
+
registry.updateHandle(sessionId, updatedHandle);
|
|
112683
|
+
return { stopReason: turn.cancelled ? "cancelled" : finishReasonToStopReason(result.finishReason) };
|
|
112684
|
+
} finally {
|
|
112685
|
+
if (activeTurns.get(sessionId) === turn) {
|
|
112686
|
+
activeTurns.delete(sessionId);
|
|
112687
|
+
}
|
|
112688
|
+
}
|
|
112689
|
+
}
|
|
112690
|
+
function handleSessionCancel(params) {
|
|
112691
|
+
const obj = requireObjectParams(params, ACP_AGENT_METHODS.sessionCancel);
|
|
112692
|
+
const sessionId = requireStringField(obj, "sessionId", ACP_AGENT_METHODS.sessionCancel);
|
|
112693
|
+
const turn = activeTurns.get(sessionId);
|
|
112694
|
+
if (turn === undefined) {
|
|
112695
|
+
options.logError(`acp: ${ACP_AGENT_METHODS.sessionCancel} for ${sessionId}: no turn is currently running for this session`);
|
|
112696
|
+
return;
|
|
112697
|
+
}
|
|
112698
|
+
turn.cancelled = true;
|
|
112699
|
+
turn.controller.abort();
|
|
112700
|
+
for (const requestId of turn.permissionRequestIds) {
|
|
112701
|
+
clientRequests.cancel(requestId, "the turn was cancelled by session/cancel");
|
|
112702
|
+
}
|
|
112703
|
+
turn.permissionRequestIds.clear();
|
|
112704
|
+
}
|
|
112705
|
+
function handleSessionList(params) {
|
|
112706
|
+
requireInitialized(ACP_AGENT_METHODS.sessionList);
|
|
112707
|
+
const obj = requireObjectParams(params, ACP_AGENT_METHODS.sessionList);
|
|
112708
|
+
const rawCwd = obj["cwd"];
|
|
112709
|
+
if (rawCwd !== undefined && rawCwd !== null && typeof rawCwd !== "string") {
|
|
112710
|
+
refuse3(invalidParams(`${ACP_AGENT_METHODS.sessionList}: cwd must be a string, null or omitted`, { received: rawCwd }));
|
|
112711
|
+
}
|
|
112712
|
+
const cwd = typeof rawCwd === "string" && rawCwd.length > 0 ? rawCwd : process.cwd();
|
|
112713
|
+
const summaries = listSessions(cwd, options.dataDir);
|
|
112714
|
+
const sessions = summaries.map((summary) => ({
|
|
112715
|
+
sessionId: summary.id,
|
|
112716
|
+
cwd: summary.projectPath,
|
|
112717
|
+
title: summary.title,
|
|
112718
|
+
updatedAt: summary.updatedAt
|
|
112719
|
+
}));
|
|
112720
|
+
return { sessions };
|
|
112721
|
+
}
|
|
112722
|
+
function replayHistory(sessionId, history) {
|
|
112723
|
+
for (const message of history) {
|
|
112724
|
+
if (message.role === "system") {
|
|
112725
|
+
continue;
|
|
112726
|
+
}
|
|
112727
|
+
if (message.role === "user") {
|
|
112728
|
+
if (message.content.length > 0) {
|
|
112729
|
+
sendUpdate(sessionId, { sessionUpdate: "user_message_chunk", content: textBlock(message.content) });
|
|
112730
|
+
}
|
|
112731
|
+
continue;
|
|
112732
|
+
}
|
|
112733
|
+
if (message.role === "assistant") {
|
|
112734
|
+
if (message.content.length > 0) {
|
|
112735
|
+
sendUpdate(sessionId, { sessionUpdate: "agent_message_chunk", content: textBlock(message.content) });
|
|
112736
|
+
}
|
|
112737
|
+
for (const call of message.toolCalls ?? []) {
|
|
112738
|
+
sendUpdate(sessionId, {
|
|
112739
|
+
sessionUpdate: "tool_call",
|
|
112740
|
+
toolCallId: call.id,
|
|
112741
|
+
title: call.name,
|
|
112742
|
+
name: call.name,
|
|
112743
|
+
kind: toolKindFor(call.name),
|
|
112744
|
+
status: "pending",
|
|
112745
|
+
rawInput: tryParseJson(call.arguments)
|
|
112746
|
+
});
|
|
112747
|
+
}
|
|
112748
|
+
continue;
|
|
112749
|
+
}
|
|
112750
|
+
const toolCallId = message.toolCallId ?? randomUUID36();
|
|
112751
|
+
sendUpdate(sessionId, {
|
|
112752
|
+
sessionUpdate: "tool_call_update",
|
|
112753
|
+
toolCallId,
|
|
112754
|
+
status: "completed",
|
|
112755
|
+
content: [{ type: "content", content: textBlock(message.content) }],
|
|
112756
|
+
rawOutput: message.content
|
|
112757
|
+
});
|
|
112758
|
+
}
|
|
112759
|
+
}
|
|
112760
|
+
function handleSessionLoad(params) {
|
|
112761
|
+
requireInitialized(ACP_AGENT_METHODS.sessionLoad);
|
|
112762
|
+
const obj = requireObjectParams(params, ACP_AGENT_METHODS.sessionLoad);
|
|
112763
|
+
const sessionId = requireStringField(obj, "sessionId", ACP_AGENT_METHODS.sessionLoad);
|
|
112764
|
+
const cwd = requireStringField(obj, "cwd", ACP_AGENT_METHODS.sessionLoad);
|
|
112765
|
+
const mcpServers = obj["mcpServers"];
|
|
112766
|
+
if (mcpServers !== undefined && !Array.isArray(mcpServers)) {
|
|
112767
|
+
refuse3(invalidParams(`${ACP_AGENT_METHODS.sessionLoad}: mcpServers must be an array`, { received: mcpServers }));
|
|
112768
|
+
}
|
|
112769
|
+
if (Array.isArray(mcpServers) && mcpServers.length > 0) {
|
|
112770
|
+
refuse3(invalidParams(`${ACP_AGENT_METHODS.sessionLoad}: keryx has no in-memory MCP server registration seam for a single ` + "session; configure servers with `keryx mcp`/`keryx integrate` (project-wide) and omit mcpServers, " + "or send an empty list", { requestedServers: mcpServers.length }));
|
|
112771
|
+
}
|
|
112772
|
+
refuseIfBusy(ACP_AGENT_METHODS.sessionLoad, sessionId);
|
|
112773
|
+
let state;
|
|
112774
|
+
try {
|
|
112775
|
+
state = registry.load(sessionId, cwd, clientCapabilities);
|
|
112776
|
+
} catch (cause) {
|
|
112777
|
+
if (!(cause instanceof AcpSessionTranscriptUnreadableError)) {
|
|
112778
|
+
throw cause;
|
|
112779
|
+
}
|
|
112780
|
+
refuse3({
|
|
112781
|
+
code: JSON_RPC_ERROR_CODES.internalError,
|
|
112782
|
+
message: `Cannot load session ${sessionId}: ${cause.cause.message}`,
|
|
112783
|
+
data: { sessionId, cwd, file: cause.cause.file, reason: cause.cause.reason }
|
|
112784
|
+
});
|
|
112785
|
+
}
|
|
112786
|
+
if (state === undefined) {
|
|
112787
|
+
refuse3({
|
|
112788
|
+
code: JSON_RPC_ERROR_CODES.resourceNotFound,
|
|
112789
|
+
message: `Unknown ACP session: ${sessionId}`,
|
|
112790
|
+
data: { sessionId, cwd }
|
|
112791
|
+
});
|
|
112792
|
+
}
|
|
112793
|
+
replayHistory(sessionId, state.history);
|
|
112794
|
+
return {};
|
|
112795
|
+
}
|
|
112796
|
+
const dispatcher = new AcpDispatcher({
|
|
112797
|
+
requests: {
|
|
112798
|
+
[ACP_AGENT_METHODS.initialize]: (params) => handleInitialize(params),
|
|
112799
|
+
[ACP_AGENT_METHODS.sessionNew]: (params) => handleSessionNew(params),
|
|
112800
|
+
[ACP_AGENT_METHODS.sessionPrompt]: (params) => handleSessionPrompt(params),
|
|
112801
|
+
[ACP_AGENT_METHODS.sessionList]: (params) => handleSessionList(params),
|
|
112802
|
+
[ACP_AGENT_METHODS.sessionLoad]: (params) => handleSessionLoad(params)
|
|
112803
|
+
},
|
|
112804
|
+
notifications: {
|
|
112805
|
+
[ACP_AGENT_METHODS.sessionCancel]: (params) => handleSessionCancel(params)
|
|
112806
|
+
}
|
|
112807
|
+
}, {
|
|
112808
|
+
onUnhandledNotification: (method) => options.logError(`acp: unhandled notification ${method}`),
|
|
112809
|
+
onHandlerError: (method, error) => options.logError(`acp: ${method} failed: ${error instanceof Error ? error.message : String(error)}`)
|
|
112810
|
+
});
|
|
112811
|
+
const handleOutcome = (outcome) => {
|
|
112812
|
+
if (outcome.kind === "reply") {
|
|
112813
|
+
writeMessage(outcome.message);
|
|
112814
|
+
return;
|
|
112815
|
+
}
|
|
112816
|
+
if (outcome.kind === "incoming-response") {
|
|
112817
|
+
if (!clientRequests.resolve(outcome.message)) {
|
|
112818
|
+
options.logError(`acp: unexpected response with no pending request: ${JSON.stringify(outcome.message)}`);
|
|
112819
|
+
}
|
|
112820
|
+
}
|
|
112821
|
+
};
|
|
112822
|
+
const inflight = new Set;
|
|
112823
|
+
const dispatch = (line) => {
|
|
112824
|
+
const task = dispatcher.handleLine(line).then(handleOutcome, (error) => {
|
|
112825
|
+
options.logError(`acp: dispatch failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
112826
|
+
});
|
|
112827
|
+
inflight.add(task);
|
|
112828
|
+
task.finally(() => {
|
|
112829
|
+
inflight.delete(task);
|
|
112830
|
+
});
|
|
112831
|
+
};
|
|
112832
|
+
const framer = new AcpLineFramer;
|
|
112833
|
+
for await (const chunk of options.input) {
|
|
112834
|
+
let lines;
|
|
112835
|
+
try {
|
|
112836
|
+
lines = framer.push(chunk);
|
|
112837
|
+
} catch (error) {
|
|
112838
|
+
options.logError(`acp: framing error: ${error instanceof Error ? error.message : String(error)}`);
|
|
112839
|
+
continue;
|
|
112840
|
+
}
|
|
112841
|
+
for (const line of lines) {
|
|
112842
|
+
dispatch(line);
|
|
112843
|
+
}
|
|
112844
|
+
}
|
|
112845
|
+
for (const line of framer.flush()) {
|
|
112846
|
+
dispatch(line);
|
|
112847
|
+
}
|
|
112848
|
+
clientRequests.close("the client closed the connection before answering");
|
|
112849
|
+
while (inflight.size > 0) {
|
|
112850
|
+
await Promise.allSettled([...inflight]);
|
|
112851
|
+
}
|
|
112852
|
+
}
|
|
112853
|
+
|
|
112854
|
+
// src/acp/fixture-provider.ts
|
|
112855
|
+
import { readFileSync as readFileSync14 } from "fs";
|
|
112856
|
+
function loadAcpFixtureProvider(path) {
|
|
112857
|
+
const raw = readFileSync14(path, "utf-8");
|
|
112858
|
+
const parsed = JSON.parse(raw);
|
|
112859
|
+
const turns = parsed.turns;
|
|
112860
|
+
let call = 0;
|
|
112861
|
+
const description = {
|
|
112862
|
+
capabilities: {
|
|
112863
|
+
streaming: true,
|
|
112864
|
+
toolCalls: true,
|
|
112865
|
+
parallelToolCalls: false,
|
|
112866
|
+
structuredOutput: false,
|
|
112867
|
+
reasoningMetadata: true,
|
|
112868
|
+
promptCaching: false,
|
|
112869
|
+
vision: false,
|
|
112870
|
+
tokenCounting: false,
|
|
112871
|
+
modelListing: false
|
|
112872
|
+
},
|
|
112873
|
+
descriptor: { providerId: "acp-fixture" }
|
|
112874
|
+
};
|
|
112875
|
+
return {
|
|
112876
|
+
describe: () => description,
|
|
112877
|
+
stream: (_request, opts) => {
|
|
112878
|
+
const events = turns[call] ?? [];
|
|
112879
|
+
call += 1;
|
|
112880
|
+
return async function* () {
|
|
112881
|
+
let sequence = 0;
|
|
112882
|
+
for (const partial of events) {
|
|
112883
|
+
yield { sequence: sequence++, attemptId: opts.attemptId, ...partial };
|
|
112884
|
+
}
|
|
112885
|
+
}();
|
|
112886
|
+
}
|
|
112887
|
+
};
|
|
112888
|
+
}
|
|
112889
|
+
|
|
112890
|
+
// src/commands/acp.ts
|
|
112891
|
+
init_make_provider();
|
|
112892
|
+
function parseArgs3(args) {
|
|
112893
|
+
let provider = "fake";
|
|
112894
|
+
let model = "fake-model";
|
|
112895
|
+
let baseUrl;
|
|
112896
|
+
let fixture;
|
|
112897
|
+
let dataDir;
|
|
112898
|
+
for (let i = 0;i < args.length; i += 1) {
|
|
112899
|
+
const arg = args[i];
|
|
112900
|
+
if (arg === "--provider") {
|
|
112901
|
+
provider = args[i += 1] ?? provider;
|
|
112902
|
+
} else if (arg === "--model") {
|
|
112903
|
+
model = args[i += 1] ?? model;
|
|
112904
|
+
} else if (arg === "--base-url") {
|
|
112905
|
+
baseUrl = args[i += 1];
|
|
112906
|
+
} else if (arg === "--fixture") {
|
|
112907
|
+
fixture = args[i += 1];
|
|
112908
|
+
} else if (arg === "--data-dir") {
|
|
112909
|
+
dataDir = args[i += 1];
|
|
112910
|
+
}
|
|
112911
|
+
}
|
|
112912
|
+
return {
|
|
112913
|
+
provider,
|
|
112914
|
+
model,
|
|
112915
|
+
...baseUrl !== undefined ? { baseUrl } : {},
|
|
112916
|
+
...fixture !== undefined ? { fixture } : {},
|
|
112917
|
+
...dataDir !== undefined ? { dataDir } : {}
|
|
112918
|
+
};
|
|
112919
|
+
}
|
|
112920
|
+
async function acpCommand(args) {
|
|
112921
|
+
if (args[0] === "help" || args.includes("--help") || args.includes("-h")) {
|
|
112922
|
+
printHelp18();
|
|
112923
|
+
return;
|
|
112924
|
+
}
|
|
112925
|
+
const parsed = parseArgs3(args);
|
|
112926
|
+
const provider = parsed.fixture !== undefined ? loadAcpFixtureProvider(parsed.fixture) : makeProvider(parsed.provider, parsed.model, {
|
|
112927
|
+
fetch,
|
|
112928
|
+
env: process.env,
|
|
112929
|
+
...parsed.baseUrl !== undefined ? { baseUrl: parsed.baseUrl } : {}
|
|
112930
|
+
});
|
|
112931
|
+
await runAcpServer({
|
|
112932
|
+
input: process.stdin,
|
|
112933
|
+
write: (chunk) => {
|
|
112934
|
+
process.stdout.write(chunk);
|
|
112935
|
+
},
|
|
112936
|
+
logError: (line) => {
|
|
112937
|
+
process.stderr.write(`${line}
|
|
112938
|
+
`);
|
|
112939
|
+
},
|
|
112940
|
+
provider,
|
|
112941
|
+
providerId: parsed.provider,
|
|
112942
|
+
modelId: parsed.model,
|
|
112943
|
+
agentInfo: { name: "keryx", version: package_default.version },
|
|
112944
|
+
...parsed.dataDir !== undefined ? { dataDir: parsed.dataDir } : {},
|
|
112945
|
+
idSeq: () => randomUUID37()
|
|
112946
|
+
});
|
|
112947
|
+
}
|
|
112948
|
+
function printHelp18() {
|
|
112949
|
+
console.log(`keryx acp
|
|
112950
|
+
|
|
112951
|
+
Speak the Agent Client Protocol (ACP) v1 over stdio: newline-delimited
|
|
112952
|
+
JSON-RPC 2.0 on stdin, the same framing on stdout. Launch this as a
|
|
112953
|
+
subprocess from an ACP client (an editor, e.g.) \u2014 it answers initialize,
|
|
112954
|
+
session/new, session/prompt (streaming session/update notifications as a
|
|
112955
|
+
turn runs rather than only at the end), session/cancel, session/list and
|
|
112956
|
+
session/load; every other agent method ACP defines is refused with -32601.
|
|
112957
|
+
A gated tool call is asked through session/request_permission \u2014 only an
|
|
112958
|
+
explicit allow runs it. Writes and shell execution always stay local; there
|
|
112959
|
+
is no fs/write_text_file or terminal/* call in this release. Nothing but
|
|
112960
|
+
protocol frames is ever written to stdout; every diagnostic goes to stderr.
|
|
112961
|
+
|
|
112962
|
+
Usage:
|
|
112963
|
+
keryx acp [--provider <p>] [--model <m>] [--base-url <url>] [--data-dir <dir>]
|
|
112964
|
+
`);
|
|
112965
|
+
}
|
|
112966
|
+
|
|
112967
|
+
// src/commands/bus.ts
|
|
112968
|
+
import { randomUUID as randomUUID39 } from "crypto";
|
|
112969
|
+
|
|
111604
112970
|
// src/bus/prune.ts
|
|
111605
112971
|
init_fs();
|
|
111606
|
-
import { randomUUID as
|
|
112972
|
+
import { randomUUID as randomUUID38 } from "crypto";
|
|
111607
112973
|
var GONE_PRESENCE_RETENTION_MS = 24 * 60 * 60 * 1000;
|
|
111608
112974
|
|
|
111609
112975
|
class LeaseStillActive extends Error {
|
|
@@ -111644,7 +113010,7 @@ async function pruneBus(root, options = {}) {
|
|
|
111644
113010
|
const file = leasePath(root, lease.leaseId);
|
|
111645
113011
|
try {
|
|
111646
113012
|
await appendEvent(root, {
|
|
111647
|
-
from: { instanceId:
|
|
113013
|
+
from: { instanceId: randomUUID38(), name: "system", origin: "system" },
|
|
111648
113014
|
to: ["*"],
|
|
111649
113015
|
toLabel: "@all",
|
|
111650
113016
|
kind: "lease-expired",
|
|
@@ -111838,7 +113204,7 @@ ${PAUSE_USAGE3}`);
|
|
|
111838
113204
|
if (toLabel === undefined || reason === undefined || reason.length === 0) {
|
|
111839
113205
|
throw new BusRefusal("invalid-event", PAUSE_USAGE3);
|
|
111840
113206
|
}
|
|
111841
|
-
const holder = { instanceId:
|
|
113207
|
+
const holder = { instanceId: randomUUID39(), name: "cli", origin: "cli" };
|
|
111842
113208
|
const lease = await createPauseLease(busRoot, {
|
|
111843
113209
|
holder,
|
|
111844
113210
|
toLabel,
|
|
@@ -111860,7 +113226,7 @@ async function resume(args, busRoot, ctx) {
|
|
|
111860
113226
|
if (leaseId === undefined) {
|
|
111861
113227
|
throw new BusRefusal("invalid-event", "usage: keryx bus resume <leaseId> [--json]");
|
|
111862
113228
|
}
|
|
111863
|
-
await resumePauseLease(busRoot, { leaseId, by: { instanceId:
|
|
113229
|
+
await resumePauseLease(busRoot, { leaseId, by: { instanceId: randomUUID39(), name: "cli", origin: "cli" }, now: ctx.now });
|
|
111864
113230
|
if (args.includes("--json")) {
|
|
111865
113231
|
ctx.out(JSON.stringify({ schemaVersion: 1, leaseId }, null, 2));
|
|
111866
113232
|
} else {
|
|
@@ -112026,7 +113392,7 @@ function buildInitFlags(next, profile) {
|
|
|
112026
113392
|
async function modulesCommand(args = []) {
|
|
112027
113393
|
const sub = args[0];
|
|
112028
113394
|
if (sub === "--help" || sub === "-h" || sub === "help") {
|
|
112029
|
-
|
|
113395
|
+
printHelp19();
|
|
112030
113396
|
return;
|
|
112031
113397
|
}
|
|
112032
113398
|
const wantsJson = args.includes("--json") && (sub === undefined || sub === "status" || sub === "list" || sub === "--json");
|
|
@@ -112083,7 +113449,7 @@ async function modulesCommand(args = []) {
|
|
|
112083
113449
|
}
|
|
112084
113450
|
}
|
|
112085
113451
|
} else {
|
|
112086
|
-
|
|
113452
|
+
printHelp19();
|
|
112087
113453
|
process.exitCode = 1;
|
|
112088
113454
|
return;
|
|
112089
113455
|
}
|
|
@@ -112126,7 +113492,7 @@ function setsEqual(a, b) {
|
|
|
112126
113492
|
}
|
|
112127
113493
|
return true;
|
|
112128
113494
|
}
|
|
112129
|
-
function
|
|
113495
|
+
function printHelp19() {
|
|
112130
113496
|
helpTitle("keryx modules", "view and toggle Metaproject modules");
|
|
112131
113497
|
helpUsage([
|
|
112132
113498
|
"keryx modules",
|
|
@@ -112145,12 +113511,12 @@ function printHelp18() {
|
|
|
112145
113511
|
}
|
|
112146
113512
|
|
|
112147
113513
|
// src/commands/serve.ts
|
|
112148
|
-
import { randomUUID as
|
|
113514
|
+
import { randomUUID as randomUUID42 } from "crypto";
|
|
112149
113515
|
init_serve_config();
|
|
112150
113516
|
|
|
112151
113517
|
// src/lib/serve-credential.ts
|
|
112152
113518
|
init_config_dir();
|
|
112153
|
-
import { createHash as createHash44, randomBytes as randomBytes5, randomUUID as
|
|
113519
|
+
import { createHash as createHash44, randomBytes as randomBytes5, randomUUID as randomUUID40 } from "crypto";
|
|
112154
113520
|
import {
|
|
112155
113521
|
chmodSync as chmodSync5,
|
|
112156
113522
|
closeSync as closeSync4,
|
|
@@ -112234,7 +113600,7 @@ function readServeCredential(dir) {
|
|
|
112234
113600
|
}
|
|
112235
113601
|
function writeStore(store, dir) {
|
|
112236
113602
|
const file = serveCredentialPath(dir);
|
|
112237
|
-
const temp = `${file}.${
|
|
113603
|
+
const temp = `${file}.${randomUUID40()}.tmp`;
|
|
112238
113604
|
try {
|
|
112239
113605
|
ensureKeryxConfigDir(dir);
|
|
112240
113606
|
const handle = openSync5(temp, "wx", 384);
|
|
@@ -112274,7 +113640,7 @@ function mintRecord(now) {
|
|
|
112274
113640
|
const salt = randomBytes5(32).toString("hex");
|
|
112275
113641
|
return {
|
|
112276
113642
|
token,
|
|
112277
|
-
record: { id:
|
|
113643
|
+
record: { id: randomUUID40(), algorithm: "sha256", salt, hash: hashToken(salt, token), createdAt: now }
|
|
112278
113644
|
};
|
|
112279
113645
|
}
|
|
112280
113646
|
function issueServeToken(dir, now = () => new Date().toISOString(), onWaiting) {
|
|
@@ -112591,7 +113957,7 @@ function releaseIdempotencyKey(project, idempotencyKey, turnId, dir) {
|
|
|
112591
113957
|
}
|
|
112592
113958
|
|
|
112593
113959
|
// src/lib/serve-turn.ts
|
|
112594
|
-
import { randomUUID as
|
|
113960
|
+
import { randomUUID as randomUUID41 } from "crypto";
|
|
112595
113961
|
import path203 from "path";
|
|
112596
113962
|
init_service();
|
|
112597
113963
|
var REMOTE_ORIGIN = "remote:http";
|
|
@@ -112710,7 +114076,7 @@ async function redactOut(security, text) {
|
|
|
112710
114076
|
async function runRemoteTurn(input) {
|
|
112711
114077
|
const scanRoot = input.scanRoot;
|
|
112712
114078
|
const security = createSecurityService(scanRoot);
|
|
112713
|
-
const newId = input.newId ?? (() =>
|
|
114079
|
+
const newId = input.newId ?? (() => randomUUID41());
|
|
112714
114080
|
const clock = input.clock ?? (() => new Date().toISOString());
|
|
112715
114081
|
const turnId = input.turnId ?? newId();
|
|
112716
114082
|
const sessionId = input.request.sessionId ?? newId();
|
|
@@ -112853,7 +114219,7 @@ function outcomeOf(status, gate, unresolvedBlockerIds) {
|
|
|
112853
114219
|
}
|
|
112854
114220
|
function createSubmitTurn(deps) {
|
|
112855
114221
|
return async (request, project) => {
|
|
112856
|
-
const turnId = (deps.newId ?? (() =>
|
|
114222
|
+
const turnId = (deps.newId ?? (() => randomUUID41()))();
|
|
112857
114223
|
const scanned = await scanPrompt(deps.dir, request.prompt);
|
|
112858
114224
|
if (scanned.rejected) {
|
|
112859
114225
|
return { kind: "rejected" };
|
|
@@ -112908,40 +114274,40 @@ function claimTurnKey(request, turnId, project, dir) {
|
|
|
112908
114274
|
// src/lib/serve-server.ts
|
|
112909
114275
|
init_serve_config();
|
|
112910
114276
|
var MAX_TURN_BODY_BYTES = 128 * 1024;
|
|
112911
|
-
function
|
|
114277
|
+
function refuse4(reason, message) {
|
|
112912
114278
|
return { ok: false, state: "refused", reason, message };
|
|
112913
114279
|
}
|
|
112914
114280
|
function resolveServeStartup(input) {
|
|
112915
114281
|
const { config, credential } = input;
|
|
112916
114282
|
if (config === null) {
|
|
112917
|
-
return
|
|
114283
|
+
return refuse4("no-configuration", serveConfigAdvice(input.configState ?? "absent"));
|
|
112918
114284
|
}
|
|
112919
114285
|
if (!config.enabled) {
|
|
112920
|
-
return
|
|
114286
|
+
return refuse4("disabled", serveConfigAdvice("valid"));
|
|
112921
114287
|
}
|
|
112922
114288
|
if (config.credentialRef.store !== "auth-json") {
|
|
112923
|
-
return
|
|
114289
|
+
return refuse4("unsupported-credential-store", `credentialRef.store "${config.credentialRef.store}" is not implemented in this release; only "auth-json" is supported.`);
|
|
112924
114290
|
}
|
|
112925
114291
|
if (credential.status === "unreadable") {
|
|
112926
|
-
return
|
|
114292
|
+
return refuse4("unreadable-credential", `${credential.message}. Inspect it, then run \`keryx serve token rotate\`.`);
|
|
112927
114293
|
}
|
|
112928
114294
|
if (credential.status === "absent") {
|
|
112929
|
-
return
|
|
114295
|
+
return refuse4("no-credential", "no serve credential exists. Run `keryx serve token issue` \u2014 the token is printed once and never again.");
|
|
112930
114296
|
}
|
|
112931
114297
|
if (credential.record.id !== config.credentialRef.id) {
|
|
112932
|
-
return
|
|
114298
|
+
return refuse4("no-credential", "the configured credential reference does not match the credential in the store. Run `keryx serve token rotate` to re-issue and re-point it.");
|
|
112933
114299
|
}
|
|
112934
114300
|
const nonLoopback = !isLoopbackAddress(config.bind.address);
|
|
112935
114301
|
if (nonLoopback && config.bind.acknowledgeNonLoopback !== true) {
|
|
112936
|
-
return
|
|
114302
|
+
return refuse4("non-loopback-not-acknowledged", `the configured bind address is reachable beyond loopback. Run \`keryx serve config set --bind ${config.bind.address} --acknowledge-non-loopback\` to acknowledge it explicitly; there is no TLS in this release.`);
|
|
112937
114303
|
}
|
|
112938
114304
|
const remoteProfile = resolveRemoteProfile(config.profile);
|
|
112939
114305
|
if (remoteProfile === null) {
|
|
112940
|
-
return
|
|
114306
|
+
return refuse4("unknown-profile", `profile "${config.profile}" is not implemented in this release; valid profiles are ${REMOTE_PROFILE_NAMES.join(", ")}. Run \`keryx serve config set --profile remote-restricted\``);
|
|
112941
114307
|
}
|
|
112942
114308
|
const comparison = compareProfiles((input.localBaseline ?? localBaselineProfile)(), remoteProfile);
|
|
112943
114309
|
if (!comparison.ok) {
|
|
112944
|
-
return
|
|
114310
|
+
return refuse4("widening-profile", `profile "${config.profile}" would grant more than the local profile allows (${comparison.widened.join(", ")}). Run \`keryx serve config set --profile remote-restricted\``);
|
|
112945
114311
|
}
|
|
112946
114312
|
return { ok: true, config, credential: credential.record, nonLoopback, profile: remoteProfile };
|
|
112947
114313
|
}
|
|
@@ -112978,7 +114344,7 @@ function matchRoute(pathname) {
|
|
|
112978
114344
|
return { route: "none" };
|
|
112979
114345
|
}
|
|
112980
114346
|
var JSON_HEADERS = { "content-type": "application/json; charset=utf-8" };
|
|
112981
|
-
function
|
|
114347
|
+
function errorResponse2(status, code, message, headers = {}) {
|
|
112982
114348
|
return new Response(`${JSON.stringify({ error: { code, message } })}
|
|
112983
114349
|
`, {
|
|
112984
114350
|
status,
|
|
@@ -112986,7 +114352,7 @@ function errorResponse(status, code, message, headers = {}) {
|
|
|
112986
114352
|
});
|
|
112987
114353
|
}
|
|
112988
114354
|
function unauthorized() {
|
|
112989
|
-
return
|
|
114355
|
+
return errorResponse2(401, "unauthorized", "Unauthorized.", { "www-authenticate": "Bearer" });
|
|
112990
114356
|
}
|
|
112991
114357
|
function bearerToken(request) {
|
|
112992
114358
|
const header = request.headers.get("authorization");
|
|
@@ -113013,33 +114379,33 @@ function summarizeRegistryWarnings(messages) {
|
|
|
113013
114379
|
async function submitTurn(request, ctx) {
|
|
113014
114380
|
const declared = Number(request.headers.get("content-length") ?? "0");
|
|
113015
114381
|
if (Number.isFinite(declared) && declared > MAX_TURN_BODY_BYTES) {
|
|
113016
|
-
return
|
|
114382
|
+
return errorResponse2(413, "too-large", "The request body exceeds the configured bound.");
|
|
113017
114383
|
}
|
|
113018
114384
|
const contentType = (request.headers.get("content-type") ?? "").split(";")[0]?.trim().toLowerCase();
|
|
113019
114385
|
if (contentType !== "application/json") {
|
|
113020
|
-
return
|
|
114386
|
+
return errorResponse2(400, "invalid-request", "Content-Type must be application/json.");
|
|
113021
114387
|
}
|
|
113022
114388
|
const raw = await request.text();
|
|
113023
114389
|
if (Buffer.byteLength(raw, "utf8") > MAX_TURN_BODY_BYTES) {
|
|
113024
|
-
return
|
|
114390
|
+
return errorResponse2(413, "too-large", "The request body exceeds the configured bound.");
|
|
113025
114391
|
}
|
|
113026
114392
|
let parsed;
|
|
113027
114393
|
try {
|
|
113028
114394
|
parsed = JSON.parse(raw);
|
|
113029
114395
|
} catch {
|
|
113030
|
-
return
|
|
114396
|
+
return errorResponse2(400, "invalid-request", "The request body is not valid JSON.");
|
|
113031
114397
|
}
|
|
113032
114398
|
const validated = validateTurnRequest(parsed);
|
|
113033
114399
|
if (!validated.ok) {
|
|
113034
|
-
return
|
|
114400
|
+
return errorResponse2(validated.problem.status, validated.problem.code, validated.problem.message);
|
|
113035
114401
|
}
|
|
113036
114402
|
const project = resolveProject(validated.request.project, ctx.dir);
|
|
113037
114403
|
if (!project.ok) {
|
|
113038
|
-
return
|
|
114404
|
+
return errorResponse2(404, project.code, project.message);
|
|
113039
114405
|
}
|
|
113040
114406
|
const submit = ctx.submitTurn;
|
|
113041
114407
|
if (submit === undefined) {
|
|
113042
|
-
return
|
|
114408
|
+
return errorResponse2(503, "unavailable", "Turn execution is not available on this listener.");
|
|
113043
114409
|
}
|
|
113044
114410
|
const outcome = await submit(validated.request, project.project);
|
|
113045
114411
|
if (outcome.kind === "duplicate") {
|
|
@@ -113047,10 +114413,10 @@ async function submitTurn(request, ctx) {
|
|
|
113047
114413
|
`, { status: 200, headers: JSON_HEADERS });
|
|
113048
114414
|
}
|
|
113049
114415
|
if (outcome.kind === "rejected") {
|
|
113050
|
-
return
|
|
114416
|
+
return errorResponse2(422, "prompt-rejected", "The prompt was rejected.");
|
|
113051
114417
|
}
|
|
113052
114418
|
if (outcome.kind === "unavailable") {
|
|
113053
|
-
return
|
|
114419
|
+
return errorResponse2(500, "record-unreadable", "The durable record for this turn could not be read.");
|
|
113054
114420
|
}
|
|
113055
114421
|
return new Response(`${JSON.stringify({ schemaVersion: "1.0.0", turnId: outcome.turnId, sessionId: outcome.sessionId }, null, 2)}
|
|
113056
114422
|
`, { status: 202, headers: JSON_HEADERS });
|
|
@@ -113061,7 +114427,7 @@ function streamTurnEvents(request, turnId, ctx) {
|
|
|
113061
114427
|
const after = Number.isInteger(parsed) && parsed >= 0 ? parsed : -1;
|
|
113062
114428
|
const events = readTurnEvents(turnId, after, ctx.dir);
|
|
113063
114429
|
if (!events.ok) {
|
|
113064
|
-
return
|
|
114430
|
+
return errorResponse2(500, "record-unreadable", "The durable record for this turn could not be read.");
|
|
113065
114431
|
}
|
|
113066
114432
|
const body = events.value.map((event) => `id: ${event.seq}
|
|
113067
114433
|
data: ${JSON.stringify(event)}
|
|
@@ -113084,7 +114450,7 @@ async function handleServeRequest(request, ctx) {
|
|
|
113084
114450
|
}
|
|
113085
114451
|
function internalErrorResponse(cause) {
|
|
113086
114452
|
console.error(`keryx serve: request failed: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
113087
|
-
return
|
|
114453
|
+
return errorResponse2(500, "internal-error", "The request could not be completed.");
|
|
113088
114454
|
}
|
|
113089
114455
|
async function routeServeRequest(request, ctx) {
|
|
113090
114456
|
const credential = ctx.resolveCredential();
|
|
@@ -113094,7 +114460,7 @@ async function routeServeRequest(request, ctx) {
|
|
|
113094
114460
|
const standing = ctx.throttle.check(peer);
|
|
113095
114461
|
const verdict = standing.throttled ? standing : ctx.throttle.recordFailure(peer);
|
|
113096
114462
|
if (verdict.throttled) {
|
|
113097
|
-
return
|
|
114463
|
+
return errorResponse2(429, "too-many-requests", "Too many requests.", {
|
|
113098
114464
|
"retry-after": String(verdict.retryAfterSeconds ?? 60)
|
|
113099
114465
|
});
|
|
113100
114466
|
}
|
|
@@ -113102,35 +114468,35 @@ async function routeServeRequest(request, ctx) {
|
|
|
113102
114468
|
return unauthorized();
|
|
113103
114469
|
}
|
|
113104
114470
|
if (ctx.state() === "draining") {
|
|
113105
|
-
return
|
|
114471
|
+
return errorResponse2(503, "draining", "The server is draining.");
|
|
113106
114472
|
}
|
|
113107
114473
|
const pathname = new URL(request.url).pathname;
|
|
113108
114474
|
const matched = matchRoute(pathname);
|
|
113109
114475
|
if (matched.route === "none") {
|
|
113110
|
-
return
|
|
114476
|
+
return errorResponse2(404, "not-found", "Not found.");
|
|
113111
114477
|
}
|
|
113112
114478
|
if (matched.route === "fixed" && pathname === "/v1/turns") {
|
|
113113
114479
|
if (request.method !== "POST") {
|
|
113114
|
-
return
|
|
114480
|
+
return errorResponse2(405, "method-not-allowed", "Method not allowed.", { allow: "POST" });
|
|
113115
114481
|
}
|
|
113116
114482
|
return submitTurn(request, ctx);
|
|
113117
114483
|
}
|
|
113118
114484
|
if (request.method !== "GET") {
|
|
113119
|
-
return
|
|
114485
|
+
return errorResponse2(405, "method-not-allowed", "Method not allowed.", { allow: "GET" });
|
|
113120
114486
|
}
|
|
113121
114487
|
if (matched.route === "turn" || matched.route === "turn-events") {
|
|
113122
114488
|
const record = readTurnRecord(matched.turnId, ctx.dir);
|
|
113123
114489
|
if (!record.ok) {
|
|
113124
114490
|
if (isServerFault(record.reason)) {
|
|
113125
|
-
return
|
|
114491
|
+
return errorResponse2(500, "record-unreadable", "The durable record for this turn could not be read.");
|
|
113126
114492
|
}
|
|
113127
|
-
return
|
|
114493
|
+
return errorResponse2(404, "not-found", "Not found.");
|
|
113128
114494
|
}
|
|
113129
114495
|
if (matched.route === "turn-events") {
|
|
113130
114496
|
return streamTurnEvents(request, matched.turnId, ctx);
|
|
113131
114497
|
}
|
|
113132
114498
|
if (record.value.result === undefined) {
|
|
113133
|
-
return
|
|
114499
|
+
return errorResponse2(409, "turn-in-progress", "The turn has not reached a terminal state.");
|
|
113134
114500
|
}
|
|
113135
114501
|
return new Response(`${JSON.stringify(record.value.result, null, 2)}
|
|
113136
114502
|
`, { headers: JSON_HEADERS });
|
|
@@ -113182,11 +114548,11 @@ async function startServeListener(input) {
|
|
|
113182
114548
|
error: internalErrorResponse
|
|
113183
114549
|
});
|
|
113184
114550
|
} catch (error) {
|
|
113185
|
-
return
|
|
114551
|
+
return refuse4("bind-failed", `could not bind ${hostname}:${startup.config.bind.port}: ${error instanceof Error ? error.message : String(error)}`);
|
|
113186
114552
|
}
|
|
113187
114553
|
if (typeof server.port !== "number" || server.port <= 0) {
|
|
113188
114554
|
await server.stop(true);
|
|
113189
|
-
return
|
|
114555
|
+
return refuse4("bind-failed", "the listener reported no bound port");
|
|
113190
114556
|
}
|
|
113191
114557
|
boundPort = server.port;
|
|
113192
114558
|
state = "listening";
|
|
@@ -113231,7 +114597,7 @@ function assembleSubmitTurn(profile, dir, seams = {}) {
|
|
|
113231
114597
|
}
|
|
113232
114598
|
|
|
113233
114599
|
// src/commands/serve.ts
|
|
113234
|
-
function
|
|
114600
|
+
function parseArgs4(args, valueFlags, booleanFlags) {
|
|
113235
114601
|
const values = new Map;
|
|
113236
114602
|
const flags = new Set;
|
|
113237
114603
|
for (let index = 0;index < args.length; index += 1) {
|
|
@@ -113277,7 +114643,7 @@ var ENABLE_FLAG = "--enable";
|
|
|
113277
114643
|
var DISABLE_FLAG = "--disable";
|
|
113278
114644
|
async function serveCommand(args = []) {
|
|
113279
114645
|
if (args.includes("--help") || args.includes("-h") || args[0] === "help") {
|
|
113280
|
-
|
|
114646
|
+
printHelp20();
|
|
113281
114647
|
return;
|
|
113282
114648
|
}
|
|
113283
114649
|
const sub = args[0];
|
|
@@ -113298,14 +114664,14 @@ async function serveCommand(args = []) {
|
|
|
113298
114664
|
return;
|
|
113299
114665
|
}
|
|
113300
114666
|
console.error(`Unknown serve command: ${sanitizeForDisplay(sub)}`);
|
|
113301
|
-
|
|
114667
|
+
printHelp20();
|
|
113302
114668
|
process.exitCode = 1;
|
|
113303
114669
|
}
|
|
113304
114670
|
async function runServe(args) {
|
|
113305
|
-
const parsed =
|
|
114671
|
+
const parsed = parseArgs4(args, BIND_FLAGS, [ACK_FLAG]);
|
|
113306
114672
|
if (!parsed.ok) {
|
|
113307
114673
|
console.error(parsed.message);
|
|
113308
|
-
|
|
114674
|
+
printHelp20();
|
|
113309
114675
|
process.exitCode = 1;
|
|
113310
114676
|
return;
|
|
113311
114677
|
}
|
|
@@ -113404,10 +114770,10 @@ function readPort(raw, options) {
|
|
|
113404
114770
|
return port;
|
|
113405
114771
|
}
|
|
113406
114772
|
function runStatus7(args) {
|
|
113407
|
-
const parsed =
|
|
114773
|
+
const parsed = parseArgs4(args, [], ["--json"]);
|
|
113408
114774
|
if (!parsed.ok) {
|
|
113409
114775
|
console.error(parsed.message);
|
|
113410
|
-
|
|
114776
|
+
printHelp20();
|
|
113411
114777
|
process.exitCode = 1;
|
|
113412
114778
|
return;
|
|
113413
114779
|
}
|
|
@@ -113451,10 +114817,10 @@ function runStatus7(args) {
|
|
|
113451
114817
|
}
|
|
113452
114818
|
function runToken(args) {
|
|
113453
114819
|
const sub = args[0];
|
|
113454
|
-
const rest =
|
|
114820
|
+
const rest = parseArgs4(args.slice(1), [], []);
|
|
113455
114821
|
if (!rest.ok) {
|
|
113456
114822
|
console.error(rest.message);
|
|
113457
|
-
|
|
114823
|
+
printHelp20();
|
|
113458
114824
|
process.exitCode = 1;
|
|
113459
114825
|
return;
|
|
113460
114826
|
}
|
|
@@ -113495,7 +114861,7 @@ function runToken(args) {
|
|
|
113495
114861
|
return;
|
|
113496
114862
|
}
|
|
113497
114863
|
console.error(sub === undefined ? "Missing token subcommand" : `Unknown token command: ${sanitizeForDisplay(sub)}`);
|
|
113498
|
-
|
|
114864
|
+
printHelp20();
|
|
113499
114865
|
process.exitCode = 1;
|
|
113500
114866
|
}
|
|
113501
114867
|
function printTokenOnce(token) {
|
|
@@ -113517,10 +114883,10 @@ function pointConfigurationAt(credentialId) {
|
|
|
113517
114883
|
function runConfig(args) {
|
|
113518
114884
|
const sub = args[0];
|
|
113519
114885
|
if (sub === "init") {
|
|
113520
|
-
const parsed =
|
|
114886
|
+
const parsed = parseArgs4(args.slice(1), BIND_FLAGS, [ACK_FLAG, FORCE_FLAG]);
|
|
113521
114887
|
if (!parsed.ok) {
|
|
113522
114888
|
console.error(parsed.message);
|
|
113523
|
-
|
|
114889
|
+
printHelp20();
|
|
113524
114890
|
process.exitCode = 1;
|
|
113525
114891
|
return;
|
|
113526
114892
|
}
|
|
@@ -113541,7 +114907,7 @@ function runConfig(args) {
|
|
|
113541
114907
|
return;
|
|
113542
114908
|
}
|
|
113543
114909
|
const credential = readServeCredential();
|
|
113544
|
-
const credentialId = credential.status === "ok" ? credential.record.id :
|
|
114910
|
+
const credentialId = credential.status === "ok" ? credential.record.id : randomUUID42();
|
|
113545
114911
|
const config = defaultServeConfig(credentialId, {
|
|
113546
114912
|
address: parsed.parsed.values.get("--bind") ?? DEFAULT_SERVE_BIND_ADDRESS,
|
|
113547
114913
|
port: port ?? DEFAULT_SERVE_PORT,
|
|
@@ -113567,10 +114933,10 @@ function runConfig(args) {
|
|
|
113567
114933
|
return;
|
|
113568
114934
|
}
|
|
113569
114935
|
if (sub === "show") {
|
|
113570
|
-
const parsed =
|
|
114936
|
+
const parsed = parseArgs4(args.slice(1), [], ["--json"]);
|
|
113571
114937
|
if (!parsed.ok) {
|
|
113572
114938
|
console.error(parsed.message);
|
|
113573
|
-
|
|
114939
|
+
printHelp20();
|
|
113574
114940
|
process.exitCode = 1;
|
|
113575
114941
|
return;
|
|
113576
114942
|
}
|
|
@@ -113587,14 +114953,14 @@ function runConfig(args) {
|
|
|
113587
114953
|
return;
|
|
113588
114954
|
}
|
|
113589
114955
|
console.error(sub === undefined ? "Missing config subcommand" : `Unknown config command: ${sanitizeForDisplay(sub)}`);
|
|
113590
|
-
|
|
114956
|
+
printHelp20();
|
|
113591
114957
|
process.exitCode = 1;
|
|
113592
114958
|
}
|
|
113593
114959
|
function runConfigSet(args) {
|
|
113594
|
-
const parsed =
|
|
114960
|
+
const parsed = parseArgs4(args, BIND_FLAGS, [ACK_FLAG, NO_ACK_FLAG, ENABLE_FLAG, DISABLE_FLAG]);
|
|
113595
114961
|
if (!parsed.ok) {
|
|
113596
114962
|
console.error(parsed.message);
|
|
113597
|
-
|
|
114963
|
+
printHelp20();
|
|
113598
114964
|
process.exitCode = 1;
|
|
113599
114965
|
return;
|
|
113600
114966
|
}
|
|
@@ -113665,7 +115031,7 @@ function printConfig(config) {
|
|
|
113665
115031
|
console.log(` approvals: expire after ${config.approval.expirySeconds}s, max ${config.approval.maxPendingPerSession} pending per session`);
|
|
113666
115032
|
console.log(` non-loopback acknowledged: ${config.bind.acknowledgeNonLoopback === true}`);
|
|
113667
115033
|
}
|
|
113668
|
-
function
|
|
115034
|
+
function printHelp20() {
|
|
113669
115035
|
helpTitle("keryx serve", "loopback-bound HTTP entry over this install (off by default)");
|
|
113670
115036
|
helpUsage([
|
|
113671
115037
|
`keryx serve [--bind <addr>] [--port <n>] [--profile <name>] [${ACK_FLAG}]`,
|
|
@@ -113712,7 +115078,7 @@ init_templates3();
|
|
|
113712
115078
|
async function updateCommand(args = []) {
|
|
113713
115079
|
const options = parseUpdateArgs(args);
|
|
113714
115080
|
if (options.help) {
|
|
113715
|
-
|
|
115081
|
+
printHelp21();
|
|
113716
115082
|
return;
|
|
113717
115083
|
}
|
|
113718
115084
|
const projectRoot = process.cwd();
|
|
@@ -114922,7 +116288,7 @@ function runtimeSourcePath2(relativePath) {
|
|
|
114922
116288
|
function escapeRegExp7(value) {
|
|
114923
116289
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
114924
116290
|
}
|
|
114925
|
-
function
|
|
116291
|
+
function printHelp21() {
|
|
114926
116292
|
helpTitle("keryx update", "refresh .metaproject service files (data left untouched)");
|
|
114927
116293
|
helpUsage(["keryx update [--skip-runtime] [--hooks] [--no-tasks]"]);
|
|
114928
116294
|
heading("Default behavior");
|
|
@@ -114953,7 +116319,7 @@ async function dashboardCommand(args = []) {
|
|
|
114953
116319
|
const options = parseOptions(args);
|
|
114954
116320
|
const subcommand = options.positionals[0];
|
|
114955
116321
|
if (!subcommand || options.help) {
|
|
114956
|
-
|
|
116322
|
+
printHelp22();
|
|
114957
116323
|
return;
|
|
114958
116324
|
}
|
|
114959
116325
|
if (subcommand === "build") {
|
|
@@ -114971,7 +116337,7 @@ async function dashboardCommand(args = []) {
|
|
|
114971
116337
|
return;
|
|
114972
116338
|
}
|
|
114973
116339
|
console.log(` ${style.red(symbols.cross)} Unknown dashboard command: ${subcommand}`);
|
|
114974
|
-
|
|
116340
|
+
printHelp22();
|
|
114975
116341
|
process.exitCode = 1;
|
|
114976
116342
|
}
|
|
114977
116343
|
function parseOptions(args) {
|
|
@@ -114996,7 +116362,7 @@ async function openFile(filePath) {
|
|
|
114996
116362
|
});
|
|
114997
116363
|
});
|
|
114998
116364
|
}
|
|
114999
|
-
function
|
|
116365
|
+
function printHelp22() {
|
|
115000
116366
|
helpTitle("keryx dashboard", "build and open the human dashboard");
|
|
115001
116367
|
helpUsage([
|
|
115002
116368
|
"keryx dashboard build",
|
|
@@ -115010,7 +116376,7 @@ function printHelp21() {
|
|
|
115010
116376
|
}
|
|
115011
116377
|
|
|
115012
116378
|
// src/commands/agents.ts
|
|
115013
|
-
import { readFileSync as
|
|
116379
|
+
import { readFileSync as readFileSync15 } from "fs";
|
|
115014
116380
|
|
|
115015
116381
|
// src/agents/bootstrap.ts
|
|
115016
116382
|
import { mkdir as mkdir66, readFile as readFile102, writeFile as writeFile55 } from "fs/promises";
|
|
@@ -115511,7 +116877,7 @@ function monitorCommand(args) {
|
|
|
115511
116877
|
}
|
|
115512
116878
|
let events;
|
|
115513
116879
|
try {
|
|
115514
|
-
events = parseAgentEvents(
|
|
116880
|
+
events = parseAgentEvents(readFileSync15(source, "utf8"));
|
|
115515
116881
|
} catch (error) {
|
|
115516
116882
|
console.error(`Failed to read/parse agent-event source "${source}": ${error.message}`);
|
|
115517
116883
|
process.exitCode = 1;
|
|
@@ -117546,7 +118912,7 @@ async function versionCommand(args, deps = {}) {
|
|
|
117546
118912
|
// src/commands/workspace.ts
|
|
117547
118913
|
init_args();
|
|
117548
118914
|
init_workspace_service();
|
|
117549
|
-
import { randomUUID as
|
|
118915
|
+
import { randomUUID as randomUUID43 } from "crypto";
|
|
117550
118916
|
import { writeFile as writeFile57 } from "fs/promises";
|
|
117551
118917
|
|
|
117552
118918
|
// src/sac/fwk-explain.ts
|
|
@@ -117617,7 +118983,7 @@ function service6() {
|
|
|
117617
118983
|
async function workspaceCommand(args) {
|
|
117618
118984
|
const subcommand = args[0];
|
|
117619
118985
|
if (!subcommand || subcommand === "help" || args.includes("--help") || args.includes("-h"))
|
|
117620
|
-
return
|
|
118986
|
+
return printHelp23();
|
|
117621
118987
|
try {
|
|
117622
118988
|
if (subcommand === "create") {
|
|
117623
118989
|
rejectUnknownOptions(args.slice(1), new Set(["--title", "--component"]));
|
|
@@ -117626,7 +118992,7 @@ async function workspaceCommand(args) {
|
|
|
117626
118992
|
const component = rawComponent === undefined ? undefined : requireWorkspaceReference(rawComponent);
|
|
117627
118993
|
if (!title)
|
|
117628
118994
|
throw new Error("Usage: keryx workspace create --title <title> [--component <workspace-relative-ref>]");
|
|
117629
|
-
const workspace = await service6().create({ request: undefined, requestCorrelationId:
|
|
118995
|
+
const workspace = await service6().create({ request: undefined, requestCorrelationId: randomUUID43(), id: newWorkspaceId(), title, ...component ? { component: { kind: "component", uri: component } } : {} });
|
|
117630
118996
|
console.log(JSON.stringify(workspace, null, 2));
|
|
117631
118997
|
return;
|
|
117632
118998
|
}
|
|
@@ -117656,7 +119022,7 @@ async function workspaceCommand(args) {
|
|
|
117656
119022
|
const revision = optionValue(args, "--revision");
|
|
117657
119023
|
if (!workspaceId || !kind || !rawUri)
|
|
117658
119024
|
throw new Error("Usage: keryx workspace add-resource <workspace-id> --kind <kind> --uri <workspace-relative-ref> [--revision <revision>]");
|
|
117659
|
-
console.log(JSON.stringify(await service6().addResource({ request: undefined, requestCorrelationId:
|
|
119025
|
+
console.log(JSON.stringify(await service6().addResource({ request: undefined, requestCorrelationId: randomUUID43(), workspaceId, resource: { kind, uri: requireWorkspaceReference(rawUri), ...revision ? { revision } : {} } }), null, 2));
|
|
117660
119026
|
return;
|
|
117661
119027
|
}
|
|
117662
119028
|
if (subcommand === "archive") {
|
|
@@ -117664,7 +119030,7 @@ async function workspaceCommand(args) {
|
|
|
117664
119030
|
const workspaceId = args[1];
|
|
117665
119031
|
if (!workspaceId)
|
|
117666
119032
|
throw new Error("Usage: keryx workspace archive <workspace-id>");
|
|
117667
|
-
console.log(JSON.stringify(await service6().archive({ request: undefined, requestCorrelationId:
|
|
119033
|
+
console.log(JSON.stringify(await service6().archive({ request: undefined, requestCorrelationId: randomUUID43(), workspaceId }), null, 2));
|
|
117668
119034
|
return;
|
|
117669
119035
|
}
|
|
117670
119036
|
if (subcommand === "remove-resource") {
|
|
@@ -117673,7 +119039,7 @@ async function workspaceCommand(args) {
|
|
|
117673
119039
|
const uri = optionValue(args, "--uri");
|
|
117674
119040
|
if (!workspaceId || !uri)
|
|
117675
119041
|
throw new Error("Usage: keryx workspace remove-resource <workspace-id> --uri <workspace-relative-ref>");
|
|
117676
|
-
console.log(JSON.stringify(await service6().removeResource({ request: undefined, requestCorrelationId:
|
|
119042
|
+
console.log(JSON.stringify(await service6().removeResource({ request: undefined, requestCorrelationId: randomUUID43(), workspaceId, uri }), null, 2));
|
|
117677
119043
|
return;
|
|
117678
119044
|
}
|
|
117679
119045
|
if (subcommand === "rename") {
|
|
@@ -117682,7 +119048,7 @@ async function workspaceCommand(args) {
|
|
|
117682
119048
|
const title = optionValue(args, "--title");
|
|
117683
119049
|
if (!workspaceId || !title)
|
|
117684
119050
|
throw new Error("Usage: keryx workspace rename <workspace-id> --title <title>");
|
|
117685
|
-
console.log(JSON.stringify(await service6().rename({ request: undefined, requestCorrelationId:
|
|
119051
|
+
console.log(JSON.stringify(await service6().rename({ request: undefined, requestCorrelationId: randomUUID43(), workspaceId, title }), null, 2));
|
|
117686
119052
|
return;
|
|
117687
119053
|
}
|
|
117688
119054
|
if (subcommand === "overview") {
|
|
@@ -117694,7 +119060,7 @@ async function workspaceCommand(args) {
|
|
|
117694
119060
|
const maxTokens = Number(optionValue(args, "--max-tokens") ?? "4096");
|
|
117695
119061
|
if (!Number.isInteger(maxItems) || !Number.isInteger(maxTokens) || maxItems < 0 || maxTokens < 0)
|
|
117696
119062
|
throw new Error("--max-items and --max-tokens must be non-negative integers");
|
|
117697
|
-
const result = await createLocalFwkReadService(process.cwd()).overview({ workspaceId, request: undefined, requestCorrelationId:
|
|
119063
|
+
const result = await createLocalFwkReadService(process.cwd()).overview({ workspaceId, request: undefined, requestCorrelationId: randomUUID43(), budget: { maxItems, maxTokens } });
|
|
117698
119064
|
const normalized = normalizeFwkResult(result);
|
|
117699
119065
|
console.log(JSON.stringify(normalized, null, 2));
|
|
117700
119066
|
if (args.includes("--explain"))
|
|
@@ -117711,7 +119077,7 @@ async function workspaceCommand(args) {
|
|
|
117711
119077
|
const maxTokens = Number(optionValue(args, "--max-tokens") ?? "4096");
|
|
117712
119078
|
if (!Number.isInteger(maxItems) || !Number.isInteger(maxTokens) || maxItems < 0 || maxTokens < 0)
|
|
117713
119079
|
throw new Error("--max-items and --max-tokens must be non-negative integers");
|
|
117714
|
-
const result = await createLocalFwkReadService(process.cwd()).read({ workspaceId, itemId, request: undefined, requestCorrelationId:
|
|
119080
|
+
const result = await createLocalFwkReadService(process.cwd()).read({ workspaceId, itemId, request: undefined, requestCorrelationId: randomUUID43(), budget: { maxItems, maxTokens } });
|
|
117715
119081
|
const normalized = normalizeFwkResult(result);
|
|
117716
119082
|
console.log(JSON.stringify(normalized, null, 2));
|
|
117717
119083
|
if (args.includes("--explain"))
|
|
@@ -117733,14 +119099,14 @@ async function workspaceCommand(args) {
|
|
|
117733
119099
|
const session = findSession(cwd, sessionRef);
|
|
117734
119100
|
if (!session)
|
|
117735
119101
|
throw new Error(`no session matching "${sessionRef}" in this project \u2014 use \`keryx sessions list\``);
|
|
117736
|
-
const proposalId = `proposal-${
|
|
119102
|
+
const proposalId = `proposal-${randomUUID43().replace(/-/g, "").slice(0, 16)}`;
|
|
117737
119103
|
const notePath = proposalNotePath(cwd, workspaceId, proposalId);
|
|
117738
119104
|
const materializedNote = note === undefined ? undefined : prepareOutputForPersistence(await guardOutput({ cwd, content: note, target: "task", source: "generated", path: notePath }), note);
|
|
117739
119105
|
if (materializedNote !== undefined && !materializedNote.allowed)
|
|
117740
119106
|
throw new Error(materializedNote.reason);
|
|
117741
119107
|
const persistedNote = materializedNote?.allowed ? materializedNote.content : undefined;
|
|
117742
119108
|
const { service, wrapUpAuthority, authorizationServer } = createHarnessProposalLifecycleService(cwd, { workspaceId, ...persistedNote ? { note: persistedNote } : {} });
|
|
117743
|
-
const requestCorrelationId =
|
|
119109
|
+
const requestCorrelationId = randomUUID43();
|
|
117744
119110
|
const actor = await authorizationServer.actorContextFor(undefined, requestCorrelationId);
|
|
117745
119111
|
if (!actor)
|
|
117746
119112
|
throw new Error("trusted ActorContext is required");
|
|
@@ -117781,13 +119147,13 @@ async function workspaceCommand(args) {
|
|
|
117781
119147
|
const proposalId = args[2];
|
|
117782
119148
|
const decision = optionValue(args, "--decision");
|
|
117783
119149
|
const reason = optionValue(args, "--reason");
|
|
117784
|
-
const idempotencyKey = optionValue(args, "--idempotency-key") ??
|
|
119150
|
+
const idempotencyKey = optionValue(args, "--idempotency-key") ?? randomUUID43();
|
|
117785
119151
|
const confirmToken = optionValue(args, "--confirm-token");
|
|
117786
119152
|
if (!workspaceId || !proposalId || !decision)
|
|
117787
119153
|
throw new Error("Usage: keryx workspace review <workspace-id> <proposal-id> --decision <accepted|rejected|dismissed> [--reason <reason>] [--idempotency-key <key>] [--confirm-token <token>]");
|
|
117788
119154
|
if (decision === "accepted" && !confirmToken)
|
|
117789
119155
|
throw new Error("--decision accepted requires --confirm-token \u2014 run `keryx workspace confirm-review " + workspaceId + " " + proposalId + "` first");
|
|
117790
|
-
const result = await createHarnessProposalLifecycleService(process.cwd(), { workspaceId }).service.review({ request: undefined, requestCorrelationId:
|
|
119156
|
+
const result = await createHarnessProposalLifecycleService(process.cwd(), { workspaceId }).service.review({ request: undefined, requestCorrelationId: randomUUID43(), workspaceId, proposalId, decision, idempotencyKey, interactive: true, ...reason ? { reason } : {}, ...confirmToken ? { confirmToken } : {} });
|
|
117791
119157
|
console.log(JSON.stringify(normalizeProposalLifecycleResult(result), null, 2));
|
|
117792
119158
|
return;
|
|
117793
119159
|
}
|
|
@@ -117812,7 +119178,7 @@ async function workspaceCommand(args) {
|
|
|
117812
119178
|
const artifactRef = optionValue(args, "--artifact");
|
|
117813
119179
|
if (!workspaceId || !to || !artifactRef)
|
|
117814
119180
|
throw new Error("Usage: keryx workspace handoff <workspace-id> --to <subject> --artifact <ref>");
|
|
117815
|
-
const activity = await createLocalCollaborationService(process.cwd()).record({ request: undefined, requestCorrelationId:
|
|
119181
|
+
const activity = await createLocalCollaborationService(process.cwd()).record({ request: undefined, requestCorrelationId: randomUUID43(), workspaceId, activity: { kind: "handoff-recorded", handoff: { to, artifactRef } } });
|
|
117816
119182
|
console.log(JSON.stringify(activity, null, 2));
|
|
117817
119183
|
return;
|
|
117818
119184
|
}
|
|
@@ -117821,7 +119187,7 @@ async function workspaceCommand(args) {
|
|
|
117821
119187
|
const workspaceId = args[1];
|
|
117822
119188
|
if (!workspaceId)
|
|
117823
119189
|
throw new Error("Usage: keryx workspace collaboration <workspace-id>");
|
|
117824
|
-
console.log(JSON.stringify(await createLocalCollaborationService(process.cwd()).overview({ workspaceId, request: undefined, requestCorrelationId:
|
|
119190
|
+
console.log(JSON.stringify(await createLocalCollaborationService(process.cwd()).overview({ workspaceId, request: undefined, requestCorrelationId: randomUUID43() }), null, 2));
|
|
117825
119191
|
return;
|
|
117826
119192
|
}
|
|
117827
119193
|
if (subcommand === "policy-readiness") {
|
|
@@ -117848,7 +119214,7 @@ async function workspaceCommand(args) {
|
|
|
117848
119214
|
const workspaceId = args[1];
|
|
117849
119215
|
const cwd = process.cwd();
|
|
117850
119216
|
const authorizationServer = localWorkspaceAuthorizationServer();
|
|
117851
|
-
const actor = await authorizationServer.actorContextFor(undefined,
|
|
119217
|
+
const actor = await authorizationServer.actorContextFor(undefined, randomUUID43());
|
|
117852
119218
|
if (!actor)
|
|
117853
119219
|
throw new Error("trusted ActorContext is required");
|
|
117854
119220
|
if (workspaceId) {
|
|
@@ -117906,7 +119272,7 @@ function booleanFlagDefaultTrue(args, name) {
|
|
|
117906
119272
|
return false;
|
|
117907
119273
|
throw new Error(`Unknown value for ${name}: "${value}" \u2014 expected true or false`);
|
|
117908
119274
|
}
|
|
117909
|
-
function
|
|
119275
|
+
function printHelp23() {
|
|
117910
119276
|
console.log(`keryx workspace create --title <title> [--component <workspace-relative-ref>]
|
|
117911
119277
|
keryx workspace list [--include-archived]
|
|
117912
119278
|
keryx workspace show <workspace-id>
|
|
@@ -118090,7 +119456,7 @@ function runAuthStatus(args) {
|
|
|
118090
119456
|
|
|
118091
119457
|
// src/commands/retention.ts
|
|
118092
119458
|
init_args();
|
|
118093
|
-
function
|
|
119459
|
+
function printHelp24() {
|
|
118094
119460
|
console.log(`keryx retention status|sweep \u2014 bound the size of stores that grow with every routed search or refused write
|
|
118095
119461
|
|
|
118096
119462
|
Usage:
|
|
@@ -118143,7 +119509,7 @@ function numericOption(args, name) {
|
|
|
118143
119509
|
async function retentionCommand(args = [], deps = {}) {
|
|
118144
119510
|
const subcommand = args[0];
|
|
118145
119511
|
if (subcommand === undefined || subcommand === "--help" || subcommand === "-h") {
|
|
118146
|
-
|
|
119512
|
+
printHelp24();
|
|
118147
119513
|
return;
|
|
118148
119514
|
}
|
|
118149
119515
|
const cwd = deps.cwd ?? process.cwd();
|
|
@@ -118159,7 +119525,7 @@ async function retentionCommand(args = [], deps = {}) {
|
|
|
118159
119525
|
return;
|
|
118160
119526
|
default:
|
|
118161
119527
|
console.error(`Unknown retention command: ${subcommand}`);
|
|
118162
|
-
|
|
119528
|
+
printHelp24();
|
|
118163
119529
|
process.exitCode = 1;
|
|
118164
119530
|
}
|
|
118165
119531
|
}
|
|
@@ -118241,7 +119607,7 @@ init_args();
|
|
|
118241
119607
|
async function forgettingCommand(args) {
|
|
118242
119608
|
const command = args[0];
|
|
118243
119609
|
if (!command || command === "--help" || command === "-h" || args.includes("--help")) {
|
|
118244
|
-
|
|
119610
|
+
printHelp25();
|
|
118245
119611
|
return;
|
|
118246
119612
|
}
|
|
118247
119613
|
if (command === "trail") {
|
|
@@ -118253,7 +119619,7 @@ async function forgettingCommand(args) {
|
|
|
118253
119619
|
return;
|
|
118254
119620
|
}
|
|
118255
119621
|
console.error(`Unknown forgetting command: ${command}`);
|
|
118256
|
-
|
|
119622
|
+
printHelp25();
|
|
118257
119623
|
process.exitCode = 1;
|
|
118258
119624
|
}
|
|
118259
119625
|
async function runTrail(args) {
|
|
@@ -118343,7 +119709,7 @@ function positional4(args) {
|
|
|
118343
119709
|
}
|
|
118344
119710
|
return;
|
|
118345
119711
|
}
|
|
118346
|
-
function
|
|
119712
|
+
function printHelp25() {
|
|
118347
119713
|
console.log(`keryx forgetting
|
|
118348
119714
|
|
|
118349
119715
|
Read the deletion trail (.metaproject/data/forgetting/journal.jsonl): what was
|
|
@@ -118365,6 +119731,1000 @@ ${TRAIL_SCOPE_CAVEAT}
|
|
|
118365
119731
|
`);
|
|
118366
119732
|
}
|
|
118367
119733
|
|
|
119734
|
+
// src/commands/trigger.ts
|
|
119735
|
+
init_gdgraph();
|
|
119736
|
+
init_service8();
|
|
119737
|
+
|
|
119738
|
+
// src/trigger/run.ts
|
|
119739
|
+
init_fs();
|
|
119740
|
+
import path211 from "path";
|
|
119741
|
+
|
|
119742
|
+
// src/trigger/config.ts
|
|
119743
|
+
init_config_dir();
|
|
119744
|
+
import { existsSync as existsSync37 } from "fs";
|
|
119745
|
+
import path209 from "path";
|
|
119746
|
+
var TRIGGER_EVENT_NAMES = ["post-merge", "post-commit", "post-checkout", "ci"];
|
|
119747
|
+
var TRIGGER_ACTION_KINDS = ["reconcile", "rebuild", "open-flow", "flow-next"];
|
|
119748
|
+
var TRIGGER_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
119749
|
+
var CRON_FIELD_PATTERN = /^[0-9*/,-]+$|^[A-Za-z]{3}(?:[,-][A-Za-z]{3})*$/;
|
|
119750
|
+
function looksLikeCron(value) {
|
|
119751
|
+
const fields = value.trim().split(/\s+/);
|
|
119752
|
+
if (fields.length < 5 || fields.length > 6)
|
|
119753
|
+
return false;
|
|
119754
|
+
return fields.every((field) => CRON_FIELD_PATTERN.test(field));
|
|
119755
|
+
}
|
|
119756
|
+
var TRIGGERS_SCHEMA_VERSION = 1;
|
|
119757
|
+
function fireProblems(value) {
|
|
119758
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
119759
|
+
return ["on: must be an object naming an event or a schedule"];
|
|
119760
|
+
}
|
|
119761
|
+
const raw = value;
|
|
119762
|
+
if (raw.kind === "event") {
|
|
119763
|
+
if (typeof raw.event !== "string" || raw.event.length === 0) {
|
|
119764
|
+
return ["on.event: required, non-empty string"];
|
|
119765
|
+
}
|
|
119766
|
+
if (!TRIGGER_EVENT_NAMES.includes(raw.event)) {
|
|
119767
|
+
return [`on.event: unknown event "${raw.event}" (expected one of ${TRIGGER_EVENT_NAMES.join(", ")})`];
|
|
119768
|
+
}
|
|
119769
|
+
return [];
|
|
119770
|
+
}
|
|
119771
|
+
if (raw.kind === "schedule") {
|
|
119772
|
+
if (typeof raw.cron !== "string" || raw.cron.trim().length === 0) {
|
|
119773
|
+
return ["on.cron: required, non-empty string"];
|
|
119774
|
+
}
|
|
119775
|
+
if (!looksLikeCron(raw.cron)) {
|
|
119776
|
+
return [`on.cron: "${raw.cron}" does not look like a 5- or 6-field cron expression`];
|
|
119777
|
+
}
|
|
119778
|
+
return [];
|
|
119779
|
+
}
|
|
119780
|
+
return ['on.kind: must be "event" or "schedule"'];
|
|
119781
|
+
}
|
|
119782
|
+
function actionProblems(value) {
|
|
119783
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
119784
|
+
return ["action: must be an object naming what the trigger does"];
|
|
119785
|
+
}
|
|
119786
|
+
const raw = value;
|
|
119787
|
+
if (typeof raw.kind !== "string" || !TRIGGER_ACTION_KINDS.includes(raw.kind)) {
|
|
119788
|
+
return [`action.kind: must be one of ${TRIGGER_ACTION_KINDS.join(", ")}`];
|
|
119789
|
+
}
|
|
119790
|
+
if (raw.kind === "open-flow") {
|
|
119791
|
+
const problems = [];
|
|
119792
|
+
if (typeof raw.template !== "string" || raw.template.length === 0) {
|
|
119793
|
+
problems.push("action.template: required, non-empty string");
|
|
119794
|
+
}
|
|
119795
|
+
if (raw.skipIfOpen !== undefined && typeof raw.skipIfOpen !== "boolean") {
|
|
119796
|
+
problems.push("action.skipIfOpen: must be a boolean when present");
|
|
119797
|
+
}
|
|
119798
|
+
return problems;
|
|
119799
|
+
}
|
|
119800
|
+
if (raw.kind === "flow-next") {
|
|
119801
|
+
if (typeof raw.flow !== "string" || raw.flow.length === 0) {
|
|
119802
|
+
return ["action.flow: required, non-empty string"];
|
|
119803
|
+
}
|
|
119804
|
+
return [];
|
|
119805
|
+
}
|
|
119806
|
+
return [];
|
|
119807
|
+
}
|
|
119808
|
+
function triggerEntryProblems(value) {
|
|
119809
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
119810
|
+
return ["entry: must be an object"];
|
|
119811
|
+
}
|
|
119812
|
+
const raw = value;
|
|
119813
|
+
const problems = [];
|
|
119814
|
+
if (typeof raw.name !== "string" || raw.name.length === 0) {
|
|
119815
|
+
problems.push("name: required, non-empty string");
|
|
119816
|
+
} else if (!TRIGGER_NAME_PATTERN.test(raw.name)) {
|
|
119817
|
+
problems.push(`name: "${raw.name}" must start with a letter or digit and contain only letters, digits, "-", "_" or "." (max 64 chars) \u2014 it is passed as the \`keryx trigger run <name>\` argument`);
|
|
119818
|
+
}
|
|
119819
|
+
if (raw.on === undefined) {
|
|
119820
|
+
problems.push("on: required");
|
|
119821
|
+
} else {
|
|
119822
|
+
problems.push(...fireProblems(raw.on));
|
|
119823
|
+
}
|
|
119824
|
+
if (raw.action === undefined) {
|
|
119825
|
+
problems.push("action: required");
|
|
119826
|
+
} else {
|
|
119827
|
+
problems.push(...actionProblems(raw.action));
|
|
119828
|
+
}
|
|
119829
|
+
if (raw.enabled !== undefined && typeof raw.enabled !== "boolean") {
|
|
119830
|
+
problems.push("enabled: must be a boolean when present");
|
|
119831
|
+
}
|
|
119832
|
+
return problems;
|
|
119833
|
+
}
|
|
119834
|
+
function parseTriggerEntry(value) {
|
|
119835
|
+
try {
|
|
119836
|
+
if (triggerEntryProblems(value).length > 0)
|
|
119837
|
+
return;
|
|
119838
|
+
const raw = value;
|
|
119839
|
+
const fire = raw.on.kind === "event" ? { kind: "event", event: raw.on.event } : { kind: "schedule", cron: raw.on.cron };
|
|
119840
|
+
return {
|
|
119841
|
+
name: raw.name,
|
|
119842
|
+
fire,
|
|
119843
|
+
action: raw.action,
|
|
119844
|
+
enabled: raw.enabled ?? true
|
|
119845
|
+
};
|
|
119846
|
+
} catch {
|
|
119847
|
+
return;
|
|
119848
|
+
}
|
|
119849
|
+
}
|
|
119850
|
+
function triggersConfigPath(projectRoot) {
|
|
119851
|
+
return path209.join(projectRoot, ".metaproject", "triggers.json");
|
|
119852
|
+
}
|
|
119853
|
+
function loadTriggersConfig(projectRoot) {
|
|
119854
|
+
const empty = (fileProblem) => ({
|
|
119855
|
+
triggers: [],
|
|
119856
|
+
rejected: [],
|
|
119857
|
+
fileProblem
|
|
119858
|
+
});
|
|
119859
|
+
const file = triggersConfigPath(projectRoot);
|
|
119860
|
+
if (!existsSync37(file)) {
|
|
119861
|
+
return empty("absent");
|
|
119862
|
+
}
|
|
119863
|
+
const read = readConfigFile(file);
|
|
119864
|
+
if (!read.ok) {
|
|
119865
|
+
return empty("unreadable");
|
|
119866
|
+
}
|
|
119867
|
+
let parsed;
|
|
119868
|
+
try {
|
|
119869
|
+
parsed = JSON.parse(read.text);
|
|
119870
|
+
} catch {
|
|
119871
|
+
return empty("not-json");
|
|
119872
|
+
}
|
|
119873
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
119874
|
+
return empty("not-an-object");
|
|
119875
|
+
}
|
|
119876
|
+
const raw = parsed;
|
|
119877
|
+
if (raw.schemaVersion !== TRIGGERS_SCHEMA_VERSION) {
|
|
119878
|
+
return empty("wrong-schema-version");
|
|
119879
|
+
}
|
|
119880
|
+
if (!Array.isArray(raw.triggers)) {
|
|
119881
|
+
return empty("triggers-not-an-array");
|
|
119882
|
+
}
|
|
119883
|
+
const triggers = [];
|
|
119884
|
+
const rejected = [];
|
|
119885
|
+
const seenNames = new Set;
|
|
119886
|
+
raw.triggers.forEach((candidate, index) => {
|
|
119887
|
+
let reasons;
|
|
119888
|
+
try {
|
|
119889
|
+
reasons = triggerEntryProblems(candidate);
|
|
119890
|
+
} catch (error) {
|
|
119891
|
+
reasons = [`entry: threw during validation (${error instanceof Error ? error.message : String(error)})`];
|
|
119892
|
+
}
|
|
119893
|
+
const candidateName = candidate !== null && typeof candidate === "object" && typeof candidate.name === "string" ? candidate.name : undefined;
|
|
119894
|
+
if (reasons.length === 0 && candidateName !== undefined && seenNames.has(candidateName)) {
|
|
119895
|
+
reasons = [`name: "${candidateName}" is already used by an earlier entry in this file`];
|
|
119896
|
+
}
|
|
119897
|
+
if (reasons.length > 0) {
|
|
119898
|
+
rejected.push({ index, name: candidateName, reasons });
|
|
119899
|
+
return;
|
|
119900
|
+
}
|
|
119901
|
+
const entry = parseTriggerEntry(candidate);
|
|
119902
|
+
if (entry === undefined) {
|
|
119903
|
+
rejected.push({ index, name: candidateName, reasons: ["entry: failed to parse after passing validation"] });
|
|
119904
|
+
return;
|
|
119905
|
+
}
|
|
119906
|
+
seenNames.add(entry.name);
|
|
119907
|
+
triggers.push(entry);
|
|
119908
|
+
});
|
|
119909
|
+
return { triggers, rejected, fileProblem: undefined };
|
|
119910
|
+
}
|
|
119911
|
+
|
|
119912
|
+
// src/trigger/record.ts
|
|
119913
|
+
init_fs();
|
|
119914
|
+
import { appendFile as appendFile10, mkdir as mkdir68, readFile as readFile104, stat as stat21 } from "fs/promises";
|
|
119915
|
+
import path210 from "path";
|
|
119916
|
+
var TRIGGER_RUN_RECORD_VERSION = 1;
|
|
119917
|
+
function triggerDataDir(projectRoot) {
|
|
119918
|
+
return path210.join(projectRoot, ".metaproject", "data", "trigger");
|
|
119919
|
+
}
|
|
119920
|
+
function triggerRunsPath(projectRoot) {
|
|
119921
|
+
return path210.join(triggerDataDir(projectRoot), "runs.jsonl");
|
|
119922
|
+
}
|
|
119923
|
+
var NO_MODEL_COST = {
|
|
119924
|
+
recorded: false,
|
|
119925
|
+
reason: "this action does not call a model \u2014 reconcile/rebuild are deterministic, no spend to record"
|
|
119926
|
+
};
|
|
119927
|
+
async function appendTriggerRunRecord(projectRoot, record) {
|
|
119928
|
+
const file = triggerRunsPath(projectRoot);
|
|
119929
|
+
const full = { v: TRIGGER_RUN_RECORD_VERSION, ...record };
|
|
119930
|
+
try {
|
|
119931
|
+
await mkdir68(path210.dirname(file), { recursive: true });
|
|
119932
|
+
await appendFile10(file, `${JSON.stringify(full)}
|
|
119933
|
+
`, "utf8");
|
|
119934
|
+
return { status: "appended", path: file, record: full };
|
|
119935
|
+
} catch (error) {
|
|
119936
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
119937
|
+
return {
|
|
119938
|
+
status: "failed",
|
|
119939
|
+
path: file,
|
|
119940
|
+
reason: `the trigger run record could not be appended (${message}). The run's own outcome above is still what ` + "happened \u2014 this failure is only about the record of it, and it must not be reported as recorded."
|
|
119941
|
+
};
|
|
119942
|
+
}
|
|
119943
|
+
}
|
|
119944
|
+
async function readTriggerRuns(projectRoot) {
|
|
119945
|
+
const file = triggerRunsPath(projectRoot);
|
|
119946
|
+
try {
|
|
119947
|
+
await stat21(file);
|
|
119948
|
+
} catch (error) {
|
|
119949
|
+
if (isNotFound(error))
|
|
119950
|
+
return { state: "absent", path: file };
|
|
119951
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
119952
|
+
return {
|
|
119953
|
+
state: "unreadable",
|
|
119954
|
+
path: file,
|
|
119955
|
+
reason: `the trigger run record could not be examined (${message}). This is not the same as "nothing has ever fired".`
|
|
119956
|
+
};
|
|
119957
|
+
}
|
|
119958
|
+
let content;
|
|
119959
|
+
try {
|
|
119960
|
+
content = await readFile104(file, "utf8");
|
|
119961
|
+
} catch (error) {
|
|
119962
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
119963
|
+
return {
|
|
119964
|
+
state: "unreadable",
|
|
119965
|
+
path: file,
|
|
119966
|
+
reason: `the trigger run record exists but could not be read (${message}). Its records are on disk, not absent.`
|
|
119967
|
+
};
|
|
119968
|
+
}
|
|
119969
|
+
const records = [];
|
|
119970
|
+
let damaged = 0;
|
|
119971
|
+
for (const line of content.split(`
|
|
119972
|
+
`)) {
|
|
119973
|
+
const trimmed = line.trim();
|
|
119974
|
+
if (trimmed.length === 0)
|
|
119975
|
+
continue;
|
|
119976
|
+
const parsed = parseRecord2(trimmed);
|
|
119977
|
+
if (parsed)
|
|
119978
|
+
records.push(parsed);
|
|
119979
|
+
else
|
|
119980
|
+
damaged += 1;
|
|
119981
|
+
}
|
|
119982
|
+
if (damaged > 0) {
|
|
119983
|
+
return {
|
|
119984
|
+
state: "unreadable",
|
|
119985
|
+
path: file,
|
|
119986
|
+
reason: `the trigger run record holds ${damaged} line${damaged === 1 ? "" : "s"} that could not be parsed. ` + "The readable remainder is not reported as the whole history."
|
|
119987
|
+
};
|
|
119988
|
+
}
|
|
119989
|
+
return { state: "present", path: file, records };
|
|
119990
|
+
}
|
|
119991
|
+
function latestRunByTrigger(records) {
|
|
119992
|
+
const latest = new Map;
|
|
119993
|
+
for (const record of records)
|
|
119994
|
+
latest.set(record.trigger, record);
|
|
119995
|
+
return latest;
|
|
119996
|
+
}
|
|
119997
|
+
function parseRecord2(line) {
|
|
119998
|
+
let value;
|
|
119999
|
+
try {
|
|
120000
|
+
value = JSON.parse(line);
|
|
120001
|
+
} catch {
|
|
120002
|
+
return null;
|
|
120003
|
+
}
|
|
120004
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
120005
|
+
return null;
|
|
120006
|
+
const raw = value;
|
|
120007
|
+
if (typeof raw["at"] !== "string" || typeof raw["trigger"] !== "string" || typeof raw["outcome"] !== "string") {
|
|
120008
|
+
return null;
|
|
120009
|
+
}
|
|
120010
|
+
return raw;
|
|
120011
|
+
}
|
|
120012
|
+
|
|
120013
|
+
// src/trigger/run.ts
|
|
120014
|
+
function resolveTriggerForRun(projectRoot, name) {
|
|
120015
|
+
const loaded = loadTriggersConfig(projectRoot);
|
|
120016
|
+
if (loaded.fileProblem === "absent") {
|
|
120017
|
+
return { kind: "config-absent" };
|
|
120018
|
+
}
|
|
120019
|
+
if (loaded.fileProblem !== undefined) {
|
|
120020
|
+
return { kind: "config-broken", fileProblem: loaded.fileProblem };
|
|
120021
|
+
}
|
|
120022
|
+
const entry = loaded.triggers.find((candidate) => candidate.name === name);
|
|
120023
|
+
if (entry !== undefined) {
|
|
120024
|
+
return entry.enabled ? { kind: "ready", entry } : { kind: "disabled", entry };
|
|
120025
|
+
}
|
|
120026
|
+
const rejected = loaded.rejected.find((candidate) => candidate.name === name);
|
|
120027
|
+
if (rejected !== undefined) {
|
|
120028
|
+
return { kind: "rejected", reasons: rejected.reasons };
|
|
120029
|
+
}
|
|
120030
|
+
return { kind: "unknown-name", known: loaded.triggers.map((candidate) => candidate.name) };
|
|
120031
|
+
}
|
|
120032
|
+
function triggerRunLockPath(projectRoot) {
|
|
120033
|
+
return path211.join(projectRoot, ".metaproject", "data", "trigger", ".run.lock");
|
|
120034
|
+
}
|
|
120035
|
+
var LOCK_TIMEOUT_PREFIX = "Timed out waiting for lock:";
|
|
120036
|
+
async function withTriggerRunLock(projectRoot, fn) {
|
|
120037
|
+
try {
|
|
120038
|
+
const result = await withFileLock(triggerRunLockPath(projectRoot), async () => {
|
|
120039
|
+
const holdMs = Number(process.env["KERYX_TRIGGER_RUN_HOLD_MS"] ?? "");
|
|
120040
|
+
if (Number.isFinite(holdMs) && holdMs > 0) {
|
|
120041
|
+
await new Promise((resolve) => setTimeout(resolve, holdMs));
|
|
120042
|
+
}
|
|
120043
|
+
return fn();
|
|
120044
|
+
}, { timeoutMs: 0 });
|
|
120045
|
+
return { acquired: true, result };
|
|
120046
|
+
} catch (error) {
|
|
120047
|
+
if (error instanceof Error && error.message.startsWith(LOCK_TIMEOUT_PREFIX)) {
|
|
120048
|
+
return {
|
|
120049
|
+
acquired: false,
|
|
120050
|
+
reason: "another `keryx trigger run` holds this project's trigger lock \u2014 refusing rather than waiting, " + "so this run stays exactly one pass (AC2) instead of blocking on another run's schedule. Retry on the next fire."
|
|
120051
|
+
};
|
|
120052
|
+
}
|
|
120053
|
+
throw error;
|
|
120054
|
+
}
|
|
120055
|
+
}
|
|
120056
|
+
async function recordedTriggerSpend(projectRoot) {
|
|
120057
|
+
const read = await readTriggerRuns(projectRoot);
|
|
120058
|
+
if (read.state === "absent")
|
|
120059
|
+
return { known: true, usd: 0 };
|
|
120060
|
+
if (read.state === "unreadable")
|
|
120061
|
+
return { known: false, reason: read.reason };
|
|
120062
|
+
const usd = read.records.reduce((sum, record) => sum + (record.cost.recorded ? record.cost.usd : 0), 0);
|
|
120063
|
+
return { known: true, usd };
|
|
120064
|
+
}
|
|
120065
|
+
async function evaluateTriggerBudget(projectRoot, actionKind, options = {}) {
|
|
120066
|
+
const spend = await recordedTriggerSpend(projectRoot);
|
|
120067
|
+
if (!spend.known) {
|
|
120068
|
+
const evaluation = evaluateSpendCap(undefined, options);
|
|
120069
|
+
return {
|
|
120070
|
+
allowed: false,
|
|
120071
|
+
evaluation,
|
|
120072
|
+
reason: `this project's fired-trigger run record could not be read (${spend.reason}) \u2014 spend cannot be verified ` + `against the ${evaluation.currency} ${evaluation.ceiling} spend ceiling, so refusing to start "${actionKind}" ` + "rather than proceed on an unknown ledger. Fix or restore `runs.jsonl` (see `keryx trigger status`), then retry."
|
|
120073
|
+
};
|
|
120074
|
+
}
|
|
120075
|
+
const evaluation = evaluateSpendCap(spend.usd, options);
|
|
120076
|
+
if (!evaluation.stop) {
|
|
120077
|
+
return { allowed: true, evaluation };
|
|
120078
|
+
}
|
|
120079
|
+
return {
|
|
120080
|
+
allowed: false,
|
|
120081
|
+
evaluation,
|
|
120082
|
+
reason: `this project has $${evaluation.spent} ${evaluation.currency} recorded against a $${evaluation.ceiling} ` + `${evaluation.currency} spend ceiling (over by $${evaluation.overBy}) \u2014 refusing to start "${actionKind}" ` + "rather than risk spending further. Raise the ceiling, or review `keryx trigger status`'s recorded cost, before retrying."
|
|
120083
|
+
};
|
|
120084
|
+
}
|
|
120085
|
+
|
|
120086
|
+
// src/lib/managed-hook.ts
|
|
120087
|
+
init_fs();
|
|
120088
|
+
init_git_hooks();
|
|
120089
|
+
import { chmod as chmod8, mkdir as mkdir69, readFile as readFile105, writeFile as writeFile58 } from "fs/promises";
|
|
120090
|
+
import path212 from "path";
|
|
120091
|
+
function escapeRegExp8(value) {
|
|
120092
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
120093
|
+
}
|
|
120094
|
+
function blockPatternFor(blockId) {
|
|
120095
|
+
const start = `# keryx:${blockId}:begin`;
|
|
120096
|
+
const end = `# keryx:${blockId}:end`;
|
|
120097
|
+
return new RegExp(`${escapeRegExp8(start)}[\\s\\S]*?${escapeRegExp8(end)}`);
|
|
120098
|
+
}
|
|
120099
|
+
async function installManagedHook3(projectRoot, hookName, blockId, content) {
|
|
120100
|
+
const hooksRoot = await resolveGitHooksRoot(projectRoot);
|
|
120101
|
+
if (!hooksRoot)
|
|
120102
|
+
return false;
|
|
120103
|
+
await mkdir69(hooksRoot, { recursive: true });
|
|
120104
|
+
const hookPath = path212.join(hooksRoot, hookName);
|
|
120105
|
+
const managedBlock = `# keryx:${blockId}:begin
|
|
120106
|
+
${content.trim()}
|
|
120107
|
+
# keryx:${blockId}:end`;
|
|
120108
|
+
const existing = await pathExists(hookPath) ? await readFile105(hookPath, "utf8") : `#!/usr/bin/env sh
|
|
120109
|
+
`;
|
|
120110
|
+
const pattern = blockPatternFor(blockId);
|
|
120111
|
+
const next = pattern.test(existing) ? existing.replace(pattern, () => managedBlock) : `${existing.trimEnd()}
|
|
120112
|
+
|
|
120113
|
+
${managedBlock}
|
|
120114
|
+
`;
|
|
120115
|
+
await writeFile58(hookPath, next, "utf8");
|
|
120116
|
+
await chmod8(hookPath, 493);
|
|
120117
|
+
return true;
|
|
120118
|
+
}
|
|
120119
|
+
async function removeManagedHook3(projectRoot, hookName, blockId) {
|
|
120120
|
+
const hooksRoot = await resolveGitHooksRoot(projectRoot);
|
|
120121
|
+
if (!hooksRoot)
|
|
120122
|
+
return false;
|
|
120123
|
+
const hookPath = path212.join(hooksRoot, hookName);
|
|
120124
|
+
if (!await pathExists(hookPath))
|
|
120125
|
+
return false;
|
|
120126
|
+
const existing = await readFile105(hookPath, "utf8");
|
|
120127
|
+
const start = `# keryx:${blockId}:begin`;
|
|
120128
|
+
const end = `# keryx:${blockId}:end`;
|
|
120129
|
+
const pattern = new RegExp(`\\n*${escapeRegExp8(start)}[\\s\\S]*?${escapeRegExp8(end)}\\n*`);
|
|
120130
|
+
if (!pattern.test(existing))
|
|
120131
|
+
return false;
|
|
120132
|
+
await writeFile58(hookPath, `${existing.replace(pattern, `
|
|
120133
|
+
`).trimEnd()}
|
|
120134
|
+
`, "utf8");
|
|
120135
|
+
await chmod8(hookPath, 493);
|
|
120136
|
+
return true;
|
|
120137
|
+
}
|
|
120138
|
+
async function hasManagedHook(projectRoot, hookName, blockId) {
|
|
120139
|
+
const hooksRoot = await resolveGitHooksRoot(projectRoot);
|
|
120140
|
+
if (!hooksRoot)
|
|
120141
|
+
return false;
|
|
120142
|
+
const hookPath = path212.join(hooksRoot, hookName);
|
|
120143
|
+
if (!await pathExists(hookPath))
|
|
120144
|
+
return false;
|
|
120145
|
+
const existing = await readFile105(hookPath, "utf8");
|
|
120146
|
+
return blockPatternFor(blockId).test(existing);
|
|
120147
|
+
}
|
|
120148
|
+
|
|
120149
|
+
// src/trigger/hooks.ts
|
|
120150
|
+
init_git_hooks();
|
|
120151
|
+
var HOOKABLE_EVENTS = ["post-merge", "post-commit", "post-checkout"];
|
|
120152
|
+
function isHookable(entry) {
|
|
120153
|
+
return entry.fire.kind === "event" && HOOKABLE_EVENTS.includes(entry.fire.event);
|
|
120154
|
+
}
|
|
120155
|
+
function blockId(name) {
|
|
120156
|
+
return `trigger-${name}`;
|
|
120157
|
+
}
|
|
120158
|
+
function hookBody2(name, hookName) {
|
|
120159
|
+
const fn = `keryx_trigger_${name.replace(/[^a-zA-Z0-9_]/g, "_")}_${hookName.replace(/-/g, "_")}`;
|
|
120160
|
+
const guard = hookName === "post-checkout" ? ' [ "${3:-1}" = "1" ] || return 0\n' : "";
|
|
120161
|
+
return `${fn}() {
|
|
120162
|
+
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || return 0
|
|
120163
|
+
${guard} command -v keryx >/dev/null 2>&1 || return 0
|
|
120164
|
+
# Fired trigger "${name}" \u2014 see .metaproject/triggers.json. Never fails the
|
|
120165
|
+
# git operation: the run's own outcome (including a failure) is recorded by
|
|
120166
|
+
# \`keryx trigger run\` itself (AC4), not surfaced as a hook error here.
|
|
120167
|
+
keryx trigger run ${name} >/dev/null 2>&1 || true
|
|
120168
|
+
return 0
|
|
120169
|
+
}
|
|
120170
|
+
${fn} "$@"`;
|
|
120171
|
+
}
|
|
120172
|
+
async function installTriggerHooks(projectRoot) {
|
|
120173
|
+
const { triggers } = loadTriggersConfig(projectRoot);
|
|
120174
|
+
const results = [];
|
|
120175
|
+
for (const entry of triggers) {
|
|
120176
|
+
if (!isHookable(entry))
|
|
120177
|
+
continue;
|
|
120178
|
+
const hookName = entry.fire.event;
|
|
120179
|
+
const wrote = await installManagedHook3(projectRoot, hookName, blockId(entry.name), hookBody2(entry.name, hookName));
|
|
120180
|
+
results.push({ name: entry.name, hook: hookName, wrote });
|
|
120181
|
+
}
|
|
120182
|
+
return results;
|
|
120183
|
+
}
|
|
120184
|
+
async function uninstallTriggerHooks(projectRoot) {
|
|
120185
|
+
const { triggers } = loadTriggersConfig(projectRoot);
|
|
120186
|
+
const results = [];
|
|
120187
|
+
for (const entry of triggers) {
|
|
120188
|
+
if (!isHookable(entry))
|
|
120189
|
+
continue;
|
|
120190
|
+
const hookName = entry.fire.event;
|
|
120191
|
+
const wrote = await removeManagedHook3(projectRoot, hookName, blockId(entry.name));
|
|
120192
|
+
results.push({ name: entry.name, hook: hookName, wrote });
|
|
120193
|
+
}
|
|
120194
|
+
return results;
|
|
120195
|
+
}
|
|
120196
|
+
async function isTriggerHookInstalled(projectRoot, entry) {
|
|
120197
|
+
if (!isHookable(entry))
|
|
120198
|
+
return false;
|
|
120199
|
+
return hasManagedHook(projectRoot, entry.fire.event, blockId(entry.name));
|
|
120200
|
+
}
|
|
120201
|
+
async function hasGitHooksRoot(projectRoot) {
|
|
120202
|
+
return await resolveGitHooksRoot(projectRoot) !== null;
|
|
120203
|
+
}
|
|
120204
|
+
|
|
120205
|
+
// src/trigger/schedule.ts
|
|
120206
|
+
import path213 from "path";
|
|
120207
|
+
function resolveKeryxInvocation() {
|
|
120208
|
+
const script = process.argv[1];
|
|
120209
|
+
return {
|
|
120210
|
+
execPath: process.execPath,
|
|
120211
|
+
scriptPath: script ? path213.resolve(script) : process.execPath
|
|
120212
|
+
};
|
|
120213
|
+
}
|
|
120214
|
+
function resolveScheduleEntry(projectRoot, name) {
|
|
120215
|
+
const { triggers, fileProblem } = loadTriggersConfig(projectRoot);
|
|
120216
|
+
if (fileProblem !== undefined)
|
|
120217
|
+
return { kind: "config-absent" };
|
|
120218
|
+
const entry = triggers.find((candidate) => candidate.name === name);
|
|
120219
|
+
if (entry === undefined)
|
|
120220
|
+
return { kind: "unknown-name", known: triggers.map((t) => t.name) };
|
|
120221
|
+
if (entry.fire.kind !== "schedule")
|
|
120222
|
+
return { kind: "not-a-schedule", entry };
|
|
120223
|
+
return { kind: "ready", entry };
|
|
120224
|
+
}
|
|
120225
|
+
function shQuote2(value) {
|
|
120226
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
120227
|
+
}
|
|
120228
|
+
function systemdQuote(value) {
|
|
120229
|
+
if (!/[\s"\\]/.test(value))
|
|
120230
|
+
return value;
|
|
120231
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
120232
|
+
}
|
|
120233
|
+
function systemdEscapePercent(value) {
|
|
120234
|
+
return value.replace(/%/g, "%%");
|
|
120235
|
+
}
|
|
120236
|
+
function systemdValue(value) {
|
|
120237
|
+
return systemdQuote(systemdEscapePercent(value));
|
|
120238
|
+
}
|
|
120239
|
+
function cronEscapePercent(value) {
|
|
120240
|
+
return value.replace(/%/g, "\\%");
|
|
120241
|
+
}
|
|
120242
|
+
function renderScheduleLines(params) {
|
|
120243
|
+
const { projectRoot, name, cron, invocation } = params;
|
|
120244
|
+
const interpreterDir = path213.dirname(invocation.execPath);
|
|
120245
|
+
const assumedPath = [interpreterDir, "/usr/local/bin", "/usr/bin", "/bin"].join(":");
|
|
120246
|
+
const logDir = path213.join(projectRoot, ".metaproject", "data", "trigger");
|
|
120247
|
+
const logPath = path213.join(logDir, `${name}.schedule.log`);
|
|
120248
|
+
const cronCommand = `cd ${shQuote2(projectRoot)} && mkdir -p ${shQuote2(logDir)} && PATH=${shQuote2(assumedPath)} ` + `${shQuote2(invocation.execPath)} ${shQuote2(invocation.scriptPath)} trigger run ${shQuote2(name)} ` + `>> ${shQuote2(logPath)} 2>&1`;
|
|
120249
|
+
const cronLine = `${cron} ${cronEscapePercent(cronCommand)}`;
|
|
120250
|
+
const serviceUnitName = `keryx-trigger-${name}.service`;
|
|
120251
|
+
const timerUnitName = `keryx-trigger-${name}.timer`;
|
|
120252
|
+
const descriptionName = systemdEscapePercent(name);
|
|
120253
|
+
const descriptionProjectRoot = systemdEscapePercent(projectRoot);
|
|
120254
|
+
const systemdService = `# ${serviceUnitName} \u2014 install under /etc/systemd/system/ (or ~/.config/systemd/user/ for --user)
|
|
120255
|
+
[Unit]
|
|
120256
|
+
Description=keryx trigger "${descriptionName}" (${descriptionProjectRoot})
|
|
120257
|
+
|
|
120258
|
+
[Service]
|
|
120259
|
+
Type=oneshot
|
|
120260
|
+
WorkingDirectory=${systemdEscapePercent(projectRoot)}
|
|
120261
|
+
Environment=${systemdQuote(systemdEscapePercent(`PATH=${assumedPath}`))}
|
|
120262
|
+
# Same reason as the cron line's own \`mkdir -p\`: StandardOutput=append: does
|
|
120263
|
+
# not create a missing PARENT directory, only a missing file. The leading "-"
|
|
120264
|
+
# means systemd ignores this step's own exit status.
|
|
120265
|
+
ExecStartPre=-/bin/mkdir -p ${systemdValue(logDir)}
|
|
120266
|
+
ExecStart=${systemdValue(invocation.execPath)} ${systemdValue(invocation.scriptPath)} trigger run ${systemdValue(name)}
|
|
120267
|
+
StandardOutput=append:${systemdEscapePercent(logPath)}
|
|
120268
|
+
StandardError=append:${systemdEscapePercent(logPath)}
|
|
120269
|
+
`;
|
|
120270
|
+
const systemdTimer = `# ${timerUnitName} \u2014 install alongside ${serviceUnitName}
|
|
120271
|
+
[Unit]
|
|
120272
|
+
Description=Schedule for keryx trigger "${descriptionName}"
|
|
120273
|
+
|
|
120274
|
+
[Timer]
|
|
120275
|
+
# Translate the cron expression "${cron}" into OnCalendar= syntax, e.g. with
|
|
120276
|
+
# \`systemd-analyze calendar '<expression>'\` to verify it, then uncomment:
|
|
120277
|
+
# OnCalendar=
|
|
120278
|
+
Persistent=true
|
|
120279
|
+
|
|
120280
|
+
[Install]
|
|
120281
|
+
WantedBy=timers.target
|
|
120282
|
+
`;
|
|
120283
|
+
return { assumedPath, cronCommand, cronLine, systemdService, systemdTimer, serviceUnitName, timerUnitName, logPath };
|
|
120284
|
+
}
|
|
120285
|
+
|
|
120286
|
+
// src/commands/trigger.ts
|
|
120287
|
+
var flowService2 = null;
|
|
120288
|
+
function getFlowService2() {
|
|
120289
|
+
flowService2 ??= createFlowService(flowServiceDeps());
|
|
120290
|
+
return flowService2;
|
|
120291
|
+
}
|
|
120292
|
+
async function triggerCommand(args) {
|
|
120293
|
+
const sub = args[0];
|
|
120294
|
+
if (!sub || sub === "--help" || sub === "-h") {
|
|
120295
|
+
printHelp26();
|
|
120296
|
+
return;
|
|
120297
|
+
}
|
|
120298
|
+
switch (sub) {
|
|
120299
|
+
case "run":
|
|
120300
|
+
return runSubcommand(args);
|
|
120301
|
+
case "install":
|
|
120302
|
+
return installSubcommand();
|
|
120303
|
+
case "uninstall":
|
|
120304
|
+
return uninstallSubcommand();
|
|
120305
|
+
case "list":
|
|
120306
|
+
return listSubcommand();
|
|
120307
|
+
case "status":
|
|
120308
|
+
return statusSubcommand(args);
|
|
120309
|
+
case "schedule":
|
|
120310
|
+
return scheduleSubcommand(args);
|
|
120311
|
+
default:
|
|
120312
|
+
console.error(`Unknown trigger command: ${sub}. See \`keryx trigger --help\`.`);
|
|
120313
|
+
printHelp26();
|
|
120314
|
+
process.exitCode = 1;
|
|
120315
|
+
return;
|
|
120316
|
+
}
|
|
120317
|
+
}
|
|
120318
|
+
async function runSubcommand(args) {
|
|
120319
|
+
const name = args[1];
|
|
120320
|
+
if (args[1] === "--help" || args[1] === "-h") {
|
|
120321
|
+
printHelp26();
|
|
120322
|
+
return;
|
|
120323
|
+
}
|
|
120324
|
+
if (!name) {
|
|
120325
|
+
console.error("Usage: keryx trigger run <name>");
|
|
120326
|
+
process.exitCode = 1;
|
|
120327
|
+
return;
|
|
120328
|
+
}
|
|
120329
|
+
await runTrigger(process.cwd(), name);
|
|
120330
|
+
}
|
|
120331
|
+
async function runTrigger(projectRoot, name) {
|
|
120332
|
+
const resolution = resolveTriggerForRun(projectRoot, name);
|
|
120333
|
+
switch (resolution.kind) {
|
|
120334
|
+
case "config-absent": {
|
|
120335
|
+
console.log(`keryx trigger run ${name}: no trigger config declared for this project ` + `(${triggersConfigPath(projectRoot)} not found) \u2014 nothing to do.`);
|
|
120336
|
+
return;
|
|
120337
|
+
}
|
|
120338
|
+
case "config-broken": {
|
|
120339
|
+
console.error(`keryx trigger run ${name}: ${triggersConfigPath(projectRoot)} could not be read as a trigger ` + `config (${resolution.fileProblem}) \u2014 fix the file and retry.`);
|
|
120340
|
+
process.exitCode = 1;
|
|
120341
|
+
return;
|
|
120342
|
+
}
|
|
120343
|
+
case "unknown-name": {
|
|
120344
|
+
console.error(`keryx trigger run: unknown trigger "${name}". ` + (resolution.known.length > 0 ? `Known: ${resolution.known.join(", ")}` : "This project declares no valid triggers."));
|
|
120345
|
+
process.exitCode = 1;
|
|
120346
|
+
return;
|
|
120347
|
+
}
|
|
120348
|
+
case "rejected": {
|
|
120349
|
+
console.error(`keryx trigger run: "${name}" is declared but malformed, so it cannot run:`);
|
|
120350
|
+
for (const reason of resolution.reasons)
|
|
120351
|
+
console.error(` - ${reason}`);
|
|
120352
|
+
process.exitCode = 1;
|
|
120353
|
+
return;
|
|
120354
|
+
}
|
|
120355
|
+
case "disabled": {
|
|
120356
|
+
console.log(`keryx trigger run ${name}: disabled \u2014 nothing to do.`);
|
|
120357
|
+
await recordRun(projectRoot, resolution.entry, {
|
|
120358
|
+
outcome: "no-op",
|
|
120359
|
+
detail: "disabled \u2014 nothing to do.",
|
|
120360
|
+
cost: { recorded: false, reason: "trigger is disabled \u2014 its action never ran" }
|
|
120361
|
+
});
|
|
120362
|
+
return;
|
|
120363
|
+
}
|
|
120364
|
+
case "ready": {
|
|
120365
|
+
await runReadyTrigger(projectRoot, name, resolution);
|
|
120366
|
+
return;
|
|
120367
|
+
}
|
|
120368
|
+
}
|
|
120369
|
+
}
|
|
120370
|
+
async function runReadyTrigger(projectRoot, name, resolution) {
|
|
120371
|
+
const { entry } = resolution;
|
|
120372
|
+
const { action } = entry;
|
|
120373
|
+
if (action.kind === "open-flow") {
|
|
120374
|
+
await runOpenFlow(projectRoot, name, entry, action);
|
|
120375
|
+
return;
|
|
120376
|
+
}
|
|
120377
|
+
if (action.kind === "flow-next") {
|
|
120378
|
+
await runFlowNext(projectRoot, name, entry, action);
|
|
120379
|
+
return;
|
|
120380
|
+
}
|
|
120381
|
+
const outcome = await withTriggerRunLock(projectRoot, async () => {
|
|
120382
|
+
const before = process.exitCode;
|
|
120383
|
+
process.exitCode = 0;
|
|
120384
|
+
if (action.kind === "reconcile") {
|
|
120385
|
+
await syncCommand(["--apply"]);
|
|
120386
|
+
} else {
|
|
120387
|
+
await gdgraphCommand(["build"]);
|
|
120388
|
+
}
|
|
120389
|
+
const failed = (process.exitCode ?? 0) !== 0;
|
|
120390
|
+
process.exitCode = before;
|
|
120391
|
+
return { failed };
|
|
120392
|
+
});
|
|
120393
|
+
if (!outcome.acquired) {
|
|
120394
|
+
console.log(`keryx trigger run ${name}: ${outcome.reason}`);
|
|
120395
|
+
await recordRun(projectRoot, entry, {
|
|
120396
|
+
outcome: "lock-refused",
|
|
120397
|
+
detail: outcome.reason,
|
|
120398
|
+
cost: { recorded: false, reason: "run was refused before the action could start" }
|
|
120399
|
+
});
|
|
120400
|
+
return;
|
|
120401
|
+
}
|
|
120402
|
+
if (outcome.result.failed) {
|
|
120403
|
+
console.error(`keryx trigger run ${name}: action "${action.kind}" failed (see output above).`);
|
|
120404
|
+
process.exitCode = 1;
|
|
120405
|
+
await recordRun(projectRoot, entry, {
|
|
120406
|
+
outcome: "failed",
|
|
120407
|
+
detail: `action "${action.kind}" failed (see command output above for detail).`,
|
|
120408
|
+
cost: NO_MODEL_COST
|
|
120409
|
+
});
|
|
120410
|
+
return;
|
|
120411
|
+
}
|
|
120412
|
+
console.log(`keryx trigger run ${name}: ok \u2014 "${action.kind}" completed.`);
|
|
120413
|
+
await recordRun(projectRoot, entry, {
|
|
120414
|
+
outcome: "ok",
|
|
120415
|
+
detail: `action "${action.kind}" completed.`,
|
|
120416
|
+
cost: NO_MODEL_COST
|
|
120417
|
+
});
|
|
120418
|
+
}
|
|
120419
|
+
function findEquivalentOpenFlow(flows, template) {
|
|
120420
|
+
return flows.find((flow) => flow.title === template && flow.status !== "done");
|
|
120421
|
+
}
|
|
120422
|
+
function noModelCost(reason) {
|
|
120423
|
+
return { recorded: false, reason };
|
|
120424
|
+
}
|
|
120425
|
+
var OPEN_FLOW_NO_MODEL_COST = noModelCost('action "open-flow" does not call a model \u2014 opening a flow is deterministic bookkeeping (a new flow.json plus scaffold ' + "files), no spend to record.");
|
|
120426
|
+
var FLOW_NEXT_NO_MODEL_COST = noModelCost('action "flow-next" does not call a model in this build \u2014 it only reports the next task into this record; it never ' + "dispatches an agent turn, so there is nothing to record a cost for.");
|
|
120427
|
+
async function runOpenFlow(projectRoot, name, entry, action) {
|
|
120428
|
+
const budget = await evaluateTriggerBudget(projectRoot, "open-flow");
|
|
120429
|
+
if (!budget.allowed) {
|
|
120430
|
+
await refuseOnBudget(projectRoot, name, entry, budget.reason);
|
|
120431
|
+
return;
|
|
120432
|
+
}
|
|
120433
|
+
const service = getFlowService2();
|
|
120434
|
+
let outcome;
|
|
120435
|
+
try {
|
|
120436
|
+
outcome = await withTriggerRunLock(projectRoot, async () => {
|
|
120437
|
+
if (action.skipIfOpen) {
|
|
120438
|
+
const flows = await service.list({ cwd: projectRoot });
|
|
120439
|
+
const equivalent = findEquivalentOpenFlow(flows, action.template);
|
|
120440
|
+
if (equivalent) {
|
|
120441
|
+
return { kind: "skipped", equivalent };
|
|
120442
|
+
}
|
|
120443
|
+
}
|
|
120444
|
+
const result = await service.init({ cwd: projectRoot, title: action.template });
|
|
120445
|
+
return { kind: "opened", flowId: result.flow.id, dir: result.dir };
|
|
120446
|
+
});
|
|
120447
|
+
} catch (error) {
|
|
120448
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
120449
|
+
console.error(`keryx trigger run ${name}: action "open-flow" failed: ${message}`);
|
|
120450
|
+
process.exitCode = 1;
|
|
120451
|
+
await recordRun(projectRoot, entry, {
|
|
120452
|
+
outcome: "failed",
|
|
120453
|
+
detail: `action "open-flow" failed: ${message}`,
|
|
120454
|
+
cost: OPEN_FLOW_NO_MODEL_COST
|
|
120455
|
+
});
|
|
120456
|
+
return;
|
|
120457
|
+
}
|
|
120458
|
+
if (!outcome.acquired) {
|
|
120459
|
+
console.log(`keryx trigger run ${name}: ${outcome.reason}`);
|
|
120460
|
+
await recordRun(projectRoot, entry, {
|
|
120461
|
+
outcome: "lock-refused",
|
|
120462
|
+
detail: outcome.reason,
|
|
120463
|
+
cost: { recorded: false, reason: "run was refused before the action could start" }
|
|
120464
|
+
});
|
|
120465
|
+
return;
|
|
120466
|
+
}
|
|
120467
|
+
if (outcome.result.kind === "skipped") {
|
|
120468
|
+
const { equivalent } = outcome.result;
|
|
120469
|
+
const detail = `flow ${equivalent.id} ("${equivalent.title}", status: ${equivalent.status}) is already open for ` + `template "${action.template}" and \`skipIfOpen\` is set \u2014 not opening a second one.`;
|
|
120470
|
+
console.log(`keryx trigger run ${name}: ${detail}`);
|
|
120471
|
+
await recordRun(projectRoot, entry, { outcome: "no-op", detail, cost: OPEN_FLOW_NO_MODEL_COST });
|
|
120472
|
+
return;
|
|
120473
|
+
}
|
|
120474
|
+
const { flowId, dir } = outcome.result;
|
|
120475
|
+
const detail = `opened flow ${flowId} ("${action.template}") at ${dir}.`;
|
|
120476
|
+
console.log(`keryx trigger run ${name}: ok \u2014 ${detail}`);
|
|
120477
|
+
await recordRun(projectRoot, entry, { outcome: "ok", detail, cost: OPEN_FLOW_NO_MODEL_COST });
|
|
120478
|
+
}
|
|
120479
|
+
async function runFlowNext(projectRoot, name, entry, action) {
|
|
120480
|
+
const budget = await evaluateTriggerBudget(projectRoot, "flow-next");
|
|
120481
|
+
if (!budget.allowed) {
|
|
120482
|
+
await refuseOnBudget(projectRoot, name, entry, budget.reason);
|
|
120483
|
+
return;
|
|
120484
|
+
}
|
|
120485
|
+
const service = getFlowService2();
|
|
120486
|
+
try {
|
|
120487
|
+
const decision = await service.next({ cwd: projectRoot, id: action.flow });
|
|
120488
|
+
const detail = describeNextTaskDecision(action.flow, decision);
|
|
120489
|
+
console.log(`keryx trigger run ${name}: ok \u2014 ${detail}`);
|
|
120490
|
+
await recordRun(projectRoot, entry, { outcome: "ok", detail, cost: FLOW_NEXT_NO_MODEL_COST });
|
|
120491
|
+
} catch (error) {
|
|
120492
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
120493
|
+
console.error(`keryx trigger run ${name}: action "flow-next" failed: ${message}`);
|
|
120494
|
+
process.exitCode = 1;
|
|
120495
|
+
await recordRun(projectRoot, entry, {
|
|
120496
|
+
outcome: "failed",
|
|
120497
|
+
detail: `action "flow-next" failed: ${message}`,
|
|
120498
|
+
cost: FLOW_NEXT_NO_MODEL_COST
|
|
120499
|
+
});
|
|
120500
|
+
}
|
|
120501
|
+
}
|
|
120502
|
+
function describeNextTaskDecision(flowId, decision) {
|
|
120503
|
+
const unresolvedNote = decision.unresolved.length > 0 ? ` (${decision.unresolved.length} other not-done task(s) carry an open, unresolved attempt.)` : "";
|
|
120504
|
+
if (decision.kind === "ready") {
|
|
120505
|
+
return `flow ${flowId}'s next task is ${decision.task.id} ("${decision.task.title}"), resume: ${decision.resume.kind}.` + unresolvedNote;
|
|
120506
|
+
}
|
|
120507
|
+
if (decision.kind === "blocked") {
|
|
120508
|
+
const waiting = decision.blocked.map((entry) => `${entry.task.id} (waiting on ${entry.waitingOn.join(", ")})`).join("; ");
|
|
120509
|
+
return `flow ${flowId} has work remaining but nothing startable \u2014 blocked: ${waiting}.${unresolvedNote}`;
|
|
120510
|
+
}
|
|
120511
|
+
return `flow ${flowId} has no undone task \u2014 nothing to report.${unresolvedNote}`;
|
|
120512
|
+
}
|
|
120513
|
+
async function refuseOnBudget(projectRoot, name, entry, reason) {
|
|
120514
|
+
console.log(`keryx trigger run ${name}: ${reason}`);
|
|
120515
|
+
await recordRun(projectRoot, entry, {
|
|
120516
|
+
outcome: "budget-refused",
|
|
120517
|
+
detail: reason,
|
|
120518
|
+
cost: { recorded: false, reason: "run was refused on the spend ceiling before the action could start" }
|
|
120519
|
+
});
|
|
120520
|
+
}
|
|
120521
|
+
async function recordRun(projectRoot, entry, outcome) {
|
|
120522
|
+
const append = await appendTriggerRunRecord(projectRoot, {
|
|
120523
|
+
at: new Date().toISOString(),
|
|
120524
|
+
trigger: entry.name,
|
|
120525
|
+
firedBy: entry.fire,
|
|
120526
|
+
action: entry.action,
|
|
120527
|
+
outcome: outcome.outcome,
|
|
120528
|
+
detail: outcome.detail,
|
|
120529
|
+
cost: outcome.cost
|
|
120530
|
+
});
|
|
120531
|
+
if (append.status === "failed") {
|
|
120532
|
+
console.error(`keryx trigger run ${entry.name}: ! ${append.reason}`);
|
|
120533
|
+
}
|
|
120534
|
+
}
|
|
120535
|
+
async function installSubcommand() {
|
|
120536
|
+
const cwd = process.cwd();
|
|
120537
|
+
if (!await hasGitHooksRoot(cwd)) {
|
|
120538
|
+
console.log("No .git directory \u2014 nothing installed.");
|
|
120539
|
+
return;
|
|
120540
|
+
}
|
|
120541
|
+
const results = await installTriggerHooks(cwd);
|
|
120542
|
+
if (results.length === 0) {
|
|
120543
|
+
console.log("keryx trigger install: no event-fired trigger declared " + `(${triggersConfigPath(cwd)}) \u2014 nothing to install. Schedule-fired and \`ci\`-fired entries install no hook; ` + "see `keryx trigger schedule <name>` for a schedule entry.");
|
|
120544
|
+
return;
|
|
120545
|
+
}
|
|
120546
|
+
console.log(`keryx trigger install: installed ${results.length} hook block(s), extending the existing hook file(s):`);
|
|
120547
|
+
for (const result of results) {
|
|
120548
|
+
console.log(` - ${result.name} -> ${result.hook} (${result.wrote ? "written" : "NOT written"})`);
|
|
120549
|
+
}
|
|
120550
|
+
console.log("\nOther managed blocks already in these hook files (e.g. `keryx sync install-hooks`'s `keryx-sync` block, " + "or a `keryx update`-installed `*-post-commit` block) are untouched and still run.");
|
|
120551
|
+
}
|
|
120552
|
+
async function uninstallSubcommand() {
|
|
120553
|
+
const cwd = process.cwd();
|
|
120554
|
+
const results = await uninstallTriggerHooks(cwd);
|
|
120555
|
+
const removed = results.filter((r) => r.wrote);
|
|
120556
|
+
console.log(removed.length > 0 ? `keryx trigger uninstall: removed ${removed.length} hook block(s): ${removed.map((r) => `${r.name} (${r.hook})`).join(", ")}` : "keryx trigger uninstall: no installed trigger hook block found to remove.");
|
|
120557
|
+
}
|
|
120558
|
+
async function listSubcommand() {
|
|
120559
|
+
const cwd = process.cwd();
|
|
120560
|
+
const { triggers, rejected, fileProblem } = loadTriggersConfig(cwd);
|
|
120561
|
+
if (fileProblem === "absent") {
|
|
120562
|
+
console.log(`keryx trigger list: no trigger config declared (${triggersConfigPath(cwd)} not found).`);
|
|
120563
|
+
return;
|
|
120564
|
+
}
|
|
120565
|
+
if (fileProblem !== undefined) {
|
|
120566
|
+
console.error(`keryx trigger list: ${triggersConfigPath(cwd)} could not be read as a trigger config (${fileProblem}).`);
|
|
120567
|
+
process.exitCode = 1;
|
|
120568
|
+
return;
|
|
120569
|
+
}
|
|
120570
|
+
if (triggers.length === 0 && rejected.length === 0) {
|
|
120571
|
+
console.log("keryx trigger list: the trigger config declares no entries.");
|
|
120572
|
+
return;
|
|
120573
|
+
}
|
|
120574
|
+
console.log(`keryx trigger list (${triggersConfigPath(cwd)}):`);
|
|
120575
|
+
for (const entry of triggers) {
|
|
120576
|
+
const hookNote = await describeHookInstalled(cwd, entry);
|
|
120577
|
+
console.log(` - ${entry.name} [${entry.enabled ? "enabled" : "disabled"}] ${describeFire(entry.fire)} -> ${describeAction(entry.action)} hook: ${hookNote}`);
|
|
120578
|
+
}
|
|
120579
|
+
for (const bad of rejected) {
|
|
120580
|
+
console.log(` - ${bad.name ?? `(entry #${bad.index})`} [REJECTED] ${bad.reasons.join("; ")}`);
|
|
120581
|
+
}
|
|
120582
|
+
}
|
|
120583
|
+
async function describeHookInstalled(cwd, entry) {
|
|
120584
|
+
if (entry.fire.kind === "schedule") {
|
|
120585
|
+
return `n/a (schedule \u2014 see \`keryx trigger schedule ${entry.name}\`)`;
|
|
120586
|
+
}
|
|
120587
|
+
if (entry.fire.event === "ci") {
|
|
120588
|
+
return "n/a (ci \u2014 fired by a CI job's own `keryx trigger run` call)";
|
|
120589
|
+
}
|
|
120590
|
+
return await isTriggerHookInstalled(cwd, entry) ? `installed (${entry.fire.event})` : `NOT installed (${entry.fire.event})`;
|
|
120591
|
+
}
|
|
120592
|
+
async function statusSubcommand(args) {
|
|
120593
|
+
const filterName = args[1];
|
|
120594
|
+
const cwd = process.cwd();
|
|
120595
|
+
const { triggers, fileProblem } = loadTriggersConfig(cwd);
|
|
120596
|
+
if (fileProblem === "absent") {
|
|
120597
|
+
console.log(`keryx trigger status: no trigger config declared (${triggersConfigPath(cwd)} not found).`);
|
|
120598
|
+
return;
|
|
120599
|
+
}
|
|
120600
|
+
if (fileProblem !== undefined) {
|
|
120601
|
+
console.error(`keryx trigger status: ${triggersConfigPath(cwd)} could not be read as a trigger config (${fileProblem}).`);
|
|
120602
|
+
process.exitCode = 1;
|
|
120603
|
+
return;
|
|
120604
|
+
}
|
|
120605
|
+
const runsRead = await readTriggerRuns(cwd);
|
|
120606
|
+
if (runsRead.state === "unreadable") {
|
|
120607
|
+
console.error(`keryx trigger status: ${runsRead.reason}`);
|
|
120608
|
+
process.exitCode = 1;
|
|
120609
|
+
return;
|
|
120610
|
+
}
|
|
120611
|
+
const latest = latestRunByTrigger(runsRead.state === "present" ? runsRead.records : []);
|
|
120612
|
+
const entries = filterName ? triggers.filter((entry) => entry.name === filterName) : triggers;
|
|
120613
|
+
if (filterName && entries.length === 0) {
|
|
120614
|
+
console.error(`keryx trigger status: unknown trigger "${filterName}". Known: ${triggers.map((t) => t.name).join(", ") || "(none)"}`);
|
|
120615
|
+
process.exitCode = 1;
|
|
120616
|
+
return;
|
|
120617
|
+
}
|
|
120618
|
+
if (entries.length === 0) {
|
|
120619
|
+
console.log("keryx trigger status: the trigger config declares no entries.");
|
|
120620
|
+
return;
|
|
120621
|
+
}
|
|
120622
|
+
console.log(`keryx trigger status (reading ${runsRead.state === "present" ? runsRead.path : "no run record yet"}):`);
|
|
120623
|
+
for (const entry of entries) {
|
|
120624
|
+
console.log(` - ${entry.name} [${entry.enabled ? "enabled" : "disabled"}] ${describeFire(entry.fire)} -> ${describeAction(entry.action)}`);
|
|
120625
|
+
const record = latest.get(entry.name);
|
|
120626
|
+
console.log(` ${record ? describeRecord(record) : "never fired \u2014 no record yet"}`);
|
|
120627
|
+
}
|
|
120628
|
+
}
|
|
120629
|
+
function describeRecord(record) {
|
|
120630
|
+
const cost = record.cost.recorded ? `cost: $${record.cost.usd.toFixed(4)}` : `cost: n/a (${record.cost.reason})`;
|
|
120631
|
+
return `last: ${record.at} \u2014 ${record.outcome} \u2014 ${record.detail} [${cost}]`;
|
|
120632
|
+
}
|
|
120633
|
+
function describeFire(fire) {
|
|
120634
|
+
return fire.kind === "event" ? `event:${fire.event}` : `schedule:"${fire.cron}"`;
|
|
120635
|
+
}
|
|
120636
|
+
function describeAction(action) {
|
|
120637
|
+
if (action.kind === "open-flow")
|
|
120638
|
+
return `open-flow(${action.template}${action.skipIfOpen ? ", skipIfOpen" : ""})`;
|
|
120639
|
+
if (action.kind === "flow-next")
|
|
120640
|
+
return `flow-next(${action.flow})`;
|
|
120641
|
+
return action.kind;
|
|
120642
|
+
}
|
|
120643
|
+
async function scheduleSubcommand(args) {
|
|
120644
|
+
const name = args[1];
|
|
120645
|
+
if (args[1] === "--help" || args[1] === "-h") {
|
|
120646
|
+
console.log("Usage: keryx trigger schedule <name>");
|
|
120647
|
+
return;
|
|
120648
|
+
}
|
|
120649
|
+
if (!name) {
|
|
120650
|
+
console.error("Usage: keryx trigger schedule <name>");
|
|
120651
|
+
process.exitCode = 1;
|
|
120652
|
+
return;
|
|
120653
|
+
}
|
|
120654
|
+
const cwd = process.cwd();
|
|
120655
|
+
const resolution = resolveScheduleEntry(cwd, name);
|
|
120656
|
+
switch (resolution.kind) {
|
|
120657
|
+
case "config-absent": {
|
|
120658
|
+
console.error(`keryx trigger schedule: no trigger config declared (${triggersConfigPath(cwd)} not found).`);
|
|
120659
|
+
process.exitCode = 1;
|
|
120660
|
+
return;
|
|
120661
|
+
}
|
|
120662
|
+
case "unknown-name": {
|
|
120663
|
+
console.error(`keryx trigger schedule: unknown trigger "${name}". ` + (resolution.known.length > 0 ? `Known: ${resolution.known.join(", ")}` : "This project declares no valid triggers."));
|
|
120664
|
+
process.exitCode = 1;
|
|
120665
|
+
return;
|
|
120666
|
+
}
|
|
120667
|
+
case "not-a-schedule": {
|
|
120668
|
+
console.error(`keryx trigger schedule: "${name}" is ${describeFire(resolution.entry.fire)}, not a schedule entry \u2014 ` + "there is nothing to print. Event-fired entries are installed as git hooks with `keryx trigger install`.");
|
|
120669
|
+
process.exitCode = 1;
|
|
120670
|
+
return;
|
|
120671
|
+
}
|
|
120672
|
+
case "ready": {
|
|
120673
|
+
const lines = renderScheduleLines({
|
|
120674
|
+
projectRoot: cwd,
|
|
120675
|
+
name,
|
|
120676
|
+
cron: resolution.entry.fire.cron,
|
|
120677
|
+
invocation: resolveKeryxInvocation()
|
|
120678
|
+
});
|
|
120679
|
+
console.log(`keryx trigger schedule ${name}: keryx runs no daemon for this \u2014 install ONE of the two below with your own scheduler.
|
|
120680
|
+
`);
|
|
120681
|
+
console.log(`# --- cron (crontab -e) --------------------------------------------------`);
|
|
120682
|
+
console.log(lines.cronLine);
|
|
120683
|
+
console.log(`# cron-command-only: ${lines.cronCommand}`);
|
|
120684
|
+
console.log(`
|
|
120685
|
+
# --- systemd (an alternative to cron) -------------------------------------`);
|
|
120686
|
+
console.log(lines.systemdService);
|
|
120687
|
+
console.log(lines.systemdTimer);
|
|
120688
|
+
console.log(`# Assumed PATH baked into both: ${lines.assumedPath}`);
|
|
120689
|
+
console.log(`# Output is appended to: ${lines.logPath}`);
|
|
120690
|
+
return;
|
|
120691
|
+
}
|
|
120692
|
+
}
|
|
120693
|
+
}
|
|
120694
|
+
function printHelp26() {
|
|
120695
|
+
console.log(`keryx trigger \u2014 fire, install and inspect this project's declared triggers
|
|
120696
|
+
|
|
120697
|
+
Usage:
|
|
120698
|
+
keryx trigger run <name> Perform exactly one pass of <name>'s action
|
|
120699
|
+
keryx trigger install Install a git hook block for every event-fired entry
|
|
120700
|
+
keryx trigger uninstall Remove those hook blocks (other managed blocks are untouched)
|
|
120701
|
+
keryx trigger list List declared entries: enabled state, fire, action, hook status
|
|
120702
|
+
keryx trigger status [<name>] Show the last recorded outcome for one or every entry (AC4)
|
|
120703
|
+
keryx trigger schedule <name> Print the cron line / systemd timer unit for a schedule entry
|
|
120704
|
+
|
|
120705
|
+
Triggers are declared by hand in .metaproject/triggers.json (keryx never
|
|
120706
|
+
writes it) and loaded by name. \`run\`:
|
|
120707
|
+
- exits 0 and says "nothing to do" when there is no trigger config, or the
|
|
120708
|
+
named trigger is disabled;
|
|
120709
|
+
- exits non-zero when the name is unknown, the matching entry is malformed,
|
|
120710
|
+
or the action itself fails;
|
|
120711
|
+
- refuses cleanly (exit 0) when another \`trigger run\` already holds this
|
|
120712
|
+
project's trigger lock ("reconcile"/"rebuild" only), or when the
|
|
120713
|
+
project's recorded trigger spend is at or over its spend ceiling
|
|
120714
|
+
("open-flow"/"flow-next" only \u2014 see \`keryx trigger status\` for the
|
|
120715
|
+
recorded cost).
|
|
120716
|
+
|
|
120717
|
+
Four actions run today: "reconcile" (-> \`keryx sync --apply\`), "rebuild"
|
|
120718
|
+
(-> \`keryx gdgraph build\`), "open-flow" (opens a flow from
|
|
120719
|
+
\`action.template\`, skipping a second one while an equivalent flow is
|
|
120720
|
+
already open when \`action.skipIfOpen\` is set), and "flow-next" (reports
|
|
120721
|
+
\`action.flow\`'s next task into the record \u2014 it does not dispatch an agent
|
|
120722
|
+
to work it). \`install\` writes hooks for event-fired entries only; a
|
|
120723
|
+
schedule entry's line comes from \`trigger schedule\`, and keryx never runs a
|
|
120724
|
+
daemon of its own for it.
|
|
120725
|
+
`);
|
|
120726
|
+
}
|
|
120727
|
+
|
|
118368
120728
|
// src/cli.ts
|
|
118369
120729
|
var VERSION2 = package_default.version;
|
|
118370
120730
|
var CLI_ROUTES = {
|
|
@@ -118405,18 +120765,20 @@ var CLI_ROUTES = {
|
|
|
118405
120765
|
shell: shellCommand,
|
|
118406
120766
|
sessions: sessionsCommand,
|
|
118407
120767
|
session: sessionsCommand,
|
|
120768
|
+
acp: acpCommand,
|
|
118408
120769
|
bus: busCommand,
|
|
118409
120770
|
version: versionCommand,
|
|
118410
120771
|
workspace: workspaceCommand,
|
|
118411
120772
|
retention: retentionCommand,
|
|
118412
|
-
forgetting: forgettingCommand
|
|
120773
|
+
forgetting: forgettingCommand,
|
|
120774
|
+
trigger: triggerCommand
|
|
118413
120775
|
};
|
|
118414
120776
|
async function main() {
|
|
118415
120777
|
setModelTurnPort(async (request) => runModelTurn(request));
|
|
118416
120778
|
const args = process.argv.slice(2);
|
|
118417
120779
|
const command = args[0];
|
|
118418
120780
|
if (command === "--help" || command === "-h" || command === "help" || !command) {
|
|
118419
|
-
|
|
120781
|
+
printHelp27();
|
|
118420
120782
|
return;
|
|
118421
120783
|
}
|
|
118422
120784
|
if (command === "--version" || command === "-v") {
|
|
@@ -118439,7 +120801,7 @@ ${usage}
|
|
|
118439
120801
|
return;
|
|
118440
120802
|
}
|
|
118441
120803
|
console.error(`Unknown command: ${command}`);
|
|
118442
|
-
|
|
120804
|
+
printHelp27();
|
|
118443
120805
|
process.exitCode = 1;
|
|
118444
120806
|
}
|
|
118445
120807
|
var USAGE_BODY = `Usage:
|
|
@@ -118448,6 +120810,8 @@ var USAGE_BODY = `Usage:
|
|
|
118448
120810
|
Start TUI agent shell (sessions are per-project)
|
|
118449
120811
|
keryx sessions list|fork <id>|export <id>|path
|
|
118450
120812
|
List / branch / export sessions for the current project
|
|
120813
|
+
keryx acp [--provider <p>] [--model <m>] [--base-url <url>] [--data-dir <dir>]
|
|
120814
|
+
Agent Client Protocol agent server over stdio
|
|
118451
120815
|
keryx bus list [--json] | log [--since <seq>] [--limit N] [--json] | send <@name|@all> [--kind <k>] [--reply-to <id>] <text> | prune
|
|
118452
120816
|
Agent bus: peers, events and messages shared across this clone's worktrees
|
|
118453
120817
|
keryx version check [--json] Check npm latest (advisory; never installs)
|
|
@@ -118562,12 +120926,15 @@ var USAGE_BODY = `Usage:
|
|
|
118562
120926
|
keryx forgetting lookup "<ref-or-path>" [--layer <layer>] [--search] [--json]
|
|
118563
120927
|
Read the deletion trail: what was removed, when, at
|
|
118564
120928
|
whose request, on what basis
|
|
120929
|
+
keryx trigger run <name> Perform exactly one pass of a declared trigger's
|
|
120930
|
+
action (.metaproject/triggers.json)
|
|
118565
120931
|
keryx --version
|
|
118566
120932
|
|
|
118567
120933
|
Commands:
|
|
118568
120934
|
shell Start the interactive TUI agent harness. Use --no-tui or --chat to opt out.
|
|
118569
120935
|
Sessions: -c continue last in this project, -r [id] resume (per-project).
|
|
118570
120936
|
sessions List or export per-project shell sessions
|
|
120937
|
+
acp Speak ACP v1 (newline-delimited JSON-RPC) over stdio, for an ACP client (e.g. an editor)
|
|
118571
120938
|
bus Agent bus: list peers and leases, read the log, send a message, prune
|
|
118572
120939
|
version Check whether a newer npm release is available
|
|
118573
120940
|
harness Run a single provider turn (harness run) and print structured events
|
|
@@ -118606,8 +120973,9 @@ Commands:
|
|
|
118606
120973
|
workspace Shared Agent Context: workspaces, FWK reads, propose/review (module sac)
|
|
118607
120974
|
retention Bound stores that grow without bound (gdctx raw/artifacts, owner write-conflict sidecars)
|
|
118608
120975
|
forgetting Read the deletion trail \u2014 was this removed, or did it never exist?
|
|
120976
|
+
trigger Fire one declared project trigger (git hook, cron line, CI job) \u2014 one pass, one exit code
|
|
118609
120977
|
`;
|
|
118610
|
-
function
|
|
120978
|
+
function printHelp27() {
|
|
118611
120979
|
console.log(`keryx ${VERSION2}
|
|
118612
120980
|
|
|
118613
120981
|
${USAGE_BODY}`);
|