@agentshouse/kit 0.2.0 → 0.2.1-alpha.10

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.
Files changed (113) hide show
  1. package/README.md +20 -2
  2. package/dist/acp.js +120 -0
  3. package/dist/agents.js +96 -0
  4. package/dist/api.js +39 -3
  5. package/dist/authority-main.js +22 -0
  6. package/dist/blocks.js +51 -0
  7. package/dist/bootstrap.json +28 -0
  8. package/dist/bridge.js +214 -0
  9. package/dist/clis.js +114 -0
  10. package/dist/configure-main.js +6 -0
  11. package/dist/conversations.js +561 -0
  12. package/dist/copies.js +796 -0
  13. package/dist/enrol-main.js +4 -0
  14. package/dist/files.js +21 -0
  15. package/dist/git-main.js +153 -0
  16. package/dist/git.js +81 -0
  17. package/dist/home.js +19 -6
  18. package/dist/house-main.js +106 -3
  19. package/dist/instructions.js +33 -0
  20. package/dist/kit-main.js +2 -0
  21. package/dist/login.js +6 -2
  22. package/dist/operation.js +9 -0
  23. package/dist/resident.js +113 -2
  24. package/dist/secret-input.js +16 -0
  25. package/dist/sign-in.js +87 -0
  26. package/dist/skills.js +83 -0
  27. package/dist/stream.js +17 -1
  28. package/dist/translate.js +51 -0
  29. package/dist/update.js +116 -0
  30. package/how-we-work.md +25 -0
  31. package/package.json +29 -3
  32. package/skills/ask-matt/LICENSE +21 -0
  33. package/skills/ask-matt/PHASE-BOUNDARIES.md +55 -0
  34. package/skills/ask-matt/SKILL.md +91 -0
  35. package/skills/ask-matt/agents/openai.yaml +4 -0
  36. package/skills/code-review/LICENSE +21 -0
  37. package/skills/code-review/SKILL.md +89 -0
  38. package/skills/code-review/agents/openai.yaml +3 -0
  39. package/skills/codebase-design/DEEPENING.md +37 -0
  40. package/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
  41. package/skills/codebase-design/LICENSE +21 -0
  42. package/skills/codebase-design/SKILL.md +116 -0
  43. package/skills/codebase-design/agents/openai.yaml +3 -0
  44. package/skills/diagnosing-bugs/LICENSE +21 -0
  45. package/skills/diagnosing-bugs/SKILL.md +140 -0
  46. package/skills/diagnosing-bugs/agents/openai.yaml +3 -0
  47. package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +44 -0
  48. package/skills/domain-modeling/ADR-FORMAT.md +47 -0
  49. package/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  50. package/skills/domain-modeling/LICENSE +21 -0
  51. package/skills/domain-modeling/SKILL.md +76 -0
  52. package/skills/domain-modeling/agents/openai.yaml +3 -0
  53. package/skills/grill-me/LICENSE +21 -0
  54. package/skills/grill-me/SKILL.md +8 -0
  55. package/skills/grill-me/agents/openai.yaml +4 -0
  56. package/skills/grill-with-docs/LICENSE +21 -0
  57. package/skills/grill-with-docs/SKILL.md +8 -0
  58. package/skills/grill-with-docs/agents/openai.yaml +4 -0
  59. package/skills/grilling/LICENSE +21 -0
  60. package/skills/grilling/SKILL.md +30 -0
  61. package/skills/grilling/agents/openai.yaml +3 -0
  62. package/skills/handoff/LICENSE +21 -0
  63. package/skills/handoff/SKILL.md +17 -0
  64. package/skills/handoff/agents/openai.yaml +4 -0
  65. package/skills/implement/LICENSE +21 -0
  66. package/skills/implement/SKILL.md +16 -0
  67. package/skills/implement/agents/openai.yaml +4 -0
  68. package/skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
  69. package/skills/improve-codebase-architecture/LICENSE +21 -0
  70. package/skills/improve-codebase-architecture/SKILL.md +72 -0
  71. package/skills/improve-codebase-architecture/agents/openai.yaml +4 -0
  72. package/skills/prototype/LICENSE +21 -0
  73. package/skills/prototype/LOGIC.md +67 -0
  74. package/skills/prototype/SKILL.md +28 -0
  75. package/skills/prototype/UI.md +112 -0
  76. package/skills/prototype/agents/openai.yaml +3 -0
  77. package/skills/research/LICENSE +21 -0
  78. package/skills/research/SKILL.md +14 -0
  79. package/skills/research/agents/openai.yaml +3 -0
  80. package/skills/resolving-merge-conflicts/LICENSE +21 -0
  81. package/skills/resolving-merge-conflicts/SKILL.md +16 -0
  82. package/skills/resolving-merge-conflicts/agents/openai.yaml +3 -0
  83. package/skills/tdd/LICENSE +21 -0
  84. package/skills/tdd/SKILL.md +40 -0
  85. package/skills/tdd/agents/openai.yaml +3 -0
  86. package/skills/tdd/mocking.md +59 -0
  87. package/skills/tdd/tests.md +77 -0
  88. package/skills/teach/GLOSSARY-FORMAT.md +35 -0
  89. package/skills/teach/LEARNING-RECORD-FORMAT.md +46 -0
  90. package/skills/teach/LICENSE +21 -0
  91. package/skills/teach/MISSION-FORMAT.md +31 -0
  92. package/skills/teach/RESOURCES-FORMAT.md +32 -0
  93. package/skills/teach/SKILL.md +141 -0
  94. package/skills/teach/agents/openai.yaml +4 -0
  95. package/skills/to-questionnaire/LICENSE +21 -0
  96. package/skills/to-questionnaire/SKILL.md +55 -0
  97. package/skills/to-questionnaire/agents/openai.yaml +4 -0
  98. package/skills/to-spec/LICENSE +21 -0
  99. package/skills/to-spec/SKILL.md +76 -0
  100. package/skills/to-spec/agents/openai.yaml +4 -0
  101. package/skills/to-tickets/LICENSE +21 -0
  102. package/skills/to-tickets/SKILL.md +106 -0
  103. package/skills/to-tickets/agents/openai.yaml +4 -0
  104. package/skills/wait-what/LICENSE +21 -0
  105. package/skills/wait-what/SKILL.md +8 -0
  106. package/skills/wait-what/agents/openai.yaml +4 -0
  107. package/skills/wayfinder/LICENSE +21 -0
  108. package/skills/wayfinder/SKILL.md +129 -0
  109. package/skills/wayfinder/agents/openai.yaml +4 -0
  110. package/skills/writing-for-agents/LICENSE +21 -0
  111. package/skills/writing-for-agents/SKILL-MECHANICS.md +22 -0
  112. package/skills/writing-for-agents/SKILL.md +83 -0
  113. package/skills/writing-for-agents/agents/openai.yaml +3 -0
