@agentshouse/kit 0.2.1-alpha.4 → 0.2.1-alpha.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/acp.js +3 -3
- package/dist/api.js +2 -0
- package/dist/bridge.js +133 -43
- package/dist/clis.js +5 -1
- package/dist/conversations.js +6 -3
- package/dist/copies.js +796 -0
- package/dist/git-main.js +153 -0
- package/dist/git.js +81 -0
- package/dist/house-main.js +30 -15
- package/dist/instructions.js +4 -2
- package/dist/kit-main.js +2 -0
- package/dist/operation.js +9 -0
- package/dist/resident.js +55 -7
- package/dist/sign-in.js +6 -2
- package/dist/translate.js +49 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -16,7 +16,10 @@ Environment's credential in `~/.house-kit`, or in `$HOUSE_KIT_HOME` when it is
|
|
|
16
16
|
set. Pass `--manual` to paste the code by hand on a machine without a browser,
|
|
17
17
|
and `--environment <id>` to reconnect an Environment you already have.
|
|
18
18
|
|
|
19
|
-
`kit resident` keeps the Environment connected
|
|
19
|
+
`kit resident` keeps the Environment connected, and keeps the Rooms you chose
|
|
20
|
+
for this Environment as Git repositories under `/agents/house/working-copies`.
|
|
21
|
+
Commit in one and run `house git push --owner` there to send the commit to
|
|
22
|
+
House; inside an Agent conversation the Agent runs `house git push`.
|
|
20
23
|
|
|
21
24
|
## License
|
|
22
25
|
|
package/dist/acp.js
CHANGED
|
@@ -2,13 +2,13 @@ import { ndJsonStream, PROTOCOL_VERSION, } from '@agentclientprotocol/sdk';
|
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
3
|
import { readdirSync, readFileSync } from 'node:fs';
|
|
4
4
|
import { Readable, Writable } from 'node:stream';
|
|
5
|
-
import { CLIS, KIT_VERSION, cliCommand } from './clis.js';
|
|
5
|
+
import { CLIS, KIT_VERSION, cliCommand, withoutProxy } from './clis.js';
|
|
6
6
|
export const TURN_STARTED = 'kit/turn_started';
|
|
7
7
|
export const TURN_ENDED = 'kit/turn_ended';
|
|
8
|
-
export async function startAdapter(kind, cwd, app, jobs = () => undefined,
|
|
8
|
+
export async function startAdapter(kind, cwd, app, jobs = () => undefined, added = {}) {
|
|
9
9
|
const child = spawn(cliCommand(kind), CLIS[kind].args, {
|
|
10
10
|
cwd,
|
|
11
|
-
env,
|
|
11
|
+
env: { ...withoutProxy(), ...added },
|
|
12
12
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
13
13
|
detached: true,
|
|
14
14
|
});
|
package/dist/api.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { readEnrolment } from './home.js';
|
|
2
2
|
export class HouseRefusal extends Error {
|
|
3
3
|
status;
|
|
4
|
+
text;
|
|
4
5
|
constructor(path, status, text) {
|
|
5
6
|
super(`House refused ${path} with ${status}: ${text}`);
|
|
6
7
|
this.status = status;
|
|
8
|
+
this.text = text;
|
|
7
9
|
}
|
|
8
10
|
}
|
|
9
11
|
export async function post(house, path, body, credential, signal) {
|
package/dist/bridge.js
CHANGED
|
@@ -1,76 +1,171 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import { once } from 'node:events';
|
|
3
3
|
import { mkdir } from 'node:fs/promises';
|
|
4
4
|
import { createServer, request as httpRequest } from 'node:http';
|
|
5
5
|
import { request as httpsRequest } from 'node:https';
|
|
6
|
-
import { join } from 'node:path';
|
|
6
|
+
import { delimiter, join } from 'node:path';
|
|
7
|
+
import { parse } from 'yaml';
|
|
7
8
|
import { KIT_VERSION } from './clis.js';
|
|
9
|
+
import { changedPaths, refusalCode, } from './copies.js';
|
|
8
10
|
import { kitHome, readEnrolment } from './home.js';
|
|
11
|
+
import { operationId } from './operation.js';
|
|
9
12
|
const MCP_PROTOCOL_VERSION = '2026-07-28';
|
|
13
|
+
const MCP_REQUEST_BYTES = 4 * 1024 * 1024;
|
|
10
14
|
const FORWARDED = new Set(['/', '/kit/attachments/upload']);
|
|
11
|
-
function
|
|
12
|
-
const bytes = randomBytes(16);
|
|
13
|
-
bytes.writeUIntBE(Date.now(), 0, 6);
|
|
14
|
-
bytes[6] = 0x70 | (bytes[6] & 0x0f);
|
|
15
|
-
bytes[8] = 0x80 | (bytes[8] & 0x3f);
|
|
16
|
-
const hex = bytes.toString('hex');
|
|
17
|
-
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
18
|
-
}
|
|
19
|
-
async function received(request) {
|
|
15
|
+
export async function received(request) {
|
|
20
16
|
let text = '';
|
|
21
17
|
request.setEncoding('utf8');
|
|
22
18
|
for await (const chunk of request)
|
|
23
19
|
text += chunk;
|
|
24
20
|
return text;
|
|
25
21
|
}
|
|
26
|
-
|
|
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) {
|
|
27
58
|
const origin = (await readEnrolment()).house;
|
|
28
59
|
const { credential } = await house.post(`/kit/conversations/${conversationId}/credential`, {}, signal);
|
|
29
60
|
const closed = new AbortController();
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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',
|
|
36
77
|
headers: {
|
|
78
|
+
'content-type': 'application/json',
|
|
79
|
+
authorization: `Bearer ${credential}`,
|
|
37
80
|
accept: 'application/json, text/event-stream',
|
|
38
81
|
'mcp-protocol-version': MCP_PROTOCOL_VERSION,
|
|
39
82
|
'mcp-method': message.method,
|
|
40
83
|
...(tool === null ? {} : { 'mcp-name': tool }),
|
|
41
84
|
},
|
|
42
|
-
body:
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
},
|
|
54
|
-
};
|
|
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 };
|
|
55
114
|
};
|
|
56
115
|
const forward = async (path, text) => {
|
|
57
|
-
|
|
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
|
+
}
|
|
58
123
|
const answer = await fetch(new URL(path, origin), {
|
|
59
124
|
method: 'POST',
|
|
60
|
-
headers: { 'content-type': 'application/json', authorization: `Bearer ${credential}
|
|
61
|
-
body:
|
|
125
|
+
headers: { 'content-type': 'application/json', authorization: `Bearer ${credential}` },
|
|
126
|
+
body: text,
|
|
62
127
|
signal: closed.signal,
|
|
63
128
|
});
|
|
64
129
|
return { status: answer.status, text: await answer.text() };
|
|
65
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
|
+
};
|
|
66
157
|
const sockets = join(kitHome(), 'bridges');
|
|
67
158
|
await mkdir(sockets, { recursive: true, mode: 0o700 });
|
|
68
159
|
const socketPath = join(sockets, `${randomUUID()}.sock`);
|
|
69
160
|
const bridge = createServer(async (request, response) => {
|
|
70
161
|
try {
|
|
71
162
|
const path = request.url ?? '';
|
|
72
|
-
|
|
73
|
-
|
|
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);
|
|
74
169
|
}
|
|
75
170
|
catch (error) {
|
|
76
171
|
response.writeHead(502).end(error instanceof Error ? error.message : String(error));
|
|
@@ -86,13 +181,7 @@ export async function openBridge(house, conversationId, signal) {
|
|
|
86
181
|
response.writeHead(404).end();
|
|
87
182
|
return;
|
|
88
183
|
}
|
|
89
|
-
|
|
90
|
-
const forwarded = send(target, { method: request.method, headers: { ...request.headers, host: target.host, authorization } }, (answer) => {
|
|
91
|
-
response.writeHead(answer.statusCode, answer.headers);
|
|
92
|
-
answer.pipe(response);
|
|
93
|
-
});
|
|
94
|
-
forwarded.on('error', () => response.destroy());
|
|
95
|
-
request.pipe(forwarded);
|
|
184
|
+
relay(request, response, target, authorization);
|
|
96
185
|
});
|
|
97
186
|
git.listen(0, '127.0.0.1');
|
|
98
187
|
await once(git, 'listening');
|
|
@@ -100,6 +189,7 @@ export async function openBridge(house, conversationId, signal) {
|
|
|
100
189
|
return {
|
|
101
190
|
env: {
|
|
102
191
|
HOUSE_BRIDGE: socketPath,
|
|
192
|
+
PATH: `${join(kitHome(), 'shim')}${delimiter}${process.env.PATH ?? ''}`,
|
|
103
193
|
GIT_CONFIG_COUNT: '1',
|
|
104
194
|
GIT_CONFIG_KEY_0: `url.${proxy}/app/.insteadOf`,
|
|
105
195
|
GIT_CONFIG_VALUE_0: app,
|
package/dist/clis.js
CHANGED
|
@@ -65,6 +65,7 @@ export const CLIS = {
|
|
|
65
65
|
};
|
|
66
66
|
const KIT_PACKAGE = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
67
67
|
export const KIT_VERSION = KIT_PACKAGE.version;
|
|
68
|
+
const PROXY = /^https?_proxy$/i;
|
|
68
69
|
export function pinned(kind) {
|
|
69
70
|
return KIT_PACKAGE.peerDependencies[CLIS[kind].package];
|
|
70
71
|
}
|
|
@@ -83,9 +84,12 @@ export async function installedRelease(kind) {
|
|
|
83
84
|
return null;
|
|
84
85
|
}
|
|
85
86
|
}
|
|
87
|
+
export function withoutProxy() {
|
|
88
|
+
return Object.fromEntries(Object.entries(process.env).filter(([name]) => !PROXY.test(name)));
|
|
89
|
+
}
|
|
86
90
|
export function run(command, args, timeoutMs) {
|
|
87
91
|
return new Promise((resolve) => {
|
|
88
|
-
const child = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'], timeout: timeoutMs });
|
|
92
|
+
const child = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'], timeout: timeoutMs, env: withoutProxy() });
|
|
89
93
|
let output = '';
|
|
90
94
|
child.stdout.on('data', (chunk) => {
|
|
91
95
|
output += chunk.toString('utf8');
|
package/dist/conversations.js
CHANGED
|
@@ -174,7 +174,10 @@ export class Conversations {
|
|
|
174
174
|
ack = { provider_session_id: opened };
|
|
175
175
|
}
|
|
176
176
|
if (input.first === true) {
|
|
177
|
-
prompt.push({
|
|
177
|
+
prompt.push({
|
|
178
|
+
type: 'text',
|
|
179
|
+
text: await instructions(conversation.running.bridge, route.base_instructions, await this.kit.copies.mapping()),
|
|
180
|
+
});
|
|
178
181
|
}
|
|
179
182
|
prompt.push({ type: 'text', text: String(input.text) });
|
|
180
183
|
if (paths.length > 0)
|
|
@@ -247,14 +250,14 @@ export class Conversations {
|
|
|
247
250
|
.onRequest('session/request_permission', ({ params, signal }) => this.ask(conversation, 'session/request_permission', params, false, signal))
|
|
248
251
|
.onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal));
|
|
249
252
|
await unlessKilled(this.kit.agents.installed(), conversation.killed.signal);
|
|
250
|
-
const bridge = await openBridge(this.kit.house, conversation.id, conversation.killed.signal);
|
|
253
|
+
const bridge = await openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal);
|
|
251
254
|
if (conversation.kills > 0) {
|
|
252
255
|
bridge.close();
|
|
253
256
|
throw new Error(KILLED);
|
|
254
257
|
}
|
|
255
258
|
conversation.opening = bridge;
|
|
256
259
|
try {
|
|
257
|
-
const adapter = await startAdapter(route.kind, route.working_directory, app, (job) => this.job(conversation, job),
|
|
260
|
+
const adapter = await startAdapter(route.kind, route.working_directory, app, (job) => this.job(conversation, job), bridge.env);
|
|
258
261
|
void adapter.exited.then(() => bridge.close());
|
|
259
262
|
const agent = adapter.connection.agent;
|
|
260
263
|
const cwd = route.working_directory;
|