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: "cursor",
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: "devin"
1027
+ model: "swe-1-6-slow"
925
1028
  }
926
1029
  }
927
1030
  };
928
- function loadAgentConfig(path = DEFAULT_CONFIG_PATH) {
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: { ...DEFAULT_CONFIG.agents, ...raw.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/session.ts
10579
- function createAcpBackend(id, label) {
10580
- return {
10581
- id,
10582
- label,
10583
- spawn(config) {
10584
- const session = new AcpSession(config);
10585
- return session.connect().then(() => session);
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
- eventResolvers = [];
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 blocks = (context ?? []).map(toContentBlock);
10614
- blocks.push({ type: "text", text });
10615
- await this.activeSession.prompt(blocks);
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
- while (!this.closed || this.eventsQueue.length) {
10652
- if (this.eventsQueue.length) {
10653
- yield this.eventsQueue.shift();
10654
- } else {
10655
- const { promise, resolve } = Promise.withResolvers();
10656
- this.eventResolvers.push(resolve);
10657
- const result = await promise;
10658
- if (result.done)
10659
- return;
10660
- yield result.value;
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
- const cwd = mkdtempSync(join3(tmpdir(), "pipe-kan-acp-"));
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) => this.push({ type: "error", message: err.message }));
10692
- this.proc.on("exit", () => {
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
- await this.ready.promise;
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.name ?? "unknown",
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.name ?? "unknown",
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
- const resolver = this.eventResolvers.shift();
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
- while (this.eventResolvers.length) {
10783
- this.eventResolvers.shift()({ value: undefined, done: true });
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 = `You can call tools by emitting a single JSON code block matching this schema:
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
- const cfg = loadAgentConfig();
11088
- json2(res, 200, {
11089
- defaultAgent: cfg.defaultAgent,
11090
- defaultSkill: cfg.defaultSkill,
11091
- agents: Object.entries(cfg.agents).map(([id, c]) => ({
11092
- id,
11093
- command: [c.command, ...c.args ?? []].join(" "),
11094
- model: c.model,
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
- const cfg = loadAgentConfig();
11106
- const backendConfig = cfg.agents[cfg.defaultAgent];
11107
- if (!backendConfig) {
11108
- json2(res, 500, { error: `No agent config for ${cfg.defaultAgent}` });
11109
- return true;
11110
- }
11111
- const backend = createAcpBackend(cfg.defaultAgent, cfg.defaultAgent);
11112
- backend.spawn(backendConfig).then((session) => {
11113
- session.setToolParser((text) => {
11114
- const call = tools.parse(text);
11115
- return call ? { requestId: call.requestId, name: call.name, args: call.args } : undefined;
11116
- });
11117
- session.setToolExecutor((name) => !tools.isMutating(name), async (call) => {
11118
- const result = await tools.execute(call.name, call.args, app);
11119
- return {
11120
- text: result.ok ? `Result: ${JSON.stringify(result.value)}` : `Error: ${result.error}`,
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
- sessions.set(session.id, session);
11125
- json2(res, 200, { sessionId: session.id });
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
- (async () => {
11210
- for await (const event of session.events()) {
11211
- writeEvent(event);
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
- writeFileSync(path, [
11729
+ writeFileSync2(path, [
11344
11730
  "installation: Cloud",
11345
11731
  `server: ${server}`,
11346
11732
  `login: ${ME.emailAddress}`,