package/README.md CHANGED
@@ -7,7 +7,19 @@ through the agent CLIs you chose and gives each Agent the `house` command.
7
7
  ## Install
8
8
 
9
9
  Connect a computer from agents.house. It gives you the one command that
10
- installs House Kit on that computer and updates it when you run it again.
10
+ installs House Kit on that computer and updates it when you run it again. On
11
+ Linux, macOS and Windows 11 it runs House Kit in one Docker container with
12
+ your workspace, `~/AgentsHouse` unless you pass `--workspace <path>`, mounted
13
+ at `/agents/house`. Append a `kit` or `house` command to the same command to
14
+ run it in that container.
15
+
16
+ On your own Ubuntu 26.04 server, run the Linux command with `--linux` to
17
+ install House Kit natively, with its workspace at `/agents/house`; `kit` and
18
+ `house` are then on the server's `PATH`.
19
+
20
+ House Kit also gives every Agent on the Environment a set of agent skills. Add
21
+ `--no-skills` to the command to leave them out; running it again without the
22
+ flag brings them back.
11
23
 
12
24
  ## Commands
13
25
 
@@ -16,8 +28,14 @@ Environment's credential in `~/.house-kit`, or in `$HOUSE_KIT_HOME` when it is
16
28
  set. Pass `--manual` to paste the code by hand on a machine without a browser,
17
29
  and `--environment <id>` to reconnect an Environment you already have.
18
30
 
