pipe-kan 0.18.1 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/pipe-kan.js
CHANGED
|
@@ -904,13 +904,116 @@ function handleAppApi(req, res, app) {
|
|
|
904
904
|
return false;
|
|
905
905
|
}
|
|
906
906
|
|
|
907
|
+
// src/agent-context.ts
|
|
908
|
+
function boardViewContext(view) {
|
|
909
|
+
return [
|
|
910
|
+
{ type: "text", text: formatBoardView({ ...view, scope: view.scope ?? "view" }) },
|
|
911
|
+
{
|
|
912
|
+
type: "resource",
|
|
913
|
+
resource: {
|
|
914
|
+
uri: "pipe-kan://board",
|
|
915
|
+
mimeType: "application/json",
|
|
916
|
+
text: JSON.stringify(view)
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
];
|
|
920
|
+
}
|
|
921
|
+
function pipedBoardContext(board) {
|
|
922
|
+
return boardViewContext({
|
|
923
|
+
scope: "pipe",
|
|
924
|
+
kind: "stories",
|
|
925
|
+
selectedEpic: null,
|
|
926
|
+
search: "",
|
|
927
|
+
filter: {},
|
|
928
|
+
sort: "payload",
|
|
929
|
+
hide: [],
|
|
930
|
+
columns: board.columns,
|
|
931
|
+
epics: board.epics
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
function hasAttachedBoard(context = []) {
|
|
935
|
+
return context.some((block) => block.type === "resource" && block.resource.uri === "pipe-kan://board");
|
|
936
|
+
}
|
|
937
|
+
function withPipedBoardContext(board, clientContext = []) {
|
|
938
|
+
if (hasAttachedBoard(clientContext))
|
|
939
|
+
return clientContext;
|
|
940
|
+
return [...pipedBoardContext(board), ...clientContext];
|
|
941
|
+
}
|
|
942
|
+
function formatBoardView(view) {
|
|
943
|
+
const opener = view.kind === "epics" ? "All epics" : "All stories";
|
|
944
|
+
const scope = view.selectedEpic ? `${opener}, children of ${view.selectedEpic}` : opener;
|
|
945
|
+
const lines = [
|
|
946
|
+
view.scope === "pipe" ? "The user is working with piped Jira issues (the full payload). Answer from these Jira issues. This is not GitHub and not the local source repo." : "The user attached the current Jira Kanban board (visible cards only). Answer from these Jira issues. This is not GitHub and not the local source repo.",
|
|
947
|
+
`Board: ${scope}`
|
|
948
|
+
];
|
|
949
|
+
if (view.search.trim())
|
|
950
|
+
lines.push(`Search: ${view.search.trim()}`);
|
|
951
|
+
if (Object.values(view.filter).some((values) => values.length)) {
|
|
952
|
+
lines.push(`Filter: ${JSON.stringify(view.filter)}`);
|
|
953
|
+
}
|
|
954
|
+
lines.push(`Sort: ${view.sort}`);
|
|
955
|
+
if (view.hide.length)
|
|
956
|
+
lines.push(`Hidden columns: ${view.hide.join(", ")}`);
|
|
957
|
+
lines.push("", "Columns:");
|
|
958
|
+
if (!view.columns.length) {
|
|
959
|
+
lines.push("(empty)");
|
|
960
|
+
} else {
|
|
961
|
+
for (const column of view.columns) {
|
|
962
|
+
lines.push(`## ${column.title}`);
|
|
963
|
+
if (!column.cards.length) {
|
|
964
|
+
lines.push("(no cards)");
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
for (const card of column.cards) {
|
|
968
|
+
lines.push(`- ${formatCard(card)}`);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
if (view.epics.length) {
|
|
973
|
+
lines.push("", "Epics:");
|
|
974
|
+
for (const epic of view.epics) {
|
|
975
|
+
const status = epic.status ? ` (${epic.status})` : "";
|
|
976
|
+
lines.push(`- ${epic.key} ${epic.summary}${status}`);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
return lines.join(`
|
|
980
|
+
`);
|
|
981
|
+
}
|
|
982
|
+
function formatCard(card) {
|
|
983
|
+
const bits = [card.key, card.summary];
|
|
984
|
+
if (card.epic)
|
|
985
|
+
bits.push(`[epic ${card.epic}]`);
|
|
986
|
+
const extras = [card.priority, card.assignee, card.dueDate, card.labels?.join(", ")].filter((value) => Boolean(value));
|
|
987
|
+
if (extras.length)
|
|
988
|
+
bits.push(`(${extras.join(", ")})`);
|
|
989
|
+
return bits.join(" ");
|
|
990
|
+
}
|
|
991
|
+
|
|
907
992
|
// src/server/agent/config.ts
|
|
908
|
-
import { existsSync as existsSync2, mkdirSync, readFileSync } from "node:fs";
|
|
993
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
909
994
|
import { homedir } from "node:os";
|
|
910
|
-
import { dirname, join as join2 } from "node:path";
|
|
995
|
+
import { delimiter as delimiter2, dirname, join as join2 } from "node:path";
|
|
911
996
|
var DEFAULT_CONFIG_PATH = join2(homedir(), ".config", "pipe-kan", "agent.json");
|
|
997
|
+
var FALLBACK_MODELS = {
|
|
998
|
+
cursor: [
|
|
999
|
+
{ id: "composer-2", name: "Composer 2" },
|
|
1000
|
+
{ id: "composer-2-fast", name: "Composer 2 Fast" },
|
|
1001
|
+
{ id: "claude-sonnet-4", name: "Claude Sonnet 4" },
|
|
1002
|
+
{ id: "gpt-4.1", name: "GPT-4.1" }
|
|
1003
|
+
],
|
|
1004
|
+
devin: [
|
|
1005
|
+
{ id: "swe-1-6-slow", name: "SWE-1.6 (slow)", description: "Free slower tier" },
|
|
1006
|
+
{ id: "swe-1-6-fast", name: "SWE-1.6 Fast" },
|
|
1007
|
+
{ id: "swe-1-7", name: "SWE-1.7" },
|
|
1008
|
+
{ id: "swe", name: "SWE (latest)" },
|
|
1009
|
+
{ id: "adaptive", name: "Adaptive" },
|
|
1010
|
+
{ id: "sonnet", name: "Sonnet" },
|
|
1011
|
+
{ id: "opus", name: "Opus" },
|
|
1012
|
+
{ id: "gpt", name: "GPT" }
|
|
1013
|
+
]
|
|
1014
|
+
};
|
|
912
1015
|
var DEFAULT_CONFIG = {
|
|
913
|
-
defaultAgent: "
|
|
1016
|
+
defaultAgent: "devin",
|
|
914
1017
|
defaultSkill: null,
|
|
915
1018
|
agents: {
|
|
916
1019
|
cursor: {
|
|
@@ -921,11 +1024,28 @@ var DEFAULT_CONFIG = {
|
|
|
921
1024
|
devin: {
|
|
922
1025
|
command: "devin",
|
|
923
1026
|
args: ["acp"],
|
|
924
|
-
model: "
|
|
1027
|
+
model: "swe-1-6-slow"
|
|
925
1028
|
}
|
|
926
1029
|
}
|
|
927
1030
|
};
|
|
928
|
-
function
|
|
1031
|
+
function agentConfigPath() {
|
|
1032
|
+
return process.env.PIPE_KAN_AGENT_CONFIG ?? DEFAULT_CONFIG_PATH;
|
|
1033
|
+
}
|
|
1034
|
+
function commandOnPath(command, env = process.env) {
|
|
1035
|
+
if (!command)
|
|
1036
|
+
return false;
|
|
1037
|
+
if (command.includes("/") || command.includes("\\"))
|
|
1038
|
+
return existsSync2(command);
|
|
1039
|
+
const pathVar = env.PATH ?? "";
|
|
1040
|
+
for (const dir of pathVar.split(delimiter2)) {
|
|
1041
|
+
if (!dir)
|
|
1042
|
+
continue;
|
|
1043
|
+
if (existsSync2(join2(dir, command)))
|
|
1044
|
+
return true;
|
|
1045
|
+
}
|
|
1046
|
+
return false;
|
|
1047
|
+
}
|
|
1048
|
+
function loadAgentConfig(path = agentConfigPath()) {
|
|
929
1049
|
if (!existsSync2(path))
|
|
930
1050
|
return structuredClone(DEFAULT_CONFIG);
|
|
931
1051
|
try {
|
|
@@ -935,17 +1055,43 @@ function loadAgentConfig(path = DEFAULT_CONFIG_PATH) {
|
|
|
935
1055
|
return structuredClone(DEFAULT_CONFIG);
|
|
936
1056
|
}
|
|
937
1057
|
}
|
|
1058
|
+
function saveAgentConfig(patch, path = agentConfigPath()) {
|
|
1059
|
+
const current = loadAgentConfig(path);
|
|
1060
|
+
const next = {
|
|
1061
|
+
defaultAgent: patch.defaultAgent ?? current.defaultAgent,
|
|
1062
|
+
defaultSkill: patch.defaultSkill !== undefined ? patch.defaultSkill : current.defaultSkill,
|
|
1063
|
+
agents: { ...current.agents }
|
|
1064
|
+
};
|
|
1065
|
+
if (patch.agents) {
|
|
1066
|
+
for (const [id, config] of Object.entries(patch.agents)) {
|
|
1067
|
+
next.agents[id] = { ...next.agents[id], ...config };
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
ensureAgentConfigDir(path);
|
|
1071
|
+
writeFileSync(path, `${JSON.stringify(next, null, 2)}
|
|
1072
|
+
`);
|
|
1073
|
+
return next;
|
|
1074
|
+
}
|
|
938
1075
|
function mergeConfig(raw) {
|
|
1076
|
+
const agents = { ...DEFAULT_CONFIG.agents };
|
|
1077
|
+
for (const [id, config] of Object.entries({ ...DEFAULT_CONFIG.agents, ...raw.agents })) {
|
|
1078
|
+
agents[id] = { ...DEFAULT_CONFIG.agents[id], ...config };
|
|
1079
|
+
}
|
|
939
1080
|
return {
|
|
940
1081
|
defaultAgent: raw.defaultAgent ?? DEFAULT_CONFIG.defaultAgent,
|
|
941
1082
|
defaultSkill: raw.defaultSkill ?? DEFAULT_CONFIG.defaultSkill,
|
|
942
|
-
agents
|
|
1083
|
+
agents
|
|
943
1084
|
};
|
|
944
1085
|
}
|
|
1086
|
+
function ensureAgentConfigDir(path = agentConfigPath()) {
|
|
1087
|
+
const dir = dirname(path);
|
|
1088
|
+
if (!existsSync2(dir))
|
|
1089
|
+
mkdirSync(dir, { recursive: true });
|
|
1090
|
+
}
|
|
945
1091
|
|
|
946
1092
|
// src/server/agent/session.ts
|
|
947
1093
|
import { spawn as spawn2 } from "node:child_process";
|
|
948
|
-
import { mkdtempSync } from "node:fs";
|
|
1094
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
949
1095
|
import { tmpdir } from "node:os";
|
|
950
1096
|
import { join as join3 } from "node:path";
|
|
951
1097
|
import { Readable } from "node:stream";
|
|
@@ -10575,25 +10721,93 @@ var legacyClientNotificationMethods = new Set([
|
|
|
10575
10721
|
CLIENT_METHODS.elicitation_complete
|
|
10576
10722
|
]);
|
|
10577
10723
|
|
|
10578
|
-
// src/server/agent/
|
|
10579
|
-
function
|
|
10580
|
-
|
|
10581
|
-
|
|
10582
|
-
|
|
10583
|
-
|
|
10584
|
-
|
|
10585
|
-
|
|
10724
|
+
// src/server/agent/models.ts
|
|
10725
|
+
function modelsFromConfigOptions(options) {
|
|
10726
|
+
if (!options?.length)
|
|
10727
|
+
return [];
|
|
10728
|
+
const model = options.find((option) => option.category === "model" || option.id === "model");
|
|
10729
|
+
if (!model || model.type !== "select")
|
|
10730
|
+
return [];
|
|
10731
|
+
return flattenSelectOptions(model.options);
|
|
10732
|
+
}
|
|
10733
|
+
function selectedModelFromConfigOptions(options) {
|
|
10734
|
+
const model = options?.find((option) => option.category === "model" || option.id === "model");
|
|
10735
|
+
if (!model || model.type !== "select")
|
|
10736
|
+
return null;
|
|
10737
|
+
return model.currentValue ?? null;
|
|
10738
|
+
}
|
|
10739
|
+
var MODEL_ALIASES = {
|
|
10740
|
+
"swe-1-6": ["swe-1-6-slow"],
|
|
10741
|
+
"swe-1.6": ["swe-1-6-slow"],
|
|
10742
|
+
"swe-1.6-slow": ["swe-1-6-slow"],
|
|
10743
|
+
"swe-1-6-slow": ["swe-1-6"]
|
|
10744
|
+
};
|
|
10745
|
+
function resolveRequestedModel(requested, live) {
|
|
10746
|
+
if (!requested)
|
|
10747
|
+
return null;
|
|
10748
|
+
if (!live.length)
|
|
10749
|
+
return requested;
|
|
10750
|
+
if (live.some((model) => model.id === requested))
|
|
10751
|
+
return requested;
|
|
10752
|
+
for (const alias of MODEL_ALIASES[requested] ?? []) {
|
|
10753
|
+
const hit = live.find((model) => model.id === alias);
|
|
10754
|
+
if (hit)
|
|
10755
|
+
return hit.id;
|
|
10756
|
+
}
|
|
10757
|
+
const lower = requested.toLowerCase();
|
|
10758
|
+
return live.find((model) => model.name.toLowerCase() === lower)?.id ?? null;
|
|
10759
|
+
}
|
|
10760
|
+
function errorText(err) {
|
|
10761
|
+
if (err instanceof Error) {
|
|
10762
|
+
const extra = err.data;
|
|
10763
|
+
return extra ? `${err.message} ${JSON.stringify(extra)}` : err.message;
|
|
10764
|
+
}
|
|
10765
|
+
return String(err);
|
|
10766
|
+
}
|
|
10767
|
+
function parseAvailableModelsFromError(err) {
|
|
10768
|
+
const text = errorText(err);
|
|
10769
|
+
const match = text.match(/Available models:\s*([^\n"}]+)/i);
|
|
10770
|
+
if (!match)
|
|
10771
|
+
return [];
|
|
10772
|
+
return match[1].split(",").map((id) => id.trim()).filter(Boolean);
|
|
10773
|
+
}
|
|
10774
|
+
function flattenSelectOptions(options) {
|
|
10775
|
+
if (!Array.isArray(options))
|
|
10776
|
+
return [];
|
|
10777
|
+
const models = [];
|
|
10778
|
+
for (const item of options) {
|
|
10779
|
+
if (!item || typeof item !== "object")
|
|
10780
|
+
continue;
|
|
10781
|
+
if ("group" in item && Array.isArray(item.options)) {
|
|
10782
|
+
models.push(...flattenSelectOptions(item.options));
|
|
10783
|
+
continue;
|
|
10586
10784
|
}
|
|
10587
|
-
|
|
10785
|
+
if ("value" in item && "name" in item) {
|
|
10786
|
+
const option = item;
|
|
10787
|
+
models.push({
|
|
10788
|
+
id: option.value,
|
|
10789
|
+
name: option.name,
|
|
10790
|
+
...option.description ? { description: option.description } : {}
|
|
10791
|
+
});
|
|
10792
|
+
}
|
|
10793
|
+
}
|
|
10794
|
+
return models;
|
|
10588
10795
|
}
|
|
10589
10796
|
|
|
10797
|
+
// src/server/agent/session.ts
|
|
10798
|
+
var CONNECT_TIMEOUT_MS = 15000;
|
|
10799
|
+
function createAcpSession(config) {
|
|
10800
|
+
return new AcpSession(config);
|
|
10801
|
+
}
|
|
10590
10802
|
class AcpSession {
|
|
10591
10803
|
config;
|
|
10592
10804
|
id;
|
|
10593
10805
|
proc = null;
|
|
10594
10806
|
activeSession = null;
|
|
10807
|
+
agentCtx = null;
|
|
10808
|
+
configOptions = [];
|
|
10595
10809
|
eventsQueue = [];
|
|
10596
|
-
|
|
10810
|
+
listeners = new Set;
|
|
10597
10811
|
pendingPermissionResolvers = new Map;
|
|
10598
10812
|
closed = false;
|
|
10599
10813
|
lifetime = null;
|
|
@@ -10603,16 +10817,28 @@ class AcpSession {
|
|
|
10603
10817
|
toolCalls = new Map;
|
|
10604
10818
|
shouldAutoApproveTool = null;
|
|
10605
10819
|
executeTool = null;
|
|
10820
|
+
stderr = "";
|
|
10821
|
+
selectedModelId = null;
|
|
10822
|
+
workspace = mkdtempSync(join3(tmpdir(), "pipe-kan-agent-"));
|
|
10606
10823
|
constructor(config) {
|
|
10607
10824
|
this.config = config;
|
|
10608
10825
|
this.id = crypto.randomUUID();
|
|
10826
|
+
this.selectedModelId = config.model ?? null;
|
|
10827
|
+
this.ready.promise.catch(() => {
|
|
10828
|
+
return;
|
|
10829
|
+
});
|
|
10609
10830
|
}
|
|
10610
10831
|
async prompt(text, context) {
|
|
10611
10832
|
if (!this.activeSession)
|
|
10612
10833
|
throw new Error("ACP session not connected");
|
|
10613
|
-
const
|
|
10614
|
-
|
|
10615
|
-
|
|
10834
|
+
const resources = (context ?? []).filter((block) => block.type === "resource").map(toContentBlock);
|
|
10835
|
+
const contextText = (context ?? []).filter((block) => block.type === "text").map((block) => block.text).filter(Boolean).join(`
|
|
10836
|
+
|
|
10837
|
+
`);
|
|
10838
|
+
const prompt = contextText ? `${contextText}
|
|
10839
|
+
|
|
10840
|
+
${text}` : text;
|
|
10841
|
+
await this.activeSession.prompt([...resources, { type: "text", text: prompt }]);
|
|
10616
10842
|
}
|
|
10617
10843
|
setToolParser(parser) {
|
|
10618
10844
|
this.detectToolCall = parser;
|
|
@@ -10644,21 +10870,78 @@ ${resultText}`).catch(() => {
|
|
|
10644
10870
|
entry.resolve(outcome);
|
|
10645
10871
|
this.pendingPermissionResolvers.delete(requestId);
|
|
10646
10872
|
}
|
|
10873
|
+
models() {
|
|
10874
|
+
const live = modelsFromConfigOptions(this.configOptions);
|
|
10875
|
+
return live.length ? live : [];
|
|
10876
|
+
}
|
|
10877
|
+
selectedModel() {
|
|
10878
|
+
return selectedModelFromConfigOptions(this.configOptions) ?? this.selectedModelId;
|
|
10879
|
+
}
|
|
10880
|
+
async setModel(id) {
|
|
10881
|
+
const live = this.models();
|
|
10882
|
+
let resolved = resolveRequestedModel(id, live);
|
|
10883
|
+
if (!resolved && live.length)
|
|
10884
|
+
return;
|
|
10885
|
+
resolved = resolved ?? id;
|
|
10886
|
+
this.selectedModelId = resolved;
|
|
10887
|
+
this.config.model = resolved;
|
|
10888
|
+
const option = this.configOptions.find((item) => item.category === "model" || item.id === "model");
|
|
10889
|
+
if (!option || option.type !== "select" || !this.agentCtx || !this.activeSession)
|
|
10890
|
+
return;
|
|
10891
|
+
try {
|
|
10892
|
+
const result = await this.agentCtx.request(methods.agent.session.setConfigOption, {
|
|
10893
|
+
sessionId: this.activeSession.sessionId,
|
|
10894
|
+
configId: option.id,
|
|
10895
|
+
value: resolved
|
|
10896
|
+
});
|
|
10897
|
+
if (result.configOptions)
|
|
10898
|
+
this.configOptions = result.configOptions;
|
|
10899
|
+
this.selectedModelId = this.selectedModel();
|
|
10900
|
+
this.push({ type: "config", models: this.models(), selectedModel: this.selectedModel() });
|
|
10901
|
+
} catch (err) {
|
|
10902
|
+
const available = parseAvailableModelsFromError(err).map((modelId) => ({ id: modelId, name: modelId }));
|
|
10903
|
+
const retry = resolveRequestedModel(id, available) ?? resolveRequestedModel(resolved, available);
|
|
10904
|
+
if (retry && retry !== resolved) {
|
|
10905
|
+
await this.setModel(retry);
|
|
10906
|
+
return;
|
|
10907
|
+
}
|
|
10908
|
+
this.selectedModelId = selectedModelFromConfigOptions(this.configOptions);
|
|
10909
|
+
throw new Error(errorText(err));
|
|
10910
|
+
}
|
|
10911
|
+
}
|
|
10912
|
+
subscribe(listener) {
|
|
10913
|
+
this.listeners.add(listener);
|
|
10914
|
+
if (this.eventsQueue.length) {
|
|
10915
|
+
const queued = this.eventsQueue.splice(0);
|
|
10916
|
+
for (const event of queued)
|
|
10917
|
+
listener(event);
|
|
10918
|
+
}
|
|
10919
|
+
return () => {
|
|
10920
|
+
this.listeners.delete(listener);
|
|
10921
|
+
};
|
|
10922
|
+
}
|
|
10647
10923
|
cancel() {
|
|
10648
10924
|
this.proc?.kill("SIGTERM");
|
|
10649
10925
|
}
|
|
10650
10926
|
async* [Symbol.asyncIterator]() {
|
|
10651
|
-
|
|
10652
|
-
|
|
10653
|
-
|
|
10654
|
-
|
|
10655
|
-
|
|
10656
|
-
|
|
10657
|
-
|
|
10658
|
-
|
|
10659
|
-
|
|
10660
|
-
|
|
10927
|
+
const queue = [];
|
|
10928
|
+
let wake = null;
|
|
10929
|
+
const unsub = this.subscribe((event) => {
|
|
10930
|
+
queue.push(event);
|
|
10931
|
+
wake?.();
|
|
10932
|
+
});
|
|
10933
|
+
try {
|
|
10934
|
+
while (!this.closed || queue.length) {
|
|
10935
|
+
if (!queue.length) {
|
|
10936
|
+
await new Promise((resolve) => {
|
|
10937
|
+
wake = resolve;
|
|
10938
|
+
});
|
|
10939
|
+
}
|
|
10940
|
+
while (queue.length)
|
|
10941
|
+
yield queue.shift();
|
|
10661
10942
|
}
|
|
10943
|
+
} finally {
|
|
10944
|
+
unsub();
|
|
10662
10945
|
}
|
|
10663
10946
|
}
|
|
10664
10947
|
events() {
|
|
@@ -10666,47 +10949,83 @@ ${resultText}`).catch(() => {
|
|
|
10666
10949
|
}
|
|
10667
10950
|
async close() {
|
|
10668
10951
|
this.closed = true;
|
|
10952
|
+
this.ready.reject(new Error("ACP session closed"));
|
|
10669
10953
|
this.activeSession?.dispose();
|
|
10670
10954
|
this.lifetime?.resolve();
|
|
10671
10955
|
this.proc?.kill("SIGTERM");
|
|
10672
10956
|
this.flushQueue();
|
|
10673
10957
|
this.proc = null;
|
|
10958
|
+
rmSync(this.workspace, { recursive: true, force: true });
|
|
10674
10959
|
}
|
|
10675
10960
|
async connect() {
|
|
10676
|
-
|
|
10961
|
+
if (this.closed)
|
|
10962
|
+
throw new Error("ACP session closed");
|
|
10963
|
+
const cwd = this.workspace;
|
|
10677
10964
|
const cmd = this.config.command;
|
|
10678
10965
|
const args = this.config.args ?? [];
|
|
10679
10966
|
this.proc = spawn2(cmd, args, {
|
|
10680
|
-
cwd,
|
|
10967
|
+
cwd: process.cwd(),
|
|
10681
10968
|
env: { ...process.env, ...this.config.env ?? {} },
|
|
10682
10969
|
stdio: ["pipe", "pipe", "pipe"]
|
|
10683
10970
|
});
|
|
10971
|
+
if (this.closed) {
|
|
10972
|
+
this.proc.kill("SIGTERM");
|
|
10973
|
+
this.proc = null;
|
|
10974
|
+
throw new Error("ACP session closed");
|
|
10975
|
+
}
|
|
10684
10976
|
if (!this.proc.stdout || !this.proc.stdin) {
|
|
10685
10977
|
throw new Error(`Failed to spawn ACP agent: ${cmd}`);
|
|
10686
10978
|
}
|
|
10979
|
+
this.proc.stderr?.on("data", (chunk) => {
|
|
10980
|
+
this.stderr += chunk.toString("utf8");
|
|
10981
|
+
});
|
|
10687
10982
|
const stdout = Readable.toWeb(this.proc.stdout);
|
|
10688
10983
|
const stream = ndJsonStream2(new WritableStream({
|
|
10689
10984
|
write: (chunk) => this.writeStdin(chunk)
|
|
10690
10985
|
}), stdout);
|
|
10691
|
-
this.proc.on("error", (err) =>
|
|
10692
|
-
|
|
10986
|
+
this.proc.on("error", (err) => {
|
|
10987
|
+
this.push({ type: "error", message: err.message });
|
|
10988
|
+
this.ready.reject(err);
|
|
10989
|
+
});
|
|
10990
|
+
this.proc.on("exit", (code) => {
|
|
10693
10991
|
this.push({ type: "disconnected" });
|
|
10694
10992
|
this.closed = true;
|
|
10695
10993
|
this.flushQueue();
|
|
10994
|
+
if (code) {
|
|
10995
|
+
this.ready.reject(new Error(`ACP agent exited (${code})${this.stderr.trim() ? `: ${this.stderr.trim()}` : ""}`));
|
|
10996
|
+
}
|
|
10696
10997
|
});
|
|
10697
10998
|
const app = client({ name: "pipe-kan" });
|
|
10698
10999
|
app.onRequest(methods.client.session.requestPermission, (ctx) => this.handlePermission(ctx.params));
|
|
10699
11000
|
app.onRequest(methods.client.fs.readTextFile, () => Promise.reject(new Error("read_file not implemented")));
|
|
10700
11001
|
this.lifetime = Promise.withResolvers();
|
|
10701
11002
|
const connected = app.connectWith(stream, async (ctx) => {
|
|
11003
|
+
this.agentCtx = ctx;
|
|
10702
11004
|
this.activeSession = await ctx.buildSession(cwd).start();
|
|
11005
|
+
this.configOptions = this.activeSession.newSessionResponse.configOptions ?? [];
|
|
11006
|
+
try {
|
|
11007
|
+
if (this.config.model)
|
|
11008
|
+
await this.setModel(this.config.model);
|
|
11009
|
+
} catch (err) {
|
|
11010
|
+
this.push({ type: "error", message: `Could not set model ${this.config.model}: ${errorText(err)}` });
|
|
11011
|
+
}
|
|
10703
11012
|
this.ready.resolve();
|
|
10704
11013
|
this.push({ type: "connected" });
|
|
11014
|
+
this.push({ type: "config", models: this.models(), selectedModel: this.selectedModel() });
|
|
10705
11015
|
this.readLoop();
|
|
10706
11016
|
await this.lifetime.promise;
|
|
10707
11017
|
});
|
|
10708
11018
|
connected.catch(() => {});
|
|
10709
|
-
|
|
11019
|
+
const timeout = setTimeout(() => {
|
|
11020
|
+
this.ready.reject(new Error(`Timed out starting ${cmd}${this.stderr.trim() ? `:
|
|
11021
|
+
${lastLines(this.stderr)}` : ""}`));
|
|
11022
|
+
this.close();
|
|
11023
|
+
}, CONNECT_TIMEOUT_MS);
|
|
11024
|
+
try {
|
|
11025
|
+
await this.ready.promise;
|
|
11026
|
+
} finally {
|
|
11027
|
+
clearTimeout(timeout);
|
|
11028
|
+
}
|
|
10710
11029
|
}
|
|
10711
11030
|
async readLoop() {
|
|
10712
11031
|
if (!this.activeSession)
|
|
@@ -10743,9 +11062,12 @@ ${resultText}`).catch(() => {
|
|
|
10743
11062
|
this.push({
|
|
10744
11063
|
type: "tool_call",
|
|
10745
11064
|
requestId: update.toolCallId ?? crypto.randomUUID(),
|
|
10746
|
-
name: update.
|
|
11065
|
+
name: update.title ?? "unknown",
|
|
10747
11066
|
args: {}
|
|
10748
11067
|
});
|
|
11068
|
+
} else if (update.sessionUpdate === "config_option_update") {
|
|
11069
|
+
this.configOptions = update.configOptions ?? this.configOptions;
|
|
11070
|
+
this.push({ type: "config", models: this.models(), selectedModel: this.selectedModel() });
|
|
10749
11071
|
}
|
|
10750
11072
|
}
|
|
10751
11073
|
handlePermission(params) {
|
|
@@ -10755,7 +11077,7 @@ ${resultText}`).catch(() => {
|
|
|
10755
11077
|
this.push({
|
|
10756
11078
|
type: "request_permission",
|
|
10757
11079
|
requestId,
|
|
10758
|
-
kind: params.toolCall.
|
|
11080
|
+
kind: params.toolCall.title ?? "unknown",
|
|
10759
11081
|
description: params.toolCall.content ? JSON.stringify(params.toolCall.content) : ""
|
|
10760
11082
|
});
|
|
10761
11083
|
return promise.then((outcome) => ({ outcome }));
|
|
@@ -10771,19 +11093,23 @@ ${resultText}`).catch(() => {
|
|
|
10771
11093
|
}
|
|
10772
11094
|
}
|
|
10773
11095
|
push(event) {
|
|
10774
|
-
|
|
10775
|
-
if (resolver) {
|
|
10776
|
-
resolver({ value: event, done: false });
|
|
10777
|
-
} else {
|
|
11096
|
+
if (this.listeners.size === 0) {
|
|
10778
11097
|
this.eventsQueue.push(event);
|
|
11098
|
+
return;
|
|
10779
11099
|
}
|
|
11100
|
+
for (const listener of this.listeners)
|
|
11101
|
+
listener(event);
|
|
10780
11102
|
}
|
|
10781
11103
|
flushQueue() {
|
|
10782
|
-
|
|
10783
|
-
|
|
10784
|
-
|
|
11104
|
+
this.eventsQueue.length = 0;
|
|
11105
|
+
for (const listener of this.listeners)
|
|
11106
|
+
listener({ type: "disconnected" });
|
|
10785
11107
|
}
|
|
10786
11108
|
}
|
|
11109
|
+
function lastLines(text, n = 12) {
|
|
11110
|
+
return text.trim().split(/\n/).slice(-n).join(`
|
|
11111
|
+
`);
|
|
11112
|
+
}
|
|
10787
11113
|
function toContentBlock(ctx) {
|
|
10788
11114
|
if (ctx.type === "resource") {
|
|
10789
11115
|
return {
|
|
@@ -11007,7 +11333,9 @@ var EXECUTORS = {
|
|
|
11007
11333
|
return { ok: true, value: { __ui_action: "set_filter", filter } };
|
|
11008
11334
|
}
|
|
11009
11335
|
};
|
|
11010
|
-
var SYSTEM_TEXT = `
|
|
11336
|
+
var SYSTEM_TEXT = `Your working context is the attached Jira issues from the pipe. Answer from that payload. Do not search GitHub, git history, or local source files unless the user explicitly asks about the pipe-kan app itself.
|
|
11337
|
+
|
|
11338
|
+
You can call tools by emitting a single JSON code block matching this schema:
|
|
11011
11339
|
|
|
11012
11340
|
{"tool": "<name>", "args": {...}}
|
|
11013
11341
|
|
|
@@ -11062,6 +11390,8 @@ function createToolRegistry() {
|
|
|
11062
11390
|
|
|
11063
11391
|
// src/server/agent/api.ts
|
|
11064
11392
|
var sessions = new Map;
|
|
11393
|
+
var connecting = null;
|
|
11394
|
+
var startEpoch = 0;
|
|
11065
11395
|
var skills2 = createSkillRegistry();
|
|
11066
11396
|
var tools = createToolRegistry();
|
|
11067
11397
|
function json2(res, status, body) {
|
|
@@ -11080,21 +11410,45 @@ function readBody2(req) {
|
|
|
11080
11410
|
function pathOf2(req) {
|
|
11081
11411
|
return new URL(req.url ?? "/", "http://127.0.0.1");
|
|
11082
11412
|
}
|
|
11413
|
+
function publicConfig(cfg) {
|
|
11414
|
+
return {
|
|
11415
|
+
defaultAgent: cfg.defaultAgent,
|
|
11416
|
+
defaultSkill: cfg.defaultSkill ?? null,
|
|
11417
|
+
agents: Object.entries(cfg.agents).map(([id, c]) => ({
|
|
11418
|
+
id,
|
|
11419
|
+
command: [c.command, ...c.args ?? []].join(" "),
|
|
11420
|
+
model: c.model ?? null,
|
|
11421
|
+
options: c.options,
|
|
11422
|
+
available: commandOnPath(c.command),
|
|
11423
|
+
models: FALLBACK_MODELS[id] ?? []
|
|
11424
|
+
}))
|
|
11425
|
+
};
|
|
11426
|
+
}
|
|
11427
|
+
function attachTools(session, app) {
|
|
11428
|
+
session.setToolParser((text) => {
|
|
11429
|
+
const call = tools.parse(text);
|
|
11430
|
+
return call ? { requestId: call.requestId, name: call.name, args: call.args } : undefined;
|
|
11431
|
+
});
|
|
11432
|
+
session.setToolExecutor((name) => !tools.isMutating(name), async (call) => {
|
|
11433
|
+
const result = await tools.execute(call.name, call.args, app);
|
|
11434
|
+
return {
|
|
11435
|
+
text: result.ok ? `Result: ${JSON.stringify(result.value)}` : `Error: ${result.error}`,
|
|
11436
|
+
result: result.ok ? result.value : undefined
|
|
11437
|
+
};
|
|
11438
|
+
});
|
|
11439
|
+
}
|
|
11083
11440
|
function handleAgentApi(req, res, app) {
|
|
11084
11441
|
const url = pathOf2(req);
|
|
11085
11442
|
const method = (req.method ?? "GET").toUpperCase();
|
|
11086
11443
|
if (url.pathname === "/api/agent/config" && method === "GET") {
|
|
11087
|
-
|
|
11088
|
-
|
|
11089
|
-
|
|
11090
|
-
|
|
11091
|
-
|
|
11092
|
-
|
|
11093
|
-
|
|
11094
|
-
|
|
11095
|
-
options: c.options
|
|
11096
|
-
}))
|
|
11097
|
-
});
|
|
11444
|
+
json2(res, 200, publicConfig(loadAgentConfig()));
|
|
11445
|
+
return true;
|
|
11446
|
+
}
|
|
11447
|
+
if (url.pathname === "/api/agent/config" && method === "POST") {
|
|
11448
|
+
readBody2(req).then((text) => {
|
|
11449
|
+
const body = text ? JSON.parse(text) : {};
|
|
11450
|
+
json2(res, 200, publicConfig(saveAgentConfig(body)));
|
|
11451
|
+
}).catch((err) => json2(res, 500, { error: String(err) }));
|
|
11098
11452
|
return true;
|
|
11099
11453
|
}
|
|
11100
11454
|
if (url.pathname === "/api/agent/skills" && method === "GET") {
|
|
@@ -11102,27 +11456,67 @@ function handleAgentApi(req, res, app) {
|
|
|
11102
11456
|
return true;
|
|
11103
11457
|
}
|
|
11104
11458
|
if (url.pathname === "/api/agent/session" && method === "POST") {
|
|
11105
|
-
|
|
11106
|
-
|
|
11107
|
-
|
|
11108
|
-
|
|
11109
|
-
|
|
11110
|
-
|
|
11111
|
-
|
|
11112
|
-
|
|
11113
|
-
|
|
11114
|
-
|
|
11115
|
-
|
|
11116
|
-
|
|
11117
|
-
|
|
11118
|
-
|
|
11119
|
-
|
|
11120
|
-
|
|
11121
|
-
result: result.ok ? result.value : undefined
|
|
11122
|
-
};
|
|
11459
|
+
readBody2(req).then(async (text) => {
|
|
11460
|
+
const body = text ? JSON.parse(text) : {};
|
|
11461
|
+
const cfg = loadAgentConfig();
|
|
11462
|
+
const agentId = body.agentId || cfg.defaultAgent;
|
|
11463
|
+
const backendConfig = cfg.agents[agentId];
|
|
11464
|
+
if (!backendConfig) {
|
|
11465
|
+
json2(res, 500, { error: `No agent config for ${agentId}` });
|
|
11466
|
+
return;
|
|
11467
|
+
}
|
|
11468
|
+
if (!commandOnPath(backendConfig.command)) {
|
|
11469
|
+
json2(res, 500, { error: `${backendConfig.command} is not on PATH` });
|
|
11470
|
+
return;
|
|
11471
|
+
}
|
|
11472
|
+
const session = createAcpSession({
|
|
11473
|
+
...backendConfig,
|
|
11474
|
+
model: body.model || backendConfig.model
|
|
11123
11475
|
});
|
|
11124
|
-
|
|
11125
|
-
|
|
11476
|
+
const epoch = ++startEpoch;
|
|
11477
|
+
const previous = connecting;
|
|
11478
|
+
const old = [...sessions.values()];
|
|
11479
|
+
connecting = session;
|
|
11480
|
+
sessions.clear();
|
|
11481
|
+
await Promise.all([previous, ...old].filter((item) => item != null).map((item) => item.close().catch(() => {
|
|
11482
|
+
return;
|
|
11483
|
+
})));
|
|
11484
|
+
try {
|
|
11485
|
+
await session.connect();
|
|
11486
|
+
if (epoch !== startEpoch) {
|
|
11487
|
+
await session.close().catch(() => {
|
|
11488
|
+
return;
|
|
11489
|
+
});
|
|
11490
|
+
json2(res, 500, { error: "Session replaced" });
|
|
11491
|
+
return;
|
|
11492
|
+
}
|
|
11493
|
+
connecting = null;
|
|
11494
|
+
attachTools(session, app);
|
|
11495
|
+
sessions.set(session.id, session);
|
|
11496
|
+
json2(res, 200, {
|
|
11497
|
+
sessionId: session.id,
|
|
11498
|
+
agentId,
|
|
11499
|
+
models: session.models().length ? session.models() : FALLBACK_MODELS[agentId] ?? [],
|
|
11500
|
+
selectedModel: session.selectedModel()
|
|
11501
|
+
});
|
|
11502
|
+
} catch (err) {
|
|
11503
|
+
if (connecting === session)
|
|
11504
|
+
connecting = null;
|
|
11505
|
+
throw err;
|
|
11506
|
+
}
|
|
11507
|
+
}).catch((err) => json2(res, 500, { error: String(err) }));
|
|
11508
|
+
return true;
|
|
11509
|
+
}
|
|
11510
|
+
if (url.pathname === "/api/agent/model" && method === "POST") {
|
|
11511
|
+
readBody2(req).then(async (text) => {
|
|
11512
|
+
const body = JSON.parse(text);
|
|
11513
|
+
const session = sessions.get(body.sessionId ?? "");
|
|
11514
|
+
if (!session) {
|
|
11515
|
+
json2(res, 404, { error: "Session not found" });
|
|
11516
|
+
return;
|
|
11517
|
+
}
|
|
11518
|
+
await session.setModel(String(body.model ?? ""));
|
|
11519
|
+
json2(res, 200, { ok: true, models: session.models(), selectedModel: session.selectedModel() });
|
|
11126
11520
|
}).catch((err) => json2(res, 500, { error: String(err) }));
|
|
11127
11521
|
return true;
|
|
11128
11522
|
}
|
|
@@ -11134,7 +11528,7 @@ function handleAgentApi(req, res, app) {
|
|
|
11134
11528
|
json2(res, 404, { error: "Session not found" });
|
|
11135
11529
|
return;
|
|
11136
11530
|
}
|
|
11137
|
-
const context = [tools.systemBlock(), ...body.context ?? []];
|
|
11531
|
+
const context = [tools.systemBlock(), ...withPipedBoardContext(app.board(), body.context ?? [])];
|
|
11138
11532
|
if (body.skillId) {
|
|
11139
11533
|
const skill = skills2.load(body.skillId);
|
|
11140
11534
|
if (skill)
|
|
@@ -11206,21 +11600,13 @@ function handleAgentApi(req, res, app) {
|
|
|
11206
11600
|
|
|
11207
11601
|
`);
|
|
11208
11602
|
};
|
|
11209
|
-
|
|
11210
|
-
|
|
11211
|
-
|
|
11212
|
-
if (event.type === "disconnected") {
|
|
11213
|
-
break;
|
|
11214
|
-
}
|
|
11215
|
-
}
|
|
11216
|
-
if (!res.writableEnded)
|
|
11217
|
-
res.end();
|
|
11218
|
-
})().catch((err) => {
|
|
11219
|
-
writeEvent({ type: "error", message: String(err) });
|
|
11220
|
-
if (!res.writableEnded)
|
|
11603
|
+
const unsubscribe = session.subscribe((event) => {
|
|
11604
|
+
writeEvent(event);
|
|
11605
|
+
if (event.type === "disconnected" && !res.writableEnded)
|
|
11221
11606
|
res.end();
|
|
11222
11607
|
});
|
|
11223
11608
|
req.on("close", () => {
|
|
11609
|
+
unsubscribe();
|
|
11224
11610
|
if (!res.writableEnded)
|
|
11225
11611
|
res.end();
|
|
11226
11612
|
});
|
|
@@ -11335,12 +11721,12 @@ function handleRequest(req, res, ctx) {
|
|
|
11335
11721
|
}
|
|
11336
11722
|
|
|
11337
11723
|
// src/jira-config.ts
|
|
11338
|
-
import { mkdirSync as mkdirSync2, writeFileSync } from "node:fs";
|
|
11724
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
11339
11725
|
import { join as join6 } from "node:path";
|
|
11340
11726
|
function writeJiraConfig(dir, server) {
|
|
11341
11727
|
mkdirSync2(dir, { recursive: true });
|
|
11342
11728
|
const path = join6(dir, "jira.config.yml");
|
|
11343
|
-
|
|
11729
|
+
writeFileSync2(path, [
|
|
11344
11730
|
"installation: Cloud",
|
|
11345
11731
|
`server: ${server}`,
|
|
11346
11732
|
`login: ${ME.emailAddress}`,
|