19
- `kit resident` keeps the Environment connected.
31
+ `kit resident` keeps the Environment connected, and keeps the Rooms you chose
32
+ for this Environment as Git repositories under `/agents/house/working-copies`.
33
+ Commit in one and run `house git push --owner` there to send the commit to
34
+ House; on a computer, run it through the install command from the copy's
35
+ directory. Inside an Agent conversation the Agent runs `house git push`.
20
36
 
21
37
  ## License
22
38
 
23
39
  MIT
40
+
41
+ The skills come from [mattpocock/skills](https://github.com/mattpocock/skills); thank you, Matt Pocock.
package/dist/acp.js ADDED
@@ -0,0 +1,120 @@
1
+ import { ndJsonStream, PROTOCOL_VERSION, } from '@agentclientprotocol/sdk';
2
+ import { spawn } from 'node:child_process';
3
+ import { readdirSync, readFileSync } from 'node:fs';
4
+ import { Readable, Writable } from 'node:stream';
5
+ import { CLIS, KIT_VERSION, cliCommand, withoutProxy } from './clis.js';
6
+ export const TURN_STARTED = 'kit/turn_started';
7
+ export const TURN_ENDED = 'kit/turn_ended';
8
+ export async function startAdapter(kind, cwd, app, jobs = () => undefined, added = {}) {
9
+ const child = spawn(cliCommand(kind), CLIS[kind].args, {
10
+ cwd,
11
+ env: { ...withoutProxy(), ...added },
12
+ stdio: ['pipe', 'pipe', 'pipe'],
13
+ detached: true,
14
+ });
15
+ let stderr = '';
16
+ child.stderr.on('data', (chunk) => {
17
+ stderr = (stderr + chunk.toString('utf8')).slice(-4000);
18
+ });
19
+ const exited = new Promise((resolve) => {
20
+ child.on('error', (error) => resolve(`the CLI did not start: ${error.message}`));
21
+ child.on('exit', (code, signal) => {
22
+ const last = stderr.trim().split('\n').at(-1);
23
+ resolve(`the CLI exited with ${code ?? signal}${last ? `: ${last}` : ''}`);
24
+ });
25
+ });
26
+ const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
27
+ const connection = app.connect({
28
+ writable: stream.writable,
29
+ readable: stream.readable.pipeThrough(new TransformStream({
30
+ transform(message, controller) {
31
+ const notice = message;
32
+ const job = CLIS[kind].job(notice);
33
+ if (job !== null)
34
+ jobs(job);
35
+ if (!notice.params?.update?.sessionUpdate?.startsWith('async_task_'))
36
+ controller.enqueue(message);
37
+ if (CLIS[kind].turnStarted(notice))
38
+ controller.enqueue({ jsonrpc: '2.0', method: TURN_STARTED });
39
+ if (CLIS[kind].turnEnded(notice))
40
+ controller.enqueue({ jsonrpc: '2.0', method: TURN_ENDED });
41
+ },
42
+ })),
43
+ });
44
+ void exited.then((cause) => connection.close(new Error(cause)));
45
+ try {
46
+ const initialized = await connection.agent.request('initialize', {
47
+ protocolVersion: PROTOCOL_VERSION,
48
+ clientCapabilities: {
49
+ fs: { readTextFile: false, writeTextFile: false },
50
+ terminal: false,
51
+ elicitation: { form: {} },
52
+ _meta: { jetbrains: { air: { version: 1, capabilities: ['asyncTasks'] } } },
53
+ },
54
+ clientInfo: { name: '@agentshouse/kit', version: KIT_VERSION },
55
+ });
56
+ return { child, connection, initialized, exited };
57
+ }
58
+ catch (error) {
59
+ child.kill('SIGKILL');
60
+ throw error;
61
+ }
62
+ }
63
+ function parents() {
64
+ const children = new Map();
65
+ for (const entry of readdirSync('/proc')) {
66
+ if (!/^\d+$/.test(entry))
67
+ continue;
68
+ try {
69
+ const stat = readFileSync(`/proc/${entry}/stat`, 'utf8');
70
+ const parent = Number(stat.slice(stat.lastIndexOf(')') + 2).split(' ')[1]);
71
+ children.set(parent, [...(children.get(parent) ?? []), Number(entry)]);
72
+ }
73
+ catch { }
74
+ }
75
+ return children;
76
+ }
77
+ function descendants(roots) {
78
+ const children = parents();
79
+ const tree = [];
80
+ const pending = roots.flatMap((root) => children.get(root) ?? []);
81
+ while (pending.length > 0) {
82
+ const pid = pending.pop();
83
+ tree.push(pid);
84
+ pending.push(...(children.get(pid) ?? []));
85
+ }
86
+ return tree;
87
+ }
88
+ function killAll(pids) {
89
+ for (const pid of pids) {
90
+ for (const target of [-pid, pid]) {
91
+ try {
92
+ process.kill(target, 'SIGKILL');
93
+ }
94
+ catch { }
95
+ }
96
+ }
97
+ }
98
+ function marked(marker) {
99
+ return readdirSync('/proc')
100
+ .filter((entry) => {
101
+ try {
102
+ return /^\d+$/.test(entry) && readFileSync(`/proc/${entry}/environ`, 'utf8').split('\0').includes(marker);
103
+ }
104
+ catch {
105
+ return false;
106
+ }
107
+ })
108
+ .map(Number);
109
+ }
110
+ export function killTree(child) {
111
+ killAll([child.pid, ...descendants([child.pid])]);
112
+ }
113
+ export function killMarked(marker) {
114
+ const pids = marked(marker);
115
+ killAll([...pids, ...descendants(pids)]);
116
+ }
117
+ export function killDescendants() {
118
+ const pids = marked(`HOUSE_KIT_RESIDENT=${process.pid}`);
119
+ killAll([...pids, ...descendants([process.pid, ...pids])]);
120
+ }
package/dist/agents.js ADDED
@@ -0,0 +1,96 @@
1
+ import { client } from '@agentclientprotocol/sdk';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { startAdapter } from './acp.js';
4
+ import { CLIS, KIT_VERSION, cliCommand, install, run } from './clis.js';
5
+ import { kitHome } from './home.js';
6
+ async function signedIn(kind) {
7
+ const probe = CLIS[kind].signedIn;
8
+ if ('command' in probe) {
9
+ return (await run(cliCommand(kind), probe.command, 30_000)).status === 0;
10
+ }
11
+ try {
12
+ const adapter = await startAdapter(kind, kitHome(), client({ name: '@agentshouse/kit' }));
13
+ adapter.child.kill('SIGKILL');
14
+ const method = adapter.initialized._meta?.[probe.initializeMeta];
15
+ return typeof method === 'string' && method.length > 0;
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
21
+ async function sshHostKey() {
22
+ const listed = await run('ssh-keygen', ['-l', '-E', 'sha256', '-f', '/etc/ssh/ssh_host_ed25519_key.pub'], 10_000);
23
+ return listed.status === 0 ? (/^\d+ (SHA256:\S+)/.exec(listed.output)?.[1] ?? null) : null;
24
+ }
25
+ async function operatingSystem() {
26
+ const release = await readFile('/etc/os-release', 'utf8');
27
+ return /^PRETTY_NAME="?([^"\n]*)"?$/m.exec(release)?.[1] ?? release;
28
+ }
29
+ export class Agents {
30
+ desired = { agents: [], routes: [] };
31
+ house;
32
+ reading = Promise.resolve();
33
+ installing = Promise.resolve();
34
+ releases = new Map();
35
+ reporting = null;
36
+ again = false;
37
+ constructor(house) {
38
+ this.house = house;
39
+ }
40
+ read() {
41
+ return this.reading.then(() => undefined, () => undefined);
42
+ }
43
+ installed() {
44
+ return this.installing.catch(() => undefined);
45
+ }
46
+ route(agentId) {
47
+ return this.desired.routes.find((route) => route.agent_id === agentId);
48
+ }
49
+ refresh() {
50
+ const reading = this.read().then(async () => {
51
+ this.desired = await this.house.deliver('/kit/agents/desired', {});
52
+ return this.desired;
53
+ });
54
+ this.reading = reading;
55
+ const installing = this.installed()
56
+ .then(() => reading)
57
+ .then(({ agents }) => this.install(agents));
58
+ this.installing = installing;
59
+ if (this.reporting === null)
60
+ this.reporting = this.reports();
61
+ else
62
+ this.again = true;
63
+ return Promise.all([installing, this.reporting]).then(() => undefined);
64
+ }
65
+ async report() {
66
+ const agents = [];
67
+ for (const [kind, release] of this.releases) {
68
+ agents.push({ kind, release, signed_in: release !== null && (await signedIn(kind)) });
69
+ }
70
+ const hostKey = await sshHostKey();
71
+ await this.house.deliver('/kit/agents/report', {
72
+ os: await operatingSystem(),
73
+ kit_version: KIT_VERSION,
74
+ agents,
75
+ ...(hostKey === null ? {} : { ssh_host_key: hostKey }),
76
+ });
77
+ }
78
+ async install(kinds) {
79
+ const releases = new Map();
80
+ for (const kind of kinds)
81
+ releases.set(kind, await install(kind));
82
+ this.releases = releases;
83
+ }
84
+ async reports() {
85
+ try {
86
+ do {
87
+ this.again = false;
88
+ await this.installing;
89
+ await this.report();
90
+ } while (this.again);
91
+ }
92
+ finally {
93
+ this.reporting = null;
94
+ }
95
+ }
96
+ }
package/dist/api.js CHANGED
@@ -1,11 +1,47 @@
1
- export async function post(house, path, body) {
1
+ import { readEnrolment } from './home.js';
2
+ export class HouseRefusal extends Error {
3
+ status;
4
+ text;
5
+ constructor(path, status, text) {
6
+ super(`House refused ${path} with ${status}: ${text}`);
7
+ this.status = status;
8
+ this.text = text;
9
+ }
10
+ }
11
+ export async function post(house, path, body, credential, signal) {
2
12
  const answer = await fetch(new URL(path, house), {
3
13
  method: 'POST',
4
- headers: { 'content-type': 'application/json' },
14
+ headers: {
15
+ 'content-type': 'application/json',
16
+ ...(credential === undefined ? {} : { authorization: `Bearer ${credential}` }),
17
+ },
5
18
  body: JSON.stringify(body),
19
+ signal,
6
20
  });
7
21
  const text = await answer.text();
8
22
  if (!answer.ok)
9
- throw new Error(`House refused ${path} with ${answer.status}: ${text}`);
23
+ throw new HouseRefusal(path, answer.status, text);
10
24
  return JSON.parse(text);
11
25
  }
26
+ async function enrolledPost(path, body, signal) {
27
+ const { house, credential } = (await readEnrolment());
28
+ return post(house, path, body, credential, signal);
29
+ }
30
+ export const house = {
31
+ post: enrolledPost,
32
+ deliver: async (path, body, current = () => true) => {
33
+ for (let attempt = 0;; attempt++) {
34
+ try {
35
+ return await enrolledPost(path, body);
36
+ }
37
+ catch (error) {
38
+ if (error instanceof HouseRefusal && error.status < 500)
39
+ throw error;
40
+ process.stderr.write(`kit: ${path} did not reach House: ${error.message}\n`);
41
+ await new Promise((resolve) => setTimeout(resolve, Math.min(30_000, 1000 * 2 ** attempt)));
42
+ if (!current())
43
+ throw error;
44
+ }
45
+ }
46
+ },
47
+ };
@@ -0,0 +1,22 @@
1
+ import { setGlobalProxyFromEnv } from 'node:http';
2
+ import { HouseRefusal, post } from './api.js';
3
+ import { readEnrolment } from './home.js';
4
+ function unanswered(error) {
5
+ return error instanceof HouseRefusal ? error.status >= 500 : error instanceof TypeError && error.message === 'fetch failed';
6
+ }
7
+ setGlobalProxyFromEnv();
8
+ try {
9
+ const { house, credential } = (await readEnrolment());
10
+ try {
11
+ await post(house, '/kit/agents/desired', {}, credential);
12
+ }
13
+ catch (error) {
14
+ if (!unanswered(error))
15
+ throw error;
16
+ process.stderr.write(`kit: House did not answer, so the stored credential stands: ${error.message}\n`);
17
+ }
18
+ }
19
+ catch (error) {
20
+ process.stderr.write(`kit: ${error.message}\n`);
21
+ process.exitCode = 1;
22
+ }
package/dist/blocks.js ADDED
@@ -0,0 +1,51 @@
1
+ const FENCE = /^\s*(`{3,}|~{3,})\s*(\S*)/;
2
+ export function blocksOf(text) {
3
+ const blocks = [];
4
+ let paragraph = [];
5
+ let fence = null;
6
+ const closeParagraph = () => {
7
+ const joined = paragraph.join('\n').trim();
8
+ if (joined.length > 0)
9
+ blocks.push({ type: 'paragraph', text: joined });
10
+ paragraph = [];
11
+ };
12
+ const closeFence = () => {
13
+ const joined = fence.lines.join('\n');
14
+ if (joined.length > 0)
15
+ blocks.push({ type: 'code', language: fence.language, text: joined });
16
+ fence = null;
17
+ };
18
+ for (const line of text.split('\n')) {
19
+ if (fence !== null) {
20
+ if (line.trim().startsWith(fence.marker))
21
+ closeFence();
22
+ else
23
+ fence.lines.push(line);
24
+ continue;
25
+ }
26
+ const opening = FENCE.exec(line);
27
+ if (opening !== null) {
28
+ closeParagraph();
29
+ fence = { marker: opening[1], language: opening[2] || null, lines: [] };
30
+ }
31
+ else if (line.trim().length === 0) {
32
+ closeParagraph();
33
+ }
34
+ else {
35
+ paragraph.push(line);
36
+ }
37
+ }
38
+ if (fence !== null)
39
+ closeFence();
40
+ closeParagraph();
41
+ return blocks;
42
+ }
43
+ export function firstChange(before, after) {
44
+ let index = 0;
45
+ while (index < before.length &&
46
+ index < after.length &&
47
+ JSON.stringify(before[index]) === JSON.stringify(after[index])) {
48
+ index++;
49
+ }
50
+ return index;
51
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "version": "0.2.1-alpha.10",
4
+ "hosts": {
5
+ "linux": {
6
+ "command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.2.1-alpha.10/connect-linux.sh)",
7
+ "asset": {
8
+ "name": "connect-linux.sh",
9
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-alpha.10/connect-linux.sh"
10
+ },
11
+ "workspace": {
12
+ "argument": "--workspace",
13
+ "quoting": "posix"
14
+ }
15
+ },
16
+ "windows": {
17
+ "command": "& ([scriptblock]::Create((irm https://github.com/agentshouse/kit/releases/download/v0.2.1-alpha.10/connect-windows.ps1)))",
18
+ "asset": {
19
+ "name": "connect-windows.ps1",
20
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-alpha.10/connect-windows.ps1"
21
+ },
22
+ "workspace": {
23
+ "argument": "--workspace",
24
+ "quoting": "powershell"
25
+ }
26
+ }
27
+ }
28
+ }
package/dist/bridge.js ADDED
@@ -0,0 +1,214 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { once } from 'node:events';
3
+ import { mkdir } from 'node:fs/promises';
4
+ import { createServer, request as httpRequest } from 'node:http';
5
+ import { request as httpsRequest } from 'node:https';
6
+ import { delimiter, join } from 'node:path';
7
+ import { parse } from 'yaml';
8
+ import { KIT_VERSION } from './clis.js';
9
+ import { changedPaths, refusalCode, } from './copies.js';
10
+ import { kitHome, readEnrolment } from './home.js';
11
+ import { operationId } from './operation.js';
12
+ const MCP_PROTOCOL_VERSION = '2026-07-28';
13
+ const MCP_REQUEST_BYTES = 4 * 1024 * 1024;
14
+ const FORWARDED = new Set(['/', '/kit/attachments/upload']);
15
+ export async function received(request) {
16
+ let text = '';
17
+ request.setEncoding('utf8');
18
+ for await (const chunk of request)
19
+ text += chunk;
20
+ return text;
21
+ }
22
+ function relay(request, response, target, authorization) {
23
+ const send = target.protocol === 'https:' ? httpsRequest : httpRequest;
24
+ const forwarded = send(target, {
25
+ method: request.method,
26
+ headers: { ...request.headers, host: target.host, ...(authorization === undefined ? {} : { authorization }) },
27
+ }, (answer) => {
28
+ response.writeHead(answer.statusCode, answer.headers);
29
+ answer.pipe(response);
30
+ });
31
+ forwarded.on('error', () => response.destroy());
32
+ request.pipe(forwarded);
33
+ }
34
+ function answered(forwarded) {
35
+ if (forwarded.status >= 500)
36
+ throw new Error(`House answered ${forwarded.status}`);
37
+ if (forwarded.status !== 200)
38
+ return { refused: { code: refusalCode(forwarded.text), text: forwarded.text } };
39
+ const answer = JSON.parse(forwarded.text);
40
+ if (answer.error !== undefined)
41
+ return { refused: { code: refusalCode(answer.error.message), text: answer.error.message } };
42
+ const text = answer.result.content.map((content) => content.text).join('\n');
43
+ return answer.result.isError === true ? { refused: { code: refusalCode(text), text } } : { text };
44
+ }
45
+ function editAnswer(forwarded) {
46
+ const answer = answered(forwarded);
47
+ if ('refused' in answer)
48
+ return answer;
49
+ const { results } = parse(answer.text);
50
+ return {
51
+ accepted: results.flatMap((result) => [
52
+ ...(result.from === undefined ? [] : [{ path: result.from, revision: null, content: null }]),
53
+ { path: result.path, revision: result.op === 'remove' ? null : result.revision },
54
+ ]),
55
+ };
56
+ }
57
+ export async function openBridge(house, copies, conversationId, signal) {
58
+ const origin = (await readEnrolment()).house;
59
+ const { credential } = await house.post(`/kit/conversations/${conversationId}/credential`, {}, signal);
60
+ const closed = new AbortController();
61
+ const body = (message, operation) => JSON.stringify({
62
+ ...message,
63
+ params: {
64
+ ...message.params,
65
+ _meta: {
66
+ 'io.modelcontextprotocol/protocolVersion': MCP_PROTOCOL_VERSION,
67
+ 'io.modelcontextprotocol/clientInfo': { name: '@agentshouse/kit', version: KIT_VERSION },
68
+ 'io.modelcontextprotocol/clientCapabilities': {},
69
+ ...(operation === null ? {} : { 'agents.house/agent-operation': operation }),
70
+ },
71
+ },
72
+ });
73
+ const send = async (message, operation) => {
74
+ const tool = message.method === 'tools/call' ? String(message.params?.name) : null;
75
+ const answer = await fetch(new URL('/', origin), {
76
+ method: 'POST',
77
+ headers: {
78
+ 'content-type': 'application/json',
79
+ authorization: `Bearer ${credential}`,
80
+ accept: 'application/json, text/event-stream',
81
+ 'mcp-protocol-version': MCP_PROTOCOL_VERSION,
82
+ 'mcp-method': message.method,
83
+ ...(tool === null ? {} : { 'mcp-name': tool }),
84
+ },
85
+ body: body(message, operation),
86
+ signal: closed.signal,
87
+ });
88
+ return { status: answer.status, text: await answer.text() };
89
+ };
90
+ const call = (name, args) => ({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name, arguments: args } });
91
+ const staged = async (operation, changes) => {
92
+ const bytes = Buffer.from(JSON.stringify({ changes }));
93
+ const sha256 = createHash('sha256').update(bytes).digest('hex');
94
+ const prepared = answered(await send(call('run_command', {
95
+ command: 'prepare_edit_upload',
96
+ arguments: { paths: changedPaths(changes), bytes: bytes.length, sha256 },
97
+ }), operation));
98
+ if ('refused' in prepared)
99
+ return prepared;
100
+ const { transfer } = parse(prepared.text);
101
+ const posted = await fetch(transfer.url, {
102
+ method: 'POST',
103
+ headers: { 'content-type': 'text/plain; charset=utf-8' },
104
+ body: Buffer.concat([Buffer.from(`${transfer.operation}\n`), bytes]),
105
+ signal: closed.signal,
106
+ });
107
+ const text = await posted.text();
108
+ if (posted.status >= 500)
109
+ throw new Error(`House answered ${posted.status}`);
110
+ if (!posted.ok)
111
+ return { refused: { code: refusalCode(text), text } };
112
+ const held = JSON.parse(text).transfer;
113
+ return { url: held.url, operation: held.operation, bytes: bytes.length, sha256 };
114
+ };
115
+ const forward = async (path, text) => {
116
+ if (path === '/') {
117
+ const message = JSON.parse(text);
118
+ const tool = message.method === 'tools/call' ? String(message.params?.name) : null;
119
+ const writes = tool !== null &&
120
+ (await house.post('/kit/tools/mutations', {}, closed.signal)).tools.some((listed) => listed.name === tool && listed.writes);
121
+ return send(message, writes ? operationId() : null);
122
+ }
123
+ const answer = await fetch(new URL(path, origin), {
124
+ method: 'POST',
125
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${credential}` },
126
+ body: text,
127
+ signal: closed.signal,
128
+ });
129
+ return { status: answer.status, text: await answer.text() };
130
+ };
131
+ const caller = {
132
+ key: `conversation:${conversationId}`,
133
+ submit: async (operation, changes, held, retain) => {
134
+ const inline = call('edit', { changes });
135
+ let upload = held;
136
+ if (upload === undefined && Buffer.byteLength(body(inline, operation)) > MCP_REQUEST_BYTES) {
137
+ const prepared = await staged(operation, changes);
138
+ if ('refused' in prepared)
139
+ return prepared;
140
+ upload = prepared;
141
+ await retain(upload);
142
+ }
143
+ return editAnswer(await send(upload === undefined ? inline : call('edit', { paths: changedPaths(changes), upload }), operation));
144
+ },
145
+ };
146
+ const local = async (path, text) => {
147
+ if (path === '/git/push') {
148
+ const { cwd, commit } = JSON.parse(text);
149
+ return { status: 200, text: JSON.stringify(await copies.push(cwd, commit, caller)) };
150
+ }
151
+ if (path === '/git/observed') {
152
+ await copies.observed(JSON.parse(text));
153
+ return { status: 200, text: '{}' };
154
+ }
155
+ return FORWARDED.has(path) ? forward(path, text) : { status: 404, text: '' };
156
+ };
157
+ const sockets = join(kitHome(), 'bridges');
158
+ await mkdir(sockets, { recursive: true, mode: 0o700 });
159
+ const socketPath = join(sockets, `${randomUUID()}.sock`);
160
+ const bridge = createServer(async (request, response) => {
161
+ try {
162
+ const path = request.url ?? '';
163
+ if (URL.canParse(path)) {
164
+ relay(request, response, new URL(path));
165
+ return;
166
+ }
167
+ const answered = await local(path, await received(request));
168
+ response.writeHead(answered.status, { 'content-type': 'application/json' }).end(answered.text);
169
+ }
170
+ catch (error) {
171
+ response.writeHead(502).end(error instanceof Error ? error.message : String(error));
172
+ }
173
+ });
174
+ bridge.listen(socketPath);
175
+ await once(bridge, 'listening');
176
+ const app = new URL('/app/', origin).href;
177
+ const authorization = `Basic ${Buffer.from(`house:${credential}`).toString('base64')}`;
178
+ const git = createServer((request, response) => {
179
+ const target = new URL(request.url ?? '', app);
180
+ if (!target.href.startsWith(app)) {
181
+ response.writeHead(404).end();
182
+ return;
183
+ }
184
+ relay(request, response, target, authorization);
185
+ });
186
+ git.listen(0, '127.0.0.1');
187
+ await once(git, 'listening');
188
+ const proxy = `http://127.0.0.1:${git.address().port}`;
189
+ return {
190
+ env: {
191
+ HOUSE_BRIDGE: socketPath,
192
+ PATH: `${join(kitHome(), 'shim')}${delimiter}${process.env.PATH ?? ''}`,
193
+ GIT_CONFIG_COUNT: '1',
194
+ GIT_CONFIG_KEY_0: `url.${proxy}/app/.insteadOf`,
195
+ GIT_CONFIG_VALUE_0: app,
196
+ },
197
+ tool: async (name, args) => {
198
+ const forwarded = await forward('/', JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name, arguments: args } }));
199
+ if (forwarded.status !== 200)
200
+ throw new Error(`House refused ${name} with ${forwarded.status}: ${forwarded.text}`);
201
+ const answer = JSON.parse(forwarded.text);
202
+ if (answer.error !== undefined)
203
+ throw new Error(`House refused ${name}: ${answer.error.message}`);
204
+ return answer.result;
205
+ },
206
+ close: () => {
207
+ closed.abort();
208
+ bridge.close();
209
+ bridge.closeAllConnections();
210
+ git.close();
211
+ git.closeAllConnections();
212
+ },
213
+ };
214
+ }